Cleanup: minor wording clarification for OBJ import
[blender-addons.git] / archipack / archipack_fence.py
blob2cd9e227a5e6856900d74f99e0b4e7f272cbbdf1
1 # -*- coding:utf-8 -*-
3 # ##### BEGIN GPL LICENSE BLOCK #####
5 # This program is free software; you can redistribute it and/or
6 # modify it under the terms of the GNU General Public License
7 # as published by the Free Software Foundation; either version 2
8 # of the License, or (at your option) any later version.
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
15 # You should have received a copy of the GNU General Public License
16 # along with this program; if not, write to the Free Software Foundation,
17 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110- 1301, USA.
19 # ##### END GPL LICENSE BLOCK #####
21 # <pep8 compliant>
23 # ----------------------------------------------------------
24 # Author: Stephen Leger (s-leger)
26 # ----------------------------------------------------------
27 # noinspection PyUnresolvedReferences
28 import bpy
29 # noinspection PyUnresolvedReferences
30 from bpy.types import Operator, PropertyGroup, Mesh, Panel
31 from bpy.props import (
32 FloatProperty, BoolProperty, IntProperty, CollectionProperty,
33 StringProperty, EnumProperty, FloatVectorProperty
35 from .bmesh_utils import BmeshEdit as bmed
36 from .panel import Panel as Lofter
37 from mathutils import Vector, Matrix
38 from mathutils.geometry import interpolate_bezier
39 from math import sin, cos, pi, acos, atan2
40 from .archipack_manipulator import Manipulable, archipack_manipulator
41 from .archipack_2d import Line, Arc
42 from .archipack_preset import ArchipackPreset, PresetMenuOperator
43 from .archipack_object import ArchipackCreateTool, ArchipackObject
46 class Fence():
48 def __init__(self):
49 # total distance from start
50 self.dist = 0
51 self.t_start = 0
52 self.t_end = 0
53 self.dz = 0
54 self.z0 = 0
56 def set_offset(self, offset, last=None):
57 """
58 Offset line and compute intersection point
59 between segments
60 """
61 self.line = self.make_offset(offset, last)
63 @property
64 def t_diff(self):
65 return self.t_end - self.t_start
67 def straight_fence(self, a0, length):
68 s = self.straight(length).rotate(a0)
69 return StraightFence(s.p, s.v)
71 def curved_fence(self, a0, da, radius):
72 n = self.normal(1).rotate(a0).scale(radius)
73 if da < 0:
74 n.v = -n.v
75 a0 = n.angle
76 c = n.p - n.v
77 return CurvedFence(c, radius, a0, da)
80 class StraightFence(Fence, Line):
81 def __str__(self):
82 return "t_start:{} t_end:{} dist:{}".format(self.t_start, self.t_end, self.dist)
84 def __init__(self, p, v):
85 Fence.__init__(self)
86 Line.__init__(self, p, v)
89 class CurvedFence(Fence, Arc):
90 def __str__(self):
91 return "t_start:{} t_end:{} dist:{}".format(self.t_start, self.t_end, self.dist)
93 def __init__(self, c, radius, a0, da):
94 Fence.__init__(self)
95 Arc.__init__(self, c, radius, a0, da)
98 class FenceSegment():
99 def __str__(self):
100 return "t_start:{} t_end:{} n_step:{} t_step:{} i_start:{} i_end:{}".format(
101 self.t_start, self.t_end, self.n_step, self.t_step, self.i_start, self.i_end)
103 def __init__(self, t_start, t_end, n_step, t_step, i_start, i_end):
104 self.t_start = t_start
105 self.t_end = t_end
106 self.n_step = n_step
107 self.t_step = t_step
108 self.i_start = i_start
109 self.i_end = i_end
112 class FenceGenerator():
114 def __init__(self, parts):
115 self.parts = parts
116 self.segs = []
117 self.length = 0
118 self.user_defined_post = None
119 self.user_defined_uvs = None
120 self.user_defined_mat = None
122 def add_part(self, part):
124 if len(self.segs) < 1:
125 s = None
126 else:
127 s = self.segs[-1]
129 # start a new fence
130 if s is None:
131 if part.type == 'S_FENCE':
132 p = Vector((0, 0))
133 v = part.length * Vector((cos(part.a0), sin(part.a0)))
134 s = StraightFence(p, v)
135 elif part.type == 'C_FENCE':
136 c = -part.radius * Vector((cos(part.a0), sin(part.a0)))
137 s = CurvedFence(c, part.radius, part.a0, part.da)
138 else:
139 if part.type == 'S_FENCE':
140 s = s.straight_fence(part.a0, part.length)
141 elif part.type == 'C_FENCE':
142 s = s.curved_fence(part.a0, part.da, part.radius)
144 # s.dist = self.length
145 # self.length += s.length
146 self.segs.append(s)
147 self.last_type = type
149 def set_offset(self, offset):
150 # @TODO:
151 # re-evaluate length of offset line here
152 last = None
153 for seg in self.segs:
154 seg.set_offset(offset, last)
155 last = seg.line
157 def param_t(self, angle_limit, post_spacing):
159 setup corners and fences dz
160 compute index of fences which belong to each group of fences between corners
161 compute t of each fence
163 # segments are group of parts separated by limit angle
164 self.segments = []
165 i_start = 0
166 t_start = 0
167 dist_0 = 0
168 z = 0
169 self.length = 0
170 n_parts = len(self.parts) - 1
171 for i, f in enumerate(self.segs):
172 f.dist = self.length
173 self.length += f.line.length
175 vz0 = Vector((1, 0))
176 angle_z = 0
177 for i, f in enumerate(self.segs):
178 dz = self.parts[i].dz
179 if f.dist > 0:
180 f.t_start = f.dist / self.length
181 else:
182 f.t_start = 0
184 f.t_end = (f.dist + f.line.length) / self.length
185 f.z0 = z
186 f.dz = dz
187 z += dz
189 if i < n_parts:
191 vz1 = Vector((self.segs[i + 1].length, self.parts[i + 1].dz))
192 angle_z = abs(vz0.angle_signed(vz1))
193 vz0 = vz1
195 if (abs(self.parts[i + 1].a0) >= angle_limit or angle_z >= angle_limit):
196 l_seg = f.dist + f.line.length - dist_0
197 t_seg = f.t_end - t_start
198 n_fences = max(1, int(l_seg / post_spacing))
199 t_fence = t_seg / n_fences
200 segment = FenceSegment(t_start, f.t_end, n_fences, t_fence, i_start, i)
201 dist_0 = f.dist + f.line.length
202 t_start = f.t_end
203 i_start = i
204 self.segments.append(segment)
206 manipulators = self.parts[i].manipulators
207 p0 = f.line.p0.to_3d()
208 p1 = f.line.p1.to_3d()
209 # angle from last to current segment
210 if i > 0:
211 v0 = self.segs[i - 1].line.straight(-1, 1).v.to_3d()
212 v1 = f.line.straight(1, 0).v.to_3d()
213 manipulators[0].set_pts([p0, v0, v1])
215 if type(f).__name__ == "StraightFence":
216 # segment length
217 manipulators[1].type_key = 'SIZE'
218 manipulators[1].prop1_name = "length"
219 manipulators[1].set_pts([p0, p1, (1, 0, 0)])
220 else:
221 # segment radius + angle
222 v0 = (f.line.p0 - f.c).to_3d()
223 v1 = (f.line.p1 - f.c).to_3d()
224 manipulators[1].type_key = 'ARC_ANGLE_RADIUS'
225 manipulators[1].prop1_name = "da"
226 manipulators[1].prop2_name = "radius"
227 manipulators[1].set_pts([f.c.to_3d(), v0, v1])
229 # snap manipulator, don't change index !
230 manipulators[2].set_pts([p0, p1, (1, 0, 0)])
232 f = self.segs[-1]
233 l_seg = f.dist + f.line.length - dist_0
234 t_seg = f.t_end - t_start
235 n_fences = max(1, int(l_seg / post_spacing))
236 t_fence = t_seg / n_fences
237 segment = FenceSegment(t_start, f.t_end, n_fences, t_fence, i_start, len(self.segs) - 1)
238 self.segments.append(segment)
240 def setup_user_defined_post(self, o, post_x, post_y, post_z):
241 self.user_defined_post = o
242 x = o.bound_box[6][0] - o.bound_box[0][0]
243 y = o.bound_box[6][1] - o.bound_box[0][1]
244 z = o.bound_box[6][2] - o.bound_box[0][2]
245 self.user_defined_post_scale = Vector((post_x / x, post_y / -y, post_z / z))
246 m = o.data
247 # create vertex group lookup dictionary for names
248 vgroup_names = {vgroup.index: vgroup.name for vgroup in o.vertex_groups}
249 # create dictionary of vertex group assignments per vertex
250 self.vertex_groups = [[vgroup_names[g.group] for g in v.groups] for v in m.vertices]
251 # uvs
252 uv_act = m.uv_layers.active
253 if uv_act is not None:
254 uv_layer = uv_act.data
255 self.user_defined_uvs = [[uv_layer[li].uv for li in p.loop_indices] for p in m.polygons]
256 else:
257 self.user_defined_uvs = [[(0, 0) for i in p.vertices] for p in m.polygons]
258 # material ids
259 self.user_defined_mat = [p.material_index for p in m.polygons]
261 def get_user_defined_post(self, tM, z0, z1, z2, slope, post_z, verts, faces, matids, uvs):
262 f = len(verts)
263 m = self.user_defined_post.data
264 for i, g in enumerate(self.vertex_groups):
265 co = m.vertices[i].co.copy()
266 co.x *= self.user_defined_post_scale.x
267 co.y *= self.user_defined_post_scale.y
268 co.z *= self.user_defined_post_scale.z
269 if 'Slope' in g:
270 co.z += co.y * slope
271 verts.append(tM @ co)
272 matids += self.user_defined_mat
273 faces += [tuple([i + f for i in p.vertices]) for p in m.polygons]
274 uvs += self.user_defined_uvs
276 def get_post(self, post, post_x, post_y, post_z, post_alt, sub_offset_x,
277 id_mat, verts, faces, matids, uvs):
279 n, dz, zl = post
280 slope = dz * post_y
282 if self.user_defined_post is not None:
283 x, y = -n.v.normalized()
284 p = n.p + sub_offset_x * n.v.normalized()
285 tM = Matrix([
286 [x, y, 0, p.x],
287 [y, -x, 0, p.y],
288 [0, 0, 1, zl + post_alt],
289 [0, 0, 0, 1]
291 self.get_user_defined_post(tM, zl, 0, 0, dz, post_z, verts, faces, matids, uvs)
292 return
294 z3 = zl + post_z + post_alt - slope
295 z4 = zl + post_z + post_alt + slope
296 z0 = zl + post_alt - slope
297 z1 = zl + post_alt + slope
298 vn = n.v.normalized()
299 dx = post_x * vn
300 dy = post_y * Vector((vn.y, -vn.x))
301 oy = sub_offset_x * vn
302 x0, y0 = n.p - dx + dy + oy
303 x1, y1 = n.p - dx - dy + oy
304 x2, y2 = n.p + dx - dy + oy
305 x3, y3 = n.p + dx + dy + oy
306 f = len(verts)
307 verts.extend([(x0, y0, z0), (x0, y0, z3),
308 (x1, y1, z1), (x1, y1, z4),
309 (x2, y2, z1), (x2, y2, z4),
310 (x3, y3, z0), (x3, y3, z3)])
311 faces.extend([(f, f + 1, f + 3, f + 2),
312 (f + 2, f + 3, f + 5, f + 4),
313 (f + 4, f + 5, f + 7, f + 6),
314 (f + 6, f + 7, f + 1, f),
315 (f, f + 2, f + 4, f + 6),
316 (f + 7, f + 5, f + 3, f + 1)])
317 matids.extend([id_mat, id_mat, id_mat, id_mat, id_mat, id_mat])
318 x = [(0, 0), (0, post_z), (post_x, post_z), (post_x, 0)]
319 y = [(0, 0), (0, post_z), (post_y, post_z), (post_y, 0)]
320 z = [(0, 0), (post_x, 0), (post_x, post_y), (0, post_y)]
321 uvs.extend([x, y, x, y, z, z])
323 def get_panel(self, subs, altitude, panel_x, panel_z, sub_offset_x, idmat, verts, faces, matids, uvs):
324 n_subs = len(subs)
325 if n_subs < 1:
326 return
327 f = len(verts)
328 x0 = sub_offset_x - 0.5 * panel_x
329 x1 = sub_offset_x + 0.5 * panel_x
330 z0 = 0
331 z1 = panel_z
332 profile = [Vector((x0, z0)), Vector((x1, z0)), Vector((x1, z1)), Vector((x0, z1))]
333 user_path_uv_v = []
334 n_sections = n_subs - 1
335 n, dz, zl = subs[0]
336 p0 = n.p
337 v0 = n.v.normalized()
338 for s, section in enumerate(subs):
339 n, dz, zl = section
340 p1 = n.p
341 if s < n_sections:
342 v1 = subs[s + 1][0].v.normalized()
343 dir = (v0 + v1).normalized()
344 scale = 1 / cos(0.5 * acos(min(1, max(-1, v0.dot(v1)))))
345 for p in profile:
346 x, y = n.p + scale * p.x * dir
347 z = zl + p.y + altitude
348 verts.append((x, y, z))
349 if s > 0:
350 user_path_uv_v.append((p1 - p0).length)
351 p0 = p1
352 v0 = v1
354 # build faces using Panel
355 lofter = Lofter(
356 # closed_shape, index, x, y, idmat
357 True,
358 [i for i in range(len(profile))],
359 [p.x for p in profile],
360 [p.y for p in profile],
361 [idmat for i in range(len(profile))],
362 closed_path=False,
363 user_path_uv_v=user_path_uv_v,
364 user_path_verts=n_subs
366 faces += lofter.faces(16, offset=f, path_type='USER_DEFINED')
367 matids += lofter.mat(16, idmat, idmat, path_type='USER_DEFINED')
368 v = Vector((0, 0))
369 uvs += lofter.uv(16, v, v, v, v, 0, v, 0, 0, path_type='USER_DEFINED')
371 def make_subs(self, x, y, z, post_y, altitude,
372 sub_spacing, offset_x, sub_offset_x, mat, verts, faces, matids, uvs):
374 t_post = (0.5 * post_y - y) / self.length
375 t_spacing = (sub_spacing + y) / self.length
377 for segment in self.segments:
378 t_step = segment.t_step
379 t_start = segment.t_start + t_post
380 s = 0
381 s_sub = t_step - 2 * t_post
382 n_sub = int(s_sub / t_spacing)
383 if n_sub > 0:
384 t_sub = s_sub / n_sub
385 else:
386 t_sub = 1
387 i = segment.i_start
388 while s < segment.n_step:
389 t_cur = t_start + s * t_step
390 for j in range(1, n_sub):
391 t_s = t_cur + t_sub * j
392 while self.segs[i].t_end < t_s:
393 i += 1
394 f = self.segs[i]
395 t = (t_s - f.t_start) / f.t_diff
396 n = f.line.normal(t)
397 post = (n, f.dz / f.length, f.z0 + f.dz * t)
398 self.get_post(post, x, y, z, altitude, sub_offset_x, mat, verts, faces, matids, uvs)
399 s += 1
401 def make_post(self, x, y, z, altitude, x_offset, mat, verts, faces, matids, uvs):
403 for segment in self.segments:
404 t_step = segment.t_step
405 t_start = segment.t_start
406 s = 0
407 i = segment.i_start
408 while s < segment.n_step:
409 t_cur = t_start + s * t_step
410 while self.segs[i].t_end < t_cur:
411 i += 1
412 f = self.segs[i]
413 t = (t_cur - f.t_start) / f.t_diff
414 n = f.line.normal(t)
415 post = (n, f.dz / f.line.length, f.z0 + f.dz * t)
416 # self.get_post(post, x, y, z, altitude, x_offset, mat, verts, faces, matids, uvs)
417 self.get_post(post, x, y, z, altitude, 0, mat, verts, faces, matids, uvs)
418 s += 1
420 if segment.i_end + 1 == len(self.segs):
421 f = self.segs[segment.i_end]
422 n = f.line.normal(1)
423 post = (n, f.dz / f.line.length, f.z0 + f.dz)
424 # self.get_post(post, x, y, z, altitude, x_offset, mat, verts, faces, matids, uvs)
425 self.get_post(post, x, y, z, altitude, 0, mat, verts, faces, matids, uvs)
427 def make_panels(self, x, z, post_y, altitude, panel_dist,
428 offset_x, sub_offset_x, idmat, verts, faces, matids, uvs):
430 t_post = (0.5 * post_y + panel_dist) / self.length
431 for segment in self.segments:
432 t_step = segment.t_step
433 t_start = segment.t_start
434 s = 0
435 i = segment.i_start
436 while s < segment.n_step:
437 subs = []
438 t_cur = t_start + s * t_step + t_post
439 t_end = t_start + (s + 1) * t_step - t_post
440 # find first section
441 while self.segs[i].t_end < t_cur and i < segment.i_end:
442 i += 1
443 f = self.segs[i]
444 # 1st section
445 t = (t_cur - f.t_start) / f.t_diff
446 n = f.line.normal(t)
447 subs.append((n, f.dz / f.line.length, f.z0 + f.dz * t))
448 # crossing sections -> new segment
449 while i < segment.i_end:
450 f = self.segs[i]
451 if f.t_end < t_end:
452 if type(f).__name__ == 'CurvedFence':
453 # can't end after segment
454 t0 = max(0, (t_cur - f.t_start) / f.t_diff)
455 t1 = min(1, (t_end - f.t_start) / f.t_diff)
456 n_s = int(max(1, abs(f.da) * (5) / pi - 1))
457 dt = (t1 - t0) / n_s
458 for j in range(1, n_s + 1):
459 t = t0 + dt * j
460 n = f.line.sized_normal(t, 1)
461 # n.p = f.lerp(x_offset)
462 subs.append((n, f.dz / f.line.length, f.z0 + f.dz * t))
463 else:
464 n = f.line.normal(1)
465 subs.append((n, f.dz / f.line.length, f.z0 + f.dz))
466 if f.t_end >= t_end:
467 break
468 elif f.t_start < t_end:
469 i += 1
471 f = self.segs[i]
472 # last section
473 if type(f).__name__ == 'CurvedFence':
474 # can't start before segment
475 t0 = max(0, (t_cur - f.t_start) / f.t_diff)
476 t1 = min(1, (t_end - f.t_start) / f.t_diff)
477 n_s = int(max(1, abs(f.da) * (5) / pi - 1))
478 dt = (t1 - t0) / n_s
479 for j in range(1, n_s + 1):
480 t = t0 + dt * j
481 n = f.line.sized_normal(t, 1)
482 # n.p = f.lerp(x_offset)
483 subs.append((n, f.dz / f.line.length, f.z0 + f.dz * t))
484 else:
485 t = (t_end - f.t_start) / f.t_diff
486 n = f.line.normal(t)
487 subs.append((n, f.dz / f.line.length, f.z0 + f.dz * t))
489 # self.get_panel(subs, altitude, x, z, 0, idmat, verts, faces, matids, uvs)
490 self.get_panel(subs, altitude, x, z, sub_offset_x, idmat, verts, faces, matids, uvs)
491 s += 1
493 def make_profile(self, profile, idmat,
494 x_offset, z_offset, extend, verts, faces, matids, uvs):
496 last = None
497 for seg in self.segs:
498 seg.p_line = seg.make_offset(x_offset, last)
499 last = seg.p_line
501 n_fences = len(self.segs) - 1
503 if n_fences < 0:
504 return
506 sections = []
508 f = self.segs[0]
510 # first step
511 if extend != 0 and f.p_line.length != 0:
512 t = -extend / self.segs[0].p_line.length
513 n = f.p_line.sized_normal(t, 1)
514 # n.p = f.lerp(x_offset)
515 sections.append((n, f.dz / f.p_line.length, f.z0 + f.dz * t))
517 # add first section
518 n = f.p_line.sized_normal(0, 1)
519 # n.p = f.lerp(x_offset)
520 sections.append((n, f.dz / f.p_line.length, f.z0))
522 for s, f in enumerate(self.segs):
523 if f.p_line.length == 0:
524 continue
525 if type(f).__name__ == 'CurvedFence':
526 n_s = int(max(1, abs(f.da) * 30 / pi - 1))
527 for i in range(1, n_s + 1):
528 t = i / n_s
529 n = f.p_line.sized_normal(t, 1)
530 # n.p = f.lerp(x_offset)
531 sections.append((n, f.dz / f.p_line.length, f.z0 + f.dz * t))
532 else:
533 n = f.p_line.sized_normal(1, 1)
534 # n.p = f.lerp(x_offset)
535 sections.append((n, f.dz / f.p_line.length, f.z0 + f.dz))
537 if extend != 0 and f.p_line.length != 0:
538 t = 1 + extend / self.segs[-1].p_line.length
539 n = f.p_line.sized_normal(t, 1)
540 # n.p = f.lerp(x_offset)
541 sections.append((n, f.dz / f.p_line.length, f.z0 + f.dz * t))
543 user_path_verts = len(sections)
544 offset = len(verts)
545 if user_path_verts > 0:
546 user_path_uv_v = []
547 n, dz, z0 = sections[-1]
548 sections[-1] = (n, dz, z0)
549 n_sections = user_path_verts - 1
550 n, dz, zl = sections[0]
551 p0 = n.p
552 v0 = n.v.normalized()
553 for s, section in enumerate(sections):
554 n, dz, zl = section
555 p1 = n.p
556 if s < n_sections:
557 v1 = sections[s + 1][0].v.normalized()
558 dir = (v0 + v1).normalized()
559 scale = min(10, 1 / cos(0.5 * acos(min(1, max(-1, v0.dot(v1) )))))
560 for p in profile:
561 # x, y = n.p + scale * (x_offset + p.x) * dir
562 x, y = n.p + scale * p.x * dir
563 z = zl + p.y + z_offset
564 verts.append((x, y, z))
565 if s > 0:
566 user_path_uv_v.append((p1 - p0).length)
567 p0 = p1
568 v0 = v1
570 # build faces using Panel
571 lofter = Lofter(
572 # closed_shape, index, x, y, idmat
573 True,
574 [i for i in range(len(profile))],
575 [p.x for p in profile],
576 [p.y for p in profile],
577 [idmat for i in range(len(profile))],
578 closed_path=False,
579 user_path_uv_v=user_path_uv_v,
580 user_path_verts=user_path_verts
582 faces += lofter.faces(16, offset=offset, path_type='USER_DEFINED')
583 matids += lofter.mat(16, idmat, idmat, path_type='USER_DEFINED')
584 v = Vector((0, 0))
585 uvs += lofter.uv(16, v, v, v, v, 0, v, 0, 0, path_type='USER_DEFINED')
588 def update(self, context):
589 self.update(context)
592 def update_manipulators(self, context):
593 self.update(context, manipulable_refresh=True)
596 def update_path(self, context):
597 self.update_path(context)
600 def update_type(self, context):
602 d = self.find_datablock_in_selection(context)
604 if d is not None and d.auto_update:
606 d.auto_update = False
607 # find part index
608 idx = 0
609 for i, part in enumerate(d.parts):
610 if part == self:
611 idx = i
612 break
613 part = d.parts[idx]
614 a0 = 0
615 if idx > 0:
616 g = d.get_generator()
617 w0 = g.segs[idx - 1]
618 a0 = w0.straight(1).angle
619 if "C_" in self.type:
620 w = w0.straight_fence(part.a0, part.length)
621 else:
622 w = w0.curved_fence(part.a0, part.da, part.radius)
623 else:
624 if "C_" in self.type:
625 p = Vector((0, 0))
626 v = self.length * Vector((cos(self.a0), sin(self.a0)))
627 w = StraightFence(p, v)
628 a0 = pi / 2
629 else:
630 c = -self.radius * Vector((cos(self.a0), sin(self.a0)))
631 w = CurvedFence(c, self.radius, self.a0, pi)
633 # not closed, see wall
634 # for closed ability
635 dp = w.p1 - w.p0
637 if "C_" in self.type:
638 part.radius = 0.5 * dp.length
639 part.da = pi
640 a0 = atan2(dp.y, dp.x) - pi / 2 - a0
641 else:
642 part.length = dp.length
643 a0 = atan2(dp.y, dp.x) - a0
645 if a0 > pi:
646 a0 -= 2 * pi
647 if a0 < -pi:
648 a0 += 2 * pi
649 part.a0 = a0
651 if idx + 1 < d.n_parts:
652 # adjust rotation of next part
653 part1 = d.parts[idx + 1]
654 if "C_" in part.type:
655 a0 = part1.a0 - pi / 2
656 else:
657 a0 = part1.a0 + w.straight(1).angle - atan2(dp.y, dp.x)
659 if a0 > pi:
660 a0 -= 2 * pi
661 if a0 < -pi:
662 a0 += 2 * pi
663 part1.a0 = a0
665 d.auto_update = True
668 materials_enum = (
669 ('0', 'Wood', '', 0),
670 ('1', 'Metal', '', 1),
671 ('2', 'Glass', '', 2)
675 class archipack_fence_material(PropertyGroup):
676 index : EnumProperty(
677 items=materials_enum,
678 default='0',
679 update=update
682 def find_datablock_in_selection(self, context):
684 find witch selected object this instance belongs to
685 provide support for "copy to selected"
687 selected = context.selected_objects[:]
688 for o in selected:
689 props = archipack_fence.datablock(o)
690 if props:
691 for part in props.rail_mat:
692 if part == self:
693 return props
694 return None
696 def update(self, context):
697 props = self.find_datablock_in_selection(context)
698 if props is not None:
699 props.update(context)
702 class archipack_fence_part(PropertyGroup):
703 type : EnumProperty(
704 items=(
705 ('S_FENCE', 'Straight fence', '', 0),
706 ('C_FENCE', 'Curved fence', '', 1),
708 default='S_FENCE',
709 update=update_type
711 length : FloatProperty(
712 name="Length",
713 min=0.01,
714 default=2.0,
715 unit='LENGTH', subtype='DISTANCE',
716 update=update
718 radius : FloatProperty(
719 name="Radius",
720 min=0.01,
721 default=0.7,
722 unit='LENGTH', subtype='DISTANCE',
723 update=update
725 da : FloatProperty(
726 name="Angle",
727 min=-pi,
728 max=pi,
729 default=pi / 2,
730 subtype='ANGLE', unit='ROTATION',
731 update=update
733 a0 : FloatProperty(
734 name="Start angle",
735 min=-2 * pi,
736 max=2 * pi,
737 default=0,
738 subtype='ANGLE', unit='ROTATION',
739 update=update
741 dz : FloatProperty(
742 name="delta z",
743 default=0,
744 unit='LENGTH', subtype='DISTANCE'
747 manipulators : CollectionProperty(type=archipack_manipulator)
749 def find_datablock_in_selection(self, context):
751 find witch selected object this instance belongs to
752 provide support for "copy to selected"
754 selected = context.selected_objects[:]
755 for o in selected:
756 props = archipack_fence.datablock(o)
757 if props is not None:
758 for part in props.parts:
759 if part == self:
760 return props
761 return None
763 def update(self, context, manipulable_refresh=False):
764 props = self.find_datablock_in_selection(context)
765 if props is not None:
766 props.update(context, manipulable_refresh)
768 def draw(self, layout, context, index):
769 box = layout.box()
770 row = box.row()
771 row.prop(self, "type", text=str(index + 1))
772 if self.type in ['C_FENCE']:
773 row = box.row()
774 row.prop(self, "radius")
775 row = box.row()
776 row.prop(self, "da")
777 else:
778 row = box.row()
779 row.prop(self, "length")
780 row = box.row()
781 row.prop(self, "a0")
784 class archipack_fence(ArchipackObject, Manipulable, PropertyGroup):
786 parts : CollectionProperty(type=archipack_fence_part)
787 user_defined_path : StringProperty(
788 name="User defined",
789 update=update_path
791 user_defined_spline : IntProperty(
792 name="Spline index",
793 min=0,
794 default=0,
795 update=update_path
797 user_defined_resolution : IntProperty(
798 name="Resolution",
799 min=1,
800 max=128,
801 default=12, update=update_path
803 n_parts : IntProperty(
804 name="Parts",
805 min=1,
806 default=1, update=update_manipulators
808 x_offset : FloatProperty(
809 name="Offset",
810 default=0.0, precision=2, step=1,
811 unit='LENGTH', subtype='DISTANCE',
812 update=update
815 radius : FloatProperty(
816 name="Radius",
817 min=0.01,
818 default=0.7,
819 unit='LENGTH', subtype='DISTANCE',
820 update=update
822 da : FloatProperty(
823 name="Angle",
824 min=-pi,
825 max=pi,
826 default=pi / 2,
827 subtype='ANGLE', unit='ROTATION',
828 update=update
830 angle_limit : FloatProperty(
831 name="Angle",
832 min=0,
833 max=2 * pi,
834 default=pi / 8,
835 subtype='ANGLE', unit='ROTATION',
836 update=update_manipulators
838 shape : EnumProperty(
839 items=(
840 ('RECTANGLE', 'Straight', '', 0),
841 ('CIRCLE', 'Curved ', '', 1)
843 default='RECTANGLE',
844 update=update
846 post : BoolProperty(
847 name='Enable',
848 default=True,
849 update=update
851 post_spacing : FloatProperty(
852 name="Spacing",
853 min=0.1,
854 default=1.0, precision=2, step=1,
855 unit='LENGTH', subtype='DISTANCE',
856 update=update
858 post_x : FloatProperty(
859 name="Width",
860 min=0.001,
861 default=0.04, precision=2, step=1,
862 unit='LENGTH', subtype='DISTANCE',
863 update=update
865 post_y : FloatProperty(
866 name="Length",
867 min=0.001, max=1000,
868 default=0.04, precision=2, step=1,
869 unit='LENGTH', subtype='DISTANCE',
870 update=update
872 post_z : FloatProperty(
873 name="Height",
874 min=0.001,
875 default=1, precision=2, step=1,
876 unit='LENGTH', subtype='DISTANCE',
877 update=update
879 post_alt : FloatProperty(
880 name="Altitude",
881 default=0, precision=2, step=1,
882 unit='LENGTH', subtype='DISTANCE',
883 update=update
885 user_defined_post_enable : BoolProperty(
886 name="User",
887 update=update,
888 default=True
890 user_defined_post : StringProperty(
891 name="User defined",
892 update=update
894 idmat_post : EnumProperty(
895 name="Post",
896 items=materials_enum,
897 default='1',
898 update=update
900 subs : BoolProperty(
901 name='Enable',
902 default=False,
903 update=update
905 subs_spacing : FloatProperty(
906 name="Spacing",
907 min=0.05,
908 default=0.10, precision=2, step=1,
909 unit='LENGTH', subtype='DISTANCE',
910 update=update
912 subs_x : FloatProperty(
913 name="Width",
914 min=0.001,
915 default=0.02, precision=2, step=1,
916 unit='LENGTH', subtype='DISTANCE',
917 update=update
919 subs_y : FloatProperty(
920 name="Length",
921 min=0.001,
922 default=0.02, precision=2, step=1,
923 unit='LENGTH', subtype='DISTANCE',
924 update=update
926 subs_z : FloatProperty(
927 name="Height",
928 min=0.001,
929 default=1, precision=2, step=1,
930 unit='LENGTH', subtype='DISTANCE',
931 update=update
933 subs_alt : FloatProperty(
934 name="Altitude",
935 default=0, precision=2, step=1,
936 unit='LENGTH', subtype='DISTANCE',
937 update=update
939 subs_offset_x : FloatProperty(
940 name="Offset",
941 default=0.0, precision=2, step=1,
942 unit='LENGTH', subtype='DISTANCE',
943 update=update
945 subs_bottom : EnumProperty(
946 name="Bottom",
947 items=(
948 ('STEP', 'Follow step', '', 0),
949 ('LINEAR', 'Linear', '', 1),
951 default='STEP',
952 update=update
954 user_defined_subs_enable : BoolProperty(
955 name="User",
956 update=update,
957 default=True
959 user_defined_subs : StringProperty(
960 name="User defined",
961 update=update
963 idmat_subs : EnumProperty(
964 name="Subs",
965 items=materials_enum,
966 default='1',
967 update=update
969 panel : BoolProperty(
970 name='Enable',
971 default=True,
972 update=update
974 panel_alt : FloatProperty(
975 name="Altitude",
976 default=0.25, precision=2, step=1,
977 unit='LENGTH', subtype='DISTANCE',
978 update=update
980 panel_x : FloatProperty(
981 name="Width",
982 min=0.001,
983 default=0.01, precision=2, step=1,
984 unit='LENGTH', subtype='DISTANCE',
985 update=update
987 panel_z : FloatProperty(
988 name="Height",
989 min=0.001,
990 default=0.6, precision=2, step=1,
991 unit='LENGTH', subtype='DISTANCE',
992 update=update
994 panel_dist : FloatProperty(
995 name="Spacing",
996 min=0.001,
997 default=0.05, precision=2, step=1,
998 unit='LENGTH', subtype='DISTANCE',
999 update=update
1001 panel_offset_x : FloatProperty(
1002 name="Offset",
1003 default=0.0, precision=2, step=1,
1004 unit='LENGTH', subtype='DISTANCE',
1005 update=update
1007 idmat_panel : EnumProperty(
1008 name="Panels",
1009 items=materials_enum,
1010 default='2',
1011 update=update
1013 rail : BoolProperty(
1014 name="Enable",
1015 update=update,
1016 default=False
1018 rail_n : IntProperty(
1019 name="#",
1020 default=1,
1021 min=0,
1022 max=31,
1023 update=update
1025 rail_x : FloatVectorProperty(
1026 name="Width",
1027 default=[
1028 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05,
1029 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05,
1030 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05,
1031 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05
1033 size=31,
1034 min=0.001,
1035 precision=2, step=1,
1036 unit='LENGTH',
1037 update=update
1039 rail_z : FloatVectorProperty(
1040 name="Height",
1041 default=[
1042 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05,
1043 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05,
1044 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05,
1045 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05
1047 size=31,
1048 min=0.001,
1049 precision=2, step=1,
1050 unit='LENGTH',
1051 update=update
1053 rail_offset : FloatVectorProperty(
1054 name="Offset",
1055 default=[
1056 0, 0, 0, 0, 0, 0, 0, 0,
1057 0, 0, 0, 0, 0, 0, 0, 0,
1058 0, 0, 0, 0, 0, 0, 0, 0,
1059 0, 0, 0, 0, 0, 0, 0
1061 size=31,
1062 precision=2, step=1,
1063 unit='LENGTH',
1064 update=update
1066 rail_alt : FloatVectorProperty(
1067 name="Altitude",
1068 default=[
1069 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0,
1070 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0,
1071 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0,
1072 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0
1074 size=31,
1075 precision=2, step=1,
1076 unit='LENGTH',
1077 update=update
1079 rail_mat : CollectionProperty(type=archipack_fence_material)
1081 handrail : BoolProperty(
1082 name="Enable",
1083 update=update,
1084 default=True
1086 handrail_offset : FloatProperty(
1087 name="Offset",
1088 default=0.0, precision=2, step=1,
1089 unit='LENGTH', subtype='DISTANCE',
1090 update=update
1092 handrail_alt : FloatProperty(
1093 name="Altitude",
1094 default=1.0, precision=2, step=1,
1095 unit='LENGTH', subtype='DISTANCE',
1096 update=update
1098 handrail_extend : FloatProperty(
1099 name="Extend",
1100 min=0,
1101 default=0.1, precision=2, step=1,
1102 unit='LENGTH', subtype='DISTANCE',
1103 update=update
1105 handrail_slice : BoolProperty(
1106 name='Slice',
1107 default=True,
1108 update=update
1110 handrail_slice_right : BoolProperty(
1111 name='Slice',
1112 default=True,
1113 update=update
1115 handrail_profil : EnumProperty(
1116 name="Profil",
1117 items=(
1118 ('SQUARE', 'Square', '', 0),
1119 ('CIRCLE', 'Circle', '', 1),
1120 ('COMPLEX', 'Circle over square', '', 2)
1122 default='SQUARE',
1123 update=update
1125 handrail_x : FloatProperty(
1126 name="Width",
1127 min=0.001,
1128 default=0.04, precision=2, step=1,
1129 unit='LENGTH', subtype='DISTANCE',
1130 update=update
1132 handrail_y : FloatProperty(
1133 name="Height",
1134 min=0.001,
1135 default=0.04, precision=2, step=1,
1136 unit='LENGTH', subtype='DISTANCE',
1137 update=update
1139 handrail_radius : FloatProperty(
1140 name="Radius",
1141 min=0.001,
1142 default=0.02, precision=2, step=1,
1143 unit='LENGTH', subtype='DISTANCE',
1144 update=update
1146 idmat_handrail : EnumProperty(
1147 name="Handrail",
1148 items=materials_enum,
1149 default='0',
1150 update=update
1153 # UI layout related
1154 parts_expand : BoolProperty(
1155 default=False
1157 rail_expand : BoolProperty(
1158 default=False
1160 idmats_expand : BoolProperty(
1161 default=False
1163 handrail_expand : BoolProperty(
1164 default=False
1166 post_expand : BoolProperty(
1167 default=False
1169 panel_expand : BoolProperty(
1170 default=False
1172 subs_expand : BoolProperty(
1173 default=False
1176 # Flag to prevent mesh update while making bulk changes over variables
1177 # use :
1178 # .auto_update = False
1179 # bulk changes
1180 # .auto_update = True
1181 auto_update : BoolProperty(
1182 options={'SKIP_SAVE'},
1183 default=True,
1184 update=update_manipulators
1187 def setup_manipulators(self):
1189 if len(self.manipulators) == 0:
1190 s = self.manipulators.add()
1191 s.prop1_name = "width"
1192 s = self.manipulators.add()
1193 s.prop1_name = "height"
1194 s.normal = Vector((0, 1, 0))
1196 for i in range(self.n_parts):
1197 p = self.parts[i]
1198 n_manips = len(p.manipulators)
1199 if n_manips == 0:
1200 s = p.manipulators.add()
1201 s.type_key = "ANGLE"
1202 s.prop1_name = "a0"
1203 s = p.manipulators.add()
1204 s.type_key = "SIZE"
1205 s.prop1_name = "length"
1206 s = p.manipulators.add()
1207 # s.type_key = 'SNAP_POINT'
1208 s.type_key = 'WALL_SNAP'
1209 s.prop1_name = str(i)
1210 s.prop2_name = 'post_z'
1212 def update_parts(self):
1214 # remove rails materials
1215 for i in range(len(self.rail_mat), self.rail_n, -1):
1216 self.rail_mat.remove(i - 1)
1218 # add rails
1219 for i in range(len(self.rail_mat), self.rail_n):
1220 self.rail_mat.add()
1222 # remove parts
1223 for i in range(len(self.parts), self.n_parts, -1):
1224 self.parts.remove(i - 1)
1226 # add parts
1227 for i in range(len(self.parts), self.n_parts):
1228 self.parts.add()
1229 self.setup_manipulators()
1231 def interpolate_bezier(self, pts, wM, p0, p1, resolution):
1232 # straight segment, worth testing here
1233 # since this can lower points count by a resolution factor
1234 # use normalized to handle non linear t
1235 if resolution == 0:
1236 pts.append(wM @ p0.co.to_3d())
1237 else:
1238 v = (p1.co - p0.co).normalized()
1239 d1 = (p0.handle_right - p0.co).normalized()
1240 d2 = (p1.co - p1.handle_left).normalized()
1241 if d1 == v and d2 == v:
1242 pts.append(wM @ p0.co.to_3d())
1243 else:
1244 seg = interpolate_bezier(wM @ p0.co,
1245 wM @ p0.handle_right,
1246 wM @ p1.handle_left,
1247 wM @ p1.co,
1248 resolution + 1)
1249 for i in range(resolution):
1250 pts.append(seg[i].to_3d())
1252 def from_spline(self, context, wM, resolution, spline):
1254 o = self.find_in_selection(context)
1256 if o is None:
1257 return
1259 tM = wM.copy()
1260 tM.row[0].normalize()
1261 tM.row[1].normalize()
1262 tM.row[2].normalize()
1263 pts = []
1264 if spline.type == 'POLY':
1265 pt = spline.points[0].co
1266 pts = [wM @ p.co.to_3d() for p in spline.points]
1267 if spline.use_cyclic_u:
1268 pts.append(pts[0])
1269 elif spline.type == 'BEZIER':
1270 pt = spline.bezier_points[0].co
1271 points = spline.bezier_points
1272 for i in range(1, len(points)):
1273 p0 = points[i - 1]
1274 p1 = points[i]
1275 self.interpolate_bezier(pts, wM, p0, p1, resolution)
1276 if spline.use_cyclic_u:
1277 p0 = points[-1]
1278 p1 = points[0]
1279 self.interpolate_bezier(pts, wM, p0, p1, resolution)
1280 pts.append(pts[0])
1281 else:
1282 pts.append(wM @ points[-1].co)
1283 auto_update = self.auto_update
1284 self.auto_update = False
1286 self.n_parts = len(pts) - 1
1287 self.update_parts()
1289 p0 = pts.pop(0)
1290 a0 = 0
1291 for i, p1 in enumerate(pts):
1292 dp = p1 - p0
1293 da = atan2(dp.y, dp.x) - a0
1294 if da > pi:
1295 da -= 2 * pi
1296 if da < -pi:
1297 da += 2 * pi
1298 p = self.parts[i]
1299 p.length = dp.to_2d().length
1300 p.dz = dp.z
1301 p.a0 = da
1302 a0 += da
1303 p0 = p1
1305 self.auto_update = auto_update
1307 o.matrix_world = tM @ Matrix.Translation(pt)
1309 def update_path(self, context):
1310 path = context.scene.objects.get(self.user_defined_path.strip())
1311 if path is not None and path.type == 'CURVE':
1312 splines = path.data.splines
1313 if len(splines) > self.user_defined_spline:
1314 self.from_spline(
1315 context,
1316 path.matrix_world,
1317 self.user_defined_resolution,
1318 splines[self.user_defined_spline])
1320 def get_generator(self):
1321 g = FenceGenerator(self.parts)
1322 for part in self.parts:
1323 # type, radius, da, length
1324 g.add_part(part)
1326 g.set_offset(self.x_offset)
1327 # param_t(da, part_length)
1328 g.param_t(self.angle_limit, self.post_spacing)
1329 return g
1331 def update(self, context, manipulable_refresh=False):
1332 o = self.find_in_selection(context, self.auto_update)
1334 if o is None:
1335 return
1337 # clean up manipulators before any data model change
1338 if manipulable_refresh:
1339 self.manipulable_disable(context)
1341 self.update_parts()
1343 verts = []
1344 faces = []
1345 matids = []
1346 uvs = []
1348 g = self.get_generator()
1350 # depth at bottom
1351 # self.manipulators[1].set_pts([(0, 0, 0), (0, 0, self.height), (1, 0, 0)])
1353 if self.user_defined_post_enable:
1354 # user defined posts
1355 user_def_post = context.scene.objects.get(self.user_defined_post.strip())
1356 if user_def_post is not None and user_def_post.type == 'MESH':
1357 g.setup_user_defined_post(user_def_post, self.post_x, self.post_y, self.post_z)
1359 if self.post:
1360 g.make_post(0.5 * self.post_x, 0.5 * self.post_y, self.post_z,
1361 self.post_alt, self.x_offset,
1362 int(self.idmat_post), verts, faces, matids, uvs)
1364 # reset user def posts
1365 g.user_defined_post = None
1367 # user defined subs
1368 if self.user_defined_subs_enable:
1369 user_def_subs = context.scene.objects.get(self.user_defined_subs.strip())
1370 if user_def_subs is not None and user_def_subs.type == 'MESH':
1371 g.setup_user_defined_post(user_def_subs, self.subs_x, self.subs_y, self.subs_z)
1373 if self.subs:
1374 g.make_subs(0.5 * self.subs_x, 0.5 * self.subs_y, self.subs_z,
1375 self.post_y, self.subs_alt, self.subs_spacing,
1376 self.x_offset, self.subs_offset_x, int(self.idmat_subs), verts, faces, matids, uvs)
1378 g.user_defined_post = None
1380 if self.panel:
1381 g.make_panels(0.5 * self.panel_x, self.panel_z, self.post_y,
1382 self.panel_alt, self.panel_dist, self.x_offset, self.panel_offset_x,
1383 int(self.idmat_panel), verts, faces, matids, uvs)
1385 if self.rail:
1386 for i in range(self.rail_n):
1387 x = 0.5 * self.rail_x[i]
1388 y = self.rail_z[i]
1389 rail = [Vector((-x, y)), Vector((-x, 0)), Vector((x, 0)), Vector((x, y))]
1390 g.make_profile(rail, int(self.rail_mat[i].index), self.x_offset - self.rail_offset[i],
1391 self.rail_alt[i], 0, verts, faces, matids, uvs)
1393 if self.handrail_profil == 'COMPLEX':
1394 sx = self.handrail_x
1395 sy = self.handrail_y
1396 handrail = [Vector((sx * x, sy * y)) for x, y in [
1397 (-0.28, 1.83), (-0.355, 1.77), (-0.415, 1.695), (-0.46, 1.605), (-0.49, 1.51), (-0.5, 1.415),
1398 (-0.49, 1.315), (-0.46, 1.225), (-0.415, 1.135), (-0.355, 1.06), (-0.28, 1.0), (-0.255, 0.925),
1399 (-0.33, 0.855), (-0.5, 0.855), (-0.5, 0.0), (0.5, 0.0), (0.5, 0.855), (0.33, 0.855), (0.255, 0.925),
1400 (0.28, 1.0), (0.355, 1.06), (0.415, 1.135), (0.46, 1.225), (0.49, 1.315), (0.5, 1.415),
1401 (0.49, 1.51), (0.46, 1.605), (0.415, 1.695), (0.355, 1.77), (0.28, 1.83), (0.19, 1.875),
1402 (0.1, 1.905), (0.0, 1.915), (-0.095, 1.905), (-0.19, 1.875)]]
1404 elif self.handrail_profil == 'SQUARE':
1405 x = 0.5 * self.handrail_x
1406 y = self.handrail_y
1407 handrail = [Vector((-x, y)), Vector((-x, 0)), Vector((x, 0)), Vector((x, y))]
1408 elif self.handrail_profil == 'CIRCLE':
1409 r = self.handrail_radius
1410 handrail = [Vector((r * sin(0.1 * -a * pi), r * (0.5 + cos(0.1 * -a * pi)))) for a in range(0, 20)]
1412 if self.handrail:
1413 g.make_profile(handrail, int(self.idmat_handrail), self.x_offset - self.handrail_offset,
1414 self.handrail_alt, self.handrail_extend, verts, faces, matids, uvs)
1416 bmed.buildmesh(context, o, verts, faces, matids=matids, uvs=uvs, weld=True, clean=True)
1418 # enable manipulators rebuild
1419 if manipulable_refresh:
1420 self.manipulable_refresh = True
1422 # restore context
1423 self.restore_context(context)
1425 def manipulable_setup(self, context):
1427 NOTE:
1428 this one assume context.active_object is the instance this
1429 data belongs to, failing to do so will result in wrong
1430 manipulators set on active object
1432 self.manipulable_disable(context)
1434 o = context.active_object
1436 self.setup_manipulators()
1438 for i, part in enumerate(self.parts):
1439 if i >= self.n_parts:
1440 break
1442 if i > 0:
1443 # start angle
1444 self.manip_stack.append(part.manipulators[0].setup(context, o, part))
1446 # length / radius + angle
1447 self.manip_stack.append(part.manipulators[1].setup(context, o, part))
1449 # snap point
1450 self.manip_stack.append(part.manipulators[2].setup(context, o, self))
1452 for m in self.manipulators:
1453 self.manip_stack.append(m.setup(context, o, self))
1456 class ARCHIPACK_PT_fence(Panel):
1457 bl_idname = "ARCHIPACK_PT_fence"
1458 bl_label = "Fence"
1459 bl_space_type = 'VIEW_3D'
1460 bl_region_type = 'UI'
1461 bl_category = 'Archipack'
1463 @classmethod
1464 def poll(cls, context):
1465 return archipack_fence.filter(context.active_object)
1467 def draw(self, context):
1468 prop = archipack_fence.datablock(context.active_object)
1469 if prop is None:
1470 return
1471 scene = context.scene
1472 layout = self.layout
1473 row = layout.row(align=True)
1474 row.operator('archipack.fence_manipulate', icon='VIEW_PAN')
1475 box = layout.box()
1476 # box.label(text="Styles")
1477 row = box.row(align=True)
1478 row.operator("archipack.fence_preset_menu", text=bpy.types.ARCHIPACK_OT_fence_preset_menu.bl_label)
1479 row.operator("archipack.fence_preset", text="", icon='ADD')
1480 row.operator("archipack.fence_preset", text="", icon='REMOVE').remove_active = True
1481 box = layout.box()
1482 row = box.row(align=True)
1483 row.operator("archipack.fence_curve_update", text="", icon='FILE_REFRESH')
1484 row.prop_search(prop, "user_defined_path", scene, "objects", text="", icon='OUTLINER_OB_CURVE')
1485 if prop.user_defined_path != "":
1486 box.prop(prop, 'user_defined_spline')
1487 box.prop(prop, 'user_defined_resolution')
1488 box.prop(prop, 'angle_limit')
1489 box.prop(prop, 'x_offset')
1490 box = layout.box()
1491 row = box.row()
1492 if prop.parts_expand:
1493 row.prop(prop, 'parts_expand', icon="TRIA_DOWN", text="Parts", emboss=False)
1494 box.prop(prop, 'n_parts')
1495 for i, part in enumerate(prop.parts):
1496 part.draw(layout, context, i)
1497 else:
1498 row.prop(prop, 'parts_expand', icon="TRIA_RIGHT", text="Parts", emboss=False)
1500 box = layout.box()
1501 row = box.row(align=True)
1502 if prop.handrail_expand:
1503 row.prop(prop, 'handrail_expand', icon="TRIA_DOWN", text="Handrail", emboss=False)
1504 else:
1505 row.prop(prop, 'handrail_expand', icon="TRIA_RIGHT", text="Handrail", emboss=False)
1507 row.prop(prop, 'handrail')
1509 if prop.handrail_expand:
1510 box.prop(prop, 'handrail_alt')
1511 box.prop(prop, 'handrail_offset')
1512 box.prop(prop, 'handrail_extend')
1513 box.prop(prop, 'handrail_profil')
1514 if prop.handrail_profil != 'CIRCLE':
1515 box.prop(prop, 'handrail_x')
1516 box.prop(prop, 'handrail_y')
1517 else:
1518 box.prop(prop, 'handrail_radius')
1519 row = box.row(align=True)
1520 row.prop(prop, 'handrail_slice')
1522 box = layout.box()
1523 row = box.row(align=True)
1524 if prop.post_expand:
1525 row.prop(prop, 'post_expand', icon="TRIA_DOWN", text="Post", emboss=False)
1526 else:
1527 row.prop(prop, 'post_expand', icon="TRIA_RIGHT", text="Post", emboss=False)
1528 row.prop(prop, 'post')
1529 if prop.post_expand:
1530 box.prop(prop, 'post_spacing')
1531 box.prop(prop, 'post_x')
1532 box.prop(prop, 'post_y')
1533 box.prop(prop, 'post_z')
1534 box.prop(prop, 'post_alt')
1535 row = box.row(align=True)
1536 row.prop(prop, 'user_defined_post_enable', text="")
1537 row.prop_search(prop, "user_defined_post", scene, "objects", text="")
1539 box = layout.box()
1540 row = box.row(align=True)
1541 if prop.subs_expand:
1542 row.prop(prop, 'subs_expand', icon="TRIA_DOWN", text="Subs", emboss=False)
1543 else:
1544 row.prop(prop, 'subs_expand', icon="TRIA_RIGHT", text="Subs", emboss=False)
1546 row.prop(prop, 'subs')
1547 if prop.subs_expand:
1548 box.prop(prop, 'subs_spacing')
1549 box.prop(prop, 'subs_x')
1550 box.prop(prop, 'subs_y')
1551 box.prop(prop, 'subs_z')
1552 box.prop(prop, 'subs_alt')
1553 box.prop(prop, 'subs_offset_x')
1554 row = box.row(align=True)
1555 row.prop(prop, 'user_defined_subs_enable', text="")
1556 row.prop_search(prop, "user_defined_subs", scene, "objects", text="")
1558 box = layout.box()
1559 row = box.row(align=True)
1560 if prop.panel_expand:
1561 row.prop(prop, 'panel_expand', icon="TRIA_DOWN", text="Panels", emboss=False)
1562 else:
1563 row.prop(prop, 'panel_expand', icon="TRIA_RIGHT", text="Panels", emboss=False)
1564 row.prop(prop, 'panel')
1565 if prop.panel_expand:
1566 box.prop(prop, 'panel_dist')
1567 box.prop(prop, 'panel_x')
1568 box.prop(prop, 'panel_z')
1569 box.prop(prop, 'panel_alt')
1570 box.prop(prop, 'panel_offset_x')
1572 box = layout.box()
1573 row = box.row(align=True)
1574 if prop.rail_expand:
1575 row.prop(prop, 'rail_expand', icon="TRIA_DOWN", text="Rails", emboss=False)
1576 else:
1577 row.prop(prop, 'rail_expand', icon="TRIA_RIGHT", text="Rails", emboss=False)
1578 row.prop(prop, 'rail')
1579 if prop.rail_expand:
1580 box.prop(prop, 'rail_n')
1581 for i in range(prop.rail_n):
1582 box = layout.box()
1583 box.label(text="Rail " + str(i + 1))
1584 box.prop(prop, 'rail_x', index=i)
1585 box.prop(prop, 'rail_z', index=i)
1586 box.prop(prop, 'rail_alt', index=i)
1587 box.prop(prop, 'rail_offset', index=i)
1588 box.prop(prop.rail_mat[i], 'index', text="")
1590 box = layout.box()
1591 row = box.row()
1593 if prop.idmats_expand:
1594 row.prop(prop, 'idmats_expand', icon="TRIA_DOWN", text="Materials", emboss=False)
1595 box.prop(prop, 'idmat_handrail')
1596 box.prop(prop, 'idmat_panel')
1597 box.prop(prop, 'idmat_post')
1598 box.prop(prop, 'idmat_subs')
1599 else:
1600 row.prop(prop, 'idmats_expand', icon="TRIA_RIGHT", text="Materials", emboss=False)
1602 # ------------------------------------------------------------------
1603 # Define operator class to create object
1604 # ------------------------------------------------------------------
1607 class ARCHIPACK_OT_fence(ArchipackCreateTool, Operator):
1608 bl_idname = "archipack.fence"
1609 bl_label = "Fence"
1610 bl_description = "Fence"
1611 bl_category = 'Archipack'
1612 bl_options = {'REGISTER', 'UNDO'}
1614 def create(self, context):
1616 m = bpy.data.meshes.new("Fence")
1617 o = bpy.data.objects.new("Fence", m)
1618 d = m.archipack_fence.add()
1619 # make manipulators selectable
1621 d.manipulable_selectable = True
1622 self.link_object_to_scene(context, o)
1623 o.select_set(state=True)
1624 context.view_layer.objects.active = o
1625 self.load_preset(d)
1627 self.add_material(o)
1628 return o
1630 def execute(self, context):
1631 if context.mode == "OBJECT":
1632 bpy.ops.object.select_all(action="DESELECT")
1633 o = self.create(context)
1634 o.location = context.scene.cursor.location
1635 o.select_set(state=True)
1636 context.view_layer.objects.active = o
1637 self.manipulate()
1638 return {'FINISHED'}
1639 else:
1640 self.report({'WARNING'}, "Archipack: Option only valid in Object mode")
1641 return {'CANCELLED'}
1644 # ------------------------------------------------------------------
1645 # Define operator class to create object
1646 # ------------------------------------------------------------------
1648 class ARCHIPACK_OT_fence_curve_update(Operator):
1649 bl_idname = "archipack.fence_curve_update"
1650 bl_label = "Fence curve update"
1651 bl_description = "Update fence data from curve"
1652 bl_category = 'Archipack'
1653 bl_options = {'REGISTER', 'UNDO'}
1655 @classmethod
1656 def poll(self, context):
1657 return archipack_fence.filter(context.active_object)
1659 def draw(self, context):
1660 layout = self.layout
1661 row = layout.row()
1662 row.label(text="Use Properties panel (N) to define parms", icon='INFO')
1664 def execute(self, context):
1665 if context.mode == "OBJECT":
1666 d = archipack_fence.datablock(context.active_object)
1667 d.update_path(context)
1668 return {'FINISHED'}
1669 else:
1670 self.report({'WARNING'}, "Archipack: Option only valid in Object mode")
1671 return {'CANCELLED'}
1674 class ARCHIPACK_OT_fence_from_curve(ArchipackCreateTool, Operator):
1675 bl_idname = "archipack.fence_from_curve"
1676 bl_label = "Fence curve"
1677 bl_description = "Create a fence from a curve"
1678 bl_category = 'Archipack'
1679 bl_options = {'REGISTER', 'UNDO'}
1681 @classmethod
1682 def poll(self, context):
1683 return context.active_object is not None and context.active_object.type == 'CURVE'
1685 def draw(self, context):
1686 layout = self.layout
1687 row = layout.row()
1688 row.label(text="Use Properties panel (N) to define parms", icon='INFO')
1690 def create(self, context):
1691 o = None
1692 curve = context.active_object
1693 for i, spline in enumerate(curve.data.splines):
1694 bpy.ops.archipack.fence('INVOKE_DEFAULT', auto_manipulate=False)
1695 o = context.active_object
1696 d = archipack_fence.datablock(o)
1697 d.auto_update = False
1698 d.user_defined_spline = i
1699 d.user_defined_path = curve.name
1700 d.auto_update = True
1701 return o
1703 def execute(self, context):
1704 if context.mode == "OBJECT":
1705 bpy.ops.object.select_all(action="DESELECT")
1706 o = self.create(context)
1707 if o is not None:
1708 o.select_set(state=True)
1709 context.view_layer.objects.active = o
1710 # self.manipulate()
1711 return {'FINISHED'}
1712 else:
1713 self.report({'WARNING'}, "Archipack: Option only valid in Object mode")
1714 return {'CANCELLED'}
1716 # ------------------------------------------------------------------
1717 # Define operator class to manipulate object
1718 # ------------------------------------------------------------------
1721 class ARCHIPACK_OT_fence_manipulate(Operator):
1722 bl_idname = "archipack.fence_manipulate"
1723 bl_label = "Manipulate"
1724 bl_description = "Manipulate"
1725 bl_options = {'REGISTER', 'UNDO'}
1727 @classmethod
1728 def poll(self, context):
1729 return archipack_fence.filter(context.active_object)
1731 def invoke(self, context, event):
1732 d = archipack_fence.datablock(context.active_object)
1733 d.manipulable_invoke(context)
1734 return {'FINISHED'}
1737 # ------------------------------------------------------------------
1738 # Define operator class to load / save presets
1739 # ------------------------------------------------------------------
1742 class ARCHIPACK_OT_fence_preset_menu(PresetMenuOperator, Operator):
1743 bl_description = "Show Fence Presets"
1744 bl_idname = "archipack.fence_preset_menu"
1745 bl_label = "Fence Styles"
1746 preset_subdir = "archipack_fence"
1749 class ARCHIPACK_OT_fence_preset(ArchipackPreset, Operator):
1750 """Add a Fence Preset"""
1751 bl_idname = "archipack.fence_preset"
1752 bl_label = "Add Fence Style"
1753 preset_menu = "ARCHIPACK_OT_fence_preset_menu"
1755 @property
1756 def blacklist(self):
1757 return ['manipulators', 'n_parts', 'parts', 'user_defined_path', 'user_defined_spline']
1760 def register():
1761 bpy.utils.register_class(archipack_fence_material)
1762 bpy.utils.register_class(archipack_fence_part)
1763 bpy.utils.register_class(archipack_fence)
1764 Mesh.archipack_fence = CollectionProperty(type=archipack_fence)
1765 bpy.utils.register_class(ARCHIPACK_OT_fence_preset_menu)
1766 bpy.utils.register_class(ARCHIPACK_PT_fence)
1767 bpy.utils.register_class(ARCHIPACK_OT_fence)
1768 bpy.utils.register_class(ARCHIPACK_OT_fence_preset)
1769 bpy.utils.register_class(ARCHIPACK_OT_fence_manipulate)
1770 bpy.utils.register_class(ARCHIPACK_OT_fence_from_curve)
1771 bpy.utils.register_class(ARCHIPACK_OT_fence_curve_update)
1774 def unregister():
1775 bpy.utils.unregister_class(archipack_fence_material)
1776 bpy.utils.unregister_class(archipack_fence_part)
1777 bpy.utils.unregister_class(archipack_fence)
1778 del Mesh.archipack_fence
1779 bpy.utils.unregister_class(ARCHIPACK_OT_fence_preset_menu)
1780 bpy.utils.unregister_class(ARCHIPACK_PT_fence)
1781 bpy.utils.unregister_class(ARCHIPACK_OT_fence)
1782 bpy.utils.unregister_class(ARCHIPACK_OT_fence_preset)
1783 bpy.utils.unregister_class(ARCHIPACK_OT_fence_manipulate)
1784 bpy.utils.unregister_class(ARCHIPACK_OT_fence_from_curve)
1785 bpy.utils.unregister_class(ARCHIPACK_OT_fence_curve_update)