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
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 # ------------------------------------------------------------------
21 # ------------------------------------------------------------------
22 class ARCHIMESH_OT_Stairs(Operator
):
23 bl_idname
= "mesh.archimesh_stairs"
25 bl_description
= "Stairs Generator"
27 bl_options
= {'REGISTER', 'UNDO'}
32 ('1', "Rectangular", ""),
36 description
="Type of steps",
41 default
=0.20, precision
=3,
42 description
='Radius factor for rounded',
45 name
="Include deformation handles",
46 description
="Include a curve to modify the stairs curve",
50 step_num
: IntProperty(
51 name
='Number of steps',
54 description
='Number total of steps',
56 max_width
: FloatProperty(
59 default
=1, precision
=3,
60 description
='Step maximum width',
65 default
=0.30, precision
=3,
66 description
='Depth of the step',
71 default
=1, precision
=3,
72 description
='Step shift in Y axis',
74 thickness
: FloatProperty(
77 default
=0.03, precision
=3,
78 description
='Step thickness',
81 name
="Variable width",
82 description
="Steps are not equal in width",
87 description
="Close all steps side to make a solid structure",
90 min_width
: FloatProperty(
93 default
=1, precision
=3,
94 description
='Step minimum width',
97 height
: FloatProperty(
100 default
=0.14, precision
=3,
101 description
='Step height',
103 front_gap
: FloatProperty(
108 description
='Front gap',
110 side_gap
: FloatProperty(
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",
122 # -----------------------------------------------------
123 # Draw (create UI interface)
124 # -----------------------------------------------------
125 # noinspection PyUnusedLocal
126 def draw(self
, context
):
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":
133 row
.label(text
="Warning: Imperial units not supported", icon
='COLOR_RED')
137 row
.prop(self
, 'model')
138 if self
.model
== "2":
139 row
.prop(self
, 'radio')
141 box
.prop(self
, 'step_num')
143 row
.prop(self
, 'max_width')
144 row
.prop(self
, 'depth')
145 row
.prop(self
, 'shift')
147 row
.prop(self
, 'back')
148 row
.prop(self
, 'sizev')
150 row
.prop(self
, 'curve')
152 if self
.sizev
is True:
153 row
.prop(self
, 'min_width')
157 row
.prop(self
, 'thickness')
158 row
.prop(self
, 'height')
160 row
.prop(self
, 'front_gap')
161 if self
.model
== "1":
162 row
.prop(self
, 'side_gap')
165 if not context
.scene
.render
.engine
in {'CYCLES', 'BLENDER_EEVEE'}:
167 box
.prop(self
, 'crt_mat')
170 row
.label(text
="Warning: Operator does not work in local view mode", icon
='ERROR')
172 # -----------------------------------------------------
174 # -----------------------------------------------------
175 # noinspection PyUnusedLocal
176 def execute(self
, context
):
177 if bpy
.context
.mode
== "OBJECT":
178 create_stairs_mesh(self
)
181 self
.report({'WARNING'}, "Archimesh: Option only valid in Object mode")
185 # ------------------------------------------------------------------------------
187 # All custom values are passed using self container (self.myvariable)
188 # ------------------------------------------------------------------------------
189 def create_stairs_mesh(self
):
192 for o
in bpy
.data
.objects
:
193 if o
.select_get() is True:
196 bpy
.ops
.object.select_all(action
='DESELECT')
198 # ------------------------
200 # ------------------------
201 mydata
= create_stairs(self
, "Stairs")
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 # ------------------------
212 x
= mystairs
.location
.x
213 y
= mystairs
.location
.y
214 z
= mystairs
.location
.z
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 # ------------------------
226 # ------------------------
227 if self
.crt_mat
and bpy
.context
.scene
.render
.engine
in {'CYCLES', 'BLENDER_EEVEE'}:
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
239 # ------------------------------------------------------------------------------
240 # Create rectangular Stairs
241 # ------------------------------------------------------------------------------
242 def create_stairs(self
, objname
):
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
)
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
):
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
285 width
= (self
.max_width
/ 2) - (step
* (((self
.max_width
- self
.min_width
) / 2) / self
.step_num
))
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)])
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)])
298 # calculate width (side gap)
299 width
= width
+ self
.side_gap
301 # Horizontal Rectangle
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)])
312 y
= y
+ (self
.depth
* self
.shift
)
317 # ------------------------------------------------------------------------------
318 # Create rounded step
319 # ------------------------------------------------------------------------------
320 def create_round_step(self
, origin
, myvertex
, myfaces
, index
, step
):
326 li
= [radians(270), radians(288), radians(306), radians(324), radians(342),
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
335 if self
.sizev
is True:
336 max_width
= max_width
- (step
* ((self
.max_width
- self
.min_width
) / self
.step_num
))
339 # ------------------------------------
341 # ------------------------------------
343 width
= half
- (half
* self
.radio
)
344 myradio
= half
- width
346 myvertex
.extend([(x
, y
, z
), (x
, y
, z
+ self
.height
)])
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
)])
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)])
362 # ------------------------------------
364 # ------------------------------------
365 # calculate width gap
366 width
= half
+ self
.front_gap
- (half
* self
.radio
)
370 myvertex
.extend([(x
, y
- self
.front_gap
, z
), (x
, y
- self
.front_gap
, z
+ self
.thickness
)])
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
)])
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)])
390 z
= z
+ self
.thickness
393 y
= y
+ (self
.depth
* self
.shift
)
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
]
417 point
.handle_left
= h1
418 point
.handle_right
= h2
419 point
.handle_left_type
= 'FREE'
420 point
.handle_right_type
= 'FREE'