1# ##### BEGIN GPL LICENSE BLOCK #####
2#
3#  This program is free software; you can redistribute it and/or
4#  modify it under the terms of the GNU General Public License
5#  as published by the Free Software Foundation; either version 2
6#  of the License, or (at your option) any later version.
7#
8#  This program is distributed in the hope that it will be useful,
9#  but WITHOUT ANY WARRANTY; without even the implied warranty of
10#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11#  GNU General Public License for more details.
12#
13#  You should have received a copy of the GNU General Public License
14#  along with this program; if not, write to the Free Software Foundation,
15#  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16#
17# ##### END GPL LICENSE BLOCK #####
18
19# <pep8 compliant>
20
21# ----------------------------------------------------------
22# Automatic generation of books
23# Author: Antonio Vazquez (antonioya)
24#
25# ----------------------------------------------------------
26# noinspection PyUnresolvedReferences
27import bpy
28from math import cos, sin, radians
29from random import randint
30from copy import copy
31from colorsys import rgb_to_hsv, hsv_to_rgb
32from bpy.types import Operator
33from bpy.props import BoolProperty, IntProperty, FloatProperty, FloatVectorProperty
34from .achm_tools import *
35
36
37# ------------------------------------------------------------------
38# Define UI class
39# Books
40# ------------------------------------------------------------------
41class ARCHIMESH_OT_Books(Operator):
42    bl_idname = "mesh.archimesh_books"
43    bl_label = "Books"
44    bl_description = "Books Generator"
45    bl_category = 'View'
46    bl_options = {'REGISTER', 'UNDO'}
47
48    width: FloatProperty(
49            name='Width', min=0.001, max=1, default=0.045, precision=3,
50            description='Bounding book width',
51            )
52    depth: FloatProperty(
53            name='Depth', min=0.001, max=1, default=0.22, precision=3,
54            description='Bounding book depth',
55            )
56    height: FloatProperty(
57            name='Height', min=0.001, max=1, default=0.30, precision=3,
58            description='Bounding book height',
59            )
60    num: IntProperty(
61            name='Number of books', min=1, max=100, default=5,
62            description='Number total of books',
63            )
64
65    rX: FloatProperty(
66            name='X', min=0.000, max=0.999, default=0, precision=3,
67            description='Randomness for X axis',
68            )
69    rY: FloatProperty(
70            name='Y', min=0.000, max=0.999, default=0, precision=3,
71            description='Randomness for Y axis',
72            )
73    rZ: FloatProperty(
74            name='Z', min=0.000, max=0.999, default=0, precision=3,
75            description='Randomness for Z axis',
76            )
77
78    rot: FloatProperty(
79            name='Rotation', min=0.000, max=1, default=0, precision=3,
80            description='Randomness for vertical position (0-> All straight)',
81            )
82    afn: IntProperty(
83            name='Affinity', min=0, max=10, default=5,
84            description='Number of books with same rotation angle',
85            )
86
87    # Materials
88    crt_mat: BoolProperty(
89            name="Create default Cycles materials",
90            description="Create default materials for Cycles render",
91            default=True,
92            )
93    objcol: FloatVectorProperty(
94            name="Color",
95            description="Color for material",
96            default=(1.0, 1.0, 1.0, 1.0),
97            min=0.1, max=1,
98            subtype='COLOR',
99            size=4,
100            )
101    rC: FloatProperty(
102            name='Randomness',
103            min=0.000, max=1, default=0, precision=3,
104            description='Randomness for color ',
105            )
106
107    # -----------------------------------------------------
108    # Draw (create UI interface)
109    # -----------------------------------------------------
110    # noinspection PyUnusedLocal
111    def draw(self, context):
112        layout = self.layout
113        space = bpy.context.space_data
114        if not space.local_view:
115            # Imperial units warning
116            if bpy.context.scene.unit_settings.system == "IMPERIAL":
117                row = layout.row()
118                row.label(text="Warning: Imperial units not supported", icon='COLOR_RED')
119
120            box = layout.box()
121            box.label(text="Book size")
122            row = box.row()
123            row.prop(self, 'width')
124            row.prop(self, 'depth')
125            row.prop(self, 'height')
126            row = box.row()
127            row.prop(self, 'num', slider=True)
128
129            box = layout.box()
130            box.label(text="Randomness")
131            row = box.row()
132            row.prop(self, 'rX', slider=True)
133            row.prop(self, 'rY', slider=True)
134            row.prop(self, 'rZ', slider=True)
135            row = box.row()
136            row.prop(self, 'rot', slider=True)
137            row.prop(self, 'afn', slider=True)
138
139            box = layout.box()
140            if not context.scene.render.engine in {'CYCLES', 'BLENDER_EEVEE'}:
141                box.enabled = False
142            box.prop(self, 'crt_mat')
143            if self.crt_mat:
144                row = box.row()
145                row.prop(self, 'objcol')
146                row = box.row()
147                row.prop(self, 'rC', slider=True)
148        else:
149            row = layout.row()
150            row.label(text="Warning: Operator does not work in local view mode", icon='ERROR')
151
152    # -----------------------------------------------------
153    # Execute
154    # -----------------------------------------------------
155    # noinspection PyUnusedLocal
156    def execute(self, context):
157        if bpy.context.mode == "OBJECT":
158            # Create shelves
159            create_book_mesh(self)
160            return {'FINISHED'}
161        else:
162            self.report({'WARNING'}, "Archimesh: Option only valid in Object mode")
163            return {'CANCELLED'}
164
165
166# ------------------------------------------------------------------------------
167# Generate mesh data
168# All custom values are passed using self container (self.myvariable)
169# ------------------------------------------------------------------------------
170def create_book_mesh(self):
171    # deactivate others
172    for o in bpy.data.objects:
173        if o.select_get() is True:
174            o.select_set(False)
175    bpy.ops.object.select_all(action='DESELECT')
176    generate_books(self)
177
178    return
179
180
181# ------------------------------------------------------------------------------
182# Generate books
183# All custom values are passed using self container (self.myvariable)
184# ------------------------------------------------------------------------------
185def generate_books(self):
186    boxes = []
187    location = bpy.context.scene.cursor.location
188    myloc = copy(location)  # copy location to keep 3D cursor position
189
190    # Create
191    lastx = myloc.x
192    ox = 0
193    oy = 0
194    oz = 0
195    ot = 0
196    i = 0
197    for x in range(self.num):
198        # reset rotation
199        if i >= self.afn:
200            i = 0
201            ot = -1
202
203        mydata = create_book("Book" + str(x),
204                             self.width, self.depth, self.height,
205                             lastx, myloc.y, myloc.z,
206                             self.crt_mat if bpy.context.scene.render.engine in {'CYCLES', 'BLENDER_EEVEE'} else False,
207                             self.rX, self.rY, self.rZ, self.rot, ox, oy, oz, ot,
208                             self.objcol, self.rC)
209        boxes.extend([mydata[0]])
210        bookdata = mydata[1]
211
212        # calculate rotation using previous book
213        ot = bookdata[3]
214        i += 1
215        oz = 0
216
217        # calculate x size after rotation
218        if i < self.afn:
219            size = 0.0002
220        else:
221            size = 0.0003 + cos(radians(90 - bookdata[3])) * bookdata[2]  # the height is the radius
222            oz = bookdata[2]
223
224        lastx = lastx + bookdata[0] + size
225
226    # refine units
227    for box in boxes:
228        remove_doubles(box)
229        set_normals(box)
230
231    # deactivate others
232    for o in bpy.data.objects:
233        if o.select_get() is True:
234            o.select_set(False)
235
236    boxes[0].select_set(True)
237    bpy.context.view_layer.objects.active = boxes[0]
238
239    return
240
241
242# ------------------------------------------------------------------------------
243# Create books unit
244#
245# objName: Name for the new object
246# thickness: wood thickness (sides)
247# sX: Size in X axis
248# sY: Size in Y axis
249# sZ: Size in Z axis
250# pX: position X axis
251# pY: position Y axis
252# pZ: position Z axis
253# mat: Flag for creating materials
254# frX: Random factor X
255# frY: Random factor Y
256# frZ: Random factor Z
257# frR: Random factor Rotation
258# oX: override x size
259# oY: override y size
260# oZ: override z size
261# oR: override rotation
262# objcol: color
263# frC: color randomness factor
264# ------------------------------------------------------------------------------
265def create_book(objname, sx, sy, sz, px, py, pz, mat, frx,
266                fry, frz, frr, ox, oy, oz, ot, objcol, frc):
267    # gap Randomness
268    ri = randint(10, 150)
269    gap = ri / 100000
270    # Randomness X
271    if ox == 0:
272        ri = randint(0, int(frx * 1000))
273        factor = ri / 1000
274        sx -= sx * factor
275        if sx < (gap * 3):
276            sx = gap * 3
277    else:
278        sx = ox
279
280        # Randomness Y
281    if oy == 0:
282        ri = randint(0, int(fry * 1000))
283        factor = ri / 1000
284        sy -= sy * factor
285        if sy < (gap * 3):
286            sy = gap * 3
287    else:
288        sy = oy
289
290        # Randomness Z
291    if oz == 0:
292        ri = randint(0, int(frz * 1000))
293        factor = ri / 1000
294        sz -= sz * factor
295        if sz < (gap * 3):
296            sz = gap * 3
297    else:
298        sz = oz
299
300        # Randomness rotation
301    rot = 0
302    if frr > 0 and ot != -1:
303        if ot == 0:
304            ri = randint(0, int(frr * 1000))
305            factor = ri / 1000
306            rot = 30 * factor
307        else:
308            rot = ot
309
310    # Randomness color (only hue)
311    hsv = rgb_to_hsv(objcol[0], objcol[1], objcol[2])
312    hue = hsv[0]
313    if frc > 0:
314        rc1 = randint(0, int(hue * 1000))  # 0 to hue
315        rc2 = randint(int(hue * 1000), 1000)  # hue to maximum
316        rc3 = randint(0, 1000)  # sign
317
318        if rc3 >= hue * 1000:
319            hue += (rc2 * frc) / 1000
320        else:
321            hue -= (rc1 * frc) / 1000
322        # Convert random color
323        objcol = hsv_to_rgb(hue, hsv[1], hsv[2])
324
325    myvertex = []
326    myfaces = []
327    x = 0
328    # Left side
329    myvertex.extend([(x, -sy, 0), (0, 0, 0), (x, 0, sz), (x, -sy, sz)])
330    myfaces.extend([(0, 1, 2, 3)])
331
332    myvertex.extend([(x + gap, -sy + gap, 0), (x + gap, 0, 0), (x + gap, 0, sz),
333                     (x + gap, -sy + gap, sz)])
334    myfaces.extend([(4, 5, 6, 7)])
335
336    # Right side
337    x = sx - gap
338    myvertex.extend([(x, -sy + gap, 0), (x, 0, 0), (x, 0, sz), (x, -sy + gap, sz)])
339    myfaces.extend([(8, 9, 10, 11)])
340
341    myvertex.extend([(x + gap, -sy, 0), (x + gap, 0, 0), (x + gap, 0, sz), (x + gap, -sy, sz)])
342    myfaces.extend([(12, 13, 14, 15)])
343
344    myfaces.extend(
345        [(0, 12, 15, 3), (4, 8, 11, 7), (3, 15, 11, 7), (0, 12, 8, 4), (0, 1, 5, 4),
346         (8, 9, 13, 12), (3, 2, 6, 7),
347         (11, 10, 14, 15), (1, 2, 6, 5), (9, 10, 14, 13)])
348
349    # Top inside
350    myvertex.extend([(gap, -sy + gap, sz - gap), (gap, -gap, sz - gap), (sx - gap, -gap, sz - gap),
351                     (sx - gap, -sy + gap, sz - gap)])
352    myfaces.extend([(16, 17, 18, 19)])
353
354    # bottom inside and front face
355    myvertex.extend([(gap, -sy + gap, gap), (gap, -gap, gap), (sx - gap, -gap, gap), (sx - gap, -sy + gap, gap)])
356    myfaces.extend([(20, 21, 22, 23), (17, 18, 22, 21)])
357
358    mymesh = bpy.data.meshes.new(objname)
359    mybook = bpy.data.objects.new(objname, mymesh)
360
361    mybook.location[0] = px
362    mybook.location[1] = py
363    mybook.location[2] = pz + sin(radians(rot)) * sx
364    bpy.context.collection.objects.link(mybook)
365
366    mymesh.from_pydata(myvertex, [], myfaces)
367    mymesh.update(calc_edges=True)
368
369    # ---------------------------------
370    # Materials and UV Maps
371    # ---------------------------------
372    if mat and bpy.context.scene.render.engine in {'CYCLES', 'BLENDER_EEVEE'}:
373        rgb = objcol
374        # External
375        mat = create_diffuse_material(objname + "_material", True,
376                                      rgb[0], rgb[1], rgb[2], rgb[0], rgb[1], rgb[2], 0.05)
377        set_material(mybook, mat)
378        # UV unwrap external
379        select_faces(mybook, 0, True)
380        select_faces(mybook, 3, False)
381        select_faces(mybook, 4, False)
382        unwrap_mesh(mybook, False)
383        # Add Internal
384        mat = create_diffuse_material(objname + "_side_material", True, 0.5, 0.5, 0.5, 0.5, 0.5, 0.3, 0.03)
385        mybook.data.materials.append(mat)
386        select_faces(mybook, 14, True)
387        select_faces(mybook, 15, False)
388        select_faces(mybook, 16, False)
389        set_material_faces(mybook, 1)
390        # UV unwrap
391        bpy.ops.object.mode_set(mode='EDIT', toggle=False)
392        bpy.ops.mesh.select_all(action='DESELECT')
393        bpy.ops.object.mode_set(mode='OBJECT')
394        select_faces(mybook, 14, True)
395        select_faces(mybook, 15, False)
396        select_faces(mybook, 16, False)
397        unwrap_mesh(mybook, False)
398
399    # ---------------------------------
400    # Rotation on Y axis
401    # ---------------------------------
402    mybook.rotation_euler = (0.0, radians(rot), 0.0)  # radians
403
404    # add some gap to the size between books
405    return mybook, (sx, sy, sz, rot)
406