Merge branch 'IfcOpenShell:v0.8.0' into fix-5563

This commit is contained in:
sboddy
2025-06-24 15:26:48 +01:00
committed by GitHub
35 changed files with 516 additions and 192 deletions
+17 -1
View File
@@ -20,6 +20,7 @@ import bpy
import bonsai.tool as tool
from bpy.types import PropertyGroup
from bonsai.bim.module.bsdd.data import BSDDData
from bonsai.bim.module.classification.data import ClassificationsData
from bonsai.bim.prop import Attribute, StrProperty
from bpy.props import (
PointerProperty,
@@ -42,6 +43,21 @@ def get_active_dictionary(self, context):
def update_is_active(self: "BSDDDictionary", context: bpy.types.Context) -> None:
BSDDData.data["active_dictionary"] = BSDDData.active_dictionary()
if ClassificationsData.is_loaded:
props = tool.Classification.get_classification_props()
# Preserve original enum value.
classification_source = props.classification_source
ClassificationsData.data["classification_source"] = ClassificationsData.classification_source()
# Try to restore enum value.
if "classification_source" not in props:
# It's already on the default value, nothing to restore.
return
try:
props.classification_source = classification_source
except TypeError:
# Item is no longer active and not present in enum, fallback to the default.
del props["classification_source"]
def update_is_selected(self: "BSDDProperty", context: bpy.types.Context) -> None:
@@ -111,7 +127,7 @@ class BSDDPset(PropertyGroup):
class BIMBSDDProperties(PropertyGroup):
active_dictionary: StringProperty(name="Active Dictionary")
# TODO: `active_dictionary` is not used anywhere?
active_dictionary: EnumProperty(items=get_active_dictionary, name="Active Dictionary")
active_uri: StringProperty(name="Active URI")
dictionaries: CollectionProperty(name="Dictionaries", type=BSDDDictionary)
+11 -6
View File
@@ -285,7 +285,7 @@ class DecoratorData:
return thickness
@classmethod
def get_section_markers_display_data(cls, obj):
def get_section_markers_display_data(cls, obj: bpy.types.Object) -> Union[dict[str, Any], None]:
"""used by IfcAnnotations with ObjectType = "SECTION" """
result = cls.data.get(obj.name, None)
if result is not None:
@@ -327,10 +327,11 @@ class DecoratorData:
return display_data
@classmethod
def get_text_data(cls, obj: bpy.types.Object) -> dict:
def get_text_data(cls, obj: bpy.types.Object) -> dict[str, Any]:
"""used by Ifc Annotations with ObjectType = "TEXT" / "TEXT_LEADER"\n
returns font size in mm for current ifc text object"""
element = tool.Ifc.get_entity(obj)
assert element
props = tool.Drawing.get_text_props(obj)
# getting font size
pset_data = ifcopenshell.util.element.get_pset(element, "EPset_Annotation") or {}
@@ -371,16 +372,18 @@ class DecoratorData:
return {"Literals": literals_data, "FontSize": font_size, "Symbol": symbol, "Newline_At": newline_at}
@classmethod
def get_dimension_data(cls, obj):
def get_dimension_data(cls, obj: bpy.types.Object) -> dict[str, Any]:
"""used by Ifc Annotations with ObjectType:
DIMENSION / DIAMETER / SECTION_LEVEL / PLAN_LEVEL / RADIUS
"""
element = tool.Ifc.get_entity(obj)
assert element
dimension_style = "arrow"
fill_bg = False
classes = ifcopenshell.util.element.get_pset(element, "EPset_Annotation", "Classes")
if classes:
assert type(classes) is str
classes_split = classes.lower().split()
if "oblique" in classes_split:
dimension_style = "oblique"
@@ -406,15 +409,17 @@ class DecoratorData:
}
@classmethod
def get_fall_data(cls, obj):
def get_fall_data(cls, obj: bpy.types.Object) -> dict[str, Union[str, None]]:
object_type = None
if element := tool.Ifc.get_entity(obj):
object_type = ifcopenshell.util.element.get_predefined_type(element)
return {"object_type": object_type}
@classmethod
def get_symbol_data(cls, obj):
return tool.Drawing.get_annotation_symbol(tool.Ifc.get_entity(obj))
def get_symbol_data(cls, obj: bpy.types.Object) -> Union[str, None]:
element = tool.Ifc.get_entity(obj)
assert element
return tool.Drawing.get_annotation_symbol(element)
@classmethod
def object_decorators(cls, handler):
@@ -66,7 +66,7 @@ class SheetBuilder:
view = ET.SubElement(root, "g")
view.attrib["data-type"] = "titleblock"
titleblock = ET.SubElement(view, "image")
titleblock.attrib["xlink:href"] = os.path.relpath(titleblock_path, sheet_dir)
titleblock.attrib["xlink:href"] = Path(os.path.relpath(titleblock_path, sheet_dir)).as_posix()
titleblock.attrib["x"] = "0"
titleblock.attrib["y"] = "0"
titleblock.attrib["width"] = str(view_width)
@@ -20,6 +20,7 @@ import bpy
from . import ui, prop, operator
from bpy.app.handlers import persistent
import ifcopenshell.util.element
import math
classes = (
operator.AddCurvelikeItem,
@@ -76,6 +77,7 @@ classes = (
operator.UpdateItemAttributes,
operator.UpdateParametricRepresentation,
operator.UpdateRepresentation,
operator.CreateInstance,
prop.RepresentationItem,
prop.RepresentationItemObject,
prop.ShapeAspect,
@@ -108,6 +110,7 @@ def block_scale(scene: bpy.types.Scene) -> None:
camera = tool.Ifc.get_entity(obj)
if ifcopenshell.util.element.get_pset(camera, "EPset_Drawing", "TargetView") == "REFLECTED_PLAN_VIEW":
obj.scale = (-1, -1, -1)
obj.rotation_euler = (0.0, 0.0, math.radians(180))
else:
if obj.scale != (1, 1, 1):
obj.scale = (1, 1, 1)
@@ -46,6 +46,7 @@ import bonsai.core.root
import bonsai.core.drawing
import bonsai.tool as tool
import bonsai.bim.handler
from bonsai.bim.module.model.data import AuthoringData
from mathutils import Vector, Matrix, Quaternion
from time import time
from bonsai.bim.ifc import IfcStore
@@ -3432,3 +3433,38 @@ class UnassignRepresentationLayer(bpy.types.Operator, tool.Ifc.Operator):
layer = ifc_file.by_id(self.layer_id)
ifcopenshell.api.layer.unassign_layer(ifc_file, [representation], layer)
return {"FINISHED"}
class CreateInstance(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.create_instance"
bl_label = "IFC Create Instance"
bl_description = "Create an instance of the type associated with the selected object"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
if not context.selected_objects or len(context.selected_objects) > 1:
self.report({"ERROR"}, "Select exactly one object to create an instance of its type")
return {"CANCELLED"}
active_obj = context.active_object
element = tool.Ifc.get_entity(active_obj)
if not element:
self.report({"ERROR"}, "Selected object is not an IFC element")
return {"CANCELLED"}
relating_type = ifcopenshell.util.element.get_type(element)
if not relating_type:
self.report({"ERROR"}, "Selected object has no associated type")
return {"CANCELLED"}
try:
props = tool.Model.get_model_props()
props.ifc_class = relating_type.is_a()
props.relating_type_id = str(relating_type.id())
except:
self.report({"ERROR"}, "You must be using the Multiobject Tool or the relevant editing tool")
return {"CANCELLED"}
bpy.ops.bim.hotkey(hotkey="S_A")
return {"FINISHED"}
@@ -66,6 +66,12 @@ def object_menu(self, context):
self.layout.menu("BIM_MT_object_set_origin", icon="PLUGIN")
self.layout.menu("BIM_MT_separate", icon="PLUGIN")
# only show the create instance operator if the current tool is the BIM tool
# if context.space_data and hasattr(context.space_data, "show_object_viewport_mesh"):
# current_tool = context.workspace.tools.from_space_view3d_mode(context.mode, create=False)
# if current_tool and current_tool.idname == "bim.bim_tool":
self.layout.operator("bim.create_instance", icon="PLUGIN")
def edit_mesh_menu(self, context):
self.layout.separator()
@@ -38,6 +38,8 @@ classes = (
operator.MoveSunPathTo3DCursor,
operator.RadianceRender,
operator.ViewFromSun,
operator.LightPickCoordinates,
operator.LightSetTimeToNow,
operator.RefreshIFCMaterials,
operator.UnmapMaterial,
operator.RADIANCE_OT_select_camera,
@@ -54,7 +56,7 @@ classes = (
def register():
bpy.types.Scene.radiance_exporter_properties = bpy.props.PointerProperty(type=prop.RadianceExporterProperties)
bpy.types.Scene.BIMRadianceExporeterProperies = bpy.props.PointerProperty(type=prop.RadianceExporterProperties)
bpy.types.Scene.BIMSolarProperties = bpy.props.PointerProperty(type=prop.BIMSolarProperties)
pyradiance_path = Path(get_pyradiance_path())
bin_path = pyradiance_path / "bin"
@@ -65,5 +67,5 @@ def register():
def unregister():
del bpy.types.Scene.radiance_exporter_properties
del bpy.types.Scene.BIMRadianceExporeterProperies
del bpy.types.Scene.BIMSolarProperties
+20 -18
View File
@@ -22,7 +22,7 @@ import gpu
import bmesh
import bonsai.tool as tool
from bpy.types import SpaceView3D
from math import radians
from math import radians, degrees
from mathutils import Vector, Matrix
from gpu_extras.batch import batch_for_shader
from bpy_extras.view3d_utils import location_3d_to_region_2d
@@ -59,10 +59,10 @@ class SolarDecorator:
shader.uniform_float("color", color)
batch.draw(shader)
def draw_text(self, context):
def draw_text(self, context: bpy.types.Context) -> None:
self.addon_prefs = tool.Blender.get_addon_preferences()
props = bpy.context.scene.BIMSolarProperties
props = tool.Blender.get_solar_props()
origin = Matrix.Translation(props.sun_path_origin)
location = origin @ (props.sun_position * 1.05)
@@ -72,11 +72,12 @@ class SolarDecorator:
self.draw_text_at_position(context, f"{props.hour:02}:{props.minute:02}", location)
self.tn_angle = props.true_north
angle = Matrix.Rotation(radians(self.tn_angle), 4, "Z")
angle = Matrix.Rotation(props.true_north, 4, "Z")
grid_north_p = origin @ angle @ (Vector((0, 0.8, 0)) * props.sun_path_size)
self.draw_text_at_position(context, "True North", grid_north_p)
def draw_text_at_position(self, context, text, position):
def draw_text_at_position(self, context: bpy.types.Context, text: str, position: Vector) -> None:
assert context.region and context.region_data
coords_2d = location_3d_to_region_2d(context.region, context.region_data, position)
if not coords_2d:
return
@@ -87,7 +88,8 @@ class SolarDecorator:
blf.position(self.font_id, co[0], co[1], 0)
blf.draw(self.font_id, line)
def draw_geometry(self, context):
def draw_geometry(self, context: bpy.types.Context) -> None:
assert context.region
self.addon_prefs = tool.Blender.get_addon_preferences()
decorator_color_special = self.addon_prefs.decorator_color_special
decorator_color_error = self.addon_prefs.decorator_color_error
@@ -102,12 +104,7 @@ class SolarDecorator:
self.line_shader.uniform_float("viewportSize", (context.region.width, context.region.height))
self.line_shader.uniform_float("lineWidth", 2.0)
# general shader
self.shader = gpu.shader.from_builtin("UNIFORM_COLOR")
vertex_shader = """
uniform mat4 ModelViewProjectionMatrix;
in vec3 pos;
void main()
{
gl_Position = ModelViewProjectionMatrix * vec4(pos, 1.0);
@@ -116,8 +113,6 @@ class SolarDecorator:
"""
fragment_shader = """
uniform vec4 color;
out vec4 fragColor;
void main()
{
float dist = length(gl_PointCoord - vec2(0.5));
@@ -129,9 +124,16 @@ class SolarDecorator:
}
"""
self.shader = gpu.types.GPUShader(vertex_shader, fragment_shader)
shader_info = gpu.types.GPUShaderCreateInfo()
shader_info.push_constant("MAT4", "ModelViewProjectionMatrix")
shader_info.vertex_in(0, "VEC3", "pos")
shader_info.vertex_source(vertex_shader)
shader_info.fragment_out(0, "VEC4", "fragColor")
shader_info.push_constant("VEC4", "color")
shader_info.fragment_source(fragment_shader)
self.shader = gpu.shader.create_from_info(shader_info)
props = bpy.context.scene.BIMSolarProperties
props = tool.Blender.get_solar_props()
origin = Matrix.Translation(props.sun_path_origin)
@@ -153,13 +155,13 @@ class SolarDecorator:
# True north
self.tn_angle = props.true_north
points = [Vector((0, 0, 0)), Vector((0, 0.75, 0))]
angle = Matrix.Rotation(radians(self.tn_angle), 4, "Z")
angle = Matrix.Rotation(self.tn_angle, 4, "Z")
points = [origin @ angle @ (v * props.sun_path_size) for v in points]
self.draw_batch("POINTS", points, decorator_color_special)
verts = [Vector((0, 0, 0)), Vector((0, 0.75, 0))]
edges = [[0, 1]]
angle = Matrix.Rotation(radians(self.tn_angle), 4, "Z")
angle = Matrix.Rotation(self.tn_angle, 4, "Z")
verts = [origin @ angle @ (v * props.sun_path_size) for v in verts]
self.draw_batch("LINES", verts, decorator_color_special, edges)
@@ -172,7 +174,7 @@ class SolarDecorator:
arc_start = Vector((0, 0.25, 0)) * props.sun_path_size
arc_end = angle @ arc_start
angle_half = Matrix.Rotation(radians(self.tn_angle / 2), 4, "Z")
angle_half = Matrix.Rotation(self.tn_angle / 2, 4, "Z")
arc_mid = angle_half @ arc_start
arc_segments = tool.Cad.create_arc_segments(
pts=[origin @ v for v in [arc_start, arc_mid, arc_end]], num_verts=12, make_edges=True
+76 -26
View File
@@ -28,8 +28,7 @@ from datetime import datetime
import bpy
import bonsai.tool as tool
from pathlib import Path
from typing import Union, Optional
from collections.abc import Sequence
from typing import Union, TYPE_CHECKING
import json
import math
import time
@@ -38,6 +37,8 @@ import ifcopenshell.util.geolocation
import webbrowser
import ifcopenshell.geom
import multiprocessing
import requests
from math import radians
from mathutils import Vector
from bonsai.bim.module.light.data import SolarData
from bpy_extras.io_utils import ExportHelper
@@ -58,7 +59,7 @@ class ExportOBJ(bpy.types.Operator):
@classmethod
def poll(cls, context):
props = context.scene.radiance_exporter_properties
props = tool.Blender.get_radiance_exporter_props()
if not props.should_load_from_memory and not props.ifc_file:
cls.poll_message_set("Select an IFC file or use 'load from memory' if it's loaded in Bonsai.")
return False
@@ -66,10 +67,11 @@ class ExportOBJ(bpy.types.Operator):
def execute(self, context):
# Get the output directory
should_load_from_memory = context.scene.radiance_exporter_properties.should_load_from_memory
output_dir = context.scene.radiance_exporter_properties.output_dir
props = tool.Blender.get_radiance_exporter_props()
should_load_from_memory = props.should_load_from_memory
output_dir = props.output_dir
context.scene.radiance_exporter_properties.is_exporting = True
props.is_exporting = True
# Conversion from IFC to OBJ
# Settings for obj
@@ -86,7 +88,7 @@ class ExportOBJ(bpy.types.Operator):
ifc_file = tool.Ifc.get()
else:
ifc_file_path = context.scene.radiance_exporter_properties.ifc_file
ifc_file_path = props.ifc_file
ifc_file = ifcopenshell.open(ifc_file_path)
obj_file_path = os.path.join(output_dir, "model.obj")
@@ -121,7 +123,7 @@ class ExportOBJ(bpy.types.Operator):
break
serialiser.finalize()
context.scene.radiance_exporter_properties.is_exporting = False
props.is_exporting = False
self.report({"INFO"}, "Exported OBJ file to: {}".format(obj_file_path))
@@ -141,9 +143,10 @@ class RadianceRender(bpy.types.Operator):
return {"CANCELLED"}
print("Starting Radiance rendering process...")
props = context.scene.radiance_exporter_properties
props = tool.Blender.get_radiance_exporter_props()
resolution_x, resolution_y = props.radiance_resolution_x, props.radiance_resolution_y
assert context.scene
context.scene.render.resolution_x = resolution_x
context.scene.render.resolution_y = resolution_y
@@ -172,8 +175,9 @@ class RadianceRender(bpy.types.Operator):
obj_file_path = os.path.join(output_dir, "model.obj")
sun_props = context.scene.BIMSolarProperties
sun_pos_props = context.scene.sun_pos_properties
sun_props = tool.Blender.get_solar_props()
sun_pos_props = tool.Blender.get_sun_props()
assert sun_pos_props
sky_file_path = os.path.join(output_dir, "sky.rad")
# latitude = sun_props.latitude
# longitude = sun_props.longitude
@@ -331,7 +335,7 @@ ground_glow source ground
# f.write("skyfunc glow ground_glow\n0\n0\n4 1.4 .9 .6 0\n")
# f.write("ground_glow source ground\n0\n0\n4 0 0 -1 180\n")
props = context.scene.radiance_exporter_properties
props = tool.Blender.get_radiance_exporter_props()
data = props.get_mappings_dict()
@@ -458,7 +462,7 @@ ground_glow source ground
return {"FINISHED"}
def get_active_camera(self, context):
props = context.scene.radiance_exporter_properties
props = tool.Blender.get_radiance_exporter_props()
if props.use_active_camera:
return context.scene.camera
else:
@@ -475,8 +479,7 @@ ground_glow source ground
return (position.x, position.y, position.z), (direction.x, direction.y, direction.z)
def getResolution(self, context):
scene = context.scene
props = scene.radiance_exporter_properties
props = tool.Blender.get_radiance_exporter_props()
resolution_x = props.radiance_resolution_x
resolution_y = props.radiance_resolution_y
return resolution_x, resolution_y
@@ -505,12 +508,12 @@ class ImportTrueNorth(bpy.types.Operator):
return SolarData.data["true_north"] is not None
def execute(self, context):
props = context.scene.BIMSolarProperties
props = tool.Blender.get_solar_props()
for context in tool.Ifc.get().by_type("IfcGeometricRepresentationContext", include_subtypes=False):
if not context.TrueNorth:
continue
value = context.TrueNorth.DirectionRatios
props.true_north = ifcopenshell.util.geolocation.yaxis2angle(*value[:2])
props.true_north = radians(ifcopenshell.util.geolocation.yaxis2angle(*value[:2]))
return {"FINISHED"}
@@ -521,7 +524,7 @@ class ImportLatLong(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
props = context.scene.BIMSolarProperties
props = tool.Blender.get_solar_props()
site = tool.Ifc.get().by_id(int(props.sites))
if site.RefLatitude and site.RefLongitude:
props.latitude = ifcopenshell.util.geolocation.dms2dd(*site.RefLatitude)
@@ -536,8 +539,9 @@ class MoveSunPathTo3DCursor(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
props = context.scene.BIMSolarProperties
props.sun_path_origin = bpy.context.scene.cursor.location
props = tool.Blender.get_solar_props()
assert context.scene
props.sun_path_origin = context.scene.cursor.location
tool.Blender.update_viewport()
return {"FINISHED"}
@@ -557,18 +561,64 @@ class ViewFromSun(bpy.types.Operator):
camera.data.ortho_scale = 100 # The default of 6m is too small
context.scene.collection.objects.link(camera)
tool.Blender.activate_camera(camera)
props = context.scene.BIMSolarProperties
props = tool.Blender.get_solar_props()
props.hour = props.hour # Just to refresh camera position
return {"FINISHED"}
class LightPickCoordinates(bpy.types.Operator):
bl_idname = "bim.light_pick_coordinates"
bl_label = "Pick Coordinates"
bl_description = (
"Open web browser with Google Maps to pick coordinates (Right Mouse Click in maps to copy selected location).\n\n"
"ALT+Click to insert current location based on the current IP-address (using ip-api.com)."
)
bl_options = {"REGISTER", "UNDO"}
use_current_location: bpy.props.BoolProperty(options={"SKIP_SAVE"}) # pyright: ignore[reportRedeclaration]
if TYPE_CHECKING:
use_current_location: bool
def invoke(self, context, event):
if event.alt:
self.use_current_location = True
return self.execute(context)
def execute(self, context):
props = tool.Blender.get_solar_props()
if not self.use_current_location:
zoom = 13.5
url = f"https://www.google.com/maps/@{props.latitude},{props.longitude},{zoom}z"
webbrowser.open(url)
return {"FINISHED"}
response = requests.get("http://ip-api.com/json/")
data = response.json()
props.latitude = data["lat"]
props.longitude = data["lon"]
return {"FINISHED"}
class LightSetTimeToNow(bpy.types.Operator):
bl_idname = "bim.light_set_time_to_now"
bl_label = "Now"
bl_description = "Set time to current local time."
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
props = tool.Blender.get_solar_props()
props.set_from_datetime(datetime.now())
return {"FINISHED"}
class RefreshIFCMaterials(bpy.types.Operator):
bl_idname = "bim.refresh_ifc_materials"
bl_label = "Refresh IFC Materials"
bl_description = "Refresh the list of IFC materials for mapping"
def execute(self, context):
props = context.scene.radiance_exporter_properties
props = tool.Blender.get_radiance_exporter_props()
ifc_file: ifcopenshell.file
ifc_file = tool.Ifc.get() if props.should_load_from_memory else ifcopenshell.open(props.ifc_file)
@@ -616,7 +666,7 @@ class UnmapMaterial(bpy.types.Operator):
material_index: bpy.props.IntProperty()
def execute(self, context):
props = context.scene.radiance_exporter_properties
props = tool.Blender.get_radiance_exporter_props()
material = props.materials[self.material_index]
props.unmap_material(material.name)
return {"FINISHED"}
@@ -633,7 +683,7 @@ class RADIANCE_OT_select_camera(bpy.types.Operator):
return context.object is not None and context.object.type == "CAMERA"
def execute(self, context):
props = context.scene.radiance_exporter_properties
props = tool.Blender.get_radiance_exporter_props()
props.selected_camera = context.object
props.use_active_camera = False
return {"FINISHED"}
@@ -648,7 +698,7 @@ class RADIANCE_OT_export_material_mappings(bpy.types.Operator, ExportHelper):
filter_glob: bpy.props.StringProperty(default="*.json", options={"HIDDEN"})
def execute(self, context):
props = context.scene.radiance_exporter_properties
props = tool.Blender.get_radiance_exporter_props()
mappings = {}
for material in props.materials:
@@ -674,7 +724,7 @@ class RADIANCE_OT_import_material_mappings(bpy.types.Operator, ImportHelper):
filename_ext = ".json"
def execute(self, context):
props = context.scene.radiance_exporter_properties
props = tool.Blender.get_radiance_exporter_props()
props.import_mappings(self.filepath)
self.report({"INFO"}, f"Material mappings imported from {self.filepath}")
return {"FINISHED"}
+143 -45
View File
@@ -22,6 +22,7 @@ import tzfpy
import json
import datetime
import bonsai.tool as tool
from typing import TYPE_CHECKING, Literal, Union
from math import radians, pi
from mathutils import Euler, Vector, Matrix, Quaternion
from bpy.props import (
@@ -40,68 +41,84 @@ from bonsai.bim.module.light.data import SolarData
from bonsai.bim.module.light.decorator import SolarDecorator
sun_position = tool.Blender.get_sun_position_addon()
now = datetime.datetime.now()
with open(os.path.join(os.path.dirname(__file__), "spectraldb.json"), "r") as f:
spectraldb = json.load(f)
spectraldb: dict[str, dict[str, str]] = json.load(f)
def get_sites(self, context):
def get_sites(self: "BIMSolarProperties", context: bpy.types.Context) -> tool.Blender.BLENDER_ENUM_ITEMS:
if not SolarData.is_loaded:
SolarData.load()
return SolarData.data["sites"]
def update_latlong(self, context):
def update_coordinates(self: "BIMSolarProperties", context: bpy.types.Context) -> None:
# We define our own `coordinates` property just to ensure changing it would update
# all other props.
# But we still need to set original prop value and retrieve it to ensure coordinate
# was parsed correctly.
sun_props = tool.Blender.get_sun_props()
assert sun_props
sun_props.coordinates = self.coordinates
self["coordinates"] = sun_props.coordinates
self["longitude"] = sun_props.longitude
self["latitude"] = sun_props.latitude
update_sun_path(self)
def update_hourminute(self, context):
def update_latlong(self: "BIMSolarProperties", context: bpy.types.Context) -> None:
sun_props = tool.Blender.get_sun_props()
assert sun_props
sun_props.longitude = sun_props.longitude
sun_props.latitude = sun_props.latitude
self["coordinates"] = sun_props.coordinates
update_sun_path(self)
def update_date(self, context):
update_sun_path(self)
def update_true_north(self, context):
update_sun_path(self)
def update_sun_path_size(self, context):
update_sun_path(self)
def update_shadow_mode(self, context):
def update_shadow_mode(self: "BIMSolarProperties", context: bpy.types.Context) -> None:
assert context.scene
if self.shadow_mode == "SHADING":
update_sun_path(self)
context.scene.render.engine = "BLENDER_WORKBENCH"
assert context.scene.display
assert context.scene.display.shading
context.scene.display.shading.light = "FLAT"
context.scene.display.shading.show_shadows = True
context.scene.display.shading.show_object_outline = True
context.scene.display.shadow_focus = 1.0
assert context.scene.view_settings
context.scene.view_settings.view_transform = "Standard" # Preserve shading colours
space = tool.Blender.get_view3d_space()
assert space
space.shading.type = "RENDERED"
elif self.shadow_mode == "RENDERING":
if context.scene.sun_pos_properties.sun_object is None:
sun_props = tool.Blender.get_sun_props()
assert sun_props
if sun_props.sun_object is None:
bpy.ops.object.light_add(type="SUN", radius=1, align="WORLD", location=(0, 0, 0), scale=(1, 1, 1))
bpy.ops.object.move_to_collection(collection_index=0)
context.scene.sun_pos_properties.sun_object = bpy.context.active_object
sun_props.sun_object = bpy.context.active_object
update_sun_path(self)
context.scene.render.engine = "BLENDER_EEVEE_NEXT"
assert context.scene.display
assert context.scene.display.shading
context.scene.display.shading.light = "FLAT"
context.scene.display.shading.show_shadows = True
context.scene.display.shading.show_object_outline = True
context.scene.display.shadow_focus = 1.0
assert context.scene.view_settings
context.scene.view_settings.view_transform = "Standard" # Preserve shading colours
space = tool.Blender.get_view3d_space()
assert space
space.shading.type = "RENDERED"
else:
space = tool.Blender.get_view3d_space()
assert space
space.shading.type = "SOLID"
def update_display_sun_path(self, context):
def update_display_sun_path(self: "BIMSolarProperties", context: bpy.types.Context) -> None:
if self.display_sun_path:
update_sun_path(self)
SolarDecorator.install(bpy.context)
@@ -109,20 +126,25 @@ def update_display_sun_path(self, context):
SolarDecorator.uninstall()
def update_resolution(self, context):
def update_resolution(self: "RadianceExporterProperties", context: bpy.types.Context) -> None:
assert context.scene
context.scene.render.resolution_x = self.radiance_resolution_x
context.scene.render.resolution_y = self.radiance_resolution_y
def update_sun_path(self):
def update_sun_path(self: "BIMSolarProperties", context: Union[bpy.types.Context, None] = None) -> None:
if not SolarData.is_loaded:
SolarData.load()
if (sun_position := SolarData.data["sun_position"]) is None:
return
props = bpy.context.scene.BIMSolarProperties
sun_props = bpy.context.scene.sun_pos_properties
if TYPE_CHECKING:
import sun_position
props = tool.Blender.get_solar_props()
sun_props = tool.Blender.get_sun_props()
assert sun_props
sun_props.sun_distance = self.sun_path_size
sun_props.latitude = self.latitude
@@ -132,7 +154,7 @@ def update_sun_path(self):
sun_props.day = self.day
sun_props.time = self.hour + (self.minute / 60)
# Preserve IFC sign convention
sun_props.north_offset = radians(self.true_north * -1)
sun_props.north_offset = self.true_north * -1
props.timezone = tzfpy.get_tz(props.longitude, props.latitude)
timezone = pytz.timezone(props.timezone)
@@ -166,6 +188,8 @@ def update_sun_path(self):
props.elevation = elevation
props.UTC_zone = zone
assert bpy.context.scene
assert bpy.context.scene.display
if sun_vector.z < 0:
bpy.context.scene.display.light_direction = mat @ Vector((0, 0, 1))
else:
@@ -173,6 +197,7 @@ def update_sun_path(self):
SolarData.data["sun"] = sun_vector
if obj := bpy.data.objects.get("SunPathCamera"):
assert isinstance(obj.data, bpy.types.Camera)
obj.matrix_world.translation = sun_vector
z180_quaternion = Quaternion((0, 0, 1), radians(180))
obj.rotation_mode = "QUATERNION"
@@ -181,25 +206,31 @@ def update_sun_path(self):
class RadianceMaterial(PropertyGroup):
name: StringProperty(name="Name")
style_id: StringProperty(name="Style ID")
category: StringProperty(name="Category")
subcategory: StringProperty(name="Subcategory")
is_mapped: BoolProperty(name="Is Mapped", default=False)
color: FloatVectorProperty(name="Color", subtype="COLOR", default=(1.0, 1.0, 1.0), min=0.0, max=1.0, size=3)
if TYPE_CHECKING:
style_id: str
category: str
subcategory: str
is_mapped: bool
color: tuple[float, float, float]
class RadianceExporterProperties(PropertyGroup):
def update_output_dir(self, context):
def update_output_dir(self, context) -> None:
if self.output_dir:
self.output_dir = bpy.path.abspath(self.output_dir)
def update_ifc_file(self, context):
def update_ifc_file(self, context) -> None:
if self.ifc_file:
self.ifc_file = bpy.path.abspath(self.ifc_file)
def add_material_mapping(self, style_id, style_name):
def add_material_mapping(self, style_id: str, style_name: str) -> RadianceMaterial:
item = self.materials.add()
item.name = style_name
item.style_id = style_id
@@ -208,7 +239,7 @@ class RadianceExporterProperties(PropertyGroup):
item.color = (1.0, 1.0, 1.0) # Default white
return item
def import_mappings(self, filepath):
def import_mappings(self, filepath: str) -> None:
with open(filepath, "r") as f:
mappings = json.load(f)
@@ -225,10 +256,10 @@ class RadianceExporterProperties(PropertyGroup):
new_material.subcategory = mapping["subcategory"]
new_material.is_mapped = True
def get_material_mapping(self, style_name):
def get_material_mapping(self, style_name: str) -> Union[RadianceMaterial, None]:
return next((item for item in self.materials if item.name == style_name), None)
def set_material_mapping(self, style_id, style_name, category, subcategory):
def set_material_mapping(self, style_id: str, style_name: str, category: str, subcategory: str) -> None:
item = self.get_material_mapping(style_name)
if item:
item.category = category
@@ -238,14 +269,14 @@ class RadianceExporterProperties(PropertyGroup):
self.materials[-1].category = category
self.materials[-1].subcategory = subcategory
def get_mappings_dict(self):
def get_mappings_dict(self) -> dict[str, tuple[str, str]]:
return {
item.style_id: (item.category, item.subcategory)
for item in self.materials
if item.category and item.subcategory
}
def unmap_material(self, style_name):
def unmap_material(self, style_name: str) -> None:
item = self.get_material_mapping(style_name)
if item:
item.category = ""
@@ -273,7 +304,7 @@ class RadianceExporterProperties(PropertyGroup):
("Glass", "Glass", ""),
]
def update_material_mapping(self, context):
def update_material_mapping(self, context: bpy.types.Context) -> None:
if self.active_material_index >= 0 and self.active_material_index < len(self.materials):
active_material = self.materials[self.active_material_index]
active_material.category = self.category
@@ -285,7 +316,7 @@ class RadianceExporterProperties(PropertyGroup):
items=categories, name="Category", description="Material category", update=update_material_mapping
)
def get_subcategories(self, context):
def get_subcategories(self, context: bpy.types.Context) -> tool.Blender.BLENDER_ENUM_ITEMS:
global SUBCATEGORIES_ENUM_ITEMS
if self.category in spectraldb:
SUBCATEGORIES_ENUM_ITEMS = [(k, k, "") for k in spectraldb[self.category].keys()]
@@ -390,21 +421,60 @@ class RadianceExporterProperties(PropertyGroup):
poll=lambda self, object: object.type == "CAMERA",
)
if TYPE_CHECKING:
is_exporting: bool
category: str
subcategory: str
materials: bpy.types.bpy_prop_collection_idprop[RadianceMaterial]
active_material_index: int
should_load_from_memory: bool
radiance_resolution_x: int
radiance_resolution_y: int
output_dir: str
ifc_file: str
radiance_quality: Literal["LOW", "MEDIUM", "HIGH"]
radiance_detail: Literal["LOW", "MEDIUM", "HIGH"]
radiance_variability: Literal["LOW", "MEDIUM", "HIGH"]
output_file_name: str
output_file_format: Literal["HDR"]
use_hdr: bool
choose_hdr_image: Literal["Noon"]
use_active_camera: bool
selected_camera: Union[bpy.types.Object, None]
class BIMSolarProperties(PropertyGroup):
sites: EnumProperty(items=get_sites, name="Sites")
latitude: FloatProperty(name="Latitude", min=-90, max=90, update=update_latlong)
longitude: FloatProperty(name="Longitude", min=-180, max=180, update=update_latlong)
coordinates: StringProperty(
name="Coordinates",
description="Latitude and longitude on Earth. Coordinates can be directly entered from an online map",
update=update_coordinates,
default="33°5154.51″S 151°1235.64″E",
)
latitude: FloatProperty(
name="Latitude",
min=-90,
max=90,
update=update_latlong,
default=-33.865143,
)
longitude: FloatProperty(
name="Longitude",
min=-180,
max=180,
update=update_latlong,
default=151.209900,
)
timezone: StringProperty(name="Timezone", default="Etc/GMT")
true_north: FloatProperty(name="True North", min=-180, max=180, update=update_true_north)
year: IntProperty(name="Year", min=1, max=9999, default=2025, update=update_date)
month: IntProperty(name="Month", min=1, max=12, default=1, update=update_date)
day: IntProperty(name="Date", min=1, max=31, default=1, update=update_date)
hour: IntProperty(name="Hour", min=0, max=23, default=12, update=update_hourminute)
minute: IntProperty(name="Minute", min=0, max=59, update=update_hourminute)
true_north: FloatProperty(name="True North", min=-pi, max=pi, subtype="ANGLE", update=update_sun_path)
year: IntProperty(name="Year", min=1, max=9999, default=now.year, update=update_sun_path)
month: IntProperty(name="Month", min=1, max=12, default=now.month, update=update_sun_path)
day: IntProperty(name="Date", min=1, max=31, default=now.day, update=update_sun_path)
hour: IntProperty(name="Hour", min=0, max=23, default=now.hour, update=update_sun_path)
minute: IntProperty(name="Minute", min=0, max=59, default=now.minute, update=update_sun_path)
sun_position: FloatVectorProperty(name="Sun Position", subtype="XYZ", default=(0, 0, 0))
sun_path_origin: FloatVectorProperty(name="Sun Path Origin", subtype="XYZ", default=(0, 0, 0))
sun_path_size: FloatProperty(name="Sun Path Size", min=0.1, default=50, update=update_sun_path_size)
sun_path_size: FloatProperty(name="Sun Path Size", min=0.1, default=50, update=update_sun_path)
azimuth: FloatProperty(name="Azimuth")
elevation: FloatProperty(name="Elevation")
UTC_zone: FloatProperty(name="UTC Zone")
@@ -436,3 +506,31 @@ class BIMSolarProperties(PropertyGroup):
description="Displays analemmas and sun position",
update=update_display_sun_path,
)
if TYPE_CHECKING:
sites: str
coordinates: str
latitude: float
longitude: float
timezone: str
true_north: float
year: int
month: int
day: int
hour: int
minute: int
sun_position: Vector
sun_path_origin: Vector
sun_path_size: float
azimuth: float
elevation: float
UTC_zone: float
shadow_mode: Literal["NONE", "SHADING", "RENDERING"]
display_sun_path: bool
def set_from_datetime(self, dt: datetime.datetime) -> None:
self.year = dt.year
self.month = dt.month
self.day = dt.day
self.hour = dt.hour
self.minute = dt.minute
+28 -9
View File
@@ -18,6 +18,7 @@
import bpy
import bonsai.tool as tool
from typing import TYPE_CHECKING
from bonsai.bim.module.light.data import SolarData
@@ -33,10 +34,9 @@ class BIM_PT_radiance_exporter(bpy.types.Panel):
bl_parent_id = "BIM_PT_tab_lighting"
def draw(self, context):
assert self.layout
layout = self.layout
scene = context.scene
props = scene.radiance_exporter_properties
props = tool.Blender.get_radiance_exporter_props()
if tool.Ifc.get():
row = self.layout.row()
@@ -145,11 +145,22 @@ class BIM_PT_solar(bpy.types.Panel):
if not SolarData.is_loaded:
SolarData.load()
if (sun_position := SolarData.data["sun_position"]) is None:
assert self.layout
# Props are more reliable as they'll come and go regardless of Data update.
if (sun_props := tool.Blender.get_sun_props()) is None:
self.layout.label(text="Enable 'Sun Position' Add-on To Continue", icon="ERROR")
return
props = context.scene.BIMSolarProperties
sun_position = SolarData.data["sun_position"]
if sun_position is None:
# There are props, so there must be an addon.
SolarData.data["sun_position"] = SolarData.sun_position()
if TYPE_CHECKING:
import sun_position
props = tool.Blender.get_solar_props()
if SolarData.data["sites"]:
row = self.layout.row(align=True)
@@ -159,10 +170,12 @@ class BIM_PT_solar(bpy.types.Panel):
row = self.layout.row(align=True)
row.label(text="No Sites With Lat/Longs Found", icon="ERROR")
sun_props = context.scene.sun_pos_properties
row = self.layout.row(align=True)
row.alignment = "RIGHT"
row.operator("bim.light_pick_coordinates", icon="URL", text="Pick")
row = self.layout.row()
row.prop(sun_props, "coordinates", icon="URL")
row = self.layout.row(align=True)
row.prop(props, "coordinates", icon="URL")
row = self.layout.row(align=True)
row.prop(props, "latitude")
row.prop(props, "longitude")
@@ -172,6 +185,10 @@ class BIM_PT_solar(bpy.types.Panel):
if SolarData.data["true_north"] is not None:
row.operator("bim.import_true_north", icon="IMPORT", text="")
row = self.layout.row()
row.alignment = "RIGHT"
row.operator("bim.light_set_time_to_now", icon="TIME", text="Now")
row = self.layout.row()
row.prop(props, "year")
row = self.layout.row(align=True)
@@ -243,7 +260,9 @@ class BIM_PT_solar(bpy.types.Panel):
row.prop(context.scene.display.shading, "shadow_intensity", text="Shadow Intensity")
elif props.shadow_mode == "RENDERING":
row = self.layout.row()
row.prop(context.scene.sun_pos_properties.sun_object.data, "energy", text="Sun Intensity")
sun_props = tool.Blender.get_sun_props()
assert sun_props
row.prop(sun_props.sun_object.data, "energy", text="Sun Intensity")
row = self.layout.row(align=True)
row.operator("bim.view_from_sun", icon="LIGHT_HEMI")
@@ -610,7 +610,7 @@ class LoadTypeThumbnails(bpy.types.Operator):
while queue:
# if bpy.app.is_job_running("RENDER_PREVIEW") does not seem to reflect asset preview generation
element = queue.pop()
tool.Model.mark_thumbnail_for_update(element)
tool.Model.update_thumbnail_for_element(element)
return {"FINISHED"}
+3
View File
@@ -36,6 +36,7 @@ from bonsai.bim.module.model.stair import regenerate_stair_mesh
from bonsai.bim.module.model.railing import update_railing_modifier_bmesh
from bonsai.bim.module.model.roof import update_roof_modifier_bmesh
from collections.abc import Iterable
from typing import Any
class BIM_MT_type_manager_menu(bpy.types.Menu):
@@ -95,6 +96,7 @@ class LaunchTypeManager(bpy.types.Operator):
return context.window_manager.invoke_props_dialog(self, width=550, title="Type Manager", confirm_text="Close")
def draw(self, context):
assert self.layout
props = tool.Model.get_model_props()
row = self.layout.row(align=True)
text = f"{AuthoringData.data['total_types']} {AuthoringData.data['ifc_element_type'] or 'Types'}"
@@ -135,6 +137,7 @@ class LaunchTypeManager(bpy.types.Operator):
flow = self.layout.grid_flow(row_major=True, columns=3, even_columns=True, even_rows=True, align=True)
relating_type: dict[str, Any]
for relating_type in AuthoringData.data["paginated_relating_types"]:
outer_col = flow.column()
box = outer_col.box()
@@ -29,7 +29,7 @@ from bpy.types import WorkSpaceTool, Menu
from bonsai.bim.module.model.data import AuthoringData, ItemData
from bonsai.bim.module.system.data import PortData
from bonsai.bim.module.model.prop import get_ifc_class
from typing import Optional, Union
from typing import Optional, Union, Any
from functools import partial
@@ -506,15 +506,17 @@ class CreateObjectUI:
cls.draw_type_manager_launcher(context)
@classmethod
def draw_container_info(cls, context):
def draw_container_info(cls, context: bpy.types.Context) -> None:
text = AuthoringData.data["default_container"]
assert context.region
if context.region.type == "UI":
text = f"Container: {text}"
cls.layout.row(align=True).label(text=text, icon="OUTLINER_COLLECTION")
@classmethod
def draw_type_manager_launcher(cls, context):
def draw_type_manager_launcher(cls, context: bpy.types.Context) -> None:
assert context.region
ui_context = context.region.type
props = tool.Model.get_model_props()
row = cls.layout.row(align=True)
@@ -568,7 +570,8 @@ class CreateObjectUI:
op.ifc_element_type = AuthoringData.data["ifc_element_type"]
@classmethod
def draw_add_object(cls, context):
def draw_add_object(cls, context: bpy.types.Context) -> None:
assert context.region
ui_context = str(context.region.type)
row = cls.layout.row(align=True)
if AuthoringData.data["relating_type_id"]:
@@ -580,7 +583,8 @@ class CreateObjectUI:
row.label(text="No Construction Type", icon="FILE_3D")
@classmethod
def draw_add_object_parameters(cls, context):
def draw_add_object_parameters(cls, context: bpy.types.Context) -> None:
assert context.region
ui_context = str(context.region.type)
row = cls.layout.row(align=True)
if not AuthoringData.data["relating_type_id"]:
@@ -633,7 +637,8 @@ class CreateObjectUI:
row.prop(data=cls.props, property="rl_mode", text="RL Mode" if ui_context != "TOOL_HEADER" else "RL")
@classmethod
def draw_thumbnail(cls, context):
def draw_thumbnail(cls, context: bpy.types.Context) -> None:
assert context.region
ui_context = context.region.type
row = cls.layout.row(align=True)
if not AuthoringData.data["ifc_element_type"]:
+10 -12
View File
@@ -61,23 +61,21 @@ class UnitsData:
return ifcopenshell.util.unit.get_full_unit_name(unit)
@classmethod
def unit_classes(cls):
def unit_classes(cls) -> tool.Blender.BLENDER_ENUM_ITEMS:
assert (entity := tool.Ifc.schema().declaration_by_name("IfcNamedUnit").as_entity())
declarations = ifcopenshell.util.schema.get_subtypes(entity)
version = tool.Ifc.get_schema()
classes = sorted([d.name() for d in declarations]) + ["IfcDerivedUnit", "IfcMonetaryUnit"]
results = [
(c, c, get_entity_doc(version, c).get("description", "")) for c in sorted([d.name() for d in declarations])
(
c,
c,
get_entity_doc(version, c).get("description", ""),
tool.Unit.get_icon_for_unit_class(c),
i,
)
for i, c in enumerate(classes)
]
results.extend(
[
("IfcDerivedUnit", "IfcDerivedUnit", get_entity_doc(version, "IfcDerivedUnit").get("description", "")),
(
"IfcMonetaryUnit",
"IfcMonetaryUnit",
get_entity_doc(version, "IfcMonetaryUnit").get("description", ""),
),
]
)
return results
@classmethod
@@ -35,6 +35,7 @@ class AssignSceneUnits(bpy.types.Operator, tool.Ifc.Operator):
class AssignUnit(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.assign_unit"
bl_label = "Assign Unit"
bl_description = "Assign provided unit as the default project unit for it's unit type."
bl_options = {"REGISTER", "UNDO"}
unit: bpy.props.IntProperty()
@@ -45,6 +46,7 @@ class AssignUnit(bpy.types.Operator, tool.Ifc.Operator):
class UnassignUnit(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.unassign_unit"
bl_label = "Unassign Unit"
bl_description = "Unassign the specified unit as the project default."
bl_options = {"REGISTER", "UNDO"}
unit: bpy.props.IntProperty()
@@ -52,7 +52,6 @@ def get_named_unit_types(self: "BIMUnitProperties", context: bpy.types.Context)
class Unit(PropertyGroup):
name: StringProperty(name="Name")
unit_type: StringProperty(name="Unit Type")
is_assigned: BoolProperty(name="Is Assigned")
ifc_class: StringProperty(name="IFC Class")
+17 -7
View File
@@ -16,11 +16,17 @@
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
from __future__ import annotations
import bonsai.bim.helper
import bonsai.tool as tool
import bpy
from bpy.types import Panel, UIList
from bonsai.bim.helper import prop_with_search
from bonsai.bim.module.unit.data import UnitsData
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from bonsai.bim.module.unit.prop import BIMUnitProperties, Unit
class BIM_PT_units(Panel):
@@ -102,15 +108,19 @@ class BIM_PT_units(Panel):
class BIM_UL_units(UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
def draw_item(
self,
context,
layout: bpy.types.UILayout,
data: BIMUnitProperties,
item: Unit,
icon_: int,
active_data,
active_propname,
) -> None:
props = tool.Unit.get_unit_props()
if item:
icon = "MOD_MESHDEFORM"
if item.ifc_class == "IfcSIUnit":
icon = "SNAP_GRID"
elif item.ifc_class == "IfcMonetaryUnit":
icon = "COPY_ID"
icon = tool.Unit.get_icon_for_unit_class(item.ifc_class)
row = layout.row(align=True)
row.label(text=item.unit_type or "No Type", icon=icon)
row.label(text=item.name or "Unnamed")
+2
View File
@@ -977,6 +977,8 @@ class BIM_OT_enum_property_search(bpy.types.Operator):
def draw(self, context):
# Mandatory to access context.data in update :
self.layout.context_pointer_set(name="data", data=self.data)
# NOTE: activate_init don't work with prop_search, so cannot activate field for typing,
# though it would fit perfectly.
self.layout.prop_search(self, "dummy_name", self, "collection_names")
def execute(self, context):
+1 -1
View File
@@ -305,7 +305,7 @@ class Drawing:
def copy_representation(cls, source, dest): pass
def create_annotation_context(cls, target_view, object_type=None): pass
def create_annotation_object(cls, drawing, object_type): pass
def create_camera(cls, name, matrix, location_hint): pass
def create_camera(cls, name, matrix, location_hint, target_view): pass
def create_svg_schedule(cls, schedule): pass
def create_svg_sheet(cls, document, titleblock): pass
def delete_collection(cls, collection): pass
+13 -13
View File
@@ -26,7 +26,7 @@ if TYPE_CHECKING:
import bonsai.tool as tool
def assign_scene_units(ifc: tool.Ifc, unit: tool.Unit) -> None:
def assign_scene_units(ifc: type[tool.Ifc], unit: type[tool.Unit]) -> None:
if unit.is_scene_unit_metric():
prefix = unit.get_scene_unit_si_prefix("LENGTHUNIT")
lengthunit = ifc.run("unit.add_si_unit", unit_type="LENGTHUNIT", prefix=prefix)
@@ -44,66 +44,66 @@ def assign_scene_units(ifc: tool.Ifc, unit: tool.Unit) -> None:
ifc.run("unit.assign_unit", units=[lengthunit, areaunit, volumeunit, planeangleunit])
def assign_unit(ifc: tool.Ifc, unit_tool: tool.Unit, unit: ifcopenshell.entity_instance) -> None:
def assign_unit(ifc: type[tool.Ifc], unit_tool: type[tool.Unit], unit: ifcopenshell.entity_instance) -> None:
ifc.run("unit.assign_unit", units=[unit])
unit_tool.import_units()
def unassign_unit(ifc: tool.Ifc, unit_tool: tool.Unit, unit: ifcopenshell.entity_instance) -> None:
def unassign_unit(ifc: type[tool.Ifc], unit_tool: type[tool.Unit], unit: ifcopenshell.entity_instance) -> None:
ifc.run("unit.unassign_unit", units=[unit])
unit_tool.import_units()
def load_units(unit: tool.Unit) -> None:
def load_units(unit: type[tool.Unit]) -> None:
unit.import_units()
unit.enable_editing_units()
def disable_unit_editing_ui(unit: tool.Unit) -> None:
def disable_unit_editing_ui(unit: type[tool.Unit]) -> None:
unit.disable_editing_units()
def remove_unit(ifc: tool.Ifc, unit_tool: tool.Unit, unit: ifcopenshell.entity_instance) -> None:
def remove_unit(ifc: type[tool.Ifc], unit_tool: type[tool.Unit], unit: ifcopenshell.entity_instance) -> None:
ifc.run("unit.remove_unit", unit=unit)
unit_tool.import_units()
def add_monetary_unit(ifc: tool.Ifc, unit: tool.Unit) -> ifcopenshell.entity_instance:
def add_monetary_unit(ifc: type[tool.Ifc], unit: type[tool.Unit]) -> ifcopenshell.entity_instance:
result = ifc.run("unit.add_monetary_unit")
unit.import_units()
return result
def add_si_unit(ifc: tool.Ifc, unit: tool.Unit, unit_type: str) -> ifcopenshell.entity_instance:
def add_si_unit(ifc: type[tool.Ifc], unit: type[tool.Unit], unit_type: str) -> ifcopenshell.entity_instance:
result = ifc.run("unit.add_si_unit", unit_type=unit_type)
unit.import_units()
return result
def add_context_dependent_unit(
ifc: tool.Ifc, unit: tool.Unit, unit_type: str, name: str
ifc: type[tool.Ifc], unit: type[tool.Unit], unit_type: str, name: str
) -> ifcopenshell.entity_instance:
result = ifc.run("unit.add_context_dependent_unit", unit_type=unit_type, name=name)
unit.import_units()
return result
def add_conversion_based_unit(ifc: tool.Ifc, unit: tool.Unit, name: str) -> ifcopenshell.entity_instance:
def add_conversion_based_unit(ifc: type[tool.Ifc], unit: type[tool.Unit], name: str) -> ifcopenshell.entity_instance:
result = ifc.run("unit.add_conversion_based_unit", name=name)
unit.import_units()
return result
def enable_editing_unit(unit_tool: tool.Unit, unit: ifcopenshell.entity_instance) -> None:
def enable_editing_unit(unit_tool: type[tool.Unit], unit: ifcopenshell.entity_instance) -> None:
unit_tool.set_active_unit(unit)
unit_tool.import_unit_attributes(unit)
def disable_editing_unit(unit: tool.Unit) -> None:
def disable_editing_unit(unit: type[tool.Unit]) -> None:
unit.clear_active_unit()
def edit_unit(ifc: tool.Ifc, unit_tool: tool.Unit, unit: ifcopenshell.entity_instance) -> None:
def edit_unit(ifc: type[tool.Ifc], unit_tool: type[tool.Unit], unit: ifcopenshell.entity_instance) -> None:
attributes = unit_tool.export_unit_attributes()
if unit_tool.is_unit_class(unit, "IfcMonetaryUnit"):
ifc.run("unit.edit_monetary_unit", unit=unit, attributes=attributes)
+18 -1
View File
@@ -54,13 +54,15 @@ from typing import (
from collections.abc import Iterable, Callable, Generator, Sequence, Sized
if TYPE_CHECKING:
from sun_position.properties import SunPosProperties
import bpy.stub_internal.rna_enums as rna_enums
from bonsai.bim.prop import BIMProperties, BIMObjectProperties
from bonsai.bim.module.attribute.prop import BIMAttributeProperties
from bonsai.bim.module.csv.prop import CsvProperties
from bonsai.bim.module.constraint.prop import BIMConstraintProperties, BIMObjectConstraintProperties
from bonsai.bim.module.csv.prop import CsvProperties
from bonsai.bim.module.diff.prop import DiffProperties
from bonsai.bim.module.group.prop import BIMGroupProperties
from bonsai.bim.module.light.prop import BIMSolarProperties, RadianceExporterProperties
T = TypeVar("T")
@@ -1495,6 +1497,11 @@ class Blender(bonsai.core.tool.Blender):
return sun_position
@classmethod
def get_sun_props(cls) -> Union[SunPosProperties, None]:
assert (scene := bpy.context.scene)
return getattr(scene, "sun_pos_properties", None)
@classmethod
def scale_font_size(cls, size):
default_dpi = 72
@@ -1694,6 +1701,16 @@ class Blender(bonsai.core.tool.Blender):
def get_object_attribute_props(cls, obj: bpy.types.Object) -> BIMAttributeProperties:
return obj.BIMAttributeProperties
@classmethod
def get_solar_props(cls) -> BIMSolarProperties:
assert (scene := bpy.context.scene)
return scene.BIMSolarProperties
@classmethod
def get_radiance_exporter_props(cls) -> RadianceExporterProperties:
assert (scene := bpy.context.scene)
return scene.BIMRadianceExporeterProperies
@classmethod
def get_ifc_definition_id(cls, obj: IFC_CONNECTED_TYPE) -> int:
if isinstance(obj, bpy.types.Object):
+8 -5
View File
@@ -1063,12 +1063,15 @@ class Model(bonsai.core.tool.Model):
return # Already processed
assert isinstance(obj, bpy.types.Object)
obj.asset_generate_preview()
while not obj.preview:
pass
# Since Blender 4.5 have to use `preview_ensure` instead of `asset_generate_preview`, see #6839.
obj.preview_ensure()
# if object has .data we can use default blender .asset_generate_preview()
if not obj.data:
if obj.data:
# If object has .data we can use default Blender preview.
# No need to preview to update, Blender will do it in background,
# `preview.icon_id` doesn't change after `asset_generate_preview()`.
obj.asset_generate_preview()
else:
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
size = 128
img = Image.new("RGBA", (size, size))
+9 -1
View File
@@ -125,7 +125,7 @@ class Unit(bonsai.core.tool.Unit):
props = tool.Unit.get_unit_props()
props.units.clear()
units = []
units: list[ifcopenshell.entity_instance] = []
for unit_class in ["IfcDerivedUnit", "IfcMonetaryUnit", "IfcNamedUnit"]:
units += tool.Ifc.get().by_type(unit_class)
@@ -206,3 +206,11 @@ class Unit(bonsai.core.tool.Unit):
precision = 1e-5
decimal_places = 5
return str(round(precision * round(value / precision), decimal_places))
@classmethod
def get_icon_for_unit_class(cls, ifc_class: str) -> str:
if ifc_class == "IfcSIUnit":
return "SNAP_GRID"
elif ifc_class == "IfcMonetaryUnit":
return "COPY_ID"
return "MOD_MESHDEFORM"
@@ -38,14 +38,23 @@ REPO_PATH = r""
# Usually don't need to change, just ensure Blender version matches.
BLENDER_PATH = Path.home() / r"AppData/Roaming/Blender Foundation/Blender/4.4"
# BONSAI_PATH: Path to 'bonsai' extension folder inside BLENDER_PATH.
# Need to ensure extensions repo folder in the path below ('raw_githubusercontent_com') matches yours.
#
# Typical scenarios:
# - Bonsai is installed from Bonsai Unstalble Repo - use 'raw_githubusercontent_com' (as it is by default)
# - Bonsai is installed via offline installation - use 'user_default'
# - Bonsai is installed from Blender's official extensions platform - use 'blender_org'
BONSAI_PATH = BLENDER_PATH / r"extensions/raw_githubusercontent_com/bonsai"
# Determine BONSAI_PATH from existing options
def find_bonsai_path(blender_path):
candidates = [
blender_path / r"extensions/raw_githubusercontent_com/bonsai",
blender_path / r"extensions/user_default/bonsai",
blender_path / r"extensions/blender_org/bonsai",
]
for path in candidates:
if path.exists():
print(f"Found Bonsai at: {path}")
return path
raise FileNotFoundError("Could not find Bonsai path in expected locations.")
BONSAI_PATH = find_bonsai_path(BLENDER_PATH)
# ---------------------------
+5 -5
View File
@@ -247,8 +247,8 @@ Scenario: Add sheet
And I look at the "Sheets" panel
And I click "IMPORT"
When I click "ADD"
Then the file "{ifc_dir}/layouts/A00 - UNTITLED.svg" should contain "titleblocks/A1.svg"
And the file "{ifc_dir}/layouts/A00 - UNTITLED.svg" should not contain "GRID NORTH"
Then the file "{ifc_dir}/layouts/A01 - UNTITLED.svg" should contain "titleblocks/A1.svg"
And the file "{ifc_dir}/layouts/A01 - UNTITLED.svg" should not contain "GRID NORTH"
Scenario: Create sheet
Given an empty IFC project
@@ -258,8 +258,8 @@ Scenario: Create sheet
And I click "ADD"
And I select the "UNTITLED" item in the "BIM_UL_sheets" list
When I click "OUTPUT"
Then the file "{ifc_dir}/sheets/A00 - UNTITLED.svg" should not contain "titleblocks/A1.svg"
And the file "{ifc_dir}/sheets/A00 - UNTITLED.svg" should contain "GRID NORTH"
Then the file "{ifc_dir}/sheets/A01 - UNTITLED.svg" should not contain "titleblocks/A1.svg"
And the file "{ifc_dir}/sheets/A01 - UNTITLED.svg" should contain "GRID NORTH"
Scenario: Add drawing to sheet
Given an empty IFC project
@@ -310,4 +310,4 @@ Scenario: Create sheet - with a drawing added to it
And I press "bim.expand_sheet(sheet={sheet})"
And I click "IMAGE_PLANE"
When I click "OUTPUT"
Then the file "{ifc_dir}/sheets/A00 - UNTITLED.svg" should contain "IfcWall"
Then the file "{ifc_dir}/sheets/A01 - UNTITLED.svg" should contain "IfcWall"
@@ -19,6 +19,8 @@ Scenario: Changing the date
And I look at the "Solar Access / Shadow" panel
When I set the "Year" property to "2024"
And I set the "Date" property to "3"
And I set the "Latitude" property to "0"
And I set the "Longitude" property to "0"
Then I see "Sunrise: 06:00:31"
Scenario: Changing the time
@@ -26,6 +28,8 @@ Scenario: Changing the time
And I look at the "Solar Access / Shadow" panel
When I set the "Hour" property to "13"
And I set the "Minute" property to "30"
And I set the "Latitude" property to "0"
And I set the "Longitude" property to "0"
Then I see "Local Time: 13:30:00"
Scenario: Automatic timezone detection based on lat / long
+1 -1
View File
@@ -1561,7 +1561,7 @@ def the_file_name_should_contain_value(name, value):
@then(parsers.parse('the file "{name}" should not contain "{value}"'))
def the_file_name_should_contain_value(name, value):
def the_file_name_should_not_contain_value(name, value):
name = replace_variables(name)
with open(name, "r") as f:
content = f.read()
+2 -2
View File
@@ -332,11 +332,11 @@ class TestDisableEditingDrawings:
class TestAddDrawing:
def test_run(self, ifc, collector, drawing):
def test_run(self, ifc: Prophecy, collector: Prophecy, drawing: Prophecy):
drawing.generate_drawing_name("target_view", "location_hint").should_be_called().will_return("drawing_name")
drawing.ensure_unique_drawing_name("drawing_name").should_be_called().will_return("name")
drawing.generate_drawing_matrix("target_view", "location_hint").should_be_called().will_return("matrix")
drawing.create_camera("name", "matrix", "location_hint").should_be_called().will_return("obj")
drawing.create_camera("name", "matrix", "location_hint", "target_view").should_be_called().will_return("obj")
drawing.get_body_context().should_be_called().will_return("context")
drawing.run_root_assign_class(
obj="obj",
+1 -1
View File
@@ -68,7 +68,7 @@ class TestImportAttributes(test.bim.bootstrap.NewFile):
assert props.context_attributes["TargetScale"].float_value == 0.5
assert props.context_attributes["TargetView"].enum_value == "NOTDEFINED"
assert props.context_attributes["UserDefinedTargetView"].string_value == "UserDefinedTargetView"
assert not props.context_attributes["Precision"]
assert "Precision" not in props.context_attributes
def test_importing_twice(self):
ifc = ifcopenshell.file()
+4 -4
View File
@@ -492,7 +492,7 @@ class TestGenerateSheetIdentification(NewFile):
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
assert subject.generate_sheet_identification() == "A01"
document = ifc.createIfcDocumentInformation()
document = ifc.createIfcDocumentInformation(Scope="SHEET")
assert subject.generate_sheet_identification() == "A02"
@@ -724,7 +724,7 @@ class TestDrawingMaintainingSheetPosition(NewFile):
props = tool.Drawing.get_document_props()
bpy.ops.bim.create_project()
ifc = tool.Ifc.get()
sheet_path = Path.cwd() / "layouts" / "A00 - UNTITLED.svg"
sheet_path = Path.cwd() / "layouts" / "A01 - UNTITLED.svg"
bpy.ops.mesh.primitive_cube_add(size=10, location=(0, 0, 4))
obj = bpy.data.objects["Cube"]
@@ -832,7 +832,7 @@ class TestUpdateTextValue(NewFile):
bpy.ops.bim.edit_text()
annotation_classes = ifcopenshell.util.element.get_pset(tool.Ifc.get_entity(obj), "EPset_Annotation", "Classes")
assert "title" in annotation_classes
assert DecoratorData.get_ifc_text_data(obj)["FontSize"] == 7.0
assert DecoratorData.get_text_data(obj)["FontSize"] == 7.0
def test_add_second_literal(self, setup=True):
if setup:
@@ -869,7 +869,7 @@ class TestUpdateTextValue(NewFile):
annotation_classes = ifcopenshell.util.element.get_pset(tool.Ifc.get_entity(obj), "EPset_Annotation", "Classes")
assert props.font_size == "7.0"
assert "title" in annotation_classes
assert DecoratorData.get_ifc_text_data(obj)["FontSize"] == 7.0
assert DecoratorData.get_text_data(obj)["FontSize"] == 7.0
# test second literal is present
assert props.literals[1].attributes["Literal"].string_value == "test_value"
@@ -22,7 +22,7 @@ from typing import Optional
def unassign_unit(file: ifcopenshell.file, units: Optional[list[ifcopenshell.entity_instance]] = None) -> None:
"""Unassigns units as default units for the project
:param units: A list of units to assign as project defaults.
:param units: A list of units to unassign as project defaults.
:return: None
Example:
@@ -31,16 +31,6 @@
"predefined_type": "TOPPING"
}
],
"IfcDistributionElement": [
{
"name": "Furnace"
}
],
"IfcDistributionElementType": [
{
"name": "Furnace"
}
],
"IfcElectricDistributionBoard": [
{
"name": "Circuit Breaker Panel",
@@ -140,6 +130,41 @@
"IfcUnitaryEquipment": [
{
"name": "Fan Coil Unit"
},
{
"name": "Roof Top Unit (RTU)",
"predefined_type": "ROOFTOPUNIT"
},
{
"name": "Furnace"
},
{
"name": "Central air conditioner (split or package)",
"predefined_type": "AIRCONDITIONINGUNIT"
},
{
"name": "Mini-split system",
"predefined_type": "SPLITSYSTEM"
}
],
"IfcUnitaryEquipmentType": [
{
"name": "Fan Coil Unit"
},
{
"name": "Roof Top Unit (RTU)",
"predefined_type": "ROOFTOPUNIT"
},
{
"name": "Furnace"
},
{
"name": "Central air conditioner (split or package)",
"predefined_type": "AIRCONDITIONINGUNIT"
},
{
"name": "Mini-split system",
"predefined_type": "SPLITSYSTEM"
}
],
"IfcWall": [
@@ -695,12 +695,14 @@ def calculate_unit_scale(ifc_file: ifcopenshell.file, unit_type: str = "LENGTHUN
if not (projects := ifc_file.by_type("IfcProject")) or not (units := projects[0].UnitsInContext):
return 1
unit_scale = 1
unit: ifcopenshell.entity_instance
for unit in units.Units:
if not hasattr(unit, "UnitType") or unit.UnitType != unit_type:
if getattr(unit, "UnitType", ...) != unit_type:
continue
while unit.is_a("IfcConversionBasedUnit"):
unit_scale *= unit.ConversionFactor.ValueComponent.wrappedValue
unit = unit.ConversionFactor.UnitComponent
conversion_factor = unit.ConversionFactor
unit_scale *= conversion_factor.ValueComponent.wrappedValue
unit = conversion_factor.UnitComponent
if unit.is_a("IfcSIUnit"):
unit_scale *= get_prefix_multiplier(unit.Prefix)
return unit_scale
@@ -694,7 +694,7 @@ def validate_ifc_applications(f: ifcopenshell.file, logger: Union[Logger, json_l
app_name: tuple[str, str] = (inst.ApplicationFullName, inst.Version)
app_id: str = inst.ApplicationIdentifier
if app_name is not None:
if all(x is not None for x in app_name):
if app_name not in used_names:
used_names[app_name] = inst
else: