1# -*- coding:utf-8 -*-
2
3# ##### BEGIN GPL LICENSE BLOCK #####
4#
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.
9#
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.
14#
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.
18#
19# ##### END GPL LICENSE BLOCK #####
20
21# <pep8 compliant>
22
23# ----------------------------------------------------------
24# Author: Stephen Leger (s-leger)
25#
26# ----------------------------------------------------------
27# noinspection PyUnresolvedReferences
28import bpy
29# noinspection PyUnresolvedReferences
30from bpy.props import BoolProperty, StringProperty
31from mathutils import Vector, Matrix
32from mathutils.geometry import (
33    intersect_line_plane
34    )
35from bpy_extras.view3d_utils import (
36    region_2d_to_origin_3d,
37    region_2d_to_vector_3d
38    )
39
40
41class ArchipackCollectionManager():
42
43    @staticmethod
44    def link_object_to_scene(context, o):
45        coll_main = context.scene.collection.children.get("Archipack")
46        if coll_main is None:
47            coll_main = bpy.data.collections.new(name="Archipack")
48            context.scene.collection.children.link(coll_main)
49        coll_main.objects.link(o)
50
51    @staticmethod
52    def unlink_object_from_scene(o):
53        for coll in o.users_collection:
54            coll.objects.unlink(o)
55
56
57class ArchipackObject(ArchipackCollectionManager):
58    """
59        Shared property of archipack's objects PropertyGroup
60        provide basic support for copy to selected
61        and datablock access / filtering by object
62    """
63
64    def iskindof(self, o, typ):
65        """
66            return true if object contains databloc of typ name
67        """
68        return o.data is not None and typ in o.data
69
70    @classmethod
71    def filter(cls, o):
72        """
73            Filter object with this class in data
74            return
75            True when object contains this datablock
76            False otherwise
77            usage:
78            class_name.filter(object) from outside world
79            self.__class__.filter(object) from instance
80        """
81        try:
82            return cls.__name__ in o.data
83        except:
84            pass
85        return False
86
87    @classmethod
88    def datablock(cls, o):
89        """
90            Retrieve datablock from base object
91            return
92                datablock when found
93                None when not found
94            usage:
95                class_name.datablock(object) from outside world
96                self.__class__.datablock(object) from instance
97        """
98        try:
99            return getattr(o.data, cls.__name__)[0]
100        except:
101            pass
102        return None
103
104    def find_in_selection(self, context, auto_update=True):
105        """
106            find witch selected object this datablock instance belongs to
107            store context to be able to restore after oops
108            provide support for "copy to selected"
109            return
110            object or None when instance not found in selected objects
111        """
112        if auto_update is False:
113            return None
114
115        active = context.active_object
116        selected = context.selected_objects[:]
117
118        for o in selected:
119
120            if self.__class__.datablock(o) == self:
121                self.previously_selected = selected
122                self.previously_active = active
123                return o
124
125        return None
126
127    def restore_context(self, context):
128        # restore context
129        bpy.ops.object.select_all(action="DESELECT")
130
131        try:
132            for o in self.previously_selected:
133                o.select_set(state=True)
134        except:
135            pass
136        if self.previously_active is not None:
137            self.previously_active.select_set(state=True)
138            context.view_layer.objects.active = self.previously_active
139        self.previously_selected = None
140        self.previously_active = None
141
142    def move_object(self, o, p):
143        """
144         When firstpoint is moving we must move object according
145         p is new x, y location in world coordsys
146        """
147        p = Vector((p.x, p.y, o.matrix_world.translation.z))
148        # p is in o coordsys
149        if o.parent:
150            o.location = p @ o.parent.matrix_world.inverted()
151            o.matrix_world.translation = p
152        else:
153            o.location = p
154            o.matrix_world.translation = p
155
156
157class ArchipackCreateTool(ArchipackCollectionManager):
158    """
159        Shared property of archipack's create tool Operator
160    """
161    auto_manipulate : BoolProperty(
162            name="Auto manipulate",
163            description="Enable object's manipulators after create",
164            options={'SKIP_SAVE'},
165            default=True
166            )
167    filepath : StringProperty(
168            options={'SKIP_SAVE'},
169            name="Preset",
170            description="Full filename of python preset to load at create time",
171            default=""
172            )
173
174    @property
175    def archipack_category(self):
176        """
177            return target object name from ARCHIPACK_OT_object_name
178        """
179        return self.bl_idname[13:]
180
181    def load_preset(self, d):
182        """
183            Load python preset
184            d: archipack object datablock
185            preset: full filename.py with path
186        """
187        d.auto_update = False
188        fallback = True
189        if self.filepath != "":
190            try:
191                bpy.ops.script.python_file_run(filepath=self.filepath)
192                fallback = False
193            except:
194                pass
195            if fallback:
196                # fallback to load preset on background process
197                try:
198                    with open(self.filepath) as f:
199                        lines = f.read()
200                        cmp = compile(lines, self.filepath, 'exec')
201                        exec(cmp)
202                except:
203                    print("Archipack unable to load preset file : %s" % (self.filepath))
204                    pass
205        d.auto_update = True
206
207    def add_material(self, o, material='DEFAULT', category=None):
208        # skip if preset already add material
209        if "archipack_material" in o:
210            return
211        try:
212            if category is None:
213                category = self.archipack_category
214            if bpy.ops.archipack.material.poll():
215                bpy.ops.archipack.material(category=category, material=material)
216        except:
217            print("Archipack %s materials not found" % (self.archipack_category))
218            pass
219
220    def manipulate(self):
221        if self.auto_manipulate:
222            try:
223                op = getattr(bpy.ops.archipack, self.archipack_category + "_manipulate")
224                if op.poll():
225                    op('INVOKE_DEFAULT')
226            except:
227                print("Archipack bpy.ops.archipack.%s_manipulate not found" % (self.archipack_category))
228                pass
229
230
231class ArchipackDrawTool(ArchipackCollectionManager):
232    """
233        Draw tools
234    """
235    def mouse_to_plane(self, context, event, origin=Vector((0, 0, 0)), normal=Vector((0, 0, 1))):
236        """
237            convert mouse pos to 3d point over plane defined by origin and normal
238        """
239        region = context.region
240        rv3d = context.region_data
241        co2d = (event.mouse_region_x, event.mouse_region_y)
242        view_vector_mouse = region_2d_to_vector_3d(region, rv3d, co2d)
243        ray_origin_mouse = region_2d_to_origin_3d(region, rv3d, co2d)
244        pt = intersect_line_plane(ray_origin_mouse, ray_origin_mouse + view_vector_mouse,
245           origin, normal, False)
246        # fix issue with parallel plane
247        if pt is None:
248            pt = intersect_line_plane(ray_origin_mouse, ray_origin_mouse + view_vector_mouse,
249                origin, view_vector_mouse, False)
250        return pt
251
252    def mouse_to_scene_raycast(self, context, event):
253        """
254            convert mouse pos to 3d point over plane defined by origin and normal
255        """
256        region = context.region
257        rv3d = context.region_data
258        co2d = (event.mouse_region_x, event.mouse_region_y)
259        view_vector_mouse = region_2d_to_vector_3d(region, rv3d, co2d)
260        ray_origin_mouse = region_2d_to_origin_3d(region, rv3d, co2d)
261        res, pos, normal, face_index, object, matrix_world = context.scene.ray_cast(
262            depsgraph=context.view_layer.depsgraph,
263            origin=ray_origin_mouse,
264            direction=view_vector_mouse)
265        return res, pos, normal, face_index, object, matrix_world
266
267    def mouse_hover_wall(self, context, event):
268        """
269            convert mouse pos to matrix at bottom of surrounded wall, y oriented outside wall
270        """
271        res, pt, y, i, o, tM = self.mouse_to_scene_raycast(context, event)
272        if res and o.data is not None and 'archipack_wall2' in o.data:
273            z = Vector((0, 0, 1))
274            d = o.data.archipack_wall2[0]
275            y = -y
276            pt += (0.5 * d.width) * y.normalized()
277            x = y.cross(z)
278            return True, Matrix([
279                [x.x, y.x, z.x, pt.x],
280                [x.y, y.y, z.y, pt.y],
281                [x.z, y.z, z.z, o.matrix_world.translation.z],
282                [0, 0, 0, 1]
283                ]), o, d.width, y, 0  # d.z_offset
284        return False, Matrix(), None, 0, Vector(), 0
285