|
| 1 | +# ##### BEGIN GPL LICENSE BLOCK ##### |
| 2 | +# |
| 3 | +# GNU GPLv3, 29 June 2007 |
| 4 | +# |
| 5 | +# Examples from Ch9 of the book "Blender Scripting with Python" by Isabel Lupiani. |
| 6 | +# Copyright (C) 2024 Isabel Lupiani, Apress. |
| 7 | +# |
| 8 | +# This program is free software: you can redistribute it and/or modify |
| 9 | +# it under the terms of the GNU General Public License as published by |
| 10 | +# the Free Software Foundation, either version 3 of the License, or |
| 11 | +# (at your option) any later version. |
| 12 | +# |
| 13 | +# This program is distributed in the hope that it will be useful, |
| 14 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 15 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 16 | +# GNU General Public License for more details. |
| 17 | +# |
| 18 | +# You should have received a copy of the GNU General Public License |
| 19 | +# along with this program. If not, see <https://www.gnu.org/licenses/>. |
| 20 | +# |
| 21 | +# ##### END GPL LICENSE BLOCK ##### |
| 22 | + |
| 23 | +if "bpy" in locals(): |
| 24 | + import importlib |
| 25 | + importlib.reload(creating_and_editing_mesh_objs) |
| 26 | + importlib.reload(utils) |
| 27 | +else: |
| 28 | + from .creating_and_editing_mesh_objs import add_circle, get_placeholder_mesh_obj_and_bm |
| 29 | + from .utils import set_viewport_rotation |
| 30 | + |
| 31 | +import bmesh |
| 32 | +import bpy |
| 33 | +from bpy_extras import view3d_utils |
| 34 | +from bpy.props import StringProperty, FloatProperty, IntProperty |
| 35 | +from bpy.types import Operator |
| 36 | + |
| 37 | +from enum import IntEnum, unique |
| 38 | +from mathutils import Vector, Euler |
| 39 | +from math import radians |
| 40 | + |
| 41 | +@unique |
| 42 | +class BarrelGenSteps(IntEnum): |
| 43 | + Before = 0 |
| 44 | + CrossSections = 1 |
| 45 | + TopBottomFaces = 2 |
| 46 | + BridgeLoops = 3 |
| 47 | + SubdivSmooth = 4 |
| 48 | + Whole = 5 |
| 49 | + |
| 50 | +demo_steps_msgs = {BarrelGenSteps.Before: "Welcome to the Barrel Generation Demo", |
| 51 | + BarrelGenSteps.CrossSections: "Create Circular Cross-Sections", |
| 52 | + BarrelGenSteps.TopBottomFaces: "Fill in Top and Bottom Faces", |
| 53 | + BarrelGenSteps.BridgeLoops: "Bridge the Cross-Sections with Edge Loops", |
| 54 | + BarrelGenSteps.SubdivSmooth: "Subdivide Smooth", |
| 55 | + BarrelGenSteps.Whole: "Complete Model"} |
| 56 | + |
| 57 | +default_barrel_name = "barrel_obj" |
| 58 | +modal_text_obj_name = "modal_text_obj" |
| 59 | + |
| 60 | +def hide_all_meshes(context): |
| 61 | + for obj in context.scene.objects: |
| 62 | + if obj.type == 'MESH': |
| 63 | + obj.hide_set(True) |
| 64 | + |
| 65 | +def generate_barrel(context, name, radius_end, radius_mid, height, num_segments, center, step): |
| 66 | + if len(name) < 1: |
| 67 | + name = default_barrel_name |
| 68 | + |
| 69 | + bm, barrel_obj = get_placeholder_mesh_obj_and_bm(context, name, center) |
| 70 | + if step.value < BarrelGenSteps.Whole.value: |
| 71 | + barrel_obj.name += ("_" + str(step.value) + "_" + step.name) |
| 72 | + |
| 73 | + if step.value > BarrelGenSteps.Before.value: |
| 74 | + bottom_cap_verts = add_circle(bm, radius_end, num_segments, -height/2) |
| 75 | + add_circle(bm, radius_mid, num_segments, 0) |
| 76 | + top_cap_verts = add_circle(bm, radius_end, num_segments, height/2) |
| 77 | + |
| 78 | + if step.value > BarrelGenSteps.CrossSections.value: |
| 79 | + bm.faces.new(top_cap_verts) |
| 80 | + bm.faces.new(bottom_cap_verts) |
| 81 | + |
| 82 | + if step.value > BarrelGenSteps.TopBottomFaces.value: |
| 83 | + bmesh.ops.bridge_loops(bm, edges=bm.edges) |
| 84 | + bmesh.ops.recalc_face_normals(bm, faces=bm.faces) |
| 85 | + |
| 86 | + if step.value > BarrelGenSteps.BridgeLoops.value: |
| 87 | + bpy.ops.mesh.select_all(action='SELECT') |
| 88 | + bpy.ops.mesh.subdivide(smoothness=1) |
| 89 | + |
| 90 | + bmesh.update_edit_mesh(barrel_obj.data) |
| 91 | + bpy.ops.object.mode_set(mode='OBJECT') |
| 92 | + |
| 93 | +def set_view(context): |
| 94 | + # Use the same viewport rotation as the default Cube object. |
| 95 | + utils.set_viewport_rotation(context, context.scene.cursor.location, wxyz=(0.716, 0.439, 0.291, 0.459), view_persp='PERSP', view_dist=30.0) |
| 96 | + |
| 97 | +class GenerateBarrelOperator(Operator): |
| 98 | + bl_idname = "mesh.generate_barrel" |
| 99 | + bl_label = "Generate Barrel" |
| 100 | + """Unwrap Side UV for the Face Mesh from View""" |
| 101 | + |
| 102 | + def execute(self, context): |
| 103 | + generate_barrel(context, context.scene.barrel_name, context.scene.barrel_radius_ends, \ |
| 104 | + context.scene.barrel_radius_mid, context.scene.barrel_height, context.scene.barrel_segments, \ |
| 105 | + context.scene.cursor.location, BarrelGenSteps.Whole) |
| 106 | + self.report({'INFO'}, "Barrel generated.") |
| 107 | + return {'FINISHED'} |
| 108 | + |
| 109 | + def invoke(self, context, event): |
| 110 | + hide_all_meshes(context) |
| 111 | + set_view(context) |
| 112 | + return self.execute(context) |
| 113 | + |
| 114 | +def get_text_obj_loc(context): |
| 115 | + z_offset = context.scene.barrel_height * 0.5 + 3 |
| 116 | + x, y, z = context.scene.cursor.location |
| 117 | + return Vector((x, y, z+z_offset)) |
| 118 | + |
| 119 | +def init_text_obj(context, text, location): |
| 120 | + curve_name = modal_text_obj_name + "_curve" |
| 121 | + curve_obj = bpy.data.curves[curve_name] if bpy.data.curves.find(curve_name) >= 0 else bpy.data.curves.new(curve_name, 'FONT') |
| 122 | + curve_obj.fill_mode = 'FRONT' |
| 123 | + curve_obj.body = text |
| 124 | + curve_obj.align_x = 'CENTER' |
| 125 | + curve_obj.align_y = 'CENTER' |
| 126 | + |
| 127 | + text_obj = bpy.data.objects[modal_text_obj_name] if bpy.data.objects.find(modal_text_obj_name) >= 0 else bpy.data.objects.new(modal_text_obj_name, curve_obj) |
| 128 | + text_obj.location = location |
| 129 | + text_obj.data.size = 1.5 |
| 130 | + text_obj.rotation_euler = Euler((radians(d) for d in [90, 0, 90]), 'XYZ') |
| 131 | + if context.collection.objects.find(modal_text_obj_name) < 0: |
| 132 | + context.collection.objects.link(text_obj) |
| 133 | + |
| 134 | + cur_active_obj = context.view_layer.objects.active |
| 135 | + context.view_layer.objects.active = text_obj |
| 136 | + bpy.ops.material.new() |
| 137 | + mat = bpy.data.materials[-1] |
| 138 | + mat.name = modal_text_obj_name + "_mat" |
| 139 | + mat.diffuse_color = (1, 1, 1, 1) |
| 140 | + bpy.ops.object.material_slot_add() |
| 141 | + text_obj.material_slots[-1].material = mat |
| 142 | + text_obj.active_material_index = text_obj.data.materials.find(mat.name) |
| 143 | + context.view_layer.objects.active = cur_active_obj |
| 144 | + |
| 145 | + text_obj.hide_set(False) |
| 146 | + bpy.types.Scene.text_obj = text_obj |
| 147 | + |
| 148 | +def update_text_obj(context, new_text, new_location): |
| 149 | + context.scene.text_obj.hide_set(False) |
| 150 | + context.scene.text_obj.data.body = new_text |
| 151 | + context.scene.text_obj.location = new_location |
| 152 | + |
| 153 | +def clear_text_obj(context): |
| 154 | + context.scene.text_obj.data.body = "" |
| 155 | + context.scene.text_obj.hide_set(True) |
| 156 | + |
| 157 | +class BarrelDemoInteractive(Operator): |
| 158 | + bl_idname = "modal.barrel_demo_interactive" |
| 159 | + bl_label = "Barrel Demo Interactive" |
| 160 | + """PCG Barrel Interactive Demo""" |
| 161 | + |
| 162 | + def modal(self, context, event): |
| 163 | + if event.type == 'ESC': |
| 164 | + self.cancel(context) |
| 165 | + return {'CANCELLED'} |
| 166 | + |
| 167 | + # Down arrow to advance one step in the barrel generation. |
| 168 | + if event.type == 'DOWN_ARROW' and event.value == 'RELEASE': |
| 169 | + if self._current_step == BarrelGenSteps.Whole: |
| 170 | + self.cancel(context) |
| 171 | + return {'FINISHED'} |
| 172 | + else: |
| 173 | + self._current_step = BarrelGenSteps(self._current_step.value+1) |
| 174 | + |
| 175 | + generate_barrel(context, context.scene.barrel_name, context.scene.barrel_radius_ends, \ |
| 176 | + context.scene.barrel_radius_mid, context.scene.barrel_height, \ |
| 177 | + context.scene.barrel_segments, context.scene.cursor.location, self._current_step) |
| 178 | + |
| 179 | + update_text_obj(context, demo_steps_msgs[self._current_step], get_text_obj_loc(context)) |
| 180 | + |
| 181 | + return {'RUNNING_MODAL'} |
| 182 | + |
| 183 | + def invoke(self, context, event): |
| 184 | + hide_all_meshes(context) |
| 185 | + set_view(context) |
| 186 | + |
| 187 | + context.window_manager.modal_handler_add(self) |
| 188 | + self._current_step = BarrelGenSteps.Before |
| 189 | + init_text_obj(context, demo_steps_msgs[self._current_step] + "\nPress Down Arrow to Continue and Esc to Quit.", context.scene.cursor.location) |
| 190 | + |
| 191 | + self.report({'INFO'}, "Barrel Demo Interactive: Invoke.") |
| 192 | + |
| 193 | + return {'RUNNING_MODAL'} |
| 194 | + |
| 195 | + def cancel(self, context): |
| 196 | + clear_text_obj(context) |
| 197 | + |
| 198 | +class BarrelDemoTimelapse(Operator): |
| 199 | + bl_idname = "modal.barrel_demo_timelapse" |
| 200 | + bl_label = "Barrel Demo Timelapse" |
| 201 | + """PCG Barrel Demo Timelapse""" |
| 202 | + |
| 203 | + def modal(self, context, event): |
| 204 | + if event.type == 'ESC': |
| 205 | + self.cancel(context) |
| 206 | + return {'CANCELLED'} |
| 207 | + |
| 208 | + if event.type == 'TIMER': |
| 209 | + if self._current_step == BarrelGenSteps.Whole: |
| 210 | + self.cancel(context) |
| 211 | + return {'FINISHED'} |
| 212 | + else: |
| 213 | + self._current_step = BarrelGenSteps(self._current_step.value+1) |
| 214 | + |
| 215 | + generate_barrel(context, context.scene.barrel_name, context.scene.barrel_radius_ends, \ |
| 216 | + context.scene.barrel_radius_mid, context.scene.barrel_height, \ |
| 217 | + context.scene.barrel_segments, context.scene.cursor.location, self._current_step) |
| 218 | + |
| 219 | + update_text_obj(context, demo_steps_msgs[self._current_step], get_text_obj_loc(context)) |
| 220 | + |
| 221 | + return {'RUNNING_MODAL'} |
| 222 | + |
| 223 | + def invoke(self, context, event): |
| 224 | + hide_all_meshes(context) |
| 225 | + set_view(context) |
| 226 | + |
| 227 | + wm = context.window_manager |
| 228 | + self._timer = wm.event_timer_add(time_step=1, window=context.window) |
| 229 | + wm.modal_handler_add(self) |
| 230 | + self._current_step = BarrelGenSteps.Before |
| 231 | + init_text_obj(context, demo_steps_msgs[self._current_step], get_text_obj_loc(context)) |
| 232 | + |
| 233 | + self.report({'INFO'}, "Barrel Demo Timelapse: Invoke.") |
| 234 | + |
| 235 | + return {'RUNNING_MODAL'} |
| 236 | + |
| 237 | + def cancel(self, context): |
| 238 | + clear_text_obj(context) |
| 239 | + if self._timer: |
| 240 | + context.window_manager.event_timer_remove(self._timer) |
| 241 | + self._timer = None |
| 242 | + |
| 243 | +class BARREL_GENERATOR_PT_ToolPanel(bpy.types.Panel): |
| 244 | + bl_idname = "BARREL_GENERATOR_PT_ToolPanel" |
| 245 | + bl_label = "BARREL_GENERATOR" |
| 246 | + bl_space_type = 'VIEW_3D' |
| 247 | + bl_region_type = 'UI' |
| 248 | + bl_category = 'Tool' |
| 249 | + """Barrel Generator Tool""" |
| 250 | + |
| 251 | + def draw(self, context): |
| 252 | + layout = self.layout |
| 253 | + col0 = layout.column() |
| 254 | + box0 = col0.box() |
| 255 | + |
| 256 | + box0.label(text = 'Barrel Generator') |
| 257 | + box0_row0 = box0.row(align=True) |
| 258 | + box0_row0.prop(context.scene, 'barrel_name') |
| 259 | + box0_row0 = box0.row(align=True) |
| 260 | + box0_row0.prop(context.scene, 'barrel_radius_ends') |
| 261 | + box0_row1 = box0.row(align=True) |
| 262 | + box0_row1.prop(context.scene, 'barrel_radius_mid') |
| 263 | + box0_row1 = box0.row(align=True) |
| 264 | + box0_row1.prop(context.scene, 'barrel_height') |
| 265 | + box0_row1 = box0.row(align=True) |
| 266 | + box0_row1.prop(context.scene, 'barrel_segments') |
| 267 | + |
| 268 | + box0.operator('mesh.generate_barrel', icon='MESH_DATA') |
| 269 | + box0.operator('modal.barrel_demo_interactive') |
| 270 | + box0.operator('modal.barrel_demo_timelapse') |
| 271 | + |
| 272 | +def init_scene_vars(): |
| 273 | + bpy.types.Scene.barrel_name = StringProperty( |
| 274 | + name="Barrel Name", |
| 275 | + description="Enter name for the barrel object.", |
| 276 | + default=default_barrel_name, |
| 277 | + subtype='NONE') |
| 278 | + |
| 279 | + bpy.types.Scene.barrel_radius_ends = bpy.props.FloatProperty( |
| 280 | + name="Radius Top/Bottom", |
| 281 | + description="Radius of the top/bottom of the barrel.", |
| 282 | + default=5.0, |
| 283 | + min=1.0) |
| 284 | + |
| 285 | + bpy.types.Scene.barrel_radius_mid = bpy.props.FloatProperty( |
| 286 | + name="Radius Middle", |
| 287 | + description="Radius of the middle of the barrel.", |
| 288 | + default=7.0, |
| 289 | + min=1.0) |
| 290 | + |
| 291 | + bpy.types.Scene.barrel_height = bpy.props.FloatProperty( |
| 292 | + name="Height", |
| 293 | + description="Height of the barrel.", |
| 294 | + default=8.0, |
| 295 | + min=1.0) |
| 296 | + |
| 297 | + bpy.types.Scene.barrel_segments = bpy.props.IntProperty( |
| 298 | + name="Segments", |
| 299 | + description="Number of circular segments for the barrel.", |
| 300 | + default=16, |
| 301 | + min=4) |
| 302 | + |
| 303 | + bpy.types.Scene.text_obj = None |
| 304 | + |
| 305 | +def del_scene_vars(): |
| 306 | + del bpy.types.Scene.text_obj |
| 307 | + del bpy.types.Scene.barrel_name |
| 308 | + del bpy.types.Scene.barrel_radius_ends |
| 309 | + del bpy.types.Scene.barrel_radius_mid |
| 310 | + del bpy.types.Scene.barrel_height |
| 311 | + del bpy.types.Scene.barrel_segments |
| 312 | + |
| 313 | +classes = [GenerateBarrelOperator, BarrelDemoInteractive, BarrelDemoTimelapse, BARREL_GENERATOR_PT_ToolPanel] |
| 314 | + |
| 315 | +def register(): |
| 316 | + for c in classes: |
| 317 | + bpy.utils.register_class(c) |
| 318 | + init_scene_vars() |
| 319 | + |
| 320 | +def unregister(): |
| 321 | + for c in classes: |
| 322 | + bpy.utils.unregister_class(c) |
| 323 | + del_scene_vars() |
| 324 | + |
| 325 | +if __name__ == "__main__": |
| 326 | + register() |
0 commit comments