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