Fix #105009: AnimAll: Error when inserting key on string attribute
[blender-addons.git] / archimesh / achm_stairs_maker.py
blobb570dc961d15e4f7d1656a745a0682ae3ddc2f05
1 # SPDX-FileCopyrightText: 2016-2022 Blender Foundation
3 # SPDX-License-Identifier: GPL-2.0-or-later
5 # ----------------------------------------------------------
6 # Automatic generation of stairs
7 # Author: Antonio Vazquez (antonioya)
9 # ----------------------------------------------------------
10 # noinspection PyUnresolvedReferences
11 import bpy
12 from math import radians, sin, cos
13 from bpy.types import Operator
14 from bpy.props import FloatProperty, BoolProperty, IntProperty, EnumProperty
15 from .achm_tools import *
18 # ------------------------------------------------------------------
19 # Define UI class
20 # Stairs
21 # ------------------------------------------------------------------
22 class ARCHIMESH_OT_Stairs(Operator):
23 bl_idname = "mesh.archimesh_stairs"
24 bl_label = "Stairs"
25 bl_description = "Stairs Generator"
26 bl_category = 'View'
27 bl_options = {'REGISTER', 'UNDO'}
29 # Define properties
30 model: EnumProperty(
31 items=(
32 ('1', "Rectangular", ""),
33 ('2', "Rounded", ""),
35 name="Model",
36 description="Type of steps",
38 radio: FloatProperty(
39 name='',
40 min=0.001, max=0.500,
41 default=0.20, precision=3,
42 description='Radius factor for rounded',
44 curve: BoolProperty(
45 name="Include deformation handles",
46 description="Include a curve to modify the stairs curve",
47 default=False,
50 step_num: IntProperty(
51 name='Number of steps',
52 min=1, max=1000,
53 default=3,
54 description='Number total of steps',
56 max_width: FloatProperty(
57 name='Width',
58 min=0.001, max=10,
59 default=1, precision=3,
60 description='Step maximum width',
62 depth: FloatProperty(
63 name='Depth',
64 min=0.001, max=10,
65 default=0.30, precision=3,
66 description='Depth of the step',
68 shift: FloatProperty(
69 name='Shift',
70 min=0.001, max=1,
71 default=1, precision=3,
72 description='Step shift in Y axis',
74 thickness: FloatProperty(
75 name='Thickness',
76 min=0.001, max=10,
77 default=0.03, precision=3,
78 description='Step thickness',
80 sizev: BoolProperty(
81 name="Variable width",
82 description="Steps are not equal in width",
83 default=False,
85 back: BoolProperty(
86 name="Close sides",
87 description="Close all steps side to make a solid structure",
88 default=False,
90 min_width: FloatProperty(
91 name='',
92 min=0.001, max=10,
93 default=1, precision=3,
94 description='Step minimum width',
97 height: FloatProperty(
98 name='height',
99 min=0.001, max=10,
100 default=0.14, precision=3,
101 description='Step height',
103 front_gap: FloatProperty(
104 name='Front',
105 min=0, max=10,
106 default=0.03,
107 precision=3,
108 description='Front gap',
110 side_gap: FloatProperty(
111 name='Side',
112 min=0, max=10,
113 default=0, precision=3,
114 description='Side gap',
116 crt_mat: BoolProperty(
117 name="Create default Cycles materials",
118 description="Create default materials for Cycles render",
119 default=True,
122 # -----------------------------------------------------
123 # Draw (create UI interface)
124 # -----------------------------------------------------
125 # noinspection PyUnusedLocal
126 def draw(self, context):
127 layout = self.layout
128 space = bpy.context.space_data
129 if not space.local_view:
130 # Imperial units warning
131 if bpy.context.scene.unit_settings.system == "IMPERIAL":
132 row = layout.row()
133 row.label(text="Warning: Imperial units not supported", icon='COLOR_RED')
135 box = layout.box()
136 row = box.row()
137 row.prop(self, 'model')
138 if self.model == "2":
139 row.prop(self, 'radio')
141 box.prop(self, 'step_num')
142 row = box.row()
143 row.prop(self, 'max_width')
144 row.prop(self, 'depth')
145 row.prop(self, 'shift')
146 row = box.row()
147 row.prop(self, 'back')
148 row.prop(self, 'sizev')
149 row = box.row()
150 row.prop(self, 'curve')
151 # all equal
152 if self.sizev is True:
153 row.prop(self, 'min_width')
155 box = layout.box()
156 row = box.row()
157 row.prop(self, 'thickness')
158 row.prop(self, 'height')
159 row = box.row()
160 row.prop(self, 'front_gap')
161 if self.model == "1":
162 row.prop(self, 'side_gap')
164 box = layout.box()
165 if not context.scene.render.engine in {'CYCLES', 'BLENDER_EEVEE'}:
166 box.enabled = False
167 box.prop(self, 'crt_mat')
168 else:
169 row = layout.row()
170 row.label(text="Warning: Operator does not work in local view mode", icon='ERROR')
172 # -----------------------------------------------------
173 # Execute
174 # -----------------------------------------------------
175 # noinspection PyUnusedLocal
176 def execute(self, context):
177 if bpy.context.mode == "OBJECT":
178 create_stairs_mesh(self)
179 return {'FINISHED'}
180 else:
181 self.report({'WARNING'}, "Archimesh: Option only valid in Object mode")
182 return {'CANCELLED'}
185 # ------------------------------------------------------------------------------
186 # Generate mesh data
187 # All custom values are passed using self container (self.myvariable)
188 # ------------------------------------------------------------------------------
189 def create_stairs_mesh(self):
191 # deactivate others
192 for o in bpy.data.objects:
193 if o.select_get() is True:
194 o.select_set(False)
196 bpy.ops.object.select_all(action='DESELECT')
198 # ------------------------
199 # Create stairs
200 # ------------------------
201 mydata = create_stairs(self, "Stairs")
202 mystairs = mydata[0]
203 mystairs.select_set(True)
204 bpy.context.view_layer.objects.active = mystairs
205 remove_doubles(mystairs)
206 set_normals(mystairs)
207 set_modifier_mirror(mystairs, "X")
208 # ------------------------
209 # Create curve handles
210 # ------------------------
211 if self.curve:
212 x = mystairs.location.x
213 y = mystairs.location.y
214 z = mystairs.location.z
216 last = mydata[1]
217 x1 = last[1] # use y
219 myp = [((0, 0, 0), (- 0.25, 0, 0), (0.25, 0, 0)),
220 ((x1, 0, 0), (x1 - 0.25, 0, 0), (x1 + 0.25, 0, 0))] # double element
221 mycurve = create_bezier("Stairs_handle", myp, (x, y, z))
222 set_modifier_curve(mystairs, mycurve)
224 # ------------------------
225 # Create materials
226 # ------------------------
227 if self.crt_mat and bpy.context.scene.render.engine in {'CYCLES', 'BLENDER_EEVEE'}:
228 # Stairs material
229 mat = create_diffuse_material("Stairs_material", False, 0.8, 0.8, 0.8)
230 set_material(mystairs, mat)
232 bpy.ops.object.select_all(action='DESELECT')
233 mystairs.select_set(True)
234 bpy.context.view_layer.objects.active = mystairs
236 return
239 # ------------------------------------------------------------------------------
240 # Create rectangular Stairs
241 # ------------------------------------------------------------------------------
242 def create_stairs(self, objname):
244 myvertex = []
245 myfaces = []
246 index = 0
248 lastpoint = (0, 0, 0)
249 for s in range(0, self.step_num):
250 if self.model == "1":
251 mydata = create_rect_step(self, lastpoint, myvertex, myfaces, index, s)
252 if self.model == "2":
253 mydata = create_round_step(self, lastpoint, myvertex, myfaces, index, s)
254 index = mydata[0]
255 lastpoint = mydata[1]
257 mesh = bpy.data.meshes.new(objname)
258 myobject = bpy.data.objects.new(objname, mesh)
260 myobject.location = bpy.context.scene.cursor.location
261 bpy.context.collection.objects.link(myobject)
263 mesh.from_pydata(myvertex, [], myfaces)
264 mesh.update(calc_edges=True)
266 return myobject, lastpoint
269 # ------------------------------------------------------------------------------
270 # Create rectangular step
271 # ------------------------------------------------------------------------------
272 def create_rect_step(self, origin, myvertex, myfaces, index, step):
273 x = origin[0]
274 y = origin[1]
275 z = origin[2]
276 i = index
277 max_depth = y + self.depth
278 if self.back is True:
279 max_depth = self.depth * self.step_num
281 # calculate width (no side gap)
282 if self.sizev is False:
283 width = self.max_width / 2
284 else:
285 width = (self.max_width / 2) - (step * (((self.max_width - self.min_width) / 2) / self.step_num))
287 # Vertical Rectangle
288 myvertex.extend([(x, y, z), (x, y, z + self.height), (x + width, y, z + self.height), (x + width, y, z)])
289 val = y + self.thickness
290 myvertex.extend([(x, val, z), (x, val, z + self.height), (x + width, val, z + self.height), (x + width, val, z)])
292 myfaces.extend([(i + 0, i + 1, i + 2, i + 3), (i + 4, i + 5, i + 6, i + 7), (i + 0, i + 3, i + 7, i + 4),
293 (i + 1, i + 2, i + 6, i + 5), (i + 0, i + 1, i + 5, i + 4), (i + 3, i + 2, i + 6, i + 7)])
294 # Side plane
295 myvertex.extend([(x + width, max_depth, z + self.height), (x + width, max_depth, z)])
296 myfaces.extend([(i + 7, i + 6, i + 8, i + 9)])
297 i += 10
298 # calculate width (side gap)
299 width = width + self.side_gap
301 # Horizontal Rectangle
302 z = z + self.height
303 myvertex.extend([(x, y - self.front_gap, z), (x, max_depth, z), (x + width, max_depth, z),
304 (x + width, y - self.front_gap, z)])
305 z = z + self.thickness
306 myvertex.extend([(x, y - self.front_gap, z), (x, max_depth, z), (x + width, max_depth, z),
307 (x + width, y - self.front_gap, z)])
308 myfaces.extend([(i + 0, i + 1, i + 2, i + 3), (i + 4, i + 5, i + 6, i + 7), (i + 0, i + 3, i + 7, i + 4),
309 (i + 1, i + 2, i + 6, i + 5), (i + 3, i + 2, i + 6, i + 7)])
310 i += 8
311 # remap origin
312 y = y + (self.depth * self.shift)
314 return i, (x, y, z)
317 # ------------------------------------------------------------------------------
318 # Create rounded step
319 # ------------------------------------------------------------------------------
320 def create_round_step(self, origin, myvertex, myfaces, index, step):
321 x = origin[0]
322 y = origin[1]
323 z = origin[2]
324 pos_x = None
325 i = index
326 li = [radians(270), radians(288), radians(306), radians(324), radians(342),
327 radians(0)]
329 max_width = self.max_width
330 max_depth = y + self.depth
331 if self.back is True:
332 max_depth = self.depth * self.step_num
334 # Resize for width
335 if self.sizev is True:
336 max_width = max_width - (step * ((self.max_width - self.min_width) / self.step_num))
338 half = max_width / 2
339 # ------------------------------------
340 # Vertical
341 # ------------------------------------
342 # calculate width
343 width = half - (half * self.radio)
344 myradio = half - width
346 myvertex.extend([(x, y, z), (x, y, z + self.height)])
347 # Round
348 for e in li:
349 pos_x = (cos(e) * myradio) + x + width - myradio
350 pos_y = (sin(e) * myradio) + y + myradio
352 myvertex.extend([(pos_x, pos_y, z), (pos_x, pos_y, z + self.height)])
354 # back point
355 myvertex.extend([(x + width, max_depth, z), (x + width, max_depth, z + self.height)])
357 myfaces.extend([(i, i + 1, i + 3, i + 2), (i + 2, i + 3, i + 5, i + 4), (i + 4, i + 5, i + 7, i + 6),
358 (i + 6, i + 7, i + 9, i + 8),
359 (i + 8, i + 9, i + 11, i + 10), (i + 10, i + 11, i + 13, i + 12), (i + 12, i + 13, i + 15, i + 14)])
361 i += 16
362 # ------------------------------------
363 # Horizontal
364 # ------------------------------------
365 # calculate width gap
366 width = half + self.front_gap - (half * self.radio)
368 z = z + self.height
369 # Vertical
370 myvertex.extend([(x, y - self.front_gap, z), (x, y - self.front_gap, z + self.thickness)])
371 # Round
372 for e in li:
373 pos_x = (cos(e) * myradio) + x + width - myradio
374 pos_y = (sin(e) * myradio) + y + myradio - self.front_gap
376 myvertex.extend([(pos_x, pos_y, z), (pos_x, pos_y, z + self.thickness)])
378 # back points
379 myvertex.extend([(pos_x, max_depth, z), (pos_x, max_depth, z + self.thickness),
380 (x, max_depth, z), (x, max_depth, z + self.thickness)])
382 myfaces.extend([(i, i + 1, i + 3, i + 2), (i + 2, i + 3, i + 5, i + 4), (i + 4, i + 5, i + 7, i + 6),
383 (i + 6, i + 7, i + 9, i + 8),
384 (i + 8, i + 9, i + 11, i + 10), (i + 10, i + 11, i + 13, i + 12), (i + 12, i + 13, i + 15, i + 14),
385 (i, i + 2, i + 4, i + 6, i + 8, i + 10, i + 12, i + 14, i + 16),
386 (i + 1, i + 3, i + 5, i + 7, i + 9, i + 11, i + 13, i + 15, i + 17),
387 (i + 14, i + 15, i + 17, i + 16)])
389 i += 18
390 z = z + self.thickness
392 # remap origin
393 y = y + (self.depth * self.shift)
395 return i, (x, y, z)
398 # ------------------------------------------------------------------------------
399 # Create bezier curve
400 # ------------------------------------------------------------------------------
401 def create_bezier(objname, points, origin):
402 curvedata = bpy.data.curves.new(name=objname, type='CURVE')
403 curvedata.dimensions = '3D'
405 myobject = bpy.data.objects.new(objname, curvedata)
406 myobject.location = origin
407 myobject.rotation_euler[2] = radians(90)
409 bpy.context.collection.objects.link(myobject)
411 polyline = curvedata.splines.new('BEZIER')
412 polyline.bezier_points.add(len(points) - 1)
414 for idx, (knot, h1, h2) in enumerate(points):
415 point = polyline.bezier_points[idx]
416 point.co = knot
417 point.handle_left = h1
418 point.handle_right = h2
419 point.handle_left_type = 'FREE'
420 point.handle_right_type = 'FREE'
422 return myobject