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# Main panel for different Archimesh general actions
23# Author: Antonio Vazquez (antonioya)
24#
25# ----------------------------------------------------------
26# noinspection PyUnresolvedReferences
27import bpy
28# noinspection PyUnresolvedReferences
29import bgl
30from bpy.types import Operator, Panel, SpaceView3D
31from math import sqrt, fabs, pi, asin
32from .achm_tools import *
33from .achm_gltools import *
34
35
36# -----------------------------------------------------
37# Verify if boolean already exist
38# -----------------------------------------------------
39def isboolean(myobject, childobject):
40    flag = False
41    for mod in myobject.modifiers:
42        if mod.type == 'BOOLEAN':
43            if mod.object == childobject:
44                flag = True
45                break
46    return flag
47
48
49# ------------------------------------------------------
50# Button: Action to link windows and doors
51# ------------------------------------------------------
52class ARCHIMESH_OT_Hole(Operator):
53    bl_idname = "object.archimesh_cut_holes"
54    bl_label = "Auto Holes"
55    bl_description = "Enable windows and doors holes for any selected object (needs wall thickness)"
56    bl_category = 'View'
57
58    # ------------------------------
59    # Execute
60    # ------------------------------
61    # noinspection PyMethodMayBeStatic
62    def execute(self, context):
63        scene = context.scene
64        listobj = []
65        # ---------------------------------------------------------------------
66        # Save the list of selected objects because the select flag is missed
67        # only can be windows or doors
68        # ---------------------------------------------------------------------
69        for obj in bpy.context.scene.objects:
70            # noinspection PyBroadException
71            try:
72                if obj["archimesh.hole_enable"]:
73                    if obj.select_get() is True or scene.archimesh_select_only is False:
74                        listobj.extend([obj])
75            except:
76                continue
77        # ---------------------------
78        # Get the baseboard object
79        # ---------------------------
80        mybaseboard = None
81        for child in context.object.children:
82            # noinspection PyBroadException
83            try:
84                if child["archimesh.room_baseboard"]:
85                    mybaseboard = child
86            except:
87                continue
88        # ---------------------------
89        # Get the shell object
90        # ---------------------------
91        myshell = None
92        for child in context.object.children:
93            # noinspection PyBroadException
94            try:
95                if child["archimesh.room_shell"]:
96                    myshell = child
97            except:
98                continue
99
100        # -----------------------------
101        # Remove all empty Boolean modifiers
102        # -----------------------------
103        for mod in context.object.modifiers:
104            if mod.type == 'BOOLEAN':
105                if mod.object is None:
106                    bpy.ops.object.modifier_remove(modifier=mod.name)
107
108        # if thickness is 0, must be > 0
109        myroom = context.object
110        if myroom.RoomGenerator[0].wall_width == 0:
111            self.report({'WARNING'}, "Walls must have thickness for using autohole function. Change it and run again")
112        # -----------------------------
113        # Now apply Wall holes
114        # -----------------------------
115        for obj in listobj:
116            parentobj = context.object
117            # Parent the empty to the room (the parent of frame)
118            if obj.parent is not None:
119                bpy.ops.object.select_all(action='DESELECT')
120                parentobj.select_set(True)
121                obj.parent.select_set(True)  # parent of object
122                bpy.ops.object.parent_set(type='OBJECT', keep_transform=False)
123            # ---------------------------------------
124            # Add the modifier to controller
125            # and the scale to use the same thickness
126            # ---------------------------------------
127            for child in obj.parent.children:
128                # noinspection PyBroadException
129                try:
130                    if child["archimesh.ctrl_hole"]:
131                        # apply scale
132                        t = parentobj.RoomGenerator[0].wall_width
133                        if t > 0:
134                            child.scale.y = (t + 0.45) / (child.dimensions.y / child.scale.y)  # Add some gap
135                        else:
136                            child.scale.y = 1
137                        # add boolean modifier
138                        if isboolean(myroom, child) is False:
139                            set_modifier_boolean(myroom, child)
140                except:
141                    # print("Unexpected error:" + str(sys.exc_info()))
142                    pass
143
144        # ---------------------------------------
145        # Now add the modifiers to baseboard
146        # ---------------------------------------
147        if mybaseboard is not None:
148            for obj in bpy.context.scene.objects:
149                # noinspection PyBroadException
150                try:
151                    if obj["archimesh.ctrl_base"]:
152                        if obj.select_get() is True or scene.archimesh_select_only is False:
153                            # add boolean modifier
154                            if isboolean(mybaseboard, obj) is False:
155                                set_modifier_boolean(mybaseboard, obj)
156                except:
157                    pass
158
159        # ---------------------------------------
160        # Now add the modifiers to shell
161        # ---------------------------------------
162        if myshell is not None:
163            # Remove all empty Boolean modifiers
164            for mod in myshell.modifiers:
165                if mod.type == 'BOOLEAN':
166                    if mod.object is None:
167                        bpy.ops.object.modifier_remove(modifier=mod.name)
168
169            for obj in bpy.context.scene.objects:
170                # noinspection PyBroadException
171                try:
172                    if obj["archimesh.ctrl_hole"]:
173                        if obj.select_get() is True or scene.archimesh_select_only is False:
174                            # add boolean modifier
175                            if isboolean(myshell, obj) is False:
176                                set_modifier_boolean(myshell, obj)
177                except:
178                    pass
179
180        return {'FINISHED'}
181
182
183# ------------------------------------------------------
184# Button: Action to create room from grease pencil
185# ------------------------------------------------------
186class ARCHIMESH_OT_Pencil(Operator):
187    bl_idname = "object.archimesh_pencil_room"
188    bl_label = "Room from Draw"
189    bl_description = "Create a room base on grease pencil strokes (draw from top view (7 key))"
190    bl_category = 'View'
191
192    # ------------------------------
193    # Execute
194    # ------------------------------
195    def execute(self, context):
196        # Enable for debugging code
197        debugmode = False
198
199        scene = context.scene
200        mypoints = None
201        clearangles = None
202
203        if debugmode is True:
204            print("======================================================================")
205            print("==                                                                  ==")
206            print("==  Grease pencil strokes analysis                                  ==")
207            print("==                                                                  ==")
208            print("======================================================================")
209
210        # -----------------------------------
211        # Get grease pencil points
212        # -----------------------------------
213        # noinspection PyBroadException
214        try:
215
216            # noinspection PyBroadException
217            try:
218                pencil = bpy.context.object.grease_pencil.layers.active
219            except:
220                pencil = bpy.context.scene.grease_pencil.layers.active
221
222            if pencil.active_frame is not None:
223                for i, stroke in enumerate(pencil.active_frame.strokes):
224                    stroke_points = pencil.active_frame.strokes[i].points
225                    allpoints = [(point.co.x, point.co.y)
226                                 for point in stroke_points]
227
228                    mypoints = []
229                    idx = 0
230                    x = 0
231                    y = 0
232                    orientation = None
233                    old_orientation = None
234
235                    for point in allpoints:
236                        if idx == 0:
237                            x = point[0]
238                            y = point[1]
239                        else:
240                            abs_x = abs(point[0] - x)
241                            abs_y = abs(point[1] - y)
242
243                            if abs_y > abs_x:
244                                orientation = "V"
245                            else:
246                                orientation = "H"
247
248                            if old_orientation == orientation:
249                                x = point[0]
250                                y = point[1]
251                            else:
252                                mypoints.extend([(x, y)])
253                                x = point[0]
254                                y = point[1]
255                                old_orientation = orientation
256
257                        idx += 1
258                    # Last point
259                    mypoints.extend([(x, y)])
260
261                    if debugmode is True:
262                        print("\nPoints\n====================")
263                        i = 0
264                        for p in mypoints:
265                            print(str(i) + ":" + str(p))
266                            i += 1
267                    # -----------------------------------
268                    # Calculate distance between points
269                    # -----------------------------------
270                    if debugmode is True:
271                        print("\nDistance\n====================")
272                    i = len(mypoints)
273                    distlist = []
274                    for e in range(1, i):
275                        d = sqrt(
276                            ((mypoints[e][0] - mypoints[e - 1][0]) ** 2) + ((mypoints[e][1] - mypoints[e - 1][1]) ** 2))
277                        # Imperial units if needed
278                        if bpy.context.scene.unit_settings.system == "IMPERIAL":
279                            d *= 3.2808399
280
281                        distlist.extend([d])
282
283                        if debugmode is True:
284                            print(str(e - 1) + ":" + str(d))
285                    # -----------------------------------
286                    # Calculate angle of walls
287                    # clamped to right angles
288                    # -----------------------------------
289                    if debugmode is True:
290                        print("\nAngle\n====================")
291
292                    i = len(mypoints)
293                    anglelist = []
294                    for e in range(1, i):
295                        sinv = (mypoints[e][1] - mypoints[e - 1][1]) / sqrt(
296                            ((mypoints[e][0] - mypoints[e - 1][0]) ** 2) + ((mypoints[e][1] - mypoints[e - 1][1]) ** 2))
297                        a = asin(sinv)
298                        # Clamp to 90 or 0 degrees
299                        if fabs(a) > pi / 4:
300                            b = pi / 2
301                        else:
302                            b = 0
303
304                        anglelist.extend([b])
305                        # Reverse de distance using angles (inverse angle to axis) for Vertical lines
306                        if a < 0.0 and b != 0:
307                            distlist[e - 1] *= -1  # reverse distance
308
309                        # Reverse de distance for horizontal lines
310                        if b == 0:
311                            if mypoints[e - 1][0] > mypoints[e][0]:
312                                distlist[e - 1] *= -1  # reverse distance
313
314                        if debugmode is True:
315                            print(str(e - 1) + ":" + str((a * 180) / pi) + "...:" + str(
316                                (b * 180) / pi) + "--->" + str(distlist[e - 1]))
317
318                    # ---------------------------------------
319                    # Verify duplications and reduce noise
320                    # ---------------------------------------
321                    if len(anglelist) >= 1:
322                        clearangles = []
323                        cleardistan = []
324                        i = len(anglelist)
325                        oldangle = anglelist[0]
326                        olddist = 0
327                        for e in range(0, i):
328                            if oldangle != anglelist[e]:
329                                clearangles.extend([oldangle])
330                                cleardistan.extend([olddist])
331                                oldangle = anglelist[e]
332                                olddist = distlist[e]
333                            else:
334                                olddist += distlist[e]
335                        # last
336                        clearangles.extend([oldangle])
337                        cleardistan.extend([olddist])
338
339            # ----------------------------
340            # Create the room
341            # ----------------------------
342            if len(mypoints) > 1 and len(clearangles) > 0:
343                # Move cursor
344                bpy.context.scene.cursor.location.x = mypoints[0][0]
345                bpy.context.scene.cursor.location.y = mypoints[0][1]
346                bpy.context.scene.cursor.location.z = 0  # always on grid floor
347
348                # Add room mesh
349                bpy.ops.mesh.archimesh_room()
350                myroom = context.object
351                mydata = myroom.RoomGenerator[0]
352                # Number of walls
353                mydata.wall_num = len(mypoints) - 1
354                mydata.ceiling = scene.archimesh_ceiling
355                mydata.floor = scene.archimesh_floor
356                mydata.merge = scene.archimesh_merge
357
358                i = len(mypoints)
359                for e in range(0, i - 1):
360                    if clearangles[e] == pi / 2:
361                        if cleardistan[e] > 0:
362                            mydata.walls[e].w = round(fabs(cleardistan[e]), 2)
363                            mydata.walls[e].r = (fabs(clearangles[e]) * 180) / pi  # from radians
364                        else:
365                            mydata.walls[e].w = round(fabs(cleardistan[e]), 2)
366                            mydata.walls[e].r = (fabs(clearangles[e]) * 180 * -1) / pi  # from radians
367
368                    else:
369                        mydata.walls[e].w = round(cleardistan[e], 2)
370                        mydata.walls[e].r = (fabs(clearangles[e]) * 180) / pi  # from radians
371
372                # Remove Grease pencil
373                if pencil is not None:
374                    for frame in pencil.frames:
375                        pencil.frames.remove(frame)
376
377                self.report({'INFO'}, "Archimesh: Room created from grease pencil strokes")
378            else:
379                self.report({'WARNING'}, "Archimesh: Not enough grease pencil strokes for creating room.")
380
381            return {'FINISHED'}
382        except:
383            self.report({'WARNING'}, "Archimesh: No grease pencil strokes. Do strokes in top view before creating room")
384            return {'CANCELLED'}
385
386
387# ------------------------------------------------------------------
388# Define panel class for main functions.
389# ------------------------------------------------------------------
390class ARCHIMESH_PT_Main(Panel):
391    bl_idname = "ARCHIMESH_PT_main"
392    bl_label = "Archimesh"
393    bl_space_type = "VIEW_3D"
394    bl_region_type = "UI"
395    bl_category = "Create"
396    bl_context = "objectmode"
397    bl_options = {'DEFAULT_CLOSED'}
398
399    # ------------------------------
400    # Draw UI
401    # ------------------------------
402    def draw(self, context):
403        layout = self.layout
404        scene = context.scene
405
406        myobj = context.object
407        # -------------------------------------------------------------------------
408        # If the selected object didn't be created with the group 'RoomGenerator',
409        # this button is not created.
410        # -------------------------------------------------------------------------
411        # noinspection PyBroadException
412        try:
413            if 'RoomGenerator' in myobj:
414                box = layout.box()
415                box.label(text="Room Tools", icon='MODIFIER')
416                row = box.row(align=False)
417                row.operator("object.archimesh_cut_holes", icon='GRID')
418                row.prop(scene, "archimesh_select_only")
419
420                # Export/Import
421                row = box.row(align=False)
422                row.operator("io_import.roomdata", text="Import", icon='COPYDOWN')
423                row.operator("io_export.roomdata", text="Export", icon='PASTEDOWN')
424        except:
425            pass
426
427        # -------------------------------------------------------------------------
428        # If the selected object isn't a kitchen
429        # this button is not created.
430        # -------------------------------------------------------------------------
431        # noinspection PyBroadException
432        try:
433            if myobj["archimesh.sku"] is not None:
434                box = layout.box()
435                box.label(text="Kitchen Tools", icon='MODIFIER')
436                # Export
437                row = box.row(align=False)
438                row.operator("io_export.kitchen_inventory", text="Export inventory", icon='PASTEDOWN')
439        except:
440            pass
441
442        # ------------------------------
443        # Elements Buttons
444        # ------------------------------
445        box = layout.box()
446        box.label(text="Elements", icon='GROUP')
447        row = box.row()
448        row.operator("mesh.archimesh_room")
449        row.operator("mesh.archimesh_column")
450        row = box.row()
451        row.operator("mesh.archimesh_door")
452        row = box.row()
453        row.operator("mesh.archimesh_window")
454        row.operator("mesh.archimesh_winpanel")
455        row = box.row()
456        row.operator("mesh.archimesh_kitchen")
457        row.operator("mesh.archimesh_shelves")
458        row = box.row()
459        row.operator("mesh.archimesh_stairs")
460        row.operator("mesh.archimesh_roof")
461
462        # ------------------------------
463        # Prop Buttons
464        # ------------------------------
465        box = layout.box()
466        box.label(text="Props", icon='LIGHT_DATA')
467        row = box.row()
468        row.operator("mesh.archimesh_books")
469        row.operator("mesh.archimesh_light")
470        row = box.row()
471        row.operator("mesh.archimesh_venetian")
472        row.operator("mesh.archimesh_roller")
473        row = box.row()
474        row.operator("mesh.archimesh_japan")
475
476        # ------------------------------
477        # OpenGL Buttons
478        # ------------------------------
479        box = layout.box()
480        box.label(text="Display hints", icon='QUESTION')
481        row = box.row()
482        if context.window_manager.archimesh_run_opengl is False:
483            icon = 'PLAY'
484            txt = 'Show'
485        else:
486            icon = "PAUSE"
487            txt = 'Hide'
488        row.operator("archimesh.runopenglbutton", text=txt, icon=icon)
489        row = box.row()
490        row.prop(scene, "archimesh_gl_measure", toggle=True, icon="ALIGN_CENTER")
491        row.prop(scene, "archimesh_gl_name", toggle=True, icon="OUTLINER_OB_FONT")
492        row.prop(scene, "archimesh_gl_ghost", icon='GHOST_ENABLED')
493        row = box.row()
494        row.prop(scene, "archimesh_text_color", text="")
495        row.prop(scene, "archimesh_walltext_color", text="")
496        row = box.row()
497        row.prop(scene, "archimesh_font_size")
498        row.prop(scene, "archimesh_wfont_size")
499        row = box.row()
500        row.prop(scene, "archimesh_hint_space")
501        # ------------------------------
502        # Grease pencil tools
503        # ------------------------------
504        box = layout.box()
505        box.label(text="Pencil Tools", icon='MODIFIER')
506        row = box.row(align=False)
507        row.operator("object.archimesh_pencil_room", icon='GREASEPENCIL')
508        row = box.row(align=False)
509        row.prop(scene, "archimesh_ceiling")
510        row.prop(scene, "archimesh_floor")
511        row.prop(scene, "archimesh_merge")
512
513
514# -------------------------------------------------------------
515# Defines button for enable/disable the tip display
516#
517# -------------------------------------------------------------
518class ARCHIMESH_OT_HintDisplay(Operator):
519    bl_idname = "archimesh.runopenglbutton"
520    bl_label = "Display hint data manager"
521    bl_description = "Display additional information in the viewport"
522    bl_category = 'View'
523
524    _handle = None  # keep function handler
525
526    # ----------------------------------
527    # Enable gl drawing adding handler
528    # ----------------------------------
529    @staticmethod
530    def handle_add(self, context):
531        if ARCHIMESH_OT_HintDisplay._handle is None:
532            ARCHIMESH_OT_HintDisplay._handle = SpaceView3D.draw_handler_add(draw_callback_px, (self, context),
533                                                                                      'WINDOW',
534                                                                                      'POST_PIXEL')
535            context.window_manager.archimesh_run_opengl = True
536
537    # ------------------------------------
538    # Disable gl drawing removing handler
539    # ------------------------------------
540    # noinspection PyUnusedLocal
541    @staticmethod
542    def handle_remove(self, context):
543        if ARCHIMESH_OT_HintDisplay._handle is not None:
544            SpaceView3D.draw_handler_remove(ARCHIMESH_OT_HintDisplay._handle, 'WINDOW')
545        ARCHIMESH_OT_HintDisplay._handle = None
546        context.window_manager.archimesh_run_opengl = False
547
548    # ------------------------------
549    # Execute button action
550    # ------------------------------
551    def execute(self, context):
552        if context.area.type == 'VIEW_3D':
553            if context.window_manager.archimesh_run_opengl is False:
554                self.handle_add(self, context)
555                context.area.tag_redraw()
556            else:
557                self.handle_remove(self, context)
558                context.area.tag_redraw()
559
560            return {'FINISHED'}
561        else:
562            self.report({'WARNING'},
563                        "View3D not found, cannot run operator")
564
565        return {'CANCELLED'}
566
567
568# -------------------------------------------------------------
569# Handler for drawing OpenGl
570# -------------------------------------------------------------
571# noinspection PyUnusedLocal
572def draw_callback_px(self, context):
573    draw_main(context)
574