diff --git a/bbp_ng/OP_ADDS_rail.py b/bbp_ng/OP_ADDS_rail.py index 72407f2..8a814ff 100644 --- a/bbp_ng/OP_ADDS_rail.py +++ b/bbp_ng/OP_ADDS_rail.py @@ -9,6 +9,27 @@ from . import UTIL_functions, UTIL_naming_convension # Equation: Sink = sqrt( ((RailRadius + BallRadius) ^ 2) - ((RailSpan / 2) ^ 2) ) - BallRadius - RailRadius # BallRadius is the radius of player ball. It always is 2. # Ref: https://tieba.baidu.com/p/6557180791 +# +# For Normal Side Rail (paper ball + wood ball can pass it): +# Rail Span: 3.864 +# Angle (between rail panel and XY panel): 79.563 degree +# For Special Side Rail (stone ball can pass it): +# Rail Span: 3.864 +# Angle (between rail panel and XY panel): 57 degree +# These infos are gotten from BallanceBug. +# +# For Side Spiral Rail, the distance between each layer is 3.6 +# Measured in Level 9 and Level 13. +# For Spiral Rail, the distance between each layer is 5 +# Measured in Level 10. + +c_DefaultRailRadius: float = 0.35 +c_DefaultRailSpan: float = 3.75 +c_SideRailSpan: float = 3.864 +c_NormalSideRailAngle: float = 79.563 +c_StoneSideRailAngle: float = 57 +c_SpiralRailScrew: float = 5 +c_SideSpiralRailScrew: float = 3.6 #region Operator Helpers @@ -28,56 +49,12 @@ class SharedRailSectionInputProperty(): default = 'RAIL', ) # type: ignore - rail_radius: bpy.props.FloatProperty( - name = "Radius", - description = "Define rail section radius", - default = 0.35, - min = 0, - unit = 'LENGTH' - ) # type: ignore - - rail_span: bpy.props.FloatProperty( - name = "Span", - description = "The length between 2 single rails.", - default = 3.75, - min = 0, - unit = 'LENGTH' - ) # type: ignore - - def draw_rail_section_input(self, layout: bpy.types.UILayout, force_monorail: bool | None) -> None: - """ - Draw rail section properties - - @param force_monorail[in] Force this draw method for monorail if True, or for rail if False. Accept None if you want user to choose it. - """ - # draw title - layout = layout.box() - layout.label(text = 'Section') - - if force_monorail is None: - # show picker to allow user pick - # force it show horizontal - row = layout.row() - row.prop(self, 'rail_type', expand = True) - # show radius - layout.prop(self, "rail_radius") - # show span for rail - if self.rail_type == 'RAIL': - layout.prop(self, "rail_span") - else: - # according to force type to show - # always show radius - layout.prop(self, "rail_radius") - # show span in condition - if not force_monorail: - layout.prop(self, "rail_span") + def draw_rail_section_input(self, layout: bpy.types.UILayout) -> None: + row = layout.row() + row.prop(self, 'rail_type', expand = True) def general_get_is_monorail(self) -> bool: return self.rail_type == 'MONORAIL' - def general_get_rail_radius(self) -> float: - return self.rail_radius - def general_get_rail_span(self) -> float: - return self.rail_span class SharedRailCapInputProperty(): """ @@ -98,9 +75,6 @@ class SharedRailCapInputProperty(): ) # type: ignore def draw_rail_cap_input(self, layout: bpy.types.UILayout) -> None: - # draw title - layout = layout.box() - layout.label(text = 'Cap') row = layout.row() row.prop(self, "rail_start_cap", toggle = 1) row.prop(self, "rail_end_cap", toggle = 1) @@ -125,9 +99,6 @@ class SharedStraightRailInputProperty(): ) # type: ignore def draw_straight_rail_input(self, layout: bpy.types.UILayout) -> None: - # draw title - layout = layout.box() - layout.label(text = 'Straight Rail') layout.prop(self, "rail_length") def general_get_rail_length(self) -> float: @@ -138,69 +109,24 @@ class SharedScrewRailInputProperty(): The properties for straight rail. """ - rail_screw_angle: bpy.props.FloatProperty( - name = "Angle", - description = "The angle of this screw rail rotated in one interation.", - default = 90, - subtype = 'ANGLE', - ) # type: ignore - - rail_screw_screw: bpy.props.FloatProperty( - name = "Screw", - description = "The increased height in each iteration. Minus height also is accepted.", - default = 6, - unit = 'LENGTH' - ) # type: ignore - - rail_screw_iterations: bpy.props.IntProperty( - name = "Iterations", - description = "The angle of this screw rail rotated in one interation.", - default = 1, - min = 1, - ) # type: ignore - rail_screw_steps: bpy.props.IntProperty( name = "Steps", - description = "The segment count per iteration.", - default = 20, + description = "The segment count per iteration. More segment, more smooth but lower performance.", + default = 16, min = 1, ) # type: ignore rail_screw_radius: bpy.props.FloatProperty( name = "Radius", description = "The screw radius. Minus radius will flip the built screw.", - default = 10, + default = 5, unit = 'LENGTH' ) # type: ignore - def draw_screw_rail_input(self, layout: bpy.types.UILayout, show_for_screw: bool) -> None: - # draw title - layout = layout.box() - layout.label(text = 'Screw Rail') + def draw_screw_rail_input(self, layout: bpy.types.UILayout) -> None: + layout.prop(self, "rail_screw_radius") + layout.prop(self, "rail_screw_steps") - if show_for_screw: - # screw do not need angle property - layout.prop(self, "rail_screw_screw") - layout.prop(self, "rail_screw_iterations") - layout.prop(self, "rail_screw_radius") - layout.prop(self, "rail_screw_steps") - else: - # curve do not need iterations (always is 1) - # and do not need screw (always is 0) - layout.prop(self, "rail_screw_angle") - layout.prop(self, "rail_screw_radius") - layout.prop(self, "rail_screw_steps") - - # Getter should return default value if corresponding field - # is not existing in that mode. - - def general_get_rail_screw_angle(self, is_for_screw: bool) -> float: - """This function return angle in degree unit.""" - return 360 if is_for_screw else self.rail_screw_angle - def general_get_rail_screw_screw(self, is_for_screw: bool) -> float: - return self.rail_screw_screw if is_for_screw else 0 - def general_get_rail_screw_iterations(self, is_for_screw: bool) -> int: - return self.rail_screw_iterations if is_for_screw else 1 def general_get_rail_screw_radius(self) -> float: return self.rail_screw_radius def general_get_rail_screw_steps(self) -> int: @@ -219,17 +145,17 @@ class BBP_OT_add_rail_section(SharedRailSectionInputProperty, bpy.types.Operator def execute(self, context): _rail_creator_wrapper( lambda bm: _create_rail_section( - bm, - self.general_get_is_monorail(), self.general_get_rail_radius(), self.general_get_rail_span() + bm, self.general_get_is_monorail(), + c_DefaultRailRadius, c_DefaultRailSpan ) ) return {'FINISHED'} def draw(self, context): layout = self.layout - self.draw_rail_section_input(layout, None) + self.draw_rail_section_input(layout) -class BBP_OT_add_transition_section(SharedRailSectionInputProperty, bpy.types.Operator): +class BBP_OT_add_transition_section(bpy.types.Operator): """Add Transition Section""" bl_idname = "bbp.add_transition_section" bl_label = "Transition Section" @@ -237,17 +163,13 @@ class BBP_OT_add_transition_section(SharedRailSectionInputProperty, bpy.types.Op def execute(self, context): _rail_creator_wrapper( - lambda bm: _create_transition_section( - bm, - self.general_get_rail_radius(), self.general_get_rail_span() - ) + lambda bm: _create_transition_section(bm, c_DefaultRailRadius, c_DefaultRailSpan) ) return {'FINISHED'} def draw(self, context): layout = self.layout - # force show double rail params - self.draw_rail_section_input(layout, False) + layout.label(text = 'No Options Available') class BBP_OT_add_straight_rail(SharedRailSectionInputProperty, SharedRailCapInputProperty, SharedStraightRailInputProperty, bpy.types.Operator): """Add Straight Rail""" @@ -259,8 +181,8 @@ class BBP_OT_add_straight_rail(SharedRailSectionInputProperty, SharedRailCapInpu _rail_creator_wrapper( lambda bm: _create_straight_rail( bm, - self.general_get_is_monorail(), self.general_get_rail_radius(), self.general_get_rail_span(), - self.general_get_rail_length(), + self.general_get_is_monorail(), c_DefaultRailRadius, c_DefaultRailSpan, + self.general_get_rail_length(), 0, self.general_get_rail_start_cap(), self.general_get_rail_end_cap() ) ) @@ -268,25 +190,71 @@ class BBP_OT_add_straight_rail(SharedRailSectionInputProperty, SharedRailCapInpu def draw(self, context): layout = self.layout - self.draw_rail_section_input(layout, None) - layout.separator() - self.draw_rail_cap_input(layout) - layout.separator() + layout.label(text = 'Straight Rail') + 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) -class BBP_OT_add_screw_rail(SharedRailSectionInputProperty, SharedRailCapInputProperty, SharedScrewRailInputProperty, bpy.types.Operator): - """Add Screw Rail""" - bl_idname = "bbp.add_screw_rail" - bl_label = "Screw Rail" +class BBP_OT_add_side_rail(SharedRailCapInputProperty, SharedStraightRailInputProperty, bpy.types.Operator): + """Add Side Rail""" + bl_idname = "bbp.add_side_rail" + bl_label = "Side Rail" bl_options = {'REGISTER', 'UNDO'} + side_rail_type: bpy.props.EnumProperty( + name = "Side Type", + description = "Side rail type", + items = [ + ('NORMAL', "Normal", "The normal side rail."), + ('STONE', "Stone Specific", "The side rail which also allow stone ball passed."), + ], + default = 'NORMAL', + ) # type: ignore + + def execute(self, context): + _rail_creator_wrapper( + lambda bm: _create_straight_rail( + bm, + False, c_DefaultRailRadius, c_DefaultRailSpan, + self.general_get_rail_length(), + c_NormalSideRailAngle if self.side_rail_type == 'NORMAL' else c_StoneSideRailAngle, + self.general_get_rail_start_cap(), self.general_get_rail_end_cap() + ) + ) + return {'FINISHED'} + + def draw(self, context): + layout = self.layout + layout.label(text = 'Side Rail') + 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) + +class BBP_OT_add_arc_rail(SharedRailSectionInputProperty, SharedRailCapInputProperty, SharedScrewRailInputProperty, bpy.types.Operator): + """Add Arc Rail""" + bl_idname = "bbp.add_arc_rail" + bl_label = "Arc Rail" + bl_options = {'REGISTER', 'UNDO'} + + rail_screw_angle: bpy.props.FloatProperty( + name = "Angle", + description = "The angle of this arc rail rotated.", + default = math.radians(90), + min = 0, max = math.radians(360), + subtype = 'ANGLE', + ) # type: ignore + def execute(self, context): _rail_creator_wrapper( lambda bm: _create_screw_rail( bm, - self.general_get_is_monorail(), self.general_get_rail_radius(), self.general_get_rail_span(), + self.general_get_is_monorail(), c_DefaultRailRadius, c_DefaultRailSpan, self.general_get_rail_start_cap(), self.general_get_rail_end_cap(), - self.general_get_rail_screw_angle(True), self.general_get_rail_screw_screw(True), self.general_get_rail_screw_iterations(True), + 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() ) ) @@ -294,11 +262,91 @@ class BBP_OT_add_screw_rail(SharedRailSectionInputProperty, SharedRailCapInputPr def draw(self, context): layout = self.layout - self.draw_rail_section_input(layout, None) + layout.label(text = 'Arc Rail') + self.draw_rail_section_input(layout) + self.draw_screw_rail_input(layout) + layout.prop(self, "rail_screw_angle") layout.separator() + layout.label(text = 'Rail Cap') self.draw_rail_cap_input(layout) + +class BBP_OT_add_spiral_rail(SharedRailCapInputProperty, SharedScrewRailInputProperty, bpy.types.Operator): + """Add Spiral Rail""" + bl_idname = "bbp.add_spiral_rail" + bl_label = "Spiral Rail" + bl_options = {'REGISTER', 'UNDO'} + + rail_screw_screw: bpy.props.FloatProperty( + name = "Screw", + description = "The increased height in each iteration. Minus height also is accepted.", + default = c_SpiralRailScrew, + unit = 'LENGTH' + ) # type: ignore + + rail_screw_iterations: bpy.props.IntProperty( + name = "Iterations", + description = "Indicate how many layers of this spiral rail should be generated.", + default = 1, + min = 1, + ) # type: ignore + + def execute(self, context): + _rail_creator_wrapper( + lambda bm: _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() + ) + ) + return {'FINISHED'} + + def draw(self, context): + layout = self.layout + layout.label(text = 'Spiral Rail') + self.draw_screw_rail_input(layout) + layout.prop(self, "rail_screw_screw") + layout.prop(self, "rail_screw_iterations") layout.separator() - self.draw_screw_rail_input(layout, True) + layout.label(text = 'Rail Cap') + self.draw_rail_cap_input(layout) + +class BBP_OT_add_side_spiral_rail(SharedRailSectionInputProperty, SharedRailCapInputProperty, SharedScrewRailInputProperty, bpy.types.Operator): + """Add Side Spiral Rail""" + bl_idname = "bbp.add_side_spiral_rail" + bl_label = "Side Spiral Rail" + bl_options = {'REGISTER', 'UNDO'} + + rail_screw_iterations: bpy.props.IntProperty( + name = "Iterations", + description = "Indicate how many layers of this spiral rail should be generated.", + default = 2, + # at least 2 ietrations can create 1 useful side spiral rail. + # becuase side spiral rail is edge shared. + min = 2, + ) # type: ignore + + def execute(self, context): + _rail_creator_wrapper( + lambda bm: _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() + ) + ) + return {'FINISHED'} + + def draw(self, context): + layout = self.layout + layout.label(text = 'Spiral Rail') + self.draw_screw_rail_input(layout) + layout.prop(self, "rail_screw_iterations") + layout.separator() + layout.label(text = 'Rail Cap') + self.draw_rail_cap_input(layout) #endregion @@ -333,6 +381,9 @@ def _bmesh_screw( 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, @@ -340,7 +391,7 @@ def _bmesh_screw( 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 = angle * iterations, + angle = math.radians(angle) * iterations, space = mathutils.Matrix.Identity(4), steps = steps * iterations, use_merge = False, @@ -487,16 +538,29 @@ def _create_transition_section( def _create_straight_rail( bm: bmesh.types.BMesh, is_monorail: bool, rail_radius: float, rail_span: float, - rail_length: 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) + _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[:] @@ -522,10 +586,14 @@ def _create_screw_rail( 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) @@ -534,7 +602,7 @@ def _create_screw_rail( end_edges: list[bmesh.types.BMEdge] = _bmesh_screw( bm, bm.verts[:], start_edges, - math.radians(rail_screw_angle), + rail_screw_angle, rail_screw_steps, rail_screw_iterations, mathutils.Vector((rail_screw_radius, 0, 0)), rail_screw_screw @@ -553,12 +621,22 @@ def _create_screw_rail( def register(): bpy.utils.register_class(BBP_OT_add_rail_section) bpy.utils.register_class(BBP_OT_add_transition_section) + bpy.utils.register_class(BBP_OT_add_straight_rail) - bpy.utils.register_class(BBP_OT_add_screw_rail) + bpy.utils.register_class(BBP_OT_add_side_rail) + + bpy.utils.register_class(BBP_OT_add_arc_rail) + bpy.utils.register_class(BBP_OT_add_spiral_rail) + bpy.utils.register_class(BBP_OT_add_side_spiral_rail) def unregister(): - bpy.utils.unregister_class(BBP_OT_add_screw_rail) + bpy.utils.unregister_class(BBP_OT_add_side_spiral_rail) + bpy.utils.unregister_class(BBP_OT_add_spiral_rail) + bpy.utils.unregister_class(BBP_OT_add_arc_rail) + + bpy.utils.unregister_class(BBP_OT_add_side_rail) bpy.utils.unregister_class(BBP_OT_add_straight_rail) + bpy.utils.unregister_class(BBP_OT_add_transition_section) bpy.utils.unregister_class(BBP_OT_add_rail_section) diff --git a/bbp_ng/__init__.py b/bbp_ng/__init__.py index e6a0554..c20058c 100644 --- a/bbp_ng/__init__.py +++ b/bbp_ng/__init__.py @@ -73,14 +73,20 @@ class BBP_MT_AddRailMenu(bpy.types.Menu): def draw(self, context): layout = self.layout - layout.label(text = "Sections") + layout.label(text = "Sections", icon = 'MESH_CIRCLE') layout.operator(OP_ADDS_rail.BBP_OT_add_rail_section.bl_idname) layout.operator(OP_ADDS_rail.BBP_OT_add_transition_section.bl_idname) layout.separator() - layout.label(text = "Rails") + layout.label(text = "Straight Rails", icon = 'IPO_CONSTANT') layout.operator(OP_ADDS_rail.BBP_OT_add_straight_rail.bl_idname) - layout.operator(OP_ADDS_rail.BBP_OT_add_screw_rail.bl_idname) + layout.operator(OP_ADDS_rail.BBP_OT_add_side_rail.bl_idname) + + layout.separator() + layout.label(text = "Curve Rails", icon = 'MOD_SCREW') + layout.operator(OP_ADDS_rail.BBP_OT_add_arc_rail.bl_idname) + layout.operator(OP_ADDS_rail.BBP_OT_add_spiral_rail.bl_idname) + layout.operator(OP_ADDS_rail.BBP_OT_add_side_spiral_rail.bl_idname) class BBP_MT_AddComponentsMenu(bpy.types.Menu): """Add Ballance Components"""