From bfd4559da8554b4477be7cc45bfe41274604b434 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Thu, 2 Nov 2023 10:54:47 +0500 Subject: [PATCH] move generate_stair_2d_profile to tool, add tests --- .../blenderbim/bim/module/model/stair.py | 122 +---------- src/blenderbim/blenderbim/tool/model.py | 124 +++++++++++ src/blenderbim/test/tool/test_model.py | 197 ++++++++++++++++++ 3 files changed, 322 insertions(+), 121 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/model/stair.py b/src/blenderbim/blenderbim/bim/module/model/stair.py index ec7d178387..1a3e700f35 100644 --- a/src/blenderbim/blenderbim/bim/module/model/stair.py +++ b/src/blenderbim/blenderbim/bim/module/model/stair.py @@ -27,7 +27,6 @@ from bmesh.types import BMVert from bpy.types import Operator from bpy.props import FloatProperty, IntProperty from bpy_extras.object_utils import AddObjectHelper, object_data_add -from ifcopenshell.util.shape_builder import V, ShapeBuilder def add_dumb_stair_object(self, context): @@ -78,129 +77,10 @@ class BIM_OT_add_object(Operator, AddObjectHelper): return {"FINISHED"} -def generate_stair_2d_profile( - number_of_treads, - height, - width, - tread_run, - stair_type, - # WOOD/STEEL CONCRETE STAIR ARGUMENTS - tread_depth=None, - # CONCRETE STAIR ARGUMENTS - has_top_nib=None, - top_slab_depth=None, - base_slab_depth=None, -): - vertices = [] - edges = [] - faces = [] - - number_of_risers = number_of_treads + 1 - tread_rise = height / number_of_risers - length = tread_run * number_of_risers - - if stair_type == "WOOD/STEEL": - builder = ShapeBuilder(None) - tread_shape = builder.get_rectangle_coords( - size=V(tread_run, 0, tread_depth), position=V(0, 0, -(tread_depth - tread_rise)) - ) - tread_offset = V(tread_run, 0, tread_rise) - - for i in range(number_of_risers): - cur_trade_shape = [v + tread_offset * i for v in tread_shape] - vertices.extend(cur_trade_shape) - - cur_vertex = i * 4 - edges.extend( - [ - (cur_vertex, cur_vertex + 1), - (cur_vertex + 1, cur_vertex + 2), - (cur_vertex + 2, cur_vertex + 3), - (cur_vertex + 3, cur_vertex), - ] - ) - faces.append(list(range(cur_vertex, cur_vertex + 1))) - - return (vertices, edges, faces) - - elif stair_type == "GENERIC": - vertices.append(Vector([0, 0, 0])) - - tread_verts = [Vector([0, 0, tread_rise]), Vector([tread_run, 0, tread_rise])] - tread_offset = Vector([tread_run, 0, tread_rise]) - - for i in range(number_of_risers): - current_tread_verts = [v + tread_offset * i for v in tread_verts] - last_vert_i = len(vertices) - 1 - edges.extend([(last_vert_i, last_vert_i + 1), (last_vert_i + 1, last_vert_i + 2)]) - vertices.extend(current_tread_verts) - - last_vert_i = len(vertices) - vertices.append(vertices[-1] * V(1, 0, 0)) - edges.extend([(last_vert_i - 1, last_vert_i), (last_vert_i, 0)]) - - return (vertices, edges, faces) - - elif stair_type == "CONCRETE": - for i in range(number_of_risers): - vertices.extend( - [Vector((tread_run * i, 0, tread_rise * i)), Vector((tread_run * i, 0, tread_rise * (i + 1)))] - ) - cur_vertex = i * 2 - if i != 0: - edges.append((cur_vertex - 1, cur_vertex)) - edges.append((cur_vertex, cur_vertex + 1)) - - vertices.append(Vector((tread_run * number_of_risers, 0, tread_rise * number_of_risers))) - edges.append((number_of_risers * 2, number_of_risers * 2 - 1)) - - td_vector = Vector((vertices[2][2], 0, -vertices[2][0])).normalized() * tread_depth - - k = tread_rise / tread_run - s0 = vertices[0] + td_vector - b = s0.z - k * s0.x # comes from y = kx + b - # you could use td_vector as depth_vector - # but then stair won't be perpendicular to the X+ - # b is kind of vertical tread_depth (along Z+) - depth_vector = Vector((0, 0, b)) - - # top nib - if has_top_nib: - vertices.append(vertices[number_of_risers * 2] + Vector((0, 0, -top_slab_depth))) - vertices.append(vertices[number_of_risers * 2] + Vector(((-top_slab_depth - b) / k, 0, -top_slab_depth))) - last_vertex_i = len(vertices) - 1 - edges.append((number_of_risers * 2, last_vertex_i - 1)) - edges.append((last_vertex_i - 1, last_vertex_i)) - else: - vertices.append(vertices[number_of_risers * 2] + depth_vector) - last_vertex_i = len(vertices) - 1 - edges.append((number_of_risers * 2, last_vertex_i)) - - top_nib_end = len(vertices) - 1 - - # bottom nib - if abs(b) <= base_slab_depth: - vertices.append(vertices[0] + depth_vector) - edges.append((0, len(vertices) - 1)) - bottom_nib_end = len(vertices) - 1 - else: - vertices.append(vertices[0] + Vector(((-base_slab_depth - b) / k, 0, -base_slab_depth))) - vertices.append(vertices[0] + Vector((0, 0, -base_slab_depth))) - last_vertex_i = len(vertices) - 1 - edges.append((0, last_vertex_i)) - edges.append((last_vertex_i - 1, last_vertex_i)) - bottom_nib_end = len(vertices) - 2 - - edges.append((bottom_nib_end, top_nib_end)) - faces = [list(range(len(vertices)))] - - return (vertices, edges, faces) - - def regenerate_stair_mesh(context): obj = context.active_object props_kwargs = obj.BIMStairProperties.get_props_kwargs() - vertices, edges, faces = generate_stair_2d_profile(**props_kwargs) + vertices, edges, faces = tool.Model.generate_stair_2d_profile(**props_kwargs) obj = context.active_object bm = bmesh.new() diff --git a/src/blenderbim/blenderbim/tool/model.py b/src/blenderbim/blenderbim/tool/model.py index e974b675e3..54d24ba15e 100644 --- a/src/blenderbim/blenderbim/tool/model.py +++ b/src/blenderbim/blenderbim/tool/model.py @@ -32,6 +32,7 @@ from mathutils import Matrix, Vector from blenderbim.bim import import_ifc from blenderbim.bim.module.geometry.helper import Helper from blenderbim.bim.module.model.data import AuthoringData, RailingData, RoofData, WindowData, DoorData +from ifcopenshell.util.shape_builder import V, ShapeBuilder class Model(blenderbim.core.tool.Model): @@ -906,3 +907,126 @@ class Model(blenderbim.core.tool.Model): @classmethod def is_parametric_door_active(cls): return (DoorData.is_loaded or not DoorData.load()) and DoorData.data["pset_data"] + + @classmethod + def generate_stair_2d_profile( + cls, + number_of_treads, + height, + width, + tread_run, + stair_type, + # WOOD/STEEL CONCRETE STAIR ARGUMENTS + tread_depth=None, + # CONCRETE STAIR ARGUMENTS + has_top_nib=None, + top_slab_depth=None, + base_slab_depth=None, + ): + """returns a tuple of stair profile data: (vertices, edges, faces)""" + vertices = [] + edges = [] + faces = [] + + number_of_risers = number_of_treads + 1 + tread_rise = height / number_of_risers + length = tread_run * number_of_risers + + if stair_type == "WOOD/STEEL": + builder = ShapeBuilder(None) + tread_shape = builder.get_rectangle_coords( + size=V(tread_run, 0, tread_depth), position=V(0, 0, -(tread_depth - tread_rise)) + ) + tread_offset = V(tread_run, 0, tread_rise) + + for i in range(number_of_risers): + cur_trade_shape = [v + tread_offset * i for v in tread_shape] + vertices.extend(cur_trade_shape) + + cur_vertex = i * 4 + edges.extend( + [ + (cur_vertex, cur_vertex + 1), + (cur_vertex + 1, cur_vertex + 2), + (cur_vertex + 2, cur_vertex + 3), + (cur_vertex + 3, cur_vertex), + ] + ) + faces.append(list(range(cur_vertex, cur_vertex + 1))) + + return (vertices, edges, faces) + + elif stair_type == "GENERIC": + vertices.append(Vector([0, 0, 0])) + + tread_verts = [Vector([0, 0, tread_rise]), Vector([tread_run, 0, tread_rise])] + tread_offset = Vector([tread_run, 0, tread_rise]) + + for i in range(number_of_risers): + current_tread_verts = [v + tread_offset * i for v in tread_verts] + last_vert_i = len(vertices) - 1 + edges.extend([(last_vert_i, last_vert_i + 1), (last_vert_i + 1, last_vert_i + 2)]) + vertices.extend(current_tread_verts) + + last_vert_i = len(vertices) + vertices.append(vertices[-1] * V(1, 0, 0)) + edges.extend([(last_vert_i - 1, last_vert_i), (last_vert_i, 0)]) + + return (vertices, edges, faces) + + elif stair_type == "CONCRETE": + for i in range(number_of_risers): + vertices.extend( + [Vector((tread_run * i, 0, tread_rise * i)), Vector((tread_run * i, 0, tread_rise * (i + 1)))] + ) + cur_vertex = i * 2 + if i != 0: + edges.append((cur_vertex - 1, cur_vertex)) + edges.append((cur_vertex, cur_vertex + 1)) + + vertices.append(Vector((tread_run * number_of_risers, 0, tread_rise * number_of_risers))) + edges.append((number_of_risers * 2, number_of_risers * 2 - 1)) + + td_vector = Vector((vertices[2][2], 0, -vertices[2][0])).normalized() * tread_depth + + k = tread_rise / tread_run + s0 = vertices[0] + td_vector + b = s0.z - k * s0.x # comes from y = kx + b + # you could use td_vector as depth_vector + # but then stair won't be perpendicular to the X+ + # b is kind of vertical tread_depth (along Z+) + depth_vector = Vector((0, 0, b)) + + # top nib + if has_top_nib: + vertices.append(vertices[number_of_risers * 2] + Vector((0, 0, -top_slab_depth))) + vertices.append( + vertices[number_of_risers * 2] + Vector(((-top_slab_depth - b) / k, 0, -top_slab_depth)) + ) + last_vertex_i = len(vertices) - 1 + edges.append((number_of_risers * 2, last_vertex_i - 1)) + edges.append((last_vertex_i - 1, last_vertex_i)) + else: + vertices.append(vertices[number_of_risers * 2] + depth_vector) + last_vertex_i = len(vertices) - 1 + edges.append((number_of_risers * 2, last_vertex_i)) + + top_nib_end = len(vertices) - 1 + + # bottom nib + if abs(b) <= base_slab_depth: + vertices.append(vertices[0] + depth_vector) + edges.append((0, len(vertices) - 1)) + bottom_nib_end = len(vertices) - 1 + else: + vertices.append(vertices[0] + Vector(((-base_slab_depth - b) / k, 0, -base_slab_depth))) + vertices.append(vertices[0] + Vector((0, 0, -base_slab_depth))) + last_vertex_i = len(vertices) - 1 + edges.append((0, last_vertex_i)) + edges.append((last_vertex_i - 1, last_vertex_i)) + bottom_nib_end = len(vertices) - 2 + + edges.append((bottom_nib_end, top_nib_end)) + faces = [list(range(len(vertices)))] + + return (vertices, edges, faces) diff --git a/src/blenderbim/test/tool/test_model.py b/src/blenderbim/test/tool/test_model.py index 71289bf64f..a2d8375af9 100644 --- a/src/blenderbim/test/tool/test_model.py +++ b/src/blenderbim/test/tool/test_model.py @@ -23,6 +23,7 @@ import blenderbim.tool as tool import numpy as np from test.bim.bootstrap import NewFile from blenderbim.tool.model import Model as subject +from ifcopenshell.util.shape_builder import V class TestImplementsTool(NewFile): @@ -105,3 +106,199 @@ class TestGetManualBooleans(NewFile): assert len(subject.get_manual_booleans(element)) == 0 subject.mark_manual_booleans(element, booleans) assert len(subject.get_manual_booleans(element)) == 2 + + +class TestGenerateStair2DProfile(NewFile): + def compare_data(self, generated_profile, expected_profile): + verts_gen, edges_gen, faces_gen = generated_profile + verts, edges, faces = expected_profile + + assert edges == tuple(edges_gen) + assert faces == tuple(tuple(face) for face in faces_gen) + for vert, vert_gen in zip(verts, verts_gen, strict=True): + assert tool.Cad.are_vectors_equal(vert, vert_gen, 0.01) + + def test_create_concrete_stair(self): + kwargs = { + "base_slab_depth": 0.25, + "has_top_nib": False, + "height": 1.0, + "number_of_treads": 3, + "stair_type": "CONCRETE", + "top_slab_depth": 0.25, + "tread_depth": 0.25, + "tread_run": 0.3, + "width": 1.2, + } + verts_data = ( + V(0.0, 0, 0.0), + V(0.0, 0, 0.25), + V(0.3, 0, 0.25), + V(0.3, 0, 0.5), + V(0.6, 0, 0.5), + V(0.6, 0, 0.75), + V(0.9, 0, 0.75), + V(0.9, 0, 1.0), + V(1.2, 0, 1.0), + V(1.2, 0, 0.67457), + V(0.1, 0, -0.25), + V(0.0, 0, -0.25), + ) + edges_data = ( + (0, 1), + (1, 2), + (2, 3), + (3, 4), + (4, 5), + (5, 6), + (6, 7), + (8, 7), + (8, 9), + (0, 11), + (10, 11), + (10, 9), + ) + faces_data = ((0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11),) + expected_profile = (verts_data, edges_data, faces_data) + generated_profile = subject.generate_stair_2d_profile(**kwargs) + self.compare_data(generated_profile, expected_profile) + + def test_create_concrete_stair_nib(self): + kwargs = { + "base_slab_depth": 0.25, + "has_top_nib": True, + "height": 1.0, + "number_of_treads": 3, + "stair_type": "CONCRETE", + "top_slab_depth": 0.25, + "tread_depth": 0.25, + "tread_run": 0.3, + "width": 1.2, + } + verts_data = ( + V(0.0, 0, 0.0), + V(0.0, 0, 0.25), + V(0.3, 0, 0.25), + V(0.3, 0, 0.5), + V(0.6, 0, 0.5), + V(0.6, 0, 0.75), + V(0.9, 0, 0.75), + V(0.9, 0, 1.0), + V(1.2, 0, 1.0), + V(1.2, 0, 0.75), + V(1.3, 0, 0.75), + V(0.1, 0, -0.25), + V(0.0, 0, -0.25), + ) + + edges_data = ( + (0, 1), + (1, 2), + (2, 3), + (3, 4), + (4, 5), + (5, 6), + (6, 7), + (8, 7), + (8, 9), + (9, 10), + (0, 12), + (11, 12), + (11, 10), + ) + + faces_data = ((0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12),) + expected_profile = (verts_data, edges_data, faces_data) + generated_profile = subject.generate_stair_2d_profile(**kwargs) + self.compare_data(generated_profile, expected_profile) + + def test_create_wood_steel_stair(self): + kwargs = { + "height": 1.0, + "number_of_treads": 3, + "stair_type": "WOOD/STEEL", + "tread_depth": 0.25, + "tread_run": 0.3, + "width": 1.2, + } + verts_data = ( + V(0.0, 0, 0.0), + V(0.3, 0, 0.0), + V(0.3, 0, 0.25), + V(0.0, 0, 0.25), + V(0.3, 0, 0.25), + V(0.6, 0, 0.25), + V(0.6, 0, 0.5), + V(0.3, 0, 0.5), + V(0.6, 0, 0.5), + V(0.9, 0, 0.5), + V(0.9, 0, 0.75), + V(0.6, 0, 0.75), + V(0.9, 0, 0.75), + V(1.2, 0, 0.75), + V(1.2, 0, 1.0), + V(0.9, 0, 1.0), + ) + + edges_data = ( + (0, 1), + (1, 2), + (2, 3), + (3, 0), + (4, 5), + (5, 6), + (6, 7), + (7, 4), + (8, 9), + (9, 10), + (10, 11), + (11, 8), + (12, 13), + (13, 14), + (14, 15), + (15, 12), + ) + + faces_data = ( + (0,), + (4,), + (8,), + (12,), + ) + + expected_profile = (verts_data, edges_data, faces_data) + generated_profile = subject.generate_stair_2d_profile(**kwargs) + self.compare_data(generated_profile, expected_profile) + + def test_create_generic_stair(self): + kwargs = {"height": 1.0, "number_of_treads": 3, "stair_type": "GENERIC", "tread_run": 0.3, "width": 1.2} + verts_data = ( + V(0.0, 0, 0.0), + V(0.0, 0, 0.25), + V(0.3, 0, 0.25), + V(0.3, 0, 0.5), + V(0.6, 0, 0.5), + V(0.6, 0, 0.75), + V(0.9, 0, 0.75), + V(0.9, 0, 1.0), + V(1.2, 0, 1.0), + V(1.2, 0, 0.0), + ) + + edges_data = ( + (0, 1), + (1, 2), + (2, 3), + (3, 4), + (4, 5), + (5, 6), + (6, 7), + (7, 8), + (8, 9), + (9, 0), + ) + + faces_data = () + expected_profile = (verts_data, edges_data, faces_data) + generated_profile = subject.generate_stair_2d_profile(**kwargs) + self.compare_data(generated_profile, expected_profile)