Skip to content

Commit 75db40d

Browse files
committed
Adding Ch9 source
1 parent 80883fd commit 75db40d

9 files changed

+1148
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,326 @@
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()
Binary file not shown.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
schema_version = "1.0.0"
2+
3+
id = "barrel_pcg_demo"
4+
version = "1.0.0"
5+
name = "Barrel PCG Demo"
6+
tagline = "Ch9 of Blender Scripting with Python by Isabel Lupiani"
7+
maintainer = "Isabel Lupiani <[email protected]>"
8+
type = "add-on"
9+
10+
website = "https://github.com/iklupiani/test_add_on_remote"
11+
12+
tags = ["Modeling", "Mesh"]
13+
14+
blender_version_min = "4.2.0"
15+
16+
# License conforming to https://spdx.org/licenses/ (use "SPDX: prefix)
17+
# https://docs.blender.org/manual/en/dev/advanced/extensions/licenses.html
18+
license = [
19+
"SPDX:GPL-2.0-or-later",
20+
]
21+
22+
copyright = [
23+
"2018-2024 Isabel Lupiani",
24+
"2024 Apress",
25+
]

0 commit comments

Comments
 (0)