From 0e8eeb18ac1aa5dbf5f7687c928b97b508b4465f Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 17 Dec 2024 15:55:02 +0500 Subject: [PATCH] shape_builder - remove mathutils dependency completely #5192 --- src/bonsai/bonsai/bim/module/model/door.py | 67 +++++++------- src/bonsai/bonsai/bim/module/model/railing.py | 6 +- src/bonsai/bonsai/bim/module/model/window.py | 19 ++-- src/bonsai/bonsai/tool/blender.py | 5 ++ src/bonsai/bonsai/tool/model.py | 41 +++++---- .../scripts/generate_furniture_library.py | 88 ++++++++++++------- .../scripts/generate_landscape_library.py | 2 +- .../generate_steel_profiles_library.py | 20 ++--- src/bonsai/test/tool/test_loader.py | 2 +- .../ifcopenshell/util/shape_builder.py | 36 ++++++-- .../test/test_create_shape.py | 2 +- 11 files changed, 170 insertions(+), 118 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/model/door.py b/src/bonsai/bonsai/bim/module/model/door.py index 786ac2c90f..04cffa9782 100644 --- a/src/bonsai/bonsai/bim/module/model/door.py +++ b/src/bonsai/bonsai/bim/module/model/door.py @@ -27,7 +27,6 @@ import ifcopenshell.util.element import ifcopenshell.util.representation import ifcopenshell.util.schema import ifcopenshell.util.unit -from ifcopenshell.util.shape_builder import V import bonsai.tool as tool import bonsai.core.geometry import bonsai.core.geometry as core @@ -40,6 +39,8 @@ from typing import Union, Any, Optional import json import collections +V_ = tool.Blender.V_ + def update_door_modifier_representation(context: bpy.types.Context) -> None: obj = context.active_object @@ -188,15 +189,15 @@ def bm_sort_out_geom( def bm_mirror( bm: bmesh.types.BMesh, verts: list[bmesh.types.BMVert], - mirror_axes: Vector = V(1, 0, 0).freeze(), - mirror_point: Vector = V(0, 0, 0).freeze(), + mirror_axes: Vector = V_(1, 0, 0).freeze(), + mirror_point: Vector = V_(0, 0, 0).freeze(), create_copy: bool = False, ) -> list[bmesh.types.BMVert]: matrix = Matrix.Translation(mirror_point) for i, v in enumerate(mirror_axes): if not v: continue - mirror_axis = V(0, 0, 0) + mirror_axis = V_(0, 0, 0) mirror_axis[i] = 1.0 matrix = matrix @ Matrix.Scale(-1, 4, mirror_axis) matrix = matrix @ Matrix.Translation(-mirror_point) @@ -219,9 +220,9 @@ def create_bm_extruded_profile( points: list[Vector], edges: Optional[list[tuple[int, int]]] = None, faces: Optional[list[list[int]]] = None, - position: Vector = V(0, 0, 0).freeze(), + position: Vector = V_(0, 0, 0).freeze(), magnitude: float = 1.0, - extrusion_vector: Vector = V(0, 0, 1).freeze(), + extrusion_vector: Vector = V_(0, 0, 1).freeze(), ) -> list[bmesh.types.BMVert]: bm.verts.index_update() bm.edges.index_update() @@ -250,7 +251,7 @@ def create_bm_extruded_profile( def create_bm_door_lining( - bm: bmesh.types.BMesh, size: Vector, thickness: list, position: Vector = V(0, 0, 0).freeze() + bm: bmesh.types.BMesh, size: Vector, thickness: list, position: Vector = V_(0, 0, 0).freeze() ) -> list[bmesh.types.BMVert]: """`thickness` of the profile is defined as list in the following order: `(SIDE, TOP)` @@ -363,9 +364,9 @@ def update_door_modifier_bmesh(context: bpy.types.Context) -> None: glass_thickness = 0.01 # handle dimensions (hardcoded) - handle_size = V(120, 40, 20) * 0.001 - handle_offset = V(60, 0, 1000) * 0.001 # to the handle center - handle_center_offset = V(handle_size.y / 2, 0, handle_size.z) / 2 + handle_size = V_(120, 40, 20) * 0.001 + handle_offset = V_(60, 0, 1000) * 0.001 # to the handle center + handle_center_offset = V_(handle_size.y / 2, 0, handle_size.z) / 2 if transfom_offset: panel_height = transfom_offset + transom_thickness - lining_to_panel_offset_x - threshold_thickness @@ -377,7 +378,7 @@ def update_door_modifier_bmesh(context: bpy.types.Context) -> None: bm = bmesh.new() # add lining - lining_size = V(overall_width, lining_depth, lining_height) + lining_size = V_(overall_width, lining_depth, lining_height) lining_thickness = [side_lining_thickness, top_lining_thickness] lining_verts = create_bm_door_lining(bm, lining_size, lining_thickness) @@ -385,8 +386,8 @@ def update_door_modifier_bmesh(context: bpy.types.Context) -> None: if not threshold_thickness: threshold_verts = [] else: - threshold_size = V(threshold_width, threshold_depth, threshold_thickness) - threshold_position = V(side_lining_thickness, threshold_offset, 0) + threshold_size = V_(threshold_width, threshold_depth, threshold_thickness) + threshold_position = V_(side_lining_thickness, threshold_offset, 0) threshold_verts = create_bm_box(bm, threshold_size, threshold_position) # add casings @@ -400,12 +401,12 @@ def update_door_modifier_bmesh(context: bpy.types.Context) -> None: ] outer_casing_thickness = inner_casing_thickness.copy() if double_swing_door else casing_thickness - casing_size = V(overall_width + casing_wall_overlap * 2, casing_depth, overall_height + casing_wall_overlap) - casing_position = V(-casing_wall_overlap, -casing_depth, 0) + casing_size = V_(overall_width + casing_wall_overlap * 2, casing_depth, overall_height + casing_wall_overlap) + casing_position = V_(-casing_wall_overlap, -casing_depth, 0) outer_casing_verts = create_bm_door_lining(bm, casing_size, outer_casing_thickness, casing_position) casing_verts.extend(outer_casing_verts) - inner_casing_position = V(-casing_wall_overlap, lining_depth, 0) + inner_casing_position = V_(-casing_wall_overlap, lining_depth, 0) inner_casing_verts = create_bm_door_lining(bm, casing_size, inner_casing_thickness, inner_casing_position) casing_verts.extend(inner_casing_verts) @@ -417,12 +418,12 @@ def update_door_modifier_bmesh(context: bpy.types.Context) -> None: door_verts.extend(create_bm_box(bm, panel_size, panel_position)) # add door handle handle_points = [ - V(0, 0, 0), - V(0, -handle_size.y, 0), - V(handle_size.x, -handle_size.y, 0), - V(handle_size.x, -handle_size.y / 2, 0), - V(handle_size.y / 2, -handle_size.y / 2, 0), - V(handle_size.y / 2, 0, 0), + V_(0, 0, 0), + V_(0, -handle_size.y, 0), + V_(handle_size.x, -handle_size.y, 0), + V_(handle_size.x, -handle_size.y / 2, 0), + V_(handle_size.y / 2, -handle_size.y / 2, 0), + V_(handle_size.y / 2, 0, 0), ] handle_position = panel_position + handle_offset - handle_center_offset door_handle_verts = create_bm_extruded_profile( @@ -432,22 +433,22 @@ def update_door_modifier_bmesh(context: bpy.types.Context) -> None: if door_swing_type == "LEFT": bm_mirror( - bm, door_handle_verts, mirror_axes=V(1, 0, 0), mirror_point=panel_position + V(panel_size.x / 2, 0, 0) + bm, door_handle_verts, mirror_axes=V_(1, 0, 0), mirror_point=panel_position + V_(panel_size.x / 2, 0, 0) ) door_handle_mirrored_verts = bm_mirror( bm, door_handle_verts, - mirror_axes=V(0, 1, 0), - mirror_point=handle_position + V(0, panel_size.y / 2, 0), + mirror_axes=V_(0, 1, 0), + mirror_point=handle_position + V_(0, panel_size.y / 2, 0), create_copy=True, ) door_verts.extend(door_handle_mirrored_verts) return door_verts door_verts = [] - panel_size = V(panel_width, panel_depth, panel_height) - panel_position = V(lining_to_panel_offset_x, lining_to_panel_offset_y, threshold_thickness) + panel_size = V_(panel_width, panel_depth, panel_height) + panel_position = V_(lining_to_panel_offset_x, lining_to_panel_offset_y, threshold_thickness) if double_door: # keeping a little space between doors for readibility @@ -455,8 +456,8 @@ def update_door_modifier_bmesh(context: bpy.types.Context) -> None: panel_size.x = panel_size.x / 2 - double_door_offset door_verts.extend(create_bm_door_panel(panel_size, panel_position, "LEFT")) - mirror_point = panel_position + V(door_opening_width / 2, 0, 0) - door_verts.extend(bm_mirror(bm, door_verts, V(1, 0, 0), mirror_point, create_copy=True)) + mirror_point = panel_position + V_(door_opening_width / 2, 0, 0) + door_verts.extend(bm_mirror(bm, door_verts, V_(1, 0, 0), mirror_point, create_copy=True)) else: door_swing_type = "LEFT" if door_type.endswith("LEFT") else "RIGHT" door_verts.extend(create_bm_door_panel(panel_size, panel_position, door_swing_type)) @@ -474,9 +475,9 @@ def update_door_modifier_bmesh(context: bpy.types.Context) -> None: transom_thickness, ] window_lining_thickness.append(transom_thickness) - window_lining_size = V(overall_width, lining_depth, window_lining_height) - window_position = V(0, 0, overall_height - window_lining_height) - frame_size = V(door_opening_width, frame_depth, frame_height) + window_lining_size = V_(overall_width, lining_depth, window_lining_height) + window_position = V_(0, 0, overall_height - window_lining_height) + frame_size = V_(door_opening_width, frame_depth, frame_height) window_lining_verts, frame_verts, glass_verts = create_bm_window( bm, window_lining_size, @@ -490,7 +491,7 @@ def update_door_modifier_bmesh(context: bpy.types.Context) -> None: ) lining_offset_verts = lining_verts + door_verts + window_lining_verts + frame_verts + glass_verts - bmesh.ops.translate(bm, vec=V(0, lining_offset, 0), verts=lining_offset_verts) + bmesh.ops.translate(bm, vec=V_(0, lining_offset, 0), verts=lining_offset_verts) bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.0001) if bpy.context.active_object.mode == "EDIT": diff --git a/src/bonsai/bonsai/bim/module/model/railing.py b/src/bonsai/bonsai/bim/module/model/railing.py index 4eb891613b..2ec1a2778d 100644 --- a/src/bonsai/bonsai/bim/module/model/railing.py +++ b/src/bonsai/bonsai/bim/module/model/railing.py @@ -23,7 +23,6 @@ import ifcopenshell import ifcopenshell.api import ifcopenshell.util.representation import ifcopenshell.util.unit -from ifcopenshell.util.shape_builder import V import bonsai.core.root import bonsai.core.geometry import bonsai.tool as tool @@ -125,6 +124,7 @@ def update_railing_modifier_bmesh(context: bpy.types.Context) -> None: """ obj = context.active_object props = obj.BIMRailingProperties + V_ = tool.Blender.V_ # NOTE: using Data since bmesh update will hapen very often if not RailingData.is_loaded: @@ -171,8 +171,8 @@ def update_railing_modifier_bmesh(context: bpy.types.Context) -> None: v0, v1 = main_edge.verts edge_dissolving_verts.extend([v0, v1]) - edge_dir = ((v1.co - v0.co) * V(1, 1, 0)).normalized() - ortho_vector = edge_dir.cross(V(0, 0, 1)) + edge_dir = ((v1.co - v0.co) * V_(1, 1, 0)).normalized() + ortho_vector = edge_dir.cross(V_(0, 0, 1)) extruded_geom = bmesh.ops.extrude_edge_only(bm, edges=[main_edge])["geom"] extruded_verts = bm_sort_out_geom(extruded_geom)["verts"] diff --git a/src/bonsai/bonsai/bim/module/model/window.py b/src/bonsai/bonsai/bim/module/model/window.py index 3621d62c35..852c34d14a 100644 --- a/src/bonsai/bonsai/bim/module/model/window.py +++ b/src/bonsai/bonsai/bim/module/model/window.py @@ -31,11 +31,12 @@ import ifcopenshell.util.element import ifcopenshell.util.representation import ifcopenshell.util.shape_builder import ifcopenshell.util.unit -from ifcopenshell.util.shape_builder import V from bmesh.types import BMVert from mathutils import Vector from typing import Optional +V_ = tool.Blender.V_ + def update_window_modifier_representation(context: bpy.types.Context) -> None: obj = context.active_object @@ -131,7 +132,7 @@ def update_window_modifier_representation(context: bpy.types.Context) -> None: def create_bm_window_frame( - bm: bmesh.types.BMesh, size: Vector, thickness: list, position: Vector = V(0, 0, 0).freeze() + bm: bmesh.types.BMesh, size: Vector, thickness: list, position: Vector = V_(0, 0, 0).freeze() ) -> list[bmesh.types.BMVert]: """`thickness` of the profile is defined as list in the following order: `(LEFT, TOP, RIGHT, BOTTOM)` @@ -198,7 +199,7 @@ def create_bm_window_frame( def create_bm_box( - bm: bmesh.types.BMesh, size: Vector = V(1, 1, 1).freeze(), position: Vector = V(0, 0, 0).freeze() + bm: bmesh.types.BMesh, size: Vector = V_(1, 1, 1).freeze(), position: Vector = V_(0, 0, 0).freeze() ) -> list[bmesh.types.BMVert]: """create a box of `size`, position box first vertex at `position`""" box_verts = bmesh.ops.create_cube(bm, size=1)["verts"] @@ -230,13 +231,13 @@ def create_bm_window( window_lining_verts = create_bm_window_frame(bm, lining_size, lining_thickness) # window frame - frame_position = V(x_offsets[0], lining_to_panel_offset_y_full, x_offsets[3]) + frame_position = V_(x_offsets[0], lining_to_panel_offset_y_full, x_offsets[3]) frame_verts = create_bm_window_frame(bm, frame_size, frame_thickness, frame_position) # window glass - glass_size = frame_size - V(frame_thickness * 2, 0, frame_thickness * 2) + glass_size = frame_size - V_(frame_thickness * 2, 0, frame_thickness * 2) glass_size.y = glass_thickness - glass_position = frame_position + V(frame_thickness, frame_size.y / 2 - glass_thickness / 2, frame_thickness) + glass_position = frame_position + V_(frame_thickness, frame_size.y / 2 - glass_thickness / 2, frame_thickness) glass_verts = create_bm_box(bm, glass_size, glass_position) @@ -324,7 +325,7 @@ def update_window_modifier_bmesh(context: bpy.types.Context) -> None: frame_thickness = props.frame_thickness[panel_i] lining_to_panel_offset_y_full = (lining_depth - frame_depth) + lining_to_panel_offset_y # add window - window_lining_size = V( + window_lining_size = V_( panel_width, lining_depth, panel_height, @@ -358,7 +359,7 @@ def update_window_modifier_bmesh(context: bpy.types.Context) -> None: frame_size.x -= x_offsets[0] + x_offsets[2] frame_size.z -= x_offsets[1] + x_offsets[3] - window_position = V(accumulated_width, 0, accumulated_height[column_i]) + window_position = V_(accumulated_width, 0, accumulated_height[column_i]) lining_verts, panel_verts, glass_verts = create_bm_window( bm, window_lining_size, @@ -377,7 +378,7 @@ def update_window_modifier_bmesh(context: bpy.types.Context) -> None: accumulated_height[column_i] += panel_height accumulated_width += panel_width - bmesh.ops.translate(bm, vec=V(0, lining_offset, 0), verts=bm.verts) + bmesh.ops.translate(bm, vec=V_(0, lining_offset, 0), verts=bm.verts) bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.0001) if bpy.context.active_object.mode == "EDIT": diff --git a/src/bonsai/bonsai/tool/blender.py b/src/bonsai/bonsai/tool/blender.py index b5878096d2..0d4bdbb7db 100644 --- a/src/bonsai/bonsai/tool/blender.py +++ b/src/bonsai/bonsai/tool/blender.py @@ -1295,3 +1295,8 @@ class Blender(bonsai.core.tool.Blender): if len(blender_binary_path.parents) > 3 and blender_binary_path.parents[2].name == "WindowsApps": return blender_binary_path.parents[1].name.rsplit("__", 1)[-1] return None + + @classmethod + def V_(cls, *args: float) -> Vector: + """Just a shortcut for creating mathutils Vector.""" + return Vector(args) diff --git a/src/bonsai/bonsai/tool/model.py b/src/bonsai/bonsai/tool/model.py index de4cedaa0a..6e309ed7af 100644 --- a/src/bonsai/bonsai/tool/model.py +++ b/src/bonsai/bonsai/tool/model.py @@ -39,13 +39,16 @@ from mathutils import Matrix, Vector from copy import deepcopy from functools import partial from bonsai.bim import import_ifc + +# TODO: This line is somehow keeping the world from falling apart with a circular import error. from bonsai.bim.module.geometry.helper import Helper from bonsai.bim.module.model.data import AuthoringData, RailingData, RoofData, WindowData, DoorData from bonsai.bim.module.model.opening import FilledOpeningGenerator -from ifcopenshell.util.shape_builder import V, ShapeBuilder +from ifcopenshell.util.shape_builder import ShapeBuilder from typing import Optional, Union, TypeVar, Any, Iterable, Literal T = TypeVar("T") +V_ = tool.Blender.V_ class Model(bonsai.core.tool.Model): @@ -1122,7 +1125,7 @@ class Model(bonsai.core.tool.Model): custom_tread_run = any(run != 0 for run in custom_first_last_tread_run) nosing_overlap = max(nosing_length, 0) nosing_tread_gap = -min(nosing_length, 0) - nosing_overlap_offset = -V(nosing_overlap, 0) + nosing_overlap_offset = -V_(nosing_overlap, 0) def define_generic_stair_treads(): vertices.append(Vector([0, 0])) @@ -1134,16 +1137,16 @@ class Model(bonsai.core.tool.Model): default_tread_edges = np.array(((0, 1), (1, 2))) # horizontal tread line if nosing_overlap == 0: - default_tread_verts = (V(0, tread_rise), V(tread_run, tread_rise)) + default_tread_verts = (V_(0, tread_rise), V_(tread_run, tread_rise)) elif nosing_depth == 0: - default_tread_verts = (V(-nosing_overlap, tread_rise), V(tread_run, tread_rise)) + default_tread_verts = (V_(-nosing_overlap, tread_rise), V_(tread_run, tread_rise)) else: # nosing_overlap > 0 nosing_depth > 0 # kind of L shape default_tread_verts = ( - V(0, tread_rise - nosing_depth), - V(-nosing_overlap, tread_rise - nosing_depth), - V(-nosing_overlap, tread_rise), - V(tread_run, tread_rise), + V_(0, tread_rise - nosing_depth), + V_(-nosing_overlap, tread_rise - nosing_depth), + V_(-nosing_overlap, tread_rise), + V_(tread_run, tread_rise), ) add_edges = ((2, 3), (3, 4)) default_tread_edges = np.concatenate((default_tread_edges, add_edges)) @@ -1166,7 +1169,7 @@ class Model(bonsai.core.tool.Model): return default_tread_offset, default_tread_verts # treads - current_offset = V(0, 0) + current_offset = V_(0, 0) for i in range(number_of_risers): last_vert_i = len(vertices) - 1 tread_offset, tread_verts = get_tread_data(i) @@ -1178,9 +1181,9 @@ class Model(bonsai.core.tool.Model): if stair_type == "WOOD/STEEL": builder = ShapeBuilder(None) # full tread rectangle - get_tread_verts = partial(builder.get_rectangle_coords, position=V(0, -(tread_depth - tread_rise))) - default_tread_verts = get_tread_verts(size=V(tread_run + nosing_overlap, tread_depth)) - default_tread_offset = V(tread_run + nosing_tread_gap, tread_rise) + get_tread_verts = partial(builder.get_rectangle_coords, position=V_(0, -(tread_depth - tread_rise))) + default_tread_verts = get_tread_verts(size=V_(tread_run + nosing_overlap, tread_depth)) + default_tread_offset = V_(tread_run + nosing_tread_gap, tread_rise) def get_tread_data(i): if custom_tread_run: @@ -1193,12 +1196,12 @@ class Model(bonsai.core.tool.Model): if current_tread_run: tread_offset = default_tread_offset.copy() tread_offset.x = current_tread_run + nosing_tread_gap - tread_verts = get_tread_verts(size=V(current_tread_run + nosing_overlap, tread_depth)) + tread_verts = get_tread_verts(size=V_(current_tread_run + nosing_overlap, tread_depth)) return tread_offset, tread_verts return default_tread_offset, default_tread_verts # each tread is a separate shape - cur_offset = V(0, 0) + cur_offset = V_(0, 0) for i in range(number_of_risers): tread_offset, tread_verts = get_tread_data(i) cur_trade_shape = [v + cur_offset + nosing_overlap_offset for v in tread_verts] @@ -1219,7 +1222,7 @@ class Model(bonsai.core.tool.Model): # close the shape last_vert_i = len(vertices) - vertices.append(vertices[-1] * V(1, 0)) + vertices.append(vertices[-1] * V_(1, 0)) edges.extend([(last_vert_i - 1, last_vert_i), (last_vert_i, 0)]) # flip edges direction for ccw polygon winding order @@ -1233,15 +1236,15 @@ class Model(bonsai.core.tool.Model): # from the tread diagonal line # we're going it define that line, sample it and abrupt it in case it meets a slab # graph: https://www.desmos.com/calculator/bilmnti3cp - tread_diagonal_dir = V(tread_run, tread_rise).normalized() + tread_diagonal_dir = V_(tread_run, tread_rise).normalized() # td_vector is clockwise orthogonal vector - td_vector = tread_diagonal_dir.yx * V(1, -1) * tread_depth + td_vector = tread_diagonal_dir.yx * V_(1, -1) * tread_depth stair_tan = tread_rise / tread_run # s0 is just a sampled point from the bottom line # we stick to the third point as the first point # is affected by customized tread run - s0 = V(custom_first_last_tread_run[0] or tread_run, tread_rise) + td_vector + s0 = V_(custom_first_last_tread_run[0] or tread_run, tread_rise) + td_vector # comes from y = stair_tan * x + b b = s0.y - stair_tan * s0.x @@ -1250,7 +1253,7 @@ class Model(bonsai.core.tool.Model): y = stair_tan * x + b elif x is None: x = (y - b) / stair_tan - return V(x, y) + return V_(x, y) # top nib last_vert = vertices[-1] diff --git a/src/bonsai/scripts/generate_furniture_library.py b/src/bonsai/scripts/generate_furniture_library.py index 0ca842cb88..7fd8cf4a81 100644 --- a/src/bonsai/scripts/generate_furniture_library.py +++ b/src/bonsai/scripts/generate_furniture_library.py @@ -16,18 +16,24 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . +import numpy as np import ifcopenshell import ifcopenshell.api +import ifcopenshell.util.element from math import cos, tan, pi from pathlib import Path from itertools import chain -from ifcopenshell.util.shape_builder import ShapeBuilder, V +from ifcopenshell.util.shape_builder import ShapeBuilder, V, np_to_3d, np_normalized +from typing import Optional, Union + +GeneratorOutput = Union[list[ifcopenshell.entity_instance], list[list[ifcopenshell.entity_instance]]] class LibraryGenerator: def generate(self, library_name, output_filename="IFC4 EU Steel.ifc"): ifcopenshell.api.pre_listeners = {} ifcopenshell.api.post_listeners = {} + np_X, np_Y, np_Z = 0, 1, 2 self.materials = {} @@ -65,10 +71,16 @@ class LibraryGenerator: builder = ShapeBuilder(self.file) - def create_box_objects(width, depth, height, shift_to_center=False, return_representations=False): + def create_box_objects( + width: float, + depth: float, + height: float, + shift_to_center: bool = False, + return_representations: bool = False, + ) -> Union[list[ifcopenshell.entity_instance], list[list[ifcopenshell.entity_instance]]]: output = [] - rectangle = builder.rectangle(size=V(width, depth)) + rectangle = builder.rectangle(size=(width, depth)) box = builder.extrude(builder.profile(rectangle), height) if return_representations: @@ -86,18 +98,20 @@ class LibraryGenerator: if shift_to_center: shift_to_center = V(-width / 2, -depth / 2) - builder.translate(output[0], shift_to_center.to_3d()) + builder.translate(output[0], np_to_3d(shift_to_center)) builder.translate(output[1], shift_to_center) return output - def create_fillet_rectangle(size=None, position=None, fillet_radius=50.0): + def create_fillet_rectangle( + size: Optional[np.ndarray] = None, position: Optional[np.ndarray] = None, fillet_radius: float = 50.0 + ) -> ifcopenshell.entity_instance: """`fillet_radius` is either float or list of floats for each corner in counter-clockwise order starting from bottom left""" kwargs = dict() - if size: + if size is not None: kwargs["size"] = size - if position: + if position is not None: kwargs["position"] = position _, _, rectangle = builder.get_simple_2dcurve_data( @@ -191,11 +205,13 @@ class LibraryGenerator: ) # chairs - def create_fancy_chair(width, depth, height, seat_level, return_representations=False): + def create_fancy_chair( + width: float, depth: float, height: float, seat_level: float, return_representations: bool = False + ) -> GeneratorOutput: thickness = 50.0 shift_to_center = V(-width / 2, 0) mirror_axis = V(0, 1) - output = [] + output: GeneratorOutput = [] items_to_adjust = [] items_3d = [] @@ -292,7 +308,9 @@ class LibraryGenerator: "IfcFurnitureType", "Generic Small Bedside Table", representation_3d, representation_2d ) - def create_fancy_rectangle_table(width, depth, height, return_representations=False): + def create_fancy_rectangle_table( + width: float, depth: float, height: float, return_representations=False + ) -> GeneratorOutput: output = [] leg_size = 50.0 countertop_thickness = 50.0 @@ -312,7 +330,7 @@ class LibraryGenerator: legs_profiles = [builder.profile(leg) for leg in legs_curves] legs = [builder.extrude(leg, height - countertop_thickness) for leg in legs_profiles] items = [countertop] + legs - builder.translate(items, shift_to_center.to_3d()) + builder.translate(items, np_to_3d(shift_to_center)) if return_representations: representation_3d = builder.get_representation(context=self.representations["model_body"], items=items) @@ -336,17 +354,17 @@ class LibraryGenerator: # tables def create_rectangle_table_with_chairs( - table_width, - table_depth, - table_height, - chair_width, - chair_depth, - chair_height, - seat_level, - table_chair_gap, - number_of_seats, - side_seats=False, - ): + table_width: float, + table_depth: float, + table_height: float, + chair_width: float, + chair_depth: float, + chair_height: float, + seat_level: float, + table_chair_gap: float, + number_of_seats: int, + side_seats: bool = False, + ) -> tuple[ifcopenshell.entity_instance, ifcopenshell.entity_instance]: items_table_3d, items_table_2d = create_fancy_rectangle_table(table_width, table_depth, table_height) items_chair_3d, items_chair_2d = create_fancy_chair(chair_width, chair_depth, chair_height, seat_level) @@ -860,7 +878,7 @@ class LibraryGenerator: seat_back = builder.extrude(seat_back, height) items = armrests + seats + [seat_back] - builder.translate(items, shift_to_center.to_3d()) + builder.translate(items, np_to_3d(shift_to_center)) # builder.mirror(items, mirror_axis) representation_3d = builder.get_representation(self.representations["model_body"], items=items) @@ -1103,7 +1121,7 @@ class LibraryGenerator: ) items_3d_to_mirror.append(cistern_3d) - builder.translate(items_3d, shift_to_center.to_3d()) + builder.translate(items_3d, np_to_3d(shift_to_center)) builder.mirror(items_3d_to_mirror, mirror_axes=mirror_axis) items_3d += items_3d_to_mirror @@ -1167,7 +1185,7 @@ class LibraryGenerator: mask_curve_profile, bottom_mask_height, position=V(0, 0, bottom_height) ) - builder.translate(items_3d_to_center, shift_to_center.to_3d()) + builder.translate(items_3d_to_center, np_to_3d(shift_to_center)) items_3d = items_3d_to_center + [triangle_bottom_extruded, triangle_bottom_wall_mask] builder.mirror(items_3d, mirror_axis) @@ -1263,7 +1281,7 @@ class LibraryGenerator: basin_downside = builder.extrude(rectangle_second_3d, basin_downside_thickness) items_3d = [main_basin, basin_downside] - builder.translate(items_3d, shift_to_center.to_3d()) + builder.translate(items_3d, np_to_3d(shift_to_center)) representation_3d = builder.get_representation(self.representations["model_body"], items_3d) return representation_3d, representation_2d @@ -1424,19 +1442,21 @@ class LibraryGenerator: "IfcFurnitureType", "Neufert Commercial Office Desk and Chair", representation_3d, representation_2d ) - def create_dishwasher(width, depth, height): + def create_dishwasher( + width: float, depth: float, height: float + ) -> tuple[ifcopenshell.entity_instance, ifcopenshell.entity_instance]: items_3d, items_2d = create_box_objects(width, depth, height) circle_first_r = width * 0.1 circle_second_r = width * 0.3 center = V(width / 2, depth / 2) - k = center.y / center.x + k = center[np_Y] / center[np_X] circle_first = builder.circle(center=center, radius=circle_second_r) circle_second = builder.circle(center=center, radius=circle_first_r) - polyline = builder.polyline([(0.0, 0.0), center - center.normalized() * circle_second_r]) + polyline = builder.polyline([(0.0, 0.0), center - np_normalized(center) * circle_second_r]) mirrored_polylines = builder.mirror( - polyline, mirror_axes=[V(1, 0), V(0, 1), V(1, 1)], mirror_point=center, create_copy=True + polyline, mirror_axes=[(1, 0), (0, 1), (1, 1)], mirror_point=center, create_copy=True ) items_2d.extend([circle_first, circle_second, polyline] + mirrored_polylines) @@ -1591,13 +1611,13 @@ class LibraryGenerator: position=V(rect_offset / 2, rect_offset / 2) + sink_offset, ) - circle_position = sink_offset + V((width - sink_offset.x) / 2, depth - rect_offset * 2 - sink_radius) + circle_position = sink_offset + V((width - sink_offset[np_X]) / 2, depth - rect_offset * 2 - sink_radius) sink_circle = builder.circle(circle_position, radius=sink_radius) faucet = builder.polyline( ( - ((width - sink_offset.x) / 2, faucet_depth), - ((width - sink_offset.x) / 2, faucet_depth - faucet_length), + ((width - sink_offset[np_X]) / 2, faucet_depth), + ((width - sink_offset[np_X]) / 2, faucet_depth - faucet_length), ) ) builder.translate(faucet, sink_offset) @@ -1626,7 +1646,7 @@ class LibraryGenerator: sink_table_bottom = builder.extrude(sink_table, height - sink_height) items_3d.append(sink_table_bottom) - builder.translate(items_3d, shift_to_center.to_3d()) + builder.translate(items_3d, np_to_3d(shift_to_center)) representation_2d = builder.get_representation(self.representations["plan_body"], items=items_2d) representation_3d = builder.get_representation(self.representations["model_body"], items=items_3d) diff --git a/src/bonsai/scripts/generate_landscape_library.py b/src/bonsai/scripts/generate_landscape_library.py index 665e0fc13e..381e517d92 100644 --- a/src/bonsai/scripts/generate_landscape_library.py +++ b/src/bonsai/scripts/generate_landscape_library.py @@ -26,7 +26,7 @@ import bonsai.tool as tool from math import cos, sin, tan, pi from pathlib import Path from itertools import chain -from ifcopenshell.util.shape_builder import ShapeBuilder, V +from ifcopenshell.util.shape_builder import ShapeBuilder from collections import namedtuple from mathutils import Vector, Matrix from random import uniform diff --git a/src/bonsai/scripts/generate_steel_profiles_library.py b/src/bonsai/scripts/generate_steel_profiles_library.py index 1156a56e43..a20f26e268 100644 --- a/src/bonsai/scripts/generate_steel_profiles_library.py +++ b/src/bonsai/scripts/generate_steel_profiles_library.py @@ -24,7 +24,7 @@ import ifcopenshell.api import boltspy as bolts from math import cos, pi from pathlib import Path -from ifcopenshell.util.shape_builder import ShapeBuilder, V +from ifcopenshell.util.shape_builder import ShapeBuilder, V, ifc_safe_vector_type class LibraryGenerator: @@ -201,20 +201,20 @@ class LibraryGenerator: if mode == "LLBB": offset = profile.Depth/2 + profiles_gap if mirrored: - transform.Axis1 = self.file.createIfcDirection(V(0, 1)) - transform.Axis2 = self.file.createIfcDirection(V(-1, 0)) - transform.LocalOrigin.Coordinates = V(-offset, 0) + transform.Axis1 = self.file.createIfcDirection(ifc_safe_vector_type(V(0, 1))) + transform.Axis2 = self.file.createIfcDirection(ifc_safe_vector_type(V(-1, 0))) + transform.LocalOrigin.Coordinates = ifc_safe_vector_type(V(-offset, 0)) else: - transform.LocalOrigin.Coordinates = V(offset, 0) - transform.Axis1 = self.file.createIfcDirection(V(0, 1)) - transform.Axis2 = self.file.createIfcDirection(V(1, 0)) + transform.LocalOrigin.Coordinates = ifc_safe_vector_type(V(offset, 0)) + transform.Axis1 = self.file.createIfcDirection(ifc_safe_vector_type(V(0, 1))) + transform.Axis2 = self.file.createIfcDirection(ifc_safe_vector_type(V(1, 0))) elif mode == "SLBB": offset = profile.Width/2 + profiles_gap if mirrored: - transform.Axis1 = self.file.createIfcDirection(V(-1, 0)) - transform.LocalOrigin.Coordinates = V(-offset, 0) + transform.Axis1 = self.file.createIfcDirection(ifc_safe_vector_type(V(-1, 0))) + transform.LocalOrigin.Coordinates = ifc_safe_vector_type(V(-offset, 0)) else: - transform.LocalOrigin.Coordinates = V(offset, 0) + transform.LocalOrigin.Coordinates = ifc_safe_vector_type(V(offset, 0)) return derived_profile diff --git a/src/bonsai/test/tool/test_loader.py b/src/bonsai/test/tool/test_loader.py index c97ebef955..3780c6b28e 100644 --- a/src/bonsai/test/tool/test_loader.py +++ b/src/bonsai/test/tool/test_loader.py @@ -25,7 +25,7 @@ import bonsai.tool as tool from test.bim.bootstrap import NewFile from bonsai.tool.loader import Loader as subject import numpy as np -from ifcopenshell.util.shape_builder import ShapeBuilder, V +from ifcopenshell.util.shape_builder import ShapeBuilder from mathutils import Vector from pathlib import Path diff --git a/src/ifcopenshell-python/ifcopenshell/util/shape_builder.py b/src/ifcopenshell-python/ifcopenshell/util/shape_builder.py index 0581e52ea0..81045bf1fe 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/shape_builder.py +++ b/src/ifcopenshell-python/ifcopenshell/util/shape_builder.py @@ -16,6 +16,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with IfcOpenShell. If not, see . +from __future__ import annotations import numpy as np import numpy.typing as npt import collections @@ -27,21 +28,42 @@ import ifcopenshell.util.placement import ifcopenshell.util.representation import ifcopenshell.util.unit from math import cos, sin, pi, tan, radians, degrees, atan, sqrt -from typing import Union, Optional, Literal, Any, Sequence +from typing import Union, Optional, Literal, Any, Sequence, TYPE_CHECKING from itertools import chain -from mathutils import Vector, Matrix +from mathutils import Vector -V = lambda *x: Vector([float(i) for i in x]) PRECISION = 1.0e-5 -VectorTuple = type[tuple[float, float, float]] -"tuple of 3 `float` values" -# Support both numpy arrays and python sequences as inputs. -VectorType = Union[Sequence[float], Vector, np.ndarray] +if TYPE_CHECKING: + # NOTE: mathutils is never used at runtime in ifcopenshell, + # only for type checking to ensure methods are compatible with + # Blender vectors. + from mathutils import Vector + + # Support both numpy arrays and python sequences as inputs. + VectorType = Union[Sequence[float], Vector, np.ndarray] +else: + # Ensure it's exportable, so other modules can reuse it for typing. + VectorType = ... + SequenceOfVectors = Union[Sequence[VectorType], np.ndarray] +def V(*args: Union[float, VectorType, SequenceOfVectors]) -> npt.NDArray[np.float64]: + """Convert floats / vector / sequence of vectors to numpy array. + + Note that `float` argument type also allows passing ints, + which will be converted to floats (a double type) as IfcOpenShell is strict + about setting int/float attributes. + """ + if isinstance(args[0], (float, int)): + return np.array(args, dtype="d") + + assert len(args) == 1, "Only single argument is supported if providing a vector or a sequence of them." + return np.array(args[0], dtype="d") + + def ifc_safe_vector_type(v: Union[VectorType, SequenceOfVectors]) -> Any: """Convert vector / sequence of vectors to a list of floats that's safe to save IFC attribute. diff --git a/src/ifcopenshell-python/test/test_create_shape.py b/src/ifcopenshell-python/test/test_create_shape.py index ad91dbbbf1..5464aad855 100644 --- a/src/ifcopenshell-python/test/test_create_shape.py +++ b/src/ifcopenshell-python/test/test_create_shape.py @@ -9,7 +9,7 @@ import ifcopenshell.api.unit import ifcopenshell.geom import ifcopenshell.ifcopenshell_wrapper as W import ifcopenshell.util.shape -from ifcopenshell.util.shape_builder import ShapeBuilder, V +from ifcopenshell.util.shape_builder import ShapeBuilder from typing import get_args