From 76f1cdc3c7aa55e3293e80de7b789240c49d503c Mon Sep 17 00:00:00 2001 From: yyc12345 Date: Sat, 4 Jan 2025 20:13:20 +0800 Subject: [PATCH] fix: fix various rail creation issue. - move rail creation function into an individual file, UTIL_rail_creator.py - add flip options for screw rails. this allow user to create any types of screw rail they needed. --- bbp_ng/OP_ADDS_rail.py | 414 ++++++------------------------------ bbp_ng/UTIL_rail_creator.py | 347 ++++++++++++++++++++++++++++++ 2 files changed, 412 insertions(+), 349 deletions(-) create mode 100644 bbp_ng/UTIL_rail_creator.py diff --git a/bbp_ng/OP_ADDS_rail.py b/bbp_ng/OP_ADDS_rail.py index 1f723fd..22b6fff 100644 --- a/bbp_ng/OP_ADDS_rail.py +++ b/bbp_ng/OP_ADDS_rail.py @@ -1,7 +1,6 @@ -import bpy, bmesh, mathutils, math +import bpy, mathutils, math import typing -from . import UTIL_functions, UTIL_naming_convension -from . import PROP_bme_material +from . import UTIL_rail_creator ## Const Value Hint: # Default Rail Radius: 0.35 (in measure) @@ -61,7 +60,7 @@ class SharedExtraTransform(): def draw_extra_transform_input(self, layout: bpy.types.UILayout) -> None: # show extra transform props # forcely order that each one are placed horizontally - layout.label(text = "Extra Transform:") + layout.label(text = "Extra Transform") # translation layout.label(text = 'Translation') row = layout.row() @@ -120,6 +119,7 @@ class SharedRailCapInputProperty(): ) # type: ignore def draw_rail_cap_input(self, layout: bpy.types.UILayout) -> None: + layout.label(text = "Cap Options") row = layout.row() row.prop(self, "rail_start_cap", toggle = 1) row.prop(self, "rail_end_cap", toggle = 1) @@ -169,6 +169,24 @@ class SharedScrewRailInputProperty(): unit = 'LENGTH' ) # type: ignore + rail_screw_flip_x: bpy.props.BoolProperty( + name = 'Flip X', + description = 'Whether flip this rail with X axis', + default = False + ) # type: ignore + + rail_screw_flip_y: bpy.props.BoolProperty( + name = 'Flip Y', + description = 'Whether flip this rail with Y axis', + default = False + ) # type: ignore + + rail_screw_flip_z: bpy.props.BoolProperty( + name = 'Flip Z', + description = 'Whether flip this rail with Z axis', + default = False + ) # type: ignore + def draw_screw_rail_input(self, layout: bpy.types.UILayout) -> None: layout.prop(self, "rail_screw_radius") layout.prop(self, "rail_screw_steps") @@ -178,6 +196,21 @@ class SharedScrewRailInputProperty(): def general_get_rail_screw_steps(self) -> int: return self.rail_screw_steps + def draw_screw_rail_flip_input(self, layout: bpy.types.UILayout) -> None: + # flip options should placed horizontally + layout.label(text = "Flip Options") + row = layout.row() + row.prop(self, "rail_screw_flip_x", toggle = 1) + row.prop(self, "rail_screw_flip_y", toggle = 1) + row.prop(self, "rail_screw_flip_z", toggle = 1) + + def general_get_rail_screw_flip_x(self) -> bool: + return self.rail_screw_flip_x + def general_get_rail_screw_flip_y(self) -> bool: + return self.rail_screw_flip_y + def general_get_rail_screw_flip_z(self) -> bool: + return self.rail_screw_flip_z + #endregion #region Operators @@ -189,8 +222,8 @@ class BBP_OT_add_rail_section(SharedRailSectionInputProperty, bpy.types.Operator bl_options = {'REGISTER', 'UNDO'} def execute(self, context): - _rail_creator_wrapper( - lambda bm: _create_rail_section( + UTIL_rail_creator.rail_creator_wrapper( + lambda bm: UTIL_rail_creator.create_rail_section( bm, self.general_get_is_monorail(), c_DefaultRailRadius, c_DefaultRailSpan ), @@ -209,8 +242,8 @@ class BBP_OT_add_transition_section(bpy.types.Operator): bl_options = {'REGISTER', 'UNDO'} def execute(self, context): - _rail_creator_wrapper( - lambda bm: _create_transition_section(bm, c_DefaultRailRadius, c_DefaultRailSpan), + UTIL_rail_creator.rail_creator_wrapper( + lambda bm: UTIL_rail_creator.create_transition_section(bm, c_DefaultRailRadius, c_DefaultRailSpan), mathutils.Matrix.Identity(4) ) return {'FINISHED'} @@ -226,8 +259,8 @@ class BBP_OT_add_straight_rail(SharedExtraTransform, SharedRailSectionInputPrope bl_options = {'REGISTER', 'UNDO'} def execute(self, context): - _rail_creator_wrapper( - lambda bm: _create_straight_rail( + UTIL_rail_creator.rail_creator_wrapper( + lambda bm: UTIL_rail_creator.create_straight_rail( bm, self.general_get_is_monorail(), c_DefaultRailRadius, c_DefaultRailSpan, self.general_get_rail_length(), 0, @@ -243,7 +276,6 @@ class BBP_OT_add_straight_rail(SharedExtraTransform, SharedRailSectionInputPrope self.draw_rail_section_input(layout) self.draw_straight_rail_input(layout) layout.separator() - layout.label(text = 'Rail Cap') self.draw_rail_cap_input(layout) layout.separator() self.draw_extra_transform_input(layout) @@ -255,8 +287,8 @@ class BBP_OT_add_transition_rail(SharedExtraTransform, SharedRailCapInputPropert bl_options = {'REGISTER', 'UNDO'} def execute(self, context): - _rail_creator_wrapper( - lambda bm: _create_transition_rail( + UTIL_rail_creator.rail_creator_wrapper( + lambda bm: UTIL_rail_creator.create_transition_rail( bm, c_DefaultRailRadius, c_DefaultRailSpan, self.general_get_rail_length(), @@ -271,7 +303,6 @@ class BBP_OT_add_transition_rail(SharedExtraTransform, SharedRailCapInputPropert layout.label(text = 'Transition Rail') self.draw_straight_rail_input(layout) layout.separator() - layout.label(text = 'Rail Cap') self.draw_rail_cap_input(layout) layout.separator() self.draw_extra_transform_input(layout) @@ -293,8 +324,8 @@ class BBP_OT_add_side_rail(SharedExtraTransform, SharedRailCapInputProperty, Sha ) # type: ignore def execute(self, context): - _rail_creator_wrapper( - lambda bm: _create_straight_rail( + UTIL_rail_creator.rail_creator_wrapper( + lambda bm: UTIL_rail_creator.create_straight_rail( bm, False, c_DefaultRailRadius, c_DefaultRailSpan, self.general_get_rail_length(), @@ -311,7 +342,6 @@ class BBP_OT_add_side_rail(SharedExtraTransform, SharedRailCapInputProperty, Sha layout.prop(self, 'side_rail_type') self.draw_straight_rail_input(layout) layout.separator() - layout.label(text = 'Rail Cap') self.draw_rail_cap_input(layout) layout.separator() self.draw_extra_transform_input(layout) @@ -331,13 +361,14 @@ class BBP_OT_add_arc_rail(SharedExtraTransform, SharedRailSectionInputProperty, ) # type: ignore def execute(self, context): - _rail_creator_wrapper( - lambda bm: _create_screw_rail( + UTIL_rail_creator.rail_creator_wrapper( + lambda bm: UTIL_rail_creator.create_screw_rail( bm, self.general_get_is_monorail(), c_DefaultRailRadius, c_DefaultRailSpan, self.general_get_rail_start_cap(), self.general_get_rail_end_cap(), math.degrees(self.rail_screw_angle), 0, 1, # blender passed value is in radians - self.general_get_rail_screw_steps(), self.general_get_rail_screw_radius() + self.general_get_rail_screw_steps(), self.general_get_rail_screw_radius(), + self.general_get_rail_screw_flip_x(), self.general_get_rail_screw_flip_y(), self.general_get_rail_screw_flip_z() ), self.general_get_extra_transform() ) @@ -350,7 +381,8 @@ class BBP_OT_add_arc_rail(SharedExtraTransform, SharedRailSectionInputProperty, self.draw_screw_rail_input(layout) layout.prop(self, "rail_screw_angle") layout.separator() - layout.label(text = 'Rail Cap') + self.draw_screw_rail_flip_input(layout) + layout.separator() self.draw_rail_cap_input(layout) layout.separator() self.draw_extra_transform_input(layout) @@ -376,13 +408,14 @@ class BBP_OT_add_spiral_rail(SharedExtraTransform, SharedRailCapInputProperty, S ) # type: ignore def execute(self, context): - _rail_creator_wrapper( - lambda bm: _create_screw_rail( + UTIL_rail_creator.rail_creator_wrapper( + lambda bm: UTIL_rail_creator.create_screw_rail( bm, False, c_DefaultRailRadius, c_DefaultRailSpan, self.general_get_rail_start_cap(), self.general_get_rail_end_cap(), 360, self.rail_screw_screw, self.rail_screw_iterations, - self.general_get_rail_screw_steps(), self.general_get_rail_screw_radius() + self.general_get_rail_screw_steps(), self.general_get_rail_screw_radius(), + self.general_get_rail_screw_flip_x(), self.general_get_rail_screw_flip_y(), self.general_get_rail_screw_flip_z() ), self.general_get_extra_transform() ) @@ -395,7 +428,8 @@ class BBP_OT_add_spiral_rail(SharedExtraTransform, SharedRailCapInputProperty, S layout.prop(self, "rail_screw_screw") layout.prop(self, "rail_screw_iterations") layout.separator() - layout.label(text = 'Rail Cap') + self.draw_screw_rail_flip_input(layout) + layout.separator() self.draw_rail_cap_input(layout) layout.separator() self.draw_extra_transform_input(layout) @@ -416,13 +450,14 @@ class BBP_OT_add_side_spiral_rail(SharedExtraTransform, SharedRailSectionInputPr ) # type: ignore def execute(self, context): - _rail_creator_wrapper( - lambda bm: _create_screw_rail( + UTIL_rail_creator.rail_creator_wrapper( + lambda bm: UTIL_rail_creator.create_screw_rail( bm, True, c_DefaultRailRadius, c_DefaultRailSpan, self.general_get_rail_start_cap(), self.general_get_rail_end_cap(), 360, c_SideSpiralRailScrew, self.rail_screw_iterations, - self.general_get_rail_screw_steps(), self.general_get_rail_screw_radius() + self.general_get_rail_screw_steps(), self.general_get_rail_screw_radius(), + self.general_get_rail_screw_flip_x(), self.general_get_rail_screw_flip_y(), self.general_get_rail_screw_flip_z() ), self.general_get_extra_transform() ) @@ -434,333 +469,14 @@ class BBP_OT_add_side_spiral_rail(SharedExtraTransform, SharedRailSectionInputPr self.draw_screw_rail_input(layout) layout.prop(self, "rail_screw_iterations") layout.separator() - layout.label(text = 'Rail Cap') + self.draw_screw_rail_flip_input(layout) + layout.separator() self.draw_rail_cap_input(layout) layout.separator() self.draw_extra_transform_input(layout) #endregion -#region BMesh Operations Helper - -def _bmesh_extrude( - bm: bmesh.types.BMesh, - start_edges: list[bmesh.types.BMEdge], - direction: mathutils.Vector) -> list[bmesh.types.BMEdge]: - # extrude - ret: dict[str, typing.Any] = bmesh.ops.extrude_edge_only( - bm, - edges = start_edges, - use_normal_flip = True, # NOTE: flip normal according to test result. - use_select_history = False - ) - - # get end edges - ret_geom = ret['geom'] - del ret - end_verts: list[bmesh.types.BMVert] = list(filter(lambda x: isinstance(x, bmesh.types.BMVert), ret_geom)) - end_edges: list[bmesh.types.BMEdge] = list(filter(lambda x: isinstance(x, bmesh.types.BMEdge) and x.is_boundary, ret_geom)) - # and move it - bmesh.ops.translate( - bm, - vec = direction, space = mathutils.Matrix.Identity(4), - verts = end_verts, - use_shapekey = False - ) - - # return value - return end_edges - -def _bmesh_screw( - bm: bmesh.types.BMesh, - start_verts: list[bmesh.types.BMVert], start_edges: list[bmesh.types.BMEdge], - angle: float, steps: int, iterations: int, - center: mathutils.Vector, screw_per_iteration: float) -> list[bmesh.types.BMEdge]: - """ - Hints: Angle is input as degree unit. - """ - # screw - ret: dict[str, typing.Any] = bmesh.ops.spin( - bm, - geom = start_edges, - cent = center, - axis = mathutils.Vector((0, 0, 1)), # default to +Z - dvec = mathutils.Vector((0, 0, screw_per_iteration / steps)), # conv to step delta - angle = math.radians(angle) * iterations, - space = mathutils.Matrix.Identity(4), - steps = steps * iterations, - use_merge = False, - use_normal_flip = True, # NOTE: flip normal according to test result. - use_duplicate = False - ) - - # return last segment - geom_last = ret['geom_last'] - del ret - return list(filter(lambda x: isinstance(x, bmesh.types.BMEdge), geom_last)) - -def _bmesh_smooth_all_edges(bm: bmesh.types.BMesh) -> None: - """ - Resrt all edges to smooth. Call this before calling edge cap function. - """ - # reset all edges to smooth - edge: bmesh.types.BMEdge - for edge in bm.edges: - edge.smooth = True - -def _bmesh_cap(bm: bmesh.types.BMesh, edges: list[bmesh.types.BMEdge]) -> None: - """ - Cap given edges. And mark it as sharp edge. - Please reset all edges to smooth one before calling this. - """ - # fill holes - bmesh.ops.triangle_fill( - bm, - use_beauty = False, use_dissolve = False, - edges = edges - # no pass to normal. - ) - - # and only set sharp for cap's edges - for edge in edges: - edge.smooth = False - -#endregion - -#region Real Rail Creators - -def _rail_creator_wrapper(fct_poly_cret: typing.Callable[[bmesh.types.BMesh], None], extra_transform: mathutils.Matrix) -> bpy.types.Object: - # create mesh first - bm: bmesh.types.BMesh = bmesh.new() - - # call cret fct - fct_poly_cret(bm) - - # finish up - mesh: bpy.types.Mesh = bpy.data.meshes.new('Rail') - bm.to_mesh(mesh) - bm.free() - - # setup smooth for mesh - mesh.shade_smooth() - - # setup default material - with PROP_bme_material.BMEMaterialsHelper(bpy.context.scene) as bmemtl: - mesh.materials.clear() - mesh.materials.append(bmemtl.get_material('Rail')) - mesh.validate_material_indices() - - # create object and assoc with it - # create info first - rail_info: UTIL_naming_convension.BallanceObjectInfo = UTIL_naming_convension.BallanceObjectInfo.create_from_others( - UTIL_naming_convension.BallanceObjectType.RAIL - ) - # then get object name - rail_name: str | None = UTIL_naming_convension.YYCToolchainConvention.set_to_name(rail_info, None) - if rail_name is None: raise UTIL_functions.BBPException('impossible null name') - # create object by name - obj: bpy.types.Object = bpy.data.objects.new(rail_name, mesh) - # assign virtools groups - UTIL_naming_convension.VirtoolsGroupConvention.set_to_object(obj, rail_info, None) - - # move to cursor - UTIL_functions.add_into_scene_and_move_to_cursor(obj) - # add extra transform - obj.matrix_world = obj.matrix_world @ extra_transform - # select created object - UTIL_functions.select_certain_objects((obj, )) - - # return rail - return obj - -def _create_rail_section( - bm: bmesh.types.BMesh, - is_monorail: bool, rail_radius: float, rail_span: float, - matrix: mathutils.Matrix = mathutils.Matrix.Identity(4)) -> None: - """ - Add a rail section. - - If created is monorail, the original point locate at the center of section. - Otherwise, the original point locate at the center point of the line connecting between left rail section and right rail section. - The section will be placed in XZ panel. - - If ordered is monorail, `rail_span` param will be ignored. - """ - if is_monorail: - # create monorail - bmesh.ops.create_circle( - bm, cap_ends = False, cap_tris = False, segments = 8, radius = rail_radius, - matrix = typing.cast(mathutils.Matrix, matrix @ mathutils.Matrix.LocRotScale( - None, - mathutils.Euler((math.radians(90), math.radians(22.5), 0), 'XYZ'), - None - )), - calc_uvs = False - ) - else: - # create rail - # create left rail - bmesh.ops.create_circle( - bm, cap_ends = False, cap_tris = False, segments = 8, radius = rail_radius, - matrix = typing.cast(mathutils.Matrix, matrix @ mathutils.Matrix.LocRotScale( - mathutils.Vector((-rail_span / 2, 0, 0)), - mathutils.Euler((math.radians(90), 0, 0), 'XYZ'), - None - )), - calc_uvs = False - ) - # create right rail - bmesh.ops.create_circle( - bm, cap_ends = False, cap_tris = False, segments = 8, radius = rail_radius, - matrix = typing.cast(mathutils.Matrix, matrix @ mathutils.Matrix.LocRotScale( - mathutils.Vector((rail_span / 2, 0, 0)), - mathutils.Euler((math.radians(90), 0, 0), 'XYZ'), - None - )), - calc_uvs = False - ) - -def _create_transition_section( - bm: bmesh.types.BMesh, - rail_radius: float, rail_span: float) -> None: - """ - Create the transition section between rail and monorail. - """ - # create rail section - _create_rail_section(bm, False, rail_radius, rail_span) - - # create monorail - # calc sink first - monorail_sink: float - try: - monorail_sink = math.sqrt((rail_radius + 2) ** 2 - (rail_span / 2) ** 2) - 2 - rail_radius - except: - monorail_sink = -2 # if sqrt(minus number) happended, it mean no triangle relation. the depth should always be -2. - # create monorail with calculated sink - _create_rail_section( - bm, True, rail_radius, rail_span, - mathutils.Matrix.Translation((0, 0, monorail_sink)) - ) - -def _create_straight_rail( - bm: bmesh.types.BMesh, - is_monorail: bool, rail_radius: float, rail_span: float, - rail_length: float, rail_angle: float, - rail_start_cap: bool, rail_end_cap: bool) -> None: - """ - Add a straight rail. - - The original point is same as `_add_rail_section()`. - The start terminal of this straight will be placed in XZ panel. - The expand direction is +Y. - - If ordered is monorail, `rail_span` param will be ignored. - - The rail angle is in degree unit and indicate how any angle this rail should rotated by its axis. - It usually used to create side rail. - """ - # create section first - _create_rail_section( - bm, is_monorail, rail_radius, rail_span, - mathutils.Matrix.LocRotScale( - None, - mathutils.Euler((0, math.radians(rail_angle), 0), 'XYZ'), - None - ) - ) - - # get start edges - start_edges: list[bmesh.types.BMEdge] = bm.edges[:] - # extrude and get end edges - end_edges: list[bmesh.types.BMEdge] = _bmesh_extrude( - bm, - start_edges, - mathutils.Vector((0, rail_length, 0)) - ) - - # smooth geometry - _bmesh_smooth_all_edges(bm) - - # cap start and end edges if needed - if rail_start_cap: - _bmesh_cap(bm, start_edges) - if rail_end_cap: - _bmesh_cap(bm, end_edges) - -def _create_transition_rail( - bm: bmesh.types.BMesh, - rail_radius: float, rail_span: float, - rail_length: float, - rail_start_cap: bool, rail_end_cap: bool) -> None: - """ - Add a transition rail. - - The original point is same as `_add_transition_section()`. - The start terminal of this straight will be placed in XZ panel. - The expand direction is +Y. - """ - # create section first - _create_transition_section(bm, rail_radius, rail_span) - - # get start edges - start_edges: list[bmesh.types.BMEdge] = bm.edges[:] - # extrude and get end edges - end_edges: list[bmesh.types.BMEdge] = _bmesh_extrude( - bm, - start_edges, - mathutils.Vector((0, rail_length, 0)) - ) - - # smooth geometry - _bmesh_smooth_all_edges(bm) - - # cap start and end edges if needed - if rail_start_cap: - _bmesh_cap(bm, start_edges) - if rail_end_cap: - _bmesh_cap(bm, end_edges) - -def _create_screw_rail( - bm: bmesh.types.BMesh, - is_monorail: bool, rail_radius: float, rail_span: float, - rail_start_cap: bool, rail_end_cap: bool, - rail_screw_angle: float, rail_screw_screw: float, rail_screw_iterations: int, - rail_screw_steps: int, rail_screw_radius: float) -> None: - """ - Add a screw rail. - - The original point is same as `_add_rail_section()`. - The start terminal of this straight will be placed in XZ panel. - The expand direction is +Y. - - If ordered is monorail, `rail_span` param will be ignored. - - Angle is input as degree unit. - """ - # create section first - _create_rail_section(bm, is_monorail, rail_radius, rail_span) - - start_edges: list[bmesh.types.BMEdge] = bm.edges[:] - end_edges: list[bmesh.types.BMEdge] = _bmesh_screw( - bm, - bm.verts[:], start_edges, - rail_screw_angle, - rail_screw_steps, rail_screw_iterations, - mathutils.Vector((rail_screw_radius, 0, 0)), - rail_screw_screw - ) - - # smooth geometry - _bmesh_smooth_all_edges(bm) - - # cap start and end edges if needed - if rail_start_cap: - _bmesh_cap(bm, start_edges) - if rail_end_cap: - _bmesh_cap(bm, end_edges) - -#endregion - def register() -> None: bpy.utils.register_class(BBP_OT_add_rail_section) bpy.utils.register_class(BBP_OT_add_transition_section) diff --git a/bbp_ng/UTIL_rail_creator.py b/bbp_ng/UTIL_rail_creator.py new file mode 100644 index 0000000..5c9c7d1 --- /dev/null +++ b/bbp_ng/UTIL_rail_creator.py @@ -0,0 +1,347 @@ +import bpy, bmesh, mathutils, math +import typing +from . import UTIL_functions, UTIL_naming_convension +from . import PROP_bme_material + +#region BMesh Operations Helper + +def _bmesh_extrude( + bm: bmesh.types.BMesh, + start_edges: list[bmesh.types.BMEdge], + direction: mathutils.Vector) -> list[bmesh.types.BMEdge]: + # extrude + ret: dict[str, typing.Any] = bmesh.ops.extrude_edge_only( + bm, + edges = start_edges, + use_normal_flip = True, # NOTE: flip normal according to test result. + use_select_history = False + ) + + # get end edges + ret_geom = ret['geom'] + del ret + end_verts: list[bmesh.types.BMVert] = list(filter(lambda x: isinstance(x, bmesh.types.BMVert), ret_geom)) + end_edges: list[bmesh.types.BMEdge] = list(filter(lambda x: isinstance(x, bmesh.types.BMEdge) and x.is_boundary, ret_geom)) + # and move it + bmesh.ops.translate( + bm, + vec = direction, space = mathutils.Matrix.Identity(4), + verts = end_verts, + use_shapekey = False + ) + + # return value + return end_edges + +def _bmesh_screw( + bm: bmesh.types.BMesh, + start_verts: list[bmesh.types.BMVert], start_edges: list[bmesh.types.BMEdge], + angle: float, steps: int, iterations: int, + center: mathutils.Vector, screw_per_iteration: float) -> list[bmesh.types.BMEdge]: + """ + Hints: Angle is input as degree unit. + """ + # screw + ret: dict[str, typing.Any] = bmesh.ops.spin( + bm, + geom = start_edges, + cent = center, + axis = mathutils.Vector((0, 0, 1)), # default to +Z + dvec = mathutils.Vector((0, 0, screw_per_iteration / steps)), # conv to step delta + angle = math.radians(angle) * iterations, + space = mathutils.Matrix.Identity(4), + steps = steps * iterations, + use_merge = False, + use_normal_flip = True, # NOTE: flip normal according to test result. + use_duplicate = False + ) + + # return last segment + geom_last = ret['geom_last'] + del ret + return list(filter(lambda x: isinstance(x, bmesh.types.BMEdge), geom_last)) + +def _bmesh_cap(bm: bmesh.types.BMesh, edges: list[bmesh.types.BMEdge]) -> None: + """ + Cap given edges. And mark it as sharp edge. + Please reset all edges to smooth one before calling this. + """ + # fill holes + bmesh.ops.triangle_fill( + bm, + use_beauty = False, use_dissolve = False, + edges = edges + # no pass to normal. + ) + + # and only set sharp for cap's edges + for edge in edges: + edge.smooth = False + +def _bmesh_smooth_all_edges(bm: bmesh.types.BMesh) -> None: + """ + Reset all edges to smooth. Call this before calling edge cap function. + """ + # reset all edges to smooth + edge: bmesh.types.BMEdge + for edge in bm.edges: + edge.smooth = True + +def _bmesh_flip_all_faces(bm: bmesh.types.BMesh, flip_x: bool, flip_y: bool, flip_z: bool) -> None: + """ + Flip the whole geometry in given bmesh with given axis. + """ + # get mirror result + scale_factor: mathutils.Vector = mathutils.Vector(( + (-1 if flip_x else 1), + (-1 if flip_y else 1), + (-1 if flip_z else 1) + )) + bmesh.ops.scale(bm, vec = scale_factor, verts = bm.verts[:]) + + # check whether we need perform normal flip. + # see UTIL_bme._is_mirror_matrix for more detail + test_matrix: mathutils.Matrix = mathutils.Matrix.LocRotScale(None, None, scale_factor) + if test_matrix.is_negative: + bmesh.ops.reverse_faces(bm, faces = bm.faces[:]) + +#endregion + +#region Real Rail Creators + +def rail_creator_wrapper(fct_poly_cret: typing.Callable[[bmesh.types.BMesh], None], extra_transform: mathutils.Matrix) -> bpy.types.Object: + # create mesh first + bm: bmesh.types.BMesh = bmesh.new() + + # call cret fct + fct_poly_cret(bm) + + # finish up + mesh: bpy.types.Mesh = bpy.data.meshes.new('Rail') + bm.to_mesh(mesh) + bm.free() + + # setup smooth for mesh + mesh.shade_smooth() + + # setup default material + with PROP_bme_material.BMEMaterialsHelper(bpy.context.scene) as bmemtl: + mesh.materials.clear() + mesh.materials.append(bmemtl.get_material('Rail')) + mesh.validate_material_indices() + + # create object and assoc with it + # create info first + rail_info: UTIL_naming_convension.BallanceObjectInfo = UTIL_naming_convension.BallanceObjectInfo.create_from_others( + UTIL_naming_convension.BallanceObjectType.RAIL + ) + # then get object name + rail_name: str | None = UTIL_naming_convension.YYCToolchainConvention.set_to_name(rail_info, None) + if rail_name is None: raise UTIL_functions.BBPException('impossible null name') + # create object by name + obj: bpy.types.Object = bpy.data.objects.new(rail_name, mesh) + # assign virtools groups + UTIL_naming_convension.VirtoolsGroupConvention.set_to_object(obj, rail_info, None) + + # move to cursor + UTIL_functions.add_into_scene_and_move_to_cursor(obj) + # add extra transform + obj.matrix_world = obj.matrix_world @ extra_transform + # select created object + UTIL_functions.select_certain_objects((obj, )) + + # return rail + return obj + +def create_rail_section( + bm: bmesh.types.BMesh, + is_monorail: bool, rail_radius: float, rail_span: float, + matrix: mathutils.Matrix = mathutils.Matrix.Identity(4)) -> None: + """ + Add a rail section. + + If created is monorail, the original point locate at the center of section. + Otherwise, the original point locate at the center point of the line connecting between left rail section and right rail section. + The section will be placed in XZ panel. + + If ordered is monorail, `rail_span` param will be ignored. + """ + if is_monorail: + # create monorail + bmesh.ops.create_circle( + bm, cap_ends = False, cap_tris = False, segments = 8, radius = rail_radius, + matrix = typing.cast(mathutils.Matrix, matrix @ mathutils.Matrix.LocRotScale( + None, + mathutils.Euler((math.radians(90), math.radians(22.5), 0), 'XYZ'), + None + )), + calc_uvs = False + ) + else: + # create rail + # create left rail + bmesh.ops.create_circle( + bm, cap_ends = False, cap_tris = False, segments = 8, radius = rail_radius, + matrix = typing.cast(mathutils.Matrix, matrix @ mathutils.Matrix.LocRotScale( + mathutils.Vector((-rail_span / 2, 0, 0)), + mathutils.Euler((math.radians(90), 0, 0), 'XYZ'), + None + )), + calc_uvs = False + ) + # create right rail + bmesh.ops.create_circle( + bm, cap_ends = False, cap_tris = False, segments = 8, radius = rail_radius, + matrix = typing.cast(mathutils.Matrix, matrix @ mathutils.Matrix.LocRotScale( + mathutils.Vector((rail_span / 2, 0, 0)), + mathutils.Euler((math.radians(90), 0, 0), 'XYZ'), + None + )), + calc_uvs = False + ) + +def create_transition_section( + bm: bmesh.types.BMesh, + rail_radius: float, rail_span: float) -> None: + """ + Create the transition section between rail and monorail. + """ + # create rail section + create_rail_section(bm, False, rail_radius, rail_span) + + # create monorail + # calc sink first + monorail_sink: float + try: + monorail_sink = math.sqrt((rail_radius + 2) ** 2 - (rail_span / 2) ** 2) - 2 - rail_radius + except: + monorail_sink = -2 # if sqrt(minus number) happended, it mean no triangle relation. the depth should always be -2. + # create monorail with calculated sink + create_rail_section( + bm, True, rail_radius, rail_span, + mathutils.Matrix.Translation((0, 0, monorail_sink)) + ) + +def create_straight_rail( + bm: bmesh.types.BMesh, + is_monorail: bool, rail_radius: float, rail_span: float, + rail_length: float, rail_angle: float, + rail_start_cap: bool, rail_end_cap: bool) -> None: + """ + Add a straight rail. + + The original point is same as `_add_rail_section()`. + The start terminal of this straight will be placed in XZ panel. + The expand direction is +Y. + + If ordered is monorail, `rail_span` param will be ignored. + + The rail angle is in degree unit and indicate how any angle this rail should rotated by its axis. + It usually used to create side rail. + """ + # create section first + create_rail_section( + bm, is_monorail, rail_radius, rail_span, + mathutils.Matrix.LocRotScale( + None, + mathutils.Euler((0, math.radians(rail_angle), 0), 'XYZ'), + None + ) + ) + + # get start edges + start_edges: list[bmesh.types.BMEdge] = bm.edges[:] + # extrude and get end edges + end_edges: list[bmesh.types.BMEdge] = _bmesh_extrude( + bm, + start_edges, + mathutils.Vector((0, rail_length, 0)) + ) + + # smooth geometry + _bmesh_smooth_all_edges(bm) + + # cap start and end edges if needed + if rail_start_cap: + _bmesh_cap(bm, start_edges) + if rail_end_cap: + _bmesh_cap(bm, end_edges) + +def create_transition_rail( + bm: bmesh.types.BMesh, + rail_radius: float, rail_span: float, + rail_length: float, + rail_start_cap: bool, rail_end_cap: bool) -> None: + """ + Add a transition rail. + + The original point is same as `_add_transition_section()`. + The start terminal of this straight will be placed in XZ panel. + The expand direction is +Y. + """ + # create section first + create_transition_section(bm, rail_radius, rail_span) + + # get start edges + start_edges: list[bmesh.types.BMEdge] = bm.edges[:] + # extrude and get end edges + end_edges: list[bmesh.types.BMEdge] = _bmesh_extrude( + bm, + start_edges, + mathutils.Vector((0, rail_length, 0)) + ) + + # smooth geometry + _bmesh_smooth_all_edges(bm) + + # cap start and end edges if needed + if rail_start_cap: + _bmesh_cap(bm, start_edges) + if rail_end_cap: + _bmesh_cap(bm, end_edges) + +def create_screw_rail( + bm: bmesh.types.BMesh, + is_monorail: bool, rail_radius: float, rail_span: float, + rail_start_cap: bool, rail_end_cap: bool, + rail_screw_angle: float, rail_screw_screw: float, rail_screw_iterations: int, + rail_screw_steps: int, rail_screw_radius: float, + rail_screw_flip_x: bool, rail_screw_flip_y: bool, rail_screw_flip_z: bool) -> None: + """ + Add a screw rail. + + The original point is same as `_add_rail_section()`. + The start terminal of this straight will be placed in XZ panel. + The expand direction is +Y. + + If ordered is monorail, `rail_span` param will be ignored. + + Angle is input as degree unit. + """ + # create section first + create_rail_section(bm, is_monorail, rail_radius, rail_span) + + start_edges: list[bmesh.types.BMEdge] = bm.edges[:] + end_edges: list[bmesh.types.BMEdge] = _bmesh_screw( + bm, + bm.verts[:], start_edges, + rail_screw_angle, + rail_screw_steps, rail_screw_iterations, + mathutils.Vector((rail_screw_radius, 0, 0)), + rail_screw_screw + ) + + # flip geometry + if rail_screw_flip_x or rail_screw_flip_y or rail_screw_flip_z: + _bmesh_flip_all_faces(bm, rail_screw_flip_x, rail_screw_flip_y, rail_screw_flip_z) + + # smooth geometry + _bmesh_smooth_all_edges(bm) + + # cap start and end edges if needed + if rail_start_cap: + _bmesh_cap(bm, start_edges) + if rail_end_cap: + _bmesh_cap(bm, end_edges) + +#endregion