mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-11 02:02:22 +00:00
Merge branch 'IfcOpenShell:v0.8.0' into fix-5563
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -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°51′54.51″S 151°12′35.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
|
||||
|
||||
@@ -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"}
|
||||
|
||||
|
||||
|
||||
@@ -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"]:
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
# ---------------------------
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user