From c1ab9b4b9c6c0226f8d7789b92efda9b0f33cfd1 Mon Sep 17 00:00:00 2001 From: Stephen Leger Date: Sat, 22 Jul 2017 13:25:28 +0200 Subject: archipack: T52120 release to official --- archipack/__init__.py | 646 +++++ archipack/archipack_2d.py | 893 ++++++ archipack/archipack_autoboolean.py | 678 +++++ archipack/archipack_door.py | 1847 +++++++++++++ archipack/archipack_fence.py | 1782 ++++++++++++ archipack/archipack_floor.py | 1190 ++++++++ archipack/archipack_gl.py | 1228 +++++++++ archipack/archipack_handle.py | 178 ++ archipack/archipack_keymaps.py | 108 + archipack/archipack_manipulator.py | 2446 +++++++++++++++++ archipack/archipack_object.py | 237 ++ archipack/archipack_polylib.py | 2274 ++++++++++++++++ archipack/archipack_preset.py | 578 ++++ archipack/archipack_reference_point.py | 368 +++ archipack/archipack_rendering.py | 529 ++++ archipack/archipack_slab.py | 1505 +++++++++++ archipack/archipack_snap.py | 309 +++ archipack/archipack_stair.py | 2849 ++++++++++++++++++++ archipack/archipack_truss.py | 380 +++ archipack/archipack_wall.py | 137 + archipack/archipack_wall2.py | 2220 +++++++++++++++ archipack/archipack_window.py | 2098 ++++++++++++++ archipack/bitarray.py | 97 + archipack/bmesh_utils.py | 249 ++ archipack/icons/archipack.png | Bin 0 -> 1364 bytes archipack/icons/detect.png | Bin 0 -> 281 bytes archipack/icons/door.png | Bin 0 -> 414 bytes archipack/icons/fence.png | Bin 0 -> 1779 bytes archipack/icons/floor.png | Bin 0 -> 1457 bytes archipack/icons/polygons.png | Bin 0 -> 242 bytes archipack/icons/selection.png | Bin 0 -> 1021 bytes archipack/icons/slab.png | Bin 0 -> 1620 bytes archipack/icons/stair.png | Bin 0 -> 1486 bytes archipack/icons/truss.png | Bin 0 -> 1462 bytes archipack/icons/union.png | Bin 0 -> 1102 bytes archipack/icons/wall.png | Bin 0 -> 637 bytes archipack/icons/window.png | Bin 0 -> 579 bytes archipack/materialutils.py | 169 ++ archipack/panel.py | 715 +++++ archipack/presets/archipack_door/160x200_dual.png | Bin 0 -> 10252 bytes archipack/presets/archipack_door/160x200_dual.py | 23 + .../presets/archipack_door/400x240_garage.png | Bin 0 -> 10492 bytes archipack/presets/archipack_door/400x240_garage.py | 23 + archipack/presets/archipack_door/80x200.png | Bin 0 -> 7840 bytes archipack/presets/archipack_door/80x200.py | 23 + archipack/presets/archipack_fence/glass_panels.png | Bin 0 -> 7106 bytes archipack/presets/archipack_fence/glass_panels.py | 67 + .../archipack_fence/inox_glass_concrete.png | Bin 0 -> 7835 bytes .../presets/archipack_fence/inox_glass_concrete.py | 64 + archipack/presets/archipack_fence/metal.png | Bin 0 -> 10234 bytes archipack/presets/archipack_fence/metal.py | 67 + archipack/presets/archipack_fence/metal_glass.png | Bin 0 -> 9582 bytes archipack/presets/archipack_fence/metal_glass.py | 67 + archipack/presets/archipack_fence/wood.png | Bin 0 -> 13183 bytes archipack/presets/archipack_fence/wood.py | 67 + .../presets/archipack_floor/herringbone_50x10.png | Bin 0 -> 11148 bytes .../presets/archipack_floor/herringbone_50x10.py | 34 + .../archipack_floor/herringbone_p_50x10.png | Bin 0 -> 10924 bytes .../presets/archipack_floor/herringbone_p_50x10.py | 34 + archipack/presets/archipack_floor/parquet_15x3.png | Bin 0 -> 13445 bytes archipack/presets/archipack_floor/parquet_15x3.py | 34 + .../presets/archipack_floor/planks_200x20.png | Bin 0 -> 11644 bytes archipack/presets/archipack_floor/planks_200x20.py | 34 + archipack/presets/archipack_floor/tiles_15x15.png | Bin 0 -> 12939 bytes archipack/presets/archipack_floor/tiles_15x15.py | 34 + archipack/presets/archipack_floor/tiles_60x30.png | Bin 0 -> 11379 bytes archipack/presets/archipack_floor/tiles_60x30.py | 34 + .../presets/archipack_floor/tiles_hex_10x10.png | Bin 0 -> 13663 bytes .../presets/archipack_floor/tiles_hex_10x10.py | 34 + .../archipack_floor/tiles_l+ms_30x30_15x15.png | Bin 0 -> 12511 bytes .../archipack_floor/tiles_l+ms_30x30_15x15.py | 34 + .../archipack_floor/tiles_l+s_30x30_15x15.png | Bin 0 -> 11631 bytes .../archipack_floor/tiles_l+s_30x30_15x15.py | 34 + .../archipack_stair/i_wood_over_concrete.png | Bin 0 -> 15606 bytes .../archipack_stair/i_wood_over_concrete.py | 117 + .../archipack_stair/l_wood_over_concrete.png | Bin 0 -> 18279 bytes .../archipack_stair/l_wood_over_concrete.py | 155 ++ .../archipack_stair/o_wood_over_concrete.png | Bin 0 -> 13886 bytes .../archipack_stair/o_wood_over_concrete.py | 136 + .../archipack_stair/u_wood_over_concrete.png | Bin 0 -> 18165 bytes .../archipack_stair/u_wood_over_concrete.py | 155 ++ .../presets/archipack_window/120x110_flat_2.png | Bin 0 -> 8410 bytes .../presets/archipack_window/120x110_flat_2.py | 50 + .../archipack_window/120x110_flat_2_elliptic.png | Bin 0 -> 8593 bytes .../archipack_window/120x110_flat_2_elliptic.py | 58 + .../archipack_window/120x110_flat_2_oblique.png | Bin 0 -> 7969 bytes .../archipack_window/120x110_flat_2_oblique.py | 50 + .../archipack_window/120x110_flat_2_round.png | Bin 0 -> 8571 bytes .../archipack_window/120x110_flat_2_round.py | 58 + .../presets/archipack_window/180x110_flat_3.png | Bin 0 -> 9492 bytes .../presets/archipack_window/180x110_flat_3.py | 50 + .../presets/archipack_window/180x210_flat_3.png | Bin 0 -> 10314 bytes .../presets/archipack_window/180x210_flat_3.py | 50 + .../presets/archipack_window/180x210_rail_2.png | Bin 0 -> 9362 bytes .../presets/archipack_window/180x210_rail_2.py | 50 + .../presets/archipack_window/240x210_rail_3.png | Bin 0 -> 10360 bytes .../presets/archipack_window/240x210_rail_3.py | 50 + .../presets/archipack_window/80x80_flat_1.png | Bin 0 -> 7291 bytes archipack/presets/archipack_window/80x80_flat_1.py | 50 + .../archipack_window/80x80_flat_1_circle.png | Bin 0 -> 6914 bytes .../archipack_window/80x80_flat_1_circle.py | 58 + archipack/presets/missing.png | Bin 0 -> 3874 bytes archipack/pyqtree.py | 187 ++ 103 files changed, 27691 insertions(+) create mode 100644 archipack/__init__.py create mode 100644 archipack/archipack_2d.py create mode 100644 archipack/archipack_autoboolean.py create mode 100644 archipack/archipack_door.py create mode 100644 archipack/archipack_fence.py create mode 100644 archipack/archipack_floor.py create mode 100644 archipack/archipack_gl.py create mode 100644 archipack/archipack_handle.py create mode 100644 archipack/archipack_keymaps.py create mode 100644 archipack/archipack_manipulator.py create mode 100644 archipack/archipack_object.py create mode 100644 archipack/archipack_polylib.py create mode 100644 archipack/archipack_preset.py create mode 100644 archipack/archipack_reference_point.py create mode 100644 archipack/archipack_rendering.py create mode 100644 archipack/archipack_slab.py create mode 100644 archipack/archipack_snap.py create mode 100644 archipack/archipack_stair.py create mode 100644 archipack/archipack_truss.py create mode 100644 archipack/archipack_wall.py create mode 100644 archipack/archipack_wall2.py create mode 100644 archipack/archipack_window.py create mode 100644 archipack/bitarray.py create mode 100644 archipack/bmesh_utils.py create mode 100644 archipack/icons/archipack.png create mode 100644 archipack/icons/detect.png create mode 100644 archipack/icons/door.png create mode 100644 archipack/icons/fence.png create mode 100644 archipack/icons/floor.png create mode 100644 archipack/icons/polygons.png create mode 100644 archipack/icons/selection.png create mode 100644 archipack/icons/slab.png create mode 100644 archipack/icons/stair.png create mode 100644 archipack/icons/truss.png create mode 100644 archipack/icons/union.png create mode 100644 archipack/icons/wall.png create mode 100644 archipack/icons/window.png create mode 100644 archipack/materialutils.py create mode 100644 archipack/panel.py create mode 100644 archipack/presets/archipack_door/160x200_dual.png create mode 100644 archipack/presets/archipack_door/160x200_dual.py create mode 100644 archipack/presets/archipack_door/400x240_garage.png create mode 100644 archipack/presets/archipack_door/400x240_garage.py create mode 100644 archipack/presets/archipack_door/80x200.png create mode 100644 archipack/presets/archipack_door/80x200.py create mode 100644 archipack/presets/archipack_fence/glass_panels.png create mode 100644 archipack/presets/archipack_fence/glass_panels.py create mode 100644 archipack/presets/archipack_fence/inox_glass_concrete.png create mode 100644 archipack/presets/archipack_fence/inox_glass_concrete.py create mode 100644 archipack/presets/archipack_fence/metal.png create mode 100644 archipack/presets/archipack_fence/metal.py create mode 100644 archipack/presets/archipack_fence/metal_glass.png create mode 100644 archipack/presets/archipack_fence/metal_glass.py create mode 100644 archipack/presets/archipack_fence/wood.png create mode 100644 archipack/presets/archipack_fence/wood.py create mode 100644 archipack/presets/archipack_floor/herringbone_50x10.png create mode 100644 archipack/presets/archipack_floor/herringbone_50x10.py create mode 100644 archipack/presets/archipack_floor/herringbone_p_50x10.png create mode 100644 archipack/presets/archipack_floor/herringbone_p_50x10.py create mode 100644 archipack/presets/archipack_floor/parquet_15x3.png create mode 100644 archipack/presets/archipack_floor/parquet_15x3.py create mode 100644 archipack/presets/archipack_floor/planks_200x20.png create mode 100644 archipack/presets/archipack_floor/planks_200x20.py create mode 100644 archipack/presets/archipack_floor/tiles_15x15.png create mode 100644 archipack/presets/archipack_floor/tiles_15x15.py create mode 100644 archipack/presets/archipack_floor/tiles_60x30.png create mode 100644 archipack/presets/archipack_floor/tiles_60x30.py create mode 100644 archipack/presets/archipack_floor/tiles_hex_10x10.png create mode 100644 archipack/presets/archipack_floor/tiles_hex_10x10.py create mode 100644 archipack/presets/archipack_floor/tiles_l+ms_30x30_15x15.png create mode 100644 archipack/presets/archipack_floor/tiles_l+ms_30x30_15x15.py create mode 100644 archipack/presets/archipack_floor/tiles_l+s_30x30_15x15.png create mode 100644 archipack/presets/archipack_floor/tiles_l+s_30x30_15x15.py create mode 100644 archipack/presets/archipack_stair/i_wood_over_concrete.png create mode 100644 archipack/presets/archipack_stair/i_wood_over_concrete.py create mode 100644 archipack/presets/archipack_stair/l_wood_over_concrete.png create mode 100644 archipack/presets/archipack_stair/l_wood_over_concrete.py create mode 100644 archipack/presets/archipack_stair/o_wood_over_concrete.png create mode 100644 archipack/presets/archipack_stair/o_wood_over_concrete.py create mode 100644 archipack/presets/archipack_stair/u_wood_over_concrete.png create mode 100644 archipack/presets/archipack_stair/u_wood_over_concrete.py create mode 100644 archipack/presets/archipack_window/120x110_flat_2.png create mode 100644 archipack/presets/archipack_window/120x110_flat_2.py create mode 100644 archipack/presets/archipack_window/120x110_flat_2_elliptic.png create mode 100644 archipack/presets/archipack_window/120x110_flat_2_elliptic.py create mode 100644 archipack/presets/archipack_window/120x110_flat_2_oblique.png create mode 100644 archipack/presets/archipack_window/120x110_flat_2_oblique.py create mode 100644 archipack/presets/archipack_window/120x110_flat_2_round.png create mode 100644 archipack/presets/archipack_window/120x110_flat_2_round.py create mode 100644 archipack/presets/archipack_window/180x110_flat_3.png create mode 100644 archipack/presets/archipack_window/180x110_flat_3.py create mode 100644 archipack/presets/archipack_window/180x210_flat_3.png create mode 100644 archipack/presets/archipack_window/180x210_flat_3.py create mode 100644 archipack/presets/archipack_window/180x210_rail_2.png create mode 100644 archipack/presets/archipack_window/180x210_rail_2.py create mode 100644 archipack/presets/archipack_window/240x210_rail_3.png create mode 100644 archipack/presets/archipack_window/240x210_rail_3.py create mode 100644 archipack/presets/archipack_window/80x80_flat_1.png create mode 100644 archipack/presets/archipack_window/80x80_flat_1.py create mode 100644 archipack/presets/archipack_window/80x80_flat_1_circle.png create mode 100644 archipack/presets/archipack_window/80x80_flat_1_circle.py create mode 100644 archipack/presets/missing.png create mode 100644 archipack/pyqtree.py diff --git a/archipack/__init__.py b/archipack/__init__.py new file mode 100644 index 00000000..79ac9879 --- /dev/null +++ b/archipack/__init__.py @@ -0,0 +1,646 @@ +# -*- coding:utf-8 -*- + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +# + +# ---------------------------------------------------------- +# Author: Stephen Leger (s-leger) +# +# ---------------------------------------------------------- + +bl_info = { + 'name': 'Archipack', + 'description': 'Architectural objects and 2d polygons detection from unordered splines', + 'author': 's-leger', + 'license': 'GPL', + 'deps': 'shapely', + 'version': (1, 2, 6), + 'blender': (2, 7, 8), + 'location': 'View3D > Tools > Create > Archipack', + 'warning': '', + 'wiki_url': 'https://github.com/s-leger/archipack/wiki', + 'tracker_url': 'https://github.com/s-leger/archipack/issues', + 'link': 'https://github.com/s-leger/archipack', + 'support': 'COMMUNITY', + 'category': 'Add Mesh' + } + +import os + +if "bpy" in locals(): + import importlib as imp + imp.reload(archipack_snap) + imp.reload(archipack_manipulator) + imp.reload(archipack_reference_point) + imp.reload(archipack_autoboolean) + imp.reload(archipack_door) + imp.reload(archipack_window) + imp.reload(archipack_stair) + imp.reload(archipack_wall) + imp.reload(archipack_wall2) + imp.reload(archipack_slab) + imp.reload(archipack_fence) + imp.reload(archipack_truss) + imp.reload(archipack_floor) + imp.reload(archipack_rendering) + try: + imp.reload(archipack_polylib) + HAS_POLYLIB = True + except: + HAS_POLYLIB = False + pass + + print("archipack: reload ready") +else: + from . import archipack_snap + from . import archipack_manipulator + from . import archipack_reference_point + from . import archipack_autoboolean + from . import archipack_door + from . import archipack_window + from . import archipack_stair + from . import archipack_wall + from . import archipack_wall2 + from . import archipack_slab + from . import archipack_fence + from . import archipack_truss + from . import archipack_floor + from . import archipack_rendering + try: + """ + polylib depends on shapely + raise ImportError when not meet + """ + from . import archipack_polylib + HAS_POLYLIB = True + except: + print("archipack: shapely not found, using built in modules only") + HAS_POLYLIB = False + pass + + print("archipack: ready") + +# noinspection PyUnresolvedReferences +import bpy +# noinspection PyUnresolvedReferences +from bpy.types import ( + Panel, WindowManager, PropertyGroup, + AddonPreferences, Menu + ) +from bpy.props import ( + EnumProperty, PointerProperty, + StringProperty, BoolProperty, + IntProperty, FloatProperty, FloatVectorProperty + ) + +from bpy.utils import previews +icons_collection = {} + + +# ---------------------------------------------------- +# Addon preferences +# ---------------------------------------------------- + +def update_panel(self, context): + try: + bpy.utils.unregister_class(TOOLS_PT_Archipack_PolyLib) + bpy.utils.unregister_class(TOOLS_PT_Archipack_Tools) + bpy.utils.unregister_class(TOOLS_PT_Archipack_Create) + except: + pass + prefs = context.user_preferences.addons[__name__].preferences + TOOLS_PT_Archipack_PolyLib.bl_category = prefs.tools_category + bpy.utils.register_class(TOOLS_PT_Archipack_PolyLib) + TOOLS_PT_Archipack_Tools.bl_category = prefs.tools_category + bpy.utils.register_class(TOOLS_PT_Archipack_Tools) + TOOLS_PT_Archipack_Create.bl_category = prefs.create_category + bpy.utils.register_class(TOOLS_PT_Archipack_Create) + + +class Archipack_Pref(AddonPreferences): + bl_idname = __name__ + + tools_category = StringProperty( + name="Tools", + description="Choose a name for the category of the Tools panel", + default="Tools", + update=update_panel + ) + create_category = StringProperty( + name="Create", + description="Choose a name for the category of the Create panel", + default="Create", + update=update_panel + ) + create_submenu = BoolProperty( + name="Use Sub-menu", + description="Put Achipack's object into a sub menu (shift+a)", + default=True + ) + max_style_draw_tool = BoolProperty( + name="Draw a wall use 3dsmax style", + description="Reverse clic / release cycle for Draw a wall", + default=True + ) + # Arrow sizes (world units) + arrow_size = FloatProperty( + name="Arrow", + description="Manipulators arrow size (blender units)", + default=0.05 + ) + # Handle area size (pixels) + handle_size = IntProperty( + name="Handle", + description="Manipulators handle sensitive area size (pixels)", + min=2, + default=10 + ) + # Font sizes and basic colour scheme + feedback_size_main = IntProperty( + name="Main", + description="Main title font size (pixels)", + min=2, + default=16 + ) + feedback_size_title = IntProperty( + name="Title", + description="Tool name font size (pixels)", + min=2, + default=14 + ) + feedback_size_shortcut = IntProperty( + name="Shortcut", + description="Shortcuts font size (pixels)", + min=2, + default=11 + ) + feedback_shortcut_area = FloatVectorProperty( + name="Background Shortcut", + description="Shortcut area background color", + subtype='COLOR_GAMMA', + default=(0, 0.4, 0.6, 0.2), + size=4, + min=0, max=1 + ) + feedback_title_area = FloatVectorProperty( + name="Background Main", + description="Title area background color", + subtype='COLOR_GAMMA', + default=(0, 0.4, 0.6, 0.5), + size=4, + min=0, max=1 + ) + feedback_colour_main = FloatVectorProperty( + name="Font Main", + description="Title color", + subtype='COLOR_GAMMA', + default=(0.95, 0.95, 0.95, 1.0), + size=4, + min=0, max=1 + ) + feedback_colour_key = FloatVectorProperty( + name="Font Shortcut key", + description="KEY label color", + subtype='COLOR_GAMMA', + default=(0.67, 0.67, 0.67, 1.0), + size=4, + min=0, max=1 + ) + feedback_colour_shortcut = FloatVectorProperty( + name="Font Shortcut hint", + description="Shortcuts text color", + subtype='COLOR_GAMMA', + default=(0.51, 0.51, 0.51, 1.0), + size=4, + min=0, max=1 + ) + + def draw(self, context): + layout = self.layout + box = layout.box() + row = box.row() + col = row.column() + col.label(text="Tab Category:") + col.prop(self, "tools_category") + col.prop(self, "create_category") + col.prop(self, "create_submenu") + col.prop(self, "max_style_draw_tool") + box = layout.box() + row = box.row() + split = row.split(percentage=0.5) + col = split.column() + col.label(text="Colors:") + row = col.row(align=True) + row.prop(self, "feedback_title_area") + row = col.row(align=True) + row.prop(self, "feedback_shortcut_area") + row = col.row(align=True) + row.prop(self, "feedback_colour_main") + row = col.row(align=True) + row.prop(self, "feedback_colour_key") + row = col.row(align=True) + row.prop(self, "feedback_colour_shortcut") + col = split.column() + col.label(text="Font size:") + col.prop(self, "feedback_size_main") + col.prop(self, "feedback_size_title") + col.prop(self, "feedback_size_shortcut") + col.label(text="Manipulators:") + col.prop(self, "arrow_size") + col.prop(self, "handle_size") + + +# ---------------------------------------------------- +# Archipack panels +# ---------------------------------------------------- + + +class TOOLS_PT_Archipack_PolyLib(Panel): + bl_label = "Archipack 2d to 3d" + bl_idname = "TOOLS_PT_Archipack_PolyLib" + bl_space_type = "VIEW_3D" + bl_region_type = "TOOLS" + bl_category = "Tools" + bl_context = "objectmode" + + @classmethod + def poll(self, context): + + global archipack_polylib + return HAS_POLYLIB and ((archipack_polylib.vars_dict['select_polygons'] is not None) or + (context.object is not None and context.object.type == 'CURVE')) + + def draw(self, context): + global icons_collection + icons = icons_collection["main"] + layout = self.layout + row = layout.row(align=True) + box = row.box() + row = box.row(align=True) + row.operator( + "archipack.polylib_detect", + icon_value=icons["detect"].icon_id, + text='Detect' + ).extend = context.window_manager.archipack_polylib.extend + row.prop(context.window_manager.archipack_polylib, "extend") + row = box.row(align=True) + row.prop(context.window_manager.archipack_polylib, "resolution") + row = box.row(align=True) + row.label(text="Polygons") + row = box.row(align=True) + row.operator( + "archipack.polylib_pick_2d_polygons", + icon_value=icons["selection"].icon_id, + text='Select' + ).action = 'select' + row.operator( + "archipack.polylib_pick_2d_polygons", + icon_value=icons["union"].icon_id, + text='Union' + ).action = 'union' + row.operator( + "archipack.polylib_output_polygons", + icon_value=icons["polygons"].icon_id, + text='All') + row = box.row(align=True) + row.operator( + "archipack.polylib_pick_2d_polygons", + text='Wall', + icon_value=icons["wall"].icon_id).action = 'wall' + row.prop(context.window_manager.archipack_polylib, "solidify_thickness") + row = box.row(align=True) + row.operator("archipack.polylib_pick_2d_polygons", + text='Window', + icon_value=icons["window"].icon_id).action = 'window' + row.operator("archipack.polylib_pick_2d_polygons", + text='Door', + icon_value=icons["door"].icon_id).action = 'door' + row.operator("archipack.polylib_pick_2d_polygons", text='Rectangle').action = 'rectangle' + row = box.row(align=True) + row.label(text="Lines") + row = box.row(align=True) + row.operator( + "archipack.polylib_pick_2d_lines", + icon_value=icons["selection"].icon_id, + text='Lines').action = 'select' + row.operator( + "archipack.polylib_pick_2d_lines", + icon_value=icons["union"].icon_id, + text='Union').action = 'union' + row.operator( + "archipack.polylib_output_lines", + icon_value=icons["polygons"].icon_id, + text='All') + row = box.row(align=True) + row.label(text="Points") + row = box.row(align=True) + row.operator( + "archipack.polylib_pick_2d_points", + icon_value=icons["selection"].icon_id, + text='Points').action = 'select' + row = layout.row(align=True) + box = row.box() + row = box.row(align=True) + row.operator("archipack.polylib_simplify") + row.prop(context.window_manager.archipack_polylib, "simplify_tolerance") + row = box.row(align=True) + row.prop(context.window_manager.archipack_polylib, "simplify_preserve_topology") + row = layout.row(align=True) + box = row.box() + row = box.row(align=True) + row.operator("archipack.polylib_offset") + row = box.row(align=True) + row.prop(context.window_manager.archipack_polylib, "offset_distance") + row = box.row(align=True) + row.prop(context.window_manager.archipack_polylib, "offset_side") + row = box.row(align=True) + row.prop(context.window_manager.archipack_polylib, "offset_resolution") + row = box.row(align=True) + row.prop(context.window_manager.archipack_polylib, "offset_join_style") + row = box.row(align=True) + row.prop(context.window_manager.archipack_polylib, "offset_mitre_limit") + + +class TOOLS_PT_Archipack_Tools(Panel): + bl_label = "Archipack Tools" + bl_idname = "TOOLS_PT_Archipack_Tools" + bl_space_type = "VIEW_3D" + bl_region_type = "TOOLS" + bl_category = "Tools" + bl_context = "objectmode" + + @classmethod + def poll(self, context): + return True + + def draw(self, context): + wm = context.window_manager + layout = self.layout + row = layout.row(align=True) + box = row.box() + box.label("Auto boolean") + row = box.row(align=True) + row.operator("archipack.auto_boolean", text="AutoBoolean", icon='AUTO').mode = 'HYBRID' + row = layout.row(align=True) + box = row.box() + box.label("Rendering") + row = box.row(align=True) + row.prop(wm.archipack, 'render_type', text="") + row = box.row(align=True) + row.operator("archipack.render", icon='RENDER_STILL') + + +class TOOLS_PT_Archipack_Create(Panel): + bl_label = "Add Archipack" + bl_idname = "TOOLS_PT_Archipack_Create" + bl_space_type = "VIEW_3D" + bl_region_type = "TOOLS" + bl_category = "Create" + bl_context = "objectmode" + + @classmethod + def poll(self, context): + return True + + def draw(self, context): + global icons_collection + icons = icons_collection["main"] + layout = self.layout + row = layout.row(align=True) + box = row.box() + box.label("Objects") + row = box.row(align=True) + row.operator("archipack.window_preset_menu", + text="Window", + icon_value=icons["window"].icon_id + ).preset_operator = "archipack.window" + row.operator("archipack.window_preset_menu", + text="", + icon='GREASEPENCIL' + ).preset_operator = "archipack.window_draw" + row = box.row(align=True) + row.operator("archipack.door_preset_menu", + text="Door", + icon_value=icons["door"].icon_id + ).preset_operator = "archipack.door" + row.operator("archipack.door_preset_menu", + text="", + icon='GREASEPENCIL' + ).preset_operator = "archipack.door_draw" + row = box.row(align=True) + row.operator("archipack.stair_preset_menu", + text="Stair", + icon_value=icons["stair"].icon_id + ).preset_operator = "archipack.stair" + row = box.row(align=True) + row.operator("archipack.wall2", + icon_value=icons["wall"].icon_id + ) + row.operator("archipack.wall2_draw", text="Draw", icon='GREASEPENCIL') + row.operator("archipack.wall2_from_curve", text="", icon='CURVE_DATA') + + row = box.row(align=True) + row.operator("archipack.fence_preset_menu", + text="Fence", + icon_value=icons["fence"].icon_id + ).preset_operator = "archipack.fence" + row.operator("archipack.fence_from_curve", text="", icon='CURVE_DATA') + row = box.row(align=True) + row.operator("archipack.truss", + icon_value=icons["truss"].icon_id + ) + row = box.row(align=True) + row.operator("archipack.slab_from_curve", + icon_value=icons["slab"].icon_id + ) + row = box.row(align=True) + row.operator("archipack.wall2_from_slab", + icon_value=icons["wall"].icon_id) + row.operator("archipack.slab_from_wall", + icon_value=icons["slab"].icon_id + ).ceiling = False + row.operator("archipack.slab_from_wall", + text="->Ceiling", + icon_value=icons["slab"].icon_id + ).ceiling = True + row = box.row(align=True) + row.operator("archipack.floor_preset_menu", + text="Floor", + icon_value=icons["floor"].icon_id + ).preset_operator = "archipack.floor" + + +# ---------------------------------------------------- +# ALT + A menu +# ---------------------------------------------------- + + +def draw_menu(self, context): + global icons_collection + icons = icons_collection["main"] + layout = self.layout + layout.operator_context = 'INVOKE_REGION_WIN' + + layout.operator("archipack.wall2", + text="Wall", + icon_value=icons["wall"].icon_id + ) + layout.operator("archipack.window_preset_menu", + text="Window", + icon_value=icons["window"].icon_id + ).preset_operator = "archipack.window" + layout.operator("archipack.door_preset_menu", + text="Door", + icon_value=icons["door"].icon_id + ).preset_operator = "archipack.door" + layout.operator("archipack.stair_preset_menu", + text="Stair", + icon_value=icons["stair"].icon_id + ).preset_operator = "archipack.stair" + layout.operator("archipack.fence_preset_menu", + text="Fence", + icon_value=icons["fence"].icon_id + ).preset_operator = "archipack.fence" + layout.operator("archipack.truss", + text="Truss", + icon_value=icons["truss"].icon_id + ) + layout.operator("archipack.floor_preset_menu", + text="Floor", + icon_value=icons["floor"].icon_id + ) + + +class ARCHIPACK_create_menu(Menu): + bl_label = 'Archipack' + bl_idname = 'ARCHIPACK_create_menu' + bl_context = "objectmode" + + def draw(self, context): + draw_menu(self, context) + + +def menu_func(self, context): + layout = self.layout + layout.separator() + global icons_collection + icons = icons_collection["main"] + + # either draw sub menu or right at end of this one + if context.user_preferences.addons[__name__].preferences.create_submenu: + layout.operator_context = 'INVOKE_REGION_WIN' + layout.menu("ARCHIPACK_create_menu", icon_value=icons["archipack"].icon_id) + else: + draw_menu(self, context) + + +# ---------------------------------------------------- +# Datablock to store global addon variables +# ---------------------------------------------------- + + +class archipack_data(PropertyGroup): + render_type = EnumProperty( + items=( + ('1', "Draw over", "Draw over last rendered image"), + ('2', "OpenGL", ""), + ('3', "Animation OpenGL", ""), + ('4', "Image", "Render image and draw over"), + ('5', "Animation", "Draw on each frame") + ), + name="Render type", + description="Render method" + ) + + +def register(): + global icons_collection + icons = previews.new() + icons_dir = os.path.join(os.path.dirname(__file__), "icons") + for icon in os.listdir(icons_dir): + name, ext = os.path.splitext(icon) + icons.load(name, os.path.join(icons_dir, icon), 'IMAGE') + icons_collection["main"] = icons + + archipack_snap.register() + archipack_manipulator.register() + archipack_reference_point.register() + archipack_autoboolean.register() + archipack_door.register() + archipack_window.register() + archipack_stair.register() + archipack_wall.register() + archipack_wall2.register() + archipack_slab.register() + archipack_fence.register() + archipack_truss.register() + archipack_floor.register() + archipack_rendering.register() + + if HAS_POLYLIB: + archipack_polylib.register() + + bpy.utils.register_class(archipack_data) + WindowManager.archipack = PointerProperty(type=archipack_data) + bpy.utils.register_class(Archipack_Pref) + update_panel(None, bpy.context) + bpy.utils.register_class(ARCHIPACK_create_menu) + bpy.types.INFO_MT_mesh_add.append(menu_func) + + +def unregister(): + global icons_collection + bpy.types.INFO_MT_mesh_add.remove(menu_func) + bpy.utils.unregister_class(ARCHIPACK_create_menu) + + bpy.utils.unregister_class(TOOLS_PT_Archipack_PolyLib) + bpy.utils.unregister_class(TOOLS_PT_Archipack_Tools) + bpy.utils.unregister_class(TOOLS_PT_Archipack_Create) + bpy.utils.unregister_class(Archipack_Pref) + + # unregister subs + archipack_snap.unregister() + archipack_manipulator.unregister() + archipack_reference_point.unregister() + archipack_autoboolean.unregister() + archipack_door.unregister() + archipack_window.unregister() + archipack_stair.unregister() + archipack_wall.unregister() + archipack_wall2.unregister() + archipack_slab.unregister() + archipack_fence.unregister() + archipack_truss.unregister() + archipack_floor.unregister() + archipack_rendering.unregister() + + if HAS_POLYLIB: + archipack_polylib.unregister() + + bpy.utils.unregister_class(archipack_data) + del WindowManager.archipack + + for icons in icons_collection.values(): + previews.remove(icons) + icons_collection.clear() + + +if __name__ == "__main__": + register() diff --git a/archipack/archipack_2d.py b/archipack/archipack_2d.py new file mode 100644 index 00000000..912e3cb8 --- /dev/null +++ b/archipack/archipack_2d.py @@ -0,0 +1,893 @@ +# -*- coding:utf-8 -*- + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110- 1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +# + +# ---------------------------------------------------------- +# Author: Stephen Leger (s-leger) +# +# ---------------------------------------------------------- +from mathutils import Vector, Matrix +from math import sin, cos, pi, atan2, sqrt, acos +import bpy +# allow to draw parts with gl for debug puropses +from .archipack_gl import GlBaseLine + + +class Projection(GlBaseLine): + + def __init__(self): + GlBaseLine.__init__(self) + + def proj_xy(self, t, next=None): + """ + length of projection of sections at crossing line / circle intersections + deformation unit vector for profil in xy axis + so f(x_profile) = position of point in xy plane + """ + if next is None: + return self.normal(t).v.normalized(), 1 + v0 = self.normal(1).v.normalized() + v1 = next.normal(0).v.normalized() + direction = v0 + v1 + adj = (v0 * self.length) * (v1 * next.length) + hyp = (self.length * next.length) + c = min(1, max(-1, adj / hyp)) + size = 1 / cos(0.5 * acos(c)) + return direction.normalized(), min(3, size) + + def proj_z(self, t, dz0, next=None, dz1=0): + """ + length of projection along crossing line / circle + deformation unit vector for profil in z axis at line / line intersection + so f(y) = position of point in yz plane + """ + return Vector((0, 1)), 1 + """ + NOTE (to myself): + In theory this is how it has to be done so sections follow path, + but in real world results are better when sections are z-up. + So return a dumb 1 so f(y) = y + """ + if next is None: + dz = dz0 / self.length + else: + dz = (dz1 + dz0) / (self.length + next.length) + return Vector((0, 1)), sqrt(1 + dz * dz) + # 1 / sqrt(1 + (dz0 / self.length) * (dz0 / self.length)) + if next is None: + return Vector((-dz0, self.length)).normalized(), 1 + v0 = Vector((self.length, dz0)) + v1 = Vector((next.length, dz1)) + direction = Vector((-dz0, self.length)).normalized() + Vector((-dz1, next.length)).normalized() + adj = v0 * v1 + hyp = (v0.length * v1.length) + c = min(1, max(-1, adj / hyp)) + size = -cos(pi - 0.5 * acos(c)) + return direction.normalized(), size + + +class Line(Projection): + """ + 2d Line + Internally stored as p: origin and v:size and direction + moving p will move both ends of line + moving p0 or p1 move only one end of line + p1 + ^ + | v + p0 == p + """ + def __init__(self, p=None, v=None, p0=None, p1=None): + """ + Init by either + p: Vector or tuple origin + v: Vector or tuple size and direction + or + p0: Vector or tuple 1 point location + p1: Vector or tuple 2 point location + Will convert any into Vector 2d + both optionnals + """ + Projection.__init__(self) + if p is not None and v is not None: + self.p = Vector(p).to_2d() + self.v = Vector(v).to_2d() + elif p0 is not None and p1 is not None: + self.p = Vector(p0).to_2d() + self.v = Vector(p1).to_2d() - self.p + else: + self.p = Vector((0, 0)) + self.v = Vector((0, 0)) + + @property + def p0(self): + return self.p + + @property + def p1(self): + return self.p + self.v + + @p0.setter + def p0(self, p0): + """ + Note: setting p0 + move p0 only + """ + p1 = self.p1 + self.p = Vector(p0).to_2d() + self.v = p1 - p0 + + @p1.setter + def p1(self, p1): + """ + Note: setting p1 + move p1 only + """ + self.v = Vector(p1).to_2d() - self.p + + @property + def length(self): + """ + 3d length + """ + return self.v.length + + @property + def angle(self): + """ + 2d angle on xy plane + """ + return atan2(self.v.y, self.v.x) + + @property + def angle_normal(self): + """ + 2d angle of perpendicular + lie on the right side + p1 + |--x + p0 + """ + return atan2(-self.v.x, self.v.y) + + @property + def reversed(self): + return Line(self.p, -self.v) + + @property + def oposite(self): + return Line(self.p + self.v, -self.v) + + @property + def cross_z(self): + """ + 2d Vector perpendicular on plane xy + lie on the right side + p1 + |--x + p0 + """ + return Vector((self.v.y, -self.v.x)) + + @property + def cross(self): + return Vector((self.v.y, -self.v.x)) + + def signed_angle(self, u, v): + """ + signed angle between two vectors range [-pi, pi] + """ + return atan2(u.x * v.y - u.y * v.x, u.x * v.x + u.y * v.y) + + def delta_angle(self, last): + """ + signed delta angle between end of line and start of this one + this value is object's a0 for segment = self + """ + if last is None: + return self.angle + return self.signed_angle(last.straight(1, 1).v, self.straight(1, 0).v) + + def normal(self, t=0): + """ + 2d Line perpendicular on plane xy + at position t in current segment + lie on the right side + p1 + |--x + p0 + """ + return Line(self.lerp(t), self.cross_z) + + def sized_normal(self, t, size): + """ + 2d Line perpendicular on plane xy + at position t in current segment + and of given length + lie on the right side when size > 0 + p1 + |--x + p0 + """ + return Line(self.lerp(t), size * self.cross_z.normalized()) + + def lerp(self, t): + """ + 3d interpolation + """ + return self.p + self.v * t + + def intersect(self, line): + """ + 2d intersection on plane xy + return + True if intersect + p: point of intersection + t: param t of intersection on current line + """ + c = line.cross_z + d = self.v * c + if d == 0: + return False, 0, 0 + t = (c * (line.p - self.p)) / d + return True, self.lerp(t), t + + def point_sur_segment(self, pt): + """ _point_sur_segment + point: Vector 2d + t: param t de l'intersection sur le segment courant + d: distance laterale perpendiculaire positif a droite + """ + dp = pt - self.p + dl = self.length + d = (self.v.x * dp.y - self.v.y * dp.x) / dl + t = (self.v * dp) / (dl * dl) + return t > 0 and t < 1, d, t + + def steps(self, len): + steps = max(1, round(self.length / len, 0)) + return 1 / steps, int(steps) + + def in_place_offset(self, offset): + """ + Offset current line + offset > 0 on the right part + """ + self.p += offset * self.cross_z.normalized() + + def offset(self, offset): + """ + Return a new line + offset > 0 on the right part + """ + return Line(self.p + offset * self.cross_z.normalized(), self.v) + + def tangeant(self, t, da, radius): + p = self.lerp(t) + if da < 0: + c = p + radius * self.cross_z.normalized() + else: + c = p - radius * self.cross_z.normalized() + return Arc(c, radius, self.angle_normal, da) + + def straight(self, length, t=1): + return Line(self.lerp(t), self.v.normalized() * length) + + def translate(self, dp): + self.p += dp + + def rotate(self, a): + """ + Rotate segment ccw arroud p0 + """ + ca = cos(a) + sa = sin(a) + self.v = Matrix([ + [ca, -sa], + [sa, ca] + ]) * self.v + return self + + def scale(self, length): + self.v = length * self.v.normalized() + return self + + def tangeant_unit_vector(self, t): + return self.v.normalized() + + def as_curve(self, context): + """ + Draw Line with open gl in screen space + aka: coords are in pixels + """ + raise NotImplementedError + + def make_offset(self, offset, last=None): + """ + Return offset between last and self. + Adjust last and self start to match + intersection point + """ + line = self.offset(offset) + if last is None: + return line + + if hasattr(last, "r"): + res, d, t = line.point_sur_segment(last.c) + c = (last.r * last.r) - (d * d) + print("t:%s" % t) + if c <= 0: + # no intersection ! + p0 = line.lerp(t) + else: + # center is past start of line + if t > 0: + p0 = line.lerp(t) - line.v.normalized() * sqrt(c) + else: + p0 = line.lerp(t) + line.v.normalized() * sqrt(c) + # compute da of arc + u = last.p0 - last.c + v = p0 - last.c + da = self.signed_angle(u, v) + # da is ccw + if last.ccw: + # da is cw + if da < 0: + # so take inverse + da = 2 * pi + da + elif da > 0: + # da is ccw + da = 2 * pi - da + last.da = da + line.p0 = p0 + else: + # intersect line / line + # 1 line -> 2 line + c = line.cross_z + d = last.v * c + if d == 0: + return line + v = line.p - last.p + t = (c * v) / d + c2 = last.cross_z + u = (c2 * v) / d + # intersect past this segment end + # or before last segment start + # print("u:%s t:%s" % (u, t)) + if u > 1 or t < 0: + return line + p = last.lerp(t) + line.p0 = p + last.p1 = p + + return line + + @property + def pts(self): + return [self.p0.to_3d(), self.p1.to_3d()] + + +class Circle(Projection): + def __init__(self, c, radius): + Projection.__init__(self) + self.r = radius + self.r2 = radius * radius + self.c = c + + def intersect(self, line): + v = line.p - self.c + A = line.v * line.v + B = 2 * v * line.v + C = v * v - self.r2 + d = B * B - 4 * A * C + if A <= 0.0000001 or d < 0: + # dosent intersect, find closest point of line + res, d, t = line.point_sur_segment(self.c) + return False, line.lerp(t), t + elif d == 0: + t = -B / 2 * A + return True, line.lerp(t), t + else: + AA = 2 * A + dsq = sqrt(d) + t0 = (-B + dsq) / AA + t1 = (-B - dsq) / AA + if abs(t0) < abs(t1): + return True, line.lerp(t0), t0 + else: + return True, line.lerp(t1), t1 + + def translate(self, dp): + self.c += dp + + +class Arc(Circle): + """ + Represent a 2d Arc + TODO: + Add some sugar here + like being able to set p0 and p1 of line + make it possible to define an arc by start point end point and center + """ + def __init__(self, c, radius, a0, da): + """ + a0 and da arguments are in radians + c Vector 2d center + radius float radius + a0 radians start angle + da radians delta angle from start to end + a0 = 0 on the right side + a0 = pi on the left side + da > 0 CCW contrary-clockwise + da < 0 CW clockwise + stored internally as radians + """ + Circle.__init__(self, Vector(c).to_2d(), radius) + self.a0 = a0 + self.da = da + + @property + def angle(self): + """ + angle of vector p0 p1 + """ + v = self.p1 - self.p0 + return atan2(v.y, v.x) + + @property + def ccw(self): + return self.da > 0 + + def signed_angle(self, u, v): + """ + signed angle between two vectors + """ + return atan2(u.x * v.y - u.y * v.x, u.x * v.x + u.y * v.y) + + def delta_angle(self, last): + """ + signed delta angle between end of line and start of this one + this value is object's a0 for segment = self + """ + if last is None: + return self.a0 + return self.signed_angle(last.straight(1, 1).v, self.straight(1, 0).v) + + def scale_rot_matrix(self, u, v): + """ + given vector u and v (from and to p0 p1) + apply scale factor to radius and + return a matrix to rotate and scale + the center around u origin so + arc fit v + """ + # signed angle old new vectors (rotation) + a = self.signed_angle(u, v) + # scale factor + scale = v.length / u.length + ca = scale * cos(a) + sa = scale * sin(a) + return scale, Matrix([ + [ca, -sa], + [sa, ca] + ]) + + @property + def p0(self): + """ + start point of arc + """ + return self.lerp(0) + + @property + def p1(self): + """ + end point of arc + """ + return self.lerp(1) + + @p0.setter + def p0(self, p0): + """ + rotate and scale arc so it intersect p0 p1 + da is not affected + """ + u = self.p0 - self.p1 + v = p0 - self.p1 + scale, rM = self.scale_rot_matrix(u, v) + self.c = self.p1 + rM * (self.c - self.p1) + self.r *= scale + self.r2 = self.r * self.r + dp = p0 - self.c + self.a0 = atan2(dp.y, dp.x) + + @p1.setter + def p1(self, p1): + """ + rotate and scale arc so it intersect p0 p1 + da is not affected + """ + p0 = self.p0 + u = self.p1 - p0 + v = p1 - p0 + + scale, rM = self.scale_rot_matrix(u, v) + self.c = p0 + rM * (self.c - p0) + self.r *= scale + self.r2 = self.r * self.r + dp = p0 - self.c + self.a0 = atan2(dp.y, dp.x) + + @property + def length(self): + """ + arc length + """ + return self.r * abs(self.da) + + def normal(self, t=0): + """ + Perpendicular line starting at t + always on the right side + """ + p = self.lerp(t) + if self.da < 0: + return Line(p, self.c - p) + else: + return Line(p, p - self.c) + + def sized_normal(self, t, size): + """ + Perpendicular line starting at t and of a length size + on the right side when size > 0 + """ + p = self.lerp(t) + if self.da < 0: + v = self.c - p + else: + v = p - self.c + return Line(p, size * v.normalized()) + + def lerp(self, t): + """ + Interpolate along segment + t parameter [0, 1] where 0 is start of arc and 1 is end + """ + a = self.a0 + t * self.da + return self.c + Vector((self.r * cos(a), self.r * sin(a))) + + def steps(self, length): + """ + Compute step count given desired step length + """ + steps = max(1, round(self.length / length, 0)) + return 1.0 / steps, int(steps) + + # this is for wall + def steps_by_angle(self, step_angle): + steps = max(1, round(abs(self.da) / step_angle, 0)) + return 1.0 / steps, int(steps) + + def offset(self, offset): + """ + Offset circle + offset > 0 on the right part + """ + if self.da > 0: + radius = self.r + offset + else: + radius = self.r - offset + return Arc(self.c, radius, self.a0, self.da) + + def tangeant(self, t, length): + """ + Tangeant line so we are able to chain Circle and lines + Beware, counterpart on Line does return an Arc ! + """ + a = self.a0 + t * self.da + ca = cos(a) + sa = sin(a) + p = self.c + Vector((self.r * ca, self.r * sa)) + v = Vector((length * sa, -length * ca)) + if self.da > 0: + v = -v + return Line(p, v) + + def tangeant_unit_vector(self, t): + """ + Return Tangeant vector of length 1 + """ + a = self.a0 + t * self.da + ca = cos(a) + sa = sin(a) + v = Vector((sa, -ca)) + if self.da > 0: + v = -v + return v + + def straight(self, length, t=1): + """ + Return a tangeant Line + Counterpart on Line also return a Line + """ + return self.tangeant(t, length) + + def point_sur_segment(self, pt): + """ + Point pt lie on arc ? + return + True when pt lie on segment + t [0, 1] where it lie (normalized between start and end) + d distance from arc + """ + dp = pt - self.c + d = dp.length - self.r + a = atan2(dp.y, dp.x) + t = (a - self.a0) / self.da + return t > 0 and t < 1, d, t + + def rotate(self, a): + """ + Rotate center so we rotate ccw arround p0 + """ + ca = cos(a) + sa = sin(a) + rM = Matrix([ + [ca, -sa], + [sa, ca] + ]) + p0 = self.p0 + self.c = p0 + rM * (self.c - p0) + dp = p0 - self.c + self.a0 = atan2(dp.y, dp.x) + return self + + # make offset for line / arc, arc / arc + def make_offset(self, offset, last=None): + + line = self.offset(offset) + + if last is None: + return line + + if hasattr(last, "v"): + # intersect line / arc + # 1 line -> 2 arc + res, d, t = last.point_sur_segment(line.c) + c = line.r2 - (d * d) + if c <= 0: + # no intersection ! + p0 = last.lerp(t) + else: + + # center is past end of line + if t > 1: + # Arc take precedence + p0 = last.lerp(t) - last.v.normalized() * sqrt(c) + else: + # line take precedence + p0 = last.lerp(t) + last.v.normalized() * sqrt(c) + + # compute a0 and da of arc + u = p0 - line.c + v = line.p1 - line.c + line.a0 = atan2(u.y, u.x) + da = self.signed_angle(u, v) + # da is ccw + if self.ccw: + # da is cw + if da < 0: + # so take inverse + da = 2 * pi + da + elif da > 0: + # da is ccw + da = 2 * pi - da + line.da = da + last.p1 = p0 + else: + # intersect arc / arc x1 = self x0 = last + # rule to determine right side -> + # same side of d as p0 of self + dc = line.c - last.c + tmp = Line(last.c, dc) + res, d, t = tmp.point_sur_segment(self.p0) + r = line.r + last.r + dist = dc.length + if dist > r or \ + dist < abs(last.r - self.r): + # no intersection + return line + if dist == r: + # 1 solution + p0 = dc * -last.r / r + self.c + else: + # 2 solutions + a = (last.r2 - line.r2 + dist * dist) / (2.0 * dist) + v2 = last.c + dc * a / dist + h = sqrt(last.r2 - a * a) + r = Vector((-dc.y, dc.x)) * (h / dist) + p0 = v2 + r + res, d1, t = tmp.point_sur_segment(p0) + # take other point if we are not on the same side + if d1 > 0: + if d < 0: + p0 = v2 - r + elif d > 0: + p0 = v2 - r + + # compute da of last + u = last.p0 - last.c + v = p0 - last.c + last.da = self.signed_angle(u, v) + + # compute a0 and da of current + u, v = v, line.p1 - line.c + line.a0 = atan2(u.y, u.x) + line.da = self.signed_angle(u, v) + return line + + # DEBUG + @property + def pts(self): + n_pts = max(1, int(round(abs(self.da) / pi * 30, 0))) + t_step = 1 / n_pts + return [self.lerp(i * t_step).to_3d() for i in range(n_pts + 1)] + + def as_curve(self, context): + """ + Draw 2d arc with open gl in screen space + aka: coords are in pixels + """ + curve = bpy.data.curves.new('ARC', type='CURVE') + curve.dimensions = '2D' + spline = curve.splines.new('POLY') + spline.use_endpoint_u = False + spline.use_cyclic_u = False + pts = self.pts + spline.points.add(len(pts) - 1) + for i, p in enumerate(pts): + x, y = p + spline.points[i].co = (x, y, 0, 1) + curve_obj = bpy.data.objects.new('ARC', curve) + context.scene.objects.link(curve_obj) + curve_obj.select = True + + +class Line3d(Line): + """ + 3d Line + mostly a gl enabled for future use in manipulators + coords are in world space + """ + def __init__(self, p=None, v=None, p0=None, p1=None, z_axis=None): + """ + Init by either + p: Vector or tuple origin + v: Vector or tuple size and direction + or + p0: Vector or tuple 1 point location + p1: Vector or tuple 2 point location + Will convert any into Vector 3d + both optionnals + """ + if p is not None and v is not None: + self.p = Vector(p).to_3d() + self.v = Vector(v).to_3d() + elif p0 is not None and p1 is not None: + self.p = Vector(p0).to_3d() + self.v = Vector(p1).to_3d() - self.p + else: + self.p = Vector((0, 0, 0)) + self.v = Vector((0, 0, 0)) + if z_axis is not None: + self.z_axis = z_axis + else: + self.z_axis = Vector((0, 0, 1)) + + @property + def p0(self): + return self.p + + @property + def p1(self): + return self.p + self.v + + @p0.setter + def p0(self, p0): + """ + Note: setting p0 + move p0 only + """ + p1 = self.p1 + self.p = Vector(p0).to_3d() + self.v = p1 - p0 + + @p1.setter + def p1(self, p1): + """ + Note: setting p1 + move p1 only + """ + self.v = Vector(p1).to_3d() - self.p + + @property + def cross_z(self): + """ + 3d Vector perpendicular on plane xy + lie on the right side + p1 + |--x + p0 + """ + return self.v.cross(Vector((0, 0, 1))) + + @property + def cross(self): + """ + 3d Vector perpendicular on plane defined by z_axis + lie on the right side + p1 + |--x + p0 + """ + return self.v.cross(self.z_axis) + + def normal(self, t=0): + """ + 3d Vector perpendicular on plane defined by z_axis + lie on the right side + p1 + |--x + p0 + """ + n = Line3d() + n.p = self.lerp(t) + n.v = self.cross + return n + + def sized_normal(self, t, size): + """ + 3d Line perpendicular on plane defined by z_axis and of given size + positionned at t in current line + lie on the right side + p1 + |--x + p0 + """ + p = self.lerp(t) + v = size * self.cross.normalized() + return Line3d(p, v, z_axis=self.z_axis) + + def offset(self, offset): + """ + offset > 0 on the right part + """ + return Line3d(self.p + offset * self.cross.normalized(), self.v) + + # unless override, 2d methods should raise NotImplementedError + def intersect(self, line): + raise NotImplementedError + + def point_sur_segment(self, pt): + raise NotImplementedError + + def tangeant(self, t, da, radius): + raise NotImplementedError diff --git a/archipack/archipack_autoboolean.py b/archipack/archipack_autoboolean.py new file mode 100644 index 00000000..a171532c --- /dev/null +++ b/archipack/archipack_autoboolean.py @@ -0,0 +1,678 @@ +# -*- coding:utf-8 -*- + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +# + +# ---------------------------------------------------------- +# Author: Stephen Leger (s-leger) +# +# ---------------------------------------------------------- +import bpy +from bpy.types import Operator +from bpy.props import EnumProperty +from mathutils import Vector +from .materialutils import MaterialUtils + +from os import path + + +def debug_using_gl(context, filename): + context.scene.update() + temp_path = "C:\\tmp\\" + context.scene.render.filepath = path.join(temp_path, filename + ".png") + bpy.ops.render.opengl(write_still=True) + + +class ArchipackBoolManager(): + """ + Handle three methods for booleans + - interactive: one modifier for each hole right on wall + - robust: one single modifier on wall and merge holes in one mesh + - mixed: merge holes with boolean and use result on wall + may be slow, but is robust + """ + def __init__(self, mode, solver_mode='CARVE'): + """ + mode in 'ROBUST', 'INTERACTIVE', 'HYBRID' + """ + self.mode = mode + self.solver_mode = solver_mode + # internal variables + self.itM = None + self.min_x = 0 + self.min_y = 0 + self.min_z = 0 + self.max_x = 0 + self.max_y = 0 + self.max_z = 0 + + def _get_bounding_box(self, wall): + self.itM = wall.matrix_world.inverted() + x, y, z = wall.bound_box[0] + self.min_x = x + self.min_y = y + self.min_z = z + x, y, z = wall.bound_box[6] + self.max_x = x + self.max_y = y + self.max_z = z + self.center = Vector(( + self.min_x + 0.5 * (self.max_x - self.min_x), + self.min_y + 0.5 * (self.max_y - self.min_y), + self.min_z + 0.5 * (self.max_z - self.min_z))) + + def _contains(self, pt): + p = self.itM * pt + return (p.x >= self.min_x and p.x <= self.max_x and + p.y >= self.min_y and p.y <= self.max_y and + p.z >= self.min_z and p.z <= self.max_z) + + def filter_wall(self, wall): + d = wall.data + return (d is None or + 'archipack_window' in d or + 'archipack_window_panel' in d or + 'archipack_door' in d or + 'archipack_doorpanel' in d or + 'archipack_hole' in wall or + 'archipack_robusthole' in wall or + 'archipack_handle' in wall) + + def datablock(self, o): + """ + get datablock from windows and doors + return + datablock if found + None when not found + """ + d = None + if o.data: + if "archipack_window" in o.data: + d = o.data.archipack_window[0] + elif "archipack_door" in o.data: + d = o.data.archipack_door[0] + return d + + def prepare_hole(self, hole): + hole.lock_location = (True, True, True) + hole.lock_rotation = (True, True, True) + hole.lock_scale = (True, True, True) + hole.draw_type = 'WIRE' + hole.hide_render = True + hole.hide_select = True + hole.select = True + hole.cycles_visibility.camera = False + hole.cycles_visibility.diffuse = False + hole.cycles_visibility.glossy = False + hole.cycles_visibility.shadow = False + hole.cycles_visibility.scatter = False + hole.cycles_visibility.transmission = False + + def get_child_hole(self, o): + for hole in o.children: + if "archipack_hole" in hole: + return hole + return None + + def _generate_hole(self, context, o): + # use existing one + if self.mode != 'ROBUST': + hole = self.get_child_hole(o) + if hole is not None: + # print("_generate_hole Use existing hole %s" % (hole.name)) + return hole + # generate single hole from archipack primitives + d = self.datablock(o) + hole = None + if d is not None: + if (self.itM is not None and ( + self._contains(o.location) or + self._contains(o.matrix_world * Vector((0, 0, 0.5 * d.z)))) + ): + if self.mode != 'ROBUST': + hole = d.interactive_hole(context, o) + else: + hole = d.robust_hole(context, o.matrix_world) + # print("_generate_hole Generate hole %s" % (hole.name)) + else: + hole = d.interactive_hole(context, o) + return hole + + def partition(self, array, begin, end): + pivot = begin + for i in range(begin + 1, end + 1): + if array[i][1] <= array[begin][1]: + pivot += 1 + array[i], array[pivot] = array[pivot], array[i] + array[pivot], array[begin] = array[begin], array[pivot] + return pivot + + def quicksort(self, array, begin=0, end=None): + if end is None: + end = len(array) - 1 + + def _quicksort(array, begin, end): + if begin >= end: + return + pivot = self.partition(array, begin, end) + _quicksort(array, begin, pivot - 1) + _quicksort(array, pivot + 1, end) + return _quicksort(array, begin, end) + + def sort_holes(self, wall, holes): + """ + sort hole from center to borders by distance from center + may improve nested booleans + """ + center = wall.matrix_world * self.center + holes = [(o, (o.matrix_world.translation - center).length) for o in holes] + self.quicksort(holes) + return [o[0] for o in holes] + + def difference(self, basis, hole, solver=None): + # print("difference %s" % (hole.name)) + m = basis.modifiers.new('AutoBoolean', 'BOOLEAN') + m.operation = 'DIFFERENCE' + if solver is None: + m.solver = self.solver_mode + else: + m.solver = solver + m.object = hole + + def union(self, basis, hole): + # print("union %s" % (hole.name)) + m = basis.modifiers.new('AutoMerge', 'BOOLEAN') + m.operation = 'UNION' + m.solver = self.solver_mode + m.object = hole + + def remove_modif_and_object(self, context, o, to_delete): + # print("remove_modif_and_object removed:%s" % (len(to_delete))) + for m, h in to_delete: + if m is not None: + if m.object is not None: + m.object = None + o.modifiers.remove(m) + if h is not None: + context.scene.objects.unlink(h) + bpy.data.objects.remove(h, do_unlink=True) + + # Mixed + def create_merge_basis(self, context, wall): + # print("create_merge_basis") + h = bpy.data.meshes.new("AutoBoolean") + hole_obj = bpy.data.objects.new("AutoBoolean", h) + context.scene.objects.link(hole_obj) + hole_obj['archipack_hybridhole'] = True + if wall.parent is not None: + hole_obj.parent = wall.parent + hole_obj.matrix_world = wall.matrix_world.copy() + MaterialUtils.add_wall2_materials(hole_obj) + return hole_obj + + def update_hybrid(self, context, wall, childs, holes): + """ + Update all holes modifiers + remove holes not found in childs + + robust -> mixed: + there is only one object taged with "archipack_robusthole" + interactive -> mixed: + many modifisers on wall taged with "archipack_hole" + keep objects + """ + existing = [] + to_delete = [] + + # robust/interactive -> mixed + for m in wall.modifiers: + if m.type == 'BOOLEAN': + if m.object is None: + to_delete.append([m, None]) + elif 'archipack_hole' in m.object: + h = m.object + if h in holes: + to_delete.append([m, None]) + else: + to_delete.append([m, h]) + elif 'archipack_robusthole' in m.object: + to_delete.append([m, m.object]) + + # remove modifier and holes not found in new list + self.remove_modif_and_object(context, wall, to_delete) + + m = wall.modifiers.get("AutoMixedBoolean") + if m is None: + m = wall.modifiers.new('AutoMixedBoolean', 'BOOLEAN') + m.solver = self.solver_mode + m.operation = 'DIFFERENCE' + + if m.object is None: + hole_obj = self.create_merge_basis(context, wall) + else: + hole_obj = m.object + # debug_using_gl(context, "260") + m.object = hole_obj + self.prepare_hole(hole_obj) + # debug_using_gl(context, "263") + to_delete = [] + + # mixed-> mixed + for m in hole_obj.modifiers: + h = m.object + if h in holes: + existing.append(h) + else: + to_delete.append([m, h]) + + # remove modifier and holes not found in new list + self.remove_modif_and_object(context, hole_obj, to_delete) + # debug_using_gl(context, "276") + # add modifier and holes not found in existing + for h in holes: + if h not in existing: + self.union(hole_obj, h) + # debug_using_gl(context, "281") + + # Interactive + def update_interactive(self, context, wall, childs, holes): + + existing = [] + + to_delete = [] + + hole_obj = None + + # mixed-> interactive + for m in wall.modifiers: + if m.type == 'BOOLEAN': + if m.object is not None and 'archipack_hybridhole' in m.object: + hole_obj = m.object + break + + if hole_obj is not None: + for m in hole_obj.modifiers: + h = m.object + if h not in holes: + to_delete.append([m, h]) + # remove modifier and holes not found in new list + self.remove_modif_and_object(context, hole_obj, to_delete) + context.scene.objects.unlink(hole_obj) + bpy.data.objects.remove(hole_obj, do_unlink=True) + + to_delete = [] + + # interactive/robust -> interactive + for m in wall.modifiers: + if m.type == 'BOOLEAN': + if m.object is None: + to_delete.append([m, None]) + elif 'archipack_hole' in m.object: + h = m.object + if h in holes: + existing.append(h) + else: + to_delete.append([m, h]) + elif 'archipack_robusthole' in m.object: + to_delete.append([m, m.object]) + + # remove modifier and holes not found in new list + self.remove_modif_and_object(context, wall, to_delete) + + # add modifier and holes not found in existing + for h in holes: + if h not in existing: + self.difference(wall, h) + + # Robust + def update_robust(self, context, wall, childs): + + modif = None + + to_delete = [] + + # robust/interactive/mixed -> robust + for m in wall.modifiers: + if m.type == 'BOOLEAN': + if m.object is None: + to_delete.append([m, None]) + elif 'archipack_robusthole' in m.object: + modif = m + to_delete.append([None, m.object]) + elif 'archipack_hole' in m.object: + to_delete.append([m, m.object]) + elif 'archipack_hybridhole' in m.object: + to_delete.append([m, m.object]) + o = m.object + for m in o.modifiers: + to_delete.append([None, m.object]) + + # remove modifier and holes + self.remove_modif_and_object(context, wall, to_delete) + + if bool(len(context.selected_objects) > 0): + # more than one hole : join, result becomes context.object + if len(context.selected_objects) > 1: + bpy.ops.object.join() + context.object['archipack_robusthole'] = True + + hole = context.object + hole.name = 'AutoBoolean' + + childs.append(hole) + + if modif is None: + self.difference(wall, hole) + else: + modif.object = hole + elif modif is not None: + wall.modifiers.remove(modif) + + def autoboolean(self, context, wall): + """ + Entry point for multi-boolean operations like + in T panel autoBoolean and RobustBoolean buttons + """ + bpy.ops.object.select_all(action='DESELECT') + context.scene.objects.active = None + childs = [] + holes = [] + # get wall bounds to find what's inside + self._get_bounding_box(wall) + + # either generate hole or get existing one + for o in context.scene.objects: + h = self._generate_hole(context, o) + if h is not None: + holes.append(h) + childs.append(o) + # debug_using_gl(context, "395") + self.sort_holes(wall, holes) + + # hole(s) are selected and active after this one + for hole in holes: + self.prepare_hole(hole) + # debug_using_gl(context, "401") + + # update / remove / add boolean modifier + if self.mode == 'INTERACTIVE': + self.update_interactive(context, wall, childs, holes) + elif self.mode == 'ROBUST': + self.update_robust(context, wall, childs) + else: + self.update_hybrid(context, wall, childs, holes) + + bpy.ops.object.select_all(action='DESELECT') + # parenting childs to wall reference point + if wall.parent is None: + x, y, z = wall.bound_box[0] + context.scene.cursor_location = wall.matrix_world * Vector((x, y, z)) + # fix issue #9 + context.scene.objects.active = wall + bpy.ops.archipack.reference_point() + else: + wall.parent.select = True + context.scene.objects.active = wall.parent + # debug_using_gl(context, "422") + wall.select = True + for o in childs: + if 'archipack_robusthole' in o: + o.hide_select = False + o.select = True + # debug_using_gl(context, "428") + + bpy.ops.archipack.parent_to_reference() + + for o in childs: + if 'archipack_robusthole' in o: + o.hide_select = True + # debug_using_gl(context, "435") + + def detect_mode(self, context, wall): + for m in wall.modifiers: + if m.type == 'BOOLEAN' and m.object is not None: + if 'archipack_hole' in m.object: + self.mode = 'INTERACTIVE' + if 'archipack_hybridhole' in m.object: + self.mode = 'HYBRID' + if 'archipack_robusthole' in m.object: + self.mode = 'ROBUST' + + def singleboolean(self, context, wall, o): + """ + Entry point for single boolean operations + in use in draw door and windows over wall + o is either a window or a door + """ + # generate holes for crossing window and doors + self.itM = wall.matrix_world.inverted() + d = self.datablock(o) + hole = None + hole_obj = None + # default mode defined by __init__ + self.detect_mode(context, wall) + + if d is not None: + if self.mode != 'ROBUST': + hole = d.interactive_hole(context, o) + else: + hole = d.robust_hole(context, o.matrix_world) + if hole is None: + return + + self.prepare_hole(hole) + + if self.mode == 'INTERACTIVE': + # update / remove / add boolean modifier + self.difference(wall, hole) + + elif self.mode == 'HYBRID': + m = wall.modifiers.get('AutoMixedBoolean') + + if m is None: + m = wall.modifiers.new('AutoMixedBoolean', 'BOOLEAN') + m.operation = 'DIFFERENCE' + m.solver = self.solver_mode + + if m.object is None: + hole_obj = self.create_merge_basis(context, wall) + m.object = hole_obj + else: + hole_obj = m.object + self.union(hole_obj, hole) + + bpy.ops.object.select_all(action='DESELECT') + + # parenting childs to wall reference point + if wall.parent is None: + x, y, z = wall.bound_box[0] + context.scene.cursor_location = wall.matrix_world * Vector((x, y, z)) + # fix issue #9 + context.scene.objects.active = wall + bpy.ops.archipack.reference_point() + else: + context.scene.objects.active = wall.parent + + if hole_obj is not None: + hole_obj.select = True + + wall.select = True + o.select = True + bpy.ops.archipack.parent_to_reference() + wall.select = True + context.scene.objects.active = wall + d = wall.data.archipack_wall2[0] + g = d.get_generator() + d.setup_childs(wall, g) + d.relocate_childs(context, wall, g) + + if hole_obj is not None: + self.prepare_hole(hole_obj) + + +class ARCHIPACK_OT_single_boolean(Operator): + bl_idname = "archipack.single_boolean" + bl_label = "SingleBoolean" + bl_description = "Add single boolean for doors and windows" + bl_category = 'Archipack' + bl_options = {'REGISTER', 'UNDO'} + mode = EnumProperty( + name="Mode", + items=( + ('INTERACTIVE', 'INTERACTIVE', 'Interactive, fast but may fail', 0), + ('ROBUST', 'ROBUST', 'Not interactive, robust', 1), + ('HYBRID', 'HYBRID', 'Interactive, slow but robust', 2) + ), + default='HYBRID' + ) + solver_mode = EnumProperty( + name="Solver", + items=( + ('CARVE', 'CARVE', 'Slow but robust (could be slow in hybrid mode with many holes)', 0), + ('BMESH', 'BMESH', 'Fast but more prone to errors', 1) + ), + default='BMESH' + ) + """ + Wall must be active object + window or door must be selected + """ + + @classmethod + def poll(cls, context): + w = context.active_object + return (w.data is not None and + "archipack_wall2" in w.data and + len(context.selected_objects) == 2 + ) + + def draw(self, context): + pass + + def execute(self, context): + if context.mode == "OBJECT": + wall = context.active_object + manager = ArchipackBoolManager(mode=self.mode, solver_mode=self.solver_mode) + for o in context.selected_objects: + if o != wall: + manager.singleboolean(context, wall, o) + break + o.select = False + wall.select = True + context.scene.objects.active = wall + return {'FINISHED'} + else: + self.report({'WARNING'}, "Archipack: Option only valid in Object mode") + return {'CANCELLED'} + + +class ARCHIPACK_OT_auto_boolean(Operator): + bl_idname = "archipack.auto_boolean" + bl_label = "AutoBoolean" + bl_description = "Automatic boolean for doors and windows" + bl_category = 'Archipack' + bl_options = {'REGISTER', 'UNDO'} + mode = EnumProperty( + name="Mode", + items=( + ('INTERACTIVE', 'INTERACTIVE', 'Interactive, fast but may fail', 0), + ('ROBUST', 'ROBUST', 'Not interactive, robust', 1), + ('HYBRID', 'HYBRID', 'Interactive, slow but robust', 2) + ), + default='HYBRID' + ) + solver_mode = EnumProperty( + name="Solver", + items=( + ('CARVE', 'CARVE', 'Slow but robust (could be slow in hybrid mode with many holes)', 0), + ('BMESH', 'BMESH', 'Fast but more prone to errors', 1) + ), + default='BMESH' + ) + + def draw(self, context): + layout = self.layout + row = layout.row() + row.prop(self, 'mode') + row.prop(self, 'solver_mode') + + def execute(self, context): + if context.mode == "OBJECT": + manager = ArchipackBoolManager(mode=self.mode, solver_mode=self.solver_mode) + active = context.scene.objects.active + walls = [wall for wall in context.selected_objects if not manager.filter_wall(wall)] + bpy.ops.object.select_all(action='DESELECT') + for wall in walls: + manager.autoboolean(context, wall) + bpy.ops.object.select_all(action='DESELECT') + wall.select = True + context.scene.objects.active = wall + if wall.data is not None and 'archipack_wall2' in wall.data: + bpy.ops.archipack.wall2_manipulate('EXEC_DEFAULT') + # reselect walls + bpy.ops.object.select_all(action='DESELECT') + for wall in walls: + wall.select = True + context.scene.objects.active = active + return {'FINISHED'} + else: + self.report({'WARNING'}, "Archipack: Option only valid in Object mode") + return {'CANCELLED'} + + +class ARCHIPACK_OT_generate_hole(Operator): + bl_idname = "archipack.generate_hole" + bl_label = "Generate hole" + bl_description = "Generate interactive hole for doors and windows" + bl_category = 'Archipack' + bl_options = {'REGISTER', 'UNDO'} + + def execute(self, context): + if context.mode == "OBJECT": + manager = ArchipackBoolManager(mode='HYBRID') + o = context.active_object + d = manager.datablock(o) + if d is None: + self.report({'WARNING'}, "Archipack: active object must be a door or a window") + return {'CANCELLED'} + bpy.ops.object.select_all(action='DESELECT') + o.select = True + context.scene.objects.active = o + hole = manager._generate_hole(context, o) + manager.prepare_hole(hole) + hole.select = False + o.select = True + context.scene.objects.active = o + return {'FINISHED'} + else: + self.report({'WARNING'}, "Archipack: Option only valid in Object mode") + return {'CANCELLED'} + + +def register(): + bpy.utils.register_class(ARCHIPACK_OT_generate_hole) + bpy.utils.register_class(ARCHIPACK_OT_single_boolean) + bpy.utils.register_class(ARCHIPACK_OT_auto_boolean) + + +def unregister(): + bpy.utils.unregister_class(ARCHIPACK_OT_generate_hole) + bpy.utils.unregister_class(ARCHIPACK_OT_single_boolean) + bpy.utils.unregister_class(ARCHIPACK_OT_auto_boolean) diff --git a/archipack/archipack_door.py b/archipack/archipack_door.py new file mode 100644 index 00000000..f29c44d1 --- /dev/null +++ b/archipack/archipack_door.py @@ -0,0 +1,1847 @@ +# -*- coding:utf-8 -*- + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +# + +# ---------------------------------------------------------- +# Author: Stephen Leger (s-leger) +# +# ---------------------------------------------------------- + +# noinspection PyUnresolvedReferences +import bpy +# noinspection PyUnresolvedReferences +from bpy.types import Operator, PropertyGroup, Mesh, Panel +from bpy.props import ( + FloatProperty, IntProperty, CollectionProperty, + EnumProperty, BoolProperty, StringProperty + ) +from mathutils import Vector +# door component objects (panels, handles ..) +from .bmesh_utils import BmeshEdit as bmed +from .panel import Panel as DoorPanel +from .materialutils import MaterialUtils +from .archipack_handle import create_handle, door_handle_horizontal_01 +from .archipack_manipulator import Manipulable +from .archipack_preset import ArchipackPreset, PresetMenuOperator +from .archipack_object import ArchipackObject, ArchipackCreateTool, ArchpackDrawTool +from .archipack_gl import FeedbackPanel +from .archipack_keymaps import Keymaps + + +SPACING = 0.005 +BATTUE = 0.01 +BOTTOM_HOLE_MARGIN = 0.001 +FRONT_HOLE_MARGIN = 0.1 + + +def update(self, context): + self.update(context) + + +def update_childs(self, context): + self.update(context, childs_only=True) + + +class archipack_door_panel(ArchipackObject, PropertyGroup): + x = FloatProperty( + name='width', + min=0.25, + default=100.0, precision=2, + unit='LENGTH', subtype='DISTANCE', + description='Width' + ) + y = FloatProperty( + name='Depth', + min=0.001, + default=0.02, precision=2, + unit='LENGTH', subtype='DISTANCE', + description='depth' + ) + z = FloatProperty( + name='height', + min=0.1, + default=2.0, precision=2, + unit='LENGTH', subtype='DISTANCE', + description='height' + ) + direction = IntProperty( + name="Direction", + min=0, + max=1, + description="open direction" + ) + model = IntProperty( + name="model", + min=0, + max=3, + default=0, + description="Model" + ) + chanfer = FloatProperty( + name='chanfer', + min=0.001, + default=0.005, precision=3, + unit='LENGTH', subtype='DISTANCE', + description='chanfer' + ) + panel_spacing = FloatProperty( + name='spacing', + min=0.001, + default=0.1, precision=2, + unit='LENGTH', subtype='DISTANCE', + description='distance between panels' + ) + panel_bottom = FloatProperty( + name='bottom', + min=0.0, + default=0.0, precision=2, + unit='LENGTH', subtype='DISTANCE', + description='distance from bottom' + ) + panel_border = FloatProperty( + name='border', + min=0.001, + default=0.2, precision=2, + unit='LENGTH', subtype='DISTANCE', + description='distance from border' + ) + panels_x = IntProperty( + name="panels h", + min=1, + max=50, + default=1, + description="panels h" + ) + panels_y = IntProperty( + name="panels v", + min=1, + max=50, + default=1, + description="panels v" + ) + panels_distrib = EnumProperty( + name='distribution', + items=( + ('REGULAR', 'Regular', '', 0), + ('ONE_THIRD', '1/3 2/3', '', 1) + ), + default='REGULAR' + ) + handle = EnumProperty( + name='Shape', + items=( + ('NONE', 'No handle', '', 0), + ('BOTH', 'Inside and outside', '', 1) + ), + default='BOTH' + ) + + @property + def panels(self): + + # subdivide side to weld panels + subdiv_x = self.panels_x - 1 + + if self.panels_distrib == 'REGULAR': + subdiv_y = self.panels_y - 1 + else: + subdiv_y = 2 + + # __ y0 + # |__ y1 + # x0 x1 + y0 = -self.y + y1 = 0 + x0 = 0 + x1 = max(0.001, self.panel_border - 0.5 * self.panel_spacing) + + side = DoorPanel( + False, # profil closed + [1, 0, 0, 1], # x index + [x0, x1], + [y0, y0, y1, y1], + [0, 1, 1, 1], # material index + closed_path=True, # + subdiv_x=subdiv_x, + subdiv_y=subdiv_y + ) + + face = None + back = None + + if self.model == 1: + # / y2-y3 + # __/ y1-y0 + # x2 x3 + x2 = 0.5 * self.panel_spacing + x3 = x2 + self.chanfer + y2 = y1 + self.chanfer + y3 = y0 - self.chanfer + + face = DoorPanel( + False, # profil closed + [0, 1, 2], # x index + [0, x2, x3], + [y1, y1, y2], + [1, 1, 1], # material index + side_cap_front=2, # cap index + closed_path=True + ) + + back = DoorPanel( + False, # profil closed + [0, 1, 2], # x index + [x3, x2, 0], + [y3, y0, y0], + [0, 0, 0], # material index + side_cap_back=0, # cap index + closed_path=True + ) + + elif self.model == 2: + # / y2-y3 + # ___ _____/ y1-y0 + # \ / + # \/ y4-y5 + # 0 x2 x4 x5 x6 x3 + x2 = 0.5 * self.panel_spacing + x4 = x2 + self.chanfer + x5 = x4 + self.chanfer + x6 = x5 + 4 * self.chanfer + x3 = x6 + self.chanfer + y2 = y1 - self.chanfer + y4 = y1 + self.chanfer + y3 = y0 + self.chanfer + y5 = y0 - self.chanfer + face = DoorPanel( + False, # profil closed + [0, 1, 2, 3, 4, 5], # x index + [0, x2, x4, x5, x6, x3], + [y1, y1, y4, y1, y1, y2], + [1, 1, 1, 1, 1, 1], # material index + side_cap_front=5, # cap index + closed_path=True + ) + + back = DoorPanel( + False, # profil closed + [0, 1, 2, 3, 4, 5], # x index + [x3, x6, x5, x4, x2, 0], + [y3, y0, y0, y5, y0, y0], + [0, 0, 0, 0, 0, 0], # material index + side_cap_back=0, # cap index + closed_path=True + ) + + elif self.model == 3: + # _____ y2-y3 + # / \ y4-y5 + # __/ y1-y0 + # 0 x2 x3 x4 x5 + x2 = 0.5 * self.panel_spacing + x3 = x2 + self.chanfer + x4 = x3 + 4 * self.chanfer + x5 = x4 + 2 * self.chanfer + y2 = y1 - self.chanfer + y3 = y0 + self.chanfer + y4 = y2 + self.chanfer + y5 = y3 - self.chanfer + face = DoorPanel( + False, # profil closed + [0, 1, 2, 3, 4], # x index + [0, x2, x3, x4, x5], + [y1, y1, y2, y2, y4], + [1, 1, 1, 1, 1], # material index + side_cap_front=4, # cap index + closed_path=True + ) + + back = DoorPanel( + False, # profil closed + [0, 1, 2, 3, 4], # x index + [x5, x4, x3, x2, 0], + [y5, y3, y3, y0, y0], + [0, 0, 0, 0, 0], # material index + side_cap_back=0, # cap index + closed_path=True + ) + + else: + side.side_cap_front = 3 + side.side_cap_back = 0 + + return side, face, back + + @property + def verts(self): + if self.panels_distrib == 'REGULAR': + subdiv_y = self.panels_y - 1 + else: + subdiv_y = 2 + + radius = Vector((0.8, 0.5, 0)) + center = Vector((0, self.z - radius.x, 0)) + + if self.direction == 0: + pivot = 1 + else: + pivot = -1 + + path_type = 'RECTANGLE' + curve_steps = 16 + side, face, back = self.panels + + x1 = max(0.001, self.panel_border - 0.5 * self.panel_spacing) + bottom_z = self.panel_bottom + shape_z = [0, bottom_z, bottom_z, 0] + origin = Vector((-pivot * 0.5 * self.x, 0, 0)) + offset = Vector((0, 0, 0)) + size = Vector((self.x, self.z, 0)) + verts = side.vertices(curve_steps, offset, center, origin, + size, radius, 0, pivot, shape_z=shape_z, path_type=path_type) + if face is not None: + p_radius = radius.copy() + p_radius.x -= x1 + p_radius.y -= x1 + if self.panels_distrib == 'REGULAR': + p_size = Vector(((self.x - 2 * x1) / self.panels_x, + (self.z - 2 * x1 - bottom_z) / self.panels_y, 0)) + for i in range(self.panels_x): + for j in range(self.panels_y): + if j < subdiv_y: + shape = 'RECTANGLE' + else: + shape = path_type + offset = Vector(((pivot * 0.5 * self.x) + p_size.x * (i + 0.5) - 0.5 * size.x + x1, + bottom_z + p_size.y * j + x1, 0)) + origin = Vector((p_size.x * (i + 0.5) - 0.5 * size.x + x1, bottom_z + p_size.y * j + x1, 0)) + verts += face.vertices(curve_steps, offset, center, origin, + p_size, p_radius, 0, 0, shape_z=None, path_type=shape) + if back is not None: + verts += back.vertices(curve_steps, offset, center, origin, + p_size, p_radius, 0, 0, shape_z=None, path_type=shape) + else: + #################################### + # Ratio vertical panels 1/3 - 2/3 + #################################### + p_size = Vector(((self.x - 2 * x1) / self.panels_x, (self.z - 2 * x1 - bottom_z) / 3, 0)) + p_size_2x = Vector((p_size.x, p_size.y * 2, 0)) + for i in range(self.panels_x): + j = 0 + offset = Vector(((pivot * 0.5 * self.x) + p_size.x * (i + 0.5) - 0.5 * size.x + x1, + bottom_z + p_size.y * j + x1, 0)) + origin = Vector((p_size.x * (i + 0.5) - 0.5 * size.x + x1, bottom_z + p_size.y * j + x1, 0)) + shape = 'RECTANGLE' + face.subdiv_y = 0 + verts += face.vertices(curve_steps, offset, center, origin, + p_size, p_radius, 0, 0, shape_z=None, path_type=shape) + if back is not None: + back.subdiv_y = 0 + verts += back.vertices(curve_steps, offset, center, origin, + p_size, p_radius, 0, 0, shape_z=None, path_type=shape) + j = 1 + offset = Vector(((pivot * 0.5 * self.x) + p_size.x * (i + 0.5) - 0.5 * size.x + x1, + bottom_z + p_size.y * j + x1, 0)) + origin = Vector((p_size.x * (i + 0.5) - 0.5 * size.x + x1, + bottom_z + p_size.y * j + x1, 0)) + shape = path_type + face.subdiv_y = 1 + verts += face.vertices(curve_steps, offset, center, origin, + p_size_2x, p_radius, 0, 0, shape_z=None, path_type=path_type) + if back is not None: + back.subdiv_y = 1 + verts += back.vertices(curve_steps, offset, center, origin, + p_size_2x, p_radius, 0, 0, shape_z=None, path_type=path_type) + + return verts + + @property + def faces(self): + if self.panels_distrib == 'REGULAR': + subdiv_y = self.panels_y - 1 + else: + subdiv_y = 2 + + path_type = 'RECTANGLE' + curve_steps = 16 + side, face, back = self.panels + + faces = side.faces(curve_steps, path_type=path_type) + faces_offset = side.n_verts(curve_steps, path_type=path_type) + + if face is not None: + if self.panels_distrib == 'REGULAR': + for i in range(self.panels_x): + for j in range(self.panels_y): + if j < subdiv_y: + shape = 'RECTANGLE' + else: + shape = path_type + faces += face.faces(curve_steps, path_type=shape, offset=faces_offset) + faces_offset += face.n_verts(curve_steps, path_type=shape) + if back is not None: + faces += back.faces(curve_steps, path_type=shape, offset=faces_offset) + faces_offset += back.n_verts(curve_steps, path_type=shape) + else: + #################################### + # Ratio vertical panels 1/3 - 2/3 + #################################### + for i in range(self.panels_x): + j = 0 + shape = 'RECTANGLE' + face.subdiv_y = 0 + faces += face.faces(curve_steps, path_type=shape, offset=faces_offset) + faces_offset += face.n_verts(curve_steps, path_type=shape) + if back is not None: + back.subdiv_y = 0 + faces += back.faces(curve_steps, path_type=shape, offset=faces_offset) + faces_offset += back.n_verts(curve_steps, path_type=shape) + j = 1 + shape = path_type + face.subdiv_y = 1 + faces += face.faces(curve_steps, path_type=path_type, offset=faces_offset) + faces_offset += face.n_verts(curve_steps, path_type=path_type) + if back is not None: + back.subdiv_y = 1 + faces += back.faces(curve_steps, path_type=path_type, offset=faces_offset) + faces_offset += back.n_verts(curve_steps, path_type=path_type) + + return faces + + @property + def uvs(self): + if self.panels_distrib == 'REGULAR': + subdiv_y = self.panels_y - 1 + else: + subdiv_y = 2 + + radius = Vector((0.8, 0.5, 0)) + center = Vector((0, self.z - radius.x, 0)) + + if self.direction == 0: + pivot = 1 + else: + pivot = -1 + + path_type = 'RECTANGLE' + curve_steps = 16 + side, face, back = self.panels + + x1 = max(0.001, self.panel_border - 0.5 * self.panel_spacing) + bottom_z = self.panel_bottom + origin = Vector((-pivot * 0.5 * self.x, 0, 0)) + size = Vector((self.x, self.z, 0)) + uvs = side.uv(curve_steps, center, origin, size, radius, 0, pivot, 0, self.panel_border, path_type=path_type) + if face is not None: + p_radius = radius.copy() + p_radius.x -= x1 + p_radius.y -= x1 + if self.panels_distrib == 'REGULAR': + p_size = Vector(((self.x - 2 * x1) / self.panels_x, (self.z - 2 * x1 - bottom_z) / self.panels_y, 0)) + for i in range(self.panels_x): + for j in range(self.panels_y): + if j < subdiv_y: + shape = 'RECTANGLE' + else: + shape = path_type + origin = Vector((p_size.x * (i + 0.5) - 0.5 * size.x + x1, bottom_z + p_size.y * j + x1, 0)) + uvs += face.uv(curve_steps, center, origin, p_size, p_radius, 0, 0, 0, 0, path_type=shape) + if back is not None: + uvs += back.uv(curve_steps, center, origin, + p_size, p_radius, 0, 0, 0, 0, path_type=shape) + else: + #################################### + # Ratio vertical panels 1/3 - 2/3 + #################################### + p_size = Vector(((self.x - 2 * x1) / self.panels_x, (self.z - 2 * x1 - bottom_z) / 3, 0)) + p_size_2x = Vector((p_size.x, p_size.y * 2, 0)) + for i in range(self.panels_x): + j = 0 + origin = Vector((p_size.x * (i + 0.5) - 0.5 * size.x + x1, bottom_z + p_size.y * j + x1, 0)) + shape = 'RECTANGLE' + face.subdiv_y = 0 + uvs += face.uv(curve_steps, center, origin, p_size, p_radius, 0, 0, 0, 0, path_type=shape) + if back is not None: + back.subdiv_y = 0 + uvs += back.uv(curve_steps, center, origin, p_size, p_radius, 0, 0, 0, 0, path_type=shape) + j = 1 + origin = Vector((p_size.x * (i + 0.5) - 0.5 * size.x + x1, bottom_z + p_size.y * j + x1, 0)) + shape = path_type + face.subdiv_y = 1 + uvs += face.uv(curve_steps, center, origin, p_size_2x, p_radius, 0, 0, 0, 0, path_type=path_type) + if back is not None: + back.subdiv_y = 1 + uvs += back.uv(curve_steps, center, origin, + p_size_2x, p_radius, 0, 0, 0, 0, path_type=path_type) + return uvs + + @property + def matids(self): + if self.panels_distrib == 'REGULAR': + subdiv_y = self.panels_y - 1 + else: + subdiv_y = 2 + + path_type = 'RECTANGLE' + curve_steps = 16 + side, face, back = self.panels + + mat = side.mat(curve_steps, 1, 0, path_type=path_type) + + if face is not None: + if self.panels_distrib == 'REGULAR': + for i in range(self.panels_x): + for j in range(self.panels_y): + if j < subdiv_y: + shape = 'RECTANGLE' + else: + shape = path_type + mat += face.mat(curve_steps, 1, 1, path_type=shape) + if back is not None: + mat += back.mat(curve_steps, 0, 0, path_type=shape) + else: + #################################### + # Ratio vertical panels 1/3 - 2/3 + #################################### + for i in range(self.panels_x): + j = 0 + shape = 'RECTANGLE' + face.subdiv_y = 0 + mat += face.mat(curve_steps, 1, 1, path_type=shape) + if back is not None: + back.subdiv_y = 0 + mat += back.mat(curve_steps, 0, 0, path_type=shape) + j = 1 + shape = path_type + face.subdiv_y = 1 + mat += face.mat(curve_steps, 1, 1, path_type=shape) + if back is not None: + back.subdiv_y = 1 + mat += back.mat(curve_steps, 0, 0, path_type=shape) + return mat + + def find_handle(self, o): + for child in o.children: + if 'archipack_handle' in child: + return child + return None + + def update_handle(self, context, o): + handle = self.find_handle(o) + if handle is None: + m = bpy.data.meshes.new("Handle") + handle = create_handle(context, o, m) + MaterialUtils.add_handle_materials(handle) + verts, faces = door_handle_horizontal_01(self.direction, 1) + b_verts, b_faces = door_handle_horizontal_01(self.direction, 0, offset=len(verts)) + b_verts = [(v[0], v[1] - self.y, v[2]) for v in b_verts] + handle_y = 0.07 + handle.location = ((1 - self.direction * 2) * (self.x - handle_y), 0, 0.5 * self.z) + bmed.buildmesh(context, handle, verts + b_verts, faces + b_faces) + + def remove_handle(self, context, o): + handle = self.find_handle(o) + if handle is not None: + context.scene.objects.unlink(handle) + bpy.data.objects.remove(handle, do_unlink=True) + + def update(self, context): + o = self.find_in_selection(context) + + if o is None: + return + + bmed.buildmesh(context, o, self.verts, self.faces, matids=self.matids, uvs=self.uvs, weld=True) + + if self.handle == 'NONE': + self.remove_handle(context, o) + else: + self.update_handle(context, o) + + self.restore_context(context) + + +class ARCHIPACK_PT_door_panel(Panel): + bl_idname = "ARCHIPACK_PT_door_panel" + bl_label = "Door" + bl_space_type = 'VIEW_3D' + bl_region_type = 'UI' + # bl_context = 'object' + bl_category = 'ArchiPack' + + @classmethod + def poll(cls, context): + return archipack_door_panel.filter(context.active_object) + + def draw(self, context): + layout = self.layout + layout.operator("archipack.select_parent") + + +# ------------------------------------------------------------------ +# Define operator class to create object +# ------------------------------------------------------------------ + + +class ARCHIPACK_OT_door_panel(Operator): + bl_idname = "archipack.door_panel" + bl_label = "Door model 1" + bl_description = "Door model 1" + bl_category = 'Archipack' + bl_options = {'REGISTER', 'UNDO'} + x = FloatProperty( + name='width', + min=0.1, + default=0.80, precision=2, + unit='LENGTH', subtype='DISTANCE', + description='Width' + ) + z = FloatProperty( + name='height', + min=0.1, + default=2.0, precision=2, + unit='LENGTH', subtype='DISTANCE', + description='height' + ) + y = FloatProperty( + name='depth', + min=0.001, + default=0.02, precision=2, + unit='LENGTH', subtype='DISTANCE', + description='Depth' + ) + direction = IntProperty( + name="direction", + min=0, + max=1, + description="open direction" + ) + model = IntProperty( + name="model", + min=0, + max=3, + description="panel type" + ) + chanfer = FloatProperty( + name='chanfer', + min=0.001, + default=0.005, precision=3, + unit='LENGTH', subtype='DISTANCE', + description='chanfer' + ) + panel_spacing = FloatProperty( + name='spacing', + min=0.001, + default=0.1, precision=2, + unit='LENGTH', subtype='DISTANCE', + description='distance between panels' + ) + panel_bottom = FloatProperty( + name='bottom', + min=0.0, + default=0.0, precision=2, + unit='LENGTH', subtype='DISTANCE', + description='distance from bottom' + ) + panel_border = FloatProperty( + name='border', + min=0.001, + default=0.2, precision=2, + unit='LENGTH', subtype='DISTANCE', + description='distance from border' + ) + panels_x = IntProperty( + name="panels h", + min=1, + max=50, + default=1, + description="panels h" + ) + panels_y = IntProperty( + name="panels v", + min=1, + max=50, + default=1, + description="panels v" + ) + panels_distrib = EnumProperty( + name='distribution', + items=( + ('REGULAR', 'Regular', '', 0), + ('ONE_THIRD', '1/3 2/3', '', 1) + ), + default='REGULAR' + ) + handle = EnumProperty( + name='Shape', + items=( + ('NONE', 'No handle', '', 0), + ('BOTH', 'Inside and outside', '', 1) + ), + default='BOTH' + ) + + def draw(self, context): + layout = self.layout + row = layout.row() + row.label("Use Properties panel (N) to define parms", icon='INFO') + + def create(self, context): + """ + expose only basic params in operator + use object property for other params + """ + m = bpy.data.meshes.new("Panel") + o = bpy.data.objects.new("Panel", m) + d = m.archipack_door_panel.add() + d.x = self.x + d.y = self.y + d.z = self.z + d.model = self.model + d.direction = self.direction + d.chanfer = self.chanfer + d.panel_border = self.panel_border + d.panel_bottom = self.panel_bottom + d.panel_spacing = self.panel_spacing + d.panels_distrib = self.panels_distrib + d.panels_x = self.panels_x + d.panels_y = self.panels_y + d.handle = self.handle + context.scene.objects.link(o) + o.lock_location[0] = True + o.lock_location[1] = True + o.lock_location[2] = True + o.lock_rotation[0] = True + o.lock_rotation[1] = True + o.lock_scale[0] = True + o.lock_scale[1] = True + o.lock_scale[2] = True + o.select = True + context.scene.objects.active = o + d.update(context) + MaterialUtils.add_door_materials(o) + o.lock_rotation[0] = True + o.lock_rotation[1] = True + return o + + def execute(self, context): + if context.mode == "OBJECT": + bpy.ops.object.select_all(action="DESELECT") + o = self.create(context) + o.select = True + context.scene.objects.active = o + return {'FINISHED'} + else: + self.report({'WARNING'}, "Archipack: Option only valid in Object mode") + return {'CANCELLED'} + + +class ARCHIPACK_OT_select_parent(Operator): + bl_idname = "archipack.select_parent" + bl_label = "Edit parameters" + bl_description = "Edit parameters located on parent" + bl_category = 'Archipack' + bl_options = {'REGISTER', 'UNDO'} + + def draw(self, context): + layout = self.layout + row = layout.row() + row.label("Use Properties panel (N) to define parms", icon='INFO') + + def execute(self, context): + if context.mode == "OBJECT": + if context.active_object is not None and context.active_object.parent is not None: + bpy.ops.object.select_all(action="DESELECT") + context.active_object.parent.select = True + context.scene.objects.active = context.active_object.parent + return {'FINISHED'} + else: + self.report({'WARNING'}, "Archipack: Option only valid in Object mode") + return {'CANCELLED'} + + +class archipack_door(ArchipackObject, Manipulable, PropertyGroup): + """ + The frame is the door main object + parent parametric object + create/remove/update her own childs + """ + x = FloatProperty( + name='width', + min=0.25, + default=100.0, precision=2, step=1, + unit='LENGTH', subtype='DISTANCE', + description='Width', update=update, + ) + y = FloatProperty( + name='depth', + min=0.1, + default=0.20, precision=2, step=1, + unit='LENGTH', subtype='DISTANCE', + description='Depth', update=update, + ) + z = FloatProperty( + name='height', + min=0.1, + default=2.0, precision=2, step=1, + unit='LENGTH', subtype='DISTANCE', + description='height', update=update, + ) + frame_x = FloatProperty( + name='Width', + min=0, + default=0.1, precision=2, step=1, + unit='LENGTH', subtype='DISTANCE', + description='frame width', update=update, + ) + frame_y = FloatProperty( + name='Depth', + default=0.03, precision=2, step=1, + unit='LENGTH', subtype='DISTANCE', + description='frame depth', update=update, + ) + direction = IntProperty( + name="Direction", + min=0, + max=1, + description="open direction", update=update, + ) + door_y = FloatProperty( + name='Depth', + min=0.001, + default=0.02, precision=2, step=1, + unit='LENGTH', subtype='DISTANCE', + description='depth', update=update, + ) + door_offset = FloatProperty( + name='Offset', + min=0, + default=0, precision=2, step=1, + unit='LENGTH', subtype='DISTANCE', + description='offset', update=update, + ) + model = IntProperty( + name="Model", + min=0, + max=3, + default=0, + description="Model", update=update, + ) + n_panels = IntProperty( + name="Panels", + min=1, + max=2, + default=1, + description="number of panels", update=update + ) + chanfer = FloatProperty( + name='chanfer', + min=0.001, + default=0.005, precision=3, step=0.01, + unit='LENGTH', subtype='DISTANCE', + description='chanfer', update=update_childs, + ) + panel_spacing = FloatProperty( + name='spacing', + min=0.001, + default=0.1, precision=2, step=1, + unit='LENGTH', subtype='DISTANCE', + description='distance between panels', update=update_childs, + ) + panel_bottom = FloatProperty( + name='bottom', + min=0.0, + default=0.0, precision=2, step=1, + unit='LENGTH', subtype='DISTANCE', + description='distance from bottom', update=update_childs, + ) + panel_border = FloatProperty( + name='border', + min=0.001, + default=0.2, precision=2, step=1, + unit='LENGTH', subtype='DISTANCE', + description='distance from border', update=update_childs, + ) + panels_x = IntProperty( + name="panels h", + min=1, + max=50, + default=1, + description="panels h", update=update_childs, + ) + panels_y = IntProperty( + name="panels v", + min=1, + max=50, + default=1, + description="panels v", update=update_childs, + ) + panels_distrib = EnumProperty( + name='distribution', + items=( + ('REGULAR', 'Regular', '', 0), + ('ONE_THIRD', '1/3 2/3', '', 1) + ), + default='REGULAR', update=update_childs, + ) + handle = EnumProperty( + name='Handle', + items=( + ('NONE', 'No handle', '', 0), + ('BOTH', 'Inside and outside', '', 1) + ), + default='BOTH', update=update_childs, + ) + hole_margin = FloatProperty( + name='hole margin', + min=0.0, + default=0.1, precision=2, step=1, + unit='LENGTH', subtype='DISTANCE', + description='how much hole surround wall' + ) + flip = BoolProperty( + default=False, + update=update, + description='flip outside and outside material of hole' + ) + auto_update = BoolProperty( + options={'SKIP_SAVE'}, + default=True, + update=update + ) + + @property + def frame(self): + + # + # _____ y0 + # | |___ y1 + # x | y3 + # | | + # |_________| y2 + # + # x2 x1 x0 + x0 = 0 + x1 = -BATTUE + x2 = -self.frame_x + y0 = max(0.25 * self.door_y + 0.0005, self.y / 2 + self.frame_y) + y1 = max(y0 - 0.5 * self.door_y - self.door_offset, -y0 + 0.001) + y2 = -y0 + y3 = 0 + return DoorPanel( + True, # closed + [0, 0, 0, 1, 1, 2, 2], # x index + [x2, x1, x0], + [y2, y3, y0, y0, y1, y1, y2], + [0, 1, 1, 1, 1, 0, 0], # material index + closed_path=False + ) + + @property + def hole(self): + # + # _____ y0 + # | + # x y2 + # | + # |_____ y1 + # + # x0 + x0 = 0 + y0 = self.y / 2 + self.hole_margin + y1 = -y0 + y2 = 0 + outside_mat = 0 + inside_mat = 1 + if self.flip: + outside_mat, inside_mat = inside_mat, outside_mat + return DoorPanel( + False, # closed + [0, 0, 0], # x index + [x0], + [y1, y2, y0], + [outside_mat, inside_mat, inside_mat], # material index + closed_path=True, + side_cap_front=2, + side_cap_back=0 # cap index + ) + + @property + def verts(self): + # door inner space + v = Vector((0, 0, 0)) + size = Vector((self.x, self.z, self.y)) + return self.frame.vertices(16, v, v, v, size, v, 0, 0, shape_z=None, path_type='RECTANGLE') + + @property + def faces(self): + return self.frame.faces(16, path_type='RECTANGLE') + + @property + def matids(self): + return self.frame.mat(16, 0, 0, path_type='RECTANGLE') + + @property + def uvs(self): + v = Vector((0, 0, 0)) + size = Vector((self.x, self.z, self.y)) + return self.frame.uv(16, v, v, size, v, 0, 0, 0, 0, path_type='RECTANGLE') + + def setup_manipulators(self): + if len(self.manipulators) == 3: + return + s = self.manipulators.add() + s.prop1_name = "x" + s.prop2_name = "x" + s.type_key = "SNAP_SIZE_LOC" + s = self.manipulators.add() + s.prop1_name = "y" + s.prop2_name = "y" + s.type_key = "SNAP_SIZE_LOC" + s = self.manipulators.add() + s.prop1_name = "z" + s.normal = Vector((0, 1, 0)) + + def remove_childs(self, context, o, to_remove): + for child in o.children: + if to_remove < 1: + return + if archipack_door_panel.filter(child): + self.remove_handle(context, child) + to_remove -= 1 + context.scene.objects.unlink(child) + bpy.data.objects.remove(child, do_unlink=True) + + def remove_handle(self, context, o): + handle = self.find_handle(o) + if handle is not None: + context.scene.objects.unlink(handle) + bpy.data.objects.remove(handle, do_unlink=True) + + def create_childs(self, context, o): + + n_childs = 0 + for child in o.children: + if archipack_door_panel.filter(child): + n_childs += 1 + + # remove child + if n_childs > self.n_panels: + self.remove_childs(context, o, n_childs - self.n_panels) + + if n_childs < 1: + # create one door panel + bpy.ops.archipack.door_panel(x=self.x, z=self.z, door_y=self.door_y, + n_panels=self.n_panels, direction=self.direction) + child = context.active_object + child.parent = o + child.matrix_world = o.matrix_world.copy() + location = self.x / 2 + BATTUE - SPACING + if self.direction == 0: + location = -location + child.location.x = location + child.location.y = self.door_y + + if self.n_panels == 2 and n_childs < 2: + # create 2nth door panel + bpy.ops.archipack.door_panel(x=self.x, z=self.z, door_y=self.door_y, + n_panels=self.n_panels, direction=1 - self.direction) + child = context.active_object + child.parent = o + child.matrix_world = o.matrix_world.copy() + location = self.x / 2 + BATTUE - SPACING + if self.direction == 1: + location = -location + child.location.x = location + child.location.y = self.door_y + + def find_handle(self, o): + for handle in o.children: + if 'archipack_handle' in handle: + return handle + return None + + def get_childs_panels(self, context, o): + return [child for child in o.children if archipack_door_panel.filter(child)] + + def _synch_childs(self, context, o, linked, childs): + """ + sub synch childs nodes of linked object + """ + # remove childs not found on source + l_childs = self.get_childs_panels(context, linked) + c_names = [c.data.name for c in childs] + for c in l_childs: + try: + id = c_names.index(c.data.name) + except: + self.remove_handle(context, c) + context.scene.objects.unlink(c) + bpy.data.objects.remove(c, do_unlink=True) + + # children ordering may not be the same, so get the right l_childs order + l_childs = self.get_childs_panels(context, linked) + l_names = [c.data.name for c in l_childs] + order = [] + for c in childs: + try: + id = l_names.index(c.data.name) + except: + id = -1 + order.append(id) + + # add missing childs and update other ones + for i, child in enumerate(childs): + if order[i] < 0: + p = bpy.data.objects.new("DoorPanel", child.data) + context.scene.objects.link(p) + p.lock_location[0] = True + p.lock_location[1] = True + p.lock_location[2] = True + p.lock_rotation[0] = True + p.lock_rotation[1] = True + p.lock_scale[0] = True + p.lock_scale[1] = True + p.lock_scale[2] = True + p.parent = linked + p.matrix_world = linked.matrix_world.copy() + p.location = child.location.copy() + else: + p = l_childs[order[i]] + + p.location = child.location.copy() + + # update handle + handle = self.find_handle(child) + h = self.find_handle(p) + if handle is not None: + if h is None: + h = create_handle(context, p, handle.data) + MaterialUtils.add_handle_materials(h) + h.location = handle.location.copy() + elif h is not None: + context.scene.objects.unlink(h) + bpy.data.objects.remove(h, do_unlink=True) + + def _synch_hole(self, context, linked, hole): + l_hole = self.find_hole(linked) + if l_hole is None: + l_hole = bpy.data.objects.new("hole", hole.data) + l_hole['archipack_hole'] = True + context.scene.objects.link(l_hole) + l_hole.parent = linked + l_hole.matrix_world = linked.matrix_world.copy() + l_hole.location = hole.location.copy() + else: + l_hole.data = hole.data + + def synch_childs(self, context, o): + """ + synch childs nodes of linked objects + """ + bpy.ops.object.select_all(action='DESELECT') + o.select = True + context.scene.objects.active = o + childs = self.get_childs_panels(context, o) + hole = self.find_hole(o) + bpy.ops.object.select_linked(type='OBDATA') + for linked in context.selected_objects: + if linked != o: + self._synch_childs(context, o, linked, childs) + if hole is not None: + self._synch_hole(context, linked, hole) + + def update_childs(self, context, o): + """ + pass params to childrens + """ + childs = self.get_childs_panels(context, o) + n_childs = len(childs) + self.remove_childs(context, o, n_childs - self.n_panels) + + childs = self.get_childs_panels(context, o) + n_childs = len(childs) + child_n = 0 + + # location_y = self.y / 2 + self.frame_y - SPACING + # location_y = min(max(self.door_offset, - location_y), location_y) + self.door_y + + location_y = max(0.25 * self.door_y + 0.0005, self.y / 2 + self.frame_y) + location_y = max(location_y - self.door_offset + 0.5 * self.door_y, -location_y + self.door_y + 0.001) + + x = self.x / self.n_panels + (3 - self.n_panels) * (BATTUE - SPACING) + y = self.door_y + z = self.z + BATTUE - SPACING + + if self.n_panels < 2: + direction = self.direction + else: + direction = 0 + + for panel in range(self.n_panels): + child_n += 1 + + if child_n == 1: + handle = self.handle + else: + handle = 'NONE' + + if child_n > 1: + direction = 1 - direction + + location_x = (2 * direction - 1) * (self.x / 2 + BATTUE - SPACING) + + if child_n > n_childs: + bpy.ops.archipack.door_panel( + x=x, + y=y, + z=z, + model=self.model, + direction=direction, + chanfer=self.chanfer, + panel_border=self.panel_border, + panel_bottom=self.panel_bottom, + panel_spacing=self.panel_spacing, + panels_distrib=self.panels_distrib, + panels_x=self.panels_x, + panels_y=self.panels_y, + handle=handle + ) + child = context.active_object + # parenting at 0, 0, 0 before set object matrix_world + # so location remains local from frame + child.parent = o + child.matrix_world = o.matrix_world.copy() + else: + child = childs[child_n - 1] + child.select = True + context.scene.objects.active = child + props = archipack_door_panel.datablock(child) + if props is not None: + props.x = x + props.y = y + props.z = z + props.model = self.model + props.direction = direction + props.chanfer = self.chanfer + props.panel_border = self.panel_border + props.panel_bottom = self.panel_bottom + props.panel_spacing = self.panel_spacing + props.panels_distrib = self.panels_distrib + props.panels_x = self.panels_x + props.panels_y = self.panels_y + props.handle = handle + props.update(context) + child.location = Vector((location_x, location_y, 0)) + + def update(self, context, childs_only=False): + + # support for "copy to selected" + o = self.find_in_selection(context, self.auto_update) + + if o is None: + return + + self.setup_manipulators() + + if childs_only is False: + bmed.buildmesh(context, o, self.verts, self.faces, self.matids, self.uvs) + + self.update_childs(context, o) + + if childs_only is False and self.find_hole(o) is not None: + self.interactive_hole(context, o) + + # support for instances childs, update at object level + self.synch_childs(context, o) + + # setup 3d points for gl manipulators + x, y = 0.5 * self.x, 0.5 * self.y + self.manipulators[0].set_pts([(-x, -y, 0), (x, -y, 0), (1, 0, 0)]) + self.manipulators[1].set_pts([(-x, -y, 0), (-x, y, 0), (-1, 0, 0)]) + self.manipulators[2].set_pts([(x, -y, 0), (x, -y, self.z), (-1, 0, 0)]) + + # restore context + self.restore_context(context) + + def find_hole(self, o): + for child in o.children: + if 'archipack_hole' in child: + return child + return None + + def interactive_hole(self, context, o): + hole_obj = self.find_hole(o) + if hole_obj is None: + m = bpy.data.meshes.new("hole") + hole_obj = bpy.data.objects.new("hole", m) + context.scene.objects.link(hole_obj) + hole_obj['archipack_hole'] = True + hole_obj.parent = o + hole_obj.matrix_world = o.matrix_world.copy() + MaterialUtils.add_wall2_materials(hole_obj) + hole = self.hole + v = Vector((0, 0, 0)) + offset = Vector((0, -0.001, 0)) + size = Vector((self.x + 2 * self.frame_x, self.z + self.frame_x + 0.001, self.y)) + verts = hole.vertices(16, offset, v, v, size, v, 0, 0, shape_z=None, path_type='RECTANGLE') + faces = hole.faces(16, path_type='RECTANGLE') + matids = hole.mat(16, 0, 1, path_type='RECTANGLE') + uvs = hole.uv(16, v, v, size, v, 0, 0, 0, 0, path_type='RECTANGLE') + bmed.buildmesh(context, hole_obj, verts, faces, matids=matids, uvs=uvs) + return hole_obj + + def robust_hole(self, context, tM): + hole = self.hole + m = bpy.data.meshes.new("hole") + o = bpy.data.objects.new("hole", m) + o['archipack_robusthole'] = True + context.scene.objects.link(o) + v = Vector((0, 0, 0)) + offset = Vector((0, -0.001, 0)) + size = Vector((self.x + 2 * self.frame_x, self.z + self.frame_x + 0.001, self.y)) + verts = hole.vertices(16, offset, v, v, size, v, 0, 0, shape_z=None, path_type='RECTANGLE') + verts = [tM * Vector(v) for v in verts] + faces = hole.faces(16, path_type='RECTANGLE') + matids = hole.mat(16, 0, 1, path_type='RECTANGLE') + uvs = hole.uv(16, v, v, size, v, 0, 0, 0, 0, path_type='RECTANGLE') + bmed.buildmesh(context, o, verts, faces, matids=matids, uvs=uvs) + MaterialUtils.add_wall2_materials(o) + o.select = True + context.scene.objects.active = o + return o + + +class ARCHIPACK_PT_door(Panel): + bl_idname = "ARCHIPACK_PT_door" + bl_label = "Door" + bl_space_type = 'VIEW_3D' + bl_region_type = 'UI' + bl_category = 'ArchiPack' + + @classmethod + def poll(cls, context): + return archipack_door.filter(context.active_object) + + def draw(self, context): + o = context.active_object + if not archipack_door.filter(o): + return + layout = self.layout + layout.operator('archipack.door_manipulate', icon='HAND') + props = archipack_door.datablock(o) + row = layout.row(align=True) + row.operator('archipack.door', text="Refresh", icon='FILE_REFRESH').mode = 'REFRESH' + if o.data.users > 1: + row.operator('archipack.door', text="Make unique", icon='UNLINKED').mode = 'UNIQUE' + row.operator('archipack.door', text="Delete", icon='ERROR').mode = 'DELETE' + box = layout.box() + # box.label(text="Styles") + row = box.row(align=True) + row.operator("archipack.door_preset_menu", text=bpy.types.ARCHIPACK_OT_door_preset_menu.bl_label) + row.operator("archipack.door_preset", text="", icon='ZOOMIN') + row.operator("archipack.door_preset", text="", icon='ZOOMOUT').remove_active = True + row = layout.row() + box = row.box() + box.label(text="Size") + box.prop(props, 'x') + box.prop(props, 'y') + box.prop(props, 'z') + box.prop(props, 'door_offset') + row = layout.row() + box = row.box() + row = box.row() + row.label(text="Door") + box.prop(props, 'direction') + box.prop(props, 'n_panels') + box.prop(props, 'door_y') + box.prop(props, 'handle') + row = layout.row() + box = row.box() + row = box.row() + row.label(text="Frame") + row = box.row(align=True) + row.prop(props, 'frame_x') + row.prop(props, 'frame_y') + row = layout.row() + box = row.box() + row = box.row() + row.label(text="Panels") + box.prop(props, 'model') + if props.model > 0: + box.prop(props, 'panels_distrib', text="") + row = box.row(align=True) + row.prop(props, 'panels_x') + if props.panels_distrib == 'REGULAR': + row.prop(props, 'panels_y') + box.prop(props, 'panel_bottom') + box.prop(props, 'panel_spacing') + box.prop(props, 'panel_border') + box.prop(props, 'chanfer') + + +# ------------------------------------------------------------------ +# Define operator class to create object +# ------------------------------------------------------------------ + + +class ARCHIPACK_OT_door(ArchipackCreateTool, Operator): + bl_idname = "archipack.door" + bl_label = "Door" + bl_description = "Door" + bl_category = 'Archipack' + bl_options = {'REGISTER', 'UNDO'} + x = FloatProperty( + name='width', + min=0.1, + default=0.80, precision=2, + unit='LENGTH', subtype='DISTANCE', + description='Width' + ) + y = FloatProperty( + name='depth', + min=0.1, + default=0.20, precision=2, + unit='LENGTH', subtype='DISTANCE', + description='Depth' + ) + z = FloatProperty( + name='height', + min=0.1, + default=2.0, precision=2, + unit='LENGTH', subtype='DISTANCE', + description='height' + ) + direction = IntProperty( + name="direction", + min=0, + max=1, + description="open direction" + ) + n_panels = IntProperty( + name="panels", + min=1, + max=2, + default=1, + description="number of panels" + ) + chanfer = FloatProperty( + name='chanfer', + min=0.001, + default=0.005, precision=3, + unit='LENGTH', subtype='DISTANCE', + description='chanfer' + ) + panel_spacing = FloatProperty( + name='spacing', + min=0.001, + default=0.1, precision=2, + unit='LENGTH', subtype='DISTANCE', + description='distance between panels' + ) + panel_bottom = FloatProperty( + name='bottom', + default=0.0, precision=2, + unit='LENGTH', subtype='DISTANCE', + description='distance from bottom' + ) + panel_border = FloatProperty( + name='border', + min=0.001, + default=0.2, precision=2, + unit='LENGTH', subtype='DISTANCE', + description='distance from border' + ) + panels_x = IntProperty( + name="panels h", + min=1, + max=50, + default=1, + description="panels h" + ) + panels_y = IntProperty( + name="panels v", + min=1, + max=50, + default=1, + description="panels v" + ) + panels_distrib = EnumProperty( + name='distribution', + items=( + ('REGULAR', 'Regular', '', 0), + ('ONE_THIRD', '1/3 2/3', '', 1) + ), + default='REGULAR' + ) + handle = EnumProperty( + name='Shape', + items=( + ('NONE', 'No handle', '', 0), + ('BOTH', 'Inside and outside', '', 1) + ), + default='BOTH' + ) + mode = EnumProperty( + items=( + ('CREATE', 'Create', '', 0), + ('DELETE', 'Delete', '', 1), + ('REFRESH', 'Refresh', '', 2), + ('UNIQUE', 'Make unique', '', 3), + ), + default='CREATE' + ) + + def create(self, context): + """ + expose only basic params in operator + use object property for other params + """ + m = bpy.data.meshes.new("Door") + o = bpy.data.objects.new("Door", m) + d = m.archipack_door.add() + d.x = self.x + d.y = self.y + d.z = self.z + d.direction = self.direction + d.n_panels = self.n_panels + d.chanfer = self.chanfer + d.panel_border = self.panel_border + d.panel_bottom = self.panel_bottom + d.panel_spacing = self.panel_spacing + d.panels_distrib = self.panels_distrib + d.panels_x = self.panels_x + d.panels_y = self.panels_y + d.handle = self.handle + context.scene.objects.link(o) + o.select = True + context.scene.objects.active = o + self.load_preset(d) + self.add_material(o) + o.select = True + context.scene.objects.active = o + return o + + def delete(self, context): + o = context.active_object + if archipack_door.filter(o): + bpy.ops.archipack.disable_manipulate() + for child in o.children: + if 'archipack_hole' in child: + context.scene.objects.unlink(child) + bpy.data.objects.remove(child, do_unlink=True) + elif child.data is not None and 'archipack_door_panel' in child.data: + for handle in child.children: + if 'archipack_handle' in handle: + context.scene.objects.unlink(handle) + bpy.data.objects.remove(handle, do_unlink=True) + context.scene.objects.unlink(child) + bpy.data.objects.remove(child, do_unlink=True) + context.scene.objects.unlink(o) + bpy.data.objects.remove(o, do_unlink=True) + + def update(self, context): + o = context.active_object + d = archipack_door.datablock(o) + if d is not None: + d.update(context) + bpy.ops.object.select_linked(type='OBDATA') + for linked in context.selected_objects: + if linked != o: + archipack_door.datablock(linked).update(context) + bpy.ops.object.select_all(action="DESELECT") + o.select = True + context.scene.objects.active = o + + def unique(self, context): + act = context.active_object + sel = [o for o in context.selected_objects] + bpy.ops.object.select_all(action="DESELECT") + for o in sel: + if archipack_door.filter(o): + o.select = True + for child in o.children: + if 'archipack_hole' in child or (child.data is not None and + 'archipack_door_panel' in child.data): + child.hide_select = False + child.select = True + if len(context.selected_objects) > 0: + bpy.ops.object.make_single_user(type='SELECTED_OBJECTS', object=True, + obdata=True, material=False, texture=False, animation=False) + for child in context.selected_objects: + if 'archipack_hole' in child: + child.hide_select = True + bpy.ops.object.select_all(action="DESELECT") + context.scene.objects.active = act + for o in sel: + o.select = True + + def execute(self, context): + if context.mode == "OBJECT": + if self.mode == 'CREATE': + bpy.ops.object.select_all(action="DESELECT") + o = self.create(context) + o.location = bpy.context.scene.cursor_location + o.select = True + context.scene.objects.active = o + self.manipulate() + elif self.mode == 'DELETE': + self.delete(context) + elif self.mode == 'REFRESH': + self.update(context) + elif self.mode == 'UNIQUE': + self.unique(context) + return {'FINISHED'} + else: + self.report({'WARNING'}, "Archipack: Option only valid in Object mode") + return {'CANCELLED'} + + +class ARCHIPACK_OT_door_draw(ArchpackDrawTool, Operator): + bl_idname = "archipack.door_draw" + bl_label = "Draw Doors" + bl_description = "Draw Doors over walls" + bl_category = 'Archipack' + bl_options = {'REGISTER', 'UNDO'} + + filepath = StringProperty(default="") + feedback = None + stack = [] + + @classmethod + def poll(cls, context): + return True + + def draw(self, context): + layout = self.layout + row = layout.row() + row.label("Use Properties panel (N) to define parms", icon='INFO') + + def draw_callback(self, _self, context): + self.feedback.draw(context) + + def add_object(self, context, event): + o = context.active_object + bpy.ops.object.select_all(action="DESELECT") + + if archipack_door.filter(o): + + o.select = True + context.scene.objects.active = o + + if event.shift: + bpy.ops.archipack.door(mode="UNIQUE") + + new_w = o.copy() + new_w.data = o.data + context.scene.objects.link(new_w) + + o = new_w + o.select = True + context.scene.objects.active = o + + # synch subs from parent instance + bpy.ops.archipack.door(mode="REFRESH") + + else: + bpy.ops.archipack.door(auto_manipulate=False, filepath=self.filepath) + o = context.active_object + + bpy.ops.archipack.generate_hole('INVOKE_DEFAULT') + o.select = True + context.scene.objects.active = o + + def modal(self, context, event): + + context.area.tag_redraw() + o = context.active_object + d = archipack_door.datablock(o) + hole = None + + if d is not None: + hole = d.find_hole(o) + + # hide hole from raycast + if hole is not None: + o.hide = True + hole.hide = True + + res, tM, wall, y = self.mouse_hover_wall(context, event) + + if hole is not None: + o.hide = False + hole.hide = False + + if res and d is not None: + o.matrix_world = tM + if d.y != wall.data.archipack_wall2[0].width: + d.y = wall.data.archipack_wall2[0].width + + if event.value == 'PRESS': + if event.type in {'LEFTMOUSE', 'RET', 'NUMPAD_ENTER', 'SPACE'}: + if wall is not None: + context.scene.objects.active = wall + wall.select = True + if bpy.ops.archipack.single_boolean.poll(): + bpy.ops.archipack.single_boolean() + wall.select = False + # o must be a door here + if d is not None: + context.scene.objects.active = o + self.stack.append(o) + self.add_object(context, event) + context.active_object.matrix_world = tM + return {'RUNNING_MODAL'} + + # prevent selection of other object + if event.type in {'RIGHTMOUSE'}: + return {'RUNNING_MODAL'} + + if self.keymap.check(event, self.keymap.undo) or ( + event.type in {'BACK_SPACE'} and event.value == 'RELEASE' + ): + if len(self.stack) > 0: + last = self.stack.pop() + context.scene.objects.active = last + bpy.ops.archipack.door(mode="DELETE") + context.scene.objects.active = o + return {'RUNNING_MODAL'} + + if event.value == 'RELEASE': + + if event.type in {'ESC', 'RIGHTMOUSE'}: + bpy.ops.archipack.door(mode='DELETE') + self.feedback.disable() + bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW') + return {'FINISHED'} + + return {'PASS_THROUGH'} + + def invoke(self, context, event): + + if context.mode == "OBJECT": + o = None + self.stack = [] + self.keymap = Keymaps(context) + # exit manipulate_mode if any + bpy.ops.archipack.disable_manipulate() + # invoke with alt pressed will use current object as basis for linked copy + if self.filepath == '' and archipack_door.filter(context.active_object): + o = context.active_object + context.scene.objects.active = None + bpy.ops.object.select_all(action="DESELECT") + if o is not None: + o.select = True + context.scene.objects.active = o + self.add_object(context, event) + self.feedback = FeedbackPanel() + self.feedback.instructions(context, "Draw a door", "Click & Drag over a wall", [ + ('LEFTCLICK, RET, SPACE, ENTER', 'Create a door'), + ('BACKSPACE, CTRL+Z', 'undo last'), + ('SHIFT', 'Make independant copy'), + ('RIGHTCLICK or ESC', 'exit') + ]) + self.feedback.enable() + args = (self, context) + + self._handle = bpy.types.SpaceView3D.draw_handler_add(self.draw_callback, args, 'WINDOW', 'POST_PIXEL') + context.window_manager.modal_handler_add(self) + return {'RUNNING_MODAL'} + else: + self.report({'WARNING'}, "Archipack: Option only valid in Object mode") + return {'CANCELLED'} + + +# ------------------------------------------------------------------ +# Define operator class to manipulate object +# ------------------------------------------------------------------ + + +class ARCHIPACK_OT_door_manipulate(Operator): + bl_idname = "archipack.door_manipulate" + bl_label = "Manipulate" + bl_description = "Manipulate" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(self, context): + return archipack_door.filter(context.active_object) + + def invoke(self, context, event): + d = archipack_door.datablock(context.active_object) + d.manipulable_invoke(context) + return {'FINISHED'} + + +# ------------------------------------------------------------------ +# Define operator class to load / save presets +# ------------------------------------------------------------------ + + +class ARCHIPACK_OT_door_preset_menu(PresetMenuOperator, Operator): + bl_description = "Show Doors presets" + bl_idname = "archipack.door_preset_menu" + bl_label = "Door Presets" + preset_subdir = "archipack_door" + + +class ARCHIPACK_OT_door_preset(ArchipackPreset, Operator): + """Add a Door Preset""" + bl_idname = "archipack.door_preset" + bl_label = "Add Door Preset" + preset_menu = "ARCHIPACK_OT_door_preset_menu" + + @property + def blacklist(self): + # 'x', 'y', 'z', 'direction', + return ['manipulators'] + + +def register(): + bpy.utils.register_class(archipack_door_panel) + Mesh.archipack_door_panel = CollectionProperty(type=archipack_door_panel) + bpy.utils.register_class(ARCHIPACK_PT_door_panel) + bpy.utils.register_class(ARCHIPACK_OT_door_panel) + bpy.utils.register_class(ARCHIPACK_OT_select_parent) + bpy.utils.register_class(archipack_door) + Mesh.archipack_door = CollectionProperty(type=archipack_door) + bpy.utils.register_class(ARCHIPACK_OT_door_preset_menu) + bpy.utils.register_class(ARCHIPACK_PT_door) + bpy.utils.register_class(ARCHIPACK_OT_door) + bpy.utils.register_class(ARCHIPACK_OT_door_preset) + bpy.utils.register_class(ARCHIPACK_OT_door_draw) + bpy.utils.register_class(ARCHIPACK_OT_door_manipulate) + + +def unregister(): + bpy.utils.unregister_class(archipack_door_panel) + del Mesh.archipack_door_panel + bpy.utils.unregister_class(ARCHIPACK_PT_door_panel) + bpy.utils.unregister_class(ARCHIPACK_OT_door_panel) + bpy.utils.unregister_class(ARCHIPACK_OT_select_parent) + bpy.utils.unregister_class(archipack_door) + del Mesh.archipack_door + bpy.utils.unregister_class(ARCHIPACK_OT_door_preset_menu) + bpy.utils.unregister_class(ARCHIPACK_PT_door) + bpy.utils.unregister_class(ARCHIPACK_OT_door) + bpy.utils.unregister_class(ARCHIPACK_OT_door_preset) + bpy.utils.unregister_class(ARCHIPACK_OT_door_draw) + bpy.utils.unregister_class(ARCHIPACK_OT_door_manipulate) diff --git a/archipack/archipack_fence.py b/archipack/archipack_fence.py new file mode 100644 index 00000000..961b516e --- /dev/null +++ b/archipack/archipack_fence.py @@ -0,0 +1,1782 @@ +# -*- coding:utf-8 -*- + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110- 1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +# + +# ---------------------------------------------------------- +# Author: Stephen Leger (s-leger) +# +# ---------------------------------------------------------- +# noinspection PyUnresolvedReferences +import bpy +# noinspection PyUnresolvedReferences +from bpy.types import Operator, PropertyGroup, Mesh, Panel +from bpy.props import ( + FloatProperty, BoolProperty, IntProperty, CollectionProperty, + StringProperty, EnumProperty, FloatVectorProperty + ) +from .bmesh_utils import BmeshEdit as bmed +from .panel import Panel as Lofter +from mathutils import Vector, Matrix +from mathutils.geometry import interpolate_bezier +from math import sin, cos, pi, acos, atan2 +from .archipack_manipulator import Manipulable, archipack_manipulator +from .archipack_2d import Line, Arc +from .archipack_preset import ArchipackPreset, PresetMenuOperator +from .archipack_object import ArchipackCreateTool, ArchipackObject + + +class Fence(): + + def __init__(self): + # total distance from start + self.dist = 0 + self.t_start = 0 + self.t_end = 0 + self.dz = 0 + self.z0 = 0 + self.a0 = 0 + + def set_offset(self, offset, last=None): + """ + Offset line and compute intersection point + between segments + """ + self.line = self.make_offset(offset, last) + + @property + def t_diff(self): + return self.t_end - self.t_start + + def straight_fence(self, a0, length): + s = self.straight(length).rotate(a0) + return StraightFence(s.p, s.v) + + def curved_fence(self, a0, da, radius): + n = self.normal(1).rotate(a0).scale(radius) + if da < 0: + n.v = -n.v + a0 = n.angle + c = n.p - n.v + return CurvedFence(c, radius, a0, da) + + +class StraightFence(Fence, Line): + def __str__(self): + return "t_start:{} t_end:{} dist:{}".format(self.t_start, self.t_end, self.dist) + + def __init__(self, p, v): + Fence.__init__(self) + Line.__init__(self, p, v) + + +class CurvedFence(Fence, Arc): + def __str__(self): + return "t_start:{} t_end:{} dist:{}".format(self.t_start, self.t_end, self.dist) + + def __init__(self, c, radius, a0, da): + Fence.__init__(self) + Arc.__init__(self, c, radius, a0, da) + + +class FenceSegment(): + def __str__(self): + return "t_start:{} t_end:{} n_step:{} t_step:{} i_start:{} i_end:{}".format( + self.t_start, self.t_end, self.n_step, self.t_step, self.i_start, self.i_end) + + def __init__(self, t_start, t_end, n_step, t_step, i_start, i_end): + self.t_start = t_start + self.t_end = t_end + self.n_step = n_step + self.t_step = t_step + self.i_start = i_start + self.i_end = i_end + + +class FenceGenerator(): + + def __init__(self, parts): + self.parts = parts + self.segs = [] + self.length = 0 + self.user_defined_post = None + self.user_defined_uvs = None + self.user_defined_mat = None + + def add_part(self, part): + + if len(self.segs) < 1: + s = None + else: + s = self.segs[-1] + + # start a new fence + if s is None: + if part.type == 'S_FENCE': + p = Vector((0, 0)) + v = part.length * Vector((cos(part.a0), sin(part.a0))) + s = StraightFence(p, v) + elif part.type == 'C_FENCE': + c = -part.radius * Vector((cos(part.a0), sin(part.a0))) + s = CurvedFence(c, part.radius, part.a0, part.da) + else: + if part.type == 'S_FENCE': + s = s.straight_fence(part.a0, part.length) + elif part.type == 'C_FENCE': + s = s.curved_fence(part.a0, part.da, part.radius) + + # s.dist = self.length + # self.length += s.length + self.segs.append(s) + self.last_type = type + + def set_offset(self, offset): + # @TODO: + # re-evaluate length of offset line here + last = None + for seg in self.segs: + seg.set_offset(offset, last) + last = seg.line + + def param_t(self, angle_limit, post_spacing): + """ + setup corners and fences dz + compute index of fences wich belong to each group of fences between corners + compute t of each fence + """ + # segments are group of parts separated by limit angle + self.segments = [] + i_start = 0 + t_start = 0 + dist_0 = 0 + z = 0 + self.length = 0 + n_parts = len(self.parts) - 1 + for i, f in enumerate(self.segs): + f.dist = self.length + self.length += f.line.length + + vz0 = Vector((1, 0)) + angle_z = 0 + for i, f in enumerate(self.segs): + dz = self.parts[i].dz + if f.dist > 0: + f.t_start = f.dist / self.length + else: + f.t_start = 0 + + f.t_end = (f.dist + f.line.length) / self.length + f.z0 = z + f.dz = dz + z += dz + + if i < n_parts: + + vz1 = Vector((self.segs[i + 1].length, self.parts[i + 1].dz)) + angle_z = abs(vz0.angle_signed(vz1)) + vz0 = vz1 + + if (abs(self.parts[i + 1].a0) >= angle_limit or angle_z >= angle_limit): + l_seg = f.dist + f.line.length - dist_0 + t_seg = f.t_end - t_start + n_fences = max(1, int(l_seg / post_spacing)) + t_fence = t_seg / n_fences + segment = FenceSegment(t_start, f.t_end, n_fences, t_fence, i_start, i) + dist_0 = f.dist + f.line.length + t_start = f.t_end + i_start = i + self.segments.append(segment) + + manipulators = self.parts[i].manipulators + p0 = f.line.p0.to_3d() + p1 = f.line.p1.to_3d() + # angle from last to current segment + if i > 0: + v0 = self.segs[i - 1].line.straight(-1, 1).v.to_3d() + v1 = f.line.straight(1, 0).v.to_3d() + manipulators[0].set_pts([p0, v0, v1]) + + if type(f).__name__ == "StraightFence": + # segment length + manipulators[1].type_key = 'SIZE' + manipulators[1].prop1_name = "length" + manipulators[1].set_pts([p0, p1, (1, 0, 0)]) + else: + # segment radius + angle + v0 = (f.line.p0 - f.c).to_3d() + v1 = (f.line.p1 - f.c).to_3d() + manipulators[1].type_key = 'ARC_ANGLE_RADIUS' + manipulators[1].prop1_name = "da" + manipulators[1].prop2_name = "radius" + manipulators[1].set_pts([f.c.to_3d(), v0, v1]) + + # snap manipulator, dont change index ! + manipulators[2].set_pts([p0, p1, (1, 0, 0)]) + + f = self.segs[-1] + l_seg = f.dist + f.line.length - dist_0 + t_seg = f.t_end - t_start + n_fences = max(1, int(l_seg / post_spacing)) + t_fence = t_seg / n_fences + segment = FenceSegment(t_start, f.t_end, n_fences, t_fence, i_start, len(self.segs) - 1) + self.segments.append(segment) + + def setup_user_defined_post(self, o, post_x, post_y, post_z): + self.user_defined_post = o + x = o.bound_box[6][0] - o.bound_box[0][0] + y = o.bound_box[6][1] - o.bound_box[0][1] + z = o.bound_box[6][2] - o.bound_box[0][2] + self.user_defined_post_scale = Vector((post_x / x, post_y / -y, post_z / z)) + m = o.data + # create vertex group lookup dictionary for names + vgroup_names = {vgroup.index: vgroup.name for vgroup in o.vertex_groups} + # create dictionary of vertex group assignments per vertex + self.vertex_groups = [[vgroup_names[g.group] for g in v.groups] for v in m.vertices] + # uvs + uv_act = m.uv_layers.active + if uv_act is not None: + uv_layer = uv_act.data + self.user_defined_uvs = [[uv_layer[li].uv for li in p.loop_indices] for p in m.polygons] + else: + self.user_defined_uvs = [[(0, 0) for i in p.vertices] for p in m.polygons] + # material ids + self.user_defined_mat = [p.material_index for p in m.polygons] + + def get_user_defined_post(self, tM, z0, z1, z2, slope, post_z, verts, faces, matids, uvs): + f = len(verts) + m = self.user_defined_post.data + for i, g in enumerate(self.vertex_groups): + co = m.vertices[i].co.copy() + co.x *= self.user_defined_post_scale.x + co.y *= self.user_defined_post_scale.y + co.z *= self.user_defined_post_scale.z + if 'Slope' in g: + co.z += co.y * slope + verts.append(tM * co) + matids += self.user_defined_mat + faces += [tuple([i + f for i in p.vertices]) for p in m.polygons] + uvs += self.user_defined_uvs + + def get_post(self, post, post_x, post_y, post_z, post_alt, sub_offset_x, + id_mat, verts, faces, matids, uvs): + + n, dz, zl = post + slope = dz * post_y + + if self.user_defined_post is not None: + x, y = -n.v.normalized() + p = n.p + sub_offset_x * n.v.normalized() + tM = Matrix([ + [x, y, 0, p.x], + [y, -x, 0, p.y], + [0, 0, 1, zl + post_alt], + [0, 0, 0, 1] + ]) + self.get_user_defined_post(tM, zl, 0, 0, dz, post_z, verts, faces, matids, uvs) + return + + z3 = zl + post_z + post_alt - slope + z4 = zl + post_z + post_alt + slope + z0 = zl + post_alt - slope + z1 = zl + post_alt + slope + vn = n.v.normalized() + dx = post_x * vn + dy = post_y * Vector((vn.y, -vn.x)) + oy = sub_offset_x * vn + x0, y0 = n.p - dx + dy + oy + x1, y1 = n.p - dx - dy + oy + x2, y2 = n.p + dx - dy + oy + x3, y3 = n.p + dx + dy + oy + f = len(verts) + verts.extend([(x0, y0, z0), (x0, y0, z3), + (x1, y1, z1), (x1, y1, z4), + (x2, y2, z1), (x2, y2, z4), + (x3, y3, z0), (x3, y3, z3)]) + faces.extend([(f, f + 1, f + 3, f + 2), + (f + 2, f + 3, f + 5, f + 4), + (f + 4, f + 5, f + 7, f + 6), + (f + 6, f + 7, f + 1, f), + (f, f + 2, f + 4, f + 6), + (f + 7, f + 5, f + 3, f + 1)]) + matids.extend([id_mat, id_mat, id_mat, id_mat, id_mat, id_mat]) + x = [(0, 0), (0, post_z), (post_x, post_z), (post_x, 0)] + y = [(0, 0), (0, post_z), (post_y, post_z), (post_y, 0)] + z = [(0, 0), (post_x, 0), (post_x, post_y), (0, post_y)] + uvs.extend([x, y, x, y, z, z]) + + def get_panel(self, subs, altitude, panel_x, panel_z, sub_offset_x, idmat, verts, faces, matids, uvs): + n_subs = len(subs) + if n_subs < 1: + return + f = len(verts) + x0 = sub_offset_x - 0.5 * panel_x + x1 = sub_offset_x + 0.5 * panel_x + z0 = 0 + z1 = panel_z + profile = [Vector((x0, z0)), Vector((x1, z0)), Vector((x1, z1)), Vector((x0, z1))] + user_path_uv_v = [] + n_sections = n_subs - 1 + n, dz, zl = subs[0] + p0 = n.p + v0 = n.v.normalized() + for s, section in enumerate(subs): + n, dz, zl = section + p1 = n.p + if s < n_sections: + v1 = subs[s + 1][0].v.normalized() + dir = (v0 + v1).normalized() + scale = 1 / cos(0.5 * acos(min(1, max(-1, v0 * v1)))) + for p in profile: + x, y = n.p + scale * p.x * dir + z = zl + p.y + altitude + verts.append((x, y, z)) + if s > 0: + user_path_uv_v.append((p1 - p0).length) + p0 = p1 + v0 = v1 + + # build faces using Panel + lofter = Lofter( + # closed_shape, index, x, y, idmat + True, + [i for i in range(len(profile))], + [p.x for p in profile], + [p.y for p in profile], + [idmat for i in range(len(profile))], + closed_path=False, + user_path_uv_v=user_path_uv_v, + user_path_verts=n_subs + ) + faces += lofter.faces(16, offset=f, path_type='USER_DEFINED') + matids += lofter.mat(16, idmat, idmat, path_type='USER_DEFINED') + v = Vector((0, 0)) + uvs += lofter.uv(16, v, v, v, v, 0, v, 0, 0, path_type='USER_DEFINED') + + def make_subs(self, x, y, z, post_y, altitude, + sub_spacing, offset_x, sub_offset_x, mat, verts, faces, matids, uvs): + + t_post = (0.5 * post_y - y) / self.length + t_spacing = (sub_spacing + y) / self.length + + for segment in self.segments: + t_step = segment.t_step + t_start = segment.t_start + t_post + s = 0 + s_sub = t_step - 2 * t_post + n_sub = int(s_sub / t_spacing) + if n_sub > 0: + t_sub = s_sub / n_sub + else: + t_sub = 1 + i = segment.i_start + while s < segment.n_step: + t_cur = t_start + s * t_step + for j in range(1, n_sub): + t_s = t_cur + t_sub * j + while self.segs[i].t_end < t_s: + i += 1 + f = self.segs[i] + t = (t_s - f.t_start) / f.t_diff + n = f.line.normal(t) + post = (n, f.dz / f.length, f.z0 + f.dz * t) + self.get_post(post, x, y, z, altitude, sub_offset_x, mat, verts, faces, matids, uvs) + s += 1 + + def make_post(self, x, y, z, altitude, x_offset, mat, verts, faces, matids, uvs): + + for segment in self.segments: + t_step = segment.t_step + t_start = segment.t_start + s = 0 + i = segment.i_start + while s < segment.n_step: + t_cur = t_start + s * t_step + while self.segs[i].t_end < t_cur: + i += 1 + f = self.segs[i] + t = (t_cur - f.t_start) / f.t_diff + n = f.line.normal(t) + post = (n, f.dz / f.line.length, f.z0 + f.dz * t) + # self.get_post(post, x, y, z, altitude, x_offset, mat, verts, faces, matids, uvs) + self.get_post(post, x, y, z, altitude, 0, mat, verts, faces, matids, uvs) + s += 1 + + if segment.i_end + 1 == len(self.segs): + f = self.segs[segment.i_end] + n = f.line.normal(1) + post = (n, f.dz / f.line.length, f.z0 + f.dz) + # self.get_post(post, x, y, z, altitude, x_offset, mat, verts, faces, matids, uvs) + self.get_post(post, x, y, z, altitude, 0, mat, verts, faces, matids, uvs) + + def make_panels(self, x, z, post_y, altitude, panel_dist, + offset_x, sub_offset_x, idmat, verts, faces, matids, uvs): + + t_post = (0.5 * post_y + panel_dist) / self.length + for segment in self.segments: + t_step = segment.t_step + t_start = segment.t_start + s = 0 + i = segment.i_start + while s < segment.n_step: + subs = [] + t_cur = t_start + s * t_step + t_post + t_end = t_start + (s + 1) * t_step - t_post + # find first section + while self.segs[i].t_end < t_cur and i < segment.i_end: + i += 1 + f = self.segs[i] + # 1st section + t = (t_cur - f.t_start) / f.t_diff + n = f.line.normal(t) + subs.append((n, f.dz / f.line.length, f.z0 + f.dz * t)) + # crossing sections -> new segment + while i < segment.i_end: + f = self.segs[i] + if f.t_end < t_end: + if type(f).__name__ == 'CurvedFence': + # cant end after segment + t0 = max(0, (t_cur - f.t_start) / f.t_diff) + t1 = min(1, (t_end - f.t_start) / f.t_diff) + n_s = int(max(1, abs(f.da) * (5) / pi - 1)) + dt = (t1 - t0) / n_s + for j in range(1, n_s + 1): + t = t0 + dt * j + n = f.line.sized_normal(t, 1) + # n.p = f.lerp(x_offset) + subs.append((n, f.dz / f.line.length, f.z0 + f.dz * t)) + else: + n = f.line.normal(1) + subs.append((n, f.dz / f.line.length, f.z0 + f.dz)) + if f.t_end >= t_end: + break + elif f.t_start < t_end: + i += 1 + + f = self.segs[i] + # last section + if type(f).__name__ == 'CurvedFence': + # cant start before segment + t0 = max(0, (t_cur - f.t_start) / f.t_diff) + t1 = min(1, (t_end - f.t_start) / f.t_diff) + n_s = int(max(1, abs(f.da) * (5) / pi - 1)) + dt = (t1 - t0) / n_s + for j in range(1, n_s + 1): + t = t0 + dt * j + n = f.line.sized_normal(t, 1) + # n.p = f.lerp(x_offset) + subs.append((n, f.dz / f.line.length, f.z0 + f.dz * t)) + else: + t = (t_end - f.t_start) / f.t_diff + n = f.line.normal(t) + subs.append((n, f.dz / f.line.length, f.z0 + f.dz * t)) + + # self.get_panel(subs, altitude, x, z, 0, idmat, verts, faces, matids, uvs) + self.get_panel(subs, altitude, x, z, sub_offset_x, idmat, verts, faces, matids, uvs) + s += 1 + + def make_profile(self, profile, idmat, + x_offset, z_offset, extend, verts, faces, matids, uvs): + + last = None + for seg in self.segs: + seg.p_line = seg.make_offset(x_offset, last) + last = seg.p_line + + n_fences = len(self.segs) - 1 + + if n_fences < 0: + return + + sections = [] + + f = self.segs[0] + + # first step + if extend != 0 and f.p_line.length != 0: + t = -extend / self.segs[0].p_line.length + n = f.p_line.sized_normal(t, 1) + # n.p = f.lerp(x_offset) + sections.append((n, f.dz / f.p_line.length, f.z0 + f.dz * t)) + + # add first section + n = f.p_line.sized_normal(0, 1) + # n.p = f.lerp(x_offset) + sections.append((n, f.dz / f.p_line.length, f.z0)) + + for s, f in enumerate(self.segs): + if f.p_line.length == 0: + continue + if type(f).__name__ == 'CurvedFence': + n_s = int(max(1, abs(f.da) * 30 / pi - 1)) + for i in range(1, n_s + 1): + t = i / n_s + n = f.p_line.sized_normal(t, 1) + # n.p = f.lerp(x_offset) + sections.append((n, f.dz / f.p_line.length, f.z0 + f.dz * t)) + else: + n = f.p_line.sized_normal(1, 1) + # n.p = f.lerp(x_offset) + sections.append((n, f.dz / f.p_line.length, f.z0 + f.dz)) + + if extend != 0 and f.p_line.length != 0: + t = 1 + extend / self.segs[-1].p_line.length + n = f.p_line.sized_normal(t, 1) + # n.p = f.lerp(x_offset) + sections.append((n, f.dz / f.p_line.length, f.z0 + f.dz * t)) + + user_path_verts = len(sections) + offset = len(verts) + if user_path_verts > 0: + user_path_uv_v = [] + n, dz, z0 = sections[-1] + sections[-1] = (n, dz, z0) + n_sections = user_path_verts - 1 + n, dz, zl = sections[0] + p0 = n.p + v0 = n.v.normalized() + for s, section in enumerate(sections): + n, dz, zl = section + p1 = n.p + if s < n_sections: + v1 = sections[s + 1][0].v.normalized() + dir = (v0 + v1).normalized() + scale = min(10, 1 / cos(0.5 * acos(min(1, max(-1, v0 * v1))))) + for p in profile: + # x, y = n.p + scale * (x_offset + p.x) * dir + x, y = n.p + scale * p.x * dir + z = zl + p.y + z_offset + verts.append((x, y, z)) + if s > 0: + user_path_uv_v.append((p1 - p0).length) + p0 = p1 + v0 = v1 + + # build faces using Panel + lofter = Lofter( + # closed_shape, index, x, y, idmat + True, + [i for i in range(len(profile))], + [p.x for p in profile], + [p.y for p in profile], + [idmat for i in range(len(profile))], + closed_path=False, + user_path_uv_v=user_path_uv_v, + user_path_verts=user_path_verts + ) + faces += lofter.faces(16, offset=offset, path_type='USER_DEFINED') + matids += lofter.mat(16, idmat, idmat, path_type='USER_DEFINED') + v = Vector((0, 0)) + uvs += lofter.uv(16, v, v, v, v, 0, v, 0, 0, path_type='USER_DEFINED') + + +def update(self, context): + self.update(context) + + +def update_manipulators(self, context): + self.update(context, manipulable_refresh=True) + + +def update_path(self, context): + self.update_path(context) + + +def update_type(self, context): + + d = self.find_datablock_in_selection(context) + + if d is not None and d.auto_update: + + d.auto_update = False + # find part index + idx = 0 + for i, part in enumerate(d.parts): + if part == self: + idx = i + break + part = d.parts[idx] + a0 = 0 + if idx > 0: + g = d.get_generator() + w0 = g.segs[idx - 1] + a0 = w0.straight(1).angle + if "C_" in self.type: + w = w0.straight_fence(part.a0, part.length) + else: + w = w0.curved_fence(part.a0, part.da, part.radius) + else: + g = FenceGenerator(None) + g.add_part(self) + w = g.segs[0] + + # w0 - w - w1 + dp = w.p1 - w.p0 + if "C_" in self.type: + part.radius = 0.5 * dp.length + part.da = pi + a0 = atan2(dp.y, dp.x) - pi / 2 - a0 + else: + part.length = dp.length + a0 = atan2(dp.y, dp.x) - a0 + + if a0 > pi: + a0 -= 2 * pi + if a0 < -pi: + a0 += 2 * pi + part.a0 = a0 + + if idx + 1 < d.n_parts: + # adjust rotation of next part + part1 = d.parts[idx + 1] + if "C_" in part.type: + a0 = part1.a0 - pi / 2 + else: + a0 = part1.a0 + w.straight(1).angle - atan2(dp.y, dp.x) + + if a0 > pi: + a0 -= 2 * pi + if a0 < -pi: + a0 += 2 * pi + part1.a0 = a0 + + d.auto_update = True + + +materials_enum = ( + ('0', 'Wood', '', 0), + ('1', 'Metal', '', 1), + ('2', 'Glass', '', 2) + ) + + +class archipack_fence_material(PropertyGroup): + index = EnumProperty( + items=materials_enum, + default='0', + update=update + ) + + def find_datablock_in_selection(self, context): + """ + find witch selected object this instance belongs to + provide support for "copy to selected" + """ + selected = [o for o in context.selected_objects] + for o in selected: + props = archipack_fence.datablock(o) + if props: + for part in props.rail_mat: + if part == self: + return props + return None + + def update(self, context): + props = self.find_datablock_in_selection(context) + if props is not None: + props.update(context) + + +class archipack_fence_part(PropertyGroup): + type = EnumProperty( + items=( + ('S_FENCE', 'Straight fence', '', 0), + ('C_FENCE', 'Curved fence', '', 1), + ), + default='S_FENCE', + update=update_type + ) + length = FloatProperty( + name="length", + min=0.01, + default=2.0, + unit='LENGTH', subtype='DISTANCE', + update=update + ) + radius = FloatProperty( + name="radius", + min=0.01, + default=0.7, + unit='LENGTH', subtype='DISTANCE', + update=update + ) + da = FloatProperty( + name="total angle", + min=-pi, + max=pi, + default=pi / 2, + subtype='ANGLE', unit='ROTATION', + update=update + ) + a0 = FloatProperty( + name="angle", + min=-2 * pi, + max=2 * pi, + default=0, + subtype='ANGLE', unit='ROTATION', + update=update + ) + dz = FloatProperty( + name="delta z", + default=0, + unit='LENGTH', subtype='DISTANCE' + ) + + manipulators = CollectionProperty(type=archipack_manipulator) + + def find_datablock_in_selection(self, context): + """ + find witch selected object this instance belongs to + provide support for "copy to selected" + """ + selected = [o for o in context.selected_objects] + for o in selected: + props = archipack_fence.datablock(o) + if props is not None: + for part in props.parts: + if part == self: + return props + return None + + def update(self, context, manipulable_refresh=False): + props = self.find_datablock_in_selection(context) + if props is not None: + props.update(context, manipulable_refresh) + + def draw(self, layout, context, index): + box = layout.box() + row = box.row() + row.prop(self, "type", text=str(index + 1)) + if self.type in ['C_FENCE']: + row = box.row() + row.prop(self, "radius") + row = box.row() + row.prop(self, "da") + else: + row = box.row() + row.prop(self, "length") + row = box.row() + row.prop(self, "a0") + + +class archipack_fence(ArchipackObject, Manipulable, PropertyGroup): + + parts = CollectionProperty(type=archipack_fence_part) + user_defined_path = StringProperty( + name="user defined", + update=update_path + ) + user_defined_spline = IntProperty( + name="Spline index", + min=0, + default=0, + update=update_path + ) + user_defined_resolution = IntProperty( + name="resolution", + min=1, + max=128, + default=12, update=update_path + ) + n_parts = IntProperty( + name="parts", + min=1, + default=1, update=update_manipulators + ) + x_offset = FloatProperty( + name="x offset", + default=0.0, precision=2, step=1, + unit='LENGTH', subtype='DISTANCE', + update=update + ) + + radius = FloatProperty( + name="radius", + min=0.01, + default=0.7, + unit='LENGTH', subtype='DISTANCE', + update=update + ) + da = FloatProperty( + name="angle", + min=-pi, + max=pi, + default=pi / 2, + subtype='ANGLE', unit='ROTATION', + update=update + ) + angle_limit = FloatProperty( + name="angle", + min=0, + max=2 * pi, + default=pi / 8, + subtype='ANGLE', unit='ROTATION', + update=update_manipulators + ) + shape = EnumProperty( + items=( + ('RECTANGLE', 'Straight', '', 0), + ('CIRCLE', 'Curved ', '', 1) + ), + default='RECTANGLE', + update=update + ) + post = BoolProperty( + name='enable', + default=True, + update=update + ) + post_spacing = FloatProperty( + name="spacing", + min=0.1, + default=1.0, precision=2, step=1, + unit='LENGTH', subtype='DISTANCE', + update=update + ) + post_x = FloatProperty( + name="width", + min=0.001, + default=0.04, precision=2, step=1, + unit='LENGTH', subtype='DISTANCE', + update=update + ) + post_y = FloatProperty( + name="length", + min=0.001, max=1000, + default=0.04, precision=2, step=1, + unit='LENGTH', subtype='DISTANCE', + update=update + ) + post_z = FloatProperty( + name="height", + min=0.001, + default=1, precision=2, step=1, + unit='LENGTH', subtype='DISTANCE', + update=update + ) + post_alt = FloatProperty( + name="altitude", + default=0, precision=2, step=1, + unit='LENGTH', subtype='DISTANCE', + update=update + ) + user_defined_post_enable = BoolProperty( + name="User", + update=update, + default=True + ) + user_defined_post = StringProperty( + name="user defined", + update=update + ) + idmat_post = EnumProperty( + name="Post", + items=materials_enum, + default='1', + update=update + ) + subs = BoolProperty( + name='enable', + default=False, + update=update + ) + subs_spacing = FloatProperty( + name="spacing", + min=0.05, + default=0.10, precision=2, step=1, + unit='LENGTH', subtype='DISTANCE', + update=update + ) + subs_x = FloatProperty( + name="width", + min=0.001, + default=0.02, precision=2, step=1, + unit='LENGTH', subtype='DISTANCE', + update=update + ) + subs_y = FloatProperty( + name="length", + min=0.001, + default=0.02, precision=2, step=1, + unit='LENGTH', subtype='DISTANCE', + update=update + ) + subs_z = FloatProperty( + name="height", + min=0.001, + default=1, precision=2, step=1, + unit='LENGTH', subtype='DISTANCE', + update=update + ) + subs_alt = FloatProperty( + name="altitude", + default=0, precision=2, step=1, + unit='LENGTH', subtype='DISTANCE', + update=update + ) + subs_offset_x = FloatProperty( + name="offset", + default=0.0, precision=2, step=1, + unit='LENGTH', subtype='DISTANCE', + update=update + ) + subs_bottom = EnumProperty( + name="Bottom", + items=( + ('STEP', 'Follow step', '', 0), + ('LINEAR', 'Linear', '', 1), + ), + default='STEP', + update=update + ) + user_defined_subs_enable = BoolProperty( + name="User", + update=update, + default=True + ) + user_defined_subs = StringProperty( + name="user defined", + update=update + ) + idmat_subs = EnumProperty( + name="Subs", + items=materials_enum, + default='1', + update=update + ) + panel = BoolProperty( + name='enable', + default=True, + update=update + ) + panel_alt = FloatProperty( + name="altitude", + default=0.25, precision=2, step=1, + unit='LENGTH', subtype='DISTANCE', + update=update + ) + panel_x = FloatProperty( + name="width", + min=0.001, + default=0.01, precision=2, step=1, + unit='LENGTH', subtype='DISTANCE', + update=update + ) + panel_z = FloatProperty( + name="height", + min=0.001, + default=0.6, precision=2, step=1, + unit='LENGTH', subtype='DISTANCE', + update=update + ) + panel_dist = FloatProperty( + name="space", + min=0.001, + default=0.05, precision=2, step=1, + unit='LENGTH', subtype='DISTANCE', + update=update + ) + panel_offset_x = FloatProperty( + name="offset", + default=0.0, precision=2, step=1, + unit='LENGTH', subtype='DISTANCE', + update=update + ) + idmat_panel = EnumProperty( + name="Panels", + items=materials_enum, + default='2', + update=update + ) + rail = BoolProperty( + name="enable", + update=update, + default=False + ) + rail_n = IntProperty( + name="number", + default=1, + min=0, + max=31, + update=update + ) + rail_x = FloatVectorProperty( + name="width", + default=[ + 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, + 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, + 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, + 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05 + ], + size=31, + min=0.001, + precision=2, step=1, + unit='LENGTH', + update=update + ) + rail_z = FloatVectorProperty( + name="height", + default=[ + 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, + 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, + 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, + 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05 + ], + size=31, + min=0.001, + precision=2, step=1, + unit='LENGTH', + update=update + ) + rail_offset = FloatVectorProperty( + name="offset", + default=[ + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0 + ], + size=31, + precision=2, step=1, + unit='LENGTH', + update=update + ) + rail_alt = FloatVectorProperty( + name="altitude", + default=[ + 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, + 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, + 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, + 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0 + ], + size=31, + precision=2, step=1, + unit='LENGTH', + update=update + ) + rail_mat = CollectionProperty(type=archipack_fence_material) + + handrail = BoolProperty( + name="enable", + update=update, + default=True + ) + handrail_offset = FloatProperty( + name="offset", + default=0.0, precision=2, step=1, + unit='LENGTH', subtype='DISTANCE', + update=update + ) + handrail_alt = FloatProperty( + name="altitude", + default=1.0, precision=2, step=1, + unit='LENGTH', subtype='DISTANCE', + update=update + ) + handrail_extend = FloatProperty( + name="extend", + min=0, + default=0.1, precision=2, step=1, + unit='LENGTH', subtype='DISTANCE', + update=update + ) + handrail_slice = BoolProperty( + name='slice', + default=True, + update=update + ) + handrail_slice_right = BoolProperty( + name='slice', + default=True, + update=update + ) + handrail_profil = EnumProperty( + name="Profil", + items=( + ('SQUARE', 'Square', '', 0), + ('CIRCLE', 'Circle', '', 1), + ('COMPLEX', 'Circle over square', '', 2) + ), + default='SQUARE', + update=update + ) + handrail_x = FloatProperty( + name="width", + min=0.001, + default=0.04, precision=2, step=1, + unit='LENGTH', subtype='DISTANCE', + update=update + ) + handrail_y = FloatProperty( + name="height", + min=0.001, + default=0.04, precision=2, step=1, + unit='LENGTH', subtype='DISTANCE', + update=update + ) + handrail_radius = FloatProperty( + name="radius", + min=0.001, + default=0.02, precision=2, step=1, + unit='LENGTH', subtype='DISTANCE', + update=update + ) + idmat_handrail = EnumProperty( + name="Handrail", + items=materials_enum, + default='0', + update=update + ) + + # UI layout related + parts_expand = BoolProperty( + default=False + ) + rail_expand = BoolProperty( + default=False + ) + idmats_expand = BoolProperty( + default=False + ) + handrail_expand = BoolProperty( + default=False + ) + post_expand = BoolProperty( + default=False + ) + panel_expand = BoolProperty( + default=False + ) + subs_expand = BoolProperty( + default=False + ) + + # Flag to prevent mesh update while making bulk changes over variables + # use : + # .auto_update = False + # bulk changes + # .auto_update = True + auto_update = BoolProperty( + options={'SKIP_SAVE'}, + default=True, + update=update_manipulators + ) + + def setup_manipulators(self): + + if len(self.manipulators) == 0: + s = self.manipulators.add() + s.prop1_name = "width" + s = self.manipulators.add() + s.prop1_name = "height" + s.normal = Vector((0, 1, 0)) + + for i in range(self.n_parts): + p = self.parts[i] + n_manips = len(p.manipulators) + if n_manips == 0: + s = p.manipulators.add() + s.type_key = "ANGLE" + s.prop1_name = "a0" + s = p.manipulators.add() + s.type_key = "SIZE" + s.prop1_name = "length" + s = p.manipulators.add() + # s.type_key = 'SNAP_POINT' + s.type_key = 'WALL_SNAP' + s.prop1_name = str(i) + s.prop2_name = 'post_z' + + def update_parts(self): + + # remove rails materials + for i in range(len(self.rail_mat), self.rail_n, -1): + self.rail_mat.remove(i - 1) + + # add rails + for i in range(len(self.rail_mat), self.rail_n): + self.rail_mat.add() + + # remove parts + for i in range(len(self.parts), self.n_parts, -1): + self.parts.remove(i - 1) + + # add parts + for i in range(len(self.parts), self.n_parts): + self.parts.add() + + self.setup_manipulators() + + def interpolate_bezier(self, pts, wM, p0, p1, resolution): + # straight segment, worth testing here + # since this can lower points count by a resolution factor + # use normalized to handle non linear t + if resolution == 0: + pts.append(wM * p0.co.to_3d()) + else: + v = (p1.co - p0.co).normalized() + d1 = (p0.handle_right - p0.co).normalized() + d2 = (p1.co - p1.handle_left).normalized() + if d1 == v and d2 == v: + pts.append(wM * p0.co.to_3d()) + else: + seg = interpolate_bezier(wM * p0.co, + wM * p0.handle_right, + wM * p1.handle_left, + wM * p1.co, + resolution + 1) + for i in range(resolution): + pts.append(seg[i].to_3d()) + + def from_spline(self, context, wM, resolution, spline): + + o = self.find_in_selection(context) + + if o is None: + return + + tM = wM.copy() + tM.row[0].normalize() + tM.row[1].normalize() + tM.row[2].normalize() + pts = [] + if spline.type == 'POLY': + pt = spline.points[0].co + pts = [wM * p.co.to_3d() for p in spline.points] + if spline.use_cyclic_u: + pts.append(pts[0]) + elif spline.type == 'BEZIER': + pt = spline.bezier_points[0].co + points = spline.bezier_points + for i in range(1, len(points)): + p0 = points[i - 1] + p1 = points[i] + self.interpolate_bezier(pts, wM, p0, p1, resolution) + if spline.use_cyclic_u: + p0 = points[-1] + p1 = points[0] + self.interpolate_bezier(pts, wM, p0, p1, resolution) + pts.append(pts[0]) + else: + pts.append(wM * points[-1].co) + auto_update = self.auto_update + self.auto_update = False + + self.n_parts = len(pts) - 1 + self.update_parts() + + p0 = pts.pop(0) + a0 = 0 + for i, p1 in enumerate(pts): + dp = p1 - p0 + da = atan2(dp.y, dp.x) - a0 + if da > pi: + da -= 2 * pi + if da < -pi: + da += 2 * pi + p = self.parts[i] + p.length = dp.to_2d().length + p.dz = dp.z + p.a0 = da + a0 += da + p0 = p1 + + self.auto_update = auto_update + + o.matrix_world = tM * Matrix([ + [1, 0, 0, pt.x], + [0, 1, 0, pt.y], + [0, 0, 1, pt.z], + [0, 0, 0, 1] + ]) + + def update_path(self, context): + path = context.scene.objects.get(self.user_defined_path) + if path is not None and path.type == 'CURVE': + splines = path.data.splines + if len(splines) > self.user_defined_spline: + self.from_spline( + context, + path.matrix_world, + self.user_defined_resolution, + splines[self.user_defined_spline]) + + def get_generator(self): + g = FenceGenerator(self.parts) + for part in self.parts: + # type, radius, da, length + g.add_part(part) + + g.set_offset(self.x_offset) + # param_t(da, part_length) + g.param_t(self.angle_limit, self.post_spacing) + return g + + def update(self, context, manipulable_refresh=False): + + o = self.find_in_selection(context, self.auto_update) + + if o is None: + return + + # clean up manipulators before any data model change + if manipulable_refresh: + self.manipulable_disable(context) + + self.update_parts() + + verts = [] + faces = [] + matids = [] + uvs = [] + + g = self.get_generator() + + # depth at bottom + # self.manipulators[1].set_pts([(0, 0, 0), (0, 0, self.height), (1, 0, 0)]) + + if self.user_defined_post_enable: + # user defined posts + user_def_post = context.scene.objects.get(self.user_defined_post) + if user_def_post is not None and user_def_post.type == 'MESH': + g.setup_user_defined_post(user_def_post, self.post_x, self.post_y, self.post_z) + + if self.post: + g.make_post(0.5 * self.post_x, 0.5 * self.post_y, self.post_z, + self.post_alt, self.x_offset, + int(self.idmat_post), verts, faces, matids, uvs) + + # reset user def posts + g.user_defined_post = None + + # user defined subs + if self.user_defined_subs_enable: + user_def_subs = context.scene.objects.get(self.user_defined_subs) + if user_def_subs is not None and user_def_subs.type == 'MESH': + g.setup_user_defined_post(user_def_subs, self.subs_x, self.subs_y, self.subs_z) + + if self.subs: + g.make_subs(0.5 * self.subs_x, 0.5 * self.subs_y, self.subs_z, + self.post_y, self.subs_alt, self.subs_spacing, + self.x_offset, self.subs_offset_x, int(self.idmat_subs), verts, faces, matids, uvs) + + g.user_defined_post = None + + if self.panel: + g.make_panels(0.5 * self.panel_x, self.panel_z, self.post_y, + self.panel_alt, self.panel_dist, self.x_offset, self.panel_offset_x, + int(self.idmat_panel), verts, faces, matids, uvs) + + if self.rail: + for i in range(self.rail_n): + x = 0.5 * self.rail_x[i] + y = self.rail_z[i] + rail = [Vector((-x, y)), Vector((-x, 0)), Vector((x, 0)), Vector((x, y))] + g.make_profile(rail, int(self.rail_mat[i].index), self.x_offset - self.rail_offset[i], + self.rail_alt[i], 0, verts, faces, matids, uvs) + + if self.handrail_profil == 'COMPLEX': + sx = self.handrail_x + sy = self.handrail_y + handrail = [Vector((sx * x, sy * y)) for x, y in [ + (-0.28, 1.83), (-0.355, 1.77), (-0.415, 1.695), (-0.46, 1.605), (-0.49, 1.51), (-0.5, 1.415), + (-0.49, 1.315), (-0.46, 1.225), (-0.415, 1.135), (-0.355, 1.06), (-0.28, 1.0), (-0.255, 0.925), + (-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), + (0.28, 1.0), (0.355, 1.06), (0.415, 1.135), (0.46, 1.225), (0.49, 1.315), (0.5, 1.415), + (0.49, 1.51), (0.46, 1.605), (0.415, 1.695), (0.355, 1.77), (0.28, 1.83), (0.19, 1.875), + (0.1, 1.905), (0.0, 1.915), (-0.095, 1.905), (-0.19, 1.875)]] + + elif self.handrail_profil == 'SQUARE': + x = 0.5 * self.handrail_x + y = self.handrail_y + handrail = [Vector((-x, y)), Vector((-x, 0)), Vector((x, 0)), Vector((x, y))] + elif self.handrail_profil == 'CIRCLE': + r = self.handrail_radius + handrail = [Vector((r * sin(0.1 * -a * pi), r * (0.5 + cos(0.1 * -a * pi)))) for a in range(0, 20)] + + if self.handrail: + g.make_profile(handrail, int(self.idmat_handrail), self.x_offset - self.handrail_offset, + self.handrail_alt, self.handrail_extend, verts, faces, matids, uvs) + + bmed.buildmesh(context, o, verts, faces, matids=matids, uvs=uvs, weld=True, clean=True) + + # enable manipulators rebuild + if manipulable_refresh: + self.manipulable_refresh = True + + # restore context + self.restore_context(context) + + def manipulable_setup(self, context): + """ + NOTE: + this one assume context.active_object is the instance this + data belongs to, failing to do so will result in wrong + manipulators set on active object + """ + self.manipulable_disable(context) + + o = context.active_object + + self.setup_manipulators() + + for i, part in enumerate(self.parts): + if i >= self.n_parts: + break + + if i > 0: + # start angle + self.manip_stack.append(part.manipulators[0].setup(context, o, part)) + + # length / radius + angle + self.manip_stack.append(part.manipulators[1].setup(context, o, part)) + + # snap point + self.manip_stack.append(part.manipulators[2].setup(context, o, self)) + + for m in self.manipulators: + self.manip_stack.append(m.setup(context, o, self)) + + +class ARCHIPACK_PT_fence(Panel): + bl_idname = "ARCHIPACK_PT_fence" + bl_label = "Fence" + bl_space_type = 'VIEW_3D' + bl_region_type = 'UI' + bl_category = 'ArchiPack' + + @classmethod + def poll(cls, context): + return archipack_fence.filter(context.active_object) + + def draw(self, context): + prop = archipack_fence.datablock(context.active_object) + if prop is None: + return + scene = context.scene + layout = self.layout + row = layout.row(align=True) + row.operator('archipack.fence_manipulate', icon='HAND') + box = layout.box() + # box.label(text="Styles") + row = box.row(align=True) + row.operator("archipack.fence_preset_menu", text=bpy.types.ARCHIPACK_OT_fence_preset_menu.bl_label) + row.operator("archipack.fence_preset", text="", icon='ZOOMIN') + row.operator("archipack.fence_preset", text="", icon='ZOOMOUT').remove_active = True + box = layout.box() + row = box.row(align=True) + row.operator("archipack.fence_curve_update", text="", icon='FILE_REFRESH') + row.prop_search(prop, "user_defined_path", scene, "objects", text="", icon='OUTLINER_OB_CURVE') + if prop.user_defined_path is not "": + box.prop(prop, 'user_defined_spline') + box.prop(prop, 'user_defined_resolution') + box.prop(prop, 'angle_limit') + box.prop(prop, 'x_offset') + box = layout.box() + row = box.row() + if prop.parts_expand: + row.prop(prop, 'parts_expand', icon="TRIA_DOWN", icon_only=True, text="Parts", emboss=False) + box.prop(prop, 'n_parts') + for i, part in enumerate(prop.parts): + part.draw(layout, context, i) + else: + row.prop(prop, 'parts_expand', icon="TRIA_RIGHT", icon_only=True, text="Parts", emboss=False) + + box = layout.box() + row = box.row(align=True) + if prop.handrail_expand: + row.prop(prop, 'handrail_expand', icon="TRIA_DOWN", icon_only=True, text="Handrail", emboss=False) + else: + row.prop(prop, 'handrail_expand', icon="TRIA_RIGHT", icon_only=True, text="Handrail", emboss=False) + + row.prop(prop, 'handrail') + + if prop.handrail_expand: + box.prop(prop, 'handrail_alt') + box.prop(prop, 'handrail_offset') + box.prop(prop, 'handrail_extend') + box.prop(prop, 'handrail_profil') + if prop.handrail_profil != 'CIRCLE': + box.prop(prop, 'handrail_x') + box.prop(prop, 'handrail_y') + else: + box.prop(prop, 'handrail_radius') + row = box.row(align=True) + row.prop(prop, 'handrail_slice') + + box = layout.box() + row = box.row(align=True) + if prop.post_expand: + row.prop(prop, 'post_expand', icon="TRIA_DOWN", icon_only=True, text="Post", emboss=False) + else: + row.prop(prop, 'post_expand', icon="TRIA_RIGHT", icon_only=True, text="Post", emboss=False) + row.prop(prop, 'post') + if prop.post_expand: + box.prop(prop, 'post_spacing') + box.prop(prop, 'post_x') + box.prop(prop, 'post_y') + box.prop(prop, 'post_z') + box.prop(prop, 'post_alt') + row = box.row(align=True) + row.prop(prop, 'user_defined_post_enable', text="") + row.prop_search(prop, "user_defined_post", scene, "objects", text="") + + box = layout.box() + row = box.row(align=True) + if prop.subs_expand: + row.prop(prop, 'subs_expand', icon="TRIA_DOWN", icon_only=True, text="Subs", emboss=False) + else: + row.prop(prop, 'subs_expand', icon="TRIA_RIGHT", icon_only=True, text="Subs", emboss=False) + + row.prop(prop, 'subs') + if prop.subs_expand: + box.prop(prop, 'subs_spacing') + box.prop(prop, 'subs_x') + box.prop(prop, 'subs_y') + box.prop(prop, 'subs_z') + box.prop(prop, 'subs_alt') + box.prop(prop, 'subs_offset_x') + row = box.row(align=True) + row.prop(prop, 'user_defined_subs_enable', text="") + row.prop_search(prop, "user_defined_subs", scene, "objects", text="") + + box = layout.box() + row = box.row(align=True) + if prop.panel_expand: + row.prop(prop, 'panel_expand', icon="TRIA_DOWN", icon_only=True, text="Panels", emboss=False) + else: + row.prop(prop, 'panel_expand', icon="TRIA_RIGHT", icon_only=True, text="Panels", emboss=False) + row.prop(prop, 'panel') + if prop.panel_expand: + box.prop(prop, 'panel_dist') + box.prop(prop, 'panel_x') + box.prop(prop, 'panel_z') + box.prop(prop, 'panel_alt') + box.prop(prop, 'panel_offset_x') + + box = layout.box() + row = box.row(align=True) + if prop.rail_expand: + row.prop(prop, 'rail_expand', icon="TRIA_DOWN", icon_only=True, text="Rails", emboss=False) + else: + row.prop(prop, 'rail_expand', icon="TRIA_RIGHT", icon_only=True, text="Rails", emboss=False) + row.prop(prop, 'rail') + if prop.rail_expand: + box.prop(prop, 'rail_n') + for i in range(prop.rail_n): + box = layout.box() + box.label(text="Rail " + str(i + 1)) + box.prop(prop, 'rail_x', index=i) + box.prop(prop, 'rail_z', index=i) + box.prop(prop, 'rail_alt', index=i) + box.prop(prop, 'rail_offset', index=i) + box.prop(prop.rail_mat[i], 'index', text="") + + box = layout.box() + row = box.row() + + if prop.idmats_expand: + row.prop(prop, 'idmats_expand', icon="TRIA_DOWN", icon_only=True, text="Materials", emboss=False) + box.prop(prop, 'idmat_handrail') + box.prop(prop, 'idmat_panel') + box.prop(prop, 'idmat_post') + box.prop(prop, 'idmat_subs') + else: + row.prop(prop, 'idmats_expand', icon="TRIA_RIGHT", icon_only=True, text="Materials", emboss=False) + +# ------------------------------------------------------------------ +# Define operator class to create object +# ------------------------------------------------------------------ + + +class ARCHIPACK_OT_fence(ArchipackCreateTool, Operator): + bl_idname = "archipack.fence" + bl_label = "Fence" + bl_description = "Fence" + bl_category = 'Archipack' + bl_options = {'REGISTER', 'UNDO'} + + def create(self, context): + m = bpy.data.meshes.new("Fence") + o = bpy.data.objects.new("Fence", m) + d = m.archipack_fence.add() + # make manipulators selectable + d.manipulable_selectable = True + context.scene.objects.link(o) + o.select = True + context.scene.objects.active = o + self.load_preset(d) + self.add_material(o) + return o + + def execute(self, context): + if context.mode == "OBJECT": + bpy.ops.object.select_all(action="DESELECT") + o = self.create(context) + o.location = bpy.context.scene.cursor_location + o.select = True + context.scene.objects.active = o + self.manipulate() + return {'FINISHED'} + else: + self.report({'WARNING'}, "Archipack: Option only valid in Object mode") + return {'CANCELLED'} + + +# ------------------------------------------------------------------ +# Define operator class to create object +# ------------------------------------------------------------------ + +class ARCHIPACK_OT_fence_curve_update(Operator): + bl_idname = "archipack.fence_curve_update" + bl_label = "Fence curve update" + bl_description = "Update fence data from curve" + bl_category = 'Archipack' + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(self, context): + return archipack_fence.filter(context.active_object) + + def draw(self, context): + layout = self.layout + row = layout.row() + row.label("Use Properties panel (N) to define parms", icon='INFO') + + def execute(self, context): + if context.mode == "OBJECT": + d = archipack_fence.datablock(context.active_object) + d.update_path(context) + return {'FINISHED'} + else: + self.report({'WARNING'}, "Archipack: Option only valid in Object mode") + return {'CANCELLED'} + + +class ARCHIPACK_OT_fence_from_curve(ArchipackCreateTool, Operator): + bl_idname = "archipack.fence_from_curve" + bl_label = "Fence curve" + bl_description = "Create a fence from a curve" + bl_category = 'Archipack' + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(self, context): + return context.active_object is not None and context.active_object.type == 'CURVE' + + def draw(self, context): + layout = self.layout + row = layout.row() + row.label("Use Properties panel (N) to define parms", icon='INFO') + + def create(self, context): + o = None + curve = context.active_object + for i, spline in enumerate(curve.data.splines): + bpy.ops.archipack.fence('INVOKE_DEFAULT', auto_manipulate=False) + o = context.active_object + d = archipack_fence.datablock(o) + d.auto_update = False + d.user_defined_spline = i + d.user_defined_path = curve.name + d.auto_update = True + return o + + def execute(self, context): + if context.mode == "OBJECT": + bpy.ops.object.select_all(action="DESELECT") + o = self.create(context) + if o is not None: + o.select = True + context.scene.objects.active = o + # self.manipulate() + return {'FINISHED'} + else: + self.report({'WARNING'}, "Archipack: Option only valid in Object mode") + return {'CANCELLED'} + +# ------------------------------------------------------------------ +# Define operator class to manipulate object +# ------------------------------------------------------------------ + + +class ARCHIPACK_OT_fence_manipulate(Operator): + bl_idname = "archipack.fence_manipulate" + bl_label = "Manipulate" + bl_description = "Manipulate" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(self, context): + return archipack_fence.filter(context.active_object) + + def invoke(self, context, event): + d = archipack_fence.datablock(context.active_object) + d.manipulable_invoke(context) + return {'FINISHED'} + + +# ------------------------------------------------------------------ +# Define operator class to load / save presets +# ------------------------------------------------------------------ + + +class ARCHIPACK_OT_fence_preset_menu(PresetMenuOperator, Operator): + bl_idname = "archipack.fence_preset_menu" + bl_label = "Fence Styles" + preset_subdir = "archipack_fence" + + +class ARCHIPACK_OT_fence_preset(ArchipackPreset, Operator): + """Add a Fence Preset""" + bl_idname = "archipack.fence_preset" + bl_label = "Add Fence Style" + preset_menu = "ARCHIPACK_OT_fence_preset_menu" + + @property + def blacklist(self): + return ['manipulators', 'n_parts', 'parts', 'user_defined_path', 'user_defined_spline'] + + +def register(): + bpy.utils.register_class(archipack_fence_material) + bpy.utils.register_class(archipack_fence_part) + bpy.utils.register_class(archipack_fence) + Mesh.archipack_fence = CollectionProperty(type=archipack_fence) + bpy.utils.register_class(ARCHIPACK_OT_fence_preset_menu) + bpy.utils.register_class(ARCHIPACK_PT_fence) + bpy.utils.register_class(ARCHIPACK_OT_fence) + bpy.utils.register_class(ARCHIPACK_OT_fence_preset) + bpy.utils.register_class(ARCHIPACK_OT_fence_manipulate) + bpy.utils.register_class(ARCHIPACK_OT_fence_from_curve) + bpy.utils.register_class(ARCHIPACK_OT_fence_curve_update) + + +def unregister(): + bpy.utils.unregister_class(archipack_fence_material) + bpy.utils.unregister_class(archipack_fence_part) + bpy.utils.unregister_class(archipack_fence) + del Mesh.archipack_fence + bpy.utils.unregister_class(ARCHIPACK_OT_fence_preset_menu) + bpy.utils.unregister_class(ARCHIPACK_PT_fence) + bpy.utils.unregister_class(ARCHIPACK_OT_fence) + bpy.utils.unregister_class(ARCHIPACK_OT_fence_preset) + bpy.utils.unregister_class(ARCHIPACK_OT_fence_manipulate) + bpy.utils.unregister_class(ARCHIPACK_OT_fence_from_curve) + bpy.utils.unregister_class(ARCHIPACK_OT_fence_curve_update) diff --git a/archipack/archipack_floor.py b/archipack/archipack_floor.py new file mode 100644 index 00000000..24917c16 --- /dev/null +++ b/archipack/archipack_floor.py @@ -0,0 +1,1190 @@ +# -*- coding:utf-8 -*- + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110- 1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +# + +# ---------------------------------------------------------- +# Base code inspired by JARCH Vis +# Original Author: Jacob Morris +# Author : Stephen Leger (s-leger) +# ---------------------------------------------------------- + +import bpy +from bpy.types import Operator, PropertyGroup, Mesh, Panel +from bpy.props import ( + BoolProperty, EnumProperty, FloatProperty, + IntProperty, CollectionProperty + ) +from random import uniform, randint +from math import tan, pi, sqrt +from mathutils import Vector +from .bmesh_utils import BmeshEdit as bmed +from .archipack_manipulator import Manipulable +from .archipack_preset import ArchipackPreset, PresetMenuOperator +from .archipack_object import ArchipackCreateTool, ArchipackObject + + +def create_flooring(if_tile, over_width, over_length, b_width, b_length, b_length2, is_length_vary, + length_vary, num_boards, space_l, space_w, spacing, t_width, t_length, is_offset, offset, + is_ran_offset, offset_vary, t_width2, is_width_vary, width_vary, max_boards, is_ran_thickness, + ran_thickness, th, hb_dir): + + # create siding + if if_tile == "1": # Tiles Regular + return tile_regular(over_width, over_length, t_width, t_length, spacing, is_offset, offset, + is_ran_offset, offset_vary, th) + elif if_tile == "2": # Large + Small + return tile_ls(over_width, over_length, t_width, t_length, spacing, th) + elif if_tile == "3": # Large + Many Small + return tile_lms(over_width, over_length, t_width, spacing, th) + elif if_tile == "4": # Hexagonal + return tile_hexagon(over_width, over_length, t_width2, spacing, th) + elif if_tile == "21": # Planks + return wood_regular(over_width, over_length, b_width, b_length, space_l, space_w, + is_length_vary, length_vary, + is_width_vary, width_vary, + is_offset, offset, + is_ran_offset, offset_vary, + max_boards, is_ran_thickness, + ran_thickness, th) + elif if_tile == "22": # Parquet + return wood_parquet(over_width, over_length, b_width, spacing, num_boards, th) + elif if_tile == "23": # Herringbone Parquet + return wood_herringbone(over_width, over_length, b_width, b_length2, spacing, th, hb_dir, True) + elif if_tile == "24": # Herringbone + return wood_herringbone(over_width, over_length, b_width, b_length2, spacing, th, hb_dir, False) + + return [], [] + + +def wood_herringbone(ow, ol, bw, bl, s, th, hb_dir, stepped): + verts = [] + faces = [] + an_45 = 0.5 * sqrt(2) + x, y, z = 0.0, 0.0, th + x_off, y_off = 0.0, 0.0 # used for finding farther forwards points when stepped + ang_s = s * an_45 + s45 = s / an_45 + + # step variables + if stepped: + x_off = an_45 * bw + y_off = an_45 * bw + + wid_off = an_45 * bl # offset from one end of the board to the other inline with width + len_off = an_45 * bl # offset from one end of the board to the other inline with length + w = bw / an_45 # width adjusted for 45 degree rotation + + # figure out starting position + if hb_dir == "1": + y = -wid_off + + elif hb_dir == "2": + x = ow + y = ol + wid_off + + elif hb_dir == "3": + x = -wid_off + y = ol + + elif hb_dir == "4": + x = ow + wid_off + + # loop going forwards + while (hb_dir == "1" and y < ol + wid_off) or (hb_dir == "2" and y > 0 - wid_off) or \ + (hb_dir == "3" and x < ow + wid_off) or (hb_dir == "4" and x > 0 - wid_off): + going_forwards = True + + # loop going right + while (hb_dir == "1" and x < ow) or (hb_dir == "2" and x > 0) or (hb_dir == "3" and y > 0 - y_off) or \ + (hb_dir == "4" and y < ol + y_off): + p = len(verts) + + # add verts + # forwards + verts.append((x, y, z)) + + if hb_dir == "1": + + if stepped and x != 0: + verts.append((x - x_off, y + y_off, z)) + else: + verts.append((x, y + w, z)) + + if going_forwards: + y += wid_off + else: + y -= wid_off + x += len_off + + verts.append((x, y, z)) + if stepped: + verts.append((x - x_off, y + y_off, z)) + x -= x_off - ang_s + if going_forwards: + y += y_off + ang_s + else: + y -= y_off + ang_s + else: + verts.append((x, y + w, z)) + x += s + + # backwards + elif hb_dir == "2": + + if stepped and x != ow: + verts.append((x + x_off, y - y_off, z)) + else: + verts.append((x, y - w, z)) + + if going_forwards: + y -= wid_off + else: + y += wid_off + x -= len_off + + verts.append((x, y, z)) + if stepped: + verts.append((x + x_off, y - y_off, z)) + x += x_off - ang_s + if going_forwards: + y -= y_off + ang_s + else: + y += y_off + ang_s + else: + verts.append((x, y - w, z)) + x -= s + # right + elif hb_dir == "3": + + if stepped and y != ol: + verts.append((x + y_off, y + x_off, z)) + else: + verts.append((x + w, y, z)) + + if going_forwards: + x += wid_off + else: + x -= wid_off + y -= len_off + + verts.append((x, y, z)) + if stepped: + verts.append((x + y_off, y + x_off, z)) + y += x_off - ang_s + if going_forwards: + x += y_off + ang_s + else: + x -= y_off + ang_s + else: + verts.append((x + w, y, z)) + y -= s + # left + else: + + if stepped and y != 0: + verts.append((x - y_off, y - x_off, z)) + else: + verts.append((x - w, y, z)) + + if going_forwards: + x -= wid_off + else: + x += wid_off + y += len_off + + verts.append((x, y, z)) + if stepped: + verts.append((x - y_off, y - x_off, z)) + y -= x_off - ang_s + if going_forwards: + x -= y_off + ang_s + else: + x += y_off + ang_s + else: + verts.append((x - w, y, z)) + y += s + + # faces + faces.append((p, p + 2, p + 3, p + 1)) + + # flip going_right + going_forwards = not going_forwards + x_off *= -1 + + # if not in forwards position, then move back before adjusting values for next row + if not going_forwards: + x_off = abs(x_off) + if hb_dir == "1": + y -= wid_off + if stepped: + y -= y_off + ang_s + elif hb_dir == "2": + y += wid_off + if stepped: + y += y_off + ang_s + elif hb_dir == "3": + x -= wid_off + if stepped: + x -= y_off + ang_s + else: + x += wid_off + if stepped: + x += y_off + ang_s + + # adjust forwards + if hb_dir == "1": + y += w + s45 + x = 0 + elif hb_dir == "2": + y -= w + s45 + x = ow + elif hb_dir == "3": + x += w + s45 + y = ol + else: + x -= w + s45 + y = 0 + + return verts, faces + + +def tile_ls(ow, ol, tw, tl, s, z): + """ + pattern: + _____ + | |_| + |___| + + x and y are axis of big one + """ + + verts = [] + faces = [] + + # big half size + hw = (tw / 2) - (s / 2) + hl = (tl / 2) - (s / 2) + # small half size + hws = (tw / 4) - (s / 2) + hls = (tl / 4) - (s / 2) + + # small, offset from big x,y + xo = 0.75 * tw + yo = 0.25 * tl + + # offset for pattern + rx = 2.5 * tw + ry = 0.5 * tl + + # width and a half of big + ow_x = ow + 0.5 * tw + ol_y = ol + 0.5 * tl + + # start pattern with big one + x = tw + y = -tl + + while y < ol_y: + + while x < ow_x: + + p = len(verts) + + # Large + x0 = max(0, x - hw) + y0 = max(0, y - hl) + x1 = min(ow, x + hw) + y1 = min(ol, y + hl) + if y1 > 0: + if x1 > 0 and x0 < ow and y0 < ol: + + verts.extend([(x0, y1, z), (x1, y1, z), (x1, y0, z), (x0, y0, z)]) + faces.append((p, p + 1, p + 2, p + 3)) + p = len(verts) + + # Small + x0 = x + xo - hws + y0 = y + yo - hls + x1 = min(ow, x + xo + hws) + + if x1 > 0 and x0 < ow and y0 < ol: + + y1 = min(ol, y + yo + hls) + verts.extend([(x0, y1, z), (x1, y1, z), (x1, y0, z), (x0, y0, z)]) + faces.append((p, p + 1, p + 2, p + 3)) + + x += rx + + y += ry + x = x % rx - tw + if x < -tw: + x += rx + + return verts, faces + + +def tile_hexagon(ow, ol, tw, s, z): + verts = [] + faces = [] + offset = False + + w = tw / 2 + y = 0.0 + h = w * tan(pi / 6) + r = sqrt((w * w) + (h * h)) + + while y < ol + tw: + if not offset: + x = 0.0 + else: + x = w + (s / 2) + + while x < ow + tw: + p = len(verts) + + verts.extend([(x + w, y + h, z), (x, y + r, z), (x - w, y + h, z), + (x - w, y - h, z), (x, y - r, z), (x + w, y - h, z)]) + faces.extend([(p, p + 1, p + 2, p + 3), (p + 3, p + 4, p + 5, p)]) + + x += tw + s + + y += r + h + s + offset = not offset + + return verts, faces + + +def tile_lms(ow, ol, tw, s, z): + verts = [] + faces = [] + small = True + + y = 0.0 + ref = (tw - s) / 2 + + while y < ol: + x = 0.0 + large = False + while x < ow: + if small: + x1 = min(x + ref, ow) + y1 = min(y + ref, ol) + p = len(verts) + verts.extend([(x, y1, z), (x, y, z)]) + verts.extend([(x1, y1, z), (x1, y, z)]) + faces.append((p, p + 1, p + 3, p + 2)) + x += ref + else: + if not large: + x1 = min(x + ref, ow) + for i in range(2): + y0 = y + i * (ref + s) + if x < ow and y0 < ol: + y1 = min(y0 + ref, ol) + p = len(verts) + verts.extend([(x, y1, z), (x, y0, z)]) + verts.extend([(x1, y1, z), (x1, y0, z)]) + faces.append((p, p + 1, p + 3, p + 2)) + x += ref + else: + x1 = min(x + tw, ow) + y1 = min(y + tw, ol) + p = len(verts) + verts.extend([(x, y1, z), (x, y, z)]) + verts.extend([(x1, y1, z), (x1, y, z)]) + faces.append((p, p + 1, p + 3, p + 2)) + x += tw + large = not large + x += s + if small: + y += ref + s + else: + y += tw + s + small = not small + + return verts, faces + + +def tile_regular(ow, ol, tw, tl, s, is_offset, offset, is_ran_offset, offset_vary, z): + verts = [] + faces = [] + off = False + o = 1 / (100 / offset) + y = 0.0 + + while y < ol: + + tw2 = 0 + if is_offset: + if is_ran_offset: + v = tw * 0.0049 * offset_vary + tw2 = uniform((tw / 2) - v, (tw / 2) + v) + elif off: + tw2 = o * tw + x = -tw2 + y1 = min(ol, y + tl) + + while x < ow: + p = len(verts) + x0 = max(0, x) + x1 = min(ow, x + tw) + + verts.extend([(x0, y1, z), (x0, y, z), (x1, y, z), (x1, y1, z)]) + faces.append((p, p + 1, p + 2, p + 3)) + x = x1 + s + + y += tl + s + off = not off + + return verts, faces + + +def wood_parquet(ow, ol, bw, s, num_boards, z): + verts = [] + faces = [] + x = 0.0 + + start_orient_length = True + + # figure board length + bl = (bw * num_boards) + (s * (num_boards - 1)) + while x < ow: + + y = 0.0 + + orient_length = start_orient_length + + while y < ol: + + if orient_length: + y0 = y + y1 = min(y + bl, ol) + + for i in range(num_boards): + + bx = x + i * (bw + s) + + if bx < ow and y < ol: + + # make sure board should be placed + x0 = bx + x1 = min(bx + bw, ow) + + p = len(verts) + verts.extend([(x0, y0, z), (x1, y0, z), (x1, y1, z), (x0, y1, z)]) + faces.append((p, p + 1, p + 2, p + 3)) + + else: + x0 = x + x1 = min(x + bl, ow) + + for i in range(num_boards): + + by = y + i * (bw + s) + + if x < ow and by < ol: + y0 = by + y1 = min(by + bw, ol) + p = len(verts) + + verts.extend([(x0, y0, z), (x1, y0, z), (x1, y1, z), (x0, y1, z)]) + faces.append((p, p + 1, p + 2, p + 3)) + + y += bl + s + + orient_length = not orient_length + + start_orient_length = not start_orient_length + + x += bl + s + + return verts, faces + + +def wood_regular(ow, ol, bw, bl, s_l, s_w, + is_length_vary, length_vary, + is_width_vary, width_vary, + is_offset, offset, + is_ran_offset, offset_vary, + max_boards, is_r_h, + r_h, th): + verts = [] + faces = [] + x = 0.0 + row = 0 + while x < ow: + + if is_width_vary: + v = bw * (width_vary / 100) * 0.499 + bw2 = uniform(bw / 2 - v, bw / 2 + v) + else: + bw2 = bw + + x1 = min(x + bw2, ow) + if is_offset: + if is_ran_offset: + v = bl * (offset_vary / 100) * 0.5 + y = -uniform(bl / 2 - v, bl / 2 + v) + else: + y = -(row % 2) * bl * (offset / 100) + else: + y = 0 + + row += 1 + counter = 1 + + while y < ol: + + z = th + + if is_r_h: + v = z * 0.5 * (r_h / 100) + z = uniform(z / 2 - v, z / 2 + v) + + bl2 = bl + + if is_length_vary: + if (counter >= max_boards): + bl2 = ol + else: + v = bl * (length_vary / 100) * 0.5 + bl2 = uniform(bl / 2 - v, bl / 2 + v) + + y0 = max(0, y) + y1 = min(y + bl2, ol) + + if y1 > y0: + p = len(verts) + + verts.extend([(x, y0, z), (x1, y0, z), (x1, y1, z), (x, y1, z)]) + faces.append((p, p + 1, p + 2, p + 3)) + + y += bl2 + s_l + + counter += 1 + + x += bw2 + s_w + + return verts, faces + + +def tile_grout(ow, ol, depth, th): + z = min(th - 0.001, max(0.001, th - depth)) + x = ow + y = ol + + verts = [(0.0, 0.0, 0.0), (0.0, 0.0, z), (x, 0.0, z), (x, 0.0, 0.0), + (0.0, y, 0.0), (0.0, y, z), (x, y, z), (x, y, 0.0)] + + faces = [(0, 3, 2, 1), (4, 5, 6, 7), (0, 1, 5, 4), + (1, 2, 6, 5), (3, 7, 6, 2), (0, 4, 7, 3)] + + return verts, faces + + +def update(self, context): + self.update(context) + + +class archipack_floor(ArchipackObject, Manipulable, PropertyGroup): + tile_types = EnumProperty( + items=( + ("1", "Tiles", ""), + ("2", "Large + Small", ""), + ("3", "Large + Many Small", ""), + ("4", "Hexagonal", ""), + ("21", "Planks", ""), + ("22", "Parquet", ""), + ("23", "Herringbone Parquet", ""), + ("24", "Herringbone", "") + ), + default="1", + description="Tile Type", + update=update, + name="") + b_length_s = FloatProperty( + name="Board Length", + min=0.01, + default=2.0, + unit='LENGTH', subtype='DISTANCE', + description="Board Length", + update=update) + hb_direction = EnumProperty( + items=( + ("1", "Forwards (+y)", ""), + ("2", "Backwards (-y)", ""), + ("3", "Right (+x)", ""), + ("4", "Left (-x)", "") + ), + name="Direction", + description="Herringbone Direction", + update=update) + thickness = FloatProperty( + name="Floor Thickness", + min=0.01, + default=0.1, + unit='LENGTH', subtype='DISTANCE', + description="Thickness Of Flooring", + update=update) + num_boards = IntProperty( + name="# Of Boards", + min=2, + max=6, + default=4, + description="Number Of Boards In Square", + update=update) + space_l = FloatProperty( + name="Length Spacing", + min=0.001, + default=0.005, + step=0.01, + unit='LENGTH', subtype='DISTANCE', + description="Space Between Boards Length Ways", + update=update) + space_w = FloatProperty( + name="Width Spacing", + min=0.001, + default=0.005, + step=0.01, + unit='LENGTH', subtype='DISTANCE', + description="Space Between Boards Width Ways", + update=update) + spacing = FloatProperty( + name="Spacing", + min=0.001, + default=0.005, + step=0.01, + unit='LENGTH', subtype='DISTANCE', + description="Space Between Tiles/Boards", + update=update) + is_bevel = BoolProperty( + name="Bevel?", + default=False, + update=update) + bevel_res = IntProperty( + name="Bevel Resolution", + min=1, + max=10, + default=1, + update=update) + bevel_amo = FloatProperty( + name="Bevel Amount", + min=0.001, + default=0.0015, + step=0.01, + unit='LENGTH', subtype='DISTANCE', + description="Bevel Amount", + update=update) + is_ran_thickness = BoolProperty( + name="Random Thickness?", + default=False, + update=update) + ran_thickness = FloatProperty( + name="Thickness Variance", + min=0.1, + max=100.0, + default=50.0, + subtype="PERCENTAGE", + update=update) + is_floor_bottom = BoolProperty( + name="Floor bottom", + default=True, + update=update) + t_width = FloatProperty( + name="Tile Width", + min=0.01, + default=0.3, + unit='LENGTH', subtype='DISTANCE', + description="Tile Width", + update=update) + t_length = FloatProperty( + name="Tile Length", + min=0.01, + default=0.3, + unit='LENGTH', subtype='DISTANCE', + description="Tile Length", + update=update) + is_grout = BoolProperty( + name="Grout", + default=False, + description="Enable grout", + update=update) + grout_depth = FloatProperty( + name="Grout Depth", + min=0.001, + default=0.005, + step=0.01, + unit='LENGTH', subtype='DISTANCE', + description="Grout Depth", + update=update) + is_offset = BoolProperty( + name="Offset ?", + default=False, + description="Offset Rows", + update=update) + offset = FloatProperty( + name="Offset", + min=0.001, + max=100.0, + default=50.0, + subtype="PERCENTAGE", + description="Tile Offset Amount", + update=update) + is_random_offset = BoolProperty( + name="Random Offset?", + default=False, + description="Offset Tile Rows Randomly", + update=update) + offset_vary = FloatProperty( + name="Offset Variance", + min=0.001, + max=100.0, + default=50.0, + subtype="PERCENTAGE", + description="Offset Variance", + update=update) + t_width_s = FloatProperty( + name="Small Tile Width", + min=0.02, + default=0.2, + unit='LENGTH', subtype='DISTANCE', + description="Small Tile Width", + update=update) + over_width = FloatProperty( + name="Overall Width", + min=0.02, + default=4, + unit='LENGTH', subtype='DISTANCE', + description="Overall Width", + update=update) + over_length = FloatProperty( + name="Overall Length", + min=0.02, + default=4, + unit='LENGTH', subtype='DISTANCE', + description="Overall Length", + update=update) + b_width = FloatProperty( + name="Board Width", + min=0.01, + default=0.2, + unit='LENGTH', subtype='DISTANCE', + description="Board Width", + update=update) + b_length = FloatProperty( + name="Board Length", + min=0.01, + default=0.8, + unit='LENGTH', subtype='DISTANCE', + description="Board Length", + update=update) + is_length_vary = BoolProperty( + name="Vary Length?", + default=False, + description="Vary Lengths?", + update=update) + length_vary = FloatProperty( + name="Length Variance", + min=1.00, + max=100.0, + default=50.0, + subtype="PERCENTAGE", + description="Length Variance", + update=update) + max_boards = IntProperty( + name="Max # Of Boards", + min=2, + default=2, + description="Maximum Number Of Boards Possible In One Length", + update=update) + is_width_vary = BoolProperty( + name="Vary Width?", + default=False, + description="Vary Widths?", + update=update) + width_vary = FloatProperty( + name="Width Variance", + min=1.00, + max=100.0, + default=50.0, + subtype="PERCENTAGE", + description="Width Variance", + update=update) + is_mat_vary = BoolProperty( + name="Vary Material?", + default=False, + description="Vary Material indexes", + update=update) + mat_vary = IntProperty( + name="#variations", + min=1, + max=10, + default=1, + description="Material index maxi", + update=update) + auto_update = BoolProperty( + options={'SKIP_SAVE'}, + default=True, + update=update + ) + + def setup_manipulators(self): + if len(self.manipulators) < 1: + # add manipulator for x property + s = self.manipulators.add() + s.prop1_name = "over_width" + # s.prop2_name = "x" + s.type_key = 'SIZE' + + # add manipulator for y property + s = self.manipulators.add() + s.prop1_name = "over_length" + # s.prop2_name = "y" + s.type_key = 'SIZE' + + def update(self, context): + + o = self.find_in_selection(context, self.auto_update) + + if o is None: + return + + self.setup_manipulators() + + verts, faces = create_flooring(self.tile_types, self.over_width, + self.over_length, self.b_width, self.b_length, self.b_length_s, + self.is_length_vary, self.length_vary, self.num_boards, self.space_l, + self.space_w, self.spacing, self.t_width, self.t_length, self.is_offset, + self.offset, self.is_random_offset, self.offset_vary, self.t_width_s, + self.is_width_vary, self.width_vary, self.max_boards, self.is_ran_thickness, + self.ran_thickness, self.thickness, self.hb_direction) + + if self.is_mat_vary: + # hexagon made of 2 faces + if self.tile_types == '4': + matids = [] + for i in range(int(len(faces) / 2)): + id = randint(1, self.mat_vary) + matids.extend([id, id]) + else: + matids = [randint(1, self.mat_vary) for i in faces] + else: + matids = [1 for i in faces] + + uvs = [[(0, 0), (0, 1), (1, 1), (1, 0)] for i in faces] + + bmed.buildmesh(context, + o, + verts, + faces, + matids=matids, + uvs=uvs, + weld=False, + auto_smooth=False) + + # cut hexa and herringbone wood + # disable when boolean modifier is found + enable_bissect = True + for m in o.modifiers: + if m.type == 'BOOLEAN': + enable_bissect = False + + if enable_bissect and self.tile_types in ('4', '23', '24'): + bmed.bissect(context, o, Vector((0, 0, 0)), Vector((0, -1, 0))) + # Up + bmed.bissect(context, o, Vector((0, self.over_length, 0)), Vector((0, 1, 0))) + # left + bmed.bissect(context, o, Vector((0, 0, 0)), Vector((-1, 0, 0))) + # right + bmed.bissect(context, o, Vector((self.over_width, 0, 0)), Vector((1, 0, 0))) + + if self.is_bevel: + bevel = self.bevel_amo + else: + bevel = 0 + + if self.is_grout: + th = min(self.grout_depth + bevel, self.thickness - 0.001) + bottom = th + else: + th = self.thickness + bottom = 0 + + bmed.solidify(context, + o, + th, + floor_bottom=( + self.is_floor_bottom and + self.is_ran_thickness and + self.tile_types in ('21') + ), + altitude=bottom) + + # bevel mesh + if self.is_bevel: + bmed.bevel(context, o, self.bevel_amo, segments=self.bevel_res) + + # create grout + if self.is_grout: + verts, faces = tile_grout(self.over_width, self.over_length, self.grout_depth, self.thickness) + matids = [0 for i in faces] + uvs = [[(0, 0), (0, 1), (1, 1), (1, 0)] for i in faces] + bmed.addmesh(context, + o, + verts, + faces, + matids=matids, + uvs=uvs, + weld=False, + auto_smooth=False) + + x, y = self.over_width, self.over_length + self.manipulators[0].set_pts([(0, 0, 0), (x, 0, 0), (1, 0, 0)]) + self.manipulators[1].set_pts([(0, 0, 0), (0, y, 0), (-1, 0, 0)]) + + self.restore_context(context) + + +class ARCHIPACK_PT_floor(Panel): + bl_idname = "ARCHIPACK_PT_floor" + bl_label = "Flooring" + bl_space_type = "VIEW_3D" + bl_region_type = "UI" + bl_category = "Archipack" + + @classmethod + def poll(cls, context): + # ensure your object panel only show when active object is the right one + return archipack_floor.filter(context.active_object) + + def draw(self, context): + o = context.active_object + if not archipack_floor.filter(o): + return + layout = self.layout + + # retrieve datablock of your object + props = archipack_floor.datablock(o) + + # Manipulate mode operator + layout.operator('archipack.floor_manipulate', icon='HAND') + + box = layout.box() + row = box.row(align=True) + + # Presets operators + row.operator("archipack.floor_preset_menu", + text=bpy.types.ARCHIPACK_OT_floor_preset_menu.bl_label) + row.operator("archipack.floor_preset", + text="", + icon='ZOOMIN') + row.operator("archipack.floor_preset", + text="", + icon='ZOOMOUT').remove_active = True + + layout.prop(props, "tile_types", icon="OBJECT_DATA") + + layout.separator() + + layout.prop(props, "over_width") + layout.prop(props, "over_length") + layout.separator() + + # width and lengths + layout.prop(props, "thickness") + + type = int(props.tile_types) + + if type > 20: + # Wood types + layout.prop(props, "b_width") + else: + # Tiles types + if type != 4: + # Not hexagonal + layout.prop(props, "t_width") + layout.prop(props, "t_length") + else: + layout.prop(props, "t_width_s") + + # Herringbone + if type in (23, 24): + layout.prop(props, "b_length_s") + layout.prop(props, "hb_direction") + + # Parquet + if type == 22: + layout.prop(props, "num_boards") + + # Planks + if type == 21: + layout.prop(props, "b_length") + layout.prop(props, "space_w") + layout.prop(props, "space_l") + + layout.separator() + layout.prop(props, "is_length_vary", icon="NLA") + if props.is_length_vary: + layout.prop(props, "length_vary") + layout.prop(props, "max_boards") + + layout.separator() + layout.prop(props, "is_width_vary", icon="UV_ISLANDSEL") + if props.is_width_vary: + layout.prop(props, "width_vary") + + layout.separator() + layout.prop(props, "is_ran_thickness", icon="RNDCURVE") + if props.is_ran_thickness: + layout.prop(props, "ran_thickness") + layout.prop(props, "is_floor_bottom", icon="MOVE_DOWN_VEC") + else: + layout.prop(props, "spacing") + + # Planks and tiles + if type in (1, 21): + layout.separator() + layout.prop(props, "is_offset", icon="OOPS") + if props.is_offset: + layout.prop(props, "is_random_offset", icon="NLA") + if not props.is_random_offset: + layout.prop(props, "offset") + else: + layout.prop(props, "offset_vary") + + # bevel + layout.separator() + layout.prop(props, "is_bevel", icon="MOD_BEVEL") + if props.is_bevel: + layout.prop(props, "bevel_res", icon="OUTLINER_DATA_CURVE") + layout.prop(props, "bevel_amo") + + # Grout + layout.separator() + layout.prop(props, "is_grout", icon="OBJECT_DATA") + if props.is_grout: + layout.prop(props, "grout_depth") + + layout.separator() + layout.prop(props, "is_mat_vary", icon="MATERIAL") + if props.is_mat_vary: + layout.prop(props, "mat_vary") + + +class ARCHIPACK_OT_floor(ArchipackCreateTool, Operator): + bl_idname = "archipack.floor" + bl_label = "Floor" + bl_description = "Create Floor" + bl_category = 'Archipack' + bl_options = {'REGISTER', 'UNDO'} + + def create(self, context): + + # Create an empty mesh datablock + m = bpy.data.meshes.new("Floor") + + # Create an object using the mesh datablock + o = bpy.data.objects.new("Floor", m) + + # Add your properties on mesh datablock + d = m.archipack_floor.add() + + # Link object into scene + context.scene.objects.link(o) + + # select and make active + o.select = True + context.scene.objects.active = o + + # Load preset into datablock + self.load_preset(d) + + # add a material + self.add_material(o) + return o + + def execute(self, context): + if context.mode == "OBJECT": + bpy.ops.object.select_all(action="DESELECT") + o = self.create(context) + o.location = bpy.context.scene.cursor_location + o.select = True + context.scene.objects.active = o + + # Start manipulate mode + self.manipulate() + return {'FINISHED'} + else: + self.report({'WARNING'}, "Archipack: Option only valid in Object mode") + return {'CANCELLED'} + + +class ARCHIPACK_OT_floor_preset_menu(PresetMenuOperator, Operator): + bl_idname = "archipack.floor_preset_menu" + bl_label = "Floor preset" + preset_subdir = "archipack_floor" + + +class ARCHIPACK_OT_floor_preset(ArchipackPreset, Operator): + """Add a Floor Preset""" + bl_idname = "archipack.floor_preset" + bl_label = "Add Floor preset" + preset_menu = "ARCHIPACK_OT_floor_preset_menu" + + @property + def blacklist(self): + return ['manipulators', 'over_length', 'over_width'] + + +class ARCHIPACK_OT_floor_manipulate(Operator): + bl_idname = "archipack.floor_manipulate" + bl_label = "Manipulate" + bl_description = "Manipulate" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(self, context): + return archipack_floor.filter(context.active_object) + + def invoke(self, context, event): + d = archipack_floor.datablock(context.active_object) + d.manipulable_invoke(context) + return {'FINISHED'} + + +def register(): + bpy.utils.register_class(archipack_floor) + Mesh.archipack_floor = CollectionProperty(type=archipack_floor) + bpy.utils.register_class(ARCHIPACK_PT_floor) + bpy.utils.register_class(ARCHIPACK_OT_floor) + bpy.utils.register_class(ARCHIPACK_OT_floor_preset_menu) + bpy.utils.register_class(ARCHIPACK_OT_floor_preset) + bpy.utils.register_class(ARCHIPACK_OT_floor_manipulate) + + +def unregister(): + bpy.utils.unregister_class(archipack_floor) + del Mesh.archipack_floor + bpy.utils.unregister_class(ARCHIPACK_PT_floor) + bpy.utils.unregister_class(ARCHIPACK_OT_floor) + bpy.utils.unregister_class(ARCHIPACK_OT_floor_preset_menu) + bpy.utils.unregister_class(ARCHIPACK_OT_floor_preset) + bpy.utils.unregister_class(ARCHIPACK_OT_floor_manipulate) diff --git a/archipack/archipack_gl.py b/archipack/archipack_gl.py new file mode 100644 index 00000000..fc1f8c03 --- /dev/null +++ b/archipack/archipack_gl.py @@ -0,0 +1,1228 @@ +# -*- coding:utf-8 -*- + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +# + +# ---------------------------------------------------------- +# Author: Stephen Leger (s-leger) +# +# ---------------------------------------------------------- + +import bgl +import blf +import bpy +from math import sin, cos, atan2, pi +from mathutils import Vector, Matrix +from bpy_extras import view3d_utils, object_utils + + +# ------------------------------------------------------------------ +# Define Gl Handle types +# ------------------------------------------------------------------ + + +class DefaultColorScheme: + """ + Font sizes and basic colour scheme + default to this when not found in addon prefs + Colors are FloatVectorProperty of size 4 and type COLOR_GAMMA + """ + feedback_size_main = 16 + feedback_size_title = 14 + feedback_size_shortcut = 11 + feedback_colour_main = (0.95, 0.95, 0.95, 1.0) + feedback_colour_key = (0.67, 0.67, 0.67, 1.0) + feedback_colour_shortcut = (0.51, 0.51, 0.51, 1.0) + feedback_shortcut_area = (0, 0.4, 0.6, 0.2) + feedback_title_area = (0, 0.4, 0.6, 0.5) + + +""" + # Addon prefs template + + feedback_size_main = IntProperty( + name="Main", + description="Main title font size (pixels)", + min=2, + default=16 + ) + feedback_size_title = IntProperty( + name="Title", + description="Tool name font size (pixels)", + min=2, + default=14 + ) + feedback_size_shortcut = IntProperty( + name="Shortcut", + description="Shortcuts font size (pixels)", + min=2, + default=11 + ) + feedback_shortcut_area = FloatVectorProperty( + name="Background Shortcut", + description="Shortcut area background color", + subtype='COLOR_GAMMA', + default=(0, 0.4, 0.6, 0.2), + size=4, + min=0, max=1 + ) + feedback_title_area = FloatVectorProperty( + name="Background Main", + description="Title area background color", + subtype='COLOR_GAMMA', + default=(0, 0.4, 0.6, 0.5), + size=4, + min=0, max=1 + ) + feedback_colour_main = FloatVectorProperty( + name="Font Main", + description="Title color", + subtype='COLOR_GAMMA', + default=(0.95, 0.95, 0.95, 1.0), + size=4, + min=0, max=1 + ) + feedback_colour_key = FloatVectorProperty( + name="Font Shortcut key", + description="KEY label color", + subtype='COLOR_GAMMA', + default=(0.67, 0.67, 0.67, 1.0), + size=4, + min=0, max=1 + ) + feedback_colour_shortcut = FloatVectorProperty( + name="Font Shortcut hint", + description="Shortcuts text color", + subtype='COLOR_GAMMA', + default=(0.51, 0.51, 0.51, 1.0), + size=4, + min=0, max=1 + ) + + def draw(self, context): + layout = self.layout + box = layout.box() + row = box.row() + split = row.split(percentage=0.5) + col = split.column() + col.label(text="Colors:") + row = col.row(align=True) + row.prop(self, "feedback_title_area") + row = col.row(align=True) + row.prop(self, "feedback_shortcut_area") + row = col.row(align=True) + row.prop(self, "feedback_colour_main") + row = col.row(align=True) + row.prop(self, "feedback_colour_key") + row = col.row(align=True) + row.prop(self, "feedback_colour_shortcut") + col = split.column() + col.label(text="Font size:") + col.prop(self, "feedback_size_main") + col.prop(self, "feedback_size_title") + col.prop(self, "feedback_size_shortcut") +""" + + +# @TODO: +# 1 Make a clear separation of 2d (pixel position) and 3d (world position) +# modes way to set gl coords +# 2 Unify methods to set points - currently set_pts, set_pos ... +# 3 Put all Gl part in a sub module as it may be used by other devs +# as gl toolkit abstraction for screen feedback +# 4 Implement cursor badges (np_station sample) +# 5 Define a clear color scheme so it is easy to customize +# 6 Allow different arguments for each classes like +# eg: for line p0 p1, p0 and vector (p1-p0) +# raising exceptions when incomplete +# 7 Use correct words, normal is not realy a normal +# but a perpendicular +# May be hard code more shapes ? +# Fine tuned text styles with shadows and surronding boxes / backgrounds +# Extending tests to hdr screens, ultra wide ones and so on +# Circular handle, handle styling (only border, filling ...) + +# Keep point 3 in mind while doing this, to keep it simple and easy to use +# Take inspiration from other's feed back systems, talk to other devs +# and find who actually work on bgl future for 2.8 release + + +class Gl(): + """ + handle 3d -> 2d gl drawing + d : dimensions + 3 to convert pos from 3d + 2 to keep pos as 2d absolute screen position + """ + def __init__(self, + d=3, + colour=(0.0, 0.0, 0.0, 1.0)): + # nth dimensions of input coords 3=word coords 2=pixel screen coords + self.d = d + self.pos_2d = Vector((0, 0)) + self.colour_inactive = colour + + @property + def colour(self): + return self.colour_inactive + + def position_2d_from_coord(self, context, coord, render=False): + """ coord given in local input coordsys + """ + if self.d == 2: + return coord + if render: + return self.get_render_location(context, coord) + region = context.region + rv3d = context.region_data + loc = view3d_utils.location_3d_to_region_2d(region, rv3d, coord, self.pos_2d) + return loc + + def get_render_location(self, context, coord): + scene = context.scene + co_2d = object_utils.world_to_camera_view(scene, scene.camera, coord) + # Get pixel coords + render_scale = scene.render.resolution_percentage / 100 + render_size = (int(scene.render.resolution_x * render_scale), + int(scene.render.resolution_y * render_scale)) + return [round(co_2d.x * render_size[0]), round(co_2d.y * render_size[1])] + + def _end(self): + bgl.glEnd() + bgl.glPopAttrib() + bgl.glLineWidth(1) + bgl.glDisable(bgl.GL_BLEND) + bgl.glColor4f(0.0, 0.0, 0.0, 1.0) + + +class GlText(Gl): + + def __init__(self, + d=3, + label="", + value=None, + precision=2, + unit_mode='AUTO', + unit_type='SIZE', + dimension=1, + angle=0, + font_size=12, + colour=(1, 1, 1, 1), + z_axis=Vector((0, 0, 1))): + """ + d: [2|3] coords type: 2 for coords in screen pixels, 3 for 3d world location + label : string label + value : float value (will add unit according following settings) + precision : integer rounding for values + dimension : [1 - 3] nth dimension of unit (single, square, cubic) + unit_mode : ['AUTO','METER','CENTIMETER','MILIMETER','FEET','INCH','RADIANS','DEGREE'] + unit type to use to postfix values + auto use scene units setup + unit_type : ['SIZE','ANGLE'] + unit type to add to value + angle : angle to rotate text + + """ + self.z_axis = z_axis + # text, add as prefix to value + self.label = label + # value with unit related + self.value = value + self.precision = precision + self.dimension = dimension + self.unit_type = unit_type + self.unit_mode = unit_mode + + self.font_size = font_size + self.angle = angle + Gl.__init__(self, d) + self.colour_inactive = colour + # store text with units + self._text = "" + + def text_size(self, context): + """ + overall on-screen size in pixels + """ + dpi, font_id = context.user_preferences.system.dpi, 0 + if self.angle != 0: + blf.enable(font_id, blf.ROTATION) + blf.rotation(font_id, self.angle) + blf.aspect(font_id, 1.0) + blf.size(font_id, self.font_size, dpi) + x, y = blf.dimensions(font_id, self.text) + if self.angle != 0: + blf.disable(font_id, blf.ROTATION) + return Vector((x, y)) + + @property + def pts(self): + return [self.pos_3d] + + @property + def text(self): + s = self.label + self._text + return s.strip() + + def add_units(self, context): + if self.value is None: + return "" + if self.unit_type == 'ANGLE': + scale = 1 + else: + scale = context.scene.unit_settings.scale_length + + val = self.value * scale + mode = self.unit_mode + if mode == 'AUTO': + if self.unit_type == 'ANGLE': + mode = context.scene.unit_settings.system_rotation + else: + if context.scene.unit_settings.system == "IMPERIAL": + if round(val * (3.2808399 ** self.dimension), 2) >= 1.0: + mode = 'FEET' + else: + mode = 'INCH' + elif context.scene.unit_settings.system == "METRIC": + if round(val, 2) >= 1.0: + mode = 'METER' + else: + if round(val, 2) >= 0.01: + mode = 'CENTIMETER' + else: + mode = 'MILIMETER' + # convert values + if mode == 'METER': + unit = "m" + elif mode == 'CENTIMETER': + val *= (100 ** self.dimension) + unit = "cm" + elif mode == 'MILIMETER': + val *= (1000 ** self.dimension) + unit = 'mm' + elif mode == 'INCH': + val *= (39.3700787 ** self.dimension) + unit = "in" + elif mode == 'FEET': + val *= (3.2808399 ** self.dimension) + unit = "ft" + elif mode == 'RADIANS': + unit = "" + elif mode == 'DEGREES': + val = self.value / pi * 180 + unit = "°" + else: + unit = "" + if self.dimension == 2: + unit += "\u00b2" # Superscript two + elif self.dimension == 3: + unit += "\u00b3" # Superscript three + + fmt = "%1." + str(self.precision) + "f " + unit + return fmt % val + + def set_pos(self, context, value, pos_3d, direction, angle=0, normal=Vector((0, 0, 1))): + self.up_axis = direction.normalized() + self.c_axis = self.up_axis.cross(normal) + self.pos_3d = pos_3d + self.value = value + self.angle = angle + self._text = self.add_units(context) + + def draw(self, context, render=False): + self.render = render + x, y = self.position_2d_from_coord(context, self.pts[0], render) + # dirty fast assignment + dpi, font_id = context.user_preferences.system.dpi, 0 + bgl.glColor4f(*self.colour) + if self.angle != 0: + blf.enable(font_id, blf.ROTATION) + blf.rotation(font_id, self.angle) + blf.size(font_id, self.font_size, dpi) + blf.position(font_id, x, y, 0) + blf.draw(font_id, self.text) + if self.angle != 0: + blf.disable(font_id, blf.ROTATION) + + +class GlBaseLine(Gl): + + def __init__(self, + d=3, + width=1, + style=bgl.GL_LINE, + closed=False): + Gl.__init__(self, d) + # default line width + self.width = width + # default line style + self.style = style + # allow closed lines + self.closed = False + + def draw(self, context, render=False): + """ + render flag when rendering + """ + bgl.glPushAttrib(bgl.GL_ENABLE_BIT) + if self.style == bgl.GL_LINE_STIPPLE: + bgl.glLineStipple(1, 0x9999) + bgl.glEnable(self.style) + bgl.glEnable(bgl.GL_BLEND) + if render: + # enable anti-alias on lines + bgl.glEnable(bgl.GL_LINE_SMOOTH) + bgl.glColor4f(*self.colour) + bgl.glLineWidth(self.width) + if self.closed: + bgl.glBegin(bgl.GL_LINE_LOOP) + else: + bgl.glBegin(bgl.GL_LINE_STRIP) + + for pt in self.pts: + x, y = self.position_2d_from_coord(context, pt, render) + bgl.glVertex2f(x, y) + self._end() + + +class GlLine(GlBaseLine): + """ + 2d/3d Line + """ + def __init__(self, d=3, p=None, v=None, p0=None, p1=None, z_axis=None): + """ + d=3 use 3d coords, d=2 use 2d pixels coords + Init by either + p: Vector or tuple origin + v: Vector or tuple size and direction + or + p0: Vector or tuple 1 point location + p1: Vector or tuple 2 point location + Will convert any into Vector 3d + both optionnals + """ + if p is not None and v is not None: + self.p = Vector(p) + self.v = Vector(v) + elif p0 is not None and p1 is not None: + self.p = Vector(p0) + self.v = Vector(p1) - self.p + else: + self.p = Vector((0, 0, 0)) + self.v = Vector((0, 0, 0)) + if z_axis is not None: + self.z_axis = z_axis + else: + self.z_axis = Vector((0, 0, 1)) + GlBaseLine.__init__(self, d) + + @property + def p0(self): + return self.p + + @property + def p1(self): + return self.p + self.v + + @p0.setter + def p0(self, p0): + """ + Note: setting p0 + move p0 only + """ + p1 = self.p1 + self.p = Vector(p0) + self.v = p1 - p0 + + @p1.setter + def p1(self, p1): + """ + Note: setting p1 + move p1 only + """ + self.v = Vector(p1) - self.p + + @property + def length(self): + return self.v.length + + @property + def angle(self): + return atan2(self.v.y, self.v.x) + + @property + def cross(self): + """ + Vector perpendicular on plane defined by z_axis + lie on the right side + p1 + |--x + p0 + """ + return self.v.cross(self.z_axis) + + def normal(self, t=0): + """ + Line perpendicular on plane defined by z_axis + lie on the right side + p1 + |--x + p0 + """ + n = GlLine() + n.p = self.lerp(t) + n.v = self.cross + return n + + def sized_normal(self, t, size): + """ + GlLine perpendicular on plane defined by z_axis and of given size + positionned at t in current line + lie on the right side + p1 + |--x + p0 + """ + n = GlLine() + n.p = self.lerp(t) + n.v = size * self.cross.normalized() + return n + + def lerp(self, t): + """ + Interpolate along segment + t parameter [0, 1] where 0 is start of arc and 1 is end + """ + return self.p + self.v * t + + def offset(self, offset): + """ + offset > 0 on the right part + """ + self.p += offset * self.cross.normalized() + + def point_sur_segment(self, pt): + """ point_sur_segment (2d) + point: Vector 3d + t: param t de l'intersection sur le segment courant + d: distance laterale perpendiculaire positif a droite + """ + dp = (pt - self.p).to_2d() + v2d = self.v.to_2d() + dl = v2d.length + d = (self.v.x * dp.y - self.v.y * dp.x) / dl + t = (v2d * dp) / (dl * dl) + return t > 0 and t < 1, d, t + + @property + def pts(self): + return [self.p0, self.p1] + + +class GlCircle(GlBaseLine): + + def __init__(self, + d=3, + radius=0, + center=Vector((0, 0, 0)), + z_axis=Vector((0, 0, 1))): + + self.r = radius + self.c = center + z = z_axis + + if z.z < 1: + x = z.cross(Vector((0, 0, 1))) + y = x.cross(z) + else: + x = Vector((1, 0, 0)) + y = Vector((0, 1, 0)) + + self.rM = Matrix([ + Vector((x.x, y.x, z.x)), + Vector((x.y, y.y, z.y)), + Vector((x.z, y.z, z.z)) + ]) + self.z_axis = z + self.a0 = 0 + self.da = 2 * pi + GlBaseLine.__init__(self, d) + + def lerp(self, t): + """ + Linear interpolation + """ + a = self.a0 + t * self.da + return self.c + self.rM * Vector((self.r * cos(a), self.r * sin(a), 0)) + + @property + def pts(self): + n_pts = max(1, int(round(abs(self.da) / pi * 30, 0))) + t_step = 1 / n_pts + return [self.lerp(i * t_step) for i in range(n_pts + 1)] + + +class GlArc(GlCircle): + + def __init__(self, + d=3, + radius=0, + center=Vector((0, 0, 0)), + z_axis=Vector((0, 0, 1)), + a0=0, + da=0): + """ + a0 and da arguments are in radians + a0 = 0 on the x+ axis side + a0 = pi on the x- axis side + da > 0 CCW contrary-clockwise + da < 0 CW clockwise + """ + GlCircle.__init__(self, d, radius, center, z_axis) + self.da = da + self.a0 = a0 + + @property + def length(self): + return self.r * abs(self.da) + + def normal(self, t=0): + """ + perpendicular line always on the right side + """ + n = GlLine(d=self.d, z_axis=self.z_axis) + n.p = self.lerp(t) + if self.da < 0: + n.v = self.c - n.p + else: + n.v = n.p - self.c + return n + + def sized_normal(self, t, size): + n = GlLine(d=self.d, z_axis=self.z_axis) + n.p = self.lerp(t) + if self.da < 0: + n.v = size * (self.c - n.p).normalized() + else: + n.v = size * (n.p - self.c).normalized() + return n + + def tangeant(self, t, length): + a = self.a0 + t * self.da + ca = cos(a) + sa = sin(a) + n = GlLine(d=self.d, z_axis=self.z_axis) + n.p = self.c + self.rM * Vector((self.r * ca, self.r * sa, 0)) + n.v = self.rM * Vector((length * sa, -length * ca, 0)) + if self.da > 0: + n.v = -n.v + return n + + def offset(self, offset): + """ + offset > 0 on the right part + """ + if self.da > 0: + radius = self.r + offset + else: + radius = self.r - offset + return GlArc(d=self.d, + radius=radius, + center=self.c, + a0=self.a0, + da=self.da, + z_axis=self.z_axis) + + +class GlPolygon(Gl): + + def __init__(self, + colour=(0.0, 0.0, 0.0, 1.0), + d=3): + + self.pts_3d = [] + Gl.__init__(self, d, colour) + + def set_pos(self, pts_3d): + self.pts_3d = pts_3d + + @property + def pts(self): + return self.pts_3d + + def draw(self, context, render=False): + """ + render flag when rendering + """ + self.render = render + bgl.glPushAttrib(bgl.GL_ENABLE_BIT) + bgl.glEnable(bgl.GL_BLEND) + if render: + # enable anti-alias on polygons + bgl.glEnable(bgl.GL_POLYGON_SMOOTH) + bgl.glColor4f(*self.colour) + bgl.glBegin(bgl.GL_POLYGON) + + for pt in self.pts: + x, y = self.position_2d_from_coord(context, pt, render) + bgl.glVertex2f(x, y) + self._end() + + +class GlRect(GlPolygon): + def __init__(self, + colour=(0.0, 0.0, 0.0, 1.0), + d=2): + GlPolygon.__init__(self, colour, d) + + def draw(self, context, render=False): + self.render = render + bgl.glPushAttrib(bgl.GL_ENABLE_BIT) + bgl.glEnable(bgl.GL_BLEND) + if render: + # enable anti-alias on polygons + bgl.glEnable(bgl.GL_POLYGON_SMOOTH) + bgl.glColor4f(*self.colour) + p0 = self.pts[0] + p1 = self.pts[1] + bgl.glRectf(p0.x, p0.y, p1.x, p1.y) + self._end() + + +class GlImage(Gl): + def __init__(self, + d=2, + image=None): + self.image = image + self.colour_inactive = (1, 1, 1, 1) + Gl.__init__(self, d) + self.pts_2d = [Vector((0, 0)), Vector((10, 10))] + + def set_pos(self, pts): + self.pts_2d = pts + + @property + def pts(self): + return self.pts_2d + + def draw(self, context, render=False): + if self.image is None: + return + bgl.glPushAttrib(bgl.GL_ENABLE_BIT) + p0 = self.pts[0] + p1 = self.pts[1] + bgl.glEnable(bgl.GL_BLEND) + bgl.glColor4f(*self.colour) + bgl.glRectf(p0.x, p0.y, p1.x, p1.y) + self.image.gl_load() + bgl.glEnable(bgl.GL_BLEND) + bgl.glBindTexture(bgl.GL_TEXTURE_2D, self.image.bindcode[0]) + bgl.glTexParameteri(bgl.GL_TEXTURE_2D, bgl.GL_TEXTURE_MIN_FILTER, bgl.GL_NEAREST) + bgl.glTexParameteri(bgl.GL_TEXTURE_2D, bgl.GL_TEXTURE_MAG_FILTER, bgl.GL_NEAREST) + bgl.glEnable(bgl.GL_TEXTURE_2D) + bgl.glBlendFunc(bgl.GL_SRC_ALPHA, bgl.GL_ONE_MINUS_SRC_ALPHA) + # bgl.glColor4f(1, 1, 1, 1) + bgl.glBegin(bgl.GL_QUADS) + bgl.glTexCoord2d(0, 0) + bgl.glVertex2d(p0.x, p0.y) + bgl.glTexCoord2d(0, 1) + bgl.glVertex2d(p0.x, p1.y) + bgl.glTexCoord2d(1, 1) + bgl.glVertex2d(p1.x, p1.y) + bgl.glTexCoord2d(1, 0) + bgl.glVertex2d(p1.x, p0.y) + bgl.glEnd() + self.image.gl_free() + bgl.glDisable(bgl.GL_TEXTURE_2D) + + +class GlPolyline(GlBaseLine): + def __init__(self, colour, d=3): + self.pts_3d = [] + GlBaseLine.__init__(self, d) + self.colour_inactive = colour + + def set_pos(self, pts_3d): + self.pts_3d = pts_3d + # self.pts_3d.append(pts_3d[0]) + + @property + def pts(self): + return self.pts_3d + + +class GlHandle(GlPolygon): + + def __init__(self, sensor_size, size, draggable=False, selectable=False, d=3): + """ + sensor_size : 2d size in pixels of sensor area + size : 3d size of handle + """ + GlPolygon.__init__(self, d=d) + self.colour_active = (1.0, 0.0, 0.0, 1.0) + self.colour_hover = (1.0, 1.0, 0.0, 1.0) + self.colour_normal = (1.0, 1.0, 1.0, 1.0) + self.colour_selected = (0.0, 0.0, 0.7, 1.0) + self.size = size + self.sensor_width = sensor_size + self.sensor_height = sensor_size + self.pos_3d = Vector((0, 0, 0)) + self.up_axis = Vector((0, 0, 0)) + self.c_axis = Vector((0, 0, 0)) + self.hover = False + self.active = False + self.draggable = draggable + self.selectable = selectable + self.selected = False + + def set_pos(self, context, pos_3d, direction, normal=Vector((0, 0, 1))): + self.up_axis = direction.normalized() + self.c_axis = self.up_axis.cross(normal) + self.pos_3d = pos_3d + self.pos_2d = self.position_2d_from_coord(context, self.sensor_center) + + def check_hover(self, pos_2d): + if self.draggable: + dp = pos_2d - self.pos_2d + self.hover = abs(dp.x) < self.sensor_width and abs(dp.y) < self.sensor_height + + @property + def sensor_center(self): + pts = self.pts + n = len(pts) + x, y, z = 0, 0, 0 + for pt in pts: + x += pt.x + y += pt.y + z += pt.z + return Vector((x / n, y / n, z / n)) + + @property + def pts(self): + raise NotImplementedError + + @property + def colour(self): + if self.render: + return self.colour_inactive + elif self.draggable: + if self.active: + return self.colour_active + elif self.hover: + return self.colour_hover + elif self.selected: + return self.colour_selected + return self.colour_normal + else: + return self.colour_inactive + + +class SquareHandle(GlHandle): + + def __init__(self, sensor_size, size, draggable=False, selectable=False): + GlHandle.__init__(self, sensor_size, size, draggable, selectable) + + @property + def pts(self): + n = self.up_axis + c = self.c_axis + if self.selected or self.hover or self.active: + scale = 1 + else: + scale = 0.5 + x = n * self.size * scale + y = c * self.size * scale + return [self.pos_3d - x - y, self.pos_3d + x - y, self.pos_3d + x + y, self.pos_3d - x + y] + + +class TriHandle(GlHandle): + + def __init__(self, sensor_size, size, draggable=False, selectable=False): + GlHandle.__init__(self, sensor_size, size, draggable, selectable) + + @property + def pts(self): + n = self.up_axis + c = self.c_axis + # does move sensitive area so disable for tri handle + # may implement sensor_center property to fix this + # if self.selected or self.hover or self.active: + scale = 1 + # else: + # scale = 0.5 + x = n * self.size * 4 * scale + y = c * self.size * scale + return [self.pos_3d - x + y, self.pos_3d - x - y, self.pos_3d] + + +class EditableText(GlText, GlHandle): + def __init__(self, sensor_size, size, draggable=False, selectable=False): + GlHandle.__init__(self, sensor_size, size, draggable, selectable) + GlText.__init__(self, colour=(0, 0, 0, 1)) + + def set_pos(self, context, value, pos_3d, direction, normal=Vector((0, 0, 1))): + self.up_axis = direction.normalized() + self.c_axis = self.up_axis.cross(normal) + self.pos_3d = pos_3d + self.value = value + self._text = self.add_units(context) + x, y = self.text_size(context) + self.pos_2d = self.position_2d_from_coord(context, pos_3d) + self.pos_2d.x += 0.5 * x + self.sensor_width, self.sensor_height = 0.5 * x, y + + @property + def sensor_center(self): + return self.pos_3d + + +class ThumbHandle(GlHandle): + + def __init__(self, size_2d, label, image=None, draggable=False, selectable=False, d=2): + GlHandle.__init__(self, size_2d, size_2d, draggable, selectable, d) + self.image = GlImage(image=image) + self.label = GlText(d=2, label=label.replace("_", " ").capitalize()) + self.frame = GlPolyline((1, 1, 1, 1), d=2) + self.frame.closed = True + self.size_2d = size_2d + self.sensor_width = 0.5 * size_2d.x + self.sensor_height = 0.5 * size_2d.y + self.colour_normal = (0.715, 0.905, 1, 0.9) + self.colour_hover = (1, 1, 1, 1) + + def set_pos(self, context, pos_2d): + """ + pos 2d is center !! + """ + self.pos_2d = pos_2d + ts = self.label.text_size(context) + self.label.pos_3d = pos_2d + Vector((-0.5 * ts.x, ts.y - 0.5 * self.size_2d.y)) + p0, p1 = self.pts + self.image.set_pos(self.pts) + self.frame.set_pos([p0, Vector((p1.x, p0.y)), p1, Vector((p0.x, p1.y))]) + + @property + def pts(self): + s = 0.5 * self.size_2d + return [self.pos_2d - s, self.pos_2d + s] + + @property + def sensor_center(self): + return self.pos_2d + 0.5 * self.size_2d + + def draw(self, context, render=False): + self.render = render + self.image.colour_inactive = self.colour + GlHandle.draw(self, context, render=False) + self.image.draw(context, render=False) + self.label.draw(context, render=False) + self.frame.draw(context, render=False) + + +class Screen(): + def __init__(self, margin): + self.margin = margin + + def size(self, context): + + system = context.user_preferences.system + w = context.region.width + h = context.region.height + y_min = self.margin + y_max = h - self.margin + x_min = self.margin + x_max = w - self.margin + if (system.use_region_overlap and + system.window_draw_method in {'TRIPLE_BUFFER', 'AUTOMATIC'}): + area = context.area + + for r in area.regions: + if r.type == 'TOOLS': + x_min += r.width + elif r.type == 'UI': + x_max -= r.width + return x_min, x_max, y_min, y_max + + +class FeedbackPanel(): + """ + Feed-back panel + inspired by np_station + """ + def __init__(self, title='Archipack'): + + prefs = self.get_prefs(bpy.context) + + self.main_title = GlText(d=2, + label=title + " : ", + font_size=prefs.feedback_size_main, + colour=prefs.feedback_colour_main + ) + self.title = GlText(d=2, + font_size=prefs.feedback_size_title, + colour=prefs.feedback_colour_main + ) + self.spacing = Vector(( + 0.5 * prefs.feedback_size_shortcut, + 0.5 * prefs.feedback_size_shortcut)) + self.margin = 50 + self.explanation = GlText(d=2, + font_size=prefs.feedback_size_shortcut, + colour=prefs.feedback_colour_main + ) + self.shortcut_area = GlPolygon(colour=prefs.feedback_shortcut_area, d=2) + self.title_area = GlPolygon(colour=prefs.feedback_title_area, d=2) + self.shortcuts = [] + self.on = False + self.show_title = True + self.show_main_title = True + # read only, when enabled, after draw() the top left coord of info box + self.top = Vector((0, 0)) + self.screen = Screen(self.margin) + + def disable(self): + self.on = False + + def enable(self): + self.on = True + + def get_prefs(self, context): + global __name__ + try: + # retrieve addon name from imports + addon_name = __name__.split('.')[0] + prefs = context.user_preferences.addons[addon_name].preferences + except: + prefs = DefaultColorScheme + pass + return prefs + + def instructions(self, context, title, explanation, shortcuts): + """ + position from bottom to top + """ + prefs = self.get_prefs(context) + + self.explanation.label = explanation + self.title.label = title + + self.shortcuts = [] + + for key, label in shortcuts: + key = GlText(d=2, label=key, + font_size=prefs.feedback_size_shortcut, + colour=prefs.feedback_colour_key) + label = GlText(d=2, label=' : ' + label, + font_size=prefs.feedback_size_shortcut, + colour=prefs.feedback_colour_shortcut) + ks = key.text_size(context) + ls = label.text_size(context) + self.shortcuts.append([key, ks, label, ls]) + + def draw(self, context, render=False): + if self.on: + """ + draw from bottom to top + so we are able to always fit needs + """ + x_min, x_max, y_min, y_max = self.screen.size(context) + available_w = x_max - x_min - 2 * self.spacing.x + main_title_size = self.main_title.text_size(context) + Vector((5, 0)) + + # h = context.region.height + # 0,0 = bottom left + pos = Vector((x_min + self.spacing.x, y_min)) + shortcuts = [] + + # sort by lines + lines = [] + line = [] + space = 0 + sum_txt = 0 + + for key, ks, label, ls in self.shortcuts: + space += ks.x + ls.x + self.spacing.x + if pos.x + space > available_w: + txt_spacing = (available_w - sum_txt) / (max(1, len(line) - 1)) + sum_txt = 0 + space = ks.x + ls.x + self.spacing.x + lines.append((txt_spacing, line)) + line = [] + sum_txt += ks.x + ls.x + line.append([key, ks, label, ls]) + + if len(line) > 0: + txt_spacing = (available_w - sum_txt) / (max(1, len(line) - 1)) + lines.append((txt_spacing, line)) + + # reverse lines to draw from bottom to top + lines = list(reversed(lines)) + for spacing, line in lines: + pos.y += self.spacing.y + pos.x = x_min + self.spacing.x + for key, ks, label, ls in line: + key.pos_3d = pos.copy() + pos.x += ks.x + label.pos_3d = pos.copy() + pos.x += ls.x + spacing + shortcuts.extend([key, label]) + pos.y += ks.y + self.spacing.y + + n_shortcuts = len(shortcuts) + # shortcut area + self.shortcut_area.pts_3d = [ + (x_min, self.margin), + (x_max, self.margin), + (x_max, pos.y), + (x_min, pos.y) + ] + + # small space between shortcut area and main title bar + if n_shortcuts > 0: + pos.y += 0.5 * self.spacing.y + + self.title_area.pts_3d = [ + (x_min, pos.y), + (x_max, pos.y), + (x_max, pos.y + main_title_size.y + 2 * self.spacing.y), + (x_min, pos.y + main_title_size.y + 2 * self.spacing.y) + ] + pos.y += self.spacing.y + + title_size = self.title.text_size(context) + # check for space available: + # if explanation + title + main_title are too big + # 1 remove main title + # 2 remove title + explanation_size = self.explanation.text_size(context) + + self.show_title = True + self.show_main_title = True + + if title_size.x + explanation_size.x > available_w: + # keep only explanation + self.show_title = False + self.show_main_title = False + elif main_title_size.x + title_size.x + explanation_size.x > available_w: + # keep title + explanation + self.show_main_title = False + self.title.pos_3d = (x_min + self.spacing.x, pos.y) + else: + self.title.pos_3d = (x_min + self.spacing.x + main_title_size.x, pos.y) + + self.explanation.pos_3d = (x_max - self.spacing.x - explanation_size.x, pos.y) + self.main_title.pos_3d = (x_min + self.spacing.x, pos.y) + + self.shortcut_area.draw(context) + self.title_area.draw(context) + if self.show_title: + self.title.draw(context) + if self.show_main_title: + self.main_title.draw(context) + self.explanation.draw(context) + for s in shortcuts: + s.draw(context) + + self.top = Vector((x_min, pos.y + main_title_size.y + self.spacing.y)) + + +class GlCursorFence(): + """ + Cursor crossing Fence + """ + def __init__(self, width=1, colour=(1.0, 1.0, 1.0, 0.5), style=2852): + self.line_x = GlLine(d=2) + self.line_x.style = style + self.line_x.width = width + self.line_x.colour_inactive = colour + self.line_y = GlLine(d=2) + self.line_y.style = style + self.line_y.width = width + self.line_y.colour_inactive = colour + self.on = True + + def set_location(self, context, location): + w = context.region.width + h = context.region.height + x, y = location + self.line_x.p = Vector((0, y)) + self.line_x.v = Vector((w, 0)) + self.line_y.p = Vector((x, 0)) + self.line_y.v = Vector((0, h)) + + def enable(self): + self.on = True + + def disable(self): + self.on = False + + def draw(self, context, render=False): + if self.on: + self.line_x.draw(context) + self.line_y.draw(context) + + +class GlCursorArea(): + def __init__(self, + width=1, + bordercolour=(1.0, 1.0, 1.0, 0.5), + areacolour=(0.5, 0.5, 0.5, 0.08), + style=2852): + + self.border = GlPolyline(bordercolour, d=2) + self.border.style = style + self.border.width = width + self.border.closed = True + self.area = GlPolygon(areacolour, d=2) + self.min = Vector((0, 0)) + self.max = Vector((0, 0)) + self.on = False + + def in_area(self, pt): + return (self.min.x <= pt.x and self.max.x >= pt.x and + self.min.y <= pt.y and self.max.y >= pt.y) + + def set_location(self, context, p0, p1): + x0, y0 = p0 + x1, y1 = p1 + if x0 > x1: + x1, x0 = x0, x1 + if y0 > y1: + y1, y0 = y0, y1 + self.min = Vector((x0, y0)) + self.max = Vector((x1, y1)) + pos = [ + Vector((x0, y0)), + Vector((x0, y1)), + Vector((x1, y1)), + Vector((x1, y0))] + self.area.set_pos(pos) + self.border.set_pos(pos) + + def enable(self): + self.on = True + + def disable(self): + self.on = False + + def draw(self, context, render=False): + if self.on: + self.area.draw(context) + self.border.draw(context) diff --git a/archipack/archipack_handle.py b/archipack/archipack_handle.py new file mode 100644 index 00000000..852fe2b6 --- /dev/null +++ b/archipack/archipack_handle.py @@ -0,0 +1,178 @@ +# -*- coding:utf-8 -*- + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +# + +# ---------------------------------------------------------- +# Author: Stephen Leger (s-leger) +# +# ---------------------------------------------------------- + +import bpy + + +def create_handle(context, parent, mesh): + handle = bpy.data.objects.new("Handle", mesh) + handle['archipack_handle'] = True + context.scene.objects.link(handle) + modif = handle.modifiers.new('Subsurf', 'SUBSURF') + modif.render_levels = 4 + modif.levels = 1 + handle.parent = parent + handle.matrix_world = parent.matrix_world.copy() + return handle + + +def door_handle_horizontal_01(direction, side, offset=0): + """ + side 1 -> inside + """ + verts = [(0.015, -0.003, -0.107), (0.008, -0.002, -0.007), (0.015, -0.002, -0.107), + (0.019, -0.002, -0.026), (-0.015, -0.003, -0.107), (-0.007, -0.002, -0.007), + (-0.015, -0.002, -0.107), (-0.018, -0.002, -0.026), (0.008, -0.002, 0.007), + (0.019, -0.002, 0.034), (-0.018, -0.002, 0.034), (-0.007, -0.002, 0.007), + (-0.018, -0.003, -0.026), (0.019, -0.003, -0.026), (-0.018, -0.003, 0.034), + (0.019, -0.003, 0.034), (-0.007, -0.042, -0.007), (0.008, -0.042, -0.007), + (-0.007, -0.042, 0.007), (0.008, -0.042, 0.007), (-0.007, -0.047, -0.016), + (0.008, -0.048, -0.018), (-0.007, -0.047, 0.016), (0.008, -0.048, 0.018), + (-0.025, -0.041, 0.013), (-0.025, -0.041, -0.012), (-0.025, -0.048, 0.013), + (-0.025, -0.048, -0.012), (0.019, -0.0, -0.026), (0.015, -0.0, -0.107), + (-0.015, -0.0, -0.107), (-0.018, -0.0, -0.026), (0.019, 0.0, 0.034), + (-0.018, 0.0, 0.034), (-0.107, -0.041, 0.013), (-0.107, -0.041, -0.012), + (-0.107, -0.048, 0.013), (-0.107, -0.048, -0.012), (-0.12, -0.041, 0.013), + (-0.12, -0.041, -0.012), (-0.12, -0.048, 0.013), (-0.12, -0.048, -0.012), + (0.008, -0.005, -0.007), (0.008, -0.005, 0.007), (-0.007, -0.005, 0.007), + (-0.007, -0.005, -0.007), (0.008, -0.041, -0.007), (0.008, -0.041, 0.007), + (-0.007, -0.041, 0.007), (-0.007, -0.041, -0.007), (0.015, -0.003, -0.091), + (0.015, -0.002, -0.091), (-0.015, -0.002, -0.091), (-0.015, -0.003, -0.091), + (0.015, -0.0, -0.091), (-0.015, -0.0, -0.091), (0.015, -0.003, 0.044), + (0.015, -0.002, 0.044), (-0.015, -0.002, 0.044), (-0.015, -0.003, 0.044), + (0.015, 0.0, 0.044), (-0.015, 0.0, 0.044)] + + faces = [(50, 51, 3, 13), (52, 55, 30, 6), (52, 53, 12, 7), (53, 50, 13, 12), + (2, 0, 4, 6), (10, 33, 31, 7), (15, 56, 59, 14), (12, 14, 10, 7), + (3, 9, 15, 13), (47, 19, 17, 46), (5, 12, 13, 1), (8, 15, 14, 11), + (11, 14, 12, 5), (1, 13, 15, 8), (22, 26, 27, 20), (48, 18, 19, 47), + (49, 16, 18, 48), (46, 17, 16, 49), (21, 23, 22, 20), (17, 21, 20, 16), + (19, 23, 21, 17), (18, 22, 23, 19), (24, 34, 36, 26), (16, 25, 24, 18), + (20, 27, 25, 16), (18, 24, 26, 22), (4, 0, 50, 53), (2, 29, 54, 51), + (6, 30, 29, 2), (10, 58, 61, 33), (3, 28, 32, 9), (51, 54, 28, 3), + (34, 38, 40, 36), (25, 35, 34, 24), (27, 37, 35, 25), (26, 36, 37, 27), + (39, 41, 40, 38), (35, 39, 38, 34), (37, 41, 39, 35), (36, 40, 41, 37), + (1, 42, 45, 5), (5, 45, 44, 11), (11, 44, 43, 8), (8, 43, 42, 1), + (42, 46, 49, 45), (45, 49, 48, 44), (44, 48, 47, 43), (43, 47, 46, 42), + (6, 4, 53, 52), (7, 31, 55, 52), (0, 2, 51, 50), (58, 59, 56, 57), + (57, 60, 61, 58), (32, 60, 57, 9), (14, 59, 58, 10), (9, 57, 56, 15)] + + if side == 1: + if direction == 1: + verts = [(-v[0], -v[1], v[2]) for v in verts] + else: + verts = [(v[0], -v[1], v[2]) for v in verts] + faces = [tuple(reversed(f)) for f in faces] + else: + if direction == 1: + verts = [(-v[0], v[1], v[2]) for v in verts] + faces = [tuple(reversed(f)) for f in faces] + if offset > 0: + faces = [tuple([i + offset for i in f]) for f in faces] + return verts, faces + + +def window_handle_vertical_01(side): + """ + side 1 -> inside + short handle for flat window + """ + verts = [(-0.01, 0.003, 0.011), (-0.013, 0.0, -0.042), (-0.018, 0.003, 0.03), (-0.01, 0.003, -0.01), + (-0.018, 0.003, -0.038), (0.01, 0.003, 0.011), (0.018, 0.003, 0.03), (0.018, 0.003, -0.038), + (0.01, 0.003, -0.01), (-0.018, 0.004, -0.038), (-0.018, 0.004, 0.03), (0.018, 0.004, -0.038), + (0.018, 0.004, 0.03), (-0.01, 0.039, -0.01), (-0.01, 0.025, 0.011), (0.01, 0.036, -0.01), + (0.01, 0.025, 0.011), (-0.017, 0.049, -0.01), (-0.01, 0.034, 0.011), (0.017, 0.049, -0.01), + (0.01, 0.034, 0.011), (0.0, 0.041, -0.048), (0.013, 0.003, 0.033), (0.019, 0.057, -0.048), + (-0.019, 0.057, -0.048), (-0.018, 0.0, 0.03), (0.013, 0.0, -0.042), (0.013, 0.004, -0.042), + (-0.018, 0.0, -0.038), (0.018, 0.0, 0.03), (0.018, 0.0, -0.038), (0.001, 0.041, -0.126), + (-0.013, 0.004, 0.033), (0.019, 0.056, -0.126), (-0.019, 0.056, -0.126), (0.001, 0.036, -0.16), + (-0.013, 0.003, 0.033), (0.019, 0.051, -0.16), (-0.019, 0.051, -0.16), (-0.01, 0.006, 0.011), + (0.01, 0.006, 0.011), (0.01, 0.006, -0.01), (-0.01, 0.006, -0.01), (-0.01, 0.025, 0.011), + (0.01, 0.025, 0.011), (0.01, 0.035, -0.01), (-0.01, 0.038, -0.01), (0.013, 0.003, -0.042), + (-0.013, 0.0, 0.033), (-0.013, 0.004, -0.042), (-0.013, 0.003, -0.042), (0.013, 0.004, 0.033), + (0.013, 0.0, 0.033)] + + faces = [(4, 2, 10, 9), (6, 12, 51, 22), (10, 2, 36, 32), (2, 25, 48, 36), + (27, 47, 50, 49), (7, 30, 26, 47), (28, 4, 50, 1), (12, 10, 32, 51), + (16, 14, 43, 44), (9, 10, 0, 3), (12, 11, 8, 5), (11, 9, 3, 8), + (10, 12, 5, 0), (23, 24, 17, 19), (15, 16, 44, 45), (13, 15, 45, 46), + (14, 13, 46, 43), (20, 19, 17, 18), (18, 17, 13, 14), (20, 18, 14, 16), + (19, 20, 16, 15), (31, 33, 23, 21), (21, 15, 13), (24, 21, 13, 17), + (21, 23, 19, 15), (9, 11, 27, 49), (26, 1, 50, 47), (4, 9, 49, 50), + (29, 6, 22, 52), (35, 37, 33, 31), (48, 52, 22, 36), (34, 31, 21, 24), + (33, 34, 24, 23), (38, 37, 35), (22, 51, 32, 36), (38, 35, 31, 34), + (37, 38, 34, 33), (39, 42, 3, 0), (42, 41, 8, 3), (41, 40, 5, 8), + (40, 39, 0, 5), (43, 46, 42, 39), (46, 45, 41, 42), (45, 44, 40, 41), + (44, 43, 39, 40), (28, 25, 2, 4), (12, 6, 7, 11), (7, 6, 29, 30), + (11, 7, 47, 27)] + + if side == 0: + verts = [(v[0], -v[1], v[2]) for v in verts] + faces = [tuple(reversed(f)) for f in faces] + + return verts, faces + + +def window_handle_vertical_02(side): + """ + side 1 -> inside + long handle for rail windows + """ + verts = [(-0.01, 0.003, 0.011), (-0.013, 0.0, -0.042), (-0.018, 0.003, 0.03), (-0.01, 0.003, -0.01), + (-0.018, 0.003, -0.038), (0.01, 0.003, 0.011), (0.018, 0.003, 0.03), (0.018, 0.003, -0.038), + (0.01, 0.003, -0.01), (-0.018, 0.004, -0.038), (-0.018, 0.004, 0.03), (0.018, 0.004, -0.038), + (0.018, 0.004, 0.03), (-0.01, 0.041, -0.01), (-0.01, 0.027, 0.011), (0.01, 0.038, -0.01), + (0.01, 0.027, 0.011), (-0.017, 0.054, -0.01), (-0.01, 0.039, 0.011), (0.017, 0.054, -0.01), + (0.01, 0.039, 0.011), (0.0, 0.041, -0.048), (0.013, 0.003, 0.033), (0.019, 0.059, -0.048), + (-0.019, 0.059, -0.048), (-0.018, 0.0, 0.03), (0.013, 0.0, -0.042), (0.013, 0.004, -0.042), + (-0.018, 0.0, -0.038), (0.018, 0.0, 0.03), (0.018, 0.0, -0.038), (0.001, 0.041, -0.322), + (-0.013, 0.004, 0.033), (0.019, 0.058, -0.322), (-0.019, 0.058, -0.322), (0.001, 0.036, -0.356), + (-0.013, 0.003, 0.033), (0.019, 0.053, -0.356), (-0.019, 0.053, -0.356), (-0.01, 0.006, 0.011), + (0.01, 0.006, 0.011), (0.01, 0.006, -0.01), (-0.01, 0.006, -0.01), (-0.01, 0.027, 0.011), + (0.01, 0.027, 0.011), (0.01, 0.037, -0.01), (-0.01, 0.04, -0.01), (0.013, 0.003, -0.042), + (-0.013, 0.0, 0.033), (-0.013, 0.004, -0.042), (-0.013, 0.003, -0.042), (0.013, 0.004, 0.033), + (0.013, 0.0, 0.033)] + + faces = [(4, 2, 10, 9), (6, 12, 51, 22), (10, 2, 36, 32), (2, 25, 48, 36), + (27, 47, 50, 49), (7, 30, 26, 47), (28, 4, 50, 1), (12, 10, 32, 51), + (16, 14, 43, 44), (9, 10, 0, 3), (12, 11, 8, 5), (11, 9, 3, 8), + (10, 12, 5, 0), (23, 24, 17, 19), (15, 16, 44, 45), (13, 15, 45, 46), + (14, 13, 46, 43), (20, 19, 17, 18), (18, 17, 13, 14), (20, 18, 14, 16), + (19, 20, 16, 15), (31, 33, 23, 21), (21, 15, 13), (24, 21, 13, 17), + (21, 23, 19, 15), (9, 11, 27, 49), (26, 1, 50, 47), (4, 9, 49, 50), + (29, 6, 22, 52), (35, 37, 33, 31), (48, 52, 22, 36), (34, 31, 21, 24), + (33, 34, 24, 23), (38, 37, 35), (22, 51, 32, 36), (38, 35, 31, 34), + (37, 38, 34, 33), (39, 42, 3, 0), (42, 41, 8, 3), (41, 40, 5, 8), + (40, 39, 0, 5), (43, 46, 42, 39), (46, 45, 41, 42), (45, 44, 40, 41), + (44, 43, 39, 40), (28, 25, 2, 4), (12, 6, 7, 11), (7, 6, 29, 30), + (11, 7, 47, 27)] + + if side == 0: + verts = [(v[0], -v[1], v[2]) for v in verts] + faces = [tuple(reversed(f)) for f in faces] + + return verts, faces diff --git a/archipack/archipack_keymaps.py b/archipack/archipack_keymaps.py new file mode 100644 index 00000000..65b295bf --- /dev/null +++ b/archipack/archipack_keymaps.py @@ -0,0 +1,108 @@ +# -*- coding:utf-8 -*- + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110- 1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +# + +# ---------------------------------------------------------- +# Author: Stephen Leger (s-leger) +# +# ---------------------------------------------------------- + + +class Keymaps: + """ + Expose user defined keymaps as event + so in modal operator we are able to + identify like + if (event == keymap.undo.event): + + and in feedback panels: + keymap.undo.key + keymap.undo.name + """ + def __init__(self, context): + """ + Init keymaps properties + """ + + # undo event + self.undo = self.get_event(context, 'Screen', 'ed.undo') + + # delete event + self.delete = self.get_event(context, 'Object Mode', 'object.delete') + + """ + # provide abstration between user and addon + # with different select mouse side + mouse_right = context.user_preferences.inputs.select_mouse + if mouse_right == 'LEFT': + mouse_left = 'RIGHT' + mouse_right_side = 'Left' + mouse_left_side = 'Right' + else: + mouse_left = 'LEFT' + mouse_right_side = 'Right' + mouse_left_side = 'Left' + + self.leftmouse = mouse_left + 'MOUSE' + self.rightmouse = mouse_right + 'MOUSE' + """ + + def check(self, event, against): + return against['event'] == (event.alt, event.ctrl, event.shift, event.type, event.value) + + def get_event(self, context, keyconfig, keymap_item): + """ + Return simple keymaps event signature as dict + NOTE: + this won't work for complex keymaps such as select_all + using properties to call operator in different manner + type: keyboard main type + name: event name as defined in user preferences + event: simple event signature to compare like : + if event == keymap.undo.event: + """ + ev = context.window_manager.keyconfigs.user.keymaps[keyconfig].keymap_items[keymap_item] + key = ev.type + if ev.ctrl: + key += '+CTRL' + if ev.alt: + key += '+ALT' + if ev.shift: + key += '+SHIFT' + return {'type': key, 'name': ev.name, 'event': (ev.alt, ev.ctrl, ev.shift, ev.type, ev.value)} + + def dump_keys(self, context, filename="c:\\tmp\\keymap.txt"): + """ + Utility for developpers : + Dump all keymaps to a file + filename : string a file path to dump keymaps + """ + str = "" + km = context.window_manager.keyconfigs.user.keymaps + for key in km.keys(): + str += "\n\n#--------------------------------\n{}:\n#--------------------------------\n\n".format(key) + for sub in km[key].keymap_items.keys(): + k = km[key].keymap_items[sub] + str += "alt:{} ctrl:{} shift:{} type:{} value:{} idname:{} name:{}\n".format( + k.alt, k.ctrl, k.shift, k.type, k.value, sub, k.name) + file = open(filename, "w") + file.write(str) + file.close() diff --git a/archipack/archipack_manipulator.py b/archipack/archipack_manipulator.py new file mode 100644 index 00000000..c3e0fc24 --- /dev/null +++ b/archipack/archipack_manipulator.py @@ -0,0 +1,2446 @@ +# -*- coding:utf-8 -*- + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +# + +# ---------------------------------------------------------- +# Author: Stephen Leger (s-leger) +# +# ---------------------------------------------------------- +import bpy +from math import atan2, pi +from mathutils import Vector, Matrix +from mathutils.geometry import intersect_line_plane, intersect_point_line, intersect_line_sphere +from bpy_extras import view3d_utils +from bpy.types import PropertyGroup, Operator +from bpy.props import FloatVectorProperty, StringProperty, CollectionProperty, BoolProperty +from bpy.app.handlers import persistent +from .archipack_snap import snap_point +from .archipack_keymaps import Keymaps +from .archipack_gl import ( + GlLine, GlArc, GlText, + GlPolyline, GlPolygon, + TriHandle, SquareHandle, EditableText, + FeedbackPanel, GlCursorArea +) + + +# NOTE: +# Snap aware manipulators use a dirty hack : +# draw() as a callback to update values in realtime +# as transform.translate in use to allow snap +# does catch all events. +# This however has a wanted side effect: +# the manipulator take precedence over allready running +# ones, and prevent select mode to start. +# +# TODO: +# Other manipulators should use same technique to take +# precedence over allready running ones when active +# +# NOTE: +# Select mode does suffer from this stack effect: +# the last running wins. The point is left mouse select mode +# requiring left drag to be RUNNING_MODAL to prevent real +# objects select and move during manipulators selection. +# +# TODO: +# First run a separate modal dedicated to select mode. +# Selecting in whole manips stack when required +# (manips[key].manipulable.manip_stack) +# Must investigate for a way to handle unselect after drag done. + +""" + @TODO: + Last modal running wins. + Manipulateurs without snap and thus not running own modal, + may loose events events caught by select mode of last + manipulable enabled +""" + +# Arrow sizes (world units) +arrow_size = 0.05 +# Handle area size (pixels) +handle_size = 10 + + +# a global manipulator stack reference +# prevent Blender "ACCESS_VIOLATION" crashes +# use a dict to prevent collisions +# between many objects being in manipulate mode +# use object names as loose keys +# NOTE : use app.drivers to reset before file load +manips = {} + + +class ArchipackActiveManip: + """ + Store manipulated object + - object_name: manipulated object name + - stack: array of Manipulators instances + - manipulable: Manipulable instance + """ + def __init__(self, object_name): + self.object_name = object_name + # manipulators stack for object + self.stack = [] + # reference to object manipulable instance + self.manipulable = None + + @property + def dirty(self): + """ + Check for manipulable validity + to disable modal when required + """ + return ( + self.manipulable is None or + bpy.data.objects.find(self.object_name) < 0 + ) + + def exit(self): + """ + Exit manipulation mode + - exit from all running manipulators + - empty manipulators stack + - set manipulable.manipulate_mode to False + - remove reference to manipulable + """ + for m in self.stack: + if m is not None: + m.exit() + if self.manipulable is not None: + self.manipulable.manipulate_mode = False + self.manipulable = None + self.object_name = "" + self.stack.clear() + + +def remove_manipulable(key): + """ + disable and remove a manipulable from stack + """ + global manips + # print("remove_manipulable key:%s" % (key)) + if key in manips.keys(): + manips[key].exit() + manips.pop(key) + + +def check_stack(key): + """ + check for stack item validity + use in modal to destroy invalid modals + return true when invalid / not found + false when valid + """ + global manips + if key not in manips.keys(): + # print("check_stack : key not found %s" % (key)) + return True + elif manips[key].dirty: + # print("check_stack : key.dirty %s" % (key)) + remove_manipulable(key) + return True + + return False + + +def empty_stack(): + # print("empty_stack()") + """ + kill every manipulators in stack + and cleanup stack + """ + global manips + for key in manips.keys(): + manips[key].exit() + manips.clear() + + +def add_manipulable(key, manipulable): + """ + add a ArchipackActiveManip into the stack + if not allready present + setup reference to manipulable + return manipulators stack + """ + global manips + if key not in manips.keys(): + # print("add_manipulable() key:%s not found create new" % (key)) + manips[key] = ArchipackActiveManip(key) + + manips[key].manipulable = manipulable + return manips[key].stack + + +# ------------------------------------------------------------------ +# Define Manipulators +# ------------------------------------------------------------------ + + +class Manipulator(): + """ + Manipulator base class to derive other + handle keyboard and modal events + provide convenient funcs including getter and setter for datablock values + store reference of base object, datablock and manipulator + """ + keyboard_ascii = { + ".", ",", "-", "+", "1", "2", "3", + "4", "5", "6", "7", "8", "9", "0", + "c", "m", "d", "k", "h", "a", + " ", "/", "*", "'", "\"" + # "=" + } + keyboard_type = { + 'BACK_SPACE', 'DEL', + 'LEFT_ARROW', 'RIGHT_ARROW' + } + + def __init__(self, context, o, datablock, manipulator, snap_callback=None): + """ + o : object to manipulate + datablock : object data to manipulate + manipulator: object archipack_manipulator datablock + snap_callback: on snap enabled manipulators, will be called when drag occurs + """ + self.keymap = Keymaps(context) + self.feedback = FeedbackPanel() + self.active = False + self.selectable = False + self.selected = False + # active text input value for manipulator + self.keyboard_input_active = False + self.label_value = 0 + # unit for keyboard input value + self.value_type = 'LENGTH' + self.pts_mode = 'SIZE' + self.o = o + self.datablock = datablock + self.manipulator = manipulator + self.snap_callback = snap_callback + self.origin = Vector((0, 0, 1)) + self.mouse_pos = Vector((0, 0)) + self.length_entered = "" + self.line_pos = 0 + args = (self, context) + self._handle = bpy.types.SpaceView3D.draw_handler_add(self.draw_callback, args, 'WINDOW', 'POST_PIXEL') + + @classmethod + def poll(cls, context): + """ + Allow manipulator enable/disable + in given context + handles will not show + """ + return True + + def exit(self): + """ + Modal exit, DONT EVEN TRY TO OVERRIDE + """ + if self._handle is not None: + bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW') + self._handle = None + else: + print("Manipulator.exit() handle not found %s" % (type(self).__name__)) + + # Mouse event handlers, MUST be overriden + def mouse_press(self, context, event): + """ + Manipulators must implement + mouse press event handler + return True to callback manipulable_manipulate + """ + raise NotImplementedError + + def mouse_release(self, context, event): + """ + Manipulators must implement + mouse mouse_release event handler + return False to callback manipulable_release + """ + raise NotImplementedError + + def mouse_move(self, context, event): + """ + Manipulators must implement + mouse move event handler + return True to callback manipulable_manipulate + """ + raise NotImplementedError + + # Keyboard event handlers, MAY be overriden + def keyboard_done(self, context, event, value): + """ + Manipulators may implement + keyboard value validated event handler + value: changed by keyboard + return True to callback manipulable_manipulate + """ + return False + + def keyboard_editing(self, context, event, value): + """ + Manipulators may implement + keyboard value changed event handler + value: string changed by keyboard + allow realtime update of label + return False to show edited value on window header + return True when feedback show right on screen + """ + self.label_value = value + return True + + def keyboard_cancel(self, context, event): + """ + Manipulators may implement + keyboard entry cancelled + """ + return + + def cancel(self, context, event): + """ + Manipulators may implement + cancelled event (ESC RIGHTCLICK) + """ + self.active = False + return + + def undo(self, context, event): + """ + Manipulators may implement + undo event (CTRL+Z) + """ + return False + + # Internal, do not override unless you realy + # realy realy deeply know what you are doing + def keyboard_eval(self, context, event): + """ + evaluate keyboard entry while typing + do not override this one + """ + c = event.ascii + if c: + if c == ",": + c = "." + self.length_entered = self.length_entered[:self.line_pos] + c + self.length_entered[self.line_pos:] + self.line_pos += 1 + + if self.length_entered: + if event.type == 'BACK_SPACE': + self.length_entered = self.length_entered[:self.line_pos - 1] + self.length_entered[self.line_pos:] + self.line_pos -= 1 + + elif event.type == 'DEL': + self.length_entered = self.length_entered[:self.line_pos] + self.length_entered[self.line_pos + 1:] + + elif event.type == 'LEFT_ARROW': + self.line_pos = (self.line_pos - 1) % (len(self.length_entered) + 1) + + elif event.type == 'RIGHT_ARROW': + self.line_pos = (self.line_pos + 1) % (len(self.length_entered) + 1) + + try: + value = bpy.utils.units.to_value(context.scene.unit_settings.system, self.value_type, self.length_entered) + draw_on_header = self.keyboard_editing(context, event, value) + except: # ValueError: + draw_on_header = True + pass + + if draw_on_header: + a = "" + if self.length_entered: + pos = self.line_pos + a = self.length_entered[:pos] + '|' + self.length_entered[pos:] + context.area.header_text_set("%s" % (a)) + + # modal mode: do not let event bubble up + return True + + def modal(self, context, event): + """ + Modal handler + handle mouse, and keyboard events + enable and disable feedback + """ + # print("Manipulator modal:%s %s" % (event.value, event.type)) + + if event.type == 'MOUSEMOVE': + return self.mouse_move(context, event) + + elif event.value == 'PRESS': + + if event.type == 'LEFTMOUSE': + active = self.mouse_press(context, event) + if active: + self.feedback.enable() + return active + + elif self.keymap.check(event, self.keymap.undo): + if self.keyboard_input_active: + self.keyboard_input_active = False + self.keyboard_cancel(context, event) + self.feedback.disable() + # prevent undo CRASH + return True + + elif self.keyboard_input_active and ( + event.ascii in self.keyboard_ascii or + event.type in self.keyboard_type + ): + # get keyboard input + return self.keyboard_eval(context, event) + + elif event.type in {'ESC', 'RIGHTMOUSE'}: + self.feedback.disable() + if self.keyboard_input_active: + # allow keyboard exit without setting value + self.length_entered = "" + self.line_pos = 0 + self.keyboard_input_active = False + self.keyboard_cancel(context, event) + return True + elif self.active: + self.cancel(context, event) + return True + return False + + elif event.value == 'RELEASE': + + if event.type == 'LEFTMOUSE': + if not self.keyboard_input_active: + self.feedback.disable() + return self.mouse_release(context, event) + + elif self.keyboard_input_active and event.type in {'RET', 'NUMPAD_ENTER'}: + # validate keyboard input + if self.length_entered != "": + try: + value = bpy.utils.units.to_value( + context.scene.unit_settings.system, + self.value_type, self.length_entered) + self.length_entered = "" + ret = self.keyboard_done(context, event, value) + except: # ValueError: + ret = False + self.keyboard_cancel(context, event) + pass + context.area.header_text_set() + self.keyboard_input_active = False + self.feedback.disable() + return ret + + return False + + def mouse_position(self, event): + """ + store mouse position in a 2d Vector + """ + self.mouse_pos.x, self.mouse_pos.y = event.mouse_region_x, event.mouse_region_y + + def get_pos3d(self, context): + """ + convert mouse pos to 3d point over plane defined by origin and normal + pt is in world space + """ + region = context.region + rv3d = context.region_data + rM = context.active_object.matrix_world.to_3x3() + view_vector_mouse = view3d_utils.region_2d_to_vector_3d(region, rv3d, self.mouse_pos) + ray_origin_mouse = view3d_utils.region_2d_to_origin_3d(region, rv3d, self.mouse_pos) + pt = intersect_line_plane(ray_origin_mouse, ray_origin_mouse + view_vector_mouse, + self.origin, rM * self.manipulator.normal, False) + # fix issue with parallel plane + if pt is None: + pt = intersect_line_plane(ray_origin_mouse, ray_origin_mouse + view_vector_mouse, + self.origin, view_vector_mouse, False) + return pt + + def get_value(self, data, attr, index=-1): + """ + Datablock value getter with index support + """ + try: + if index > -1: + return getattr(data, attr)[index] + else: + return getattr(data, attr) + except: + print("get_value of %s %s failed" % (data, attr)) + return 0 + + def set_value(self, context, data, attr, value, index=-1): + """ + Datablock value setter with index support + """ + try: + if self.get_value(data, attr, index) != value: + # switch context so unselected object may be manipulable too + old = context.active_object + state = self.o.select + self.o.select = True + context.scene.objects.active = self.o + if index > -1: + getattr(data, attr)[index] = value + else: + setattr(data, attr, value) + self.o.select = state + old.select = True + context.scene.objects.active = old + except: + pass + + def preTranslate(self, tM, vec): + """ + return a preTranslated Matrix + tM Matrix source + vec Vector translation + """ + return tM * Matrix([ + [1, 0, 0, vec.x], + [0, 1, 0, vec.y], + [0, 0, 1, vec.z], + [0, 0, 0, 1]]) + + def _move(self, o, axis, value): + if axis == 'x': + vec = Vector((value, 0, 0)) + elif axis == 'y': + vec = Vector((0, value, 0)) + else: + vec = Vector((0, 0, value)) + o.matrix_world = self.preTranslate(o.matrix_world, vec) + + def move_linked(self, context, axis, value): + """ + Move an object along local axis + takes care of linked too, fix issue #8 + """ + old = context.active_object + bpy.ops.object.select_all(action='DESELECT') + self.o.select = True + context.scene.objects.active = self.o + bpy.ops.object.select_linked(type='OBDATA') + for o in context.selected_objects: + if o != self.o: + self._move(o, axis, value) + bpy.ops.object.select_all(action='DESELECT') + old.select = True + context.scene.objects.active = old + + def move(self, context, axis, value): + """ + Move an object along local axis + """ + self._move(self.o, axis, value) + + +# OUT OF ORDER +class SnapPointManipulator(Manipulator): + """ + np_station based snap manipulator + dosent update anything by itself. + NOTE : currently out of order + and disabled in __init__ + """ + def __init__(self, context, o, datablock, manipulator, handle_size, snap_callback=None): + + raise NotImplementedError + + self.handle = SquareHandle(handle_size, 1.2 * arrow_size, draggable=True) + Manipulator.__init__(self, context, o, datablock, manipulator, snap_callback) + + def check_hover(self): + self.handle.check_hover(self.mouse_pos) + + def mouse_press(self, context, event): + if self.handle.hover: + self.handle.hover = False + self.handle.active = True + self.o.select = True + # takeloc = self.o.matrix_world * self.manipulator.p0 + # print("Invoke sp_point_move %s" % (takeloc)) + # @TODO: + # implement and add draw and callbacks + # snap_point(takeloc, draw, callback) + return True + return False + + def mouse_release(self, context, event): + self.check_hover() + self.handle.active = False + # False to callback manipulable_release + return False + + def update(self, context, event): + # NOTE: + # dosent set anything internally + return + + def mouse_move(self, context, event): + """ + + """ + self.mouse_position(event) + if self.handle.active: + # self.handle.active = np_snap.is_running + # self.update(context) + # True here to callback manipulable_manipulate + return True + else: + self.check_hover() + return False + + def draw_callback(self, _self, context, render=False): + left, right, side, normal = self.manipulator.get_pts(self.o.matrix_world) + self.handle.set_pos(context, left, Vector((1, 0, 0)), normal=normal) + self.handle.draw(context, render) + + +# Generic snap tool for line based archipack objects (fence, wall, maybe stair too) +gl_pts3d = [] + + +class WallSnapManipulator(Manipulator): + """ + np_station snap inspired manipulator + Use prop1_name as string part index + Use prop2_name as string identifier height property for placeholders + + Misnamed as it work for all line based archipack's + primitives, currently wall and fences, + but may also work with stairs (sharing same data structure) + """ + def __init__(self, context, o, datablock, manipulator, handle_size, snap_callback=None): + self.placeholder_area = GlPolygon((0.5, 0, 0, 0.2)) + self.placeholder_line = GlPolyline((0.5, 0, 0, 0.8)) + self.placeholder_line.closed = True + self.label = GlText() + self.line = GlLine() + self.handle = SquareHandle(handle_size, 1.2 * arrow_size, draggable=True, selectable=True) + Manipulator.__init__(self, context, o, datablock, manipulator, snap_callback) + self.selectable = True + + def select(self, cursor_area): + self.selected = self.selected or cursor_area.in_area(self.handle.pos_2d) + self.handle.selected = self.selected + + def deselect(self, cursor_area): + self.selected = not cursor_area.in_area(self.handle.pos_2d) + self.handle.selected = self.selected + + def check_hover(self): + self.handle.check_hover(self.mouse_pos) + + def mouse_press(self, context, event): + global gl_pts3d + global manips + if self.handle.hover: + self.active = True + self.handle.active = True + gl_pts3d = [] + idx = int(self.manipulator.prop1_name) + + # get selected manipulators idx + selection = [] + for m in manips[self.o.name].stack: + if m is not None and m.selected: + selection.append(int(m.manipulator.prop1_name)) + + # store all points of wall + for i, part in enumerate(self.datablock.parts): + p0, p1, side, normal = part.manipulators[2].get_pts(self.o.matrix_world) + # if selected p0 will move and require placeholder + gl_pts3d.append((p0, p1, i in selection or i == idx)) + + self.feedback.instructions(context, "Move / Snap", "Drag to move, use keyboard to input values", [ + ('CTRL', 'Snap'), + ('X Y', 'Constraint to axis (toggle Global Local None)'), + ('SHIFT+Z', 'Constraint to xy plane'), + ('MMBTN', 'Constraint to axis'), + ('RIGHTCLICK or ESC', 'exit without change') + ]) + self.feedback.enable() + self.handle.hover = False + self.o.select = True + takeloc, right, side, dz = self.manipulator.get_pts(self.o.matrix_world) + dx = (right - takeloc).normalized() + dy = dz.cross(dx) + takemat = Matrix([ + [dx.x, dy.x, dz.x, takeloc.x], + [dx.y, dy.y, dz.y, takeloc.y], + [dx.z, dy.z, dz.z, takeloc.z], + [0, 0, 0, 1] + ]) + snap_point(takemat=takemat, draw=self.sp_draw, callback=self.sp_callback, + constraint_axis=(True, True, False)) + # this prevent other selected to run + return True + + return False + + def mouse_release(self, context, event): + self.check_hover() + self.handle.active = False + self.active = False + self.feedback.disable() + # False to callback manipulable_release + return False + + def sp_callback(self, context, event, state, sp): + """ + np station callback on moving, place, or cancel + """ + global gl_pts3d + + if state == 'SUCCESS': + + self.o.select = True + # apply changes to wall + d = self.datablock + d.auto_update = False + + g = d.get_generator() + + # rotation relative to object + rM = self.o.matrix_world.inverted().to_3x3() + delta = (rM * sp.delta).to_2d() + # x_axis = (rM * Vector((1, 0, 0))).to_2d() + + # update generator + idx = 0 + for p0, p1, selected in gl_pts3d: + + if selected: + + # new location in object space + pt = g.segs[idx].lerp(0) + delta + + # move last point of segment before current + if idx > 0: + g.segs[idx - 1].p1 = pt + + # move first point of current segment + g.segs[idx].p0 = pt + + idx += 1 + + # update properties from generator + idx = 0 + for p0, p1, selected in gl_pts3d: + + if selected: + + # adjust segment before current + if idx > 0: + w = g.segs[idx - 1] + part = d.parts[idx - 1] + + if idx > 1: + part.a0 = w.delta_angle(g.segs[idx - 2]) + else: + part.a0 = w.straight(1, 0).angle + + if "C_" in part.type: + part.radius = w.r + else: + part.length = w.length + + # adjust current segment + w = g.segs[idx] + part = d.parts[idx] + + if idx > 0: + part.a0 = w.delta_angle(g.segs[idx - 1]) + else: + part.a0 = w.straight(1, 0).angle + # move object when point 0 + self.o.location += sp.delta + + if "C_" in part.type: + part.radius = w.r + else: + part.length = w.length + + # adjust next one + if idx + 1 < d.n_parts: + d.parts[idx + 1].a0 = g.segs[idx + 1].delta_angle(w) + + idx += 1 + + self.mouse_release(context, event) + d.auto_update = True + + if state == 'CANCEL': + self.mouse_release(context, event) + + return + + def sp_draw(self, sp, context): + # draw wall placeholders + + global gl_pts3d + + if self.o is None: + return + + z = self.get_value(self.datablock, self.manipulator.prop2_name) + + placeholders = [] + for p0, p1, selected in gl_pts3d: + pt = p0.copy() + if selected: + # when selected, p0 is moving + # last one p1 should move too + # last one require a placeholder too + pt += sp.delta + if len(placeholders) > 0: + placeholders[-1][1] = pt + placeholders[-1][2] = True + placeholders.append([pt, p1, selected]) + + # first selected and closed -> should move last p1 too + if gl_pts3d[0][2] and self.datablock.closed: + placeholders[-1][1] = placeholders[0][0].copy() + placeholders[-1][2] = True + + # last one not visible when not closed + if not self.datablock.closed: + placeholders[-1][2] = False + + for p0, p1, selected in placeholders: + if selected: + self.placeholder_area.set_pos([p0, p1, Vector((p1.x, p1.y, p1.z + z)), Vector((p0.x, p0.y, p0.z + z))]) + self.placeholder_line.set_pos([p0, p1, Vector((p1.x, p1.y, p1.z + z)), Vector((p0.x, p0.y, p0.z + z))]) + self.placeholder_area.draw(context, render=False) + self.placeholder_line.draw(context, render=False) + + p0, p1, side, normal = self.manipulator.get_pts(self.o.matrix_world) + self.line.p = p0 + self.line.v = sp.delta + self.label.set_pos(context, self.line.length, self.line.lerp(0.5), self.line.v, normal=Vector((0, 0, 1))) + self.line.draw(context, render=False) + self.label.draw(context, render=False) + + def mouse_move(self, context, event): + self.mouse_position(event) + if self.handle.active: + # False here to pass_through + # print("i'm able to pick up mouse move event while transform running") + return False + else: + self.check_hover() + return False + + def draw_callback(self, _self, context, render=False): + left, right, side, normal = self.manipulator.get_pts(self.o.matrix_world) + self.handle.set_pos(context, left, (left - right).normalized(), normal=normal) + self.handle.draw(context, render) + self.feedback.draw(context, render) + + +class CounterManipulator(Manipulator): + """ + increase or decrease an integer step by step + right on click to prevent misuse + """ + def __init__(self, context, o, datablock, manipulator, handle_size, snap_callback=None): + self.handle_left = TriHandle(handle_size, arrow_size, draggable=True) + self.handle_right = TriHandle(handle_size, arrow_size, draggable=True) + self.line_0 = GlLine() + self.label = GlText() + self.label.unit_mode = 'NONE' + self.label.precision = 0 + Manipulator.__init__(self, context, o, datablock, manipulator, snap_callback) + + def check_hover(self): + self.handle_right.check_hover(self.mouse_pos) + self.handle_left.check_hover(self.mouse_pos) + + def mouse_press(self, context, event): + if self.handle_right.hover: + value = self.get_value(self.datablock, self.manipulator.prop1_name) + self.set_value(context, self.datablock, self.manipulator.prop1_name, value + 1) + self.handle_right.active = True + return True + if self.handle_left.hover: + value = self.get_value(self.datablock, self.manipulator.prop1_name) + self.set_value(context, self.datablock, self.manipulator.prop1_name, value - 1) + self.handle_left.active = True + return True + return False + + def mouse_release(self, context, event): + self.check_hover() + self.handle_right.active = False + self.handle_left.active = False + return False + + def mouse_move(self, context, event): + self.mouse_position(event) + if self.handle_right.active: + return True + if self.handle_left.active: + return True + else: + self.check_hover() + return False + + def draw_callback(self, _self, context, render=False): + """ + draw on screen feedback using gl. + """ + # won't render counter + if render: + return + left, right, side, normal = self.manipulator.get_pts(self.o.matrix_world) + self.origin = left + self.line_0.p = left + self.line_0.v = right - left + self.line_0.z_axis = normal + self.label.z_axis = normal + value = self.get_value(self.datablock, self.manipulator.prop1_name) + self.handle_left.set_pos(context, self.line_0.p, -self.line_0.v, normal=normal) + self.handle_right.set_pos(context, self.line_0.lerp(1), self.line_0.v, normal=normal) + self.label.set_pos(context, value, self.line_0.lerp(0.5), self.line_0.v, normal=normal) + self.label.draw(context, render) + self.handle_left.draw(context, render) + self.handle_right.draw(context, render) + + +class DumbStringManipulator(Manipulator): + """ + not a real manipulator, but allow to show a string + """ + def __init__(self, context, o, datablock, manipulator, handle_size, snap_callback=None): + self.label = GlText(colour=(0, 0, 0, 1)) + self.label.unit_mode = 'NONE' + self.label.label = manipulator.prop1_name + Manipulator.__init__(self, context, o, datablock, manipulator, snap_callback) + + def check_hover(self): + return False + + def mouse_press(self, context, event): + return False + + def mouse_release(self, context, event): + return False + + def mouse_move(self, context, event): + return False + + def draw_callback(self, _self, context, render=False): + """ + draw on screen feedback using gl. + """ + # won't render string + if render: + return + left, right, side, normal = self.manipulator.get_pts(self.o.matrix_world) + pos = left + 0.5 * (right - left) + self.label.set_pos(context, None, pos, pos, normal=normal) + self.label.draw(context, render) + + +class SizeManipulator(Manipulator): + + def __init__(self, context, o, datablock, manipulator, handle_size, snap_callback=None): + self.handle_left = TriHandle(handle_size, arrow_size) + self.handle_right = TriHandle(handle_size, arrow_size, draggable=True) + self.line_0 = GlLine() + self.line_1 = GlLine() + self.line_2 = GlLine() + self.label = EditableText(handle_size, arrow_size, draggable=True) + # self.label.label = 'S ' + Manipulator.__init__(self, context, o, datablock, manipulator, snap_callback) + + def check_hover(self): + self.handle_right.check_hover(self.mouse_pos) + self.label.check_hover(self.mouse_pos) + + def mouse_press(self, context, event): + global gl_pts3d + if self.handle_right.hover: + self.active = True + self.original_size = self.get_value(self.datablock, self.manipulator.prop1_name) + self.original_location = self.o.matrix_world.translation.copy() + self.feedback.instructions(context, "Size", "Drag or Keyboard to modify size", [ + ('CTRL', 'Snap'), + ('SHIFT', 'Round'), + ('RIGHTCLICK or ESC', 'cancel') + ]) + left, right, side, dz = self.manipulator.get_pts(self.o.matrix_world) + dx = (right - left).normalized() + dy = dz.cross(dx) + takemat = Matrix([ + [dx.x, dy.x, dz.x, right.x], + [dx.y, dy.y, dz.y, right.y], + [dx.z, dy.z, dz.z, right.z], + [0, 0, 0, 1] + ]) + gl_pts3d = [left, right] + snap_point(takemat=takemat, + draw=self.sp_draw, + callback=self.sp_callback, + constraint_axis=(True, False, False)) + self.handle_right.active = True + return True + if self.label.hover: + self.feedback.instructions(context, "Size", "Use keyboard to modify size", + [('ENTER', 'Validate'), ('RIGHTCLICK or ESC', 'cancel')]) + self.label.active = True + self.keyboard_input_active = True + return True + return False + + def mouse_release(self, context, event): + self.active = False + self.check_hover() + self.handle_right.active = False + if not self.keyboard_input_active: + self.feedback.disable() + return False + + def mouse_move(self, context, event): + self.mouse_position(event) + if self.active: + self.update(context, event) + return True + else: + self.check_hover() + return False + + def cancel(self, context, event): + if self.active: + self.mouse_release(context, event) + self.set_value(context, self.datablock, self.manipulator.prop1_name, self.original_size) + + def keyboard_done(self, context, event, value): + self.set_value(context, self.datablock, self.manipulator.prop1_name, value) + self.label.active = False + return True + + def keyboard_cancel(self, context, event): + self.label.active = False + return False + + def update(self, context, event): + # 0 1 2 + # |_____| + # + pt = self.get_pos3d(context) + pt, t = intersect_point_line(pt, self.line_0.p, self.line_2.p) + length = (self.line_0.p - pt).length + if event.alt: + length = round(length, 1) + self.set_value(context, self.datablock, self.manipulator.prop1_name, length) + + def draw_callback(self, _self, context, render=False): + """ + draw on screen feedback using gl. + """ + left, right, side, normal = self.manipulator.get_pts(self.o.matrix_world) + self.origin = left + self.line_1.p = left + self.line_1.v = right - left + self.line_0.z_axis = normal + self.line_1.z_axis = normal + self.line_2.z_axis = normal + self.label.z_axis = normal + self.line_0 = self.line_1.sized_normal(0, side.x * 1.1) + self.line_2 = self.line_1.sized_normal(1, side.x * 1.1) + self.line_1.offset(side.x * 1.0) + self.handle_left.set_pos(context, self.line_1.p, -self.line_1.v, normal=normal) + self.handle_right.set_pos(context, self.line_1.lerp(1), self.line_1.v, normal=normal) + if not self.keyboard_input_active: + self.label_value = self.line_1.length + self.label.set_pos(context, self.label_value, self.line_1.lerp(0.5), self.line_1.v, normal=normal) + self.line_0.draw(context, render) + self.line_1.draw(context, render) + self.line_2.draw(context, render) + self.handle_left.draw(context, render) + self.handle_right.draw(context, render) + self.label.draw(context, render) + self.feedback.draw(context, render) + + def sp_draw(self, sp, context): + global gl_pts3d + if self.o is None: + return + p0 = gl_pts3d[0].copy() + p1 = gl_pts3d[1].copy() + p1 += sp.delta + self.sp_update(context, p0, p1) + return + + def sp_callback(self, context, event, state, sp): + + if state == 'SUCCESS': + self.sp_draw(sp, context) + self.mouse_release(context, event) + + if state == 'CANCEL': + p0 = gl_pts3d[0].copy() + p1 = gl_pts3d[1].copy() + self.sp_update(context, p0, p1) + self.mouse_release(context, event) + + def sp_update(self, context, p0, p1): + length = (p0 - p1).length + self.set_value(context, self.datablock, self.manipulator.prop1_name, length) + + +class SizeLocationManipulator(SizeManipulator): + """ + Handle resizing by any of the boundaries + of objects with centered pivots + so when size change, object should move of the + half of the change in the direction of change. + + Also take care of moving linked objects too + Changing size is not necessary as link does + allredy handle this and childs panels are + updated by base object. + """ + def __init__(self, context, o, datablock, manipulator, handle_size, snap_callback=None): + SizeManipulator.__init__(self, context, o, datablock, manipulator, handle_size, snap_callback) + self.handle_left.draggable = True + + def check_hover(self): + self.handle_right.check_hover(self.mouse_pos) + self.handle_left.check_hover(self.mouse_pos) + self.label.check_hover(self.mouse_pos) + + def mouse_press(self, context, event): + if self.handle_right.hover: + self.active = True + self.original_location = self.o.matrix_world.translation.copy() + self.original_size = self.get_value(self.datablock, self.manipulator.prop1_name) + self.feedback.instructions(context, "Size", "Drag to modify size", [ + ('ALT', 'Round value'), ('RIGHTCLICK or ESC', 'cancel') + ]) + self.handle_right.active = True + return True + if self.handle_left.hover: + self.active = True + self.original_location = self.o.matrix_world.translation.copy() + self.original_size = self.get_value(self.datablock, self.manipulator.prop1_name) + self.feedback.instructions(context, "Size", "Drag to modify size", [ + ('ALT', 'Round value'), ('RIGHTCLICK or ESC', 'cancel') + ]) + self.handle_left.active = True + return True + if self.label.hover: + self.feedback.instructions(context, "Size", "Use keyboard to modify size", + [('ENTER', 'Validate'), ('RIGHTCLICK or ESC', 'cancel')]) + self.label.active = True + self.keyboard_input_active = True + return True + return False + + def mouse_release(self, context, event): + self.active = False + self.check_hover() + self.handle_right.active = False + self.handle_left.active = False + if not self.keyboard_input_active: + self.feedback.disable() + return False + + def mouse_move(self, context, event): + self.mouse_position(event) + if self.handle_right.active or self.handle_left.active: + self.update(context, event) + return True + else: + self.check_hover() + return False + + def keyboard_done(self, context, event, value): + self.set_value(context, self.datablock, self.manipulator.prop1_name, value) + # self.move_linked(context, self.manipulator.prop2_name, dl) + self.label.active = False + self.feedback.disable() + return True + + def cancel(self, context, event): + if self.active: + self.mouse_release(context, event) + # must move back to original location + itM = self.o.matrix_world.inverted() + dl = self.get_value(itM * self.original_location, self.manipulator.prop2_name) + + self.move(context, self.manipulator.prop2_name, dl) + self.set_value(context, self.datablock, self.manipulator.prop1_name, self.original_size) + self.move_linked(context, self.manipulator.prop2_name, dl) + + def update(self, context, event): + # 0 1 2 + # |_____| + # + pt = self.get_pos3d(context) + pt, t = intersect_point_line(pt, self.line_0.p, self.line_2.p) + + len_0 = (pt - self.line_0.p).length + len_1 = (pt - self.line_2.p).length + + length = max(len_0, len_1) + + if event.alt: + length = round(length, 1) + + dl = length - self.line_1.length + + if len_0 > len_1: + dl = 0.5 * dl + else: + dl = -0.5 * dl + + self.move(context, self.manipulator.prop2_name, dl) + self.set_value(context, self.datablock, self.manipulator.prop1_name, length) + self.move_linked(context, self.manipulator.prop2_name, dl) + + +class SnapSizeLocationManipulator(SizeLocationManipulator): + """ + Snap aware extension of SizeLocationManipulator + Handle resizing by any of the boundaries + of objects with centered pivots + so when size change, object should move of the + half of the change in the direction of change. + + Also take care of moving linked objects too + Changing size is not necessary as link does + allredy handle this and childs panels are + updated by base object. + + + """ + def __init__(self, context, o, datablock, manipulator, handle_size, snap_callback=None): + SizeLocationManipulator.__init__(self, context, o, datablock, manipulator, handle_size, snap_callback) + + def mouse_press(self, context, event): + global gl_pts3d + if self.handle_right.hover: + self.active = True + self.original_size = self.get_value(self.datablock, self.manipulator.prop1_name) + self.original_location = self.o.matrix_world.translation.copy() + self.feedback.instructions(context, "Size", "Drag or Keyboard to modify size", [ + ('CTRL', 'Snap'), + ('SHIFT', 'Round'), + ('RIGHTCLICK or ESC', 'cancel') + ]) + left, right, side, dz = self.manipulator.get_pts(self.o.matrix_world) + dx = (right - left).normalized() + dy = dz.cross(dx) + takemat = Matrix([ + [dx.x, dy.x, dz.x, right.x], + [dx.y, dy.y, dz.y, right.y], + [dx.z, dy.z, dz.z, right.z], + [0, 0, 0, 1] + ]) + gl_pts3d = [left, right] + snap_point(takemat=takemat, + draw=self.sp_draw, + callback=self.sp_callback, + constraint_axis=(True, False, False)) + + self.handle_right.active = True + return True + + if self.handle_left.hover: + self.active = True + self.original_size = self.get_value(self.datablock, self.manipulator.prop1_name) + self.original_location = self.o.matrix_world.translation.copy() + self.feedback.instructions(context, "Size", "Drag or Keyboard to modify size", [ + ('CTRL', 'Snap'), + ('SHIFT', 'Round'), + ('RIGHTCLICK or ESC', 'cancel') + ]) + left, right, side, dz = self.manipulator.get_pts(self.o.matrix_world) + dx = (left - right).normalized() + dy = dz.cross(dx) + takemat = Matrix([ + [dx.x, dy.x, dz.x, left.x], + [dx.y, dy.y, dz.y, left.y], + [dx.z, dy.z, dz.z, left.z], + [0, 0, 0, 1] + ]) + gl_pts3d = [left, right] + snap_point(takemat=takemat, + draw=self.sp_draw, + callback=self.sp_callback, + constraint_axis=(True, False, False)) + self.handle_left.active = True + return True + + if self.label.hover: + self.feedback.instructions(context, "Size", "Use keyboard to modify size", + [('ENTER', 'Validate'), ('RIGHTCLICK or ESC', 'cancel')]) + self.label.active = True + self.keyboard_input_active = True + return True + + return False + + def sp_draw(self, sp, context): + global gl_pts3d + if self.o is None: + return + p0 = gl_pts3d[0].copy() + p1 = gl_pts3d[1].copy() + if self.handle_right.active: + p1 += sp.delta + else: + p0 += sp.delta + self.sp_update(context, p0, p1) + + # snapping child objects may require base object update + # eg manipulating windows requiring wall update + if self.snap_callback is not None: + snap_helper = context.active_object + self.snap_callback(context, o=self.o, manipulator=self) + context.scene.objects.active = snap_helper + + return + + def sp_callback(self, context, event, state, sp): + + if state == 'SUCCESS': + self.sp_draw(sp, context) + self.mouse_release(context, event) + + if state == 'CANCEL': + p0 = gl_pts3d[0].copy() + p1 = gl_pts3d[1].copy() + self.sp_update(context, p0, p1) + self.mouse_release(context, event) + + def sp_update(self, context, p0, p1): + l0 = self.get_value(self.datablock, self.manipulator.prop1_name) + length = (p0 - p1).length + dp = length - l0 + if self.handle_left.active: + dp = -dp + dl = 0.5 * dp + self.move(context, self.manipulator.prop2_name, dl) + self.set_value(context, self.datablock, self.manipulator.prop1_name, length) + self.move_linked(context, self.manipulator.prop2_name, dl) + + +class DeltaLocationManipulator(SizeManipulator): + """ + Move a child window or door in wall segment + not limited to this by the way + """ + def __init__(self, context, o, datablock, manipulator, handle_size, snap_callback=None): + SizeManipulator.__init__(self, context, o, datablock, manipulator, handle_size, snap_callback) + self.label.label = '' + self.feedback.instructions(context, "Move", "Drag to move", [ + ('CTRL', 'Snap'), + ('SHIFT', 'Round value'), + ('RIGHTCLICK or ESC', 'cancel') + ]) + + def check_hover(self): + self.handle_right.check_hover(self.mouse_pos) + + def mouse_press(self, context, event): + global gl_pts3d + if self.handle_right.hover: + self.original_location = self.o.matrix_world.translation.copy() + self.active = True + self.feedback.enable() + self.handle_right.active = True + + left, right, side, dz = self.manipulator.get_pts(self.o.matrix_world) + dp = (right - left) + dx = dp.normalized() + dy = dz.cross(dx) + p0 = left + 0.5 * dp + takemat = Matrix([ + [dx.x, dy.x, dz.x, p0.x], + [dx.y, dy.y, dz.y, p0.y], + [dx.z, dy.z, dz.z, p0.z], + [0, 0, 0, 1] + ]) + gl_pts3d = [p0] + snap_point(takemat=takemat, + draw=self.sp_draw, + callback=self.sp_callback, + constraint_axis=( + self.manipulator.prop1_name == 'x', + self.manipulator.prop1_name == 'y', + self.manipulator.prop1_name == 'z')) + return True + return False + + def mouse_release(self, context, event): + self.check_hover() + self.feedback.disable() + self.active = False + self.handle_right.active = False + return False + + def mouse_move(self, context, event): + self.mouse_position(event) + if self.handle_right.active: + # self.update(context, event) + return True + else: + self.check_hover() + return False + + def sp_draw(self, sp, context): + global gl_pts3d + if self.o is None: + return + p0 = gl_pts3d[0].copy() + p1 = p0 + sp.delta + itM = self.o.matrix_world.inverted() + dl = self.get_value(itM * p1, self.manipulator.prop1_name) + self.move(context, self.manipulator.prop1_name, dl) + + # snapping child objects may require base object update + # eg manipulating windows requiring wall update + if self.snap_callback is not None: + snap_helper = context.active_object + self.snap_callback(context, o=self.o, manipulator=self) + context.scene.objects.active = snap_helper + + return + + def sp_callback(self, context, event, state, sp): + + if state == 'SUCCESS': + self.sp_draw(sp, context) + self.mouse_release(context, event) + + if state == 'CANCEL': + self.cancel(context, event) + + def cancel(self, context, event): + if self.active: + self.mouse_release(context, event) + # must move back to original location + itM = self.o.matrix_world.inverted() + dl = self.get_value(itM * self.original_location, self.manipulator.prop1_name) + self.move(context, self.manipulator.prop1_name, dl) + + def draw_callback(self, _self, context, render=False): + """ + draw on screen feedback using gl. + """ + left, right, side, normal = self.manipulator.get_pts(self.o.matrix_world) + self.origin = left + self.line_1.p = left + self.line_1.v = right - left + self.line_1.z_axis = normal + self.handle_left.set_pos(context, self.line_1.lerp(0.5), -self.line_1.v, normal=normal) + self.handle_right.set_pos(context, self.line_1.lerp(0.5), self.line_1.v, normal=normal) + self.handle_left.draw(context, render) + self.handle_right.draw(context, render) + self.feedback.draw(context) + + +class DumbSizeManipulator(SizeManipulator): + """ + Show a size while not being editable + """ + def __init__(self, context, o, datablock, manipulator, handle_size, snap_callback=None): + SizeManipulator.__init__(self, context, o, datablock, manipulator, handle_size, snap_callback) + self.handle_right.draggable = False + self.label.draggable = False + self.label.colour_inactive = (0, 0, 0, 1) + # self.label.label = 'Dumb ' + + def mouse_move(self, context, event): + return False + + +class AngleManipulator(Manipulator): + """ + NOTE: + There is a default shortcut to +5 and -5 on angles with left/right arrows + + Manipulate angle between segments + bound to [-pi, pi] + """ + + def __init__(self, context, o, datablock, manipulator, handle_size, snap_callback=None): + # Angle + self.handle_right = TriHandle(handle_size, arrow_size, draggable=True) + self.handle_center = SquareHandle(handle_size, arrow_size) + self.arc = GlArc() + self.line_0 = GlLine() + self.line_1 = GlLine() + self.label_a = EditableText(handle_size, arrow_size, draggable=True) + self.label_a.unit_type = 'ANGLE' + Manipulator.__init__(self, context, o, datablock, manipulator, snap_callback) + self.pts_mode = 'RADIUS' + + def check_hover(self): + self.handle_right.check_hover(self.mouse_pos) + self.label_a.check_hover(self.mouse_pos) + + def mouse_press(self, context, event): + if self.handle_right.hover: + self.active = True + self.original_angle = self.get_value(self.datablock, self.manipulator.prop1_name) + self.feedback.instructions(context, "Angle", "Drag to modify angle", [ + ('SHIFT', 'Round value'), + ('RIGHTCLICK or ESC', 'cancel') + ]) + self.handle_right.active = True + return True + if self.label_a.hover: + self.feedback.instructions(context, "Angle (degree)", "Use keyboard to modify angle", + [('ENTER', 'validate'), + ('RIGHTCLICK or ESC', 'cancel')]) + self.value_type = 'ROTATION' + self.label_a.active = True + self.label_value = self.get_value(self.datablock, self.manipulator.prop1_name) + self.keyboard_input_active = True + return True + return False + + def mouse_release(self, context, event): + self.check_hover() + self.handle_right.active = False + self.active = False + return False + + def mouse_move(self, context, event): + self.mouse_position(event) + if self.active: + # print("AngleManipulator.mouse_move") + self.update(context, event) + return True + else: + self.check_hover() + return False + + def keyboard_done(self, context, event, value): + self.set_value(context, self.datablock, self.manipulator.prop1_name, value) + self.label_a.active = False + return True + + def keyboard_cancel(self, context, event): + self.label_a.active = False + return False + + def cancel(self, context, event): + if self.active: + self.mouse_release(context, event) + self.set_value(context, self.datablock, self.manipulator.prop1_name, self.original_angle) + + def update(self, context, event): + pt = self.get_pos3d(context) + c = self.arc.c + v = 2 * self.arc.r * (pt - c).normalized() + v0 = c - v + v1 = c + v + p0, p1 = intersect_line_sphere(v0, v1, c, self.arc.r) + if p0 is not None and p1 is not None: + + if (p1 - pt).length < (p0 - pt).length: + p0, p1 = p1, p0 + + v = p0 - self.arc.c + da = atan2(v.y, v.x) - self.line_0.angle + if da > pi: + da -= 2 * pi + if da < -pi: + da += 2 * pi + # from there pi > da > -pi + # print("a:%.4f da:%.4f a0:%.4f" % (atan2(v.y, v.x), da, self.line_0.angle)) + if da > pi: + da = pi + if da < -pi: + da = -pi + if event.shift: + da = round(da / pi * 180, 0) / 180 * pi + self.set_value(context, self.datablock, self.manipulator.prop1_name, da) + + def draw_callback(self, _self, context, render=False): + c, left, right, normal = self.manipulator.get_pts(self.o.matrix_world) + self.line_0.z_axis = normal + self.line_1.z_axis = normal + self.arc.z_axis = normal + self.label_a.z_axis = normal + self.origin = c + self.line_0.p = c + self.line_1.p = c + self.arc.c = c + self.line_0.v = left + self.line_0.v = -self.line_0.cross.normalized() + self.line_1.v = right + self.line_1.v = self.line_1.cross.normalized() + self.arc.a0 = self.line_0.angle + self.arc.da = self.get_value(self.datablock, self.manipulator.prop1_name) + self.arc.r = 1.0 + self.handle_right.set_pos(context, self.line_1.lerp(1), + self.line_1.sized_normal(1, -1 if self.arc.da > 0 else 1).v) + self.handle_center.set_pos(context, self.arc.c, -self.line_0.v) + label_value = self.arc.da + if self.keyboard_input_active: + label_value = self.label_value + self.label_a.set_pos(context, label_value, self.arc.lerp(0.5), -self.line_0.v) + self.arc.draw(context, render) + self.line_0.draw(context, render) + self.line_1.draw(context, render) + self.handle_right.draw(context, render) + self.handle_center.draw(context, render) + self.label_a.draw(context, render) + self.feedback.draw(context, render) + + +class DumbAngleManipulator(AngleManipulator): + def __init__(self, context, o, datablock, manipulator, handle_size, snap_callback=None): + AngleManipulator.__init__(self, context, o, datablock, manipulator, handle_size, snap_callback=None) + self.handle_right.draggable = False + self.label_a.draggable = False + + def draw_callback(self, _self, context, render=False): + c, left, right, normal = self.manipulator.get_pts(self.o.matrix_world) + self.line_0.z_axis = normal + self.line_1.z_axis = normal + self.arc.z_axis = normal + self.label_a.z_axis = normal + self.origin = c + self.line_0.p = c + self.line_1.p = c + self.arc.c = c + self.line_0.v = left + self.line_0.v = -self.line_0.cross.normalized() + self.line_1.v = right + self.line_1.v = self.line_1.cross.normalized() + self.arc.a0 = self.line_0.angle + self.arc.da = self.line_1.v.to_2d().angle_signed(self.line_0.v.to_2d()) + self.arc.r = 1.0 + self.handle_right.set_pos(context, self.line_1.lerp(1), + self.line_1.sized_normal(1, -1 if self.arc.da > 0 else 1).v) + self.handle_center.set_pos(context, self.arc.c, -self.line_0.v) + label_value = self.arc.da + self.label_a.set_pos(context, label_value, self.arc.lerp(0.5), -self.line_0.v) + self.arc.draw(context, render) + self.line_0.draw(context, render) + self.line_1.draw(context, render) + self.handle_right.draw(context, render) + self.handle_center.draw(context, render) + self.label_a.draw(context, render) + self.feedback.draw(context, render) + + +class ArcAngleManipulator(Manipulator): + """ + Manipulate angle of an arc + when angle < 0 the arc center is on the left part of the circle + when angle > 0 the arc center is on the right part of the circle + bound to [-pi, pi] + """ + + def __init__(self, context, o, datablock, manipulator, handle_size, snap_callback=None): + + # Fixed + self.handle_left = SquareHandle(handle_size, arrow_size) + # Angle + self.handle_right = TriHandle(handle_size, arrow_size, draggable=True) + self.handle_center = SquareHandle(handle_size, arrow_size) + self.arc = GlArc() + self.line_0 = GlLine() + self.line_1 = GlLine() + self.label_a = EditableText(handle_size, arrow_size, draggable=True) + self.label_r = EditableText(handle_size, arrow_size, draggable=False) + self.label_a.unit_type = 'ANGLE' + Manipulator.__init__(self, context, o, datablock, manipulator, snap_callback) + self.pts_mode = 'RADIUS' + + def check_hover(self): + self.handle_right.check_hover(self.mouse_pos) + self.label_a.check_hover(self.mouse_pos) + + def mouse_press(self, context, event): + if self.handle_right.hover: + self.active = True + self.original_angle = self.get_value(self.datablock, self.manipulator.prop1_name) + self.feedback.instructions(context, "Angle (degree)", "Drag to modify angle", [ + ('SHIFT', 'Round value'), + ('RIGHTCLICK or ESC', 'cancel') + ]) + self.handle_right.active = True + return True + if self.label_a.hover: + self.feedback.instructions(context, "Angle (degree)", "Use keyboard to modify angle", + [('ENTER', 'validate'), + ('RIGHTCLICK or ESC', 'cancel')]) + self.value_type = 'ROTATION' + self.label_value = self.get_value(self.datablock, self.manipulator.prop1_name) + self.label_a.active = True + self.keyboard_input_active = True + return True + if self.label_r.hover: + self.feedback.instructions(context, "Radius", "Use keyboard to modify radius", + [('ENTER', 'validate'), + ('RIGHTCLICK or ESC', 'cancel')]) + self.value_type = 'LENGTH' + self.label_r.active = True + self.keyboard_input_active = True + return True + return False + + def mouse_release(self, context, event): + self.check_hover() + self.handle_right.active = False + self.active = False + return False + + def mouse_move(self, context, event): + self.mouse_position(event) + if self.handle_right.active: + self.update(context, event) + return True + else: + self.check_hover() + return False + + def keyboard_done(self, context, event, value): + self.set_value(context, self.datablock, self.manipulator.prop1_name, value) + self.label_a.active = False + self.label_r.active = False + return True + + def keyboard_cancel(self, context, event): + self.label_a.active = False + self.label_r.active = False + return False + + def cancel(self, context, event): + if self.active: + self.mouse_release(context, event) + self.set_value(context, self.datablock, self.manipulator.prop1_name, self.original_angle) + + def update(self, context, event): + + pt = self.get_pos3d(context) + c = self.arc.c + + v = 2 * self.arc.r * (pt - c).normalized() + v0 = c - v + v1 = c + v + p0, p1 = intersect_line_sphere(v0, v1, c, self.arc.r) + + if p0 is not None and p1 is not None: + # find nearest mouse intersection point + if (p1 - pt).length < (p0 - pt).length: + p0, p1 = p1, p0 + + v = p0 - self.arc.c + + s = self.arc.tangeant(0, 1) + res, d, t = s.point_sur_segment(pt) + if d > 0: + # right side + a = self.arc.sized_normal(0, self.arc.r).angle + else: + a = self.arc.sized_normal(0, -self.arc.r).angle + + da = atan2(v.y, v.x) - a + + # bottom side +- pi + if t < 0: + # right + if d > 0: + da = pi + else: + da = -pi + # top side bound to +- pi + else: + if da > pi: + da -= 2 * pi + if da < -pi: + da += 2 * pi + + if event.shift: + da = round(da / pi * 180, 0) / 180 * pi + self.set_value(context, self.datablock, self.manipulator.prop1_name, da) + + def draw_callback(self, _self, context, render=False): + # center : 3d points + # left : 3d vector pt-c + # right : 3d vector pt-c + c, left, right, normal = self.manipulator.get_pts(self.o.matrix_world) + self.line_0.z_axis = normal + self.line_1.z_axis = normal + self.arc.z_axis = normal + self.label_a.z_axis = normal + self.label_r.z_axis = normal + self.origin = c + self.line_0.p = c + self.line_1.p = c + self.arc.c = c + self.line_0.v = left + self.line_1.v = right + self.arc.a0 = self.line_0.angle + self.arc.da = self.get_value(self.datablock, self.manipulator.prop1_name) + self.arc.r = left.length + self.handle_left.set_pos(context, self.line_0.lerp(1), self.line_0.v) + self.handle_right.set_pos(context, self.line_1.lerp(1), + self.line_1.sized_normal(1, -1 if self.arc.da > 0 else 1).v) + self.handle_center.set_pos(context, self.arc.c, -self.line_0.v) + label_a_value = self.arc.da + label_r_value = self.arc.r + if self.keyboard_input_active: + if self.value_type == 'LENGTH': + label_r_value = self.label_value + else: + label_a_value = self.label_value + self.label_a.set_pos(context, label_a_value, self.arc.lerp(0.5), -self.line_0.v) + self.label_r.set_pos(context, label_r_value, self.line_0.lerp(0.5), self.line_0.v) + self.arc.draw(context, render) + self.line_0.draw(context, render) + self.line_1.draw(context, render) + self.handle_left.draw(context, render) + self.handle_right.draw(context, render) + self.handle_center.draw(context, render) + self.label_r.draw(context, render) + self.label_a.draw(context, render) + self.feedback.draw(context, render) + + +class ArcAngleRadiusManipulator(ArcAngleManipulator): + """ + Manipulate angle and radius of an arc + when angle < 0 the arc center is on the left part of the circle + when angle > 0 the arc center is on the right part of the circle + bound to [-pi, pi] + """ + + def __init__(self, context, o, datablock, manipulator, handle_size, snap_callback=None): + ArcAngleManipulator.__init__(self, context, o, datablock, manipulator, handle_size, snap_callback) + self.handle_center = TriHandle(handle_size, arrow_size, draggable=True) + self.label_r.draggable = True + + def check_hover(self): + self.handle_right.check_hover(self.mouse_pos) + self.handle_center.check_hover(self.mouse_pos) + self.label_a.check_hover(self.mouse_pos) + self.label_r.check_hover(self.mouse_pos) + + def mouse_press(self, context, event): + if self.handle_right.hover: + self.active = True + self.original_angle = self.get_value(self.datablock, self.manipulator.prop1_name) + self.feedback.instructions(context, "Angle (degree)", "Drag to modify angle", [ + ('SHIFT', 'Round value'), + ('RIGHTCLICK or ESC', 'cancel') + ]) + self.handle_right.active = True + return True + if self.handle_center.hover: + self.active = True + self.original_radius = self.get_value(self.datablock, self.manipulator.prop2_name) + self.feedback.instructions(context, "Radius", "Drag to modify radius", [ + ('SHIFT', 'Round value'), + ('RIGHTCLICK or ESC', 'cancel') + ]) + self.handle_center.active = True + return True + if self.label_a.hover: + self.feedback.instructions(context, "Angle (degree)", "Use keyboard to modify angle", + [('ENTER', 'validate'), + ('RIGHTCLICK or ESC', 'cancel')]) + self.value_type = 'ROTATION' + self.label_value = self.get_value(self.datablock, self.manipulator.prop1_name) + self.label_a.active = True + self.keyboard_input_active = True + return True + if self.label_r.hover: + self.feedback.instructions(context, "Radius", "Use keyboard to modify radius", + [('ENTER', 'validate'), + ('RIGHTCLICK or ESC', 'cancel')]) + self.value_type = 'LENGTH' + self.label_r.active = True + self.keyboard_input_active = True + return True + return False + + def mouse_release(self, context, event): + self.check_hover() + self.active = False + self.handle_right.active = False + self.handle_center.active = False + return False + + def mouse_move(self, context, event): + self.mouse_position(event) + if self.handle_right.active: + self.update(context, event) + return True + elif self.handle_center.active: + self.update_radius(context, event) + return True + else: + self.check_hover() + return False + + def keyboard_done(self, context, event, value): + if self.value_type == 'LENGTH': + self.set_value(context, self.datablock, self.manipulator.prop2_name, value) + self.label_r.active = False + else: + self.set_value(context, self.datablock, self.manipulator.prop1_name, value) + self.label_a.active = False + return True + + def update_radius(self, context, event): + pt = self.get_pos3d(context) + c = self.arc.c + left = self.line_0.lerp(1) + p, t = intersect_point_line(pt, c, left) + radius = (left - p).length + if event.alt: + radius = round(radius, 1) + self.set_value(context, self.datablock, self.manipulator.prop2_name, radius) + + def cancel(self, context, event): + if self.handle_right.active: + self.mouse_release(context, event) + self.set_value(context, self.datablock, self.manipulator.prop1_name, self.original_angle) + if self.handle_center.active: + self.mouse_release(context, event) + self.set_value(context, self.datablock, self.manipulator.prop2_name, self.original_radius) + + +# ------------------------------------------------------------------ +# Define a single Manipulator Properties to store on object +# ------------------------------------------------------------------ + + +# Allow registering manipulators classes +manipulators_class_lookup = {} + + +def register_manipulator(type_key, manipulator_class): + if type_key in manipulators_class_lookup.keys(): + raise RuntimeError("Manipulator of type {} allready exists, unable to override".format(type_key)) + manipulators_class_lookup[type_key] = manipulator_class + + +class archipack_manipulator(PropertyGroup): + """ + A property group to add to manipulable objects + type_key: type of manipulator + prop1_name = the property name of object to modify + prop2_name = another property name of object to modify (eg: angle and radius) + p0, p1, p2 3d Vectors as base points to represent manipulators on screen + normal Vector normal of plane on with draw manipulator + """ + type_key = StringProperty(default='SIZE') + + # How 3d points are stored in manipulators ? + # SIZE = 2 absolute positionned and a scaling vector + # RADIUS = 1 absolute positionned (center) and 2 relatives (sides) + # POLYGON = 2 absolute positionned and a relative vector (for rect polygons) + + pts_mode = StringProperty(default='SIZE') + prop1_name = StringProperty() + prop2_name = StringProperty() + p0 = FloatVectorProperty(subtype='XYZ') + p1 = FloatVectorProperty(subtype='XYZ') + p2 = FloatVectorProperty(subtype='XYZ') + # allow orientation of manipulators by default on xy plane, + # but may be used to constrain heights on local object space + normal = FloatVectorProperty(subtype='XYZ', default=(0, 0, 1)) + + def set_pts(self, pts, normal=None): + """ + set 3d location of gl points (in object space) + pts: array of 3 vectors 3d + normal: optionnal vector 3d default to Z axis + """ + pts = [Vector(p) for p in pts] + self.p0, self.p1, self.p2 = pts + if normal is not None: + self.normal = Vector(normal) + + def get_pts(self, tM): + """ + convert points from local to world absolute + to draw them at the right place + tM : object's world matrix + """ + rM = tM.to_3x3() + if self.pts_mode in ['SIZE', 'POLYGON']: + return tM * self.p0, tM * self.p1, self.p2, rM * self.normal + else: + return tM * self.p0, rM * self.p1, rM * self.p2, rM * self.normal + + def get_prefs(self, context): + global __name__ + global arrow_size + global handle_size + try: + # retrieve addon name from imports + addon_name = __name__.split('.')[0] + prefs = context.user_preferences.addons[addon_name].preferences + arrow_size = prefs.arrow_size + handle_size = prefs.handle_size + except: + pass + + def setup(self, context, o, datablock, snap_callback=None): + """ + Factory return a manipulator object or None + o: object + datablock: datablock to modify + snap_callback: function call y + """ + + self.get_prefs(context) + + global manipulators_class_lookup + + if self.type_key not in manipulators_class_lookup.keys() or \ + not manipulators_class_lookup[self.type_key].poll(context): + # RuntimeError is overkill but may be enabled for debug purposes + # Silentely ignore allow skipping manipulators if / when deps as not meet + # manip stack will simply be filled with None objects + # raise RuntimeError("Manipulator of type {} not found".format(self.type_key)) + return None + + m = manipulators_class_lookup[self.type_key](context, o, datablock, self, handle_size, snap_callback) + # points storage model as described upside + self.pts_mode = m.pts_mode + return m + + +# ------------------------------------------------------------------ +# Define Manipulable to make a PropertyGroup manipulable +# ------------------------------------------------------------------ + + +class ARCHIPACK_OT_manipulate(Operator): + bl_idname = "archipack.manipulate" + bl_label = "Manipulate" + bl_description = "Manipulate" + bl_options = {'REGISTER', 'UNDO'} + + object_name = StringProperty(default="") + + @classmethod + def poll(self, context): + return context.active_object is not None + + def exit_selectmode(self, context, key): + """ + Hide select area on exit + """ + global manips + if key in manips.keys(): + if manips[key].manipulable is not None: + manips[key].manipulable.manipulable_exit_selectmode(context) + + def modal(self, context, event): + global manips + # Exit on stack change + # handle multiple object stack + # use object_name property to find manupulated object in stack + # select and make object active + # and exit when not found + if context.area is not None: + context.area.tag_redraw() + key = self.object_name + if check_stack(key): + self.exit_selectmode(context, key) + remove_manipulable(key) + # print("modal exit by check_stack(%s)" % (key)) + return {'FINISHED'} + + res = manips[key].manipulable.manipulable_modal(context, event) + + if 'FINISHED' in res: + self.exit_selectmode(context, key) + remove_manipulable(key) + # print("modal exit by {FINISHED}") + + return res + + def invoke(self, context, event): + if context.space_data.type == 'VIEW_3D': + context.window_manager.modal_handler_add(self) + return {'RUNNING_MODAL'} + else: + self.report({'WARNING'}, "Active space must be a View3d") + return {'CANCELLED'} + + +class ARCHIPACK_OT_disable_manipulate(Operator): + bl_idname = "archipack.disable_manipulate" + bl_label = "Disable Manipulate" + bl_description = "Disable any active manipulator" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(self, context): + return True + + def execute(self, context): + empty_stack() + return {'FINISHED'} + + +class Manipulable(): + """ + A class extending PropertyGroup to setup gl manipulators + Beware : prevent crash calling manipulable_disable() + before changing manipulated data structure + """ + manipulators = CollectionProperty( + type=archipack_manipulator, + # options={'SKIP_SAVE'}, + # options={'HIDDEN'}, + description="store 3d points to draw gl manipulators" + ) + manipulable_refresh = BoolProperty( + default=False, + options={'SKIP_SAVE'}, + description="Flag enable to rebuild manipulators when data model change" + ) + manipulate_mode = BoolProperty( + default=False, + options={'SKIP_SAVE'}, + description="Flag manipulation state so we are able to toggle" + ) + select_mode = BoolProperty( + default=False, + options={'SKIP_SAVE'}, + description="Flag select state so we are able to toggle" + ) + manipulable_selectable = BoolProperty( + default=False, + options={'SKIP_SAVE'}, + description="Flag make manipulators selectable" + ) + keymap = None + + # selectable manipulators + manipulable_area = GlCursorArea() + manipulable_start_point = Vector((0, 0)) + manipulable_end_point = Vector((0, 0)) + manipulable_draw_handler = None + + def setup_manipulators(self): + """ + Must implement manipulators creation + TODO: call from update and manipulable_setup + """ + raise NotImplementedError + + def manipulable_draw_callback(self, _self, context): + self.manipulable_area.draw(context) + + def manipulable_disable(self, context): + """ + disable gl draw handlers + """ + o = context.active_object + if o is not None: + self.manipulable_exit_selectmode(context) + remove_manipulable(o.name) + self.manip_stack = add_manipulable(o.name, self) + + self.manipulate_mode = False + self.select_mode = False + + def manipulable_exit_selectmode(self, context): + self.manipulable_area.disable() + self.select_mode = False + # remove select draw handler + if self.manipulable_draw_handler is not None: + bpy.types.SpaceView3D.draw_handler_remove( + self.manipulable_draw_handler, + 'WINDOW') + self.manipulable_draw_handler = None + + def manipulable_setup(self, context): + """ + TODO: Implement the setup part as per parent object basis + """ + self.manipulable_disable(context) + o = context.active_object + self.setup_manipulators() + for m in self.manipulators: + self.manip_stack.append(m.setup(context, o, self)) + + def _manipulable_invoke(self, context): + + object_name = context.active_object.name + + # store a reference to self for operators + add_manipulable(object_name, self) + + # copy context so manipulator always use + # invoke time context + ctx = context.copy() + + # take care of context switching + # when call from outside of 3d view + if context.space_data.type != 'VIEW_3D': + for window in bpy.context.window_manager.windows: + screen = window.screen + for area in screen.areas: + if area.type == 'VIEW_3D': + ctx['area'] = area + for region in area.regions: + if region.type == 'WINDOW': + ctx['region'] = region + break + if ctx is not None: + bpy.ops.archipack.manipulate(ctx, 'INVOKE_DEFAULT', object_name=object_name) + + def manipulable_invoke(self, context): + """ + call this in operator invoke() + NB: + if override dont forget to call: + _manipulable_invoke(context) + + """ + # print("manipulable_invoke self.manipulate_mode:%s" % (self.manipulate_mode)) + + if self.manipulate_mode: + self.manipulable_disable(context) + return False + # else: + # bpy.ops.archipack.disable_manipulate('INVOKE_DEFAULT') + + # self.manip_stack = [] + # kills other's manipulators + # self.manipulate_mode = True + self.manipulable_setup(context) + self.manipulate_mode = True + + self._manipulable_invoke(context) + + return True + + def manipulable_modal(self, context, event): + """ + call in operator modal() + should not be overriden + as it provide all needed + functionnality out of the box + """ + # setup again when manipulators type change + if self.manipulable_refresh: + # print("manipulable_refresh") + self.manipulable_refresh = False + self.manipulable_setup(context) + self.manipulate_mode = True + + if context.area is None: + self.manipulable_disable(context) + return {'FINISHED'} + + context.area.tag_redraw() + + if self.keymap is None: + self.keymap = Keymaps(context) + + if self.keymap.check(event, self.keymap.undo): + # user feedback on undo by disabling manipulators + self.manipulable_disable(context) + return {'FINISHED'} + + # clean up manipulator on delete + if self.keymap.check(event, self.keymap.delete): # {'X'}: + # @TODO: + # for doors and windows, seek and destroy holes object if any + # a dedicated delete method into those objects may be an option ? + # A type check is required any way we choose + # + # Time for a generic archipack's datablock getter / filter into utils + # + # May also be implemented into nearly hidden "reference point" + # to delete / duplicate / link duplicate / unlink of + # a complete set of wall, doors and windows at once + self.manipulable_disable(context) + + if bpy.ops.object.delete.poll(): + bpy.ops.object.delete('INVOKE_DEFAULT', use_global=False) + + return {'FINISHED'} + + """ + # handle keyborad for select mode + if self.select_mode: + if event.type in {'A'} and event.value == 'RELEASE': + return {'RUNNING_MODAL'} + """ + + for manipulator in self.manip_stack: + # manipulator should return false on left mouse release + # so proper release handler is called + # and return true to call manipulate when required + # print("manipulator:%s" % manipulator) + if manipulator is not None and manipulator.modal(context, event): + self.manipulable_manipulate(context, event, manipulator) + return {'RUNNING_MODAL'} + + # print("Manipulable %s %s" % (event.type, event.value)) + + # Manipulators are not active so check for selection + if event.type == 'LEFTMOUSE': + + # either we are starting select mode + # user press on area not over maniuplator + # Prevent 3 mouse emultation to select when alt pressed + if self.manipulable_selectable and event.value == 'PRESS' and not event.alt: + self.select_mode = True + self.manipulable_area.enable() + self.manipulable_start_point = Vector((event.mouse_region_x, event.mouse_region_y)) + self.manipulable_area.set_location( + context, + self.manipulable_start_point, + self.manipulable_start_point) + # add a select draw handler + args = (self, context) + self.manipulable_draw_handler = bpy.types.SpaceView3D.draw_handler_add( + self.manipulable_draw_callback, + args, + 'WINDOW', + 'POST_PIXEL') + # don't keep focus + # as this prevent click over ui + # return {'RUNNING_MODAL'} + + elif event.value == 'RELEASE': + if self.select_mode: + # confirm selection + + self.manipulable_exit_selectmode(context) + + # keep focus + # return {'RUNNING_MODAL'} + + else: + # allow manipulator action on release + for manipulator in self.manip_stack: + if manipulator is not None and manipulator.selectable: + manipulator.selected = False + self.manipulable_release(context) + + elif self.select_mode and event.type == 'MOUSEMOVE' and event.value == 'PRESS': + # update select area size + self.manipulable_end_point = Vector((event.mouse_region_x, event.mouse_region_y)) + self.manipulable_area.set_location( + context, + self.manipulable_start_point, + self.manipulable_end_point) + if event.shift: + # deselect + for i, manipulator in enumerate(self.manip_stack): + if manipulator is not None and manipulator.selectable: + manipulator.deselect(self.manipulable_area) + else: + # select / more + for i, manipulator in enumerate(self.manip_stack): + if manipulator is not None and manipulator.selectable: + manipulator.select(self.manipulable_area) + # keep focus to prevent left select mouse to actually move object + return {'RUNNING_MODAL'} + + # event.alt here to prevent 3 button mouse emulation exit while zooming + if event.type in {'RIGHTMOUSE', 'ESC'} and event.value == 'PRESS' and not event.alt: + self.manipulable_disable(context) + self.manipulable_exit(context) + return {'FINISHED'} + + return {'PASS_THROUGH'} + + # Callbacks + def manipulable_release(self, context): + """ + Override with action to do on mouse release + eg: big update + """ + return + + def manipulable_exit(self, context): + """ + Override with action to do when modal exit + """ + return + + def manipulable_manipulate(self, context, event, manipulator): + """ + Override with action to do when a handle is active (pressed and mousemove) + """ + return + + +@persistent +def cleanup(dummy=None): + empty_stack() + + +def register(): + # Register default manipulators + global manips + global manipulators_class_lookup + manipulators_class_lookup = {} + manips = {} + register_manipulator('SIZE', SizeManipulator) + register_manipulator('SIZE_LOC', SizeLocationManipulator) + register_manipulator('ANGLE', AngleManipulator) + register_manipulator('DUMB_ANGLE', DumbAngleManipulator) + register_manipulator('ARC_ANGLE_RADIUS', ArcAngleRadiusManipulator) + register_manipulator('COUNTER', CounterManipulator) + register_manipulator('DUMB_SIZE', DumbSizeManipulator) + register_manipulator('DELTA_LOC', DeltaLocationManipulator) + register_manipulator('DUMB_STRING', DumbStringManipulator) + + # snap aware size loc + register_manipulator('SNAP_SIZE_LOC', SnapSizeLocationManipulator) + # register_manipulator('SNAP_POINT', SnapPointManipulator) + # wall's line based object snap + register_manipulator('WALL_SNAP', WallSnapManipulator) + bpy.utils.register_class(ARCHIPACK_OT_manipulate) + bpy.utils.register_class(ARCHIPACK_OT_disable_manipulate) + bpy.utils.register_class(archipack_manipulator) + bpy.app.handlers.load_pre.append(cleanup) + + +def unregister(): + global manips + global manipulators_class_lookup + empty_stack() + del manips + manipulators_class_lookup.clear() + del manipulators_class_lookup + bpy.utils.unregister_class(ARCHIPACK_OT_manipulate) + bpy.utils.unregister_class(ARCHIPACK_OT_disable_manipulate) + bpy.utils.unregister_class(archipack_manipulator) + bpy.app.handlers.load_pre.remove(cleanup) diff --git a/archipack/archipack_object.py b/archipack/archipack_object.py new file mode 100644 index 00000000..18ae43e5 --- /dev/null +++ b/archipack/archipack_object.py @@ -0,0 +1,237 @@ +# -*- coding:utf-8 -*- + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110- 1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +# + +# ---------------------------------------------------------- +# Author: Stephen Leger (s-leger) +# +# ---------------------------------------------------------- +# noinspection PyUnresolvedReferences +import bpy +# noinspection PyUnresolvedReferences +from bpy.props import BoolProperty, StringProperty +from mathutils import Vector, Matrix +from mathutils.geometry import ( + intersect_line_plane + ) +from bpy_extras.view3d_utils import ( + region_2d_to_origin_3d, + region_2d_to_vector_3d + ) +from .materialutils import MaterialUtils + + +class ArchipackObject(): + """ + Shared property of archipack's objects PropertyGroup + provide basic support for copy to selected + and datablock access / filtering by object + """ + + def iskindof(self, o, typ): + """ + return true if object contains databloc of typ name + """ + return o.data is not None and typ in o.data + + @classmethod + def filter(cls, o): + """ + Filter object with this class in data + return + True when object contains this datablock + False otherwhise + usage: + class_name.filter(object) from outside world + self.__class__.filter(object) from instance + """ + try: + return cls.__name__ in o.data + except: + pass + return False + + @classmethod + def datablock(cls, o): + """ + Retrieve datablock from base object + return + datablock when found + None when not found + usage: + class_name.datablock(object) from outside world + self.__class__.datablock(object) from instance + """ + try: + return getattr(o.data, cls.__name__)[0] + except: + pass + return None + + def find_in_selection(self, context, auto_update=True): + """ + find witch selected object this datablock instance belongs to + store context to be able to restore after oops + provide support for "copy to selected" + return + object or None when instance not found in selected objects + """ + if auto_update is False: + return None + + active = context.active_object + selected = [o for o in context.selected_objects] + + for o in selected: + if self.__class__.datablock(o) == self: + self.previously_selected = selected + self.previously_active = active + return o + + return None + + def restore_context(self, context): + # restore context + bpy.ops.object.select_all(action="DESELECT") + + try: + for o in self.previously_selected: + o.select = True + except: + pass + + self.previously_active.select = True + context.scene.objects.active = self.previously_active + self.previously_selected = None + self.previously_active = None + + +class ArchipackCreateTool(): + """ + Shared property of archipack's create tool Operator + """ + auto_manipulate = BoolProperty( + name="Auto manipulate", + description="Enable object's manipulators after create", + options={'SKIP_SAVE'}, + default=True + ) + filepath = StringProperty( + options={'SKIP_SAVE'}, + name="Preset", + description="Full filename of python preset to load at create time", + default="" + ) + + @property + def archipack_category(self): + """ + return target object name from ARCHIPACK_OT_object_name + """ + return self.bl_idname[13:] + + def load_preset(self, d): + """ + Load python preset + preset: full filename.py with path + """ + d.auto_update = False + if self.filepath != "": + try: + # print("Archipack loading preset: %s" % d.auto_update) + bpy.ops.script.python_file_run(filepath=self.filepath) + # print("Archipack preset loaded auto_update: %s" % d.auto_update) + except: + print("Archipack unable to load preset file : %s" % (self.filepath)) + pass + d.auto_update = True + + def add_material(self, o): + try: + getattr(MaterialUtils, "add_" + self.archipack_category + "_materials")(o) + except: + print("Archipack MaterialUtils.add_%s_materials not found" % (self.archipack_category)) + pass + + def manipulate(self): + if self.auto_manipulate: + try: + op = getattr(bpy.ops.archipack, self.archipack_category + "_manipulate") + if op.poll(): + op('INVOKE_DEFAULT') + except: + print("Archipack bpy.ops.archipack.%s_manipulate not found" % (self.archipack_category)) + pass + + +class ArchpackDrawTool(): + """ + Draw tools + """ + def mouse_to_plane(self, context, event, origin=Vector((0, 0, 0)), normal=Vector((0, 0, 1))): + """ + convert mouse pos to 3d point over plane defined by origin and normal + """ + region = context.region + rv3d = context.region_data + co2d = (event.mouse_region_x, event.mouse_region_y) + view_vector_mouse = region_2d_to_vector_3d(region, rv3d, co2d) + ray_origin_mouse = region_2d_to_origin_3d(region, rv3d, co2d) + pt = intersect_line_plane(ray_origin_mouse, ray_origin_mouse + view_vector_mouse, + origin, normal, False) + # fix issue with parallel plane + if pt is None: + pt = intersect_line_plane(ray_origin_mouse, ray_origin_mouse + view_vector_mouse, + origin, view_vector_mouse, False) + return pt + + def mouse_to_scene_raycast(self, context, event): + """ + convert mouse pos to 3d point over plane defined by origin and normal + """ + region = context.region + rv3d = context.region_data + co2d = (event.mouse_region_x, event.mouse_region_y) + view_vector_mouse = region_2d_to_vector_3d(region, rv3d, co2d) + ray_origin_mouse = region_2d_to_origin_3d(region, rv3d, co2d) + res, pos, normal, face_index, object, matrix_world = context.scene.ray_cast( + ray_origin_mouse, + view_vector_mouse) + return res, pos, normal, face_index, object, matrix_world + + def mouse_hover_wall(self, context, event): + """ + convert mouse pos to matrix at bottom of surrounded wall, y oriented outside wall + """ + res, pt, y, i, o, tM = self.mouse_to_scene_raycast(context, event) + if res and o.data is not None and 'archipack_wall2' in o.data: + z = Vector((0, 0, 1)) + d = o.data.archipack_wall2[0] + y = -y + pt += (0.5 * d.width) * y.normalized() + x = y.cross(z) + return True, Matrix([ + [x.x, y.x, z.x, pt.x], + [x.y, y.y, z.y, pt.y], + [x.z, y.z, z.z, o.matrix_world.translation.z], + [0, 0, 0, 1] + ]), o, y + return False, Matrix(), None, Vector() diff --git a/archipack/archipack_polylib.py b/archipack/archipack_polylib.py new file mode 100644 index 00000000..886029ba --- /dev/null +++ b/archipack/archipack_polylib.py @@ -0,0 +1,2274 @@ +# -*- coding:utf-8 -*- + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110- 1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +# + +# ---------------------------------------------------------- +# Author: Stephen Leger (s-leger) +# +# ---------------------------------------------------------- + +bl_info = { + 'name': 'PolyLib', + 'description': 'Polygons detection from unordered splines', + 'author': 's-leger', + 'license': 'GPL', + 'deps': 'shapely', + 'version': (1, 1), + 'blender': (2, 7, 8), + 'location': 'View3D > Tools > Polygons', + 'warning': '', + 'wiki_url': 'https://github.com/s-leger/blenderPolygons/wiki', + 'tracker_url': 'https://github.com/s-leger/blenderPolygons/issues', + 'link': 'https://github.com/s-leger/blenderPolygons', + 'support': 'COMMUNITY', + 'category': '3D View' + } + +import sys +import time +import bpy +import bgl +import numpy as np +from math import cos, sin, pi, atan2 +import bmesh + +# let shapely import raise ImportError when missing +import shapely.ops +import shapely.prepared +from shapely.geometry import Point as ShapelyPoint +from shapely.geometry import Polygon as ShapelyPolygon + +try: + import shapely.speedups + if shapely.speedups.available: + shapely.speedups.enable() +except: + pass + +from .bitarray import BitArray +from .pyqtree import _QuadTree +from mathutils import Vector, Matrix +from mathutils.geometry import intersect_line_plane, interpolate_bezier +from bpy_extras import view3d_utils +from bpy.types import Operator, PropertyGroup +from bpy.props import StringProperty, FloatProperty, PointerProperty, EnumProperty, IntProperty, BoolProperty +from bpy.app.handlers import persistent +from .materialutils import MaterialUtils +from .archipack_gl import ( + FeedbackPanel, + GlCursorFence, + GlCursorArea, + GlLine, + GlPolyline +) + +# module globals vars dict +vars_dict = { + # spacial tree for segments and points + 'seg_tree': None, + 'point_tree': None, + # keep track of shapely geometry selection sets + 'select_polygons': None, + 'select_lines': None, + 'select_points': None + } + + +# module constants +# precision 1e-4 = 0.1mm +EPSILON = 1.0e-4 +# Qtree params +MAX_ITEMS = 10 +MAX_DEPTH = 20 + + +class CoordSys(object): + """ + reference coordsys + world : matrix from local to world + invert: matrix from world to local + width, height: bonding region size + """ + def __init__(self, objs): + x = [] + y = [] + if len(objs) > 0: + if hasattr(objs[0], 'bound_box'): + for obj in objs: + pos = obj.location + x.append(obj.bound_box[0][0] + pos.x) + x.append(obj.bound_box[6][0] + pos.x) + y.append(obj.bound_box[0][1] + pos.y) + y.append(obj.bound_box[6][1] + pos.y) + elif hasattr(objs[0], 'bounds'): + for geom in objs: + x0, y0, x1, y1 = geom.bounds + x.append(x0) + x.append(x1) + y.append(y0) + y.append(y1) + else: + raise Exception("CoordSys require at least one object with bounds or bound_box property to initialize") + else: + raise Exception("CoordSys require at least one object to initialize bounds") + x0 = min(x) + y0 = min(y) + x1 = max(x) + y1 = max(y) + width, height = x1 - x0, y1 - y0 + midx, midy = x0 + width / 2.0, y0 + height / 2.0 + # reference coordsys bounding box center + self.world = Matrix([ + [1, 0, 0, midx], + [0, 1, 0, midy], + [0, 0, 1, 0], + [0, 0, 0, 1], + ]) + self.invert = self.world.inverted() + self.width = width + self.height = height + + +class Prolongement(): + """ intersection of two segments outside segment (projection) + c0 = extremite sur le segment courant + c1 = intersection point on oposite segment + id = oposite segment id + t = param t on oposite segment + d = distance from ends to segment + insert = do we need to insert the point on other segment + use id, c1 and t to insert segment slices + """ + def __init__(self, c0, c1, id, t, d): + self.length = c0.distance(c1) + self.c0 = c0 + self.c1 = c1 + self.id = id + self.t = t + self.d = d + + +class Point(): + + def __init__(self, co, precision=EPSILON): + self.users = 0 + self.co = tuple(co) + x, y, z = co + self.shapeIds = [] + self.bounds = (x - precision, y - precision, x + precision, y + precision) + + @property + def geom(self): + return ShapelyPoint(self.co) + + def vect(self, point): + """ vector from this point to another """ + return np.subtract(point.co, self.co) + + def distance(self, point): + """ euclidian distance between points """ + return np.linalg.norm(self.vect(point)) + + def add_user(self): + self.users += 1 + + +class Segment(): + + def __init__(self, c0, c1, extend=EPSILON): + + self.c0 = c0 + self.c1 = c1 + self._splits = [] + + self.available = True + # ensure uniqueness when merge + + self.opposite = False + # this seg has an opposite + + self.original = False + # source of opposite + + x0, y0, z0 = c0.co + x1, y1, z1 = c1.co + self.bounds = (min(x0, x1) - extend, min(y0, y1) - extend, max(x0, x1) + extend, max(y0, y1) + extend) + + @property + def splits(self): + return sorted(self._splits) + + @property + def vect(self): + """ vector c0-c1""" + return np.subtract(self.c1.co, self.c0.co) + + @property + def vect_2d(self): + v = self.vect + v[2] = 0 + return v + + def lerp(self, t): + return np.add(self.c0.co, np.multiply(t, self.vect)) + + def _point_sur_segment(self, point): + """ _point_sur_segment + point: Point + t: param t de l'intersection sur le segment courant + d: distance laterale perpendiculaire + """ + vect = self.vect + dp = point.vect(self.c0) + dl = np.linalg.norm(vect) + d = np.linalg.norm(np.cross(vect, dp)) / dl + t = -np.divide(np.dot(dp, vect), np.multiply(dl, dl)) + if d < EPSILON: + if t > 0 and t < 1: + self._append_splits((t, point)) + + def is_end(self, point): + return point == self.c0 or point == self.c1 + + def min_intersect_dist(self, t, point): + """ distance intersection extremite la plus proche + t: param t de l'intersection sur le segment courant + point: Point d'intersection + return d: distance + """ + if t > 0.5: + return self.c1.distance(point) + else: + return self.c0.distance(point) + + def intersect(self, segment): + """ point_sur_segment return + p: point d'intersection + u: param t de l'intersection sur le segment courant + v: param t de l'intersection sur le segment segment + """ + v2d = self.vect_2d + c2 = np.cross(segment.vect_2d, (0, 0, 1)) + d = np.dot(v2d, c2) + if d == 0: + # segments paralleles + segment._point_sur_segment(self.c0) + segment._point_sur_segment(self.c1) + self._point_sur_segment(segment.c0) + self._point_sur_segment(segment.c1) + return False, 0, 0, 0 + c1 = np.cross(v2d, (0, 0, 1)) + v3 = self.c0.vect(segment.c0) + v3[2] = 0.0 + u = np.dot(c2, v3) / d + v = np.dot(c1, v3) / d + co = self.lerp(u) + return True, co, u, v + + def _append_splits(self, split): + """ + append a unique split point + """ + if split not in self._splits: + self._splits.append(split) + + def slice(self, d, t, point): + if d > EPSILON: + if t > 0.5: + if point != self.c1: + self._append_splits((t, point)) + else: + if point != self.c0: + self._append_splits((t, point)) + + def add_user(self): + self.c0.add_user() + self.c1.add_user() + + def consume(self): + self.available = False + + +class Shape(): + """ + Ensure uniqueness and fix precision issues by design + implicit closed with last point + require point_tree and seg_tree + """ + + def __init__(self, points=[]): + """ + @vertex: list of coords + """ + self.available = True + # Ensure uniqueness of shape when merging + + self._segs = [] + # Shape segments + + self.shapeId = [] + # Id of shape in shapes to keep a track of shape parts when merging + + self._create_segments(points) + + def _create_segments(self, points): + global vars_dict + if vars_dict['seg_tree'] is None: + raise RuntimeError('Shape._create_segments() require spacial index ') + # skip null segments with unique points test + self._segs = list(vars_dict['seg_tree'].newSegment(points[v], points[v + 1]) + for v in range(len(points) - 1) if points[v] != points[v + 1]) + + @property + def coords(self): + coords = list(seg.c0.co for seg in self._segs) + coords.append(self.c1.co) + return coords + + @property + def points(self): + points = list(seg.c0 for seg in self._segs) + points.append(self.c1) + return points + + @property + def c0(self): + if not self.valid: + raise RuntimeError('Shape does not contains any segments') + return self._segs[0].c0 + + @property + def c1(self): + if not self.valid: + raise RuntimeError('Shape does not contains any segments') + return self._segs[-1].c1 + + @property + def nbsegs(self): + return len(self._segs) + + @property + def valid(self): + return self.nbsegs > 0 + + @property + def closed(self): + return self.valid and bool(self.c0 == self.c1) + + def merge(self, shape): + """ merge this shape with specified shape + shapes must share at least one vertex + """ + if not self.valid or not shape.valid: + raise RuntimeError('Trying to merge invalid shape') + if self.c1 == shape.c1 or self.c0 == shape.c0: + shape._reverse() + if self.c1 == shape.c0: + self._segs += shape._segs + elif shape.c1 == self.c0: + self._segs = shape._segs + self._segs + else: + # should never happen + raise RuntimeError("Shape merge failed {} {} {} {}".format( + id(self), id(shape), self.shapeId, shape.shapeId)) + + def _reverse(self): + """ + reverse vertex order + """ + points = self.points[::-1] + self._create_segments(points) + + def slice(self, shapes): + """ + slice shape into smaller parts at intersections + """ + if not self.valid: + raise RuntimeError('Cant slice invalid shape') + points = [] + for seg in self._segs: + if seg.available and not seg.original: + seg.consume() + points.append(seg.c0) + if seg.c1.users > 2: + points.append(seg.c1) + shape = Shape(points) + shapes.append(shape) + points = [] + if len(points) > 0: + points.append(self.c1) + shape = Shape(points) + shapes.append(shape) + + def add_points(self): + """ + add points from intersection data + """ + points = [] + if self.nbsegs > 0: + for seg in self._segs: + points.append(seg.c0) + for split in seg.splits: + points.append(split[1]) + points.append(self.c1) + self._create_segments(points) + + def set_users(self): + """ + add users on segments and points + """ + for seg in self._segs: + seg.add_user() + + def consume(self): + self.available = False + + +class Qtree(_QuadTree): + """ + The top spatial index to be created by the user. Once created it can be + populated with geographically placed members that can later be tested for + intersection with a user inputted geographic bounding box. + """ + def __init__(self, coordsys, extend=EPSILON, max_items=MAX_ITEMS, max_depth=MAX_DEPTH): + """ + objs may be blender objects or shapely geoms + extend: how much seek arround + """ + self._extend = extend + self._geoms = [] + + # store input coordsys + self.coordsys = coordsys + + super(Qtree, self).__init__(0, 0, coordsys.width, coordsys.height, max_items, max_depth) + + @property + def ngeoms(self): + return len(self._geoms) + + def build(self, geoms): + """ + Build a spacial index from shapely geoms + """ + t = time.time() + self._geoms = geoms + for i, geom in enumerate(geoms): + self._insert(i, geom.bounds) + print("Qtree.build() :%.2f seconds" % (time.time() - t)) + + def insert(self, id, geom): + self._geoms.append(geom) + self._insert(id, geom.bounds) + + def newPoint(self, co): + point = Point(co, self._extend) + count, found = self.intersects(point) + for id in found: + return self._geoms[id] + self.insert(self.ngeoms, point) + return point + + def newSegment(self, c0, c1): + """ + allow "opposite" segments, + those segments are not found by intersects + and not stored in self.geoms + """ + new_seg = Segment(c0, c1, self._extend) + count, found = self.intersects(new_seg) + for id in found: + old_seg = self._geoms[id] + if (old_seg.c0 == c0 and old_seg.c1 == c1): + return old_seg + if (old_seg.c0 == c1 and old_seg.c1 == c0): + if not old_seg.opposite: + old_seg.opposite = new_seg + new_seg.original = old_seg + return old_seg.opposite + self.insert(self.ngeoms, new_seg) + return new_seg + + def intersects(self, geom): + selection = list(self._intersect(geom.bounds)) + count = len(selection) + return count, sorted(selection) + + +class Io(): + + @staticmethod + def ensure_iterable(obj): + try: + iter(obj) + except TypeError: + obj = [obj] + return obj + + # Conversion methods + @staticmethod + def _to_geom(shape): + if not shape.valid: + raise RuntimeError('Cant convert invalid shape to Shapely LineString') + return shapely.geometry.LineString(shape.coords) + + @staticmethod + def shapes_to_geoms(shapes): + return [Io._to_geom(shape) for shape in shapes] + + @staticmethod + def _to_shape(geometry, shapes): + global vars_dict + if vars_dict['point_tree'] is None: + raise RuntimeError("geoms to shapes require a global point_tree spacial index") + if hasattr(geometry, 'exterior'): + Io._to_shape(geometry.exterior, shapes) + for geom in geometry.interiors: + Io._to_shape(geom, shapes) + elif hasattr(geometry, 'geoms'): + # Multi and Collections + for geom in geometry.geoms: + Io._to_shape(geom, shapes) + else: + points = list(vars_dict['point_tree'].newPoint(p) for p in list(geometry.coords)) + shape = Shape(points) + shapes.append(shape) + + @staticmethod + def geoms_to_shapes(geoms, shapes=[]): + for geom in geoms: + Io._to_shape(geom, shapes) + return shapes + + # Input methods + @staticmethod + def _interpolate_bezier(pts, wM, p0, p1, resolution): + # straight segment, worth testing here + # since this can lower points count by a resolution factor + # use normalized to handle non linear t + if resolution == 0: + pts.append(wM * p0.co.to_3d()) + else: + v = (p1.co - p0.co).normalized() + d1 = (p0.handle_right - p0.co).normalized() + d2 = (p1.co - p1.handle_left).normalized() + if d1 == v and d2 == v: + pts.append(wM * p0.co.to_3d()) + else: + seg = interpolate_bezier(wM * p0.co, + wM * p0.handle_right, + wM * p1.handle_left, + wM * p1.co, + resolution) + for i in range(resolution - 1): + pts.append(seg[i].to_3d()) + + @staticmethod + def _coords_from_spline(wM, resolution, spline): + pts = [] + if spline.type == 'POLY': + pts = [wM * p.co.to_3d() for p in spline.points] + if spline.use_cyclic_u: + pts.append(pts[0]) + elif spline.type == 'BEZIER': + points = spline.bezier_points + for i in range(1, len(points)): + p0 = points[i - 1] + p1 = points[i] + Io._interpolate_bezier(pts, wM, p0, p1, resolution) + pts.append(wM * points[-1].co) + if spline.use_cyclic_u: + p0 = points[-1] + p1 = points[0] + Io._interpolate_bezier(pts, wM, p0, p1, resolution) + pts.append(pts[0]) + return pts + + @staticmethod + def _add_geom_from_curve(curve, invert_world, resolution, geoms): + wM = invert_world * curve.matrix_world + for spline in curve.data.splines: + pts = Io._coords_from_spline(wM, resolution, spline) + geom = shapely.geometry.LineString(pts) + geoms.append(geom) + + @staticmethod + def curves_to_geoms(curves, resolution, geoms=[]): + """ + @curves : blender curves collection + Return coordsys for outputs + """ + curves = Io.ensure_iterable(curves) + coordsys = CoordSys(curves) + t = time.time() + for curve in curves: + Io._add_geom_from_curve(curve, coordsys.invert, resolution, geoms) + print("Io.curves_as_line() :%.2f seconds" % (time.time() - t)) + return coordsys + + @staticmethod + def _add_shape_from_curve(curve, invert_world, resolution, shapes): + global vars_dict + wM = invert_world * curve.matrix_world + for spline in curve.data.splines: + pts = Io._coords_from_spline(wM, resolution, spline) + pts = [vars_dict['point_tree'].newPoint(pt) for pt in pts] + shape = Shape(points=pts) + shapes.append(shape) + + @staticmethod + def curves_to_shapes(curves, coordsys, resolution, shapes=[]): + """ + @curves : blender curves collection + Return simple shapes + """ + curves = Io.ensure_iterable(curves) + t = time.time() + for curve in curves: + Io._add_shape_from_curve(curve, coordsys.invert, resolution, shapes) + print("Io.curves_to_shapes() :%.2f seconds" % (time.time() - t)) + + # Output methods + @staticmethod + def _poly_to_wall(scene, matrix_world, poly, height, name): + global vars_dict + curve = bpy.data.curves.new(name, type='CURVE') + curve.dimensions = "2D" + curve.fill_mode = 'BOTH' + curve.extrude = height + n_ext = len(poly.exterior.coords) + n_int = len(poly.interiors) + Io._add_spline(curve, poly.exterior) + for geom in poly.interiors: + Io._add_spline(curve, geom) + curve_obj = bpy.data.objects.new(name, curve) + curve_obj.matrix_world = matrix_world + scene.objects.link(curve_obj) + curve_obj.select = True + scene.objects.active = curve_obj + return n_ext, n_int, curve_obj + + @staticmethod + def wall_uv(me, bm): + + for face in bm.faces: + face.select = face.material_index > 0 + + bmesh.update_edit_mesh(me, True) + bpy.ops.uv.cube_project(scale_to_bounds=False, correct_aspect=True) + + for face in bm.faces: + face.select = face.material_index < 1 + + bmesh.update_edit_mesh(me, True) + bpy.ops.uv.smart_project(use_aspect=True, stretch_to_bounds=False) + + @staticmethod + def to_wall(scene, coordsys, geoms, height, name, walls=[]): + """ + use curve extrude as it does respect vertices number and is not removing doubles + so it is easy to set material index + cap faces are tri, sides faces are quads + """ + bpy.ops.object.select_all(action='DESELECT') + geoms = Io.ensure_iterable(geoms) + for poly in geoms: + if hasattr(poly, 'exterior'): + half_height = height / 2.0 + n_ext, n_int, obj = Io._poly_to_wall(scene, coordsys.world, poly, half_height, name) + bpy.ops.object.convert(target="MESH") + bpy.ops.object.mode_set(mode='EDIT') + me = obj.data + bm = bmesh.from_edit_mesh(me) + bm.verts.ensure_lookup_table() + bm.faces.ensure_lookup_table() + for v in bm.verts: + v.co.z += half_height + nfaces = 0 + for i, f in enumerate(bm.faces): + bm.faces[i].material_index = 2 + if len(f.verts) > 3: + nfaces = i + break + # walls without holes are inside + mat_index = 0 if n_int > 0 else 1 + for i in range(nfaces, nfaces + n_ext - 1): + bm.faces[i].material_index = mat_index + for i in range(nfaces + n_ext - 1, len(bm.faces)): + bm.faces[i].material_index = 1 + bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.003) + bmesh.update_edit_mesh(me, True) + Io.wall_uv(me, bm) + bpy.ops.mesh.dissolve_limited(angle_limit=0.00349066, delimit={'NORMAL'}) + bpy.ops.mesh.dissolve_degenerate() + bpy.ops.object.mode_set(mode='OBJECT') + bpy.ops.object.shade_flat() + MaterialUtils.add_wall_materials(obj) + walls.append(obj) + return walls + + @staticmethod + def _add_spline(curve, geometry): + coords = list(geometry.coords) + spline = curve.splines.new('POLY') + spline.use_endpoint_u = False + spline.use_cyclic_u = coords[0] == coords[-1] + spline.points.add(len(coords) - 1) + for i, coord in enumerate(coords): + x, y, z = Vector(coord).to_3d() + spline.points[i].co = (x, y, z, 1) + + @staticmethod + def _as_spline(curve, geometry): + """ + add a spline into a blender curve + @curve : blender curve + """ + if hasattr(geometry, 'exterior'): + # Polygon + Io._add_spline(curve, geometry.exterior) + for geom in geometry.interiors: + Io._add_spline(curve, geom) + elif hasattr(geometry, 'geoms'): + # Multi and Collections + for geom in geometry.geoms: + Io._as_spline(curve, geom) + else: + # LinearRing, LineString and Shape + Io._add_spline(curve, geometry) + + @staticmethod + def to_curve(scene, coordsys, geoms, name, dimensions='3D'): + global vars_dict + t = time.time() + geoms = Io.ensure_iterable(geoms) + curve = bpy.data.curves.new(name, type='CURVE') + curve.dimensions = dimensions + for geom in geoms: + Io._as_spline(curve, geom) + curve_obj = bpy.data.objects.new(name, curve) + curve_obj.matrix_world = coordsys.world + scene.objects.link(curve_obj) + curve_obj.select = True + print("Io.to_curves() :%.2f seconds" % (time.time() - t)) + return curve_obj + + @staticmethod + def to_curves(scene, coordsys, geoms, name, dimensions='3D'): + geoms = Io.ensure_iterable(geoms) + return [Io.to_curve(scene, coordsys, geom, name, dimensions) for geom in geoms] + + +class ShapelyOps(): + + @staticmethod + def min_bounding_rect(geom): + """ min_bounding_rect + minimum area oriented bounding rect + """ + # Compute edges (x2-x1,y2-y1) + if geom.convex_hull.geom_type == 'Polygon': + hull_points_2d = [list(coord[0:2]) for coord in list(geom.convex_hull.exterior.coords)] + else: + hull_points_2d = [list(coord[0:2]) for coord in list(geom.convex_hull.coords)] + edges = np.zeros((len(hull_points_2d) - 1, 2)) + # empty 2 column array + for i in range(len(edges)): + edge_x = hull_points_2d[i + 1][0] - hull_points_2d[i][0] + edge_y = hull_points_2d[i + 1][1] - hull_points_2d[i][1] + edges[i] = [edge_x, edge_y] + # Calculate edge angles atan2(y/x) + edge_angles = np.zeros((len(edges))) # empty 1 column array + for i in range(len(edge_angles)): + edge_angles[i] = atan2(edges[i, 1], edges[i, 0]) + # Check for angles in 1st quadrant + for i in range(len(edge_angles)): + edge_angles[i] = abs(edge_angles[i] % (pi / 2)) # want strictly positive answers + # Remove duplicate angles + edge_angles = np.unique(edge_angles) + # Test each angle to find bounding box with smallest area + min_bbox = (0, sys.maxsize, 0, 0, 0, 0, 0, 0) # rot_angle, area, width, height, min_x, max_x, min_y, max_y + # print "Testing", len(edge_angles), "possible rotations for bounding box... \n" + for i in range(len(edge_angles)): + # Create rotation matrix to shift points to baseline + # R = [ cos(theta) , cos(theta-PI/2) + # cos(theta+PI/2) , cos(theta) ] + R = np.array([[cos(edge_angles[i]), cos(edge_angles[i] - (pi / 2))], + [cos(edge_angles[i] + (pi / 2)), cos(edge_angles[i])]]) + # Apply this rotation to convex hull points + rot_points = np.dot(R, np.transpose(hull_points_2d)) # 2x2 * 2xn + # Find min/max x,y points + min_x = np.nanmin(rot_points[0], axis=0) + max_x = np.nanmax(rot_points[0], axis=0) + min_y = np.nanmin(rot_points[1], axis=0) + max_y = np.nanmax(rot_points[1], axis=0) + # Calculate height/width/area of this bounding rectangle + width = max_x - min_x + height = max_y - min_y + area = width * height + # Store the smallest rect found first + if (area < min_bbox[1]): + min_bbox = (edge_angles[i], area, width, height, min_x, max_x, min_y, max_y) + # Re-create rotation matrix for smallest rect + angle = min_bbox[0] + R = np.array([[cos(angle), cos(angle - (pi / 2))], [cos(angle + (pi / 2)), cos(angle)]]) + # min/max x,y points are against baseline + min_x = min_bbox[4] + max_x = min_bbox[5] + min_y = min_bbox[6] + max_y = min_bbox[7] + # Calculate center point and project onto rotated frame + center_x = (min_x + max_x) / 2 + center_y = (min_y + max_y) / 2 + center_point = np.dot([center_x, center_y], R) + if min_bbox[2] > min_bbox[3]: + a = -cos(angle) + b = sin(angle) + w = min_bbox[2] / 2 + h = min_bbox[3] / 2 + else: + a = -cos(angle + (pi / 2)) + b = sin(angle + (pi / 2)) + w = min_bbox[3] / 2 + h = min_bbox[2] / 2 + tM = Matrix([[a, b, 0, center_point[0]], [-b, a, 0, center_point[1]], [0, 0, 1, 0], [0, 0, 0, 1]]) + l_pts = [Vector((-w, -h, 0)), Vector((-w, h, 0)), Vector((w, h, 0)), Vector((w, -h, 0))] + w_pts = [tM * pt for pt in l_pts] + return tM, 2 * w, 2 * h, l_pts, w_pts + + @staticmethod + def detect_polygons(geoms): + """ detect_polygons + """ + print("Ops.detect_polygons()") + t = time.time() + result, dangles, cuts, invalids = shapely.ops.polygonize_full(geoms) + print("Ops.detect_polygons() :%.2f seconds" % (time.time() - t)) + return result, dangles, cuts, invalids + + @staticmethod + def optimize(geoms, tolerance=0.001, preserve_topology=True): + """ optimize + """ + t = time.time() + geoms = Io.ensure_iterable(geoms) + optimized = [geom.simplify(tolerance, preserve_topology) for geom in geoms] + print("Ops.optimize() :%.2f seconds" % (time.time() - t)) + return optimized + + @staticmethod + def union(geoms): + """ union (shapely based) + cascaded union - may require snap before use to fix precision issues + use union2 for best performances + """ + t = time.time() + geoms = Io.ensure_iterable(geoms) + collection = shapely.geometry.GeometryCollection(geoms) + union = shapely.ops.cascaded_union(collection) + print("Ops.union() :%.2f seconds" % (time.time() - t)) + return union + + +class ShapeOps(): + + @staticmethod + def union(shapes, extend=0.001): + """ union2 (Shape based) + cascaded union + require point_tree and seg_tree + """ + split = ShapeOps.split(shapes, extend=extend) + union = ShapeOps.merge(split) + return union + + @staticmethod + def _intersection_point(d, t, point, seg): + if d > EPSILON: + return point + elif t > 0.5: + return seg.c1 + else: + return seg.c0 + + @staticmethod + def split(shapes, extend=0.01): + """ _split + detect intersections between segments and slice shapes according + is able to project segment ends on closest segment + require point_tree and seg_tree + """ + global vars_dict + t = time.time() + new_shapes = [] + segs = vars_dict['seg_tree']._geoms + nbsegs = len(segs) + it_start = [None for x in range(nbsegs)] + it_end = [None for x in range(nbsegs)] + for s, seg in enumerate(segs): + count, idx = vars_dict['seg_tree'].intersects(seg) + for id in idx: + if id > s: + intersect, co, u, v = seg.intersect(segs[id]) + if intersect: + point = vars_dict['point_tree'].newPoint(co) + du = seg.min_intersect_dist(u, point) + dv = segs[id].min_intersect_dist(v, point) + # point intersection sur segment id + pt = ShapeOps._intersection_point(dv, v, point, segs[id]) + # print("s:%s id:%s u:%7f v:%7f du:%7f dv:%7f" % (s, id, u, v, du, dv)) + if u <= 0: + # prolonge segment s c0 + if du < extend and not seg.is_end(pt): + it = Prolongement(seg.c0, pt, id, v, du) + last = it_start[s] + if last is None or last.length > it.length: + it_start[s] = it + elif u < 1: + # intersection sur segment s + seg.slice(du, u, pt) + else: + # prolonge segment s c1 + if du < extend and not seg.is_end(pt): + it = Prolongement(seg.c1, pt, id, v, du) + last = it_end[s] + if last is None or last.length > it.length: + it_end[s] = it + pt = ShapeOps._intersection_point(du, u, point, seg) + if v <= 0: + # prolonge segment id c0 + if dv < extend and not segs[id].is_end(pt): + it = Prolongement(segs[id].c0, pt, s, u, dv) + last = it_start[id] + if last is None or last.length > it.length: + it_start[id] = it + elif v < 1: + # intersection sur segment s + segs[id].slice(dv, v, pt) + else: + # prolonge segment s c1 + if dv < extend and not segs[id].is_end(pt): + it = Prolongement(segs[id].c1, pt, s, u, dv) + last = it_end[id] + if last is None or last.length > it.length: + it_end[id] = it + for it in it_start: + if it is not None: + # print("it_start[%s] id:%s t:%4f d:%4f" % (s, it.id, it.t, it.d) ) + if it.t > 0 and it.t < 1: + segs[it.id]._append_splits((it.t, it.c1)) + if it.d > EPSILON: + shape = Shape([it.c0, it.c1]) + shapes.append(shape) + for it in it_end: + if it is not None: + # print("it_end[%s] id:%s t:%4f d:%4f" % (s, it.id, it.t, it.d) ) + if it.t > 0 and it.t < 1: + segs[it.id]._append_splits((it.t, it.c1)) + if it.d > EPSILON: + shape = Shape([it.c0, it.c1]) + shapes.append(shape) + print("Ops.split() intersect :%.2f seconds" % (time.time() - t)) + t = time.time() + for shape in shapes: + shape.add_points() + for shape in shapes: + shape.set_users() + for shape in shapes: + if shape.valid: + shape.slice(new_shapes) + print("Ops.split() slice :%.2f seconds" % (time.time() - t)) + return new_shapes + + @staticmethod + def merge(shapes): + """ merge + merge shapes ends + reverse use seg_tree + does not need tree as all: + - set shape ids to end vertices + - traverse shapes looking for points with 2 shape ids + - merge different shapes according + """ + t = time.time() + merged = [] + for i, shape in enumerate(shapes): + shape.available = True + shape.shapeId = [i] + shape.c0.shapeIds = [] + shape.c1.shapeIds = [] + for i, shape in enumerate(shapes): + shape.c0.shapeIds.append(i) + shape.c1.shapeIds.append(i) + for i, shape in enumerate(shapes): + shapeIds = shape.c1.shapeIds + if len(shapeIds) == 2: + if shapeIds[0] in shape.shapeId: + s = shapeIds[1] + else: + s = shapeIds[0] + if shape != shapes[s]: + shape.merge(shapes[s]) + shape.shapeId += shapes[s].shapeId + for j in shape.shapeId: + shapes[j] = shape + shapeIds = shape.c0.shapeIds + if len(shapeIds) == 2: + if shapeIds[0] in shape.shapeId: + s = shapeIds[1] + else: + s = shapeIds[0] + if shape != shapes[s]: + shape.merge(shapes[s]) + shape.shapeId += shapes[s].shapeId + for j in shape.shapeId: + shapes[j] = shape + for shape in shapes: + if shape.available: + shape.consume() + merged.append(shape) + print("Ops.merge() :%.2f seconds" % (time.time() - t)) + return merged + + +class Selectable(object): + + """ selectable shapely geoms """ + def __init__(self, geoms, coordsys): + # selection sets (bitArray) + self.selections = [] + # selected objects on screen representation + self.curves = [] + # Rtree to speedup region selections + self.tree = Qtree(coordsys) + self.tree.build(geoms) + # BitArray ids of selected geoms + self.ba = BitArray(self.ngeoms) + # Material to represent selection on screen + self.mat = self.build_display_mat("Selected", + color=bpy.context.user_preferences.themes[0].view_3d.object_selected) + self.cursor_fence = GlCursorFence() + self.cursor_fence.enable() + self.cursor_area = GlCursorArea() + self.feedback = FeedbackPanel() + self.action = None + self.store_index = 0 + + @property + def coordsys(self): + return self.tree.coordsys + + @property + def geoms(self): + return self.tree._geoms + + @property + def ngeoms(self): + return self.tree.ngeoms + + @property + def nsets(self): + return len(self.selections) + + def build_display_mat(self, name, color=(0.2, 0.2, 0)): + mat = MaterialUtils.build_default_mat(name, color) + mat.use_object_color = True + mat.emit = 0.2 + mat.alpha = 0.2 + mat.game_settings.alpha_blend = 'ADD' + return mat + + def _unselect(self, selection): + t = time.time() + for i in selection: + self.ba.clear(i) + print("Selectable._unselect() :%.2f seconds" % (time.time() - t)) + + def _select(self, selection): + t = time.time() + for i in selection: + self.ba.set(i) + print("Selectable._select() :%.2f seconds" % (time.time() - t)) + + def _position_3d_from_coord(self, context, coord): + """return point in local input coordsys + """ + region = context.region + rv3d = context.region_data + view_vector_mouse = view3d_utils.region_2d_to_vector_3d(region, rv3d, coord) + ray_origin_mouse = view3d_utils.region_2d_to_origin_3d(region, rv3d, coord) + loc = intersect_line_plane(ray_origin_mouse, ray_origin_mouse + view_vector_mouse, + Vector((0, 0, 0)), Vector((0, 0, 1)), False) + x, y, z = self.coordsys.invert * loc + return Vector((x, y, z)) + + def _position_2d_from_coord(self, context, coord): + """ coord given in local input coordsys + """ + region = context.region + rv3d = context.region_data + loc = view3d_utils.location_3d_to_region_2d(region, rv3d, self.coordsys.world * coord) + x, y = loc + return Vector((x, y)) + + def _contains(self, context, coord, event): + t = time.time() + point = self._position_3d_from_coord(context, coord) + selection = [] + pt = ShapelyPoint(point) + prepared_pt = shapely.prepared.prep(pt) + count, gids = self.tree.intersects(pt) + selection = [i for i in gids if prepared_pt.intersects(self.geoms[i])] + print("Selectable._contains() :%.2f seconds" % (time.time() - t)) + if event.shift: + self._unselect(selection) + else: + self._select(selection) + self._draw(context) + + def _intersects(self, context, coord, event): + t = time.time() + c0 = self._position_3d_from_coord(context, coord) + c1 = self._position_3d_from_coord(context, (coord[0], event.mouse_region_y)) + c2 = self._position_3d_from_coord(context, (event.mouse_region_x, event.mouse_region_y)) + c3 = self._position_3d_from_coord(context, (event.mouse_region_x, coord[1])) + poly = ShapelyPolygon([c0, c1, c2, c3]) + prepared_poly = shapely.prepared.prep(poly) + count, gids = self.tree.intersects(poly) + if event.ctrl: + selection = [i for i in gids if prepared_poly.contains(self.geoms[i])] + else: + selection = [i for i in gids if prepared_poly.intersects(self.geoms[i])] + print("Selectable._intersects() :%.2f seconds" % (time.time() - t)) + if event.shift: + self._unselect(selection) + else: + self._select(selection) + self._draw(context) + + def _hide(self, context): + t = time.time() + if len(self.curves) > 0: + try: + for curve in self.curves: + data = curve.data + context.scene.objects.unlink(curve) + bpy.data.objects.remove(curve, do_unlink=True) + if data is None: + return + name = data.name + if bpy.data.curves.find(name) > - 1: + bpy.data.curves.remove(data, do_unlink=True) + except: + pass + self.curves = [] + print("Selectable._hide() :%.2f seconds" % (time.time() - t)) + + def _draw(self, context): + print("Selectable._draw() %s" % (self.coordsys.world)) + t = time.time() + self._hide(context) + selection = [self.geoms[i] for i in self.ba.list] + if len(selection) > 1000: + self.curves = [Io.to_curve(context.scene, self.coordsys, selection, 'selection', '3D')] + else: + self.curves = Io.to_curves(context.scene, self.coordsys, selection, 'selection', '2D') + for curve in self.curves: + curve.color = (1, 1, 0, 1) + if len(curve.data.materials) < 1: + curve.data.materials.append(self.mat) + curve.active_material = self.mat + curve.select = True + print("Selectable._draw() :%.2f seconds" % (time.time() - t)) + + def store(self): + self.selections.append(self.ba.copy) + self.store_index = self.nsets + + def recall(self): + if self.nsets > 0: + if self.store_index < 1: + self.store_index = self.nsets + self.store_index -= 1 + self.ba = self.selections[self.store_index].copy + + def select(self, context, coord, event): + if abs(event.mouse_region_x - coord[0]) > 2 and abs(event.mouse_region_y - coord[1]) > 2: + self._intersects(context, coord, event) + else: + self._contains(context, (event.mouse_region_x, event.mouse_region_y), event) + + def init(self, pick_tool, context, action): + raise NotImplementedError("Selectable must implement init(self, pick_tool, context, action)") + + def keyboard(self, context, event): + """ keyboard events modal handler """ + raise NotImplementedError("Selectable must implement keyboard(self, context, event)") + + def complete(self, context): + raise NotImplementedError("Selectable must implement complete(self, context)") + + def modal(self, context, event): + """ modal handler """ + raise NotImplementedError("Selectable must implement modal(self, context, event)") + + def draw_callback(self, _self, context): + """ a gl draw callback """ + raise NotImplementedError("Selectable must implement draw_callback(self, _self, context)") + + +class SelectPoints(Selectable): + + def __init__(self, shapes, coordsys): + geoms = [] + for shape in shapes: + if shape.valid: + for point in shape.points: + point.users = 1 + for shape in shapes: + if shape.valid: + for point in shape.points: + if point.users > 0: + point.users = 0 + geoms.append(point.geom) + super(SelectPoints, self).__init__(geoms, coordsys) + + def _draw(self, context): + """ override draw method """ + print("SelectPoints._draw()") + t = time.time() + self._hide(context) + selection = list(self.geoms[i] for i in self.ba.list) + geom = ShapelyOps.union(selection) + self.curves = [Io.to_curve(context.scene, self.coordsys, geom.convex_hull, 'selection', '3D')] + for curve in self.curves: + curve.color = (1, 1, 0, 1) + curve.select = True + print("SelectPoints._draw() :%.2f seconds" % (time.time() - t)) + + def init(self, pick_tool, context, action): + # Post selection actions + self.selectMode = True + self.object_location = None + self.startPoint = (0, 0) + self.endPoint = (0, 0) + self.drag = False + self.feedback.instructions(context, "Select Points", "Click & Drag to select points in area", [ + ('SHIFT', 'deselect'), + ('CTRL', 'contains'), + ('A', 'All'), + ('I', 'Inverse'), + ('F', 'Create line around selection'), + # ('W', 'Create window using selection'), + # ('D', 'Create door using selection'), + ('ALT+F', 'Create best fit rectangle'), + ('R', 'Retrieve selection'), + ('S', 'Store selection'), + ('ESC or RIGHTMOUSE', 'exit when done') + ]) + self.feedback.enable() + args = (self, context) + self._handle = bpy.types.SpaceView3D.draw_handler_add(self.draw_callback, args, 'WINDOW', 'POST_PIXEL') + self.action = action + self._draw(context) + print("SelectPoints.init()") + + def complete(self, context): + self.feedback.disable() + self._hide(context) + + def keyboard(self, context, event): + if event.type in {'A'}: + if len(self.ba.list) > 0: + self.ba.none() + else: + self.ba.all() + elif event.type in {'I'}: + self.ba.reverse() + elif event.type in {'S'}: + self.store() + elif event.type in {'R'}: + self.recall() + elif event.type in {'F'}: + sel = [self.geoms[i] for i in self.ba.list] + if len(sel) > 0: + scene = context.scene + geom = ShapelyOps.union(sel) + if event.alt: + tM, w, h, l_pts, w_pts = ShapelyOps.min_bounding_rect(geom) + x0 = -w / 2.0 + y0 = -h / 2.0 + x1 = w / 2.0 + y1 = h / 2.0 + poly = shapely.geometry.LineString([(x0, y0, 0), (x1, y0, 0), (x1, y1, 0), + (x0, y1, 0), (x0, y0, 0)]) + result = Io.to_curve(scene, self.coordsys, poly, 'points') + result.matrix_world = self.coordsys.world * tM + scene.objects.active = result + else: + result = Io.to_curve(scene, self.coordsys, geom.convex_hull, 'points') + scene.objects.active = result + self.ba.none() + self.complete(context) + elif event.type in {'W'}: + sel = [self.geoms[i] for i in self.ba.list] + if len(sel) > 0: + scene = context.scene + geom = ShapelyOps.union(sel) + if event.alt: + tM, w, h, l_pts, w_pts = ShapelyOps.min_bounding_rect(geom) + x0 = -w / 2.0 + y0 = -h / 2.0 + x1 = w / 2.0 + y1 = h / 2.0 + poly = shapely.geometry.LineString([(x0, y0, 0), (x1, y0, 0), (x1, y1, 0), + (x0, y1, 0), (x0, y0, 0)]) + result = Io.to_curve(scene, self.coordsys, poly, 'points') + result.matrix_world = self.coordsys.world * tM + scene.objects.active = result + else: + result = Io.to_curve(scene, self.coordsys, geom.convex_hull, 'points') + scene.objects.active = result + self.ba.none() + self.complete(context) + elif event.type in {'D'}: + sel = [self.geoms[i] for i in self.ba.list] + if len(sel) > 0: + scene = context.scene + geom = ShapelyOps.union(sel) + if event.alt: + tM, w, h, l_pts, w_pts = ShapelyOps.min_bounding_rect(geom) + x0 = -w / 2.0 + y0 = -h / 2.0 + x1 = w / 2.0 + y1 = h / 2.0 + poly = shapely.geometry.LineString([(x0, y0, 0), (x1, y0, 0), (x1, y1, 0), + (x0, y1, 0), (x0, y0, 0)]) + result = Io.to_curve(scene, self.coordsys, poly, 'points') + result.matrix_world = self.coordsys.world * tM + scene.objects.active = result + else: + result = Io.to_curve(scene, self.coordsys, geom.convex_hull, 'points') + scene.objects.active = result + self.ba.none() + self.complete(context) + self._draw(context) + + def modal(self, context, event): + if event.type in {'I', 'A', 'S', 'R', 'F'} and event.value == 'PRESS': + self.keyboard(context, event) + elif event.type in {'RIGHTMOUSE', 'ESC'}: + self.complete(context) + bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW') + return {'FINISHED'} + elif event.type == 'LEFTMOUSE' and event.value == 'PRESS': + self.drag = True + self.cursor_area.enable() + self.cursor_fence.disable() + self.startPoint = (event.mouse_region_x, event.mouse_region_y) + self.endPoint = (event.mouse_region_x, event.mouse_region_y) + elif event.type == 'LEFTMOUSE' and event.value == 'RELEASE': + self.drag = False + self.cursor_area.disable() + self.cursor_fence.enable() + self.endPoint = (event.mouse_region_x, event.mouse_region_y) + self.select(context, self.startPoint, event) + elif event.type == 'MOUSEMOVE': + self.endPoint = (event.mouse_region_x, event.mouse_region_y) + return {'RUNNING_MODAL'} + + def draw_callback(self, _self, context): + self.feedback.draw(context) + self.cursor_area.set_location(context, self.startPoint, self.endPoint) + self.cursor_fence.set_location(context, self.endPoint) + self.cursor_area.draw(context) + self.cursor_fence.draw(context) + + +class SelectLines(Selectable): + + def __init__(self, geoms, coordsys): + super(SelectLines, self).__init__(geoms, coordsys) + + def _draw(self, context): + """ override draw method """ + print("SelectLines._draw()") + t = time.time() + self._hide(context) + selection = list(self.geoms[i] for i in self.ba.list) + self.curves = [Io.to_curve(context.scene, self.coordsys, selection, 'selection', '3D')] + for curve in self.curves: + curve.color = (1, 1, 0, 1) + curve.select = True + print("SelectLines._draw() :%.2f seconds" % (time.time() - t)) + + def init(self, pick_tool, context, action): + # Post selection actions + self.selectMode = True + self.object_location = None + self.startPoint = (0, 0) + self.endPoint = (0, 0) + self.drag = False + self.feedback.instructions(context, "Select Lines", "Click & Drag to select lines in area", [ + ('SHIFT', 'deselect'), + ('CTRL', 'contains'), + ('A', 'All'), + ('I', 'Inverse'), + # ('F', 'Create lines from selection'), + ('R', 'Retrieve selection'), + ('S', 'Store selection'), + ('ESC or RIGHTMOUSE', 'exit when done') + ]) + self.feedback.enable() + args = (self, context) + self._handle = bpy.types.SpaceView3D.draw_handler_add( + self.draw_callback, args, 'WINDOW', 'POST_PIXEL') + self.action = action + self._draw(context) + print("SelectLines.init()") + + def complete(self, context): + print("SelectLines.complete()") + t = time.time() + self._hide(context) + scene = context.scene + selection = list(self.geoms[i] for i in self.ba.list) + if len(selection) > 0: + if self.action == 'select': + result = Io.to_curve(scene, self.coordsys, selection, 'selection') + scene.objects.active = result + elif self.action == 'union': + shapes = Io.geoms_to_shapes(selection) + merged = ShapeOps.merge(shapes) + union = Io.shapes_to_geoms(merged) + # union = self.ops.union(selection) + resopt = ShapelyOps.optimize(union) + result = Io.to_curve(scene, self.coordsys, resopt, 'union') + scene.objects.active = result + self.feedback.disable() + print("SelectLines.complete() :%.2f seconds" % (time.time() - t)) + + def keyboard(self, context, event): + if event.type in {'A'}: + if len(self.ba.list) > 0: + self.ba.none() + else: + self.ba.all() + elif event.type in {'I'}: + self.ba.reverse() + elif event.type in {'S'}: + self.store() + elif event.type in {'R'}: + self.recall() + self._draw(context) + + def modal(self, context, event): + if event.type in {'I', 'A', 'S', 'R'} and event.value == 'PRESS': + self.keyboard(context, event) + elif event.type in {'RIGHTMOUSE', 'ESC'}: + self.complete(context) + bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW') + return {'FINISHED'} + elif event.type == 'LEFTMOUSE' and event.value == 'PRESS': + self.drag = True + self.cursor_area.enable() + self.cursor_fence.disable() + self.startPoint = (event.mouse_region_x, event.mouse_region_y) + self.endPoint = (event.mouse_region_x, event.mouse_region_y) + elif event.type == 'LEFTMOUSE' and event.value == 'RELEASE': + self.drag = False + self.cursor_area.disable() + self.cursor_fence.enable() + self.endPoint = (event.mouse_region_x, event.mouse_region_y) + self.select(context, self.startPoint, event) + elif event.type == 'MOUSEMOVE': + self.endPoint = (event.mouse_region_x, event.mouse_region_y) + return {'RUNNING_MODAL'} + + def draw_callback(self, _self, context): + self.feedback.draw(context) + self.cursor_area.set_location(context, self.startPoint, self.endPoint) + self.cursor_fence.set_location(context, self.endPoint) + self.cursor_area.draw(context) + self.cursor_fence.draw(context) + + +class SelectPolygons(Selectable): + + def __init__(self, geoms, coordsys): + super(SelectPolygons, self).__init__(geoms, coordsys) + + """ + pick_tools actions + """ + def init(self, pick_tool, context, action): + # Post selection actions + self.need_rotation = False + self.direction = 0 + self.object_location = None + self.selectMode = True + self.startPoint = (0, 0) + self.endPoint = (0, 0) + if action in ['select', 'union', 'rectangle']: + self.feedback.instructions(context, "Select Polygons", "Click & Drag to select polygons in area", [ + ('SHIFT', 'deselect'), + ('CTRL', 'contains'), + ('A', 'All'), + ('I', 'Inverse'), + ('B', 'Bigger than current'), + # ('F', 'Create from selection'), + ('R', 'Retrieve selection'), + ('S', 'Store selection'), + ('ESC or RIGHTMOUSE', 'exit when done') + ]) + elif action == 'wall': + self.feedback.instructions(context, "Select Polygons", "Click & Drag to select polygons in area", [ + ('SHIFT', 'deselect'), + ('CTRL', 'contains'), + ('A', 'All'), + ('I', 'Inverse'), + ('B', 'Bigger than current'), + ('R', 'Retrieve selection'), + ('S', 'Store selection'), + ('ESC or RIGHTMOUSE', 'exit and build wall when done') + ]) + elif action == 'window': + self.feedback.instructions(context, "Select Polygons", "Click & Drag to select polygons in area", [ + ('SHIFT', 'deselect'), + ('CTRL', 'contains'), + ('A', 'All'), + ('I', 'Inverse'), + ('B', 'Bigger than current'), + ('F', 'Create a window from selection'), + ('ESC or RIGHTMOUSE', 'exit tool when done') + ]) + elif action == 'door': + self.feedback.instructions(context, "Select Polygons", "Click & Drag to select polygons in area", [ + ('SHIFT', 'deselect'), + ('CTRL', 'contains'), + ('A', 'All'), + ('I', 'Inverse'), + ('B', 'Bigger than current'), + ('F', 'Create a door from selection'), + ('ESC or RIGHTMOUSE', 'exit tool when done') + ]) + self.gl_arc = GlPolyline((1.0, 1.0, 1.0, 0.5), d=3) + self.gl_arc.width = 1 + self.gl_arc.style = bgl.GL_LINE_STIPPLE + self.gl_line = GlLine(d=3) + self.gl_line.colour_inactive = (1.0, 1.0, 1.0, 0.5) + self.gl_line.width = 2 + self.gl_line.style = bgl.GL_LINE_STIPPLE + self.gl_side = GlLine(d=2) + self.gl_side.colour_inactive = (1.0, 1.0, 1.0, 0.5) + self.gl_side.width = 2 + self.gl_side.style = bgl.GL_LINE_STIPPLE + self.feedback.enable() + self.drag = False + args = (self, context) + self._handle = bpy.types.SpaceView3D.draw_handler_add( + self.draw_callback, args, 'WINDOW', 'POST_PIXEL') + self.action = action + self._draw(context) + print("SelectPolygons.init()") + + def complete(self, context): + print("SelectPolygons.complete()") + t = time.time() + scene = context.scene + self._hide(context) + selection = list(self.geoms[i] for i in self.ba.list) + if len(selection) > 0: + if self.action == 'select': + result = Io.to_curve(scene, self.coordsys, selection, 'selection') + scene.objects.active = result + elif self.action == 'union': + union = ShapelyOps.union(selection) + resopt = ShapelyOps.optimize(union) + result = Io.to_curve(scene, self.coordsys, resopt, 'union') + scene.objects.active = result + elif self.action == 'wall': + union = ShapelyOps.union(selection) + union = ShapelyOps.optimize(union) + res = [] + z = context.window_manager.archipack_polylib.solidify_thickness + Io.to_wall(scene, self.coordsys, union, z, 'wall', res) + if len(res) > 0: + scene.objects.active = res[0] + if len(res) > 1: + bpy.ops.object.join() + bpy.ops.archipack.wall(z=z) + elif self.action == 'rectangle': + # currently only output a best fitted rectangle + # over selection + if self.object_location is not None: + tM, w, h, l_pts, w_pts = self.object_location + poly = shapely.geometry.LineString(l_pts) + result = Io.to_curve(scene, self.coordsys, poly, 'rectangle') + result.matrix_world = self.coordsys.world * tM + scene.objects.active = result + self.ba.none() + elif self.action == 'window': + if self.object_location is not None: + + tM, w, h, l_pts, w_pts = self.object_location + + if self.need_rotation: + rM = Matrix([ + [-1, 0, 0, 0], + [0, -1, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 1], + ]) + else: + rM = Matrix() + + if w > 1.8: + z = 2.2 + altitude = 0.0 + else: + z = 1.2 + altitude = 1.0 + + bpy.ops.archipack.window(x=w, y=h, z=z, altitude=altitude, auto_manipulate=False) + result = context.object + result.matrix_world = self.coordsys.world * tM * rM + result.data.archipack_window[0].hole_margin = 0.02 + self.ba.none() + elif self.action == 'door': + if self.object_location is not None: + + tM, w, h, l_pts, w_pts = self.object_location + + if self.need_rotation: + rM = Matrix([ + [-1, 0, 0, 0], + [0, -1, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 1], + ]) + else: + rM = Matrix() + + if w < 1.5: + n_panels = 1 + else: + n_panels = 2 + + bpy.ops.archipack.door(x=w, y=h, z=2.0, n_panels=n_panels, + direction=self.direction, auto_manipulate=False) + result = context.object + result.matrix_world = self.coordsys.world * tM * rM + result.data.archipack_door[0].hole_margin = 0.02 + self.ba.none() + + if self.action not in ['window', 'door']: + self.feedback.disable() + + print("SelectPolygons.complete() :%.2f seconds" % (time.time() - t)) + + def keyboard(self, context, event): + if event.type in {'A'}: + if len(self.ba.list) > 0: + self.ba.none() + else: + self.ba.all() + elif event.type in {'I'}: + self.ba.reverse() + elif event.type in {'S'}: + self.store() + elif event.type in {'R'}: + self.recall() + elif event.type in {'B'}: + areas = [self.geoms[i].area for i in self.ba.list] + area = max(areas) + self.ba.none() + for i, geom in enumerate(self.geoms): + if geom.area > area: + self.ba.set(i) + elif event.type in {'F'}: + if self.action == 'rectangle': + self.complete(context) + else: + sel = [self.geoms[i] for i in self.ba.list] + if len(sel) > 0: + if self.action == 'window': + self.feedback.instructions(context, + "Select Polygons", "Click & Drag to select polygons in area", [ + ('CLICK & DRAG', 'Set window orientation'), + ('RELEASE', 'Create window'), + ('F', 'Return to select mode'), + ('ESC or RIGHTMOUSE', 'exit tool when done') + ]) + elif self.action == 'door': + self.feedback.instructions(context, + "Select Polygons", "Click & Drag to select polygons in area", [ + ('CLICK & DRAG', 'Set door orientation'), + ('RELEASE', 'Create door'), + ('F', 'Return to select mode'), + ('ESC or RIGHTMOUSE', 'exit tool when done') + ]) + self.selectMode = not self.selectMode + geom = ShapelyOps.union(sel) + tM, w, h, l_pts, w_pts = ShapelyOps.min_bounding_rect(geom) + self.object_location = (tM, w, h, l_pts, w_pts) + self.startPoint = self._position_2d_from_coord(context, tM.translation) + self._draw(context) + + def modal(self, context, event): + if event.type in {'I', 'A', 'S', 'R', 'F', 'B'} and event.value == 'PRESS': + + self.keyboard(context, event) + elif event.type in {'RIGHTMOUSE', 'ESC'}: + if self.action == 'object': + self._hide(context) + else: + self.complete(context) + bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW') + return {'FINISHED'} + elif event.type == 'LEFTMOUSE' and event.value == 'PRESS': + self.drag = True + self.cursor_area.enable() + self.cursor_fence.disable() + if self.selectMode: + self.startPoint = (event.mouse_region_x, event.mouse_region_y) + self.endPoint = (event.mouse_region_x, event.mouse_region_y) + elif event.type == 'LEFTMOUSE' and event.value == 'RELEASE': + self.drag = False + self.cursor_area.disable() + self.cursor_fence.enable() + self.endPoint = (event.mouse_region_x, event.mouse_region_y) + if self.selectMode: + self.select(context, self.startPoint, event) + else: + self.complete(context) + if self.action == 'window': + self.feedback.instructions(context, "Select Polygons", "Click & Drag to select polygons in area", [ + ('SHIFT', 'deselect'), + ('CTRL', 'contains'), + ('A', 'All'), + ('I', 'Inverse'), + ('B', 'Bigger than current'), + ('F', 'Create a window from selection'), + ('ESC or RIGHTMOUSE', 'exit tool when done') + ]) + elif self.action == 'door': + self.feedback.instructions(context, "Select Polygons", "Click & Drag to select polygons in area", [ + ('SHIFT', 'deselect'), + ('CTRL', 'contains'), + ('A', 'All'), + ('I', 'Inverse'), + ('B', 'Bigger than current'), + ('F', 'Create a door from selection'), + ('ESC or RIGHTMOUSE', 'exit tool when done') + ]) + self.selectMode = True + if event.type == 'MOUSEMOVE': + self.endPoint = (event.mouse_region_x, event.mouse_region_y) + return {'RUNNING_MODAL'} + + def _draw_2d_arc(self, context, c, p0, p1): + """ + draw projection of 3d arc in 2d space + """ + d0 = np.subtract(c, p0) + d1 = np.subtract(p1, c) + a0 = atan2(d0[1], d0[0]) + a1 = atan2(d1[1], d1[0]) + da = a1 - a0 + if da < pi: + da += 2 * pi + if da > pi: + da -= 2 * pi + da = da / 12 + r = np.linalg.norm(d1) + pts = [] + for i in range(13): + a = a0 + da * i + p3d = c + Vector((cos(a) * r, sin(a) * r, 0)) + pts.append(self.coordsys.world * p3d) + + self.gl_arc.set_pos(pts) + self.gl_arc.draw(context) + self.gl_line.p = self.coordsys.world * c + self.gl_line.v = pts[0] - self.gl_line.p + self.gl_line.draw(context) + + def draw_callback(self, _self, context): + """ + draw on screen feedback using gl. + """ + self.feedback.draw(context) + + if self.selectMode: + self.cursor_area.set_location(context, self.startPoint, self.endPoint) + self.cursor_fence.set_location(context, self.endPoint) + self.cursor_area.draw(context) + self.cursor_fence.draw(context) + else: + if self.drag: + x0, y0 = self.startPoint + x1, y1 = self.endPoint + # draw 2d line marker + # self.gl.Line(x0, y0, x1, y1, self.gl.line_colour) + + # 2d line + self.gl_side.p = Vector(self.startPoint) + self.gl_side.v = Vector(self.endPoint) - Vector(self.startPoint) + self.gl_side.draw(context) + + tM, w, h, l_pts, w_pts = self.object_location + pt = self._position_3d_from_coord(context, self.endPoint) + pt = tM.inverted() * Vector(pt) + self.need_rotation = pt.y < 0 + if self.action == 'door': + # symbole porte + if pt.x > 0: + if pt.y > 0: + self.direction = 1 + i_s, i_c, i_e = 3, 2, 1 + else: + self.direction = 0 + i_s, i_c, i_e = 2, 3, 0 + else: + if pt.y > 0: + self.direction = 0 + i_s, i_c, i_e = 0, 1, 2 + else: + self.direction = 1 + i_s, i_c, i_e = 1, 0, 3 + self._draw_2d_arc(context, w_pts[i_c], w_pts[i_s], w_pts[i_e]) + elif self.action == 'window': + # symbole fenetre + if pt.y > 0: + i_s0, i_c0 = 0, 1 + i_s1, i_c1 = 3, 2 + else: + i_s0, i_c0 = 1, 0 + i_s1, i_c1 = 2, 3 + pc = w_pts[i_c0] + 0.5 * (w_pts[i_c1] - w_pts[i_c0]) + self._draw_2d_arc(context, w_pts[i_c0], w_pts[i_s0], pc) + self._draw_2d_arc(context, w_pts[i_c1], w_pts[i_s1], pc) + + +class ARCHIPACK_OP_PolyLib_Pick2DPoints(Operator): + bl_idname = "archipack.polylib_pick_2d_points" + bl_label = "Pick lines" + bl_description = "Pick lines" + bl_options = {'REGISTER', 'UNDO'} + pass_keys = ['NUMPAD_0', 'NUMPAD_1', 'NUMPAD_3', 'NUMPAD_4', + 'NUMPAD_5', 'NUMPAD_6', 'NUMPAD_7', 'NUMPAD_8', + 'NUMPAD_9', 'MIDDLEMOUSE', 'WHEELUPMOUSE', 'WHEELDOWNMOUSE'] + action = StringProperty(name="action", default="select") + + @classmethod + def poll(self, context): + global vars_dict + return vars_dict['select_points'] is not None + + def modal(self, context, event): + global vars_dict + context.area.tag_redraw() + if event.type in self.pass_keys: + return {'PASS_THROUGH'} + return vars_dict['select_points'].modal(context, event) + + def invoke(self, context, event): + global vars_dict + if vars_dict['select_points'] is None: + self.report({'WARNING'}, "Use detect before") + return {'CANCELLED'} + elif context.space_data.type == 'VIEW_3D': + vars_dict['select_points'].init(self, context, self.action) + context.window_manager.modal_handler_add(self) + return {'RUNNING_MODAL'} + else: + self.report({'WARNING'}, "Active space must be a View3d") + return {'CANCELLED'} + + +class ARCHIPACK_OP_PolyLib_Pick2DLines(Operator): + bl_idname = "archipack.polylib_pick_2d_lines" + bl_label = "Pick lines" + bl_description = "Pick lines" + bl_options = {'REGISTER', 'UNDO'} + pass_keys = ['NUMPAD_0', 'NUMPAD_1', 'NUMPAD_3', 'NUMPAD_4', + 'NUMPAD_5', 'NUMPAD_6', 'NUMPAD_7', 'NUMPAD_8', + 'NUMPAD_9', 'MIDDLEMOUSE', 'WHEELUPMOUSE', 'WHEELDOWNMOUSE'] + action = StringProperty(name="action", default="select") + + @classmethod + def poll(self, context): + global vars_dict + return vars_dict['select_lines'] is not None + + def modal(self, context, event): + global vars_dict + context.area.tag_redraw() + if event.type in self.pass_keys: + return {'PASS_THROUGH'} + return vars_dict['select_lines'].modal(context, event) + + def invoke(self, context, event): + global vars_dict + if vars_dict['select_lines'] is None: + self.report({'WARNING'}, "Use detect before") + return {'CANCELLED'} + elif context.space_data.type == 'VIEW_3D': + vars_dict['select_lines'].init(self, context, self.action) + context.window_manager.modal_handler_add(self) + return {'RUNNING_MODAL'} + else: + self.report({'WARNING'}, "Active space must be a View3d") + return {'CANCELLED'} + + +class ARCHIPACK_OP_PolyLib_Pick2DPolygons(Operator): + bl_idname = "archipack.polylib_pick_2d_polygons" + bl_label = "Pick 2d" + bl_description = "Pick polygons" + bl_options = {'REGISTER', 'UNDO'} + pass_keys = ['NUMPAD_0', 'NUMPAD_1', 'NUMPAD_3', 'NUMPAD_4', + 'NUMPAD_5', 'NUMPAD_6', 'NUMPAD_7', 'NUMPAD_8', + 'NUMPAD_9', 'MIDDLEMOUSE', 'WHEELUPMOUSE', 'WHEELDOWNMOUSE'] + action = StringProperty(name="action", default="select") + + @classmethod + def poll(self, context): + global vars_dict + return vars_dict['select_polygons'] is not None + + def modal(self, context, event): + global vars_dict + context.area.tag_redraw() + if event.type in self.pass_keys: + return {'PASS_THROUGH'} + return vars_dict['select_polygons'].modal(context, event) + + def invoke(self, context, event): + global vars_dict + if vars_dict['select_polygons'] is None: + self.report({'WARNING'}, "Use detect before") + return {'CANCELLED'} + elif context.space_data.type == 'VIEW_3D': + vars_dict['select_polygons'].init(self, context, self.action) + context.window_manager.modal_handler_add(self) + return {'RUNNING_MODAL'} + else: + self.report({'WARNING'}, "Active space must be a View3d") + return {'CANCELLED'} + + +class ARCHIPACK_OP_PolyLib_Detect(Operator): + bl_idname = "archipack.polylib_detect" + bl_label = "Detect Polygons" + bl_description = "Detect polygons from unordered splines" + bl_options = {'REGISTER', 'UNDO'} + extend = FloatProperty(name="extend", default=0.01, subtype='DISTANCE', unit='LENGTH', min=0) + + @classmethod + def poll(self, context): + return len(context.selected_objects) > 0 and context.object is not None and context.object.type == 'CURVE' + + def execute(self, context): + global vars_dict + print("Detect") + t = time.time() + objs = [obj for obj in context.selected_objects if obj.type == 'CURVE'] + + if len(objs) < 1: + self.report({'WARNING'}, "Select a curve object before") + return {'CANCELLED'} + + for obj in objs: + obj.select = False + + coordsys = CoordSys(objs) + + vars_dict['point_tree'] = Qtree(coordsys, extend=0.5 * EPSILON) + vars_dict['seg_tree'] = Qtree(coordsys, extend=self.extend) + + # Shape based union + shapes = [] + Io.curves_to_shapes(objs, coordsys, context.window_manager.archipack_polylib.resolution, shapes) + union = ShapeOps.union(shapes, self.extend) + + # output select points + vars_dict['select_points'] = SelectPoints(shapes, coordsys) + + geoms = Io.shapes_to_geoms(union) + + # output select_lines + vars_dict['select_lines'] = SelectLines(geoms, coordsys) + + # Shapely based union + # vars_dict['select_polygons'].io.curves_as_shapely(objs, lines) + # geoms = vars_dict['select_polygons'].ops.union(lines, self.extend) + + result, dangles, cuts, invalids = ShapelyOps.detect_polygons(geoms) + vars_dict['select_polygons'] = SelectPolygons(result, coordsys) + + if len(invalids) > 0: + errs = Io.to_curve(context.scene, coordsys, invalids, "invalid_polygons") + err_mat = vars_dict['select_polygons'].build_display_mat("Invalid_polygon", (1, 0, 0)) + # curve.data.bevel_depth = 0.02 + errs.color = (1, 0, 0, 1) + if len(errs.data.materials) < 1: + errs.data.materials.append(err_mat) + errs.active_material = err_mat + errs.select = True + self.report({'WARNING'}, str(len(invalids)) + " invalid polygons detected") + print("Detect :%.2f seconds polygons:%s invalids:%s" % (time.time() - t, len(result), len(invalids))) + return {'FINISHED'} + + +class ARCHIPACK_OP_PolyLib_Offset(Operator): + bl_idname = "archipack.polylib_offset" + bl_label = "Offset" + bl_description = "Offset lines" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(self, context): + return len(context.selected_objects) > 0 and context.object is not None and context.object.type == 'CURVE' + + def execute(self, context): + wm = context.window_manager.archipack_polylib + objs = list(obj for obj in context.selected_objects if obj.type == 'CURVE') + if len(objs) < 1: + self.report({'WARNING'}, "Select a curve object before") + return {'CANCELLED'} + for obj in objs: + obj.select = False + lines = [] + coordsys = Io.curves_to_geoms(objs, wm.resolution, lines) + offset = [] + for line in lines: + res = line.parallel_offset(wm.offset_distance, wm.offset_side, resolution=wm.offset_resolution, + join_style=int(wm.offset_join_style), mitre_limit=wm.offset_mitre_limit) + offset.append(res) + Io.to_curve(context.scene, coordsys, offset, 'offset') + return {'FINISHED'} + + +class ARCHIPACK_OP_PolyLib_Simplify(Operator): + bl_idname = "archipack.polylib_simplify" + bl_label = "Simplify" + bl_description = "Simplify lines" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(self, context): + return (len(context.selected_objects) > 0 and + context.object is not None and + context.object.type == 'CURVE') + + def execute(self, context): + global vars_dict + wm = context.window_manager.archipack_polylib + objs = [obj for obj in context.selected_objects if obj.type == 'CURVE'] + if len(objs) < 1: + self.report({'WARNING'}, "Select a curve object before") + return {'CANCELLED'} + for obj in objs: + obj.select = False + simple = [] + lines = [] + coordsys = Io.curves_to_geoms(objs, wm.resolution, lines) + for line in lines: + res = line.simplify(wm.simplify_tolerance, preserve_topology=wm.simplify_preserve_topology) + simple.append(res) + Io.to_curve(context.scene, coordsys, simple, 'simplify') + return {'FINISHED'} + + +class ARCHIPACK_OP_PolyLib_OutputPolygons(Operator): + bl_idname = "archipack.polylib_output_polygons" + bl_label = "Output Polygons" + bl_description = "Output all polygons" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(self, context): + global vars_dict + return vars_dict['select_polygons'] is not None + + def execute(self, context): + global vars_dict + result = Io.to_curve(context.scene, vars_dict['select_polygons'].coordsys, + vars_dict['select_polygons'].geoms, 'polygons') + context.scene.objects.active = result + return {'FINISHED'} + + +class ARCHIPACK_OP_PolyLib_OutputLines(Operator): + bl_idname = "archipack.polylib_output_lines" + bl_label = "Output lines" + bl_description = "Output all lines" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(self, context): + global vars_dict + return vars_dict['select_lines'] is not None + + def execute(self, context): + global vars_dict + result = Io.to_curve(context.scene, vars_dict['select_lines'].coordsys, + vars_dict['select_lines'].geoms, 'lines') + context.scene.objects.active = result + return {'FINISHED'} + + +class ARCHIPACK_OP_PolyLib_Solidify(Operator): + bl_idname = "archipack.polylib_solidify" + bl_label = "Extrude" + bl_description = "Extrude all polygons" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(self, context): + return (len(context.selected_objects) > 0 and + context.object is not None and + context.object.type == 'CURVE') + + def execute(self, context): + wm = context.window_manager.archipack_polylib + objs = [obj for obj in context.selected_objects if obj.type == 'CURVE'] + if len(objs) < 1: + self.report({'WARNING'}, "Select a curve object before") + return {'CANCELLED'} + for obj in objs: + obj.data.dimensions = '2D' + mod = obj.modifiers.new("Solidify", 'SOLIDIFY') + mod.thickness = wm.solidify_thickness + mod.offset = 1.00 + mod.use_even_offset = True + mod.use_quality_normals = True + return {'FINISHED'} + + +class archipack_polylib(PropertyGroup): + bl_idname = 'archipack.polylib_parameters' + extend = FloatProperty( + name="Extend", + description="Extend to closest intersecting segment", + default=0.01, + subtype='DISTANCE', unit='LENGTH', min=0 + ) + offset_distance = FloatProperty( + name="Distance", + default=0.05, + subtype='DISTANCE', unit='LENGTH', min=0 + ) + offset_side = EnumProperty( + name="Side", default='left', + items=[('left', 'Left', 'Left'), + ('right', 'Right', 'Right')] + ) + offset_resolution = IntProperty( + name="Resolution", default=16 + ) + offset_join_style = EnumProperty( + name="Style", default='2', + items=[('1', 'Round', 'Round'), + ('2', 'Mitre', 'Mitre'), + ('3', 'Bevel', 'Bevel')] + ) + offset_mitre_limit = FloatProperty( + name="Mitre limit", + default=10.0, + subtype='DISTANCE', + unit='LENGTH', min=0 + ) + simplify_tolerance = FloatProperty( + name="Tolerance", + default=0.01, + subtype='DISTANCE', unit='LENGTH', min=0 + ) + simplify_preserve_topology = BoolProperty( + name="Preserve topology", + description="Preserve topology (fast without, but may introduce self crossing)", + default=True + ) + solidify_thickness = FloatProperty( + name="Thickness", + default=2.7, + subtype='DISTANCE', unit='LENGTH', min=0 + ) + resolution = IntProperty( + name="Bezier resolution", min=0, default=12 + ) + + +@persistent +def load_handler(dummy): + global vars_dict + vars_dict['select_polygons'] = None + vars_dict['select_lines'] = None + vars_dict['seg_tree'] = None + vars_dict['point_tree'] = None + + +def register(): + global vars_dict + vars_dict = { + # spacial tree for segments and points + 'seg_tree': None, + 'point_tree': None, + # keep track of shapely geometry selection sets + 'select_polygons': None, + 'select_lines': None, + 'select_points': None + } + bpy.utils.register_class(ARCHIPACK_OP_PolyLib_Pick2DPolygons) + bpy.utils.register_class(ARCHIPACK_OP_PolyLib_Pick2DLines) + bpy.utils.register_class(ARCHIPACK_OP_PolyLib_Pick2DPoints) + bpy.utils.register_class(ARCHIPACK_OP_PolyLib_OutputPolygons) + bpy.utils.register_class(ARCHIPACK_OP_PolyLib_OutputLines) + bpy.utils.register_class(ARCHIPACK_OP_PolyLib_Offset) + bpy.utils.register_class(ARCHIPACK_OP_PolyLib_Simplify) + bpy.utils.register_class(ARCHIPACK_OP_PolyLib_Detect) + bpy.utils.register_class(ARCHIPACK_OP_PolyLib_Solidify) + bpy.utils.register_class(archipack_polylib) + bpy.types.WindowManager.archipack_polylib = PointerProperty(type=archipack_polylib) + bpy.app.handlers.load_post.append(load_handler) + + +def unregister(): + global vars_dict + del vars_dict + bpy.utils.unregister_class(ARCHIPACK_OP_PolyLib_Pick2DPolygons) + bpy.utils.unregister_class(ARCHIPACK_OP_PolyLib_Pick2DLines) + bpy.utils.unregister_class(ARCHIPACK_OP_PolyLib_Pick2DPoints) + bpy.utils.unregister_class(ARCHIPACK_OP_PolyLib_Detect) + bpy.utils.unregister_class(ARCHIPACK_OP_PolyLib_OutputPolygons) + bpy.utils.unregister_class(ARCHIPACK_OP_PolyLib_OutputLines) + bpy.utils.unregister_class(ARCHIPACK_OP_PolyLib_Offset) + bpy.utils.unregister_class(ARCHIPACK_OP_PolyLib_Simplify) + bpy.utils.unregister_class(ARCHIPACK_OP_PolyLib_Solidify) + bpy.utils.unregister_class(archipack_polylib) + bpy.app.handlers.load_post.remove(load_handler) + del bpy.types.WindowManager.archipack_polylib + + +if __name__ == "__main__": + register() diff --git a/archipack/archipack_preset.py b/archipack/archipack_preset.py new file mode 100644 index 00000000..c5fe9446 --- /dev/null +++ b/archipack/archipack_preset.py @@ -0,0 +1,578 @@ +# -*- coding:utf-8 -*- + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110- 1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +# + +# ---------------------------------------------------------- +# Author: Stephen Leger (s-leger) +# +# ---------------------------------------------------------- +import bpy +import os +from bl_operators.presets import AddPresetBase +from mathutils import Vector +from bpy.props import StringProperty +from .archipack_gl import ( + ThumbHandle, Screen, GlRect, + GlPolyline, GlPolygon, GlText, GlHandle +) + + +class CruxHandle(GlHandle): + + def __init__(self, sensor_size, depth): + GlHandle.__init__(self, sensor_size, 0, True, False) + self.branch_0 = GlPolygon((1, 1, 1, 1), d=2) + self.branch_1 = GlPolygon((1, 1, 1, 1), d=2) + self.branch_2 = GlPolygon((1, 1, 1, 1), d=2) + self.branch_3 = GlPolygon((1, 1, 1, 1), d=2) + self.depth = depth + + def set_pos(self, pos_2d): + self.pos_2d = pos_2d + o = pos_2d + w = 0.5 * self.sensor_width + d = self.depth + c = d / 1.4242 + s = w - c + p0 = o + Vector((s, w)) + p1 = o + Vector((w, s)) + p2 = o + Vector((c, 0)) + p3 = o + Vector((w, -s)) + p4 = o + Vector((s, -w)) + p5 = o + Vector((0, -c)) + p6 = o + Vector((-s, -w)) + p7 = o + Vector((-w, -s)) + p8 = o + Vector((-c, 0)) + p9 = o + Vector((-w, s)) + p10 = o + Vector((-s, w)) + p11 = o + Vector((0, c)) + self.branch_0.set_pos([p11, p0, p1, p2, o]) + self.branch_1.set_pos([p2, p3, p4, p5, o]) + self.branch_2.set_pos([p5, p6, p7, p8, o]) + self.branch_3.set_pos([p8, p9, p10, p11, o]) + + @property + def pts(self): + return [self.pos_2d] + + @property + def sensor_center(self): + return self.pos_2d + + def draw(self, context, render=False): + self.render = render + self.branch_0.colour_inactive = self.colour + self.branch_1.colour_inactive = self.colour + self.branch_2.colour_inactive = self.colour + self.branch_3.colour_inactive = self.colour + self.branch_0.draw(context) + self.branch_1.draw(context) + self.branch_2.draw(context) + self.branch_3.draw(context) + + +class SeekBox(GlText, GlHandle): + """ + Text input to filter items by label + TODO: + - add cross to empty text + - get text from keyboard + """ + + def __init__(self): + GlHandle.__init__(self, 0, 0, True, False, d=2) + GlText.__init__(self, d=2) + self.sensor_width = 250 + self.pos_3d = Vector((0, 0)) + self.bg = GlRect(colour=(0, 0, 0, 0.7)) + self.frame = GlPolyline((1, 1, 1, 1), d=2) + self.frame.closed = True + self.cancel = CruxHandle(16, 4) + self.line_pos = 0 + + @property + def pts(self): + return [self.pos_3d] + + def set_pos(self, context, pos_2d): + x, ty = self.text_size(context) + w = self.sensor_width + y = 12 + pos_2d.y += y + pos_2d.x -= 0.5 * w + self.pos_2d = pos_2d.copy() + self.pos_3d = pos_2d.copy() + self.pos_3d.x += 6 + self.sensor_height = y + p0 = pos_2d + Vector((w, -0.5 * y)) + p1 = pos_2d + Vector((w, 1.5 * y)) + p2 = pos_2d + Vector((0, 1.5 * y)) + p3 = pos_2d + Vector((0, -0.5 * y)) + self.bg.set_pos([p0, p2]) + self.frame.set_pos([p0, p1, p2, p3]) + self.cancel.set_pos(pos_2d + Vector((w + 15, 0.5 * y))) + + def keyboard_entry(self, context, event): + c = event.ascii + if c: + if c == ",": + c = "." + self.label = self.label[:self.line_pos] + c + self.label[self.line_pos:] + self.line_pos += 1 + + if self.label: + if event.type == 'BACK_SPACE': + self.label = self.label[:self.line_pos - 1] + self.label[self.line_pos:] + self.line_pos -= 1 + + elif event.type == 'DEL': + self.label = self.label[:self.line_pos] + self.label[self.line_pos + 1:] + + elif event.type == 'LEFT_ARROW': + self.line_pos = (self.line_pos - 1) % (len(self.label) + 1) + + elif event.type == 'RIGHT_ARROW': + self.line_pos = (self.line_pos + 1) % (len(self.label) + 1) + + def draw(self, context): + self.bg.draw(context) + self.frame.draw(context) + GlText.draw(self, context) + self.cancel.draw(context) + + @property + def sensor_center(self): + return self.pos_3d + + +preset_paths = bpy.utils.script_paths("presets") +addons_paths = bpy.utils.script_paths("addons") + + +class PresetMenuItem(): + def __init__(self, thumbsize, preset, image=None): + name = bpy.path.display_name_from_filepath(preset) + self.preset = preset + self.handle = ThumbHandle(thumbsize, name, image, draggable=True) + self.enable = True + + def filter(self, keywords): + for key in keywords: + if key not in self.handle.label.label: + return False + return True + + def set_pos(self, context, pos): + self.handle.set_pos(context, pos) + + def check_hover(self, mouse_pos): + self.handle.check_hover(mouse_pos) + + def mouse_press(self): + if self.handle.hover: + self.handle.hover = False + self.handle.active = True + return True + return False + + def draw(self, context): + if self.enable: + self.handle.draw(context) + + +class PresetMenu(): + + keyboard_type = { + 'BACK_SPACE', 'DEL', + 'LEFT_ARROW', 'RIGHT_ARROW' + } + + def __init__(self, context, category, thumbsize=Vector((150, 100))): + self.imageList = [] + self.menuItems = [] + self.thumbsize = thumbsize + file_list = self.scan_files(category) + self.default_image = None + self.load_default_image() + for filepath in file_list: + self.make_menuitem(filepath) + self.margin = 50 + self.y_scroll = 0 + self.scroll_max = 1000 + self.spacing = Vector((25, 25)) + self.screen = Screen(self.margin) + self.mouse_pos = Vector((0, 0)) + self.bg = GlRect(colour=(0, 0, 0, 0.7)) + self.border = GlPolyline((0.7, 0.7, 0.7, 1), d=2) + self.keywords = SeekBox() + self.keywords.colour_normal = (1, 1, 1, 1) + + self.border.closed = True + self.set_pos(context) + + def load_default_image(self): + img_idx = bpy.data.images.find("missing.png") + if img_idx > -1: + self.default_image = bpy.data.images[img_idx] + self.imageList.append(self.default_image.filepath_raw) + return + dir_path = os.path.dirname(os.path.realpath(__file__)) + sub_path = "presets" + os.path.sep + "missing.png" + filepath = os.path.join(dir_path, sub_path) + if os.path.exists(filepath) and os.path.isfile(filepath): + self.default_image = bpy.data.images.load(filepath=filepath) + self.imageList.append(self.default_image.filepath_raw) + if self.default_image is None: + raise EnvironmentError("archipack/presets/missing.png not found") + + def scan_files(self, category): + file_list = [] + # load default presets + dir_path = os.path.dirname(os.path.realpath(__file__)) + sub_path = "presets" + os.path.sep + category + presets_path = os.path.join(dir_path, sub_path) + if os.path.exists(presets_path): + file_list += [presets_path + os.path.sep + f[:-3] + for f in os.listdir(presets_path) + if f.endswith('.py') and + not f.startswith('.')] + # load user def presets + for path in preset_paths: + presets_path = os.path.join(path, category) + if os.path.exists(presets_path): + file_list += [presets_path + os.path.sep + f[:-3] + for f in os.listdir(presets_path) + if f.endswith('.py') and + not f.startswith('.')] + + file_list.sort() + return file_list + + def clearImages(self): + for image in bpy.data.images: + if image.filepath_raw in self.imageList: + # image.user_clear() + bpy.data.images.remove(image, do_unlink=True) + self.imageList.clear() + + def make_menuitem(self, filepath): + """ + @TODO: + Lazy load images + """ + image = None + img_idx = bpy.data.images.find(os.path.basename(filepath) + '.png') + if img_idx > -1: + image = bpy.data.images[img_idx] + self.imageList.append(image.filepath_raw) + elif os.path.exists(filepath + '.png') and os.path.isfile(filepath + '.png'): + image = bpy.data.images.load(filepath=filepath + '.png') + self.imageList.append(image) + if image is None: + image = self.default_image + item = PresetMenuItem(self.thumbsize, filepath + '.py', image) + self.menuItems.append(item) + + def set_pos(self, context): + + x_min, x_max, y_min, y_max = self.screen.size(context) + p0, p1, p2, p3 = Vector((x_min, y_min)), Vector((x_min, y_max)), Vector((x_max, y_max)), Vector((x_max, y_min)) + self.bg.set_pos([p0, p2]) + self.border.set_pos([p0, p1, p2, p3]) + x_min += 0.5 * self.thumbsize.x + 0.5 * self.margin + x_max -= 0.5 * self.thumbsize.x + 0.5 * self.margin + y_max -= 0.5 * self.thumbsize.y + 0.5 * self.margin + y_min += 0.5 * self.margin + x = x_min + y = y_max + self.y_scroll + n_rows = 0 + + self.keywords.set_pos(context, p1 + 0.5 * (p2 - p1)) + keywords = self.keywords.label.split(" ") + + for item in self.menuItems: + if y > y_max or y < y_min: + item.enable = False + else: + item.enable = True + + # filter items by name + if len(keywords) > 0 and not item.filter(keywords): + item.enable = False + continue + + item.set_pos(context, Vector((x, y))) + x += self.thumbsize.x + self.spacing.x + if x > x_max: + n_rows += 1 + x = x_min + y -= self.thumbsize.y + self.spacing.y + + self.scroll_max = max(0, n_rows - 1) * (self.thumbsize.y + self.spacing.y) + + def draw(self, context): + self.bg.draw(context) + self.border.draw(context) + self.keywords.draw(context) + for item in self.menuItems: + item.draw(context) + + def mouse_press(self, context, event): + self.mouse_position(event) + + if self.keywords.cancel.hover: + self.keywords.label = "" + self.keywords.line_pos = 0 + self.set_pos(context) + + for item in self.menuItems: + if item.enable and item.mouse_press(): + # load item preset + return item.preset + return None + + def mouse_position(self, event): + self.mouse_pos.x, self.mouse_pos.y = event.mouse_region_x, event.mouse_region_y + + def mouse_move(self, context, event): + self.mouse_position(event) + self.keywords.check_hover(self.mouse_pos) + self.keywords.cancel.check_hover(self.mouse_pos) + for item in self.menuItems: + item.check_hover(self.mouse_pos) + + def scroll_up(self, context, event): + self.y_scroll = max(0, self.y_scroll - (self.thumbsize.y + self.spacing.y)) + self.set_pos(context) + # print("scroll_up %s" % (self.y_scroll)) + + def scroll_down(self, context, event): + self.y_scroll = min(self.scroll_max, self.y_scroll + (self.thumbsize.y + self.spacing.y)) + self.set_pos(context) + # print("scroll_down %s" % (self.y_scroll)) + + def keyboard_entry(self, context, event): + self.keywords.keyboard_entry(context, event) + self.set_pos(context) + + +class PresetMenuOperator(): + + preset_operator = StringProperty( + options={'SKIP_SAVE'}, + default="script.execute_preset" + ) + + def __init__(self): + self.menu = None + self._handle = None + + def exit(self, context): + self.menu.clearImages() + bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW') + + def draw_handler(self, _self, context): + self.menu.draw(context) + + def modal(self, context, event): + if self.menu is None: + return {'FINISHED'} + context.area.tag_redraw() + if event.type == 'MOUSEMOVE': + self.menu.mouse_move(context, event) + elif event.type == 'WHEELUPMOUSE' or \ + (event.type == 'UP_ARROW' and event.value == 'PRESS'): + self.menu.scroll_up(context, event) + elif event.type == 'WHEELDOWNMOUSE' or \ + (event.type == 'DOWN_ARROW' and event.value == 'PRESS'): + self.menu.scroll_down(context, event) + elif event.type == 'LEFTMOUSE' and event.value == 'RELEASE': + preset = self.menu.mouse_press(context, event) + if preset is not None: + self.exit(context) + po = self.preset_operator.split(".") + op = getattr(getattr(bpy.ops, po[0]), po[1]) + if self.preset_operator == 'script.execute_preset': + # call from preset menu + # ensure right active_object class + d = getattr(bpy.types, self.preset_subdir).datablock(context.active_object) + if d is not None: + d.auto_update = False + # print("Archipack execute_preset loading auto_update:%s" % d.auto_update) + op('INVOKE_DEFAULT', filepath=preset, menu_idname=self.bl_idname) + # print("Archipack execute_preset loaded auto_update: %s" % d.auto_update) + d.auto_update = True + else: + # call draw operator + if op.poll(): + op('INVOKE_DEFAULT', filepath=preset) + else: + print("Poll failed") + return {'FINISHED'} + elif event.ascii or ( + event.type in self.menu.keyboard_type and + event.value == 'RELEASE'): + self.menu.keyboard_entry(context, event) + elif event.type in {'RIGHTMOUSE', 'ESC'}: + self.exit(context) + return {'CANCELLED'} + + return {'RUNNING_MODAL'} + + def invoke(self, context, event): + if context.area.type == 'VIEW_3D': + + # with alt pressed on invoke, will bypass menu operator and + # call preset_operator + # allow start drawing linked copy of active object + if event.alt or event.ctrl: + po = self.preset_operator.split(".") + op = getattr(getattr(bpy.ops, po[0]), po[1]) + d = context.active_object.data + + if d is not None and self.preset_subdir in d and op.poll(): + op('INVOKE_DEFAULT') + else: + self.report({'WARNING'}, "Active object must be a " + self.preset_subdir.split("_")[1].capitalize()) + return {'CANCELLED'} + return {'FINISHED'} + + self.menu = PresetMenu(context, self.preset_subdir) + + # the arguments we pass the the callback + args = (self, context) + # Add the region OpenGL drawing callback + # draw in view space with 'POST_VIEW' and 'PRE_VIEW' + self._handle = bpy.types.SpaceView3D.draw_handler_add(self.draw_handler, args, 'WINDOW', 'POST_PIXEL') + context.window_manager.modal_handler_add(self) + return {'RUNNING_MODAL'} + else: + self.report({'WARNING'}, "View3D not found, cannot show preset flinger") + return {'CANCELLED'} + + +class ArchipackPreset(AddPresetBase): + + @classmethod + def poll(cls, context): + o = context.active_object + return o is not None and \ + o.data is not None and \ + "archipack_" + cls.__name__[13:-7] in o.data + + @property + def preset_subdir(self): + return "archipack_" + self.__class__.__name__[13:-7] + + @property + def blacklist(self): + """ + properties black list for presets + may override on addon basis + """ + return [] + + @property + def preset_values(self): + blacklist = self.blacklist + blacklist.extend(bpy.types.Mesh.bl_rna.properties.keys()) + d = getattr(bpy.context.active_object.data, self.preset_subdir)[0] + props = d.rna_type.bl_rna.properties.items() + ret = [] + for prop_id, prop in props: + if prop_id not in blacklist: + if not (prop.is_hidden or prop.is_skip_save): + ret.append("d.%s" % prop_id) + return ret + + @property + def preset_defines(self): + return ["d = bpy.context.active_object.data." + self.preset_subdir + "[0]"] + + def pre_cb(self, context): + return + + def remove(self, context, filepath): + # remove preset + os.remove(filepath) + # remove thumb + os.remove(filepath[:-3] + ".png") + + def post_cb(self, context): + + if not self.remove_active: + + name = self.name.strip() + if not name: + return + + filename = self.as_filename(name) + target_path = os.path.join("presets", self.preset_subdir) + target_path = bpy.utils.user_resource('SCRIPTS', + target_path, + create=True) + + filepath = os.path.join(target_path, filename) + ".png" + + # render thumb + scene = context.scene + render = scene.render + + # save render parame + resolution_x = render.resolution_x + resolution_y = render.resolution_y + resolution_percentage = render.resolution_percentage + old_filepath = render.filepath + use_file_extension = render.use_file_extension + use_overwrite = render.use_overwrite + use_compositing = render.use_compositing + use_sequencer = render.use_sequencer + file_format = render.image_settings.file_format + color_mode = render.image_settings.color_mode + color_depth = render.image_settings.color_depth + + render.resolution_x = 150 + render.resolution_y = 100 + render.resolution_percentage = 100 + render.filepath = filepath + render.use_file_extension = True + render.use_overwrite = True + render.use_compositing = False + render.use_sequencer = False + render.image_settings.file_format = 'PNG' + render.image_settings.color_mode = 'RGBA' + render.image_settings.color_depth = '8' + bpy.ops.render.render(animation=False, write_still=True, use_viewport=False) + + # restore render params + render.resolution_x = resolution_x + render.resolution_y = resolution_y + render.resolution_percentage = resolution_percentage + render.filepath = old_filepath + render.use_file_extension = use_file_extension + render.use_overwrite = use_overwrite + render.use_compositing = use_compositing + render.use_sequencer = use_sequencer + render.image_settings.file_format = file_format + render.image_settings.color_mode = color_mode + render.image_settings.color_depth = color_depth + + return diff --git a/archipack/archipack_reference_point.py b/archipack/archipack_reference_point.py new file mode 100644 index 00000000..d81a6839 --- /dev/null +++ b/archipack/archipack_reference_point.py @@ -0,0 +1,368 @@ +# -*- coding:utf-8 -*- + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110- 1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +# + +# ---------------------------------------------------------- +# Author: Stephen Leger (s-leger) +# +# ---------------------------------------------------------- +import bpy +from bpy.types import Operator, PropertyGroup, Object, Panel +from bpy.props import ( + FloatVectorProperty, + CollectionProperty, + FloatProperty + ) +from mathutils import Vector +from .bmesh_utils import BmeshEdit as bmed + + +def update(self, context): + self.update(context) + + +class archipack_reference_point(PropertyGroup): + location_2d = FloatVectorProperty( + subtype='XYZ', + name="position 2d", + default=Vector((0, 0, 0)) + ) + location_3d = FloatVectorProperty( + subtype='XYZ', + name="position 3d", + default=Vector((0, 0, 0)) + ) + symbol_scale = FloatProperty( + name="Screen scale", + default=1, + min=0.01, + update=update) + + @classmethod + def filter(cls, o): + """ + Filter object with this class in data + return + True when object contains this datablock + False otherwhise + usage: + class_name.filter(object) from outside world + self.__class__.filter(object) from instance + """ + try: + return cls.__name__ in o + except: + pass + return False + + @classmethod + def datablock(cls, o): + """ + Retrieve datablock from base object + return + datablock when found + None when not found + usage: + class_name.datablock(object) from outside world + self.__class__.datablock(object) from instance + """ + try: + return getattr(o, cls.__name__)[0] + except: + pass + return None + + def update(self, context): + + o = context.active_object + + if self.datablock(o) != self: + return + + s = self.symbol_scale + verts = [(s * x, s * y, s * z) for x, y, z in [ + (-0.25, 0.25, 0.0), (0.25, 0.25, 0.0), (-0.25, -0.25, 0.0), (0.25, -0.25, 0.0), + (0.0, 0.0, 0.487), (-0.107, 0.107, 0.216), (0.108, 0.107, 0.216), (-0.107, -0.107, 0.216), + (0.108, -0.107, 0.216), (-0.05, 0.05, 0.5), (0.05, 0.05, 0.5), (0.05, -0.05, 0.5), + (-0.05, -0.05, 0.5), (-0.193, 0.193, 0.0), (0.193, 0.193, 0.0), (0.193, -0.193, 0.0), + (-0.193, -0.193, 0.0), (0.0, 0.0, 0.8), (0.0, 0.8, -0.0), (0.0, 0.0, -0.0), + (0.0, 0.0, 0.0), (0.05, 0.05, 0.674), (-0.05, 0.674, -0.05), (0.0, 0.8, -0.0), + (-0.05, -0.05, 0.674), (-0.05, 0.674, 0.05), (0.05, 0.674, -0.05), (-0.129, 0.129, 0.162), + (0.129, 0.129, 0.162), (-0.129, -0.129, 0.162), (0.129, -0.129, 0.162), (0.0, 0.0, 0.8), + (-0.05, 0.05, 0.674), (0.05, -0.05, 0.674), (0.05, 0.674, 0.05), (0.8, -0.0, -0.0), + (0.0, -0.0, -0.0), (0.674, 0.05, -0.05), (0.8, -0.0, -0.0), (0.674, 0.05, 0.05), + (0.674, -0.05, -0.05), (0.674, -0.05, 0.05)]] + + edges = [(1, 0), (0, 9), (9, 10), (10, 1), (3, 1), (10, 11), + (11, 3), (2, 3), (11, 12), (12, 2), (0, 2), (12, 9), + (6, 5), (8, 6), (7, 8), (5, 7), (17, 24), (17, 20), + (18, 25), (18, 19), (13, 14), (14, 15), (15, 16), (16, 13), + (4, 6), (15, 30), (17, 21), (26, 22), (23, 22), (23, 34), + (18, 26), (28, 27), (30, 28), (29, 30), (27, 29), (14, 28), + (13, 27), (16, 29), (4, 7), (4, 8), (4, 5), (31, 33), + (31, 32), (21, 32), (24, 32), (24, 33), (21, 33), (25, 22), + (25, 34), (26, 34), (35, 39), (35, 36), (40, 37), (38, 37), + (38, 41), (35, 40), (39, 37), (39, 41), (40, 41)] + + bm = bmed._start(context, o) + bm.clear() + for v in verts: + bm.verts.new(v) + bm.verts.ensure_lookup_table() + for ed in edges: + bm.edges.new((bm.verts[ed[0]], bm.verts[ed[1]])) + bmed._end(bm, o) + + +class ARCHIPACK_PT_reference_point(Panel): + bl_idname = "ARCHIPACK_PT_reference_point" + bl_label = "Reference point" + bl_space_type = 'VIEW_3D' + bl_region_type = 'UI' + bl_category = 'ArchiPack' + + @classmethod + def poll(cls, context): + return archipack_reference_point.filter(context.active_object) + + def draw(self, context): + o = context.active_object + props = archipack_reference_point.datablock(o) + if props is None: + return + layout = self.layout + if (o.location - props.location_2d).length < 0.01: + layout.operator('archipack.move_to_3d') + layout.operator('archipack.move_2d_reference_to_cursor') + else: + layout.operator('archipack.move_to_2d') + + layout.prop(props, 'symbol_scale') + + +class ARCHIPACK_OT_reference_point(Operator): + """Add reference point""" + bl_idname = "archipack.reference_point" + bl_label = "Reference point" + bl_description = "Add reference point" + bl_category = 'Archipack' + bl_options = {'REGISTER', 'UNDO'} + location_3d = FloatVectorProperty( + subtype='XYZ', + name="position 3d", + default=Vector((0, 0, 0)) + ) + + @classmethod + def poll(cls, context): + return context.active_object is not None + + def draw(self, context): + layout = self.layout + row = layout.row() + row.label("Use Properties panel (N) to define parms", icon='INFO') + + def create(self, context): + x, y, z = context.scene.cursor_location + # bpy.ops.object.empty_add(type='ARROWS', radius=0.5, location=Vector((x, y, 0))) + m = bpy.data.meshes.new(name="Reference") + o = bpy.data.objects.new("Reference", m) + o.location = Vector((x, y, 0)) + context.scene.objects.link(o) + d = o.archipack_reference_point.add() + d.location_2d = Vector((x, y, 0)) + d.location_3d = self.location_3d + o.select = True + context.scene.objects.active = o + d.update(context) + return o + + def execute(self, context): + if context.mode == "OBJECT": + o = self.create(context) + o.select = True + context.scene.objects.active = o + return {'FINISHED'} + else: + self.report({'WARNING'}, "Archipack: Option only valid in Object mode") + return {'CANCELLED'} + + +class ARCHIPACK_OT_move_to_3d(Operator): + bl_idname = "archipack.move_to_3d" + bl_label = "Move to 3d" + bl_description = "Move point to 3d position" + bl_category = 'Archipack' + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + return archipack_reference_point.filter(context.active_object) + + def execute(self, context): + if context.mode == "OBJECT": + o = context.active_object + props = archipack_reference_point.datablock(o) + if props is None: + return {'CANCELLED'} + o.location = props.location_3d + return {'FINISHED'} + else: + self.report({'WARNING'}, "Archipack: Option only valid in Object mode") + return {'CANCELLED'} + + +class ARCHIPACK_OT_move_to_2d(Operator): + bl_idname = "archipack.move_to_2d" + bl_label = "Move to 2d" + bl_description = "Move point to 2d position" + bl_category = 'Archipack' + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + return archipack_reference_point.filter(context.active_object) + + def execute(self, context): + if context.mode == "OBJECT": + o = context.active_object + props = archipack_reference_point.datablock(o) + if props is None: + return {'CANCELLED'} + props.location_3d = o.location + o.location = props.location_2d + return {'FINISHED'} + else: + self.report({'WARNING'}, "Archipack: Option only valid in Object mode") + return {'CANCELLED'} + + +class ARCHIPACK_OT_store_2d_reference(Operator): + bl_idname = "archipack.store_2d_reference" + bl_label = "Set 2d" + bl_description = "Set 2d reference position" + bl_category = 'Archipack' + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + return archipack_reference_point.filter(context.active_object) + + def execute(self, context): + if context.mode == "OBJECT": + o = context.active_object + props = archipack_reference_point.datablock(o) + if props is None: + return {'CANCELLED'} + x, y, z = o.location + props.location_2d = Vector((x, y, 0)) + return {'FINISHED'} + else: + self.report({'WARNING'}, "Archipack: Option only valid in Object mode") + return {'CANCELLED'} + + +class ARCHIPACK_OT_move_2d_reference_to_cursor(Operator): + bl_idname = "archipack.move_2d_reference_to_cursor" + bl_label = "Change 2d" + bl_description = "Change 2d reference position to cursor location without moving childs" + bl_category = 'Archipack' + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + return archipack_reference_point.filter(context.active_object) + + def execute(self, context): + if context.mode == "OBJECT": + o = context.active_object + props = archipack_reference_point.datablock(o) + if props is None: + return {'CANCELLED'} + bpy.ops.object.select_all(action="DESELECT") + bpy.ops.archipack.reference_point(location_3d=props.location_3d) + for child in o.children: + child.select = True + bpy.ops.archipack.parent_to_reference() + context.scene.objects.unlink(o) + return {'FINISHED'} + else: + self.report({'WARNING'}, "Archipack: Option only valid in Object mode") + return {'CANCELLED'} + + +class ARCHIPACK_OT_parent_to_reference(Operator): + bl_idname = "archipack.parent_to_reference" + bl_label = "Parent" + bl_description = "Make selected object childs of parent reference point" + bl_category = 'Archipack' + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + return archipack_reference_point.filter(context.active_object) + + def execute(self, context): + if context.mode == "OBJECT": + o = context.active_object + props = archipack_reference_point.datablock(o) + if props is None: + return {'CANCELLED'} + sel = [obj for obj in context.selected_objects if obj != o and obj.parent != o] + itM = o.matrix_world.inverted() + # print("parent_to_reference parenting:%s objects" % (len(sel))) + for child in sel: + rs = child.matrix_world.to_3x3().to_4x4() + loc = itM * child.matrix_world.translation + child.parent = None + child.matrix_parent_inverse.identity() + child.location = Vector((0, 0, 0)) + child.parent = o + child.matrix_world = rs + child.location = loc + return {'FINISHED'} + else: + self.report({'WARNING'}, "Archipack: Option only valid in Object mode") + return {'CANCELLED'} + + +def register(): + bpy.utils.register_class(archipack_reference_point) + Object.archipack_reference_point = CollectionProperty(type=archipack_reference_point) + bpy.utils.register_class(ARCHIPACK_PT_reference_point) + bpy.utils.register_class(ARCHIPACK_OT_reference_point) + bpy.utils.register_class(ARCHIPACK_OT_move_to_3d) + bpy.utils.register_class(ARCHIPACK_OT_move_to_2d) + bpy.utils.register_class(ARCHIPACK_OT_store_2d_reference) + bpy.utils.register_class(ARCHIPACK_OT_move_2d_reference_to_cursor) + bpy.utils.register_class(ARCHIPACK_OT_parent_to_reference) + + +def unregister(): + bpy.utils.unregister_class(archipack_reference_point) + del Object.archipack_reference_point + bpy.utils.unregister_class(ARCHIPACK_PT_reference_point) + bpy.utils.unregister_class(ARCHIPACK_OT_reference_point) + bpy.utils.unregister_class(ARCHIPACK_OT_move_to_3d) + bpy.utils.unregister_class(ARCHIPACK_OT_move_to_2d) + bpy.utils.unregister_class(ARCHIPACK_OT_store_2d_reference) + bpy.utils.unregister_class(ARCHIPACK_OT_move_2d_reference_to_cursor) + bpy.utils.unregister_class(ARCHIPACK_OT_parent_to_reference) diff --git a/archipack/archipack_rendering.py b/archipack/archipack_rendering.py new file mode 100644 index 00000000..3d86d4d8 --- /dev/null +++ b/archipack/archipack_rendering.py @@ -0,0 +1,529 @@ +# -*- coding:utf-8 -*- + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110- 1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +# + +# ---------------------------------------------------------- +# support routines for render measures in final image +# Author: Antonio Vazquez (antonioya) +# Archipack adaptation by : Stephen Leger (s-leger) +# +# ---------------------------------------------------------- +# noinspection PyUnresolvedReferences +import bpy +# noinspection PyUnresolvedReferences +import bgl +from os import path, remove +from sys import exc_info +# noinspection PyUnresolvedReferences +import bpy_extras.image_utils as img_utils +# noinspection PyUnresolvedReferences +from math import ceil +from bpy.types import Operator + + +# ------------------------------------------------------------- +# Defines button for render +# +# ------------------------------------------------------------- +class ARCHIPACK_OT_render(Operator): + bl_idname = "archipack.render" + bl_label = "Render" + bl_category = 'Archipack' + bl_description = "Create a render image with measures. Use UV/Image editor to view image generated" + bl_category = 'Archipack' + + # -------------------------------------------------------------------- + # Get the final render image and return as image object + # + # return None if no render available + # -------------------------------------------------------------------- + + def get_render_image(self, outpath): + saved = False + # noinspection PyBroadException + try: + # noinspection PyBroadException + try: + result = bpy.data.images['Render Result'] + if result.has_data is False: + # this save produce to fill data image + result.save_render(outpath) + saved = True + except: + print("No render image found") + return None + + # Save and reload + if saved is False: + result.save_render(outpath) + + img = img_utils.load_image(outpath) + + return img + except: + print("Unexpected render image error") + return None + + # ------------------------------------- + # Save image to file + # ------------------------------------- + + def save_image(self, filepath, myimage): + # noinspection PyBroadException + try: + + # Save old info + settings = bpy.context.scene.render.image_settings + myformat = settings.file_format + mode = settings.color_mode + depth = settings.color_depth + + # Apply new info and save + settings.file_format = 'PNG' + settings.color_mode = "RGBA" + settings.color_depth = '8' + myimage.save_render(filepath) + print("Archipack: Image " + filepath + " saved") + + # Restore old info + settings.file_format = myformat + settings.color_mode = mode + settings.color_depth = depth + except: + print("Unexpected error:" + str(exc_info())) + self.report({'ERROR'}, "Archipack: Unable to save render image") + return + + # ------------------------------------------------------------- + # Render image main entry point + # + # ------------------------------------------------------------- + + def render_main(self, context, objlist, animation=False): + # noinspection PyBroadException,PyBroadException + # Save old info + scene = context.scene + render = scene.render + settings = render.image_settings + depth = settings.color_depth + settings.color_depth = '8' + # noinspection PyBroadException + try: + + # Get visible layers + layers = [] + for x in range(0, 20): + if scene.layers[x] is True: + layers.extend([x]) + + # -------------------- + # Get resolution + # -------------------- + render_scale = render.resolution_percentage / 100 + + width = int(render.resolution_x * render_scale) + height = int(render.resolution_y * render_scale) + # --------------------------------------- + # Get output path + # --------------------------------------- + temp_path = path.realpath(bpy.app.tempdir) + if len(temp_path) > 0: + outpath = path.join(temp_path, "archipack_tmp_render.png") + else: + self.report({'ERROR'}, + "Archipack: Unable to save temporary render image. Define a valid temp path") + settings.color_depth = depth + return False + + # Get Render Image + img = self.get_render_image(outpath) + if img is None: + self.report({'ERROR'}, + "Archipack: Unable to save temporary render image. Define a valid temp path") + settings.color_depth = depth + return False + + # ----------------------------- + # Calculate rows and columns + # ----------------------------- + tile_x = 240 + tile_y = 216 + row_num = ceil(height / tile_y) + col_num = ceil(width / tile_x) + print("Archipack: Image divided in " + str(row_num) + "x" + str(col_num) + " tiles") + + # pixels out of visible area + cut4 = (col_num * tile_x * 4) - width * 4 # pixels aout of drawing area + totpixel4 = width * height * 4 # total pixels RGBA + + viewport_info = bgl.Buffer(bgl.GL_INT, 4) + bgl.glGetIntegerv(bgl.GL_VIEWPORT, viewport_info) + + # Load image on memory + img.gl_load(0, bgl.GL_NEAREST, bgl.GL_NEAREST) + + # 2.77 API change + if bpy.app.version >= (2, 77, 0): + tex = img.bindcode[0] + else: + tex = img.bindcode + + # -------------------------------------------- + # Create output image (to apply texture) + # -------------------------------------------- + if "archipack_output" in bpy.data.images: + out_img = bpy.data.images["archipack_output"] + if out_img is not None: + out_img.user_clear() + bpy.data.images.remove(out_img) + + out = bpy.data.images.new("archipack_output", width, height) + tmp_pixels = [1] * totpixel4 + + # -------------------------------- + # Loop for all tiles + # -------------------------------- + for row in range(0, row_num): + for col in range(0, col_num): + buffer = bgl.Buffer(bgl.GL_FLOAT, width * height * 4) + bgl.glDisable(bgl.GL_SCISSOR_TEST) # if remove this line, get blender screenshot not image + bgl.glViewport(0, 0, tile_x, tile_y) + + bgl.glMatrixMode(bgl.GL_PROJECTION) + bgl.glLoadIdentity() + + # defines ortographic view for single tile + x1 = tile_x * col + y1 = tile_y * row + bgl.gluOrtho2D(x1, x1 + tile_x, y1, y1 + tile_y) + + # Clear + bgl.glClearColor(0.0, 0.0, 0.0, 0.0) + bgl.glClear(bgl.GL_COLOR_BUFFER_BIT | bgl.GL_DEPTH_BUFFER_BIT) + + bgl.glEnable(bgl.GL_TEXTURE_2D) + bgl.glBindTexture(bgl.GL_TEXTURE_2D, tex) + + # defines drawing area + bgl.glBegin(bgl.GL_QUADS) + + bgl.glColor3f(1.0, 1.0, 1.0) + bgl.glTexCoord2f(0.0, 0.0) + bgl.glVertex2f(0.0, 0.0) + + bgl.glTexCoord2f(1.0, 0.0) + bgl.glVertex2f(width, 0.0) + + bgl.glTexCoord2f(1.0, 1.0) + bgl.glVertex2f(width, height) + + bgl.glTexCoord2f(0.0, 1.0) + bgl.glVertex2f(0.0, height) + + bgl.glEnd() + + # ----------------------------- + # Loop to draw all lines + # ----------------------------- + for o, d in objlist: + if o.hide is False: + # verify visible layer + for x in range(0, 20): + if o.layers[x] is True: + if x in layers: + context.scene.objects.active = o + # print("%s: %s" % (o.name, d.manip_stack)) + manipulators = d.manip_stack + if manipulators is not None: + for m in manipulators: + if m is not None: + m.draw_callback(m, context, render=True) + break + + # ----------------------------- + # Loop to draw all debug + # ----------------------------- + """ + if scene.archipack_debug is True: + selobj = bpy.context.selected_objects + for myobj in selobj: + if scene.archipack_debug_vertices is True: + draw_vertices(context, myobj, None, None) + if scene.archipack_debug_faces is True or scene.archipack_debug_normals is True: + draw_faces(context, myobj, None, None) + """ + """ + if scene.archipack_rf is True: + bgl.glColor3f(1.0, 1.0, 1.0) + rfcolor = scene.archipack_rf_color + rfborder = scene.archipack_rf_border + rfline = scene.archipack_rf_line + + bgl.glLineWidth(rfline) + bgl.glColor4f(rfcolor[0], rfcolor[1], rfcolor[2], rfcolor[3]) + + x1 = rfborder + x2 = width - rfborder + y1 = int(ceil(rfborder / (width / height))) + y2 = height - y1 + draw_rectangle((x1, y1), (x2, y2)) + """ + # -------------------------------- + # copy pixels to temporary area + # -------------------------------- + bgl.glFinish() + bgl.glReadPixels(0, 0, width, height, bgl.GL_RGBA, bgl.GL_FLOAT, buffer) # read image data + for y in range(0, tile_y): + # final image pixels position + p1 = (y * width * 4) + (row * tile_y * width * 4) + (col * tile_x * 4) + p2 = p1 + (tile_x * 4) + # buffer pixels position + b1 = y * width * 4 + b2 = b1 + (tile_x * 4) + + if p1 < totpixel4: # avoid pixel row out of area + if col == col_num - 1: # avoid pixel columns out of area + p2 -= cut4 + b2 -= cut4 + + tmp_pixels[p1:p2] = buffer[b1:b2] + + # ----------------------- + # Copy temporary to final + # ----------------------- + out.pixels = tmp_pixels[:] # Assign image data + img.gl_free() # free opengl image memory + + # delete image + img.user_clear() + bpy.data.images.remove(img) + # remove temp file + remove(outpath) + # reset + bgl.glEnable(bgl.GL_SCISSOR_TEST) + # ----------------------- + # restore opengl defaults + # ----------------------- + bgl.glLineWidth(1) + bgl.glDisable(bgl.GL_BLEND) + bgl.glColor4f(0.0, 0.0, 0.0, 1.0) + # Saves image + if out is not None: + # and (scene.archipack_render is True or animation is True): + ren_path = bpy.context.scene.render.filepath + filename = "ap_frame" + if len(ren_path) > 0: + if ren_path.endswith(path.sep): + initpath = path.realpath(ren_path) + path.sep + else: + (initpath, filename) = path.split(ren_path) + + ftxt = "%04d" % scene.frame_current + outpath = path.realpath(path.join(initpath, filename + ftxt + ".png")) + + self.save_image(outpath, out) + + settings.color_depth = depth + return True + + except: + settings.color_depth = depth + print("Unexpected error:" + str(exc_info())) + self.report( + {'ERROR'}, + "Archipack: Unable to create render image. Be sure the output render path is correct" + ) + return False + + def get_objlist(self, context): + """ + Get objects with gl manipulators + """ + objlist = [] + for o in context.scene.objects: + if o.data is not None: + d = None + if 'archipack_window' in o.data: + d = o.data.archipack_window[0] + elif 'archipack_door' in o.data: + d = o.data.archipack_door[0] + elif 'archipack_wall2' in o.data: + d = o.data.archipack_wall2[0] + elif 'archipack_stair' in o.data: + d = o.data.archipack_stair[0] + elif 'archipack_fence' in o.data: + d = o.data.archipack_fence[0] + if d is not None: + objlist.append((o, d)) + return objlist + + def draw_gl(self, context): + objlist = self.get_objlist(context) + for o, d in objlist: + context.scene.objects.active = o + d.manipulable_disable(context) + d.manipulable_invoke(context) + return objlist + + def hide_gl(self, context, objlist): + for o, d in objlist: + context.scene.objects.active = o + d.manipulable_disable(context) + + # ------------------------------ + # Execute button action + # ------------------------------ + # noinspection PyMethodMayBeStatic,PyUnusedLocal + def execute(self, context): + scene = context.scene + wm = context.window_manager + msg = "New image created with measures. Open it in UV/image editor" + camera_msg = "Unable to render. No camera found" + + # ----------------------------- + # Check camera + # ----------------------------- + if scene.camera is None: + self.report({'ERROR'}, camera_msg) + return {'FINISHED'} + + objlist = self.draw_gl(context) + + # ----------------------------- + # Use current rendered image + # ----------------------------- + if wm.archipack.render_type == "1": + # noinspection PyBroadException + try: + result = bpy.data.images['Render Result'] + if result.has_data is False: + bpy.ops.render.render() + except: + bpy.ops.render.render() + + print("Archipack: Using current render image on buffer") + if self.render_main(context, objlist) is True: + self.report({'INFO'}, msg) + + # ----------------------------- + # OpenGL image + # ----------------------------- + elif wm.archipack.render_type == "2": + self.set_camera_view() + self.set_only_render(True) + + print("Archipack: Rendering opengl image") + bpy.ops.render.opengl() + if self.render_main(context, objlist) is True: + self.report({'INFO'}, msg) + + self.set_only_render(False) + + # ----------------------------- + # OpenGL Animation + # ----------------------------- + elif wm.archipack.render_type == "3": + oldframe = scene.frame_current + self.set_camera_view() + self.set_only_render(True) + flag = False + # loop frames + for frm in range(scene.frame_start, scene.frame_end + 1): + scene.frame_set(frm) + print("Archipack: Rendering opengl frame %04d" % frm) + bpy.ops.render.opengl() + flag = self.render_main(context, objlist, True) + if flag is False: + break + + self.set_only_render(False) + scene.frame_current = oldframe + if flag is True: + self.report({'INFO'}, msg) + + # ----------------------------- + # Image + # ----------------------------- + elif wm.archipack.render_type == "4": + print("Archipack: Rendering image") + bpy.ops.render.render() + if self.render_main(context, objlist) is True: + self.report({'INFO'}, msg) + + # ----------------------------- + # Animation + # ----------------------------- + elif wm.archipack.render_type == "5": + oldframe = scene.frame_current + flag = False + # loop frames + for frm in range(scene.frame_start, scene.frame_end + 1): + scene.frame_set(frm) + print("Archipack: Rendering frame %04d" % frm) + bpy.ops.render.render() + flag = self.render_main(context, objlist, True) + if flag is False: + break + + scene.frame_current = oldframe + if flag is True: + self.report({'INFO'}, msg) + + self.hide_gl(context, objlist) + + return {'FINISHED'} + + # --------------------- + # Set cameraView + # --------------------- + # noinspection PyMethodMayBeStatic + def set_camera_view(self): + for area in bpy.context.screen.areas: + if area.type == 'VIEW_3D': + area.spaces[0].region_3d.view_perspective = 'CAMERA' + + # ------------------------------------- + # Set only render status + # ------------------------------------- + # noinspection PyMethodMayBeStatic + def set_only_render(self, status): + screen = bpy.context.screen + + v3d = False + s = None + # get spaceview_3d in current screen + for a in screen.areas: + if a.type == 'VIEW_3D': + for s in a.spaces: + if s.type == 'VIEW_3D': + v3d = s + break + + if v3d is not False: + s.show_only_render = status + + +def register(): + bpy.utils.register_class(ARCHIPACK_OT_render) + + +def unregister(): + bpy.utils.unregister_class(ARCHIPACK_OT_render) diff --git a/archipack/archipack_slab.py b/archipack/archipack_slab.py new file mode 100644 index 00000000..d29c1678 --- /dev/null +++ b/archipack/archipack_slab.py @@ -0,0 +1,1505 @@ +# -*- coding:utf-8 -*- + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110- 1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +# + +# ---------------------------------------------------------- +# Author: Stephen Leger (s-leger) +# +# ---------------------------------------------------------- +# noinspection PyUnresolvedReferences +import bpy +# noinspection PyUnresolvedReferences +from bpy.types import Operator, PropertyGroup, Mesh, Panel +from bpy.props import ( + FloatProperty, BoolProperty, IntProperty, + StringProperty, EnumProperty, + CollectionProperty + ) +import bmesh +from mathutils import Vector, Matrix +from mathutils.geometry import interpolate_bezier +from math import sin, cos, pi, atan2 +from .archipack_manipulator import Manipulable, archipack_manipulator +from .archipack_object import ArchipackCreateTool, ArchipackObject +from .archipack_2d import Line, Arc + + +class Slab(): + + def __init__(self): + # self.colour_inactive = (1, 1, 1, 1) + pass + + def set_offset(self, offset, last=None): + """ + Offset line and compute intersection point + between segments + """ + self.line = self.make_offset(offset, last) + + def straight_slab(self, a0, length): + s = self.straight(length).rotate(a0) + return StraightSlab(s.p, s.v) + + def curved_slab(self, a0, da, radius): + n = self.normal(1).rotate(a0).scale(radius) + if da < 0: + n.v = -n.v + a0 = n.angle + c = n.p - n.v + return CurvedSlab(c, radius, a0, da) + + +class StraightSlab(Slab, Line): + + def __init__(self, p, v): + Line.__init__(self, p, v) + Slab.__init__(self) + + +class CurvedSlab(Slab, Arc): + + def __init__(self, c, radius, a0, da): + Arc.__init__(self, c, radius, a0, da) + Slab.__init__(self) + + +class SlabGenerator(): + + def __init__(self, parts): + self.parts = parts + self.segs = [] + + def add_part(self, part): + + if len(self.segs) < 1: + s = None + else: + s = self.segs[-1] + # start a new slab + if s is None: + if part.type == 'S_SEG': + p = Vector((0, 0)) + v = part.length * Vector((cos(part.a0), sin(part.a0))) + s = StraightSlab(p, v) + elif part.type == 'C_SEG': + c = -part.radius * Vector((cos(part.a0), sin(part.a0))) + s = CurvedSlab(c, part.radius, part.a0, part.da) + else: + if part.type == 'S_SEG': + s = s.straight_slab(part.a0, part.length) + elif part.type == 'C_SEG': + s = s.curved_slab(part.a0, part.da, part.radius) + + self.segs.append(s) + self.last_type = part.type + + def set_offset(self): + last = None + for i, seg in enumerate(self.segs): + seg.set_offset(self.parts[i].offset, last) + last = seg.line + + """ + def close(self, closed): + # Make last segment implicit closing one + if closed: + return + """ + + def close(self, closed): + # Make last segment implicit closing one + if closed: + part = self.parts[-1] + w = self.segs[-1] + dp = self.segs[0].p0 - self.segs[-1].p0 + if "C_" in part.type: + dw = (w.p1 - w.p0) + w.r = part.radius / dw.length * dp.length + # angle pt - p0 - angle p0 p1 + da = atan2(dp.y, dp.x) - atan2(dw.y, dw.x) + a0 = w.a0 + da + if a0 > pi: + a0 -= 2 * pi + if a0 < -pi: + a0 += 2 * pi + w.a0 = a0 + else: + w.v = dp + + if len(self.segs) > 1: + w.line = w.make_offset(self.parts[-1].offset, self.segs[-2]) + + w = self.segs[-1] + p1 = self.segs[0].line.p1 + self.segs[0].line = self.segs[0].make_offset(self.parts[0].offset, w.line) + self.segs[0].line.p1 = p1 + + def locate_manipulators(self): + """ + setup manipulators + """ + for i, f in enumerate(self.segs): + + manipulators = self.parts[i].manipulators + p0 = f.p0.to_3d() + p1 = f.p1.to_3d() + # angle from last to current segment + if i > 0: + v0 = self.segs[i - 1].straight(-1, 1).v.to_3d() + v1 = f.straight(1, 0).v.to_3d() + manipulators[0].set_pts([p0, v0, v1]) + + if type(f).__name__ == "StraightSlab": + # segment length + manipulators[1].type_key = 'SIZE' + manipulators[1].prop1_name = "length" + manipulators[1].set_pts([p0, p1, (1, 0, 0)]) + else: + # segment radius + angle + v0 = (f.p0 - f.c).to_3d() + v1 = (f.p1 - f.c).to_3d() + manipulators[1].type_key = 'ARC_ANGLE_RADIUS' + manipulators[1].prop1_name = "da" + manipulators[1].prop2_name = "radius" + manipulators[1].set_pts([f.c.to_3d(), v0, v1]) + + # snap manipulator, dont change index ! + manipulators[2].set_pts([p0, p1, (1, 0, 0)]) + # dumb segment id + manipulators[3].set_pts([p0, p1, (1, 0, 0)]) + + def get_verts(self, verts): + for slab in self.segs: + if "Curved" in type(slab).__name__: + for i in range(16): + x, y = slab.line.lerp(i / 16) + verts.append((x, y, 0)) + else: + x, y = slab.line.p0 + verts.append((x, y, 0)) + """ + for i in range(33): + x, y = slab.line.lerp(i / 32) + verts.append((x, y, 0)) + """ + + def rotate(self, idx_from, a): + """ + apply rotation to all following segs + """ + self.segs[idx_from].rotate(a) + ca = cos(a) + sa = sin(a) + rM = Matrix([ + [ca, -sa], + [sa, ca] + ]) + # rotation center + p0 = self.segs[idx_from].p0 + for i in range(idx_from + 1, len(self.segs)): + seg = self.segs[i] + # rotate seg + seg.rotate(a) + # rotate delta from rotation center to segment start + dp = rM * (seg.p0 - p0) + seg.translate(dp) + + def translate(self, idx_from, dp): + """ + apply translation to all following segs + """ + self.segs[idx_from].p1 += dp + for i in range(idx_from + 1, len(self.segs)): + self.segs[i].translate(dp) + + def draw(self, context): + """ + draw generator using gl + """ + for seg in self.segs: + seg.draw(context, render=False) + + +def update(self, context): + self.update(context) + + +def update_manipulators(self, context): + self.update(context, manipulable_refresh=True) + + +def update_path(self, context): + self.update_path(context) + + +materials_enum = ( + ('0', 'Ceiling', '', 0), + ('1', 'White', '', 1), + ('2', 'Concrete', '', 2), + ('3', 'Wood', '', 3), + ('4', 'Metal', '', 4), + ('5', 'Glass', '', 5) + ) + + +class archipack_slab_material(PropertyGroup): + index = EnumProperty( + items=materials_enum, + default='4', + update=update + ) + + def find_in_selection(self, context): + """ + find witch selected object this instance belongs to + provide support for "copy to selected" + """ + selected = [o for o in context.selected_objects] + for o in selected: + props = archipack_slab.datablock(o) + if props: + for part in props.rail_mat: + if part == self: + return props + return None + + def update(self, context): + props = self.find_in_selection(context) + if props is not None: + props.update(context) + + +class archipack_slab_child(PropertyGroup): + """ + Store child fences to be able to sync + """ + child_name = StringProperty() + idx = IntProperty() + + def get_child(self, context): + d = None + child = context.scene.objects.get(self.child_name) + if child is not None and child.data is not None: + if 'archipack_fence' in child.data: + d = child.data.archipack_fence[0] + return child, d + + +def update_type(self, context): + + d = self.find_in_selection(context) + + if d is not None and d.auto_update: + + d.auto_update = False + # find part index + idx = 0 + for i, part in enumerate(d.parts): + if part == self: + idx = i + break + + part = d.parts[idx] + a0 = 0 + if idx > 0: + g = d.get_generator() + w0 = g.segs[idx - 1] + a0 = w0.straight(1).angle + if "C_" in self.type: + w = w0.straight_slab(part.a0, part.length) + else: + w = w0.curved_slab(part.a0, part.da, part.radius) + else: + g = SlabGenerator(None) + g.add_part(self) + w = g.segs[0] + + # w0 - w - w1 + dp = w.p1 - w.p0 + if "C_" in self.type: + part.radius = 0.5 * dp.length + part.da = pi + a0 = atan2(dp.y, dp.x) - pi / 2 - a0 + else: + part.length = dp.length + a0 = atan2(dp.y, dp.x) - a0 + + if a0 > pi: + a0 -= 2 * pi + if a0 < -pi: + a0 += 2 * pi + part.a0 = a0 + + if idx + 1 < d.n_parts: + # adjust rotation of next part + part1 = d.parts[idx + 1] + if "C_" in part.type: + a0 = part1.a0 - pi / 2 + else: + a0 = part1.a0 + w.straight(1).angle - atan2(dp.y, dp.x) + + if a0 > pi: + a0 -= 2 * pi + if a0 < -pi: + a0 += 2 * pi + part1.a0 = a0 + + d.auto_update = True + + +class ArchipackSegment(): + """ + A single manipulable polyline like segment + polyline like segment line or arc based + @TODO: share this base class with + stair, wall, fence, slab + """ + type = EnumProperty( + items=( + ('S_SEG', 'Straight', '', 0), + ('C_SEG', 'Curved', '', 1), + ), + default='S_SEG', + update=update_type + ) + length = FloatProperty( + name="length", + min=0.01, + default=2.0, + update=update + ) + radius = FloatProperty( + name="radius", + min=0.5, + default=0.7, + update=update + ) + da = FloatProperty( + name="angle", + min=-pi, + max=pi, + default=pi / 2, + subtype='ANGLE', unit='ROTATION', + update=update + ) + a0 = FloatProperty( + name="start angle", + min=-2 * pi, + max=2 * pi, + default=0, + subtype='ANGLE', unit='ROTATION', + update=update + ) + offset = FloatProperty( + name="Offset", + description="Add to current segment offset", + default=0, + unit='LENGTH', subtype='DISTANCE', + update=update + ) + linked_idx = IntProperty(default=-1) + + # @TODO: + # flag to handle wall's x_offset + # when set add wall offset value to segment offset + # pay attention at allowing per wall segment offset + + manipulators = CollectionProperty(type=archipack_manipulator) + + def find_in_selection(self, context): + raise NotImplementedError + + def update(self, context, manipulable_refresh=False): + props = self.find_in_selection(context) + if props is not None: + props.update(context, manipulable_refresh) + + def draw_insert(self, context, layout, index): + """ + May implement draw for insert / remove segment operators + """ + pass + + def draw(self, context, layout, index): + box = layout.box() + row = box.row() + row.prop(self, "type", text=str(index + 1)) + self.draw_insert(context, box, index) + if self.type in ['C_SEG']: + row = box.row() + row.prop(self, "radius") + row = box.row() + row.prop(self, "da") + else: + row = box.row() + row.prop(self, "length") + row = box.row() + row.prop(self, "a0") + row = box.row() + row.prop(self, "offset") + # row.prop(self, "linked_idx") + + +class archipack_slab_part(ArchipackSegment, PropertyGroup): + + def draw_insert(self, context, layout, index): + row = layout.row(align=True) + row.operator("archipack.slab_insert", text="Split").index = index + row.operator("archipack.slab_balcony", text="Balcony").index = index + row.operator("archipack.slab_remove", text="Remove").index = index + + def find_in_selection(self, context): + """ + find witch selected object this instance belongs to + provide support for "copy to selected" + """ + selected = [o for o in context.selected_objects] + for o in selected: + props = archipack_slab.datablock(o) + if props: + for part in props.parts: + if part == self: + return props + return None + + +class archipack_slab(ArchipackObject, Manipulable, PropertyGroup): + # boundary + n_parts = IntProperty( + name="parts", + min=1, + default=1, update=update_manipulators + ) + parts = CollectionProperty(type=archipack_slab_part) + closed = BoolProperty( + default=False, + name="Close", + update=update_manipulators + ) + # UI layout related + parts_expand = BoolProperty( + options={'SKIP_SAVE'}, + default=False + ) + + x_offset = FloatProperty( + name="x offset", + min=-1000, max=1000, + default=0.0, precision=2, step=1, + unit='LENGTH', subtype='DISTANCE', + update=update + ) + z = FloatProperty( + name="z", + default=0.3, precision=2, step=1, + unit='LENGTH', subtype='DISTANCE', + update=update + ) + auto_synch = BoolProperty( + name="AutoSynch", + description="Keep wall in synch when editing", + default=True, + update=update_manipulators + ) + # @TODO: + # Global slab offset + # will only affect slab parts sharing a wall + + childs = CollectionProperty(type=archipack_slab_child) + # Flag to prevent mesh update while making bulk changes over variables + # use : + # .auto_update = False + # bulk changes + # .auto_update = True + auto_update = BoolProperty( + options={'SKIP_SAVE'}, + default=True, + update=update_manipulators + ) + + def get_generator(self): + g = SlabGenerator(self.parts) + for part in self.parts: + # type, radius, da, length + g.add_part(part) + + g.set_offset() + + g.close(self.closed) + g.locate_manipulators() + return g + + def insert_part(self, context, where): + self.manipulable_disable(context) + self.auto_update = False + # the part we do split + part_0 = self.parts[where] + part_0.length /= 2 + part_0.da /= 2 + self.parts.add() + part_1 = self.parts[len(self.parts) - 1] + part_1.type = part_0.type + part_1.length = part_0.length + part_1.offset = part_0.offset + part_1.da = part_0.da + part_1.a0 = 0 + # move after current one + self.parts.move(len(self.parts) - 1, where + 1) + self.n_parts += 1 + for c in self.childs: + if c.idx > where: + c.idx += 1 + self.setup_manipulators() + self.auto_update = True + + def insert_balcony(self, context, where): + self.manipulable_disable(context) + self.auto_update = False + + # the part we do split + part_0 = self.parts[where] + part_0.length /= 3 + part_0.da /= 3 + + # 1st part 90deg + self.parts.add() + part_1 = self.parts[len(self.parts) - 1] + part_1.type = "S_SEG" + part_1.length = 1.5 + part_1.da = part_0.da + part_1.a0 = -pi / 2 + # move after current one + self.parts.move(len(self.parts) - 1, where + 1) + + # 2nd part -90deg + self.parts.add() + part_1 = self.parts[len(self.parts) - 1] + part_1.type = part_0.type + part_1.length = part_0.length + part_1.radius = part_0.radius + 1.5 + part_1.da = part_0.da + part_1.a0 = pi / 2 + # move after current one + self.parts.move(len(self.parts) - 1, where + 2) + + # 3nd part -90deg + self.parts.add() + part_1 = self.parts[len(self.parts) - 1] + part_1.type = "S_SEG" + part_1.length = 1.5 + part_1.da = part_0.da + part_1.a0 = pi / 2 + # move after current one + self.parts.move(len(self.parts) - 1, where + 3) + + # 4nd part -90deg + self.parts.add() + part_1 = self.parts[len(self.parts) - 1] + part_1.type = part_0.type + part_1.length = part_0.length + part_1.radius = part_0.radius + part_1.offset = part_0.offset + part_1.da = part_0.da + part_1.a0 = -pi / 2 + # move after current one + self.parts.move(len(self.parts) - 1, where + 4) + + self.n_parts += 4 + self.setup_manipulators() + + for c in self.childs: + if c.idx > where: + c.idx += 4 + + self.auto_update = True + g = self.get_generator() + + o = context.active_object + bpy.ops.archipack.fence(auto_manipulate=False) + c = context.active_object + c.select = True + c.data.archipack_fence[0].n_parts = 3 + c.select = False + # link to o + c.location = Vector((0, 0, 0)) + c.parent = o + c.location = g.segs[where + 1].p0.to_3d() + self.add_child(c.name, where + 1) + # c.matrix_world.translation = g.segs[where].p1.to_3d() + o.select = True + context.scene.objects.active = o + self.relocate_childs(context, o, g) + + def add_part(self, context, length): + self.manipulable_disable(context) + self.auto_update = False + p = self.parts.add() + p.length = length + self.n_parts += 1 + self.setup_manipulators() + self.auto_update = True + return p + + def add_child(self, name, idx): + c = self.childs.add() + c.child_name = name + c.idx = idx + + def setup_childs(self, o, g): + """ + Store childs + call after a boolean oop + """ + # print("setup_childs") + self.childs.clear() + itM = o.matrix_world.inverted() + + dmax = 0.2 + for c in o.children: + if (c.data and 'archipack_fence' in c.data): + pt = (itM * c.matrix_world.translation).to_2d() + for idx, seg in enumerate(g.segs): + # may be optimized with a bound check + res, d, t = seg.point_sur_segment(pt) + # p1 + # |-- x + # p0 + dist = abs(t) * seg.length + if dist < dmax and abs(d) < dmax: + # print("%s %s %s %s" % (idx, dist, d, c.name)) + self.add_child(c.name, idx) + + # synch wall + # store index of segments with p0 match + if self.auto_synch: + + if o.parent is not None: + + for i, part in enumerate(self.parts): + part.linked_idx = -1 + + # find first child wall + d = None + for c in o.parent.children: + if c.data and "archipack_wall2" in c.data: + d = c.data.archipack_wall2[0] + break + + if d is not None: + og = d.get_generator() + j = 0 + for i, part in enumerate(self.parts): + ji = j + while ji < d.n_parts + 1: + if (g.segs[i].p0 - og.segs[ji].p0).length < 0.005: + j = ji + 1 + part.linked_idx = ji + # print("link: %s to %s" % (i, ji)) + break + ji += 1 + + def relocate_childs(self, context, o, g): + """ + Move and resize childs after edition + """ + # print("relocate_childs") + + # Wall child syncro + # must store - idx of shared segs + # -> store this in parts provide 1:1 map + # share type: full, start only, end only + # -> may compute on the fly with idx stored + # when full segment does match + # -update type, radius, length, a0, and da + # when start only does match + # -update type, radius, a0 + # when end only does match + # -compute length/radius + # @TODO: + # handle p0 and p1 changes right in Generator (archipack_2d) + # and retrieve params from there + if self.auto_synch: + if o.parent is not None: + wall = None + + for child in o.parent.children: + if child.data and "archipack_wall2" in child.data: + wall = child + break + + if wall is not None: + d = wall.data.archipack_wall2[0] + d.auto_update = False + w = d.get_generator() + + last_idx = -1 + + # update og from g + for i, part in enumerate(self.parts): + idx = part.linked_idx + seg = g.segs[i] + + if i + 1 < self.n_parts: + next_idx = self.parts[i + 1].linked_idx + elif d.closed: + next_idx = self.parts[0].linked_idx + else: + next_idx = -1 + + if idx > -1: + + # start and shared: update rotation + a = seg.angle - w.segs[idx].angle + if abs(a) > 0.00001: + w.rotate(idx, a) + + if last_idx > -1: + w.segs[last_idx].p1 = seg.p0 + + if next_idx > -1: + + if idx + 1 == next_idx: + # shared: should move last point + # and apply to next segments + # this is overriden for common segs + # but translate non common ones + dp = seg.p1 - w.segs[idx].p1 + w.translate(idx, dp) + + # shared: transfert type too + if "C_" in part.type: + d.parts[idx].type = 'C_WALL' + w.segs[idx] = CurvedSlab(seg.c, seg.r, seg.a0, seg.da) + else: + d.parts[idx].type = 'S_WALL' + w.segs[idx] = StraightSlab(seg.p.copy(), seg.v.copy()) + last_idx = -1 + + elif next_idx > -1: + # only last is shared + # note: on next run will be part of start + last_idx = next_idx - 1 + + # update d from og + for i, seg in enumerate(w.segs): + if i > 0: + d.parts[i].a0 = seg.delta_angle(w.segs[i - 1]) + else: + d.parts[i].a0 = seg.angle + if "C_" in d.parts[i].type: + d.parts[i].radius = seg.r + d.parts[i].da = seg.da + else: + d.parts[i].length = max(0.01, seg.length) + + wall.select = True + context.scene.objects.active = wall + + d.auto_update = True + wall.select = False + + o.select = True + context.scene.objects.active = o + + wall.matrix_world = o.matrix_world.copy() + + tM = o.matrix_world + for child in self.childs: + c, d = child.get_child(context) + if c is None: + continue + + a = g.segs[child.idx].angle + x, y = g.segs[child.idx].p0 + sa = sin(a) + ca = cos(a) + + if d is not None: + c.select = True + + # auto_update need object to be active to + # setup manipulators on the right object + context.scene.objects.active = c + + d.auto_update = False + for i, part in enumerate(d.parts): + if "C_" in self.parts[i + child.idx].type: + part.type = "C_FENCE" + else: + part.type = "S_FENCE" + part.a0 = self.parts[i + child.idx].a0 + part.da = self.parts[i + child.idx].da + part.length = self.parts[i + child.idx].length + part.radius = self.parts[i + child.idx].radius + d.parts[0].a0 = pi / 2 + d.auto_update = True + c.select = False + + context.scene.objects.active = o + # preTranslate + c.matrix_world = tM * Matrix([ + [sa, ca, 0, x], + [-ca, sa, 0, y], + [0, 0, 1, 0], + [0, 0, 0, 1] + ]) + + def remove_part(self, context, where): + self.manipulable_disable(context) + self.auto_update = False + + # preserve shape + # using generator + if where > 0: + + g = self.get_generator() + w = g.segs[where - 1] + w.p1 = g.segs[where].p1 + + if where + 1 < self.n_parts: + self.parts[where + 1].a0 = g.segs[where + 1].delta_angle(w) + + part = self.parts[where - 1] + + if "C_" in part.type: + part.radius = w.r + else: + part.length = w.length + + if where > 1: + part.a0 = w.delta_angle(g.segs[where - 2]) + else: + part.a0 = w.straight(1, 0).angle + + for c in self.childs: + if c.idx >= where: + c.idx -= 1 + self.parts.remove(where) + self.n_parts -= 1 + # fix snap manipulators index + self.setup_manipulators() + self.auto_update = True + + def update_parts(self, o, update_childs=False): + # print("update_parts") + # remove rows + # NOTE: + # n_parts+1 + # as last one is end point of last segment or closing one + row_change = False + for i in range(len(self.parts), self.n_parts, -1): + row_change = True + self.parts.remove(i - 1) + + # add rows + for i in range(len(self.parts), self.n_parts): + row_change = True + self.parts.add() + + self.setup_manipulators() + + g = self.get_generator() + + if o is not None and (row_change or update_childs): + self.setup_childs(o, g) + + return g + + def setup_manipulators(self): + + if len(self.manipulators) < 1: + s = self.manipulators.add() + s.type_key = "SIZE" + s.prop1_name = "z" + s.normal = Vector((0, 1, 0)) + + for i in range(self.n_parts): + p = self.parts[i] + n_manips = len(p.manipulators) + if n_manips < 1: + s = p.manipulators.add() + s.type_key = "ANGLE" + s.prop1_name = "a0" + if n_manips < 2: + s = p.manipulators.add() + s.type_key = "SIZE" + s.prop1_name = "length" + if n_manips < 3: + s = p.manipulators.add() + s.type_key = 'WALL_SNAP' + s.prop1_name = str(i) + s.prop2_name = 'z' + if n_manips < 4: + s = p.manipulators.add() + s.type_key = 'DUMB_STRING' + s.prop1_name = str(i + 1) + p.manipulators[2].prop1_name = str(i) + p.manipulators[3].prop1_name = str(i + 1) + + self.parts[-1].manipulators[0].type_key = 'DUMB_ANGLE' + + def is_cw(self, pts): + p0 = pts[0] + d = 0 + for p in pts[1:]: + d += (p.x * p0.y - p.y * p0.x) + p0 = p + return d > 0 + + def interpolate_bezier(self, pts, wM, p0, p1, resolution): + # straight segment, worth testing here + # since this can lower points count by a resolution factor + # use normalized to handle non linear t + if resolution == 0: + pts.append(wM * p0.co.to_3d()) + else: + v = (p1.co - p0.co).normalized() + d1 = (p0.handle_right - p0.co).normalized() + d2 = (p1.co - p1.handle_left).normalized() + if d1 == v and d2 == v: + pts.append(wM * p0.co.to_3d()) + else: + seg = interpolate_bezier(wM * p0.co, + wM * p0.handle_right, + wM * p1.handle_left, + wM * p1.co, + resolution + 1) + for i in range(resolution): + pts.append(seg[i].to_3d()) + + def from_spline(self, wM, resolution, spline): + pts = [] + if spline.type == 'POLY': + pts = [wM * p.co.to_3d() for p in spline.points] + if spline.use_cyclic_u: + pts.append(pts[0]) + elif spline.type == 'BEZIER': + points = spline.bezier_points + for i in range(1, len(points)): + p0 = points[i - 1] + p1 = points[i] + self.interpolate_bezier(pts, wM, p0, p1, resolution) + if spline.use_cyclic_u: + p0 = points[-1] + p1 = points[0] + self.interpolate_bezier(pts, wM, p0, p1, resolution) + pts.append(pts[0]) + else: + pts.append(wM * points[-1].co) + + self.from_points(pts, spline.use_cyclic_u) + + def from_points(self, pts, closed): + + if self.is_cw(pts): + pts = list(reversed(pts)) + + self.auto_update = False + + self.n_parts = len(pts) - 1 + + self.update_parts(None) + + p0 = pts.pop(0) + a0 = 0 + for i, p1 in enumerate(pts): + dp = p1 - p0 + da = atan2(dp.y, dp.x) - a0 + if da > pi: + da -= 2 * pi + if da < -pi: + da += 2 * pi + if i >= len(self.parts): + break + p = self.parts[i] + p.length = dp.to_2d().length + p.dz = dp.z + p.a0 = da + a0 += da + p0 = p1 + + self.closed = closed + self.auto_update = True + + def make_surface(self, o, verts): + bm = bmesh.new() + for v in verts: + bm.verts.new(v) + bm.verts.ensure_lookup_table() + for i in range(1, len(verts)): + bm.edges.new((bm.verts[i - 1], bm.verts[i])) + bm.edges.new((bm.verts[-1], bm.verts[0])) + bm.edges.ensure_lookup_table() + bmesh.ops.contextual_create(bm, geom=bm.edges) + bm.to_mesh(o.data) + bm.free() + + def unwrap_uv(self, o): + bm = bmesh.new() + bm.from_mesh(o.data) + for face in bm.faces: + face.select = face.material_index > 0 + bm.to_mesh(o.data) + bpy.ops.uv.cube_project(scale_to_bounds=False, correct_aspect=True) + + for face in bm.faces: + face.select = face.material_index < 1 + bm.to_mesh(o.data) + bpy.ops.uv.smart_project(use_aspect=True, stretch_to_bounds=False) + bm.free() + + def update(self, context, manipulable_refresh=False, update_childs=False): + + o = self.find_in_selection(context, self.auto_update) + + if o is None: + return + + # clean up manipulators before any data model change + if manipulable_refresh: + self.manipulable_disable(context) + + g = self.update_parts(o, update_childs) + + verts = [] + + g.get_verts(verts) + if len(verts) > 2: + self.make_surface(o, verts) + + modif = o.modifiers.get('Slab') + if modif is None: + modif = o.modifiers.new('Slab', 'SOLIDIFY') + modif.use_quality_normals = True + modif.use_even_offset = True + modif.material_offset_rim = 2 + modif.material_offset = 1 + + modif.thickness = self.z + modif.offset = 1.0 + o.data.use_auto_smooth = True + bpy.ops.object.shade_smooth() + + # Height + self.manipulators[0].set_pts([ + (0, 0, 0), + (0, 0, -self.z), + (-1, 0, 0) + ], normal=g.segs[0].straight(-1, 0).v.to_3d()) + + self.relocate_childs(context, o, g) + + # enable manipulators rebuild + if manipulable_refresh: + self.manipulable_refresh = True + + # restore context + self.restore_context(context) + + def manipulable_setup(self, context): + """ + NOTE: + this one assume context.active_object is the instance this + data belongs to, failing to do so will result in wrong + manipulators set on active object + """ + self.manipulable_disable(context) + + o = context.active_object + + self.setup_manipulators() + + for i, part in enumerate(self.parts): + if i >= self.n_parts: + break + + if i > 0: + # start angle + self.manip_stack.append(part.manipulators[0].setup(context, o, part)) + + # length / radius + angle + self.manip_stack.append(part.manipulators[1].setup(context, o, part)) + + # snap point + self.manip_stack.append(part.manipulators[2].setup(context, o, self)) + # index + self.manip_stack.append(part.manipulators[3].setup(context, o, self)) + + for m in self.manipulators: + self.manip_stack.append(m.setup(context, o, self)) + + def manipulable_invoke(self, context): + """ + call this in operator invoke() + """ + # print("manipulable_invoke") + if self.manipulate_mode: + self.manipulable_disable(context) + return False + + o = context.active_object + g = self.get_generator() + # setup childs manipulators + self.setup_childs(o, g) + self.manipulable_setup(context) + self.manipulate_mode = True + + self._manipulable_invoke(context) + + return True + + +class ARCHIPACK_PT_slab(Panel): + """Archipack Slab""" + bl_idname = "ARCHIPACK_PT_slab" + bl_label = "Slab" + bl_space_type = 'VIEW_3D' + bl_region_type = 'UI' + # bl_context = 'object' + bl_category = 'ArchiPack' + + @classmethod + def poll(cls, context): + return archipack_slab.filter(context.active_object) + + def draw(self, context): + prop = archipack_slab.datablock(context.active_object) + if prop is None: + return + layout = self.layout + row = layout.row(align=True) + # self.set_context_3dview(context, row) + row.operator('archipack.slab_manipulate', icon='HAND') + box = layout.box() + box.prop(prop, 'z') + box = layout.box() + box.prop(prop, 'auto_synch') + box = layout.box() + row = box.row() + if prop.parts_expand: + row.prop(prop, 'parts_expand', icon="TRIA_DOWN", icon_only=True, text="Parts", emboss=False) + box.prop(prop, 'n_parts') + # box.prop(prop, 'closed') + for i, part in enumerate(prop.parts): + part.draw(context, layout, i) + else: + row.prop(prop, 'parts_expand', icon="TRIA_RIGHT", icon_only=True, text="Parts", emboss=False) + + +# ------------------------------------------------------------------ +# Define operator class to create object +# ------------------------------------------------------------------ + + +class ARCHIPACK_OT_slab_insert(Operator): + bl_idname = "archipack.slab_insert" + bl_label = "Insert" + bl_description = "Insert part" + bl_category = 'Archipack' + bl_options = {'REGISTER', 'UNDO'} + index = IntProperty(default=0) + + def execute(self, context): + if context.mode == "OBJECT": + d = archipack_slab.datablock(context.active_object) + if d is None: + return {'CANCELLED'} + d.insert_part(context, self.index) + return {'FINISHED'} + else: + self.report({'WARNING'}, "Archipack: Option only valid in Object mode") + return {'CANCELLED'} + + +class ARCHIPACK_OT_slab_balcony(Operator): + bl_idname = "archipack.slab_balcony" + bl_label = "Insert" + bl_description = "Insert part" + bl_category = 'Archipack' + bl_options = {'REGISTER', 'UNDO'} + index = IntProperty(default=0) + + def execute(self, context): + if context.mode == "OBJECT": + d = archipack_slab.datablock(context.active_object) + if d is None: + return {'CANCELLED'} + d.insert_balcony(context, self.index) + return {'FINISHED'} + else: + self.report({'WARNING'}, "Archipack: Option only valid in Object mode") + return {'CANCELLED'} + + +class ARCHIPACK_OT_slab_remove(Operator): + bl_idname = "archipack.slab_remove" + bl_label = "Remove" + bl_description = "Remove part" + bl_category = 'Archipack' + bl_options = {'REGISTER', 'UNDO'} + index = IntProperty(default=0) + + def execute(self, context): + if context.mode == "OBJECT": + d = archipack_slab.datablock(context.active_object) + if d is None: + return {'CANCELLED'} + d.remove_part(context, self.index) + return {'FINISHED'} + else: + self.report({'WARNING'}, "Archipack: Option only valid in Object mode") + return {'CANCELLED'} + + +# ------------------------------------------------------------------ +# Define operator class to create object +# ------------------------------------------------------------------ + + +class ARCHIPACK_OT_slab(ArchipackCreateTool, Operator): + bl_idname = "archipack.slab" + bl_label = "Slab" + bl_description = "Slab" + bl_category = 'Archipack' + bl_options = {'REGISTER', 'UNDO'} + + def create(self, context): + m = bpy.data.meshes.new("Slab") + o = bpy.data.objects.new("Slab", m) + d = m.archipack_slab.add() + # make manipulators selectable + d.manipulable_selectable = True + context.scene.objects.link(o) + o.select = True + context.scene.objects.active = o + self.load_preset(d) + self.add_material(o) + return o + + # ----------------------------------------------------- + # Execute + # ----------------------------------------------------- + def execute(self, context): + if context.mode == "OBJECT": + bpy.ops.object.select_all(action="DESELECT") + o = self.create(context) + o.location = bpy.context.scene.cursor_location + o.select = True + context.scene.objects.active = o + self.manipulate() + return {'FINISHED'} + else: + self.report({'WARNING'}, "Archipack: Option only valid in Object mode") + return {'CANCELLED'} + + +class ARCHIPACK_OT_slab_from_curve(Operator): + bl_idname = "archipack.slab_from_curve" + bl_label = "Slab curve" + bl_description = "Create a slab from a curve" + bl_category = 'Archipack' + bl_options = {'REGISTER', 'UNDO'} + + auto_manipulate = BoolProperty(default=True) + + @classmethod + def poll(self, context): + return context.active_object is not None and context.active_object.type == 'CURVE' + # ----------------------------------------------------- + # Draw (create UI interface) + # ----------------------------------------------------- + # noinspection PyUnusedLocal + + def draw(self, context): + layout = self.layout + row = layout.row() + row.label("Use Properties panel (N) to define parms", icon='INFO') + + def create(self, context): + curve = context.active_object + bpy.ops.archipack.slab(auto_manipulate=self.auto_manipulate) + o = context.scene.objects.active + d = archipack_slab.datablock(o) + spline = curve.data.splines[0] + d.from_spline(curve.matrix_world, 12, spline) + if spline.type == 'POLY': + pt = spline.points[0].co + elif spline.type == 'BEZIER': + pt = spline.bezier_points[0].co + else: + pt = Vector((0, 0, 0)) + # pretranslate + o.matrix_world = curve.matrix_world * Matrix([ + [1, 0, 0, pt.x], + [0, 1, 0, pt.y], + [0, 0, 1, pt.z], + [0, 0, 0, 1] + ]) + return o + + # ----------------------------------------------------- + # Execute + # ----------------------------------------------------- + def execute(self, context): + if context.mode == "OBJECT": + bpy.ops.object.select_all(action="DESELECT") + self.create(context) + return {'FINISHED'} + else: + self.report({'WARNING'}, "Archipack: Option only valid in Object mode") + return {'CANCELLED'} + + +class ARCHIPACK_OT_slab_from_wall(Operator): + bl_idname = "archipack.slab_from_wall" + bl_label = "->Slab" + bl_description = "Create a slab from a wall" + bl_category = 'Archipack' + bl_options = {'REGISTER', 'UNDO'} + + auto_manipulate = BoolProperty(default=True) + ceiling = BoolProperty(default=False) + + @classmethod + def poll(self, context): + o = context.active_object + return o is not None and o.data is not None and 'archipack_wall2' in o.data + + def create(self, context): + wall = context.active_object + wd = wall.data.archipack_wall2[0] + bpy.ops.archipack.slab(auto_manipulate=False) + o = context.scene.objects.active + d = archipack_slab.datablock(o) + d.auto_update = False + d.closed = True + d.parts.clear() + d.n_parts = wd.n_parts + 1 + for part in wd.parts: + p = d.parts.add() + if "S_" in part.type: + p.type = "S_SEG" + else: + p.type = "C_SEG" + p.length = part.length + p.radius = part.radius + p.da = part.da + p.a0 = part.a0 + d.auto_update = True + # pretranslate + if self.ceiling: + o.matrix_world = Matrix([ + [1, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, 1, wd.z + d.z], + [0, 0, 0, 1], + ]) * wall.matrix_world + else: + o.matrix_world = wall.matrix_world.copy() + bpy.ops.object.select_all(action='DESELECT') + # parenting childs to wall reference point + if wall.parent is None: + x, y, z = wall.bound_box[0] + context.scene.cursor_location = wall.matrix_world * Vector((x, y, z)) + # fix issue #9 + context.scene.objects.active = wall + bpy.ops.archipack.reference_point() + else: + wall.parent.select = True + context.scene.objects.active = wall.parent + wall.select = True + o.select = True + bpy.ops.archipack.parent_to_reference() + wall.parent.select = False + + return o + + # ----------------------------------------------------- + # Execute + # ----------------------------------------------------- + def execute(self, context): + if context.mode == "OBJECT": + bpy.ops.object.select_all(action="DESELECT") + o = self.create(context) + o.select = True + context.scene.objects.active = o + if self.auto_manipulate: + bpy.ops.archipack.slab_manipulate('INVOKE_DEFAULT') + return {'FINISHED'} + else: + self.report({'WARNING'}, "Archipack: Option only valid in Object mode") + return {'CANCELLED'} + + +# ------------------------------------------------------------------ +# Define operator class to manipulate object +# ------------------------------------------------------------------ + + +class ARCHIPACK_OT_slab_manipulate(Operator): + bl_idname = "archipack.slab_manipulate" + bl_label = "Manipulate" + bl_description = "Manipulate" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(self, context): + return archipack_slab.filter(context.active_object) + + def invoke(self, context, event): + d = archipack_slab.datablock(context.active_object) + d.manipulable_invoke(context) + return {'FINISHED'} + + +def register(): + bpy.utils.register_class(archipack_slab_material) + bpy.utils.register_class(archipack_slab_child) + bpy.utils.register_class(archipack_slab_part) + bpy.utils.register_class(archipack_slab) + Mesh.archipack_slab = CollectionProperty(type=archipack_slab) + bpy.utils.register_class(ARCHIPACK_PT_slab) + bpy.utils.register_class(ARCHIPACK_OT_slab) + bpy.utils.register_class(ARCHIPACK_OT_slab_insert) + bpy.utils.register_class(ARCHIPACK_OT_slab_balcony) + bpy.utils.register_class(ARCHIPACK_OT_slab_remove) + # bpy.utils.register_class(ARCHIPACK_OT_slab_manipulate_ctx) + bpy.utils.register_class(ARCHIPACK_OT_slab_manipulate) + bpy.utils.register_class(ARCHIPACK_OT_slab_from_curve) + bpy.utils.register_class(ARCHIPACK_OT_slab_from_wall) + + +def unregister(): + bpy.utils.unregister_class(archipack_slab_material) + bpy.utils.unregister_class(archipack_slab_child) + bpy.utils.unregister_class(archipack_slab_part) + bpy.utils.unregister_class(archipack_slab) + del Mesh.archipack_slab + bpy.utils.unregister_class(ARCHIPACK_PT_slab) + bpy.utils.unregister_class(ARCHIPACK_OT_slab) + bpy.utils.unregister_class(ARCHIPACK_OT_slab_insert) + bpy.utils.unregister_class(ARCHIPACK_OT_slab_balcony) + bpy.utils.unregister_class(ARCHIPACK_OT_slab_remove) + # bpy.utils.unregister_class(ARCHIPACK_OT_slab_manipulate_ctx) + bpy.utils.unregister_class(ARCHIPACK_OT_slab_manipulate) + bpy.utils.unregister_class(ARCHIPACK_OT_slab_from_curve) + bpy.utils.unregister_class(ARCHIPACK_OT_slab_from_wall) diff --git a/archipack/archipack_snap.py b/archipack/archipack_snap.py new file mode 100644 index 00000000..936a07d8 --- /dev/null +++ b/archipack/archipack_snap.py @@ -0,0 +1,309 @@ +# -*- coding:utf-8 -*- + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110- 1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +# + +# ---------------------------------------------------------- +# Author: Stephen Leger (s-leger) +# Inspired by Okavango's np_point_move +# ---------------------------------------------------------- +""" + Usage: + from .archipack_snap import snap_point + + snap_point(takeloc, draw_callback, action_callback, constraint_axis) + + arguments: + + takeloc Vector3d location of point to snap + + constraint_axis boolean tuple for each axis + eg: (True, True, False) to constrtaint to xy plane + + draw_callback(context, sp) + sp.takeloc + sp.placeloc + sp.delta + + action_callback(context, event, state, sp) + state in {'SUCCESS', 'CANCEL'} + sp.takeloc + sp.placeloc + sp.delta + + with 3d Vectors + - delta = placeloc - takeloc + - takeloc + - placeloc + + + NOTE: + may change grid size to 0.1 round feature (SHIFT) + see https://blenderartists.org/forum/showthread.php?205158-Blender-2-5-Snap-mode-increment + then use a SHIFT use grid snap + +""" + +import bpy +from bpy.types import Operator +from mathutils import Vector, Matrix + + +def dumb_callback(context, event, state, sp): + return + + +def dumb_draw(sp, context): + return + + +class SnapStore: + """ + Global store + """ + callback = None + draw = None + helper = None + takeloc = Vector((0, 0, 0)) + placeloc = Vector((0, 0, 0)) + constraint_axis = (True, True, False) + helper_matrix = Matrix() + transform_orientation = 'GLOBAL' + release_confirm = True + instances_running = 0 + + # context related + act = None + sel = [] + use_snap = False + snap_element = None + snap_target = None + pivot_point = None + trans_orientation = None + + +def snap_point(takeloc=None, + draw=dumb_draw, + callback=dumb_callback, + takemat=None, + constraint_axis=(True, True, False), + transform_orientation='GLOBAL', + mode='OBJECT', + release_confirm=True): + """ + Invoke op from outside world + in a convenient importable function + + transform_orientation in [‘GLOBAL’, ‘LOCAL’, ‘NORMAL’, ‘GIMBAL’, ‘VIEW’] + + draw(sp, context) a draw callback + callback(context, event, state, sp) action callback + + Use either : + takeloc Vector, unconstraint or system axis constraints + takemat Matrix, constaint to this matrix as 'LOCAL' coordsys + The snap source helper use it as world matrix + so it is possible to constraint to user defined coordsys. + """ + SnapStore.draw = draw + SnapStore.callback = callback + SnapStore.constraint_axis = constraint_axis + SnapStore.release_confirm = release_confirm + if takemat is not None: + SnapStore.helper_matrix = takemat + takeloc = takemat.translation + transform_orientation = 'LOCAL' + elif takeloc is not None: + SnapStore.helper_matrix = Matrix().Translation(takeloc) + else: + raise ValueError("ArchipackSnap: Either takeloc or takemat must be defined") + SnapStore.takeloc = takeloc + SnapStore.placeloc = takeloc + SnapStore.transform_orientation = transform_orientation + + # @NOTE: unused mode var to switch between OBJECT and EDIT mode + # for ArchipackSnapBase to be able to handle both modes + # must implements corresponding helper create and delete actions + SnapStore.mode = mode + res = bpy.ops.archipack.snap('INVOKE_DEFAULT') + # return helper so we are able to move it "live" + return SnapStore.helper + +class ArchipackSnapBase(): + """ + Helper class for snap Operators + store and restore context + create and destroy helper + install and remove a draw_callback working while snapping + + store and provide access to 3d Vectors + in draw_callback and action_callback + - delta = placeloc - takeloc + - takeloc + - placeloc + """ + def __init__(self): + self._draw_handler = None + + def init(self, context, event): + # Store context data + if SnapStore.instances_running < 1: + SnapStore.sel = [o for o in context.selected_objects] + SnapStore.act = context.active_object + bpy.ops.object.select_all(action="DESELECT") + SnapStore.use_snap = context.tool_settings.use_snap + SnapStore.snap_element = context.tool_settings.snap_element + SnapStore.snap_target = context.tool_settings.snap_target + SnapStore.pivot_point = context.space_data.pivot_point + SnapStore.trans_orientation = context.space_data.transform_orientation + self.create_helper(context) + SnapStore.instances_running += 1 + # print("ArchipackSnapBase init: %s" % (SnapStore.instances_running)) + self.set_transform_orientation(context) + args = (self, context) + self._draw_handler = bpy.types.SpaceView3D.draw_handler_add(SnapStore.draw, args, 'WINDOW', 'POST_PIXEL') + + def exit(self, context): + bpy.types.SpaceView3D.draw_handler_remove(self._draw_handler, 'WINDOW') + # trick to allow launch 2nd instance + # via callback, preserve context as it + SnapStore.instances_running -= 1 + # print("ArchipackSnapBase exit: %s" % (SnapStore.instances_running)) + if SnapStore.instances_running > 0: + return + + self.destroy_helper(context) + # Restore original context + context.tool_settings.use_snap = SnapStore.use_snap + context.tool_settings.snap_element = SnapStore.snap_element + context.tool_settings.snap_target = SnapStore.snap_target + context.space_data.pivot_point = SnapStore.pivot_point + context.space_data.transform_orientation = SnapStore.trans_orientation + for o in SnapStore.sel: + o.select = True + if SnapStore.act is not None: + context.scene.objects.active = SnapStore.act + + def set_transform_orientation(self, context): + """ + Allow local constraint orientation to be set + """ + context.space_data.transform_orientation = SnapStore.transform_orientation + + def create_helper(self, context): + """ + Create a helper with fake user + or find older one in bpy data and relink to scene + currently only support OBJECT mode + + Do target helper be linked to scene in order to work ? + + """ + + helper_idx = bpy.data.objects.find('Archipack_snap_helper') + if helper_idx > -1: + helper = bpy.data.objects[helper_idx] + if context.scene.objects.find('Archipack_snap_helper') < 0: + context.scene.objects.link(helper) + else: + bpy.ops.object.add(type='MESH') + helper = context.active_object + helper.name = 'Archipack_snap_helper' + helper.use_fake_user = True + helper.data.use_fake_user = True + # hide snap helper + # helper.hide = True + helper.matrix_world = SnapStore.helper_matrix + helper.select = True + context.scene.objects.active = helper + SnapStore.helper = helper + + def destroy_helper(self, context): + """ + Unlink helper + currently only support OBJECT mode + """ + if SnapStore.helper is not None: + context.scene.objects.unlink(SnapStore.helper) + SnapStore.helper = None + + @property + def delta(self): + return self.placeloc - self.takeloc + + @property + def takeloc(self): + return SnapStore.takeloc + + @property + def placeloc(self): + # take from helper when there so the delta + # is working even while modal is running + if SnapStore.helper is not None: + return SnapStore.helper.location + else: + return SnapStore.placeloc + + +class ARCHIPACK_OT_snap(ArchipackSnapBase, Operator): + bl_idname = 'archipack.snap' + bl_label = 'Archipack snap' + bl_options = {'UNDO'} + + def modal(self, context, event): + # print("Snap.modal event %s %s" % (event.type, event.value)) + context.area.tag_redraw() + # NOTE: this part only run after transform LEFTMOUSE RELEASE + # or with ESC and RIGHTMOUSE + if event.type not in {'ESC', 'RIGHTMOUSE', 'LEFTMOUSE', 'MOUSEMOVE'}: + # print("Snap.modal skip unknown event %s %s" % (event.type, event.value)) + # self.report({'WARNING'}, "ARCHIPACK_OT_snap unknown event") + return{'PASS_THROUGH'} + if event.type in {'ESC', 'RIGHTMOUSE'}: + SnapStore.callback(context, event, 'CANCEL', self) + else: + SnapStore.placeloc = SnapStore.helper.location + SnapStore.callback(context, event, 'SUCCESS', self) + self.exit(context) + # self.report({'INFO'}, "ARCHIPACK_OT_snap exit") + return{'FINISHED'} + + def invoke(self, context, event): + if context.area.type == 'VIEW_3D': + # print("Snap.invoke event %s %s" % (event.type, event.value)) + self.init(context, event) + context.window_manager.modal_handler_add(self) + # print("SnapStore.transform_orientation%s" % (SnapStore.transform_orientation)) + bpy.ops.transform.translate('INVOKE_DEFAULT', + constraint_axis=SnapStore.constraint_axis, + constraint_orientation=SnapStore.transform_orientation, + release_confirm=SnapStore.release_confirm) + return {'RUNNING_MODAL'} + else: + self.report({'WARNING'}, "View3D not found, cannot run operator") + return {'FINISHED'} + + +def register(): + bpy.utils.register_class(ARCHIPACK_OT_snap) + + +def unregister(): + bpy.utils.unregister_class(ARCHIPACK_OT_snap) diff --git a/archipack/archipack_stair.py b/archipack/archipack_stair.py new file mode 100644 index 00000000..c7e7f02c --- /dev/null +++ b/archipack/archipack_stair.py @@ -0,0 +1,2849 @@ +# -*- coding:utf-8 -*- + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110- 1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +# + +# ---------------------------------------------------------- +# Author: Stephen Leger (s-leger) +# +# ---------------------------------------------------------- +# noinspection PyUnresolvedReferences +import bpy +# noinspection PyUnresolvedReferences +from bpy.types import Operator, PropertyGroup, Mesh, Panel +from bpy.props import ( + FloatProperty, BoolProperty, IntProperty, CollectionProperty, + StringProperty, EnumProperty, FloatVectorProperty + ) +from .bmesh_utils import BmeshEdit as bmed +from .panel import Panel as Lofter +from mathutils import Vector, Matrix +from math import sin, cos, pi, floor, acos +from .archipack_manipulator import Manipulable, archipack_manipulator +from .archipack_2d import Line, Arc +from .archipack_preset import ArchipackPreset, PresetMenuOperator +from .archipack_object import ArchipackCreateTool, ArchipackObject + + +class Stair(): + def __init__(self, left_offset, right_offset, steps_type, nose_type, z_mode, nose_z, bottom_z): + self.steps_type = steps_type + self.nose_type = nose_type + self.l_shape = None + self.r_shape = None + self.next_type = 'NONE' + self.last_type = 'NONE' + self.z_mode = z_mode + # depth of open step + self.nose_z = nose_z + # size under the step on bottom + self.bottom_z = bottom_z + self.left_offset = left_offset + self.right_offset = right_offset + self.last_height = 0 + + def set_matids(self, matids): + self.idmat_top, self.idmat_step_front, self.idmat_raise, \ + self.idmat_side, self.idmat_bottom, self.idmat_step_side = matids + + def set_height(self, step_height, z0): + self.step_height = step_height + self.z0 = z0 + + @property + def height(self): + return self.n_step * self.step_height + + @property + def top_offset(self): + return self.t_step / self.step_depth + + @property + def top(self): + return self.z0 + self.height + + @property + def left_length(self): + return self.get_length("LEFT") + + @property + def right_length(self): + return self.get_length("RIGHT") + + def step_size(self, step_depth): + t_step, n_step = self.steps(step_depth) + self.n_step = n_step + self.t_step = t_step + self.step_depth = step_depth + return n_step + + def p3d_left(self, verts, p2d, i, t, landing=False): + x, y = p2d + nose_z = min(self.step_height, self.nose_z) + zl = self.z0 + t * self.height + zs = self.z0 + i * self.step_height + if self.z_mode == 'LINEAR': + z0 = max(0, zl) + z1 = z0 - self.bottom_z + verts.extend([(x, y, z0), (x, y, z1)]) + else: + if "FULL" in self.steps_type: + z0 = 0 + else: + z0 = max(0, zl - nose_z - self.bottom_z) + z3 = zs + max(0, self.step_height - nose_z) + z4 = zs + self.step_height + if landing: + if "FULL" in self.steps_type: + z2 = 0 + z1 = 0 + else: + z2 = max(0, min(z3, z3 - self.bottom_z)) + z1 = z2 + else: + z1 = min(z3, max(z0, zl - nose_z)) + z2 = min(z3, max(z1, zl)) + verts.extend([(x, y, z0), + (x, y, z1), + (x, y, z2), + (x, y, z3), + (x, y, z4)]) + + def p3d_right(self, verts, p2d, i, t, landing=False): + x, y = p2d + nose_z = min(self.step_height, self.nose_z) + zl = self.z0 + t * self.height + zs = self.z0 + i * self.step_height + if self.z_mode == 'LINEAR': + z0 = max(0, zl) + z1 = z0 - self.bottom_z + verts.extend([(x, y, z1), (x, y, z0)]) + else: + if "FULL" in self.steps_type: + z0 = 0 + else: + z0 = max(0, zl - nose_z - self.bottom_z) + z3 = zs + max(0, self.step_height - nose_z) + z4 = zs + self.step_height + if landing: + if "FULL" in self.steps_type: + z2 = 0 + z1 = 0 + else: + z2 = max(0, min(z3, z3 - self.bottom_z)) + z1 = z2 + else: + z1 = min(z3, max(z0, zl - nose_z)) + z2 = min(z3, max(z1, zl)) + verts.extend([(x, y, z4), + (x, y, z3), + (x, y, z2), + (x, y, z1), + (x, y, z0)]) + + def p3d_cstep_left(self, verts, p2d, i, t): + x, y = p2d + nose_z = min(self.step_height, self.nose_z) + zs = self.z0 + i * self.step_height + z3 = zs + max(0, self.step_height - nose_z) + z1 = min(z3, zs - nose_z) + verts.append((x, y, z1)) + verts.append((x, y, z3)) + + def p3d_cstep_right(self, verts, p2d, i, t): + x, y = p2d + nose_z = min(self.step_height, self.nose_z) + zs = self.z0 + i * self.step_height + z3 = zs + max(0, self.step_height - nose_z) + z1 = min(z3, zs - nose_z) + verts.append((x, y, z3)) + verts.append((x, y, z1)) + + def straight_stair(self, length): + self.next_type = 'STAIR' + s = self.straight(length) + return StraightStair(s.p, s.v, self.left_offset, self.right_offset, self.steps_type, + self.nose_type, self.z_mode, self.nose_z, self.bottom_z) + + def straight_landing(self, length, last_type='STAIR'): + self.next_type = 'LANDING' + s = self.straight(length) + return StraightLanding(s.p, s.v, self.left_offset, self.right_offset, self.steps_type, + self.nose_type, self.z_mode, self.nose_z, self.bottom_z, last_type=last_type) + + def curved_stair(self, da, radius, left_shape, right_shape, double_limit=pi): + self.next_type = 'STAIR' + n = self.normal(1) + n.v = radius * n.v.normalized() + if da < 0: + n.v = -n.v + a0 = n.angle + c = n.p - n.v + return CurvedStair(c, radius, a0, da, self.left_offset, self.right_offset, + self.steps_type, self.nose_type, self.z_mode, self.nose_z, self.bottom_z, + left_shape, right_shape, double_limit=double_limit) + + def curved_landing(self, da, radius, left_shape, right_shape, double_limit=pi, last_type='STAIR'): + self.next_type = 'LANDING' + n = self.normal(1) + n.v = radius * n.v.normalized() + if da < 0: + n.v = -n.v + a0 = n.angle + c = n.p - n.v + return CurvedLanding(c, radius, a0, da, self.left_offset, self.right_offset, + self.steps_type, self.nose_type, self.z_mode, self.nose_z, self.bottom_z, + left_shape, right_shape, double_limit=double_limit, last_type=last_type) + + def get_z(self, t, mode): + if mode == 'LINEAR': + return self.z0 + t * self.height + else: + step = 1 + floor(t / self.t_step) + return self.z0 + step * self.step_height + + def make_profile(self, t, side, profile, verts, faces, matids, next=None, tnext=0): + z0 = self.get_z(t, 'LINEAR') + dz1 = 0 + t, part, dz0, shape = self.get_part(t, side) + if next is not None: + tnext, next, dz1, shape1 = next.get_part(tnext, side) + xy, s = part.proj_xy(t, next) + v_xy = s * xy.to_3d() + z, s = part.proj_z(t, dz0, next, dz1) + v_z = s * Vector((-xy.y * z.x, xy.x * z.x, z.y)) + x, y = part.lerp(t) + verts += [Vector((x, y, z0)) + v.x * v_xy + v.y * v_z for v in profile] + + def project_uv(self, rM, uvs, verts, indexes, up_axis='Z'): + if up_axis == 'Z': + uvs.append([(rM * Vector(verts[i])).to_2d() for i in indexes]) + elif up_axis == 'Y': + uvs.append([(x, z) for x, y, z in [(rM * Vector(verts[i])) for i in indexes]]) + else: + uvs.append([(y, z) for x, y, z in [(rM * Vector(verts[i])) for i in indexes]]) + + def get_proj_matrix(self, part, t, nose_y): + # a matrix to project verts + # into uv space for horizontal parts of this step + # so uv = (rM * vertex).to_2d() + tl = t - nose_y / self.get_length("LEFT") + tr = t - nose_y / self.get_length("RIGHT") + t2, part, dz, shape = self.get_part(tl, "LEFT") + p0 = part.lerp(t2) + t2, part, dz, shape = self.get_part(tr, "RIGHT") + p1 = part.lerp(t2) + v = (p1 - p0).normalized() + return Matrix([ + [-v.y, v.x, 0, p0.x], + [v.x, v.y, 0, p0.y], + [0, 0, 1, 0], + [0, 0, 0, 1] + ]).inverted() + + def _make_nose(self, i, s, verts, faces, matids, uvs, nose_y): + + t = self.t_step * i + + # a matrix to project verts + # into uv space for horizontal parts of this step + # so uv = (rM * vertex).to_2d() + rM = self.get_proj_matrix(self, t, nose_y) + + if self.z_mode == 'LINEAR': + return rM + + f = len(verts) + + tl = t - nose_y / self.get_length("LEFT") + tr = t - nose_y / self.get_length("RIGHT") + + t2, part, dz, shape = self.get_part(tl, "LEFT") + p0 = part.lerp(t2) + self.p3d_left(verts, p0, s, t2) + + t2, part, dz, shape = self.get_part(tr, "RIGHT") + p1 = part.lerp(t2) + self.p3d_right(verts, p1, s, t2) + + start = 3 + end = 6 + offset = 10 + + # left, top, right + matids.extend([self.idmat_step_side, + self.idmat_top, + self.idmat_step_side]) + + faces += [(f + j, f + j + 1, f + j + offset + 1, f + j + offset) for j in range(start, end)] + + u = nose_y + v = (p1 - p0).length + w = verts[f + 2][2] - verts[f + 3][2] + s = int((end - start) / 2) + + uvs += [[(u, verts[f + j][2]), (u, verts[f + j + 1][2]), + (0, verts[f + j + 1][2]), (0, verts[f + j][2])] for j in range(start, start + s)] + + uvs.append([(0, 0), (0, v), (u, v), (u, 0)]) + + uvs += [[(u, verts[f + j][2]), (u, verts[f + j + 1][2]), + (0, verts[f + j + 1][2]), (0, verts[f + j][2])] for j in range(start + s + 1, end)] + + if 'STRAIGHT' in self.nose_type or 'OPEN' in self.steps_type: + # face bottom + matids.append(self.idmat_bottom) + faces.append((f + end, f + start, f + offset + start, f + offset + end)) + uvs.append([(u, v), (u, 0), (0, 0), (0, v)]) + + if self.steps_type != 'OPEN': + if 'STRAIGHT' in self.nose_type: + # front face bottom straight + matids.append(self.idmat_raise) + faces.append((f + 12, f + 17, f + 16, f + 13)) + uvs.append([(0, w), (v, w), (v, 0), (0, 0)]) + + elif 'OBLIQUE' in self.nose_type: + # front face bottom oblique + matids.append(self.idmat_raise) + faces.append((f + 12, f + 17, f + 6, f + 3)) + + uvs.append([(0, w), (v, w), (v, 0), (0, 0)]) + + matids.append(self.idmat_side) + faces.append((f + 3, f + 13, f + 12)) + uvs.append([(0, 0), (u, 0), (u, w)]) + + matids.append(self.idmat_side) + faces.append((f + 6, f + 17, f + 16)) + uvs.append([(0, 0), (u, w), (u, 0)]) + + # front face top + w = verts[f + 3][2] - verts[f + 4][2] + matids.append(self.idmat_step_front) + faces.append((f + 4, f + 3, f + 6, f + 5)) + uvs.append([(0, 0), (0, w), (v, w), (v, 0)]) + return rM + + def make_faces(self, f, rM, verts, faces, matids, uvs): + + if self.z_mode == 'LINEAR': + start = 0 + end = 3 + offset = 4 + matids.extend([self.idmat_side, + self.idmat_top, + self.idmat_side, + self.idmat_bottom]) + elif "OPEN" in self.steps_type: + # faces dessus-dessous-lateral marches fermees + start = 3 + end = 6 + offset = 10 + matids.extend([self.idmat_step_side, + self.idmat_top, + self.idmat_step_side, + self.idmat_bottom]) + else: + # faces dessus-dessous-lateral marches fermees + start = 0 + end = 9 + offset = 10 + matids.extend([self.idmat_side, + self.idmat_side, + self.idmat_side, + self.idmat_step_side, + self.idmat_top, + self.idmat_step_side, + self.idmat_side, + self.idmat_side, + self.idmat_side, + self.idmat_bottom]) + + u_l0 = 0 + u_l1 = self.t_step * self.left_length + u_r0 = 0 + u_r1 = self.t_step * self.right_length + + s = int((end - start) / 2) + uvs += [[(u_l0, verts[f + j][2]), (u_l0, verts[f + j + 1][2]), + (u_l1, verts[f + j + offset + 1][2]), (u_l1, verts[f + j + offset][2])] for j in range(start, start + s)] + + self.project_uv(rM, uvs, verts, [f + start + s, f + start + s + 1, + f + start + s + offset + 1, f + start + s + offset]) + + uvs += [[(u_r0, verts[f + j][2]), (u_r0, verts[f + j + 1][2]), + (u_r1, verts[f + j + offset + 1][2]), (u_r1, verts[f + j + offset][2])] for j in range(start + s + 1, end)] + + self.project_uv(rM, uvs, verts, [f + end, f + start, f + offset + start, f + offset + end]) + + faces += [(f + j, f + j + 1, f + j + offset + 1, f + j + offset) for j in range(start, end)] + faces.append((f + end, f + start, f + offset + start, f + offset + end)) + + +class StraightStair(Stair, Line): + def __init__(self, p, v, left_offset, right_offset, steps_type, nose_type, z_mode, nose_z, bottom_z): + Stair.__init__(self, left_offset, right_offset, steps_type, nose_type, z_mode, nose_z, bottom_z) + Line.__init__(self, p, v) + self.l_line = self.offset(-left_offset) + self.r_line = self.offset(right_offset) + + def make_step(self, i, verts, faces, matids, uvs, nose_y=0): + + rM = self._make_nose(i, i, verts, faces, matids, uvs, nose_y) + + t0 = self.t_step * i + + f = len(verts) + + p = self.l_line.lerp(t0) + self.p3d_left(verts, p, i, t0) + p = self.r_line.lerp(t0) + self.p3d_right(verts, p, i, t0) + + t1 = t0 + self.t_step + + p = self.l_line.lerp(t1) + self.p3d_left(verts, p, i, t1) + p = self.r_line.lerp(t1) + self.p3d_right(verts, p, i, t1) + + self.make_faces(f, rM, verts, faces, matids, uvs) + + if "OPEN" in self.steps_type: + faces.append((f + 13, f + 14, f + 15, f + 16)) + matids.append(self.idmat_step_front) + uvs.append([(0, 0), (0, 1), (1, 1), (1, 0)]) + + def get_length(self, side): + return self.length + + def get_lerp_vect(self, posts, side, i, t_step, respect_edges, z_offset=0, t0_abs=None): + if t0_abs is not None: + t0 = t0_abs + else: + t0 = i * t_step + t, part, dz, shape = self.get_part(t0, side) + dz /= part.length + n = part.normal(t) + z0 = self.get_z(t0, 'STEP') + z1 = self.get_z(t0, 'LINEAR') + posts.append((n, dz, z0, z1 + t0 * z_offset)) + return [t0] + + def n_posts(self, post_spacing, side, respect_edges): + return self.steps(post_spacing) + + def get_part(self, t, side): + if side == 'LEFT': + part = self.l_line + else: + part = self.r_line + return t, part, self.height, 'LINE' + + +class CurvedStair(Stair, Arc): + def __init__(self, c, radius, a0, da, left_offset, right_offset, steps_type, nose_type, + z_mode, nose_z, bottom_z, left_shape, right_shape, double_limit=pi): + + Stair.__init__(self, left_offset, right_offset, steps_type, nose_type, z_mode, nose_z, bottom_z) + Arc.__init__(self, c, radius, a0, da) + self.l_shape = left_shape + self.r_shape = right_shape + self.edges_multiples = round(abs(da), 6) > double_limit + # left arc, tangeant at start and end + self.l_arc, self.l_t0, self.l_t1, self.l_tc = self.set_offset(-left_offset, left_shape) + self.r_arc, self.r_t0, self.r_t1, self.r_tc = self.set_offset(right_offset, right_shape) + + def set_offset(self, offset, shape): + arc = self.offset(offset) + t0 = arc.tangeant(0, 1) + t1 = arc.tangeant(1, 1) + tc = arc.tangeant(0.5, 1) + if self.edges_multiples: + i, p, t = t0.intersect(tc) + tc.v *= 2 * t + tc.p = p + i, p, t2 = tc.intersect(t1) + else: + i, p, t = t0.intersect(t1) + t0.v *= t + t1.p = p + t1.v *= t + return arc, t0, t1, tc + + def get_length(self, side): + if side == 'RIGHT': + arc = self.r_arc + shape = self.r_shape + t0 = self.r_t0 + else: + arc = self.l_arc + shape = self.l_shape + t0 = self.l_t0 + if shape == 'CIRCLE': + return arc.length + else: + if self.edges_multiples: + # two edges + return t0.length * 4 + else: + return t0.length * 2 + + def _make_step(self, t_step, i, s, verts, landing=False): + + tb = t_step * i + + f = len(verts) + + t, part, dz, shape = self.get_part(tb, "LEFT") + p = part.lerp(t) + self.p3d_left(verts, p, s, tb, landing) + + t, part, dz, shape = self.get_part(tb, "RIGHT") + p = part.lerp(t) + self.p3d_right(verts, p, s, tb, landing) + return f + + def _make_edge(self, t_step, i, j, f, rM, verts, faces, matids, uvs): + tb = t_step * i + # make edges verts after regular ones + if self.l_shape != 'CIRCLE' or self.r_shape != 'CIRCLE': + if self.edges_multiples: + # edge 1 + if tb < 0.25 and tb + t_step > 0.25: + f0 = f + f = len(verts) + if self.l_shape == 'CIRCLE': + self.p3d_left(verts, self.l_arc.lerp(0.25), j, 0.25) + else: + self.p3d_left(verts, self.l_tc.p, j, 0.25) + if self.r_shape == 'CIRCLE': + self.p3d_right(verts, self.r_arc.lerp(0.25), j, 0.25) + else: + self.p3d_right(verts, self.r_tc.p, j, 0.25) + self.make_faces(f0, rM, verts, faces, matids, uvs) + # edge 2 + if tb < 0.75 and tb + t_step > 0.75: + f0 = f + f = len(verts) + if self.l_shape == 'CIRCLE': + self.p3d_left(verts, self.l_arc.lerp(0.75), j, 0.75) + else: + self.p3d_left(verts, self.l_t1.p, j, 0.75) + if self.r_shape == 'CIRCLE': + self.p3d_right(verts, self.r_arc.lerp(0.75), j, 0.75) + else: + self.p3d_right(verts, self.r_t1.p, j, 0.75) + self.make_faces(f0, rM, verts, faces, matids, uvs) + else: + if tb < 0.5 and tb + t_step > 0.5: + f0 = f + f = len(verts) + # the step goes through the edge + if self.l_shape == 'CIRCLE': + self.p3d_left(verts, self.l_arc.lerp(0.5), j, 0.5) + else: + self.p3d_left(verts, self.l_t1.p, j, 0.5) + if self.r_shape == 'CIRCLE': + self.p3d_right(verts, self.r_arc.lerp(0.5), j, 0.5) + else: + self.p3d_right(verts, self.r_t1.p, j, 0.5) + self.make_faces(f0, rM, verts, faces, matids, uvs) + return f + + def make_step(self, i, verts, faces, matids, uvs, nose_y=0): + + # open stair with closed face + + # step nose + rM = self._make_nose(i, i, verts, faces, matids, uvs, nose_y) + f = 0 + if self.l_shape == 'CIRCLE' or self.r_shape == 'CIRCLE': + # every 6 degree + n_subs = max(1, int(abs(self.da) / pi * 30 / self.n_step)) + t_step = self.t_step / n_subs + for j in range(n_subs): + f0 = f + f = self._make_step(t_step, n_subs * i + j, i, verts) + if j > 0: + self.make_faces(f0, rM, verts, faces, matids, uvs) + f = self._make_edge(t_step, n_subs * i + j, i, f, rM, verts, faces, matids, uvs) + else: + f = self._make_step(self.t_step, i, i, verts) + f = self._make_edge(self.t_step, i, i, f, rM, verts, faces, matids, uvs) + + self._make_step(self.t_step, i + 1, i, verts) + self.make_faces(f, rM, verts, faces, matids, uvs) + + if "OPEN" in self.steps_type and self.z_mode != 'LINEAR': + # back face top + faces.append((f + 13, f + 14, f + 15, f + 16)) + matids.append(self.idmat_step_front) + uvs.append([(0, 0), (0, 1), (1, 1), (1, 0)]) + + def get_part(self, t, side): + if side == 'RIGHT': + arc = self.r_arc + shape = self.r_shape + t0, t1, tc = self.r_t0, self.r_t1, self.r_tc + else: + arc = self.l_arc + shape = self.l_shape + t0, t1, tc = self.l_t0, self.l_t1, self.l_tc + if shape == 'CIRCLE': + return t, arc, self.height, shape + else: + if self.edges_multiples: + # two edges + if t <= 0.25: + return 4 * t, t0, 0.25 * self.height, shape + elif t <= 0.75: + return 2 * (t - 0.25), tc, 0.5 * self.height, shape + else: + return 4 * (t - 0.75), t1, 0.25 * self.height, shape + else: + if t <= 0.5: + return 2 * t, t0, 0.5 * self.height, shape + else: + return 2 * (t - 0.5), t1, 0.5 * self.height, shape + + def get_lerp_vect(self, posts, side, i, t_step, respect_edges, z_offset=0, t0_abs=None): + if t0_abs is not None: + t0 = t0_abs + else: + t0 = i * t_step + res = [t0] + t1 = t0 + t_step + zs = self.get_z(t0, 'STEP') + zl = self.get_z(t0, 'LINEAR') + + # vect normal + t, part, dz, shape = self.get_part(t0, side) + n = part.normal(t) + dz /= part.length + posts.append((n, dz, zs, zl + t0 * z_offset)) + + if shape != 'CIRCLE' and respect_edges: + if self.edges_multiples: + if t0 < 0.25 and t1 > 0.25: + zs = self.get_z(0.25, 'STEP') + zl = self.get_z(0.25, 'LINEAR') + t, part, dz, shape = self.get_part(0.25, side) + n = part.normal(1) + posts.append((n, dz, zs, zl + 0.25 * z_offset)) + res.append(0.25) + if t0 < 0.75 and t1 > 0.75: + zs = self.get_z(0.75, 'STEP') + zl = self.get_z(0.75, 'LINEAR') + t, part, dz, shape = self.get_part(0.75, side) + n = part.normal(1) + posts.append((n, dz, zs, zl + 0.75 * z_offset)) + res.append(0.75) + elif t0 < 0.5 and t1 > 0.5: + zs = self.get_z(0.5, 'STEP') + zl = self.get_z(0.5, 'LINEAR') + t, part, dz, shape = self.get_part(0.5, side) + n = part.normal(1) + posts.append((n, dz, zs, zl + 0.5 * z_offset)) + res.append(0.5) + return res + + def n_posts(self, post_spacing, side, respect_edges): + if side == 'LEFT': + arc, t0, shape = self.l_arc, self.l_t0, self.l_shape + else: + arc, t0, shape = self.r_arc, self.r_t0, self.r_shape + step_factor = 1 + if shape == 'CIRCLE': + length = arc.length + else: + if self.edges_multiples: + if respect_edges: + step_factor = 2 + length = 4 * t0.length + else: + length = 2 * t0.length + steps = step_factor * max(1, round(length / post_spacing, 0)) + # print("respect_edges:%s t_step:%s n_step:%s" % (respect_edges, 1.0 / steps, int(steps))) + return 1.0 / steps, int(steps) + + +class StraightLanding(StraightStair): + def __init__(self, p, v, left_offset, right_offset, steps_type, + nose_type, z_mode, nose_z, bottom_z, last_type='STAIR'): + + StraightStair.__init__(self, p, v, left_offset, right_offset, steps_type, + nose_type, z_mode, nose_z, bottom_z) + + self.last_type = last_type + + @property + def height(self): + return 0 + + @property + def top_offset(self): + return self.t_step / self.v.length + + @property + def top(self): + if self.next_type == 'LANDING': + return self.z0 + else: + return self.z0 + self.step_height + + def step_size(self, step_depth): + self.n_step = 1 + self.t_step = 1 + self.step_depth = step_depth + if self.last_type == 'LANDING': + return 0 + else: + return 1 + + def make_step(self, i, verts, faces, matids, uvs, nose_y=0): + + if i == 0 and self.last_type != 'LANDING': + rM = self._make_nose(i, 0, verts, faces, matids, uvs, nose_y) + else: + rM = self.get_proj_matrix(self.l_line, self.t_step * i, nose_y) + + f = len(verts) + j = 0 + t0 = self.t_step * i + + p = self.l_line.lerp(t0) + self.p3d_left(verts, p, j, t0) + + p = self.r_line.lerp(t0) + self.p3d_right(verts, p, j, t0) + + t1 = t0 + self.t_step + p = self.l_line.lerp(t1) + self.p3d_left(verts, p, j, t1, self.next_type != 'LANDING') + + p = self.r_line.lerp(t1) + self.p3d_right(verts, p, j, t1, self.next_type != 'LANDING') + + self.make_faces(f, rM, verts, faces, matids, uvs) + + if "OPEN" in self.steps_type and self.next_type != 'LANDING': + faces.append((f + 13, f + 14, f + 15, f + 16)) + matids.append(self.idmat_step_front) + uvs.append([(0, 0), (0, 1), (1, 1), (1, 0)]) + + def straight_landing(self, length): + return Stair.straight_landing(self, length, last_type='LANDING') + + def curved_landing(self, da, radius, left_shape, right_shape, double_limit=pi): + return Stair.curved_landing(self, da, radius, left_shape, + right_shape, double_limit=double_limit, last_type='LANDING') + + def get_z(self, t, mode): + if mode == 'STEP': + return self.z0 + self.step_height + else: + return self.z0 + + +class CurvedLanding(CurvedStair): + def __init__(self, c, radius, a0, da, left_offset, right_offset, steps_type, + nose_type, z_mode, nose_z, bottom_z, left_shape, right_shape, double_limit=pi, last_type='STAIR'): + + CurvedStair.__init__(self, c, radius, a0, da, left_offset, right_offset, steps_type, + nose_type, z_mode, nose_z, bottom_z, left_shape, right_shape, double_limit=double_limit) + + self.last_type = last_type + + @property + def top_offset(self): + if self.l_shape == 'CIRCLE' or self.r_shape == 'CIRCLE': + return self.t_step / self.step_depth + else: + if self.edges_multiples: + return 0.5 / self.length + else: + return 1 / self.length + + @property + def height(self): + return 0 + + @property + def top(self): + if self.next_type == 'LANDING': + return self.z0 + else: + return self.z0 + self.step_height + + def step_size(self, step_depth): + if self.l_shape == 'CIRCLE' or self.r_shape == 'CIRCLE': + t_step, n_step = self.steps(step_depth) + else: + if self.edges_multiples: + t_step, n_step = 0.5, 2 + else: + t_step, n_step = 1, 1 + self.n_step = n_step + self.t_step = t_step + self.step_depth = step_depth + if self.last_type == 'LANDING': + return 0 + else: + return 1 + + def make_step(self, i, verts, faces, matids, uvs, nose_y=0): + + if i == 0 and 'LANDING' not in self.last_type: + rM = self._make_nose(i, 0, verts, faces, matids, uvs, nose_y) + else: + rM = self.get_proj_matrix(self.l_arc, self.t_step * i, nose_y) + + f = len(verts) + + if self.l_shape == 'CIRCLE' or self.r_shape == 'CIRCLE': + n_subs = max(1, int(abs(self.da / pi * 30 / self.n_step))) + t_step = self.t_step / n_subs + for j in range(n_subs): + f0 = f + f = self._make_step(t_step, n_subs * i + j, 0, verts) + if j > 0: + self.make_faces(f0, rM, verts, faces, matids, uvs) + f = self._make_edge(t_step, n_subs * i + j, 0, f, rM, verts, faces, matids, uvs) + else: + f = self._make_step(self.t_step, i, 0, verts) + f = self._make_edge(self.t_step, i, 0, f, rM, verts, faces, matids, uvs) + + self._make_step(self.t_step, i + 1, 0, verts, i == self.n_step - 1 and 'LANDING' not in self.next_type) + self.make_faces(f, rM, verts, faces, matids, uvs) + + if "OPEN" in self.steps_type and 'LANDING' not in self.next_type: + faces.append((f + 13, f + 14, f + 15, f + 16)) + matids.append(self.idmat_step_front) + uvs.append([(0, 0), (0, 1), (1, 1), (1, 0)]) + + def straight_landing(self, length): + return Stair.straight_landing(self, length, last_type='LANDING') + + def curved_landing(self, da, radius, left_shape, right_shape, double_limit=pi): + return Stair.curved_landing(self, da, radius, left_shape, + right_shape, double_limit=double_limit, last_type='LANDING') + + def get_z(self, t, mode): + if mode == 'STEP': + return self.z0 + self.step_height + else: + return self.z0 + + +class StairGenerator(): + def __init__(self, parts): + self.parts = parts + self.last_type = 'NONE' + self.stairs = [] + self.steps_type = 'NONE' + self.sum_da = 0 + self.user_defined_post = None + self.user_defined_uvs = None + self.user_defined_mat = None + + def add_part(self, type, steps_type, nose_type, z_mode, nose_z, bottom_z, center, + radius, da, width_left, width_right, length, left_shape, right_shape): + + self.steps_type = steps_type + if len(self.stairs) < 1: + s = None + else: + s = self.stairs[-1] + + if "S_" not in type: + self.sum_da += da + + # start a new stair + if s is None: + if type == 'S_STAIR': + p = Vector((0, 0)) + v = Vector((0, length)) + s = StraightStair(p, v, width_left, width_right, steps_type, nose_type, z_mode, nose_z, bottom_z) + elif type == 'C_STAIR': + if da < 0: + c = Vector((radius, 0)) + else: + c = Vector((-radius, 0)) + s = CurvedStair(c, radius, 0, da, width_left, width_right, steps_type, + nose_type, z_mode, nose_z, bottom_z, left_shape, right_shape) + elif type == 'D_STAIR': + if da < 0: + c = Vector((radius, 0)) + else: + c = Vector((-radius, 0)) + s = CurvedStair(c, radius, 0, da, width_left, width_right, steps_type, + nose_type, z_mode, nose_z, bottom_z, left_shape, right_shape, double_limit=0) + elif type == 'S_LANDING': + p = Vector((0, 0)) + v = Vector((0, length)) + s = StraightLanding(p, v, width_left, width_right, steps_type, nose_type, z_mode, nose_z, bottom_z) + elif type == 'C_LANDING': + if da < 0: + c = Vector((radius, 0)) + else: + c = Vector((-radius, 0)) + s = CurvedLanding(c, radius, 0, da, width_left, width_right, steps_type, + nose_type, z_mode, nose_z, bottom_z, left_shape, right_shape) + elif type == 'D_LANDING': + if da < 0: + c = Vector((radius, 0)) + else: + c = Vector((-radius, 0)) + s = CurvedLanding(c, radius, 0, da, width_left, width_right, steps_type, + nose_type, z_mode, nose_z, bottom_z, left_shape, right_shape, double_limit=0) + else: + if type == 'S_STAIR': + s = s.straight_stair(length) + elif type == 'C_STAIR': + s = s.curved_stair(da, radius, left_shape, right_shape) + elif type == 'D_STAIR': + s = s.curved_stair(da, radius, left_shape, right_shape, double_limit=0) + elif type == 'S_LANDING': + s = s.straight_landing(length) + elif type == 'C_LANDING': + s = s.curved_landing(da, radius, left_shape, right_shape) + elif type == 'D_LANDING': + s = s.curved_landing(da, radius, left_shape, right_shape, double_limit=0) + self.stairs.append(s) + self.last_type = type + + def n_steps(self, step_depth): + n_steps = 0 + for stair in self.stairs: + n_steps += stair.step_size(step_depth) + return n_steps + + def set_height(self, step_height): + z = 0 + for stair in self.stairs: + stair.set_height(step_height, z) + z = stair.top + + def make_stair(self, height, step_depth, verts, faces, matids, uvs, nose_y=0): + n_steps = self.n_steps(step_depth) + self.set_height(height / n_steps) + + for s, stair in enumerate(self.stairs): + if s < len(self.parts): + manipulator = self.parts[s].manipulators[0] + # Store Gl Points for manipulators + if 'Curved' in type(stair).__name__: + c = stair.c + p0 = (stair.p0 - c).to_3d() + p1 = (stair.p1 - c).to_3d() + manipulator.set_pts([(c.x, c.y, stair.top), p0, p1]) + manipulator.type_key = 'ARC_ANGLE_RADIUS' + manipulator.prop1_name = 'da' + manipulator.prop2_name = 'radius' + else: + if self.sum_da > 0: + side = 1 + else: + side = -1 + v0 = stair.p0 + v1 = stair.p1 + manipulator.set_pts([(v0.x, v0.y, stair.top), (v1.x, v1.y, stair.top), (side, 0, 0)]) + manipulator.type_key = 'SIZE' + manipulator.prop1_name = 'length' + + for i in range(stair.n_step): + stair.make_step(i, verts, faces, matids, uvs, nose_y=nose_y) + if s < len(self.stairs) - 1 and self.steps_type != 'OPEN' and \ + 'Landing' in type(stair).__name__ and stair.next_type != "LANDING": + f = len(verts) - 10 + faces.append((f, f + 1, f + 8, f + 9)) + matids.append(self.stairs[-1].idmat_bottom) + u = verts[f + 1][2] - verts[f][2] + v = (Vector(verts[f]) - Vector(verts[f + 9])).length + uvs.append([(0, 0), (0, u), (v, u), (v, 0)]) + + if self.steps_type != 'OPEN' and len(self.stairs) > 0: + f = len(verts) - 10 + faces.append((f, f + 1, f + 2, f + 3, f + 4, f + 5, f + 6, f + 7, f + 8, f + 9)) + matids.append(self.stairs[-1].idmat_bottom) + uvs.append([(0, 0), (.1, 0), (.2, 0), (.3, 0), (.4, 0), (.4, 1), (.3, 1), (.2, 1), (.1, 1), (0, 1)]) + + def setup_user_defined_post(self, o, post_x, post_y, post_z): + self.user_defined_post = o + x = o.bound_box[6][0] - o.bound_box[0][0] + y = o.bound_box[6][1] - o.bound_box[0][1] + z = o.bound_box[6][2] - o.bound_box[0][2] + self.user_defined_post_scale = Vector((post_x / x, post_y / -y, post_z / z)) + m = o.data + # create vertex group lookup dictionary for names + vgroup_names = {vgroup.index: vgroup.name for vgroup in o.vertex_groups} + # create dictionary of vertex group assignments per vertex + self.vertex_groups = [[vgroup_names[g.group] for g in v.groups] for v in m.vertices] + # uvs + uv_act = m.uv_layers.active + if uv_act is not None: + uv_layer = uv_act.data + self.user_defined_uvs = [[uv_layer[li].uv for li in p.loop_indices] for p in m.polygons] + else: + self.user_defined_uvs = [[(0, 0) for i in p.vertices] for p in m.polygons] + # material ids + self.user_defined_mat = [p.material_index for p in m.polygons] + + def get_user_defined_post(self, tM, z0, z1, z2, slope, post_z, verts, faces, matids, uvs): + f = len(verts) + m = self.user_defined_post.data + for i, g in enumerate(self.vertex_groups): + co = m.vertices[i].co.copy() + co.x *= self.user_defined_post_scale.x + co.y *= self.user_defined_post_scale.y + co.z *= self.user_defined_post_scale.z + if 'Top' in g: + co.z += z2 + elif 'Bottom' in g: + co.z += 0 + else: + co.z += z1 + if 'Slope' in g: + co.z += co.y * slope + verts.append(tM * co) + matids += self.user_defined_mat + faces += [tuple([i + f for i in p.vertices]) for p in m.polygons] + uvs += self.user_defined_uvs + + def get_post(self, post, post_x, post_y, post_z, post_alt, sub_offset_x, + id_mat, verts, faces, matids, uvs, bottom="STEP"): + + n, dz, zs, zl = post + slope = dz * post_y + + if self.user_defined_post is not None: + if bottom == "STEP": + z0 = zs + else: + z0 = zl + z1 = zl - z0 + z2 = zl - z0 + x, y = -n.v.normalized() + tM = Matrix([ + [x, y, 0, n.p.x], + [y, -x, 0, n.p.y], + [0, 0, 1, z0 + post_alt], + [0, 0, 0, 1] + ]) + self.get_user_defined_post(tM, z0, z1, z2, dz, post_z, verts, faces, matids, uvs) + return + + z3 = zl + post_z + post_alt - slope + z4 = zl + post_z + post_alt + slope + if bottom == "STEP": + z0 = zs + post_alt + z1 = zs + post_alt + else: + z0 = zl + post_alt - slope + z1 = zl + post_alt + slope + vn = n.v.normalized() + dx = post_x * vn + dy = post_y * Vector((vn.y, -vn.x)) + oy = sub_offset_x * vn + x0, y0 = n.p - dx + dy + oy + x1, y1 = n.p - dx - dy + oy + x2, y2 = n.p + dx - dy + oy + x3, y3 = n.p + dx + dy + oy + f = len(verts) + verts.extend([(x0, y0, z0), (x0, y0, z3), + (x1, y1, z1), (x1, y1, z4), + (x2, y2, z1), (x2, y2, z4), + (x3, y3, z0), (x3, y3, z3)]) + faces.extend([(f, f + 1, f + 3, f + 2), + (f + 2, f + 3, f + 5, f + 4), + (f + 4, f + 5, f + 7, f + 6), + (f + 6, f + 7, f + 1, f), + (f, f + 2, f + 4, f + 6), + (f + 7, f + 5, f + 3, f + 1)]) + matids.extend([id_mat, id_mat, id_mat, id_mat, id_mat, id_mat]) + x = [(0, 0), (0, post_z), (post_x, post_z), (post_x, 0)] + y = [(0, 0), (0, post_z), (post_y, post_z), (post_y, 0)] + z = [(0, 0), (post_x, 0), (post_x, post_y), (0, post_y)] + uvs.extend([x, y, x, y, z, z]) + + def get_panel(self, subs, altitude, panel_x, panel_z, sub_offset_x, idmat, verts, faces, matids, uvs): + n_subs = len(subs) + if n_subs < 1: + return + f = len(verts) + x0 = sub_offset_x - 0.5 * panel_x + x1 = sub_offset_x + 0.5 * panel_x + z0 = 0 + z1 = panel_z + profile = [Vector((x0, z0)), Vector((x1, z0)), Vector((x1, z1)), Vector((x0, z1))] + user_path_uv_v = [] + n_sections = n_subs - 1 + n, dz, zs, zl = subs[0] + p0 = n.p + v0 = n.v.normalized() + for s, section in enumerate(subs): + n, dz, zs, zl = section + p1 = n.p + if s < n_sections: + v1 = subs[s + 1][0].v.normalized() + dir = (v0 + v1).normalized() + scale = 1 / cos(0.5 * acos(min(1, max(-1, v0 * v1)))) + for p in profile: + x, y = n.p + scale * p.x * dir + z = zl + p.y + altitude + verts.append((x, y, z)) + if s > 0: + user_path_uv_v.append((p1 - p0).length) + p0 = p1 + v0 = v1 + + # build faces using Panel + lofter = Lofter( + # closed_shape, index, x, y, idmat + True, + [i for i in range(len(profile))], + [p.x for p in profile], + [p.y for p in profile], + [idmat for i in range(len(profile))], + closed_path=False, + user_path_uv_v=user_path_uv_v, + user_path_verts=n_subs + ) + faces += lofter.faces(16, offset=f, path_type='USER_DEFINED') + matids += lofter.mat(16, idmat, idmat, path_type='USER_DEFINED') + v = Vector((0, 0)) + uvs += lofter.uv(16, v, v, v, v, 0, v, 0, 0, path_type='USER_DEFINED') + + def reset_shapes(self): + for s, stair in enumerate(self.stairs): + if 'Curved' in type(stair).__name__: + stair.l_shape = self.parts[s].left_shape + stair.r_shape = self.parts[s].right_shape + + def make_subs(self, height, step_depth, x, y, z, post_y, altitude, bottom, side, slice, + post_spacing, sub_spacing, respect_edges, move_x, x_offset, sub_offset_x, mat, + verts, faces, matids, uvs): + + n_steps = self.n_steps(step_depth) + self.set_height(height / n_steps) + n_stairs = len(self.stairs) - 1 + subs = [] + + if side == "LEFT": + offset = move_x - x_offset + # offset_sub = offset - sub_offset_x + else: + offset = move_x + x_offset + # offset_sub = offset + sub_offset_x + + for s, stair in enumerate(self.stairs): + if 'Curved' in type(stair).__name__: + if side == "LEFT": + part = stair.l_arc + shape = stair.l_shape + else: + part = stair.r_arc + shape = stair.r_shape + # Note: use left part as reference for post distances + # use right part as reference for panels + stair.l_arc, stair.l_t0, stair.l_t1, stair.l_tc = stair.set_offset(offset, shape) + stair.r_arc, stair.r_t0, stair.r_t1, stair.r_tc = stair.set_offset(offset, shape) + else: + stair.l_line = stair.offset(offset) + stair.r_line = stair.offset(offset) + part = stair.l_line + + lerp_z = 0 + edge_t = 1 + edge_size = 0 + # interpolate z near end landing + if 'Landing' in type(stair).__name__ and stair.next_type == 'STAIR': + if not slice: + line = stair.normal(1).offset(self.stairs[s + 1].step_depth) + res, p, t_part = part.intersect(line) + # does perpendicular line intersects circle ? + if res: + edge_size = self.stairs[s + 1].step_depth / stair.get_length(side) + edge_t = 1 - edge_size + else: + # in this case, lerp z over one step + lerp_z = stair.step_height + + t_step, n_step = stair.n_posts(post_spacing, side, respect_edges) + + # space between posts + sp = stair.get_length(side) + # post size + t_post = post_y / sp + + if s == n_stairs: + n_step += 1 + for i in range(n_step): + res_t = stair.get_lerp_vect([], side, i, t_step, respect_edges) + # subs + if s < n_stairs or i < n_step - 1: + res_t.append((i + 1) * t_step) + for j in range(len(res_t) - 1): + t0 = res_t[j] + t_post + t1 = res_t[j + 1] - t_post + dt = t1 - t0 + n_subs = int(sp * dt / sub_spacing) + if n_subs > 0: + t_subs = dt / n_subs + for k in range(1, n_subs): + t = t0 + k * t_subs + stair.get_lerp_vect(subs, side, 1, t0 + k * t_subs, False) + if t > edge_t: + n, dz, z0, z1 = subs[-1] + subs[-1] = n, dz, z0, z1 + (t - edge_t) / edge_size * stair.step_height + if lerp_z > 0: + n, dz, z0, z1 = subs[-1] + subs[-1] = n, dz, z0, z1 + t * stair.step_height + + for i, post in enumerate(subs): + self.get_post(post, x, y, z, altitude, sub_offset_x, mat, verts, faces, matids, uvs, bottom=bottom) + + def make_post(self, height, step_depth, x, y, z, altitude, side, post_spacing, respect_edges, move_x, x_offset, mat, + verts, faces, matids, uvs): + n_steps = self.n_steps(step_depth) + self.set_height(height / n_steps) + l_posts = [] + n_stairs = len(self.stairs) - 1 + + for s, stair in enumerate(self.stairs): + if type(stair).__name__ in ['CurvedStair', 'CurvedLanding']: + stair.l_arc, stair.l_t0, stair.l_t1, stair.l_tc = stair.set_offset(move_x - x_offset, stair.l_shape) + stair.r_arc, stair.r_t0, stair.r_t1, stair.r_tc = stair.set_offset(move_x + x_offset, stair.r_shape) + else: + stair.l_line = stair.offset(move_x - x_offset) + stair.r_line = stair.offset(move_x + x_offset) + + t_step, n_step = stair.n_posts(post_spacing, side, respect_edges) + + if s == n_stairs: + n_step += 1 + for i in range(n_step): + stair.get_lerp_vect(l_posts, side, i, t_step, respect_edges) + + if s == n_stairs and i == n_step - 1: + n, dz, z0, z1 = l_posts[-1] + l_posts[-1] = (n, dz, z0 - stair.step_height, z1) + + for i, post in enumerate(l_posts): + self.get_post(post, x, y, z, altitude, 0, mat, verts, faces, matids, uvs) + + def make_panels(self, height, step_depth, x, z, post_y, altitude, side, post_spacing, + panel_dist, respect_edges, move_x, x_offset, sub_offset_x, mat, verts, faces, matids, uvs): + + n_steps = self.n_steps(step_depth) + self.set_height(height / n_steps) + subs = [] + n_stairs = len(self.stairs) - 1 + + if side == "LEFT": + offset = move_x - x_offset + else: + offset = move_x + x_offset + + for s, stair in enumerate(self.stairs): + + is_circle = False + if 'Curved' in type(stair).__name__: + if side == "LEFT": + is_circle = stair.l_shape == "CIRCLE" + shape = stair.l_shape + else: + is_circle = stair.r_shape == "CIRCLE" + shape = stair.r_shape + stair.l_arc, stair.l_t0, stair.l_t1, stair.l_tc = stair.set_offset(offset, shape) + stair.r_arc, stair.r_t0, stair.r_t1, stair.r_tc = stair.set_offset(offset, shape) + else: + stair.l_line = stair.offset(offset) + stair.r_line = stair.offset(offset) + + # space between posts + sp = stair.get_length(side) + + t_step, n_step = stair.n_posts(post_spacing, side, respect_edges) + + if is_circle and 'Curved' in type(stair).__name__: + panel_da = abs(stair.da) / pi * 180 / n_step + panel_step = max(1, int(panel_da / 6)) + else: + panel_step = 1 + + # post size + t_post = (post_y + panel_dist) / sp + + if s == n_stairs: + n_step += 1 + for i in range(n_step): + res_t = stair.get_lerp_vect([], side, i, t_step, respect_edges) + # subs + if s < n_stairs or i < n_step - 1: + res_t.append((i + 1) * t_step) + for j in range(len(res_t) - 1): + t0 = res_t[j] + t_post + t1 = res_t[j + 1] - t_post + dt = t1 - t0 + t_curve = dt / panel_step + if dt > 0: + panel = [] + for k in range(panel_step): + stair.get_lerp_vect(panel, side, 1, t_curve, True, t0_abs=t0 + k * t_curve) + stair.get_lerp_vect(panel, side, 1, t1, False) + subs.append(panel) + for sub in subs: + self.get_panel(sub, altitude, x, z, sub_offset_x, mat, verts, faces, matids, uvs) + + def make_part(self, height, step_depth, part_x, part_z, x_move, x_offset, + z_offset, z_mode, steps_type, verts, faces, matids, uvs): + + params = [(stair.z_mode, stair.l_shape, stair.r_shape, + stair.bottom_z, stair.steps_type) for stair in self.stairs] + + for stair in self.stairs: + if x_offset > 0: + stair.l_shape = stair.r_shape + else: + stair.r_shape = stair.l_shape + stair.steps_type = steps_type + stair.z_mode = "LINEAR" + stair.bottom_z = part_z + if 'Curved' in type(stair).__name__: + stair.l_arc, stair.l_t0, stair.l_t1, stair.l_tc = \ + stair.set_offset(x_move + x_offset + 0.5 * part_x, stair.l_shape) + stair.r_arc, stair.r_t0, stair.r_t1, stair.r_tc = \ + stair.set_offset(x_move + x_offset - 0.5 * part_x, stair.r_shape) + else: + stair.l_line = stair.offset(x_move + x_offset + 0.5 * part_x) + stair.r_line = stair.offset(x_move + x_offset - 0.5 * part_x) + n_steps = self.n_steps(step_depth) + self.set_height(height / n_steps) + for j, stair in enumerate(self.stairs): + stair.z0 += z_offset + part_z + stair.n_step *= 2 + stair.t_step /= 2 + stair.step_height /= 2 + for i in range(stair.n_step): + stair.make_step(i, verts, faces, matids, uvs, nose_y=0) + stair.n_step /= 2 + stair.t_step *= 2 + stair.step_height *= 2 + stair.z_mode = params[j][0] + stair.l_shape = params[j][1] + stair.r_shape = params[j][2] + stair.bottom_z = params[j][3] + stair.steps_type = params[j][4] + stair.z0 -= z_offset + part_z + + def make_profile(self, profile, idmat, side, slice, height, step_depth, + x_offset, z_offset, extend, verts, faces, matids, uvs): + + for stair in self.stairs: + if 'Curved' in type(stair).__name__: + stair.l_arc, stair.l_t0, stair.l_t1, stair.l_tc = stair.set_offset(-x_offset, stair.l_shape) + stair.r_arc, stair.r_t0, stair.r_t1, stair.r_tc = stair.set_offset(x_offset, stair.r_shape) + else: + stair.l_line = stair.offset(-x_offset) + stair.r_line = stair.offset(x_offset) + + n_steps = self.n_steps(step_depth) + self.set_height(height / n_steps) + + n_stairs = len(self.stairs) - 1 + + if n_stairs < 0: + return + + sections = [] + sections.append([]) + + # first step + if extend != 0: + t = -extend / self.stairs[0].length + self.stairs[0].get_lerp_vect(sections[-1], side, 1, t, True) + + for s, stair in enumerate(self.stairs): + n_step = 1 + is_circle = False + + if 'Curved' in type(stair).__name__: + if side == "LEFT": + part = stair.l_arc + is_circle = stair.l_shape == "CIRCLE" + else: + part = stair.r_arc + is_circle = stair.r_shape == "CIRCLE" + else: + if side == "LEFT": + part = stair.l_line + else: + part = stair.r_line + + if is_circle: + n_step = 3 * stair.n_step + + t_step = 1 / n_step + + last_t = 1.0 + do_last = True + lerp_z = 0 + # last section 1 step before stair + if 'Landing' in type(stair).__name__ and stair.next_type == 'STAIR': + if not slice: + line = stair.normal(1).offset(self.stairs[s + 1].step_depth) + res, p, t_part = part.intersect(line) + # does perpendicular line intersects circle ? + if res: + last_t = 1 - self.stairs[s + 1].step_depth / stair.get_length(side) + if last_t < 0: + do_last = False + else: + # in this case, lerp z over one step + do_last = False + lerp_z = stair.step_height + + if s == n_stairs: + n_step += 1 + + for i in range(n_step): + res_t = stair.get_lerp_vect(sections[-1], side, i, t_step, True, z_offset=lerp_z) + # remove corner section + for cur_t in res_t: + if cur_t > 0 and cur_t > last_t: + sections[-1] = sections[-1][:-1] + + # last section 1 step before next stair start + if 'Landing' in type(stair).__name__ and stair.next_type == 'STAIR': + if do_last: + stair.get_lerp_vect(sections[-1], side, 1, last_t, False) + if slice: + sections.append([]) + if extend > 0: + t = -extend / self.stairs[s + 1].length + self.stairs[s + 1].get_lerp_vect(sections[-1], side, 1, t, True) + + t = 1 + extend / self.stairs[-1].length + self.stairs[-1].get_lerp_vect(sections[-1], side, 1, t, True) + + for cur_sect in sections: + user_path_verts = len(cur_sect) + f = len(verts) + if user_path_verts > 0: + user_path_uv_v = [] + n, dz, z0, z1 = cur_sect[-1] + cur_sect[-1] = (n, dz, z0 - stair.step_height, z1) + n_sections = user_path_verts - 1 + n, dz, zs, zl = cur_sect[0] + p0 = n.p + v0 = n.v.normalized() + for s, section in enumerate(cur_sect): + n, dz, zs, zl = section + p1 = n.p + if s < n_sections: + v1 = cur_sect[s + 1][0].v.normalized() + dir = (v0 + v1).normalized() + scale = 1 / cos(0.5 * acos(min(1, max(-1, v0 * v1)))) + for p in profile: + x, y = n.p + scale * p.x * dir + z = zl + p.y + z_offset + verts.append((x, y, z)) + if s > 0: + user_path_uv_v.append((p1 - p0).length) + p0 = p1 + v0 = v1 + + # build faces using Panel + lofter = Lofter( + # closed_shape, index, x, y, idmat + True, + [i for i in range(len(profile))], + [p.x for p in profile], + [p.y for p in profile], + [idmat for i in range(len(profile))], + closed_path=False, + user_path_uv_v=user_path_uv_v, + user_path_verts=user_path_verts + ) + faces += lofter.faces(16, offset=f, path_type='USER_DEFINED') + matids += lofter.mat(16, idmat, idmat, path_type='USER_DEFINED') + v = Vector((0, 0)) + uvs += lofter.uv(16, v, v, v, v, 0, v, 0, 0, path_type='USER_DEFINED') + + def set_matids(self, id_materials): + for stair in self.stairs: + stair.set_matids(id_materials) + + +def update(self, context): + self.update(context) + + +def update_manipulators(self, context): + self.update(context, manipulable_refresh=True) + + +def update_preset(self, context): + auto_update = self.auto_update + self.auto_update = False + if self.presets == 'STAIR_I': + self.n_parts = 1 + self.update_parts() + self.parts[0].type = 'S_STAIR' + elif self.presets == 'STAIR_L': + self.n_parts = 3 + self.update_parts() + self.parts[0].type = 'S_STAIR' + self.parts[1].type = 'C_STAIR' + self.parts[2].type = 'S_STAIR' + self.da = pi / 2 + elif self.presets == 'STAIR_U': + self.n_parts = 3 + self.update_parts() + self.parts[0].type = 'S_STAIR' + self.parts[1].type = 'D_STAIR' + self.parts[2].type = 'S_STAIR' + self.da = pi + elif self.presets == 'STAIR_O': + self.n_parts = 2 + self.update_parts() + self.parts[0].type = 'D_STAIR' + self.parts[1].type = 'D_STAIR' + self.da = pi + # keep auto_update state same + # prevent unwanted load_preset update + self.auto_update = auto_update + + +materials_enum = ( + ('0', 'Ceiling', '', 0), + ('1', 'White', '', 1), + ('2', 'Concrete', '', 2), + ('3', 'Wood', '', 3), + ('4', 'Metal', '', 4), + ('5', 'Glass', '', 5) + ) + + +class archipack_stair_material(PropertyGroup): + index = EnumProperty( + items=materials_enum, + default='4', + update=update + ) + + def find_datablock_in_selection(self, context): + """ + find witch selected object this instance belongs to + provide support for "copy to selected" + """ + selected = [o for o in context.selected_objects] + for o in selected: + props = archipack_stair.datablock(o) + if props: + for part in props.rail_mat: + if part == self: + return props + return None + + def update(self, context): + props = self.find_datablock_in_selection(context) + if props is not None: + props.update(context) + + +class archipack_stair_part(PropertyGroup): + type = EnumProperty( + items=( + ('S_STAIR', 'Straight stair', '', 0), + ('C_STAIR', 'Curved stair', '', 1), + ('D_STAIR', 'Dual Curved stair', '', 2), + ('S_LANDING', 'Straight landing', '', 3), + ('C_LANDING', 'Curved landing', '', 4), + ('D_LANDING', 'Dual Curved landing', '', 5) + ), + default='S_STAIR', + update=update_manipulators + ) + length = FloatProperty( + name="length", + min=0.5, + default=2.0, + unit='LENGTH', subtype='DISTANCE', + update=update + ) + radius = FloatProperty( + name="radius", + min=0.5, + default=0.7, + unit='LENGTH', subtype='DISTANCE', + update=update + ) + da = FloatProperty( + name="angle", + min=-pi, + max=pi, + default=pi / 2, + subtype='ANGLE', unit='ROTATION', + update=update + ) + left_shape = EnumProperty( + items=( + ('RECTANGLE', 'Straight', '', 0), + ('CIRCLE', 'Curved ', '', 1) + ), + default='RECTANGLE', + update=update + ) + right_shape = EnumProperty( + items=( + ('RECTANGLE', 'Straight', '', 0), + ('CIRCLE', 'Curved ', '', 1) + ), + default='RECTANGLE', + update=update + ) + manipulators = CollectionProperty(type=archipack_manipulator) + + def find_datablock_in_selection(self, context): + """ + find witch selected object this instance belongs to + provide support for "copy to selected" + """ + selected = [o for o in context.selected_objects] + for o in selected: + props = archipack_stair.datablock(o) + if props: + for part in props.parts: + if part == self: + return props + return None + + def update(self, context, manipulable_refresh=False): + props = self.find_datablock_in_selection(context) + if props is not None: + props.update(context, manipulable_refresh) + + def draw(self, layout, context, index, user_mode): + if user_mode: + box = layout.box() + row = box.row() + row.prop(self, "type", text=str(index + 1)) + if self.type in ['C_STAIR', 'C_LANDING', 'D_STAIR', 'D_LANDING']: + row = box.row() + row.prop(self, "radius") + row = box.row() + row.prop(self, "da") + else: + row = box.row() + row.prop(self, "length") + if self.type in ['C_STAIR', 'C_LANDING', 'D_STAIR', 'D_LANDING']: + row = box.row(align=True) + row.prop(self, "left_shape", text="") + row.prop(self, "right_shape", text="") + else: + if self.type in ['S_STAIR', 'S_LANDING']: + box = layout.box() + row = box.row() + row.prop(self, "length") + + +class archipack_stair(ArchipackObject, Manipulable, PropertyGroup): + + parts = CollectionProperty(type=archipack_stair_part) + n_parts = IntProperty( + name="parts", + min=1, + max=32, + default=1, update=update_manipulators + ) + step_depth = FloatProperty( + name="Going", + min=0.2, + default=0.25, + unit='LENGTH', subtype='DISTANCE', + update=update + ) + width = FloatProperty( + name="width", + min=0.01, + default=1.2, + unit='LENGTH', subtype='DISTANCE', + update=update + ) + height = FloatProperty( + name="Height", + min=0.1, + default=2.4, precision=2, step=1, + unit='LENGTH', subtype='DISTANCE', + update=update + ) + nose_y = FloatProperty( + name="Depth", + min=0.0, + default=0.02, precision=2, step=1, + unit='LENGTH', subtype='DISTANCE', + update=update + ) + x_offset = FloatProperty( + name="x offset", + default=0.0, precision=2, step=1, + unit='LENGTH', subtype='DISTANCE', + update=update + ) + nose_z = FloatProperty( + name="Height", + min=0.001, + default=0.03, precision=2, step=1, + unit='LENGTH', subtype='DISTANCE', + update=update + ) + bottom_z = FloatProperty( + name="Stair bottom", + min=0.001, + default=0.03, precision=2, step=1, + unit='LENGTH', subtype='DISTANCE', + update=update + ) + radius = FloatProperty( + name="radius", + min=0.5, + default=0.7, + unit='LENGTH', subtype='DISTANCE', + update=update + ) + da = FloatProperty( + name="angle", + min=-pi, + max=pi, + default=pi / 2, + subtype='ANGLE', unit='ROTATION', + update=update + ) + total_angle = FloatProperty( + name="angle", + min=-50 * pi, + max=50 * pi, + default=2 * pi, + subtype='ANGLE', unit='ROTATION', + update=update + ) + steps_type = EnumProperty( + name="Steps", + items=( + ('CLOSED', 'Closed', '', 0), + ('FULL', 'Full height', '', 1), + ('OPEN', 'Open ', '', 2) + ), + default='CLOSED', + update=update + ) + nose_type = EnumProperty( + name="Nosing", + items=( + ('STRAIGHT', 'Straight', '', 0), + ('OBLIQUE', 'Oblique', '', 1), + ), + default='STRAIGHT', + update=update + ) + left_shape = EnumProperty( + items=( + ('RECTANGLE', 'Straight', '', 0), + ('CIRCLE', 'Curved ', '', 1) + ), + default='RECTANGLE', + update=update + ) + right_shape = EnumProperty( + items=( + ('RECTANGLE', 'Straight', '', 0), + ('CIRCLE', 'Curved ', '', 1) + ), + default='RECTANGLE', + update=update + ) + z_mode = EnumProperty( + name="Interp z", + items=( + ('STANDARD', 'Standard', '', 0), + ('LINEAR', 'Bottom Linear', '', 1), + ('LINEAR_TOP', 'All Linear', '', 2) + ), + default='STANDARD', + update=update + ) + presets = EnumProperty( + items=( + ('STAIR_I', 'I stair', '', 0), + ('STAIR_L', 'L stair', '', 1), + ('STAIR_U', 'U stair', '', 2), + ('STAIR_O', 'O stair', '', 3), + ('STAIR_USER', 'User defined stair', '', 4), + ), + default='STAIR_I', update=update_preset + ) + left_post = BoolProperty( + name='left', + default=True, + update=update + ) + right_post = BoolProperty( + name='right', + default=True, + update=update + ) + post_spacing = FloatProperty( + name="spacing", + min=0.1, + default=1.0, precision=2, step=1, + unit='LENGTH', subtype='DISTANCE', + update=update + ) + post_x = FloatProperty( + name="width", + min=0.001, + default=0.04, precision=2, step=1, + unit='LENGTH', subtype='DISTANCE', + update=update + ) + post_y = FloatProperty( + name="length", + min=0.001, + default=0.04, precision=2, step=1, + unit='LENGTH', subtype='DISTANCE', + update=update + ) + post_z = FloatProperty( + name="height", + min=0.001, + default=1, precision=2, step=1, + unit='LENGTH', subtype='DISTANCE', + update=update + ) + post_alt = FloatProperty( + name="altitude", + min=-100, + default=0, precision=2, step=1, + unit='LENGTH', subtype='DISTANCE', + update=update + ) + post_offset_x = FloatProperty( + name="offset", + min=-100.0, max=100, + default=0.02, precision=2, step=1, + unit='LENGTH', subtype='DISTANCE', + update=update + ) + post_corners = BoolProperty( + name="only on edges", + update=update, + default=False + ) + user_defined_post_enable = BoolProperty( + name="User", + update=update, + default=True + ) + user_defined_post = StringProperty( + name="user defined", + update=update + ) + idmat_post = EnumProperty( + name="Post", + items=materials_enum, + default='4', + update=update + ) + left_subs = BoolProperty( + name='left', + default=False, + update=update + ) + right_subs = BoolProperty( + name='right', + default=False, + update=update + ) + subs_spacing = FloatProperty( + name="spacing", + min=0.05, + default=0.10, precision=2, step=1, + unit='LENGTH', subtype='DISTANCE', + update=update + ) + subs_x = FloatProperty( + name="width", + min=0.001, + default=0.02, precision=2, step=1, + unit='LENGTH', subtype='DISTANCE', + update=update + ) + subs_y = FloatProperty( + name="length", + min=0.001, + default=0.02, precision=2, step=1, + unit='LENGTH', subtype='DISTANCE', + update=update + ) + subs_z = FloatProperty( + name="height", + min=0.001, + default=1, precision=2, step=1, + unit='LENGTH', subtype='DISTANCE', + update=update + ) + subs_alt = FloatProperty( + name="altitude", + min=-100, + default=0, precision=2, step=1, + unit='LENGTH', subtype='DISTANCE', + update=update + ) + subs_offset_x = FloatProperty( + name="offset", + min=-100.0, max=100, + default=0.0, precision=2, step=1, + unit='LENGTH', subtype='DISTANCE', + update=update + ) + subs_bottom = EnumProperty( + name="Bottom", + items=( + ('STEP', 'Follow step', '', 0), + ('LINEAR', 'Linear', '', 1), + ), + default='STEP', + update=update + ) + user_defined_subs_enable = BoolProperty( + name="User", + update=update, + default=True + ) + user_defined_subs = StringProperty( + name="user defined", + update=update + ) + idmat_subs = EnumProperty( + name="Subs", + items=materials_enum, + default='4', + update=update + ) + left_panel = BoolProperty( + name='left', + default=True, + update=update + ) + right_panel = BoolProperty( + name='right', + default=True, + update=update + ) + panel_alt = FloatProperty( + name="altitude", + default=0.25, precision=2, step=1, + unit='LENGTH', subtype='DISTANCE', + update=update + ) + panel_x = FloatProperty( + name="width", + min=0.001, + default=0.01, precision=2, step=1, + unit='LENGTH', subtype='DISTANCE', + update=update + ) + panel_z = FloatProperty( + name="height", + min=0.001, + default=0.6, precision=2, step=1, + unit='LENGTH', subtype='DISTANCE', + update=update + ) + panel_dist = FloatProperty( + name="space", + min=0.001, + default=0.05, precision=2, step=1, + unit='LENGTH', subtype='DISTANCE', + update=update + ) + panel_offset_x = FloatProperty( + name="offset", + default=0.0, precision=2, step=1, + unit='LENGTH', subtype='DISTANCE', + update=update + ) + idmat_panel = EnumProperty( + name="Panels", + items=materials_enum, + default='5', + update=update + ) + left_rail = BoolProperty( + name="left", + update=update, + default=False + ) + right_rail = BoolProperty( + name="right", + update=update, + default=False + ) + rail_n = IntProperty( + name="number", + default=1, + min=0, + max=31, + update=update + ) + rail_x = FloatVectorProperty( + name="width", + default=[ + 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, + 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, + 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, + 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05 + ], + size=31, + min=0.001, + precision=2, step=1, + unit='LENGTH', + update=update + ) + rail_z = FloatVectorProperty( + name="height", + default=[ + 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, + 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, + 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, + 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05 + ], + size=31, + min=0.001, + precision=2, step=1, + unit='LENGTH', + update=update + ) + rail_offset = FloatVectorProperty( + name="offset", + default=[ + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0 + ], + size=31, + precision=2, step=1, + unit='LENGTH', + update=update + ) + rail_alt = FloatVectorProperty( + name="altitude", + default=[ + 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, + 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, + 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, + 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0 + ], + size=31, + precision=2, step=1, + unit='LENGTH', + update=update + ) + rail_mat = CollectionProperty(type=archipack_stair_material) + + left_handrail = BoolProperty( + name="left", + update=update, + default=True + ) + right_handrail = BoolProperty( + name="right", + update=update, + default=True + ) + handrail_offset = FloatProperty( + name="offset", + default=0.0, precision=2, step=1, + unit='LENGTH', subtype='DISTANCE', + update=update + ) + handrail_alt = FloatProperty( + name="altitude", + default=1.0, precision=2, step=1, + unit='LENGTH', subtype='DISTANCE', + update=update + ) + handrail_extend = FloatProperty( + name="extend", + default=0.1, precision=2, step=1, + unit='LENGTH', subtype='DISTANCE', + update=update + ) + handrail_slice_left = BoolProperty( + name='slice', + default=True, + update=update + ) + handrail_slice_right = BoolProperty( + name='slice', + default=True, + update=update + ) + handrail_profil = EnumProperty( + name="Profil", + items=( + ('SQUARE', 'Square', '', 0), + ('CIRCLE', 'Circle', '', 1), + ('COMPLEX', 'Circle over square', '', 2) + ), + default='SQUARE', + update=update + ) + handrail_x = FloatProperty( + name="width", + min=0.001, + default=0.04, precision=2, step=1, + unit='LENGTH', subtype='DISTANCE', + update=update + ) + handrail_y = FloatProperty( + name="height", + min=0.001, + default=0.04, precision=2, step=1, + unit='LENGTH', subtype='DISTANCE', + update=update + ) + handrail_radius = FloatProperty( + name="radius", + min=0.001, + default=0.02, precision=2, step=1, + unit='LENGTH', subtype='DISTANCE', + update=update + ) + + left_string = BoolProperty( + name="left", + update=update, + default=False + ) + right_string = BoolProperty( + name="right", + update=update, + default=False + ) + string_x = FloatProperty( + name="width", + min=-100.0, + default=0.02, precision=2, step=1, + unit='LENGTH', subtype='DISTANCE', + update=update + ) + string_z = FloatProperty( + name="height", + default=0.3, precision=2, step=1, + unit='LENGTH', subtype='DISTANCE', + update=update + ) + string_offset = FloatProperty( + name="offset", + default=0.0, precision=2, step=1, + unit='LENGTH', subtype='DISTANCE', + update=update + ) + string_alt = FloatProperty( + name="altitude", + default=-0.04, precision=2, step=1, + unit='LENGTH', subtype='DISTANCE', + update=update + ) + + idmat_bottom = EnumProperty( + name="Bottom", + items=materials_enum, + default='1', + update=update + ) + idmat_raise = EnumProperty( + name="Raise", + items=materials_enum, + default='1', + update=update + ) + idmat_step_front = EnumProperty( + name="Step front", + items=materials_enum, + default='3', + update=update + ) + idmat_top = EnumProperty( + name="Top", + items=materials_enum, + default='3', + update=update + ) + idmat_side = EnumProperty( + name="Side", + items=materials_enum, + default='1', + update=update + ) + idmat_step_side = EnumProperty( + name="Step Side", + items=materials_enum, + default='3', + update=update + ) + idmat_handrail = EnumProperty( + name="Handrail", + items=materials_enum, + default='3', + update=update + ) + idmat_string = EnumProperty( + name="String", + items=materials_enum, + default='3', + update=update + ) + + # UI layout related + parts_expand = BoolProperty( + default=False + ) + steps_expand = BoolProperty( + default=False + ) + rail_expand = BoolProperty( + default=False + ) + idmats_expand = BoolProperty( + default=False + ) + handrail_expand = BoolProperty( + default=False + ) + string_expand = BoolProperty( + default=False + ) + post_expand = BoolProperty( + default=False + ) + panel_expand = BoolProperty( + default=False + ) + subs_expand = BoolProperty( + default=False + ) + + auto_update = BoolProperty( + options={'SKIP_SAVE'}, + default=True, + update=update_manipulators + ) + + def setup_manipulators(self): + + if len(self.manipulators) == 0: + s = self.manipulators.add() + s.prop1_name = "width" + s = self.manipulators.add() + s.prop1_name = "height" + s.normal = Vector((0, 1, 0)) + + for i in range(self.n_parts): + p = self.parts[i] + n_manips = len(p.manipulators) + if n_manips < 1: + m = p.manipulators.add() + m.type_key = 'SIZE' + m.prop1_name = 'length' + + def update_parts(self): + + # remove rails materials + for i in range(len(self.rail_mat), self.rail_n, -1): + self.rail_mat.remove(i - 1) + + # add rails + for i in range(len(self.rail_mat), self.rail_n): + self.rail_mat.add() + + # remove parts + for i in range(len(self.parts), self.n_parts, -1): + self.parts.remove(i - 1) + + # add parts + for i in range(len(self.parts), self.n_parts): + self.parts.add() + + self.setup_manipulators() + + def update(self, context, manipulable_refresh=False): + + o = self.find_in_selection(context, self.auto_update) + + if o is None: + return + + # clean up manipulators before any data model change + if manipulable_refresh: + self.manipulable_disable(context) + + self.update_parts() + + center = Vector((0, 0)) + verts = [] + faces = [] + matids = [] + uvs = [] + id_materials = [int(self.idmat_top), int(self.idmat_step_front), int(self.idmat_raise), + int(self.idmat_side), int(self.idmat_bottom), int(self.idmat_step_side)] + + # depth at bottom + bottom_z = self.bottom_z + if self.steps_type == 'OPEN': + # depth at front + bottom_z = self.nose_z + + width_left = 0.5 * self.width - self.x_offset + width_right = 0.5 * self.width + self.x_offset + + self.manipulators[0].set_pts([(-width_left, 0, 0), (width_right, 0, 0), (1, 0, 0)]) + self.manipulators[1].set_pts([(0, 0, 0), (0, 0, self.height), (1, 0, 0)]) + + g = StairGenerator(self.parts) + if self.presets == 'STAIR_USER': + for part in self.parts: + g.add_part(part.type, self.steps_type, self.nose_type, self.z_mode, self.nose_z, + bottom_z, center, max(width_left + 0.01, width_right + 0.01, part.radius), part.da, + width_left, width_right, part.length, part.left_shape, part.right_shape) + + elif self.presets == 'STAIR_O': + n_parts = max(1, int(round(abs(self.total_angle) / pi, 0))) + if self.total_angle > 0: + dir = 1 + else: + dir = -1 + last_da = self.total_angle - dir * (n_parts - 1) * pi + if dir * last_da > pi: + n_parts += 1 + last_da -= dir * pi + abs_last = dir * last_da + + for part in range(n_parts - 1): + g.add_part('D_STAIR', self.steps_type, self.nose_type, self.z_mode, self.nose_z, + bottom_z, center, max(width_left + 0.01, width_right + 0.01, self.radius), dir * pi, + width_left, width_right, 1.0, self.left_shape, self.right_shape) + if round(abs_last, 2) > 0: + if abs_last > pi / 2: + g.add_part('C_STAIR', self.steps_type, self.nose_type, self.z_mode, self.nose_z, + bottom_z, center, max(width_left + 0.01, width_right + 0.01, self.radius), + dir * pi / 2, + width_left, width_right, 1.0, self.left_shape, self.right_shape) + g.add_part('C_STAIR', self.steps_type, self.nose_type, self.z_mode, self.nose_z, + bottom_z, center, max(width_left + 0.01, width_right + 0.01, self.radius), + last_da - dir * pi / 2, + width_left, width_right, 1.0, self.left_shape, self.right_shape) + else: + g.add_part('C_STAIR', self.steps_type, self.nose_type, self.z_mode, self.nose_z, + bottom_z, center, max(width_left + 0.01, width_right + 0.01, self.radius), last_da, + width_left, width_right, 1.0, self.left_shape, self.right_shape) + else: + # STAIR_L STAIR_I STAIR_U + for part in self.parts: + g.add_part(part.type, self.steps_type, self.nose_type, self.z_mode, self.nose_z, + bottom_z, center, max(width_left + 0.01, width_right + 0.01, self.radius), self.da, + width_left, width_right, part.length, self.left_shape, self.right_shape) + + # Stair basis + g.set_matids(id_materials) + g.make_stair(self.height, self.step_depth, verts, faces, matids, uvs, nose_y=self.nose_y) + + # Ladder + offset_x = 0.5 * self.width - self.post_offset_x + post_spacing = self.post_spacing + if self.post_corners: + post_spacing = 10000 + + if self.user_defined_post_enable: + # user defined posts + user_def_post = context.scene.objects.get(self.user_defined_post) + if user_def_post is not None and user_def_post.type == 'MESH': + g.setup_user_defined_post(user_def_post, self.post_x, self.post_y, self.post_z) + + if self.left_post: + g.make_post(self.height, self.step_depth, 0.5 * self.post_x, 0.5 * self.post_y, + self.post_z, self.post_alt, 'LEFT', post_spacing, self.post_corners, + self.x_offset, offset_x, int(self.idmat_post), verts, faces, matids, uvs) + + if self.right_post: + g.make_post(self.height, self.step_depth, 0.5 * self.post_x, 0.5 * self.post_y, + self.post_z, self.post_alt, 'RIGHT', post_spacing, self.post_corners, + self.x_offset, offset_x, int(self.idmat_post), verts, faces, matids, uvs) + + # reset user def posts + g.user_defined_post = None + + # user defined subs + if self.user_defined_subs_enable: + user_def_subs = context.scene.objects.get(self.user_defined_subs) + if user_def_subs is not None and user_def_subs.type == 'MESH': + g.setup_user_defined_post(user_def_subs, self.subs_x, self.subs_y, self.subs_z) + + if self.left_subs: + g.make_subs(self.height, self.step_depth, 0.5 * self.subs_x, 0.5 * self.subs_y, + self.subs_z, 0.5 * self.post_y, self.subs_alt, self.subs_bottom, 'LEFT', + self.handrail_slice_left, post_spacing, self.subs_spacing, self.post_corners, + self.x_offset, offset_x, -self.subs_offset_x, int(self.idmat_subs), verts, faces, matids, uvs) + + if self.right_subs: + g.make_subs(self.height, self.step_depth, 0.5 * self.subs_x, 0.5 * self.subs_y, + self.subs_z, 0.5 * self.post_y, self.subs_alt, self.subs_bottom, 'RIGHT', + self.handrail_slice_right, post_spacing, self.subs_spacing, self.post_corners, + self.x_offset, offset_x, self.subs_offset_x, int(self.idmat_subs), verts, faces, matids, uvs) + + g.user_defined_post = None + + if self.left_panel: + g.make_panels(self.height, self.step_depth, 0.5 * self.panel_x, self.panel_z, 0.5 * self.post_y, + self.panel_alt, 'LEFT', post_spacing, self.panel_dist, self.post_corners, + self.x_offset, offset_x, -self.panel_offset_x, int(self.idmat_panel), verts, faces, matids, uvs) + + if self.right_panel: + g.make_panels(self.height, self.step_depth, 0.5 * self.panel_x, self.panel_z, 0.5 * self.post_y, + self.panel_alt, 'RIGHT', post_spacing, self.panel_dist, self.post_corners, + self.x_offset, offset_x, self.panel_offset_x, int(self.idmat_panel), verts, faces, matids, uvs) + + if self.right_rail: + for i in range(self.rail_n): + id_materials = [int(self.rail_mat[i].index) for j in range(6)] + g.set_matids(id_materials) + g.make_part(self.height, self.step_depth, self.rail_x[i], self.rail_z[i], + self.x_offset, offset_x + self.rail_offset[i], + self.rail_alt[i], 'LINEAR', 'CLOSED', verts, faces, matids, uvs) + + if self.left_rail: + for i in range(self.rail_n): + id_materials = [int(self.rail_mat[i].index) for j in range(6)] + g.set_matids(id_materials) + g.make_part(self.height, self.step_depth, self.rail_x[i], self.rail_z[i], + self.x_offset, -offset_x - self.rail_offset[i], + self.rail_alt[i], 'LINEAR', 'CLOSED', verts, faces, matids, uvs) + + if self.handrail_profil == 'COMPLEX': + sx = self.handrail_x + sy = self.handrail_y + handrail = [Vector((sx * x, sy * y)) for x, y in [ + (-0.28, 1.83), (-0.355, 1.77), (-0.415, 1.695), (-0.46, 1.605), (-0.49, 1.51), (-0.5, 1.415), + (-0.49, 1.315), (-0.46, 1.225), (-0.415, 1.135), (-0.355, 1.06), (-0.28, 1.0), (-0.255, 0.925), + (-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), + (0.28, 1.0), (0.355, 1.06), (0.415, 1.135), (0.46, 1.225), (0.49, 1.315), (0.5, 1.415), + (0.49, 1.51), (0.46, 1.605), (0.415, 1.695), (0.355, 1.77), (0.28, 1.83), (0.19, 1.875), + (0.1, 1.905), (0.0, 1.915), (-0.095, 1.905), (-0.19, 1.875)]] + + elif self.handrail_profil == 'SQUARE': + x = 0.5 * self.handrail_x + y = self.handrail_y + handrail = [Vector((-x, y)), Vector((-x, 0)), Vector((x, 0)), Vector((x, y))] + elif self.handrail_profil == 'CIRCLE': + r = self.handrail_radius + handrail = [Vector((r * sin(0.1 * -a * pi), r * (0.5 + cos(0.1 * -a * pi)))) for a in range(0, 20)] + + if self.right_handrail: + g.make_profile(handrail, int(self.idmat_handrail), "RIGHT", self.handrail_slice_right, + self.height, self.step_depth, self.x_offset + offset_x + self.handrail_offset, + self.handrail_alt, self.handrail_extend, verts, faces, matids, uvs) + + if self.left_handrail: + g.make_profile(handrail, int(self.idmat_handrail), "LEFT", self.handrail_slice_left, + self.height, self.step_depth, -self.x_offset + offset_x + self.handrail_offset, + self.handrail_alt, self.handrail_extend, verts, faces, matids, uvs) + + w = 0.5 * self.string_x + h = self.string_z + string = [Vector((-w, 0)), Vector((w, 0)), Vector((w, h)), Vector((-w, h))] + + if self.right_string: + g.make_profile(string, int(self.idmat_string), "RIGHT", False, self.height, self.step_depth, + self.x_offset + 0.5 * self.width + self.string_offset, + self.string_alt, 0, verts, faces, matids, uvs) + + if self.left_string: + g.make_profile(string, int(self.idmat_string), "LEFT", False, self.height, self.step_depth, + -self.x_offset + 0.5 * self.width + self.string_offset, + self.string_alt, 0, verts, faces, matids, uvs) + + bmed.buildmesh(context, o, verts, faces, matids=matids, uvs=uvs, weld=True, clean=True) + + # enable manipulators rebuild + if manipulable_refresh: + self.manipulable_refresh = True + + self.restore_context(context) + + def manipulable_setup(self, context): + """ + TODO: Implement the setup part as per parent object basis + + self.manipulable_disable(context) + o = context.active_object + for m in self.manipulators: + self.manip_stack.append(m.setup(context, o, self)) + + """ + self.manipulable_disable(context) + o = context.active_object + + self.setup_manipulators() + + if self.presets is not 'STAIR_O': + for i, part in enumerate(self.parts): + if i >= self.n_parts: + break + if "S_" in part.type or self.presets in ['STAIR_USER']: + for j, m in enumerate(part.manipulators): + self.manip_stack.append(m.setup(context, o, part)) + + if self.presets in ['STAIR_U', 'STAIR_L']: + self.manip_stack.append(self.parts[1].manipulators[0].setup(context, o, self)) + + for m in self.manipulators: + self.manip_stack.append(m.setup(context, o, self)) + + +class ARCHIPACK_PT_stair(Panel): + bl_idname = "ARCHIPACK_PT_stair" + bl_label = "Stair" + bl_space_type = 'VIEW_3D' + bl_region_type = 'UI' + # bl_context = 'object' + bl_category = 'ArchiPack' + + @classmethod + def poll(cls, context): + return archipack_stair.filter(context.active_object) + + def draw(self, context): + prop = archipack_stair.datablock(context.active_object) + if prop is None: + return + scene = context.scene + layout = self.layout + row = layout.row(align=True) + row.operator('archipack.stair_manipulate', icon='HAND') + row = layout.row(align=True) + row.prop(prop, 'presets', text="") + box = layout.box() + # box.label(text="Styles") + row = box.row(align=True) + # row.menu("ARCHIPACK_MT_stair_preset", text=bpy.types.ARCHIPACK_MT_stair_preset.bl_label) + row.operator("archipack.stair_preset_menu", text=bpy.types.ARCHIPACK_OT_stair_preset_menu.bl_label) + row.operator("archipack.stair_preset", text="", icon='ZOOMIN') + row.operator("archipack.stair_preset", text="", icon='ZOOMOUT').remove_active = True + box = layout.box() + box.prop(prop, 'width') + box.prop(prop, 'height') + box.prop(prop, 'bottom_z') + box.prop(prop, 'x_offset') + # box.prop(prop, 'z_mode') + box = layout.box() + row = box.row() + if prop.parts_expand: + row.prop(prop, 'parts_expand', icon="TRIA_DOWN", icon_only=True, text="Parts", emboss=False) + if prop.presets == 'STAIR_USER': + box.prop(prop, 'n_parts') + if prop.presets != 'STAIR_USER': + row = box.row(align=True) + row.prop(prop, "left_shape", text="") + row.prop(prop, "right_shape", text="") + row = box.row() + row.prop(prop, "radius") + row = box.row() + if prop.presets == 'STAIR_O': + row.prop(prop, 'total_angle') + else: + row.prop(prop, 'da') + if prop.presets != 'STAIR_O': + for i, part in enumerate(prop.parts): + part.draw(layout, context, i, prop.presets == 'STAIR_USER') + else: + row.prop(prop, 'parts_expand', icon="TRIA_RIGHT", icon_only=True, text="Parts", emboss=False) + + box = layout.box() + row = box.row() + if prop.steps_expand: + row.prop(prop, 'steps_expand', icon="TRIA_DOWN", icon_only=True, text="Steps", emboss=False) + box.prop(prop, 'steps_type') + box.prop(prop, 'step_depth') + box.prop(prop, 'nose_type') + box.prop(prop, 'nose_z') + box.prop(prop, 'nose_y') + else: + row.prop(prop, 'steps_expand', icon="TRIA_RIGHT", icon_only=True, text="Steps", emboss=False) + + box = layout.box() + row = box.row(align=True) + if prop.handrail_expand: + row.prop(prop, 'handrail_expand', icon="TRIA_DOWN", icon_only=True, text="Handrail", emboss=False) + else: + row.prop(prop, 'handrail_expand', icon="TRIA_RIGHT", icon_only=True, text="Handrail", emboss=False) + + row.prop(prop, 'left_handrail') + row.prop(prop, 'right_handrail') + + if prop.handrail_expand: + box.prop(prop, 'handrail_alt') + box.prop(prop, 'handrail_offset') + box.prop(prop, 'handrail_extend') + box.prop(prop, 'handrail_profil') + if prop.handrail_profil != 'CIRCLE': + box.prop(prop, 'handrail_x') + box.prop(prop, 'handrail_y') + else: + box.prop(prop, 'handrail_radius') + row = box.row(align=True) + row.prop(prop, 'handrail_slice_left') + row.prop(prop, 'handrail_slice_right') + + box = layout.box() + row = box.row(align=True) + if prop.string_expand: + row.prop(prop, 'string_expand', icon="TRIA_DOWN", icon_only=True, text="String", emboss=False) + else: + row.prop(prop, 'string_expand', icon="TRIA_RIGHT", icon_only=True, text="String", emboss=False) + row.prop(prop, 'left_string') + row.prop(prop, 'right_string') + if prop.string_expand: + box.prop(prop, 'string_x') + box.prop(prop, 'string_z') + box.prop(prop, 'string_alt') + box.prop(prop, 'string_offset') + + box = layout.box() + row = box.row(align=True) + if prop.post_expand: + row.prop(prop, 'post_expand', icon="TRIA_DOWN", icon_only=True, text="Post", emboss=False) + else: + row.prop(prop, 'post_expand', icon="TRIA_RIGHT", icon_only=True, text="Post", emboss=False) + row.prop(prop, 'left_post') + row.prop(prop, 'right_post') + if prop.post_expand: + box.prop(prop, 'post_corners') + if not prop.post_corners: + box.prop(prop, 'post_spacing') + box.prop(prop, 'post_x') + box.prop(prop, 'post_y') + box.prop(prop, 'post_z') + box.prop(prop, 'post_alt') + box.prop(prop, 'post_offset_x') + row = box.row(align=True) + row.prop(prop, 'user_defined_post_enable', text="") + row.prop_search(prop, "user_defined_post", scene, "objects", text="") + + box = layout.box() + row = box.row(align=True) + if prop.subs_expand: + row.prop(prop, 'subs_expand', icon="TRIA_DOWN", icon_only=True, text="Subs", emboss=False) + else: + row.prop(prop, 'subs_expand', icon="TRIA_RIGHT", icon_only=True, text="Subs", emboss=False) + + row.prop(prop, 'left_subs') + row.prop(prop, 'right_subs') + if prop.subs_expand: + box.prop(prop, 'subs_spacing') + box.prop(prop, 'subs_x') + box.prop(prop, 'subs_y') + box.prop(prop, 'subs_z') + box.prop(prop, 'subs_alt') + box.prop(prop, 'subs_offset_x') + box.prop(prop, 'subs_bottom') + row = box.row(align=True) + row.prop(prop, 'user_defined_subs_enable', text="") + row.prop_search(prop, "user_defined_subs", scene, "objects", text="") + + box = layout.box() + row = box.row(align=True) + if prop.panel_expand: + row.prop(prop, 'panel_expand', icon="TRIA_DOWN", icon_only=True, text="Panels", emboss=False) + else: + row.prop(prop, 'panel_expand', icon="TRIA_RIGHT", icon_only=True, text="Panels", emboss=False) + row.prop(prop, 'left_panel') + row.prop(prop, 'right_panel') + if prop.panel_expand: + box.prop(prop, 'panel_dist') + box.prop(prop, 'panel_x') + box.prop(prop, 'panel_z') + box.prop(prop, 'panel_alt') + box.prop(prop, 'panel_offset_x') + + box = layout.box() + row = box.row(align=True) + if prop.rail_expand: + row.prop(prop, 'rail_expand', icon="TRIA_DOWN", icon_only=True, text="Rails", emboss=False) + else: + row.prop(prop, 'rail_expand', icon="TRIA_RIGHT", icon_only=True, text="Rails", emboss=False) + row.prop(prop, 'left_rail') + row.prop(prop, 'right_rail') + if prop.rail_expand: + box.prop(prop, 'rail_n') + for i in range(prop.rail_n): + box = layout.box() + box.label(text="Rail " + str(i + 1)) + box.prop(prop, 'rail_x', index=i) + box.prop(prop, 'rail_z', index=i) + box.prop(prop, 'rail_alt', index=i) + box.prop(prop, 'rail_offset', index=i) + box.prop(prop.rail_mat[i], 'index', text="") + + box = layout.box() + row = box.row() + + if prop.idmats_expand: + row.prop(prop, 'idmats_expand', icon="TRIA_DOWN", icon_only=True, text="Materials", emboss=False) + box.prop(prop, 'idmat_top') + box.prop(prop, 'idmat_side') + box.prop(prop, 'idmat_bottom') + box.prop(prop, 'idmat_step_side') + box.prop(prop, 'idmat_step_front') + box.prop(prop, 'idmat_raise') + box.prop(prop, 'idmat_handrail') + box.prop(prop, 'idmat_panel') + box.prop(prop, 'idmat_post') + box.prop(prop, 'idmat_subs') + box.prop(prop, 'idmat_string') + else: + row.prop(prop, 'idmats_expand', icon="TRIA_RIGHT", icon_only=True, text="Materials", emboss=False) + + +# ------------------------------------------------------------------ +# Define operator class to create object +# ------------------------------------------------------------------ + + +class ARCHIPACK_OT_stair(ArchipackCreateTool, Operator): + bl_idname = "archipack.stair" + bl_label = "Stair" + bl_description = "Create a Stair" + bl_category = 'Archipack' + bl_options = {'REGISTER', 'UNDO'} + + def create(self, context): + m = bpy.data.meshes.new("Stair") + o = bpy.data.objects.new("Stair", m) + d = m.archipack_stair.add() + context.scene.objects.link(o) + o.select = True + context.scene.objects.active = o + self.load_preset(d) + self.add_material(o) + m.auto_smooth_angle = 0.20944 + return o + + # ----------------------------------------------------- + # Execute + # ----------------------------------------------------- + def execute(self, context): + if context.mode == "OBJECT": + bpy.ops.object.select_all(action="DESELECT") + o = self.create(context) + o.location = context.scene.cursor_location + o.select = True + context.scene.objects.active = o + self.manipulate() + return {'FINISHED'} + else: + self.report({'WARNING'}, "Archipack: Option only valid in Object mode") + return {'CANCELLED'} + +# ------------------------------------------------------------------ +# Define operator class to manipulate object +# ------------------------------------------------------------------ + + +class ARCHIPACK_OT_stair_manipulate(Operator): + bl_idname = "archipack.stair_manipulate" + bl_label = "Manipulate" + bl_description = "Manipulate" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(self, context): + return archipack_stair.filter(context.active_object) + + def invoke(self, context, event): + d = archipack_stair.datablock(context.active_object) + d.manipulable_invoke(context) + return {'FINISHED'} + + +# ------------------------------------------------------------------ +# Define operator class to load / save presets +# ------------------------------------------------------------------ + + +class ARCHIPACK_OT_stair_preset_menu(PresetMenuOperator, Operator): + bl_idname = "archipack.stair_preset_menu" + bl_label = "Stair style" + preset_subdir = "archipack_stair" + + +class ARCHIPACK_OT_stair_preset(ArchipackPreset, Operator): + """Add a Stair Preset""" + bl_idname = "archipack.stair_preset" + bl_label = "Add Stair Style" + preset_menu = "ARCHIPACK_OT_stair_preset_menu" + + @property + def blacklist(self): + return ['manipulators'] + + """ + 'presets', 'n_parts', 'parts', 'width', 'height', 'radius', + 'total_angle', 'da', + """ + + +def register(): + bpy.utils.register_class(archipack_stair_material) + bpy.utils.register_class(archipack_stair_part) + bpy.utils.register_class(archipack_stair) + Mesh.archipack_stair = CollectionProperty(type=archipack_stair) + bpy.utils.register_class(ARCHIPACK_PT_stair) + bpy.utils.register_class(ARCHIPACK_OT_stair) + bpy.utils.register_class(ARCHIPACK_OT_stair_preset_menu) + bpy.utils.register_class(ARCHIPACK_OT_stair_preset) + bpy.utils.register_class(ARCHIPACK_OT_stair_manipulate) + + +def unregister(): + bpy.utils.unregister_class(archipack_stair_material) + bpy.utils.unregister_class(archipack_stair_part) + bpy.utils.unregister_class(archipack_stair) + del Mesh.archipack_stair + bpy.utils.unregister_class(ARCHIPACK_PT_stair) + bpy.utils.unregister_class(ARCHIPACK_OT_stair) + bpy.utils.unregister_class(ARCHIPACK_OT_stair_preset_menu) + bpy.utils.unregister_class(ARCHIPACK_OT_stair_preset) + bpy.utils.unregister_class(ARCHIPACK_OT_stair_manipulate) diff --git a/archipack/archipack_truss.py b/archipack/archipack_truss.py new file mode 100644 index 00000000..b8056daa --- /dev/null +++ b/archipack/archipack_truss.py @@ -0,0 +1,380 @@ +# -*- coding:utf-8 -*- + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110- 1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +# + +# ---------------------------------------------------------- +# Author: Stephen Leger (s-leger) +# +# ---------------------------------------------------------- +import bpy +from bpy.types import Operator, PropertyGroup, Mesh, Panel +from bpy.props import ( + FloatProperty, IntProperty, BoolProperty, + CollectionProperty, EnumProperty +) +from .bmesh_utils import BmeshEdit as bmed +# from .materialutils import MaterialUtils +from mathutils import Vector, Matrix +from math import sin, cos, pi +from .archipack_manipulator import Manipulable +from .archipack_object import ArchipackCreateTool, ArchipackObject + + +def update(self, context): + self.update(context) + + +class archipack_truss(ArchipackObject, Manipulable, PropertyGroup): + truss_type = EnumProperty( + name="Type", + items=( + ('1', 'Prolyte E20', 'Prolyte E20', 0), + ('2', 'Prolyte X30', 'Prolyte X30', 1), + ('3', 'Prolyte H30', 'Prolyte H30', 2), + ('4', 'Prolyte H40', 'Prolyte H40', 3), + ('5', 'OPTI Trilite 100', 'OPTI Trilite 100', 4), + ('6', 'OPTI Trilite 200', 'OPTI Trilite 200', 5), + ('7', 'User defined', 'User defined', 6) + ), + default='2', + update=update + ) + z = FloatProperty( + name="Height", + default=2.0, min=0.01, + unit='LENGTH', subtype='DISTANCE', + update=update + ) + segs = IntProperty( + name="Segs", + default=6, min=3, + update=update + ) + master_segs = IntProperty( + name="Master Segs", + default=1, min=1, + update=update + ) + master_count = IntProperty( + name="Masters", + default=3, min=2, + update=update + ) + entre_axe = FloatProperty( + name="Distance", + default=0.239, min=0.001, + unit='LENGTH', subtype='DISTANCE', + update=update + ) + master_radius = FloatProperty( + name="Radius", + default=0.02415, min=0.0001, + unit='LENGTH', subtype='DISTANCE', + update=update + ) + slaves_radius = FloatProperty( + name="Subs radius", + default=0.01, min=0.0001, + unit='LENGTH', subtype='DISTANCE', + update=update + ) + # Flag to prevent mesh update while making bulk changes over variables + # use : + # .auto_update = False + # bulk changes + # .auto_update = True + auto_update = BoolProperty( + options={'SKIP_SAVE'}, + default=True, + update=update + ) + + def setup_manipulators(self): + if len(self.manipulators) < 1: + s = self.manipulators.add() + s.prop1_name = "z" + s.type_key = 'SIZE' + s.normal = Vector((0, 1, 0)) + + def docylinder(self, faces, verts, radius, segs, tMt, tMb, tM, add=False): + segs_step = 2 * pi / segs + tmpverts = [0 for i in range(segs)] + if add: + cv = len(verts) - segs + else: + cv = len(verts) + for seg in range(segs): + seg_angle = pi / 4 + seg * segs_step + tmpverts[seg] = radius * Vector((sin(seg_angle), -cos(seg_angle), 0)) + + if not add: + for seg in range(segs): + verts.append(tM * tMb * tmpverts[seg]) + + for seg in range(segs): + verts.append(tM * tMt * tmpverts[seg]) + + for seg in range(segs - 1): + f = cv + seg + faces.append((f + 1, f, f + segs, f + segs + 1)) + f = cv + faces.append((f, f + segs - 1, f + 2 * segs - 1, f + segs)) + + def update(self, context): + + o = self.find_in_selection(context, self.auto_update) + + if o is None: + return + + self.setup_manipulators() + + if self.truss_type == '1': + EntreAxe = 0.19 + master_radius = 0.016 + slaves_radius = 0.005 + elif self.truss_type == '2': + EntreAxe = 0.239 + master_radius = 0.0255 + slaves_radius = 0.008 + elif self.truss_type == '3': + EntreAxe = 0.239 + master_radius = 0.02415 + slaves_radius = 0.008 + elif self.truss_type == '4': + EntreAxe = 0.339 + master_radius = 0.02415 + slaves_radius = 0.01 + elif self.truss_type == '5': + EntreAxe = 0.15 + master_radius = 0.0127 + slaves_radius = 0.004 + elif self.truss_type == '6': + EntreAxe = 0.200 + master_radius = 0.0254 + slaves_radius = 0.00635 + elif self.truss_type == '7': + EntreAxe = self.entre_axe + master_radius = min(0.5 * self.entre_axe, self.master_radius) + slaves_radius = min(0.5 * self.entre_axe, self.master_radius, self.slaves_radius) + + master_sepang = (pi * (self.master_count - 2) / self.master_count) / 2 + radius = (EntreAxe / 2) / cos(master_sepang) + master_step = pi * 2 / self.master_count + + verts = [] + faces = [] + + if self.master_count == 4: + master_rotation = pi / 4 # 45.0 + else: + master_rotation = 0.0 + + slaves_width = 2 * radius * sin(master_step / 2) + slaves_count = int(self.z / slaves_width) + slave_firstOffset = (self.z - slaves_count * slaves_width) / 2 + master_z = self.z / self.master_segs + + for master in range(self.master_count): + + master_angle = master_rotation + master * master_step + + tM = Matrix([ + [1, 0, 0, radius * sin(master_angle)], + [0, 1, 0, radius * -cos(master_angle)], + [0, 0, 1, 0], + [0, 0, 0, 1]]) + + tMb = Matrix([ + [1, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, 1, self.z], + [0, 0, 0, 1]]) + + for n in range(1, self.master_segs + 1): + tMt = Matrix([ + [1, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, 1, self.z - n * master_z], + [0, 0, 0, 1]]) + self.docylinder(faces, verts, master_radius, self.segs, tMt, tMb, tM, add=(n > 1)) + + if self.master_count < 3 and master == 1: + continue + + ma = master_angle + master_sepang + + tM = Matrix([ + [cos(ma), sin(ma), 0, radius * sin(master_angle)], + [sin(ma), -cos(ma), 0, radius * -cos(master_angle)], + [0, 0, 1, slave_firstOffset], + [0, 0, 0, 1]]) + + if int(self.truss_type) < 5: + tMb = Matrix([ + [1, 0, 0, 0], + [0, 0, 1, 0], + [0, 1, 0, 0], + [0, 0, 0, 1]]) + tMt = Matrix([ + [1, 0, 0, 0], + [0, 0, 1, -slaves_width], + [0, 1, 0, 0], + [0, 0, 0, 1]]) + self.docylinder(faces, verts, slaves_radius, self.segs, tMt, tMb, tM) + + tMb = Matrix([ + [1, 0, 0, 0], + [0, 1.4142, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 1]]) + + for n in range(1, slaves_count + 1): + tMt = Matrix([ + [1, 0, 0, 0], + [0, 1.4142, 0, -(n % 2) * slaves_width], + [0, 0, 1, n * slaves_width], + [0, 0, 0, 1]]) + self.docylinder(faces, verts, slaves_radius, self.segs, tMt, tMb, tM, add=(n > 1)) + + if int(self.truss_type) < 5: + tMb = Matrix([ + [1, 0, 0, 0], + [0, 0, 1, 0], + [0, 1, 0, slaves_count * slaves_width], + [0, 0, 0, 1]]) + tMt = Matrix([ + [1, 0, 0, 0], + [0, 0, 1, -slaves_width], + [0, 1, 0, slaves_count * slaves_width], + [0, 0, 0, 1]]) + self.docylinder(faces, verts, slaves_radius, self.segs, tMt, tMb, tM) + + bmed.buildmesh(context, o, verts, faces, matids=None, uvs=None, weld=False) + self.manipulators[0].set_pts([(0, 0, 0), (0, 0, self.z), (1, 0, 0)]) + + self.restore_context(context) + + +class ARCHIPACK_PT_truss(Panel): + """Archipack Truss""" + bl_idname = "ARCHIPACK_PT_truss" + bl_label = "Truss" + bl_space_type = 'VIEW_3D' + bl_region_type = 'UI' + bl_category = 'ArchiPack' + + @classmethod + def poll(cls, context): + return archipack_truss.filter(context.active_object) + + def draw(self, context): + prop = archipack_truss.datablock(context.active_object) + if prop is None: + return + layout = self.layout + row = layout.row(align=True) + row.operator('archipack.truss_manipulate', icon='HAND') + box = layout.box() + box.prop(prop, 'truss_type') + box.prop(prop, 'z') + box.prop(prop, 'segs') + box.prop(prop, 'master_segs') + box.prop(prop, 'master_count') + if prop.truss_type == '7': + box.prop(prop, 'master_radius') + box.prop(prop, 'slaves_radius') + box.prop(prop, 'entre_axe') + + +class ARCHIPACK_OT_truss(ArchipackCreateTool, Operator): + bl_idname = "archipack.truss" + bl_label = "Truss" + bl_description = "Create Truss" + bl_category = 'Archipack' + bl_options = {'REGISTER', 'UNDO'} + + def create(self, context): + m = bpy.data.meshes.new("Truss") + o = bpy.data.objects.new("Truss", m) + d = m.archipack_truss.add() + # make manipulators selectable + # d.manipulable_selectable = True + context.scene.objects.link(o) + o.select = True + context.scene.objects.active = o + self.load_preset(d) + self.add_material(o) + m.auto_smooth_angle = 1.15 + return o + + # ----------------------------------------------------- + # Execute + # ----------------------------------------------------- + def execute(self, context): + if context.mode == "OBJECT": + bpy.ops.object.select_all(action="DESELECT") + o = self.create(context) + o.location = bpy.context.scene.cursor_location + o.select = True + context.scene.objects.active = o + self.manipulate() + return {'FINISHED'} + else: + self.report({'WARNING'}, "Archipack: Option only valid in Object mode") + return {'CANCELLED'} + + +# ------------------------------------------------------------------ +# Define operator class to manipulate object +# ------------------------------------------------------------------ + + +class ARCHIPACK_OT_truss_manipulate(Operator): + bl_idname = "archipack.truss_manipulate" + bl_label = "Manipulate" + bl_description = "Manipulate" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(self, context): + return archipack_truss.filter(context.active_object) + + def invoke(self, context, event): + d = archipack_truss.datablock(context.active_object) + d.manipulable_invoke(context) + return {'FINISHED'} + + +def register(): + bpy.utils.register_class(archipack_truss) + Mesh.archipack_truss = CollectionProperty(type=archipack_truss) + bpy.utils.register_class(ARCHIPACK_PT_truss) + bpy.utils.register_class(ARCHIPACK_OT_truss) + bpy.utils.register_class(ARCHIPACK_OT_truss_manipulate) + + +def unregister(): + bpy.utils.unregister_class(archipack_truss) + del Mesh.archipack_truss + bpy.utils.unregister_class(ARCHIPACK_PT_truss) + bpy.utils.unregister_class(ARCHIPACK_OT_truss) + bpy.utils.unregister_class(ARCHIPACK_OT_truss_manipulate) diff --git a/archipack/archipack_wall.py b/archipack/archipack_wall.py new file mode 100644 index 00000000..5adf92c2 --- /dev/null +++ b/archipack/archipack_wall.py @@ -0,0 +1,137 @@ +# -*- coding:utf-8 -*- + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110- 1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +# + +# ---------------------------------------------------------- +# Author: Stephen Leger (s-leger) +# +# ---------------------------------------------------------- +import bpy +import bmesh +from bpy.types import Operator, PropertyGroup, Mesh, Panel +from bpy.props import FloatProperty, CollectionProperty +from .archipack_object import ArchipackObject + + +def update_wall(self, context): + self.update(context) + + +class archipack_wall(ArchipackObject, PropertyGroup): + z = FloatProperty( + name='height', + min=0.1, max=10000, + default=2.7, precision=2, + description='height', update=update_wall, + ) + + def update(self, context): + # update height via bmesh to avoid loosing material ids + # this should be the rule for other simple objects + # as long as there is no topologic changes + o = context.active_object + if archipack_wall.datablock(o) != self: + return + bpy.ops.object.mode_set(mode='EDIT') + me = o.data + bm = bmesh.from_edit_mesh(me) + bm.verts.ensure_lookup_table() + bm.faces.ensure_lookup_table() + new_z = self.z + last_z = list(v.co.z for v in bm.verts) + max_z = max(last_z) + for v in bm.verts: + if v.co.z == max_z: + v.co.z = new_z + bmesh.update_edit_mesh(me, True) + bpy.ops.object.mode_set(mode='OBJECT') + + +class ARCHIPACK_PT_wall(Panel): + bl_idname = "ARCHIPACK_PT_wall" + bl_label = "Wall" + bl_space_type = 'VIEW_3D' + bl_region_type = 'UI' + bl_category = 'ArchiPack' + + @classmethod + def poll(cls, context): + return archipack_wall.filter(context.active_object) + + def draw(self, context): + + prop = archipack_wall.datablock(context.active_object) + if prop is None: + return + layout = self.layout + layout.prop(prop, 'z') + + +# ------------------------------------------------------------------ +# Define operator class to create object +# ------------------------------------------------------------------ + + +class ARCHIPACK_OT_wall(Operator): + bl_idname = "archipack.wall" + bl_label = "Wall" + bl_description = "Add wall parameters to active object" + bl_category = 'Archipack' + bl_options = {'REGISTER', 'UNDO'} + z = FloatProperty( + name="z", + default=2.7 + ) + + @classmethod + def poll(cls, context): + return context.active_object is not None + + def draw(self, context): + layout = self.layout + row = layout.row() + row.label("Use Properties panel (N) to define parms", icon='INFO') + + def execute(self, context): + if context.mode == "OBJECT": + o = context.active_object + if archipack_wall.filter(o): + return {'CANCELLED'} + params = o.data.archipack_wall.add() + params.z = self.z + return {'FINISHED'} + else: + self.report({'WARNING'}, "Archipack: Option only valid in Object mode") + return {'CANCELLED'} + + +def register(): + bpy.utils.register_class(archipack_wall) + Mesh.archipack_wall = CollectionProperty(type=archipack_wall) + bpy.utils.register_class(ARCHIPACK_PT_wall) + bpy.utils.register_class(ARCHIPACK_OT_wall) + + +def unregister(): + bpy.utils.unregister_class(archipack_wall) + del Mesh.archipack_wall + bpy.utils.unregister_class(ARCHIPACK_PT_wall) + bpy.utils.unregister_class(ARCHIPACK_OT_wall) diff --git a/archipack/archipack_wall2.py b/archipack/archipack_wall2.py new file mode 100644 index 00000000..4944f59f --- /dev/null +++ b/archipack/archipack_wall2.py @@ -0,0 +1,2220 @@ +# -*- coding:utf-8 -*- + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110- 1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +# + +# ---------------------------------------------------------- +# Author: Stephen Leger (s-leger) +# +# ---------------------------------------------------------- +import bpy +# import time +from bpy.types import Operator, PropertyGroup, Mesh, Panel +from bpy.props import ( + FloatProperty, BoolProperty, IntProperty, StringProperty, + FloatVectorProperty, CollectionProperty, EnumProperty +) +from .bmesh_utils import BmeshEdit as bmed +from mathutils import Vector, Matrix +from mathutils.geometry import ( + interpolate_bezier + ) +from math import sin, cos, pi, atan2 +from .archipack_manipulator import ( + Manipulable, archipack_manipulator, + GlPolygon, GlPolyline, + GlLine, GlText, FeedbackPanel + ) +from .archipack_object import ArchipackObject, ArchipackCreateTool, ArchpackDrawTool +from .archipack_2d import Line, Arc +from .archipack_snap import snap_point +from .archipack_keymaps import Keymaps + + +class Wall(): + def __init__(self, wall_z, z, t, flip): + self.z = z + self.wall_z = wall_z + self.t = t + self.flip = flip + self.z_step = len(z) + + def get_z(self, t): + t0 = self.t[0] + z0 = self.z[0] + for i in range(1, self.z_step): + t1 = self.t[i] + z1 = self.z[i] + if t <= t1: + return z0 + (t - t0) / (t1 - t0) * (z1 - z0) + t0, z0 = t1, z1 + return self.z[-1] + + def make_faces(self, i, f, faces): + if i < self.n_step: + # 1 3 5 7 + # 0 2 4 6 + if self.flip: + faces.append((f + 2, f, f + 1, f + 3)) + else: + faces.append((f, f + 2, f + 3, f + 1)) + + def p3d(self, verts, t): + x, y = self.lerp(t) + z = self.wall_z + self.get_z(t) + verts.append((x, y, 0)) + verts.append((x, y, z)) + + def make_wall(self, i, verts, faces): + t = self.t_step[i] + f = len(verts) + self.p3d(verts, t) + self.make_faces(i, f, faces) + + def straight_wall(self, a0, length, wall_z, z, t): + r = self.straight(length).rotate(a0) + return StraightWall(r.p, r.v, wall_z, z, t, self.flip) + + def curved_wall(self, a0, da, radius, wall_z, z, t): + n = self.normal(1).rotate(a0).scale(radius) + if da < 0: + n.v = -n.v + a0 = n.angle + c = n.p - n.v + return CurvedWall(c, radius, a0, da, wall_z, z, t, self.flip) + + +class StraightWall(Wall, Line): + def __init__(self, p, v, wall_z, z, t, flip): + Line.__init__(self, p, v) + Wall.__init__(self, wall_z, z, t, flip) + + def param_t(self, step_angle): + self.t_step = self.t + self.n_step = len(self.t) - 1 + + +class CurvedWall(Wall, Arc): + def __init__(self, c, radius, a0, da, wall_z, z, t, flip): + Arc.__init__(self, c, radius, a0, da) + Wall.__init__(self, wall_z, z, t, flip) + + def param_t(self, step_angle): + t_step, n_step = self.steps_by_angle(step_angle) + self.t_step = list(sorted([i * t_step for i in range(1, n_step)] + self.t)) + self.n_step = len(self.t_step) - 1 + + +class WallGenerator(): + def __init__(self, parts): + self.last_type = 'NONE' + self.segs = [] + self.parts = parts + self.faces_type = 'NONE' + self.closed = False + + def add_part(self, part, wall_z, flip): + + # TODO: + # refactor this part (height manipulators) + manip_index = [] + if len(self.segs) < 1: + s = None + z = [part.z[0]] + manip_index.append(0) + else: + s = self.segs[-1] + z = [s.z[-1]] + + t_cur = 0 + z_last = part.n_splits - 1 + t = [0] + + for i in range(part.n_splits): + t_try = t[-1] + part.t[i] + if t_try == t_cur: + continue + if t_try <= 1: + t_cur = t_try + t.append(t_cur) + z.append(part.z[i]) + manip_index.append(i) + else: + z_last = i + break + + if t_cur < 1: + t.append(1) + manip_index.append(z_last) + z.append(part.z[z_last]) + + # start a new wall + if s is None: + if part.type == 'S_WALL': + p = Vector((0, 0)) + v = part.length * Vector((cos(part.a0), sin(part.a0))) + s = StraightWall(p, v, wall_z, z, t, flip) + elif part.type == 'C_WALL': + c = -part.radius * Vector((cos(part.a0), sin(part.a0))) + s = CurvedWall(c, part.radius, part.a0, part.da, wall_z, z, t, flip) + else: + if part.type == 'S_WALL': + s = s.straight_wall(part.a0, part.length, wall_z, z, t) + elif part.type == 'C_WALL': + s = s.curved_wall(part.a0, part.da, part.radius, wall_z, z, t) + + self.segs.append(s) + self.last_type = part.type + + return manip_index + + def close(self, closed): + # Make last segment implicit closing one + if closed: + part = self.parts[-1] + w = self.segs[-1] + dp = self.segs[0].p0 - self.segs[-1].p0 + if "C_" in part.type: + dw = (w.p1 - w.p0) + w.r = part.radius / dw.length * dp.length + # angle pt - p0 - angle p0 p1 + da = atan2(dp.y, dp.x) - atan2(dw.y, dw.x) + a0 = w.a0 + da + if a0 > pi: + a0 -= 2 * pi + if a0 < -pi: + a0 += 2 * pi + w.a0 = a0 + else: + w.v = dp + + def make_wall(self, step_angle, flip, closed, verts, faces): + + # swap manipulators so they always face outside + side = 1 + if flip: + side = -1 + + # Make last segment implicit closing one + + nb_segs = len(self.segs) - 1 + if closed: + nb_segs += 1 + + for i, wall in enumerate(self.segs): + + manipulators = self.parts[i].manipulators + + p0 = wall.p0.to_3d() + p1 = wall.p1.to_3d() + + # angle from last to current segment + if i > 0: + + if i < len(self.segs) - 1: + manipulators[0].type_key = 'ANGLE' + else: + manipulators[0].type_key = 'DUMB_ANGLE' + + v0 = self.segs[i - 1].straight(-side, 1).v.to_3d() + v1 = wall.straight(side, 0).v.to_3d() + manipulators[0].set_pts([p0, v0, v1]) + + if type(wall).__name__ == "StraightWall": + # segment length + manipulators[1].type_key = 'SIZE' + manipulators[1].prop1_name = "length" + manipulators[1].set_pts([p0, p1, (side, 0, 0)]) + else: + # segment radius + angle + # scale to fix overlap with drag + v0 = side * (wall.p0 - wall.c).to_3d() + v1 = side * (wall.p1 - wall.c).to_3d() + scale = 1.0 + (0.5 / v0.length) + manipulators[1].type_key = 'ARC_ANGLE_RADIUS' + manipulators[1].prop1_name = "da" + manipulators[1].prop2_name = "radius" + manipulators[1].set_pts([wall.c.to_3d(), scale * v0, scale * v1]) + + # snap manipulator, dont change index ! + manipulators[2].set_pts([p0, p1, (1, 0, 0)]) + + # dumb, segment index + z = Vector((0, 0, 0.75 * wall.wall_z)) + manipulators[3].set_pts([p0 + z, p1 + z, (1, 0, 0)]) + + wall.param_t(step_angle) + if i < nb_segs: + for j in range(wall.n_step + 1): + wall.make_wall(j, verts, faces) + else: + # last segment + for j in range(wall.n_step): + continue + # print("%s" % (wall.n_step)) + # wall.make_wall(j, verts, faces) + + def rotate(self, idx_from, a): + """ + apply rotation to all following segs + """ + self.segs[idx_from].rotate(a) + ca = cos(a) + sa = sin(a) + rM = Matrix([ + [ca, -sa], + [sa, ca] + ]) + # rotation center + p0 = self.segs[idx_from].p0 + for i in range(idx_from + 1, len(self.segs)): + seg = self.segs[i] + seg.rotate(a) + dp = rM * (seg.p0 - p0) + seg.translate(dp) + + def translate(self, idx_from, dp): + """ + apply translation to all following segs + """ + self.segs[idx_from].p1 += dp + for i in range(idx_from + 1, len(self.segs)): + self.segs[i].translate(dp) + + def draw(self, context): + for seg in self.segs: + seg.draw(context, render=False) + + def debug(self, verts): + for wall in self.segs: + for i in range(33): + x, y = wall.lerp(i / 32) + verts.append((x, y, 0)) + + +def update(self, context): + self.update(context) + + +def update_childs(self, context): + self.update(context, update_childs=True, manipulable_refresh=True) + + +def update_manipulators(self, context): + self.update(context, manipulable_refresh=True) + + +def update_t_part(self, context): + """ + Make this wall a T child of parent wall + orient child so y points inside wall and x follow wall segment + set child a0 according + """ + o = self.find_in_selection(context) + if o is not None: + + # w is parent wall + w = context.scene.objects.get(self.t_part) + wd = archipack_wall2.datablock(w) + + if wd is not None: + og = self.get_generator() + self.setup_childs(o, og) + + bpy.ops.object.select_all(action="DESELECT") + + # 5 cases here: + # 1 No parents at all + # 2 o has parent + # 3 w has parent + # 4 o and w share same parent allready + # 5 o and w dosent share parent + link_to_parent = False + + # when both walls do have a reference point, we may delete one of them + to_delete = None + + # select childs and make parent reference point active + if w.parent is None: + # Either link to o.parent or create new parent + link_to_parent = True + if o.parent is None: + # create a reference point and make it active + x, y, z = w.bound_box[0] + context.scene.cursor_location = w.matrix_world * Vector((x, y, z)) + # fix issue #9 + context.scene.objects.active = o + bpy.ops.archipack.reference_point() + o.select = True + else: + context.scene.objects.active = o.parent + w.select = True + else: + # w has parent + if o.parent is not w.parent: + link_to_parent = True + context.scene.objects.active = w.parent + o.select = True + if o.parent is not None: + # store o.parent to delete it + to_delete = o.parent + for c in o.parent.children: + if c is not o: + c.hide_select = False + c.select = True + + parent = context.active_object + + dmax = 2 * wd.width + + wg = wd.get_generator() + + otM = o.matrix_world + orM = Matrix([ + otM[0].to_2d(), + otM[1].to_2d() + ]) + + wtM = w.matrix_world + wrM = Matrix([ + wtM[0].to_2d(), + wtM[1].to_2d() + ]) + + # dir in absolute world coordsys + dir = orM * og.segs[0].straight(1, 0).v + + # pt in w coordsys + pos = otM.translation + pt = (wtM.inverted() * pos).to_2d() + + for wall_idx, wall in enumerate(wg.segs): + res, dist, t = wall.point_sur_segment(pt) + # outside is on the right side of the wall + # p1 + # |-- x + # p0 + + # NOTE: + # rotation here is wrong when w has not parent while o has parent + + if res and t > 0 and t < 1 and abs(dist) < dmax: + x = wrM * wall.straight(1, t).v + y = wrM * wall.normal(t).v.normalized() + self.parts[0].a0 = dir.angle_signed(x) + o.matrix_world = Matrix([ + [x.x, -y.x, 0, pos.x], + [x.y, -y.y, 0, pos.y], + [0, 0, 1, pos.z], + [0, 0, 0, 1] + ]) + break + + if link_to_parent and bpy.ops.archipack.parent_to_reference.poll(): + bpy.ops.archipack.parent_to_reference('INVOKE_DEFAULT') + + # update generator to take new rotation in account + # use this to relocate windows on wall after reparenting + g = self.get_generator() + self.relocate_childs(context, o, g) + + # hide holes from select + for c in parent.children: + if "archipack_hybridhole" in c: + c.hide_select = True + + # delete unneeded reference point + if to_delete is not None: + bpy.ops.object.select_all(action="DESELECT") + to_delete.select = True + context.scene.objects.active = to_delete + if bpy.ops.object.delete.poll(): + bpy.ops.object.delete(use_global=False) + + elif self.t_part != "": + self.t_part = "" + + self.restore_context(context) + + +def set_splits(self, value): + if self.n_splits != value: + self.auto_update = False + self._set_t(value) + self.auto_update = True + self.n_splits = value + return None + + +def get_splits(self): + return self.n_splits + + +def update_type(self, context): + + d = self.find_datablock_in_selection(context) + + if d is not None and d.auto_update: + + d.auto_update = False + idx = 0 + for i, part in enumerate(d.parts): + if part == self: + idx = i + break + a0 = 0 + if idx > 0: + g = d.get_generator() + w0 = g.segs[idx - 1] + a0 = w0.straight(1).angle + if "C_" in self.type: + w = w0.straight_wall(self.a0, self.length, d.z, self.z, self.t) + else: + w = w0.curved_wall(self.a0, self.da, self.radius, d.z, self.z, self.t) + else: + g = WallGenerator(None) + g.add_part(self, d.z, d.flip) + w = g.segs[0] + # w0 - w - w1 + dp = w.p1 - w.p0 + if "C_" in self.type: + self.radius = 0.5 * dp.length + self.da = pi + a0 = atan2(dp.y, dp.x) - pi / 2 - a0 + else: + self.length = dp.length + a0 = atan2(dp.y, dp.x) - a0 + + if a0 > pi: + a0 -= 2 * pi + if a0 < -pi: + a0 += 2 * pi + self.a0 = a0 + + if idx + 1 < d.n_parts: + # adjust rotation of next part + part1 = d.parts[idx + 1] + if "C_" in self.type: + a0 = part1.a0 - pi / 2 + else: + a0 = part1.a0 + w.straight(1).angle - atan2(dp.y, dp.x) + + if a0 > pi: + a0 -= 2 * pi + if a0 < -pi: + a0 += 2 * pi + part1.a0 = a0 + + d.auto_update = True + + +class archipack_wall2_part(PropertyGroup): + type = EnumProperty( + items=( + ('S_WALL', 'Straight', '', 0), + ('C_WALL', 'Curved', '', 1) + ), + default='S_WALL', + update=update_type + ) + length = FloatProperty( + name="length", + min=0.01, + default=2.0, + unit='LENGTH', subtype='DISTANCE', + update=update + ) + radius = FloatProperty( + name="radius", + min=0.5, + default=0.7, + unit='LENGTH', subtype='DISTANCE', + update=update + ) + a0 = FloatProperty( + name="start angle", + min=-pi, + max=pi, + default=pi / 2, + subtype='ANGLE', unit='ROTATION', + update=update + ) + da = FloatProperty( + name="angle", + min=-pi, + max=pi, + default=pi / 2, + subtype='ANGLE', unit='ROTATION', + update=update + ) + z = FloatVectorProperty( + name="height", + min=0, + default=[ + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0 + ], + size=31, + update=update + ) + t = FloatVectorProperty( + name="position", + min=0, + max=1, + default=[ + 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1 + ], + size=31, + update=update + ) + splits = IntProperty( + name="splits", + default=1, + min=1, + max=31, + get=get_splits, set=set_splits + ) + n_splits = IntProperty( + name="splits", + default=1, + min=1, + max=31, + update=update + ) + auto_update = BoolProperty(default=True) + manipulators = CollectionProperty(type=archipack_manipulator) + # ui related + expand = BoolProperty(default=False) + + def _set_t(self, splits): + t = 1 / splits + for i in range(splits): + self.t[i] = t + + def find_datablock_in_selection(self, context): + """ + find witch selected object this instance belongs to + provide support for "copy to selected" + """ + selected = [o for o in context.selected_objects] + for o in selected: + props = archipack_wall2.datablock(o) + if props: + for part in props.parts: + if part == self: + return props + return None + + def update(self, context, manipulable_refresh=False): + if not self.auto_update: + return + props = self.find_datablock_in_selection(context) + if props is not None: + props.update(context, manipulable_refresh) + + def draw(self, layout, context, index): + + row = layout.row(align=True) + if self.expand: + row.prop(self, 'expand', icon="TRIA_DOWN", icon_only=True, text="Part " + str(index + 1), emboss=False) + else: + row.prop(self, 'expand', icon="TRIA_RIGHT", icon_only=True, text="Part " + str(index + 1), emboss=False) + + row.prop(self, "type", text="") + + if self.expand: + row = layout.row(align=True) + row.operator("archipack.wall2_insert", text="Split").index = index + row.operator("archipack.wall2_remove", text="Remove").index = index + if self.type == 'C_WALL': + row = layout.row() + row.prop(self, "radius") + row = layout.row() + row.prop(self, "da") + else: + row = layout.row() + row.prop(self, "length") + row = layout.row() + row.prop(self, "a0") + row = layout.row() + row.prop(self, "splits") + for split in range(self.n_splits): + row = layout.row() + row.prop(self, "z", text="alt", index=split) + row.prop(self, "t", text="pos", index=split) + + +class archipack_wall2_child(PropertyGroup): + # Size Loc + # Delta Loc + manipulators = CollectionProperty(type=archipack_manipulator) + child_name = StringProperty() + wall_idx = IntProperty() + pos = FloatVectorProperty(subtype='XYZ') + flip = BoolProperty(default=False) + + def get_child(self, context): + d = None + child = context.scene.objects.get(self.child_name) + if child is not None and child.data is not None: + cd = child.data + if 'archipack_window' in cd: + d = cd.archipack_window[0] + elif 'archipack_door' in cd: + d = cd.archipack_door[0] + return child, d + + +class archipack_wall2(ArchipackObject, Manipulable, PropertyGroup): + parts = CollectionProperty(type=archipack_wall2_part) + n_parts = IntProperty( + name="parts", + min=1, + max=1024, + default=1, update=update_manipulators + ) + step_angle = FloatProperty( + description="Curved parts segmentation", + name="step angle", + min=1 / 180 * pi, + max=pi, + default=6 / 180 * pi, + subtype='ANGLE', unit='ROTATION', + update=update + ) + width = FloatProperty( + name="width", + min=0.01, + default=0.2, + unit='LENGTH', subtype='DISTANCE', + update=update + ) + z = FloatProperty( + name='height', + min=0.1, + default=2.7, precision=2, + unit='LENGTH', subtype='DISTANCE', + description='height', update=update, + ) + x_offset = FloatProperty( + name="x offset", + min=-1, max=1, + default=-1, precision=2, step=1, + update=update + ) + radius = FloatProperty( + name="radius", + min=0.5, + default=0.7, + unit='LENGTH', subtype='DISTANCE', + update=update + ) + da = FloatProperty( + name="angle", + min=-pi, + max=pi, + default=pi / 2, + subtype='ANGLE', unit='ROTATION', + update=update + ) + flip = BoolProperty(default=False, update=update_childs) + closed = BoolProperty( + default=False, + name="Close", + update=update_manipulators + ) + auto_update = BoolProperty( + options={'SKIP_SAVE'}, + default=True, + update=update_manipulators + ) + realtime = BoolProperty( + options={'SKIP_SAVE'}, + default=True, + name="RealTime", + description="Relocate childs in realtime" + ) + # dumb manipulators to show sizes between childs + childs_manipulators = CollectionProperty(type=archipack_manipulator) + # store to manipulate windows and doors + childs = CollectionProperty(type=archipack_wall2_child) + t_part = StringProperty( + name="Parent wall", + description="This part will follow parent when set", + default="", + update=update_t_part + ) + + def insert_part(self, context, o, where): + self.manipulable_disable(context) + self.auto_update = False + # the part we do split + part_0 = self.parts[where] + part_0.length /= 2 + part_0.da /= 2 + self.parts.add() + part_1 = self.parts[len(self.parts) - 1] + part_1.type = part_0.type + part_1.length = part_0.length + part_1.da = part_0.da + part_1.a0 = 0 + # move after current one + self.parts.move(len(self.parts) - 1, where + 1) + self.n_parts += 1 + # re-eval childs location + g = self.get_generator() + self.setup_childs(o, g) + + self.setup_manipulators() + self.auto_update = True + + def add_part(self, context, length): + self.manipulable_disable(context) + self.auto_update = False + p = self.parts.add() + p.length = length + self.parts.move(len(self.parts) - 1, self.n_parts) + self.n_parts += 1 + self.setup_manipulators() + self.auto_update = True + return self.parts[self.n_parts - 1] + + def remove_part(self, context, o, where): + self.manipulable_disable(context) + self.auto_update = False + # preserve shape + # using generator + if where > 0: + g = self.get_generator() + w = g.segs[where - 1] + w.p1 = g.segs[where].p1 + + if where + 1 < self.n_parts: + self.parts[where + 1].a0 = g.segs[where + 1].delta_angle(w) + + part = self.parts[where - 1] + + if "C_" in part.type: + part.radius = w.r + else: + part.length = w.length + + if where > 1: + part.a0 = w.delta_angle(g.segs[where - 2]) + else: + part.a0 = w.straight(1, 0).angle + + self.parts.remove(where) + self.n_parts -= 1 + + # re-eval child location + g = self.get_generator() + self.setup_childs(o, g) + + # fix snap manipulators index + self.setup_manipulators() + self.auto_update = True + + def get_generator(self): + # print("get_generator") + g = WallGenerator(self.parts) + for part in self.parts: + g.add_part(part, self.z, self.flip) + g.close(self.closed) + return g + + def update_parts(self, o, update_childs=False): + # print("update_parts") + # remove rows + # NOTE: + # n_parts+1 + # as last one is end point of last segment or closing one + row_change = False + for i in range(len(self.parts), self.n_parts + 1, -1): + row_change = True + self.parts.remove(i - 1) + + # add rows + for i in range(len(self.parts), self.n_parts + 1): + row_change = True + self.parts.add() + + self.setup_manipulators() + + g = self.get_generator() + + if o is not None and (row_change or update_childs): + self.setup_childs(o, g) + + return g + + def setup_manipulators(self): + + if len(self.manipulators) == 0: + # make manipulators selectable + s = self.manipulators.add() + s.prop1_name = "width" + s = self.manipulators.add() + s.prop1_name = "n_parts" + s.type_key = 'COUNTER' + s = self.manipulators.add() + s.prop1_name = "z" + s.normal = (0, 1, 0) + + if self.t_part != "" and len(self.manipulators) < 4: + s = self.manipulators.add() + s.prop1_name = "x" + s.type_key = 'DELTA_LOC' + + for i in range(self.n_parts + 1): + p = self.parts[i] + n_manips = len(p.manipulators) + if n_manips < 1: + s = p.manipulators.add() + s.type_key = "ANGLE" + s.prop1_name = "a0" + if n_manips < 2: + s = p.manipulators.add() + s.type_key = "SIZE" + s.prop1_name = "length" + if n_manips < 3: + s = p.manipulators.add() + s.type_key = 'WALL_SNAP' + s.prop1_name = str(i) + s.prop2_name = 'z' + if n_manips < 4: + s = p.manipulators.add() + s.type_key = 'DUMB_STRING' + s.prop1_name = str(i + 1) + p.manipulators[2].prop1_name = str(i) + p.manipulators[3].prop1_name = str(i + 1) + + def interpolate_bezier(self, pts, wM, p0, p1, resolution): + if resolution == 0: + pts.append(wM * p0.co.to_3d()) + else: + v = (p1.co - p0.co).normalized() + d1 = (p0.handle_right - p0.co).normalized() + d2 = (p1.co - p1.handle_left).normalized() + if d1 == v and d2 == v: + pts.append(wM * p0.co.to_3d()) + else: + seg = interpolate_bezier(wM * p0.co, + wM * p0.handle_right, + wM * p1.handle_left, + wM * p1.co, + resolution + 1) + for i in range(resolution): + pts.append(seg[i].to_3d()) + + def is_cw(self, pts): + p0 = pts[0] + d = 0 + for p in pts[1:]: + d += (p.x * p0.y - p.y * p0.x) + p0 = p + return d > 0 + + def from_spline(self, wM, resolution, spline): + pts = [] + if spline.type == 'POLY': + pts = [wM * p.co.to_3d() for p in spline.points] + if spline.use_cyclic_u: + pts.append(pts[0]) + elif spline.type == 'BEZIER': + points = spline.bezier_points + for i in range(1, len(points)): + p0 = points[i - 1] + p1 = points[i] + self.interpolate_bezier(pts, wM, p0, p1, resolution) + if spline.use_cyclic_u: + p0 = points[-1] + p1 = points[0] + self.interpolate_bezier(pts, wM, p0, p1, resolution) + pts.append(pts[0]) + else: + pts.append(wM * points[-1].co) + + if self.is_cw(pts): + pts = list(reversed(pts)) + + self.auto_update = False + self.from_points(pts, spline.use_cyclic_u) + self.auto_update = True + + def from_points(self, pts, closed): + + self.n_parts = len(pts) - 1 + + if closed: + self.n_parts -= 1 + + self.update_parts(None) + + p0 = pts.pop(0) + a0 = 0 + for i, p1 in enumerate(pts): + dp = p1 - p0 + da = atan2(dp.y, dp.x) - a0 + if da > pi: + da -= 2 * pi + if da < -pi: + da += 2 * pi + if i >= len(self.parts): + print("Too many pts for parts") + break + p = self.parts[i] + p.length = dp.to_2d().length + p.dz = dp.z + p.a0 = da + a0 += da + p0 = p1 + + self.closed = closed + + def reverse(self, context, o): + + g = self.get_generator() + pts = [seg.p0.to_3d() for seg in g.segs] + + if self.closed: + pts.append(pts[0]) + + pts = list(reversed(pts)) + self.auto_update = False + + # location wont change for closed walls + if not self.closed: + dp = pts[0] - pts[-1] + # pre-translate as dp is in local coordsys + o.matrix_world = o.matrix_world * Matrix([ + [1, 0, 0, dp.x], + [0, 1, 0, dp.y], + [0, 0, 1, 0], + [0, 0, 0, 1], + ]) + + self.from_points(pts, self.closed) + g = self.get_generator() + + self.setup_childs(o, g) + self.auto_update = True + + # flip does trigger relocate and keep childs orientation + self.flip = not self.flip + + def update(self, context, manipulable_refresh=False, update_childs=False): + + o = self.find_in_selection(context, self.auto_update) + + if o is None: + return + + if manipulable_refresh: + # prevent crash by removing all manipulators refs to datablock before changes + self.manipulable_disable(context) + + verts = [] + faces = [] + + g = self.update_parts(o, update_childs) + # print("make_wall") + g.make_wall(self.step_angle, self.flip, self.closed, verts, faces) + + if self.closed: + f = len(verts) + if self.flip: + faces.append((0, f - 2, f - 1, 1)) + else: + faces.append((f - 2, 0, 1, f - 1)) + + # print("buildmesh") + bmed.buildmesh(context, o, verts, faces, matids=None, uvs=None, weld=True) + + side = 1 + if self.flip: + side = -1 + # Width + offset = side * (0.5 * self.x_offset) * self.width + self.manipulators[0].set_pts([ + g.segs[0].sized_normal(0, offset + 0.5 * side * self.width).v.to_3d(), + g.segs[0].sized_normal(0, offset - 0.5 * side * self.width).v.to_3d(), + (-side, 0, 0) + ]) + + # Parts COUNTER + self.manipulators[1].set_pts([g.segs[-2].lerp(1.1).to_3d(), + g.segs[-2].lerp(1.1 + 0.5 / g.segs[-2].length).to_3d(), + (-side, 0, 0) + ]) + + # Height + self.manipulators[2].set_pts([ + (0, 0, 0), + (0, 0, self.z), + (-1, 0, 0) + ], normal=g.segs[0].straight(side, 0).v.to_3d()) + + if self.t_part != "": + t = 0.3 / g.segs[0].length + self.manipulators[3].set_pts([ + g.segs[0].sized_normal(t, 0.1).p1.to_3d(), + g.segs[0].sized_normal(t, -0.1).p1.to_3d(), + (1, 0, 0) + ]) + + if self.realtime: + # update child location and size + self.relocate_childs(context, o, g) + # store gl points + self.update_childs(context, o, g) + else: + bpy.ops.archipack.wall2_throttle_update(name=o.name) + + modif = o.modifiers.get('Wall') + if modif is None: + modif = o.modifiers.new('Wall', 'SOLIDIFY') + modif.use_quality_normals = True + modif.use_even_offset = True + modif.material_offset_rim = 2 + modif.material_offset = 1 + + modif.thickness = self.width + modif.offset = self.x_offset + + if manipulable_refresh: + # print("manipulable_refresh=True") + self.manipulable_refresh = True + + self.restore_context(context) + + # manipulable children objects like windows and doors + def child_partition(self, array, begin, end): + pivot = begin + for i in range(begin + 1, end + 1): + # wall idx + if array[i][1] < array[begin][1]: + pivot += 1 + array[i], array[pivot] = array[pivot], array[i] + # param t on the wall + elif array[i][1] == array[begin][1] and array[i][4] <= array[begin][4]: + pivot += 1 + array[i], array[pivot] = array[pivot], array[i] + array[pivot], array[begin] = array[begin], array[pivot] + return pivot + + def sort_child(self, array, begin=0, end=None): + # print("sort_child") + if end is None: + end = len(array) - 1 + + def _quicksort(array, begin, end): + if begin >= end: + return + pivot = self.child_partition(array, begin, end) + _quicksort(array, begin, pivot - 1) + _quicksort(array, pivot + 1, end) + return _quicksort(array, begin, end) + + def add_child(self, name, wall_idx, pos, flip): + # print("add_child %s %s" % (name, wall_idx)) + c = self.childs.add() + c.child_name = name + c.wall_idx = wall_idx + c.pos = pos + c.flip = flip + m = c.manipulators.add() + m.type_key = 'DELTA_LOC' + m.prop1_name = "x" + m = c.manipulators.add() + m.type_key = 'SNAP_SIZE_LOC' + m.prop1_name = "x" + m.prop2_name = "x" + + def setup_childs(self, o, g): + """ + Store childs + create manipulators + call after a boolean oop + """ + # tim = time.time() + self.childs.clear() + self.childs_manipulators.clear() + if o.parent is None: + return + wall_with_childs = [0 for i in range(self.n_parts + 1)] + relocate = [] + dmax = 2 * self.width + + wtM = o.matrix_world + wrM = Matrix([ + wtM[0].to_2d(), + wtM[1].to_2d() + ]) + witM = wtM.inverted() + + for child in o.parent.children: + # filter allowed childs + cd = child.data + wd = archipack_wall2.datablock(child) + if (child != o and cd is not None and ( + 'archipack_window' in cd or + 'archipack_door' in cd or ( + wd is not None and + o.name in wd.t_part + ) + )): + + # setup on T linked walls + if wd is not None: + wg = wd.get_generator() + wd.setup_childs(child, wg) + + ctM = child.matrix_world + crM = Matrix([ + ctM[0].to_2d(), + ctM[1].to_2d() + ]) + + # pt in w coordsys + pos = ctM.translation + pt = (witM * pos).to_2d() + + for wall_idx, wall in enumerate(g.segs): + # may be optimized with a bound check + res, dist, t = wall.point_sur_segment(pt) + # outside is on the right side of the wall + # p1 + # |-- x + # p0 + if res and t > 0 and t < 1 and abs(dist) < dmax: + # dir in world coordsys + dir = wrM * wall.sized_normal(t, 1).v + wall_with_childs[wall_idx] = 1 + m = self.childs_manipulators.add() + m.type_key = 'DUMB_SIZE' + # always make window points outside + if "archipack_window" in cd: + flip = self.flip + else: + dir_y = crM * Vector((0, -1)) + # let door orient where user want + flip = (dir_y - dir).length > 0.5 + # store z in wall space + relocate.append(( + child.name, + wall_idx, + (t * wall.length, dist, (witM * pos).z), + flip, + t)) + break + + self.sort_child(relocate) + for child in relocate: + name, wall_idx, pos, flip, t = child + self.add_child(name, wall_idx, pos, flip) + + # add a dumb size from last child to end of wall segment + for i in range(sum(wall_with_childs)): + m = self.childs_manipulators.add() + m.type_key = 'DUMB_SIZE' + # print("setup_childs:%1.4f" % (time.time()-tim)) + + def relocate_childs(self, context, o, g): + """ + Move and resize childs after wall edition + """ + # print("relocate_childs") + # tim = time.time() + w = -self.x_offset * self.width + if self.flip: + w = -w + tM = o.matrix_world + for child in self.childs: + c, d = child.get_child(context) + if c is None: + continue + t = child.pos.x / g.segs[child.wall_idx].length + n = g.segs[child.wall_idx].sized_normal(t, 1) + rx, ry = -n.v + rx, ry = ry, -rx + if child.flip: + rx, ry = -rx, -ry + + if d is not None: + # print("change flip:%s width:%s" % (d.flip != child.flip, d.y != self.width)) + if d.y != self.width or d.flip != child.flip: + c.select = True + d.auto_update = False + d.flip = child.flip + d.y = self.width + d.auto_update = True + c.select = False + x, y = n.p - (0.5 * w * n.v) + else: + x, y = n.p - (child.pos.y * n.v) + + context.scene.objects.active = o + # preTranslate + c.matrix_world = tM * Matrix([ + [rx, -ry, 0, x], + [ry, rx, 0, y], + [0, 0, 1, child.pos.z], + [0, 0, 0, 1] + ]) + + # Update T linked wall's childs + if archipack_wall2.filter(c): + d = archipack_wall2.datablock(c) + cg = d.get_generator() + d.relocate_childs(context, c, cg) + + # print("relocate_childs:%1.4f" % (time.time()-tim)) + + def update_childs(self, context, o, g): + """ + setup gl points for childs + """ + # print("update_childs") + + if o.parent is None: + return + + # swap manipulators so they always face outside + manip_side = 1 + if self.flip: + manip_side = -1 + + itM = o.matrix_world.inverted() + m_idx = 0 + for wall_idx, wall in enumerate(g.segs): + p0 = wall.lerp(0) + wall_has_childs = False + for child in self.childs: + if child.wall_idx == wall_idx: + c, d = child.get_child(context) + if d is not None: + # child is either a window or a door + wall_has_childs = True + dt = 0.5 * d.x / wall.length + pt = (itM * c.matrix_world.translation).to_2d() + res, y, t = wall.point_sur_segment(pt) + child.pos = (wall.length * t, y, child.pos.z) + p1 = wall.lerp(t - dt) + # dumb size between childs + self.childs_manipulators[m_idx].set_pts([ + (p0.x, p0.y, 0), + (p1.x, p1.y, 0), + (manip_side * 0.5, 0, 0)]) + m_idx += 1 + x, y = 0.5 * d.x, -self.x_offset * 0.5 * d.y + + if child.flip: + side = -manip_side + else: + side = manip_side + + # delta loc + child.manipulators[0].set_pts([(-x, side * -y, 0), (x, side * -y, 0), (side, 0, 0)]) + # loc size + child.manipulators[1].set_pts([ + (-x, side * -y, 0), + (x, side * -y, 0), + (0.5 * side, 0, 0)]) + p0 = wall.lerp(t + dt) + p1 = wall.lerp(1) + if wall_has_childs: + # dub size after all childs + self.childs_manipulators[m_idx].set_pts([ + (p0.x, p0.y, 0), + (p1.x, p1.y, 0), + (manip_side * 0.5, 0, 0)]) + m_idx += 1 + + def manipulate_childs(self, context): + """ + setup child manipulators + """ + # print("manipulate_childs") + n_parts = self.n_parts + if self.closed: + n_parts += 1 + + for wall_idx in range(n_parts): + for child in self.childs: + if child.wall_idx == wall_idx: + c, d = child.get_child(context) + if d is not None: + # delta loc + self.manip_stack.append(child.manipulators[0].setup(context, c, d, self.manipulate_callback)) + # loc size + self.manip_stack.append(child.manipulators[1].setup(context, c, d, self.manipulate_callback)) + + def manipulate_callback(self, context, o=None, manipulator=None): + found = False + if o.parent is not None: + for c in o.parent.children: + if (archipack_wall2.datablock(c) == self): + context.scene.objects.active = c + found = True + break + if found: + self.manipulable_manipulate(context, manipulator=manipulator) + + def manipulable_manipulate(self, context, event=None, manipulator=None): + type_name = type(manipulator).__name__ + # print("manipulable_manipulate %s" % (type_name)) + if type_name in [ + 'DeltaLocationManipulator', + 'SizeLocationManipulator', + 'SnapSizeLocationManipulator' + ]: + # update manipulators pos of childs + o = context.active_object + if o.parent is None: + return + g = self.get_generator() + itM = o.matrix_world.inverted() * o.parent.matrix_world + for child in self.childs: + c, d = child.get_child(context) + if d is not None: + wall = g.segs[child.wall_idx] + pt = (itM * c.location).to_2d() + res, d, t = wall.point_sur_segment(pt) + child.pos = (t * wall.length, d, child.pos.z) + # update childs manipulators + self.update_childs(context, o, g) + + def manipulable_move_t_part(self, context, o=None, manipulator=None): + type_name = type(manipulator).__name__ + # print("manipulable_manipulate %s" % (type_name)) + if type_name in [ + 'DeltaLocationManipulator' + ]: + # update manipulators pos of childs + if archipack_wall2.datablock(o) != self: + return + g = self.get_generator() + # update childs + self.relocate_childs(context, o, g) + + def manipulable_release(self, context): + """ + Override with action to do on mouse release + eg: big update + """ + return + + def manipulable_setup(self, context): + # print("manipulable_setup") + self.manipulable_disable(context) + o = context.active_object + + # setup childs manipulators + self.manipulate_childs(context) + n_parts = self.n_parts + if self.closed: + n_parts += 1 + + # update manipulators on version change + self.setup_manipulators() + + for i, part in enumerate(self.parts): + + if i < n_parts: + if i > 0: + # start angle + self.manip_stack.append(part.manipulators[0].setup(context, o, part)) + + # length / radius + angle + self.manip_stack.append(part.manipulators[1].setup(context, o, part)) + # segment index + self.manip_stack.append(part.manipulators[3].setup(context, o, self)) + + # snap point + self.manip_stack.append(part.manipulators[2].setup(context, o, self)) + + # height as per segment will be here when done + + # width + counter + for m in self.manipulators: + self.manip_stack.append(m.setup(context, o, self, self.manipulable_move_t_part)) + + # dumb between childs + for m in self.childs_manipulators: + self.manip_stack.append(m.setup(context, o, self)) + + def manipulable_exit(self, context): + """ + Override with action to do when modal exit + """ + return + + def manipulable_invoke(self, context): + """ + call this in operator invoke() + """ + # print("manipulable_invoke") + if self.manipulate_mode: + self.manipulable_disable(context) + return False + + # self.manip_stack = [] + o = context.active_object + g = self.get_generator() + # setup childs manipulators + self.setup_childs(o, g) + # store gl points + self.update_childs(context, o, g) + # dont do anything .. + # self.manipulable_release(context) + # self.manipulate_mode = True + self.manipulable_setup(context) + self.manipulate_mode = True + + self._manipulable_invoke(context) + + return True + + +# Update throttle (smell hack here) +# use 2 globals to store a timer and state of update_action +# NO MORE USING THIS PART, kept as it as it may be usefull in some cases +update_timer = None +update_timer_updating = False + + +class ARCHIPACK_OT_wall2_throttle_update(Operator): + bl_idname = "archipack.wall2_throttle_update" + bl_label = "Update childs with a delay" + + name = StringProperty() + + def modal(self, context, event): + global update_timer_updating + if event.type == 'TIMER' and not update_timer_updating: + update_timer_updating = True + o = context.scene.objects.get(self.name) + # print("delay update of %s" % (self.name)) + if o is not None: + o.select = True + context.scene.objects.active = o + d = o.data.archipack_wall2[0] + g = d.get_generator() + # update child location and size + d.relocate_childs(context, o, g) + # store gl points + d.update_childs(context, o, g) + return self.cancel(context) + return {'PASS_THROUGH'} + + def execute(self, context): + global update_timer + global update_timer_updating + if update_timer is not None: + if update_timer_updating: + return {'CANCELLED'} + # reset update_timer so it only occurs once 0.1s after last action + context.window_manager.event_timer_remove(update_timer) + update_timer = context.window_manager.event_timer_add(0.1, context.window) + return {'CANCELLED'} + update_timer_updating = False + context.window_manager.modal_handler_add(self) + update_timer = context.window_manager.event_timer_add(0.1, context.window) + return {'RUNNING_MODAL'} + + def cancel(self, context): + global update_timer + context.window_manager.event_timer_remove(update_timer) + update_timer = None + return {'CANCELLED'} + + +class ARCHIPACK_PT_wall2(Panel): + bl_idname = "ARCHIPACK_PT_wall2" + bl_label = "Wall" + bl_space_type = 'VIEW_3D' + bl_region_type = 'UI' + bl_category = 'ArchiPack' + + def draw(self, context): + prop = archipack_wall2.datablock(context.object) + if prop is None: + return + layout = self.layout + row = layout.row(align=True) + row.operator("archipack.wall2_manipulate", icon='HAND') + # row = layout.row(align=True) + # row.prop(prop, 'realtime') + box = layout.box() + box.prop(prop, 'n_parts') + box.prop(prop, 'step_angle') + box.prop(prop, 'width') + box.prop(prop, 'z') + box.prop(prop, 'flip') + box.prop(prop, 'x_offset') + row = layout.row() + row.prop(prop, "closed") + row = layout.row() + row.prop_search(prop, "t_part", context.scene, "objects", text="T link", icon='OBJECT_DATAMODE') + row = layout.row() + row.operator("archipack.wall2_reverse", icon='FILE_REFRESH') + n_parts = prop.n_parts + if prop.closed: + n_parts += 1 + for i, part in enumerate(prop.parts): + if i < n_parts: + box = layout.box() + part.draw(box, context, i) + + @classmethod + def poll(cls, context): + return archipack_wall2.filter(context.active_object) + + +# ------------------------------------------------------------------ +# Define operator class to create object +# ------------------------------------------------------------------ + + +class ARCHIPACK_OT_wall2(ArchipackCreateTool, Operator): + bl_idname = "archipack.wall2" + bl_label = "Wall" + bl_description = "Create a Wall" + bl_category = 'Archipack' + bl_options = {'REGISTER', 'UNDO'} + + def create(self, context): + m = bpy.data.meshes.new("Wall") + o = bpy.data.objects.new("Wall", m) + d = m.archipack_wall2.add() + d.manipulable_selectable = True + context.scene.objects.link(o) + o.select = True + # around 12 degree + m.auto_smooth_angle = 0.20944 + context.scene.objects.active = o + self.load_preset(d) + self.add_material(o) + return o + + def execute(self, context): + if context.mode == "OBJECT": + bpy.ops.object.select_all(action="DESELECT") + o = self.create(context) + o.location = bpy.context.scene.cursor_location + o.select = True + context.scene.objects.active = o + self.manipulate() + return {'FINISHED'} + else: + self.report({'WARNING'}, "Archipack: Option only valid in Object mode") + return {'CANCELLED'} + + +class ARCHIPACK_OT_wall2_from_curve(Operator): + bl_idname = "archipack.wall2_from_curve" + bl_label = "Wall curve" + bl_description = "Create a wall from a curve" + bl_category = 'Archipack' + bl_options = {'REGISTER', 'UNDO'} + + auto_manipulate = BoolProperty(default=True) + + @classmethod + def poll(self, context): + return context.active_object is not None and context.active_object.type == 'CURVE' + + def create(self, context): + curve = context.active_object + for spline in curve.data.splines: + bpy.ops.archipack.wall2(auto_manipulate=self.auto_manipulate) + o = context.scene.objects.active + d = archipack_wall2.datablock(o) + d.from_spline(curve.matrix_world, 12, spline) + if spline.type == 'POLY': + pt = spline.points[0].co + elif spline.type == 'BEZIER': + pt = spline.bezier_points[0].co + else: + pt = Vector((0, 0, 0)) + # pretranslate + o.matrix_world = curve.matrix_world * Matrix([ + [1, 0, 0, pt.x], + [0, 1, 0, pt.y], + [0, 0, 1, pt.z], + [0, 0, 0, 1] + ]) + return o + + # ----------------------------------------------------- + # Execute + # ----------------------------------------------------- + def execute(self, context): + if context.mode == "OBJECT": + bpy.ops.object.select_all(action="DESELECT") + o = self.create(context) + if o is not None: + o.select = True + context.scene.objects.active = o + return {'FINISHED'} + else: + self.report({'WARNING'}, "Archipack: Option only valid in Object mode") + return {'CANCELLED'} + + +class ARCHIPACK_OT_wall2_from_slab(Operator): + bl_idname = "archipack.wall2_from_slab" + bl_label = "->Wall" + bl_description = "Create a wall from a slab" + bl_category = 'Archipack' + bl_options = {'REGISTER', 'UNDO'} + + auto_manipulate = BoolProperty(default=True) + + @classmethod + def poll(self, context): + o = context.active_object + return o is not None and o.data is not None and 'archipack_slab' in o.data + + def create(self, context): + slab = context.active_object + wd = slab.data.archipack_slab[0] + bpy.ops.archipack.wall2(auto_manipulate=self.auto_manipulate) + o = context.scene.objects.active + d = archipack_wall2.datablock(o) + d.auto_update = False + d.parts.clear() + d.n_parts = wd.n_parts - 1 + d.closed = True + for part in wd.parts: + p = d.parts.add() + if "S_" in part.type: + p.type = "S_WALL" + else: + p.type = "C_WALL" + p.length = part.length + p.radius = part.radius + p.da = part.da + p.a0 = part.a0 + o.select = True + context.scene.objects.active = o + d.auto_update = True + # pretranslate + o.matrix_world = slab.matrix_world.copy() + + bpy.ops.object.select_all(action='DESELECT') + # parenting childs to wall reference point + if o.parent is None: + x, y, z = o.bound_box[0] + context.scene.cursor_location = o.matrix_world * Vector((x, y, z)) + # fix issue #9 + context.scene.objects.active = o + bpy.ops.archipack.reference_point() + else: + o.parent.select = True + context.scene.objects.active = o.parent + o.select = True + slab.select = True + bpy.ops.archipack.parent_to_reference() + o.parent.select = False + return o + + # ----------------------------------------------------- + # Execute + # ----------------------------------------------------- + def execute(self, context): + if context.mode == "OBJECT": + bpy.ops.object.select_all(action="DESELECT") + o = self.create(context) + o.select = True + context.scene.objects.active = o + return {'FINISHED'} + else: + self.report({'WARNING'}, "Archipack: Option only valid in Object mode") + return {'CANCELLED'} + + +# ------------------------------------------------------------------ +# Define operator class to draw a wall +# ------------------------------------------------------------------ + + +class ARCHIPACK_OT_wall2_draw(ArchpackDrawTool, Operator): + bl_idname = "archipack.wall2_draw" + bl_label = "Draw a Wall" + bl_description = "Draw a Wall" + bl_category = 'Archipack' + bl_options = {'REGISTER', 'UNDO'} + + o = None + state = 'RUNNING' + flag_create = False + flag_next = False + wall_part1 = None + wall_line1 = None + line = None + label = None + feedback = None + takeloc = Vector((0, 0, 0)) + sel = [] + act = None + + # constraint to other wall and make a T child + parent = None + takemat = None + + max_style_draw_tool = False + + @classmethod + def poll(cls, context): + return True + + def draw_callback(self, _self, context): + self.feedback.draw(context) + + def sp_draw(self, sp, context): + z = 2.7 + if self.state == 'CREATE': + p0 = self.takeloc + else: + p0 = sp.takeloc + + p1 = sp.placeloc + delta = p1 - p0 + # print("sp_draw state:%s delta:%s p0:%s p1:%s" % (self.state, delta.length, p0, p1)) + if delta.length == 0: + return + self.wall_part1.set_pos([p0, p1, Vector((p1.x, p1.y, p1.z + z)), Vector((p0.x, p0.y, p0.z + z))]) + self.wall_line1.set_pos([p0, p1, Vector((p1.x, p1.y, p1.z + z)), Vector((p0.x, p0.y, p0.z + z))]) + self.wall_part1.draw(context) + self.wall_line1.draw(context) + self.line.p = p0 + self.line.v = delta + self.label.set_pos(context, self.line.length, self.line.lerp(0.5), self.line.v, normal=Vector((0, 0, 1))) + self.label.draw(context) + self.line.draw(context) + + def sp_callback(self, context, event, state, sp): + # print("sp_callback event %s %s state:%s" % (event.type, event.value, state)) + + if state == 'SUCCESS': + + if self.state == 'CREATE': + takeloc = self.takeloc + delta = sp.placeloc - self.takeloc + else: + takeloc = sp.takeloc + delta = sp.delta + + old = context.active_object + if self.o is None: + bpy.ops.archipack.wall2(auto_manipulate=False) + o = context.active_object + o.location = takeloc + self.o = o + d = archipack_wall2.datablock(o) + part = d.parts[0] + part.length = delta.length + else: + o = self.o + o.select = True + context.scene.objects.active = o + d = archipack_wall2.datablock(o) + # Check for end close to start and close when applicable + dp = sp.placeloc - o.location + if dp.length < 0.01: + d.closed = True + self.state = 'CANCEL' + return + + part = d.add_part(context, delta.length) + + # print("self.o :%s" % o.name) + rM = o.matrix_world.inverted().to_3x3() + g = d.get_generator() + w = g.segs[-2] + dp = rM * delta + da = atan2(dp.y, dp.x) - w.straight(1).angle + a0 = part.a0 + da + if a0 > pi: + a0 -= 2 * pi + if a0 < -pi: + a0 += 2 * pi + part.a0 = a0 + + context.scene.objects.active = old + self.flag_next = True + context.area.tag_redraw() + # print("feedback.on:%s" % self.feedback.on) + + self.state = state + + def sp_init(self, context, event, state, sp): + # print("sp_init event %s %s %s" % (event.type, event.value, state)) + if state == 'SUCCESS': + # point placed, check if a wall was under mouse + res, tM, wall, y = self.mouse_hover_wall(context, event) + if res: + d = archipack_wall2.datablock(wall) + if event.ctrl: + # user snap, use direction as constraint + tM.translation = sp.placeloc.copy() + else: + # without snap, use wall's bottom + tM.translation -= y.normalized() * (0.5 * d.width) + self.takeloc = tM.translation + self.parent = wall.name + self.takemat = tM + else: + self.takeloc = sp.placeloc.copy() + + self.state = 'RUNNING' + # print("feedback.on:%s" % self.feedback.on) + elif state == 'CANCEL': + self.state = state + return + + def ensure_ccw(self): + """ + Wall to slab expect wall vertex order to be ccw + so reverse order here when needed + """ + d = archipack_wall2.datablock(self.o) + g = d.get_generator() + pts = [seg.p0.to_3d() for seg in g.segs] + + if d.closed: + pts.append(pts[0]) + + if d.is_cw(pts): + d.x_offset = 1 + pts = list(reversed(pts)) + self.o.location += pts[0] - pts[-1] + + d.from_points(pts, d.closed) + + def modal(self, context, event): + + context.area.tag_redraw() + # print("modal event %s %s" % (event.type, event.value)) + if event.type == 'NONE': + return {'PASS_THROUGH'} + + if self.state == 'STARTING': + takeloc = self.mouse_to_plane(context, event) + # wait for takeloc being visible when button is over horizon + rv3d = context.region_data + viewinv = rv3d.view_matrix.inverted() + if (takeloc * viewinv).z < 0: + # print("STARTING") + # when user press draw button + snap_point(takeloc=takeloc, + callback=self.sp_init, + # transform_orientation=context.space_data.transform_orientation, + constraint_axis=(True, True, False), + release_confirm=True) + return {'RUNNING_MODAL'} + + elif self.state == 'RUNNING': + # print("RUNNING") + # when user start drawing + + # release confirm = False on blender mode + # release confirm = True on max mode + self.state = 'CREATE' + snap_point(takeloc=self.takeloc, + draw=self.sp_draw, + takemat=self.takemat, + transform_orientation=context.space_data.transform_orientation, + callback=self.sp_callback, + constraint_axis=(True, True, False), + release_confirm=self.max_style_draw_tool) + return {'RUNNING_MODAL'} + + elif self.state != 'CANCEL' and event.type in {'LEFTMOUSE', 'RET', 'NUMPAD_ENTER', 'SPACE'}: + + # print('LEFTMOUSE %s' % (event.value)) + self.feedback.instructions(context, "Draw a wall", "Click & Drag to add a segment", [ + ('CTRL', 'Snap'), + ('MMBTN', 'Constraint to axis'), + ('X Y', 'Constraint to axis'), + ('BACK_SPACE', 'Remove part'), + ('RIGHTCLICK or ESC', 'exit') + ]) + + # press with max mode release with blender mode + if self.max_style_draw_tool: + evt_value = 'PRESS' + else: + evt_value = 'RELEASE' + + if event.value == evt_value: + if self.flag_next: + self.flag_next = False + o = self.o + o.select = True + context.scene.objects.active = o + d = archipack_wall2.datablock(o) + g = d.get_generator() + p0 = g.segs[-2].p0 + p1 = g.segs[-2].p1 + dp = p1 - p0 + takemat = o.matrix_world * Matrix([ + [dp.x, dp.y, 0, p1.x], + [dp.y, -dp.x, 0, p1.y], + [0, 0, 1, 0], + [0, 0, 0, 1] + ]) + takeloc = o.matrix_world * p1.to_3d() + o.select = False + else: + takeloc = self.mouse_to_plane(context, event) + takemat = None + + snap_point(takeloc=takeloc, + takemat=takemat, + draw=self.sp_draw, + callback=self.sp_callback, + constraint_axis=(True, True, False), + release_confirm=self.max_style_draw_tool) + + return {'RUNNING_MODAL'} + + if self.keymap.check(event, self.keymap.undo) or ( + event.type in {'BACK_SPACE'} and event.value == 'RELEASE' + ): + if self.o is not None: + o = self.o + o.select = True + context.scene.objects.active = o + d = archipack_wall2.datablock(o) + if d.n_parts > 1: + d.n_parts -= 1 + return {'RUNNING_MODAL'} + + if self.state == 'CANCEL' or (event.type in {'ESC', 'RIGHTMOUSE'} and + event.value == 'RELEASE'): + + self.feedback.disable() + bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW') + + if self.o is None: + context.scene.objects.active = self.act + for o in self.sel: + o.select = True + else: + self.o.select = True + context.scene.objects.active = self.o + d = archipack_wall2.datablock(self.o) + + # remove last segment with blender mode + if not self.max_style_draw_tool: + if not d.closed and d.n_parts > 1: + d.n_parts -= 1 + + self.o.select = True + context.scene.objects.active = self.o + # make T child + if self.parent is not None: + d.t_part = self.parent + + if bpy.ops.archipack.wall2_manipulate.poll(): + bpy.ops.archipack.wall2_manipulate('INVOKE_DEFAULT') + + return {'FINISHED'} + + return {'PASS_THROUGH'} + + def invoke(self, context, event): + + if context.mode == "OBJECT": + prefs = context.user_preferences.addons[__name__.split('.')[0]].preferences + self.max_style_draw_tool = prefs.max_style_draw_tool + self.keymap = Keymaps(context) + self.wall_part1 = GlPolygon((0.5, 0, 0, 0.2)) + self.wall_line1 = GlPolyline((0.5, 0, 0, 0.8)) + self.line = GlLine() + self.label = GlText() + self.feedback = FeedbackPanel() + self.feedback.instructions(context, "Draw a wall", "Click & Drag to start", [ + ('CTRL', 'Snap'), + ('MMBTN', 'Constraint to axis'), + ('X Y', 'Constraint to axis'), + ('SHIFT+CTRL+TAB', 'Switch snap mode'), + ('RIGHTCLICK or ESC', 'exit without change') + ]) + self.feedback.enable() + args = (self, context) + + self.sel = [o for o in context.selected_objects] + self.act = context.active_object + bpy.ops.object.select_all(action="DESELECT") + + self.state = 'STARTING' + + self._handle = bpy.types.SpaceView3D.draw_handler_add(self.draw_callback, args, 'WINDOW', 'POST_PIXEL') + context.window_manager.modal_handler_add(self) + return {'RUNNING_MODAL'} + else: + self.report({'WARNING'}, "Archipack: Option only valid in Object mode") + return {'CANCELLED'} + + +# ------------------------------------------------------------------ +# Define operator class to manage parts +# ------------------------------------------------------------------ + + +class ARCHIPACK_OT_wall2_insert(Operator): + bl_idname = "archipack.wall2_insert" + bl_label = "Insert" + bl_description = "Insert part" + bl_category = 'Archipack' + bl_options = {'REGISTER', 'UNDO'} + index = IntProperty(default=0) + + def execute(self, context): + if context.mode == "OBJECT": + o = context.active_object + d = archipack_wall2.datablock(o) + if d is None: + return {'CANCELLED'} + d.insert_part(context, o, self.index) + return {'FINISHED'} + else: + self.report({'WARNING'}, "Archipack: Option only valid in Object mode") + return {'CANCELLED'} + + +class ARCHIPACK_OT_wall2_remove(Operator): + bl_idname = "archipack.wall2_remove" + bl_label = "Remove" + bl_description = "Remove part" + bl_category = 'Archipack' + bl_options = {'REGISTER', 'UNDO'} + index = IntProperty(default=0) + + def execute(self, context): + if context.mode == "OBJECT": + o = context.active_object + d = archipack_wall2.datablock(o) + if d is None: + return {'CANCELLED'} + d.remove_part(context, o, self.index) + return {'FINISHED'} + else: + self.report({'WARNING'}, "Archipack: Option only valid in Object mode") + return {'CANCELLED'} + + +class ARCHIPACK_OT_wall2_reverse(Operator): + bl_idname = "archipack.wall2_reverse" + bl_label = "Reverse" + bl_description = "Reverse parts order" + bl_category = 'Archipack' + bl_options = {'REGISTER', 'UNDO'} + + def execute(self, context): + if context.mode == "OBJECT": + o = context.active_object + d = archipack_wall2.datablock(o) + if d is None: + return {'CANCELLED'} + d.reverse(context, o) + return {'FINISHED'} + else: + self.report({'WARNING'}, "Archipack: Option only valid in Object mode") + return {'CANCELLED'} + + +# ------------------------------------------------------------------ +# Define operator class to manipulate object +# ------------------------------------------------------------------ + + +class ARCHIPACK_OT_wall2_manipulate(Operator): + bl_idname = "archipack.wall2_manipulate" + bl_label = "Manipulate" + bl_description = "Manipulate" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(self, context): + return archipack_wall2.filter(context.active_object) + + def invoke(self, context, event): + d = archipack_wall2.datablock(context.active_object) + d.manipulable_invoke(context) + return {'FINISHED'} + + def execute(self, context): + """ + For use in boolean ops + """ + if archipack_wall2.filter(context.active_object): + o = context.active_object + d = archipack_wall2.datablock(o) + g = d.get_generator() + d.setup_childs(o, g) + d.update_childs(context, o, g) + d.update(context) + o.select = True + context.scene.objects.active = o + return {'FINISHED'} + + +def register(): + bpy.utils.register_class(archipack_wall2_part) + bpy.utils.register_class(archipack_wall2_child) + bpy.utils.register_class(archipack_wall2) + Mesh.archipack_wall2 = CollectionProperty(type=archipack_wall2) + bpy.utils.register_class(ARCHIPACK_PT_wall2) + bpy.utils.register_class(ARCHIPACK_OT_wall2) + bpy.utils.register_class(ARCHIPACK_OT_wall2_draw) + bpy.utils.register_class(ARCHIPACK_OT_wall2_insert) + bpy.utils.register_class(ARCHIPACK_OT_wall2_remove) + bpy.utils.register_class(ARCHIPACK_OT_wall2_reverse) + bpy.utils.register_class(ARCHIPACK_OT_wall2_manipulate) + bpy.utils.register_class(ARCHIPACK_OT_wall2_from_curve) + bpy.utils.register_class(ARCHIPACK_OT_wall2_from_slab) + bpy.utils.register_class(ARCHIPACK_OT_wall2_throttle_update) + + +def unregister(): + bpy.utils.unregister_class(archipack_wall2_part) + bpy.utils.unregister_class(archipack_wall2_child) + bpy.utils.unregister_class(archipack_wall2) + del Mesh.archipack_wall2 + bpy.utils.unregister_class(ARCHIPACK_PT_wall2) + bpy.utils.unregister_class(ARCHIPACK_OT_wall2) + bpy.utils.unregister_class(ARCHIPACK_OT_wall2_draw) + bpy.utils.unregister_class(ARCHIPACK_OT_wall2_insert) + bpy.utils.unregister_class(ARCHIPACK_OT_wall2_remove) + bpy.utils.unregister_class(ARCHIPACK_OT_wall2_reverse) + bpy.utils.unregister_class(ARCHIPACK_OT_wall2_manipulate) + bpy.utils.unregister_class(ARCHIPACK_OT_wall2_from_curve) + bpy.utils.unregister_class(ARCHIPACK_OT_wall2_from_slab) + bpy.utils.unregister_class(ARCHIPACK_OT_wall2_throttle_update) diff --git a/archipack/archipack_window.py b/archipack/archipack_window.py new file mode 100644 index 00000000..2be55947 --- /dev/null +++ b/archipack/archipack_window.py @@ -0,0 +1,2098 @@ +# -*- coding:utf-8 -*- + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110- 1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +# + +# ---------------------------------------------------------- +# Author: Stephen Leger (s-leger) +# +# ---------------------------------------------------------- +# noinspection PyUnresolvedReferences +import bpy +# noinspection PyUnresolvedReferences +from bpy.types import Operator, PropertyGroup, Mesh, Panel +from bpy.props import ( + FloatProperty, IntProperty, BoolProperty, BoolVectorProperty, + CollectionProperty, FloatVectorProperty, EnumProperty, StringProperty +) +from mathutils import Vector +from math import tan, sqrt +from .bmesh_utils import BmeshEdit as bmed +from .panel import Panel as WindowPanel +from .materialutils import MaterialUtils +from .archipack_handle import create_handle, window_handle_vertical_01, window_handle_vertical_02 +# from .archipack_door_panel import ARCHIPACK_OT_select_parent +from .archipack_manipulator import Manipulable +from .archipack_preset import ArchipackPreset, PresetMenuOperator +from .archipack_gl import FeedbackPanel +from .archipack_object import ArchipackObject, ArchipackCreateTool, ArchpackDrawTool +from .archipack_keymaps import Keymaps + + +def update(self, context): + self.update(context) + + +def update_childs(self, context): + self.update(context, childs_only=True) + + +def set_cols(self, value): + if self.n_cols != value: + self.auto_update = False + self._set_width(value) + self.auto_update = True + self.n_cols = value + return None + + +def get_cols(self): + return self.n_cols + + +class archipack_window_panelrow(PropertyGroup): + width = FloatVectorProperty( + name="width", + min=0.5, + max=100.0, + default=[ + 50, 50, 50, 50, 50, 50, 50, 50, + 50, 50, 50, 50, 50, 50, 50, 50, + 50, 50, 50, 50, 50, 50, 50, 50, + 50, 50, 50, 50, 50, 50, 50 + ], + size=31, + update=update + ) + fixed = BoolVectorProperty( + name="Fixed", + default=[ + False, False, False, False, False, False, False, False, + False, False, False, False, False, False, False, False, + False, False, False, False, False, False, False, False, + False, False, False, False, False, False, False, False + ], + size=32, + update=update + ) + cols = IntProperty( + name="panels", + description="number of panels getter and setter, to avoid infinite recursion", + min=1, + max=32, + default=2, + get=get_cols, set=set_cols + ) + n_cols = IntProperty( + name="panels", + description="store number of panels, internal use only to avoid infinite recursion", + min=1, + max=32, + default=2, + update=update + ) + height = FloatProperty( + name="Height", + min=0.1, + default=1.0, precision=2, step=1, + unit='LENGTH', subtype='DISTANCE', + update=update + ) + auto_update = BoolProperty( + options={'SKIP_SAVE'}, + name="auto_update", + description="disable auto update to avoid infinite recursion", + default=True + ) + + def get_row(self, x, y): + size = [Vector((x * self.width[w] / 100, y, 0)) for w in range(self.cols - 1)] + sum_x = sum([s.x for s in size]) + size.append(Vector((x - sum_x, y, 0))) + origin = [] + pivot = [] + ttl = 0 + xh = x / 2 + n_center = len(size) / 2 + for i, sx in enumerate(size): + ttl += sx.x + if i < n_center: + # pivot left + origin.append(Vector((ttl - xh - sx.x, 0))) + pivot.append(1) + else: + # pivot right + origin.append(Vector((ttl - xh, 0))) + pivot.append(-1) + return size, origin, pivot + + def _set_width(self, cols): + width = 100 / cols + for i in range(cols - 1): + self.width[i] = width + + def find_datablock_in_selection(self, context): + """ + find witch selected object this instance belongs to + provide support for "copy to selected" + """ + selected = [o for o in context.selected_objects] + for o in selected: + props = archipack_window.datablock(o) + if props: + for row in props.rows: + if row == self: + return props + return None + + def update(self, context): + if self.auto_update: + props = self.find_datablock_in_selection(context) + if props is not None: + props.update(context, childs_only=False) + + def draw(self, layout, context, last_row): + # store parent at runtime to trigger update on parent + row = layout.row() + row.prop(self, "cols") + row = layout.row() + if not last_row: + row.prop(self, "height") + for i in range(self.cols - 1): + row = layout.row() + row.prop(self, "width", text="col " + str(i + 1), index=i) + row.prop(self, "fixed", text="fixed", index=i) + row = layout.row() + row.label(text="col " + str(self.cols)) + row.prop(self, "fixed", text="fixed", index=(self.cols - 1)) + + +class archipack_window_panel(ArchipackObject, PropertyGroup): + center = FloatVectorProperty( + subtype='XYZ' + ) + origin = FloatVectorProperty( + subtype='XYZ' + ) + size = FloatVectorProperty( + subtype='XYZ' + ) + radius = FloatVectorProperty( + subtype='XYZ' + ) + angle_y = FloatProperty( + name='angle', + unit='ROTATION', + subtype='ANGLE', + min=-1.5, max=1.5, + default=0, precision=2, + description='angle' + ) + frame_y = FloatProperty( + name='Depth', + min=0, + default=0.06, precision=2, + unit='LENGTH', subtype='DISTANCE', + description='frame depth' + ) + frame_x = FloatProperty( + name='Width', + min=0, + default=0.06, precision=2, + unit='LENGTH', subtype='DISTANCE', + description='frame width' + ) + curve_steps = IntProperty( + name="curve steps", + min=1, + max=128, + default=1 + ) + shape = EnumProperty( + name='Shape', + items=( + ('RECTANGLE', 'Rectangle', '', 0), + ('ROUND', 'Top Round', '', 1), + ('ELLIPSIS', 'Top elliptic', '', 2), + ('QUADRI', 'Top oblique', '', 3), + ('CIRCLE', 'Full circle', '', 4) + ), + default='RECTANGLE' + ) + pivot = FloatProperty( + name='pivot', + min=-1, max=1, + default=-1, precision=2, + description='pivot' + ) + side_material = IntProperty( + name="side material", + min=0, + max=2, + default=0 + ) + handle = EnumProperty( + name='Shape', + items=( + ('NONE', 'No handle', '', 0), + ('INSIDE', 'Inside', '', 1), + ('BOTH', 'Inside and outside', '', 2) + ), + default='NONE' + ) + handle_model = IntProperty( + name="handle model", + default=1, + min=1, + max=2 + ) + handle_altitude = FloatProperty( + name='handle altitude', + min=0, + default=0.2, precision=2, + unit='LENGTH', subtype='DISTANCE', + description='handle altitude' + ) + fixed = BoolProperty( + name="Fixed", + default=False + ) + + @property + def window(self): + verre = 0.005 + chanfer = 0.004 + x0 = 0 + x1 = self.frame_x + x2 = 0.75 * self.frame_x + x3 = chanfer + y0 = -self.frame_y + y1 = 0 + y2 = -0.5 * self.frame_y + y3 = -chanfer + y4 = chanfer - self.frame_y + + if self.fixed: + # profil carre avec support pour verre + # p ______ y1 + # / | y3 + # | |___ + # x |___ y2 verre + # | | y4 + # \______| y0 + # x0 x3 x1 + # + x1 = 0.5 * self.frame_x + y1 = -0.45 * self.frame_y + y3 = y1 - chanfer + y4 = chanfer + y0 + y2 = (y0 + y2) / 2 + return WindowPanel( + True, # closed + [1, 0, 0, 0, 1, 2, 2, 2, 2], # x index + [x0, x3, x1], + [y0, y4, y2, y3, y1, y1, y2 + verre, y2 - verre, y0], + [0, 0, 1, 1, 1, 1, 0, 0, 0], # materials + side_cap_front=6, + side_cap_back=7 # cap index + ) + else: + # profil avec chanfrein et joint et support pour verre + # p ____ y1 inside + # / |_ y3 + # | |___ + # x |___ y2 verre + # | _| y4 + # \____| y0 + # x0 x3 x2 x1 outside + if self.side_material == 0: + materials = [0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0] + elif self.side_material == 1: + # rail window exterior + materials = [0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0] + else: + # rail window interior + materials = [0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0] + return WindowPanel( + True, # closed shape + [1, 0, 0, 0, 1, 2, 2, 3, 3, 3, 3, 2, 2], # x index + [x0, x3, x2, x1], # unique x positions + [y0, y4, y2, y3, y1, y1, y3, y3, y2 + verre, y2 - verre, y4, y4, y0], + materials, # materials + side_cap_front=8, + side_cap_back=9 # cap index + ) + + @property + def verts(self): + offset = Vector((0, 0, 0)) + return self.window.vertices(self.curve_steps, offset, self.center, self.origin, self.size, + self.radius, self.angle_y, self.pivot, shape_z=None, path_type=self.shape) + + @property + def faces(self): + return self.window.faces(self.curve_steps, path_type=self.shape) + + @property + def matids(self): + return self.window.mat(self.curve_steps, 2, 2, path_type=self.shape) + + @property + def uvs(self): + return self.window.uv(self.curve_steps, self.center, self.origin, self.size, + self.radius, self.angle_y, self.pivot, 0, self.frame_x, path_type=self.shape) + + def find_handle(self, o): + for child in o.children: + if 'archipack_handle' in child: + return child + return None + + def update_handle(self, context, o): + handle = self.find_handle(o) + if handle is None: + m = bpy.data.meshes.new("Handle") + handle = create_handle(context, o, m) + MaterialUtils.add_handle_materials(handle) + if self.handle_model == 1: + verts, faces = window_handle_vertical_01(1) + else: + verts, faces = window_handle_vertical_02(1) + handle.location = (self.pivot * (self.size.x - 0.4 * self.frame_x), 0, self.handle_altitude) + bmed.buildmesh(context, handle, verts, faces) + + def remove_handle(self, context, o): + handle = self.find_handle(o) + if handle is not None: + context.scene.objects.unlink(handle) + bpy.data.objects.remove(handle, do_unlink=True) + + def update(self, context): + + o = self.find_in_selection(context) + + if o is None: + return + + # update handle, dosent care of instances as window will do + if self.handle == 'NONE': + self.remove_handle(context, o) + else: + self.update_handle(context, o) + + bmed.buildmesh(context, o, self.verts, self.faces, self.matids, self.uvs) + + self.restore_context(context) + + +class archipack_window(ArchipackObject, Manipulable, PropertyGroup): + x = FloatProperty( + name='width', + min=0.25, + default=100.0, precision=2, step=1, + unit='LENGTH', subtype='DISTANCE', + description='Width', update=update + ) + y = FloatProperty( + name='depth', + min=0.1, + default=0.20, precision=2, step=1, + unit='LENGTH', subtype='DISTANCE', + description='Depth', update=update, + ) + z = FloatProperty( + name='height', + min=0.1, + default=1.2, precision=2, step=1, + unit='LENGTH', subtype='DISTANCE', + description='height', update=update, + ) + angle_y = FloatProperty( + name='angle', + unit='ROTATION', + subtype='ANGLE', + min=-1.5, max=1.5, + default=0, precision=2, + description='angle', update=update, + ) + radius = FloatProperty( + name='radius', + min=0.1, + default=2.5, precision=2, step=1, + unit='LENGTH', subtype='DISTANCE', + description='radius', update=update, + ) + elipsis_b = FloatProperty( + name='ellipsis', + min=0.1, + default=0.5, precision=2, step=1, + unit='LENGTH', subtype='DISTANCE', + description='ellipsis vertical size', update=update, + ) + altitude = FloatProperty( + name='altitude', + default=1.0, precision=2, step=1, + unit='LENGTH', subtype='DISTANCE', + description='altitude', update=update, + ) + offset = FloatProperty( + name='offset', + default=0.1, precision=2, step=1, + unit='LENGTH', subtype='DISTANCE', + description='offset', update=update, + ) + frame_y = FloatProperty( + name='Depth', + min=0, + default=0.06, precision=2, step=1, + unit='LENGTH', subtype='DISTANCE', + description='frame depth', update=update, + ) + frame_x = FloatProperty( + name='Width', + min=0, + default=0.06, precision=2, step=1, + unit='LENGTH', subtype='DISTANCE', + description='frame width', update=update, + ) + out_frame = BoolProperty( + name="Out frame", + default=False, update=update, + ) + out_frame_y = FloatProperty( + name='Side depth', + min=0.001, + default=0.02, precision=2, step=1, + unit='LENGTH', subtype='DISTANCE', + description='frame side depth', update=update, + ) + out_frame_y2 = FloatProperty( + name='Front depth', + min=0.001, + default=0.02, precision=2, step=1, + unit='LENGTH', subtype='DISTANCE', + description='frame front depth', update=update, + ) + out_frame_x = FloatProperty( + name='Front Width', + min=0.0, + default=0.1, precision=2, step=1, + unit='LENGTH', subtype='DISTANCE', + description='frame width set to 0 disable front frame', update=update, + ) + out_frame_offset = FloatProperty( + name='offset', + min=0.0, + default=0.0, precision=3, step=0.1, + unit='LENGTH', subtype='DISTANCE', + description='frame offset', update=update, + ) + out_tablet_enable = BoolProperty( + name="Out tablet", + default=True, update=update, + ) + out_tablet_x = FloatProperty( + name='Width', + min=0.0, + default=0.04, precision=2, step=1, + unit='LENGTH', subtype='DISTANCE', + description='tablet width', update=update, + ) + out_tablet_y = FloatProperty( + name='Depth', + min=0.001, + default=0.04, precision=2, step=1, + unit='LENGTH', subtype='DISTANCE', + description='tablet depth', update=update, + ) + out_tablet_z = FloatProperty( + name='Height', + min=0.001, + default=0.03, precision=2, step=1, + unit='LENGTH', subtype='DISTANCE', + description='tablet height', update=update, + ) + in_tablet_enable = BoolProperty( + name="In tablet", + default=True, update=update, + ) + in_tablet_x = FloatProperty( + name='Width', + min=0.0, + default=0.04, precision=2, step=1, + unit='LENGTH', subtype='DISTANCE', + description='tablet width', update=update, + ) + in_tablet_y = FloatProperty( + name='Depth', + min=0.001, + default=0.04, precision=2, step=1, + unit='LENGTH', subtype='DISTANCE', + description='tablet depth', update=update, + ) + in_tablet_z = FloatProperty( + name='Height', + min=0.001, + default=0.03, precision=2, step=1, + unit='LENGTH', subtype='DISTANCE', + description='tablet height', update=update, + ) + blind_enable = BoolProperty( + name="Blind", + default=False, update=update, + ) + blind_y = FloatProperty( + name='Depth', + min=0.001, + default=0.002, precision=2, step=1, + unit='LENGTH', subtype='DISTANCE', + description='Store depth', update=update, + ) + blind_z = FloatProperty( + name='Height', + min=0.001, + default=0.03, precision=2, step=1, + unit='LENGTH', subtype='DISTANCE', + description='Store height', update=update, + ) + blind_open = FloatProperty( + name='Open', + min=0.0, max=100, + default=80, precision=1, + subtype='PERCENTAGE', + description='Store open', update=update, + ) + rows = CollectionProperty(type=archipack_window_panelrow) + n_rows = IntProperty( + name="number of rows", + min=1, + max=32, + default=1, update=update, + ) + curve_steps = IntProperty( + name="curve steps", + min=6, + max=128, + default=16, update=update, + ) + hole_outside_mat = IntProperty( + name="Outside", + min=0, + max=128, + default=0, update=update, + ) + hole_inside_mat = IntProperty( + name="Inside", + min=0, + max=128, + default=1, update=update, + ) + window_shape = EnumProperty( + name='Shape', + items=( + ('RECTANGLE', 'Rectangle', '', 0), + ('ROUND', 'Top Round', '', 1), + ('ELLIPSIS', 'Top elliptic', '', 2), + ('QUADRI', 'Top oblique', '', 3), + ('CIRCLE', 'Full circle', '', 4) + ), + default='RECTANGLE', update=update, + ) + window_type = EnumProperty( + name='Type', + items=( + ('FLAT', 'Flat window', '', 0), + ('RAIL', 'Rail window', '', 1) + ), + default='FLAT', update=update, + ) + warning = BoolProperty( + name="warning", + default=False + ) + handle_enable = BoolProperty( + name='handle', + default=True, update=update_childs, + ) + handle_altitude = FloatProperty( + name="altitude", + min=0, + default=1.4, precision=2, step=1, + unit='LENGTH', subtype='DISTANCE', + description='handle altitude', update=update_childs, + ) + hole_margin = FloatProperty( + name='hole margin', + min=0.0, + default=0.1, precision=2, step=1, + unit='LENGTH', subtype='DISTANCE', + description='how much hole surround wall' + ) + flip = BoolProperty( + default=False, + update=update, + description='flip outside and outside material of hole' + ) + # layout related + display_detail = BoolProperty( + options={'SKIP_SAVE'}, + default=False + ) + display_panels = BoolProperty( + options={'SKIP_SAVE'}, + default=True + ) + display_materials = BoolProperty( + options={'SKIP_SAVE'}, + default=True + ) + + auto_update = BoolProperty( + options={'SKIP_SAVE'}, + default=True, + update=update + ) + + @property + def shape(self): + if self.window_type == 'RAIL': + return 'RECTANGLE' + else: + return self.window_shape + + @property + def window(self): + # Flat window frame profil + # ___ y1 + # | |__ + # | | y2 + # |______| y0 + # + x0 = 0 + x1 = -x0 - self.frame_x + x2 = x0 + 0.5 * self.frame_x + y0 = 0.5 * self.y - self.offset + y2 = y0 + 0.5 * self.frame_y + + if self.window_type == 'FLAT': + y1 = y0 + self.frame_y + return WindowPanel( + True, # closed + [0, 0, 1, 1, 2, 2], # x index + [x1, x0, x2], + [y0, y1, y1, y2, y2, y0], + [1, 1, 1, 1, 0, 0] # material index + ) + else: + # Rail window frame profil + # ________ y1 + # | __| y5 + # | |__ y4 + # | __| y3 + # | |____ + # | | y2 + # |__________| y0 + # -x1 0 x3 x2 + x2 = x0 + 0.35 * self.frame_y + x3 = x0 + 0.2 * self.frame_x + y1 = y0 + 2.55 * self.frame_y + y3 = y0 + 1.45 * self.frame_y + y4 = y0 + 1.55 * self.frame_y + y5 = y0 + 2.45 * self.frame_y + + return WindowPanel( + True, # closed + [0, 0, 2, 2, 1, 1, 2, 2, 1, 1, 3, 3], # x index + [x1, x0, x3, x2], + [y0, y1, y1, y5, y5, y4, y4, y3, y3, y2, y2, y0], + [1, 1, 1, 3, 1, 3, 3, 3, 0, 3, 0, 0] # material index + ) + + @property + def hole(self): + # profil percement ____ + # _____ y_inside vertical ___| x1 + # | + # |__ y0 outside ___ + # |___ y_outside |____ x1-shape_z inside + # -x1 x0 + y0 = 0.5 * self.y - self.offset + x1 = self.frame_x # sur-largeur percement interieur + y_inside = 0.5 * self.y + self.hole_margin # outside wall + + if self.out_frame is False: + x0 = 0 + else: + x0 = -min(self.frame_x - 0.001, self.out_frame_y + self.out_frame_offset) + + outside_mat = self.hole_outside_mat + inside_mat = self.hole_inside_mat + # if self.flip: + # outside_mat, inside_mat = inside_mat, outside_mat + + y_outside = -y_inside # inside wall + + return WindowPanel( + False, # closed + [1, 1, 0, 0], # x index + [-x1, x0], + [y_outside, y0, y0, y_inside], + [outside_mat, outside_mat, inside_mat], # material index + side_cap_front=3, # cap index + side_cap_back=0 + ) + + @property + def frame(self): + # profil cadre + # ___ y0 + # __| | + # | | y2 + # |______| y1 + # x1 x2 x0 + y2 = -0.5 * self.y + y0 = 0.5 * self.y - self.offset + y1 = y2 - self.out_frame_y2 + x0 = 0 # -min(self.frame_x - 0.001, self.out_frame_offset) + x1 = x0 - self.out_frame_x + x2 = x0 - self.out_frame_y + # y = depth + # x = width + if self.out_frame_x <= self.out_frame_y: + if self.out_frame_x == 0: + pts_y = [y2, y0, y0, y2] + else: + pts_y = [y1, y0, y0, y1] + return WindowPanel( + True, # closed profil + [0, 0, 1, 1], # x index + [x2, x0], + pts_y, + [0, 0, 0, 0], # material index + closed_path=bool(self.shape == 'CIRCLE') # closed path + ) + else: + return WindowPanel( + True, # closed profil + [0, 0, 1, 1, 2, 2], # x index + [x1, x2, x0], + [y1, y2, y2, y0, y0, y1], + [0, 0, 0, 0, 0, 0], # material index + closed_path=bool(self.shape == 'CIRCLE') # closed path + ) + + @property + def out_tablet(self): + # profil tablette + # __ y0 + # | | y2 + # | / y3 + # |_| y1 + # x0 x2 x1 + y0 = 0.001 + 0.5 * self.y - self.offset + y1 = -0.5 * self.y - self.out_tablet_y + y2 = y0 - 0.01 + y3 = y2 - 0.04 + x2 = 0 + x0 = x2 - self.out_tablet_z + x1 = 0.3 * self.frame_x + # y = depth + # x = width1 + return WindowPanel( + True, # closed profil + [1, 1, 2, 2, 0, 0], # x index + [x0, x2, x1], + [y1, y3, y2, y0, y0, y1], + [4, 3, 3, 4, 4, 4], # material index + closed_path=False # closed path + ) + + @property + def in_tablet(self): + # profil tablette + # __ y0 + # | | + # | | + # |__| y1 + # x0 x1 + y0 = 0.5 * self.y + self.frame_y - self.offset + y1 = 0.5 * self.y + self.in_tablet_y + if self.window_type == 'RAIL': + y0 += 1.55 * self.frame_y + y1 += 1.55 * self.frame_y + x0 = -self.frame_x + x1 = min(x0 + self.in_tablet_z, x0 + self.frame_x - 0.001) + # y = depth + # x = width1 + return WindowPanel( + True, # closed profil + [0, 0, 1, 1], # x index + [x0, x1], + [y1, y0, y0, y1], + [1, 1, 1, 1], # material index + closed_path=False # closed path + ) + + @property + def blind(self): + # profil blind + # y0 + # | / | / | / + # y1 + # xn x1 x0 + dx = self.z / self.blind_z + nx = int(self.z / dx) + y0 = -0.5 * self.offset + # -0.5 * self.y + 0.5 * (0.5 * self.y - self.offset) + # 0.5 * (-0.5 * self.y-0.5 * self.offset) + y1 = y0 + self.blind_y + nx = int(self.z / self.blind_z) + dx = self.z / nx + open = 1.0 - self.blind_open / 100 + return WindowPanel( + False, # profil closed + [int((i + (i % 2)) / 2) for i in range(2 * nx)], # x index + [self.z - (dx * i * open) for i in range(nx + 1)], # x + [[y1, y0][i % 2] for i in range(2 * nx)], # + [5 for i in range(2 * nx - 1)], # material index + closed_path=False # + ) + + @property + def verts(self): + center, origin, size, radius = self.get_radius() + is_not_circle = self.shape != 'CIRCLE' + offset = Vector((0, self.altitude, 0)) + verts = self.window.vertices(self.curve_steps, offset, center, origin, + size, radius, self.angle_y, 0, shape_z=None, path_type=self.shape) + if self.out_frame: + verts += self.frame.vertices(self.curve_steps, offset, center, origin, + size, radius, self.angle_y, 0, shape_z=None, path_type=self.shape) + if is_not_circle and self.out_tablet_enable: + verts += self.out_tablet.vertices(self.curve_steps, offset, center, origin, + Vector((size.x + 2 * self.out_tablet_x, size.y, size.z)), + radius, self.angle_y, 0, shape_z=None, path_type='HORIZONTAL') + if is_not_circle and self.in_tablet_enable: + verts += self.in_tablet.vertices(self.curve_steps, offset, center, origin, + Vector((size.x + 2 * (self.frame_x + self.in_tablet_x), size.y, size.z)), + radius, self.angle_y, 0, shape_z=None, path_type='HORIZONTAL') + if is_not_circle and self.blind_enable: + verts += self.blind.vertices(self.curve_steps, offset, center, origin, + Vector((-size.x, 0, 0)), radius, 0, 0, shape_z=None, path_type='HORIZONTAL') + return verts + + @property + def faces(self): + window = self.window + faces = window.faces(self.curve_steps, path_type=self.shape) + verts_offset = window.n_verts(self.curve_steps, path_type=self.shape) + is_not_circle = self.shape != 'CIRCLE' + if self.out_frame: + frame = self.frame + faces += frame.faces(self.curve_steps, path_type=self.shape, offset=verts_offset) + verts_offset += frame.n_verts(self.curve_steps, path_type=self.shape) + if is_not_circle and self.out_tablet_enable: + tablet = self.out_tablet + faces += tablet.faces(self.curve_steps, path_type='HORIZONTAL', offset=verts_offset) + verts_offset += tablet.n_verts(self.curve_steps, path_type='HORIZONTAL') + if is_not_circle and self.in_tablet_enable: + tablet = self.in_tablet + faces += tablet.faces(self.curve_steps, path_type='HORIZONTAL', offset=verts_offset) + verts_offset += tablet.n_verts(self.curve_steps, path_type='HORIZONTAL') + if is_not_circle and self.blind_enable: + blind = self.blind + faces += blind.faces(self.curve_steps, path_type='HORIZONTAL', offset=verts_offset) + verts_offset += blind.n_verts(self.curve_steps, path_type='HORIZONTAL') + + return faces + + @property + def matids(self): + mat = self.window.mat(self.curve_steps, 2, 2, path_type=self.shape) + is_not_circle = self.shape != 'CIRCLE' + if self.out_frame: + mat += self.frame.mat(self.curve_steps, 0, 0, path_type=self.shape) + if is_not_circle and self.out_tablet_enable: + mat += self.out_tablet.mat(self.curve_steps, 0, 0, path_type='HORIZONTAL') + if is_not_circle and self.in_tablet_enable: + mat += self.in_tablet.mat(self.curve_steps, 0, 0, path_type='HORIZONTAL') + if is_not_circle and self.blind_enable: + mat += self.blind.mat(self.curve_steps, 0, 0, path_type='HORIZONTAL') + return mat + + @property + def uvs(self): + center, origin, size, radius = self.get_radius() + uvs = self.window.uv(self.curve_steps, center, origin, size, radius, + self.angle_y, 0, 0, self.frame_x, path_type=self.shape) + is_not_circle = self.shape != 'CIRCLE' + if self.out_frame: + uvs += self.frame.uv(self.curve_steps, center, origin, size, radius, + self.angle_y, 0, 0, self.frame_x, path_type=self.shape) + if is_not_circle and self.out_tablet_enable: + uvs += self.out_tablet.uv(self.curve_steps, center, origin, size, radius, + self.angle_y, 0, 0, self.frame_x, path_type='HORIZONTAL') + if is_not_circle and self.in_tablet_enable: + uvs += self.in_tablet.uv(self.curve_steps, center, origin, size, radius, + self.angle_y, 0, 0, self.frame_x, path_type='HORIZONTAL') + if is_not_circle and self.blind_enable: + uvs += self.blind.uv(self.curve_steps, center, origin, size, radius, + self.angle_y, 0, 0, self.frame_x, path_type='HORIZONTAL') + return uvs + + def setup_manipulators(self): + if len(self.manipulators) == 4: + return + s = self.manipulators.add() + s.prop1_name = "x" + s.prop2_name = "x" + s.type_key = "SNAP_SIZE_LOC" + s = self.manipulators.add() + s.prop1_name = "y" + s.prop2_name = "y" + s.type_key = "SNAP_SIZE_LOC" + s = self.manipulators.add() + s.prop1_name = "z" + s.normal = Vector((0, 1, 0)) + s = self.manipulators.add() + s.prop1_name = "altitude" + s.normal = Vector((0, 1, 0)) + + def remove_childs(self, context, o, to_remove): + for child in o.children: + if to_remove < 1: + return + if archipack_window_panel.filter(child): + to_remove -= 1 + self.remove_handle(context, child) + context.scene.objects.unlink(child) + bpy.data.objects.remove(child, do_unlink=True) + + def remove_handle(self, context, o): + handle = self.find_handle(o) + if handle is not None: + context.scene.objects.unlink(handle) + bpy.data.objects.remove(handle, do_unlink=True) + + def update_rows(self, context, o): + # remove rows + for i in range(len(self.rows), self.n_rows, -1): + self.rows.remove(i - 1) + + # add rows + for i in range(len(self.rows), self.n_rows): + self.rows.add() + + # wanted childs + if self.shape == 'CIRCLE': + w_childs = 1 + elif self.window_type == 'RAIL': + w_childs = self.rows[0].cols + else: + w_childs = sum([row.cols for row in self.rows]) + + # real childs + childs = self.get_childs_panels(context, o) + n_childs = len(childs) + + # remove child + if n_childs > w_childs: + self.remove_childs(context, o, n_childs - w_childs) + + def get_childs_panels(self, context, o): + return [child for child in o.children if archipack_window_panel.filter(child)] + + def adjust_size_and_origin(self, size, origin, pivot, materials): + if len(size) > 1: + size[0].x += 0.5 * self.frame_x + size[-1].x += 0.5 * self.frame_x + for i in range(1, len(size) - 1): + size[i].x += 0.5 * self.frame_x + origin[i].x += -0.25 * self.frame_x * pivot[i] + for i, o in enumerate(origin): + o.y = (1 - (i % 2)) * self.frame_y + for i, o in enumerate(origin): + materials[i] = (1 - (i % 2)) + 1 + + def find_handle(self, o): + for handle in o.children: + if 'archipack_handle' in handle: + return handle + return None + + def _synch_childs(self, context, o, linked, childs): + """ + sub synch childs nodes of linked object + """ + # remove childs not found on source + l_childs = self.get_childs_panels(context, linked) + c_names = [c.data.name for c in childs] + for c in l_childs: + try: + id = c_names.index(c.data.name) + except: + self.remove_handle(context, c) + context.scene.objects.unlink(c) + bpy.data.objects.remove(c, do_unlink=True) + + # children ordering may not be the same, so get the right l_childs order + l_childs = self.get_childs_panels(context, linked) + l_names = [c.data.name for c in l_childs] + order = [] + for c in childs: + try: + id = l_names.index(c.data.name) + except: + id = -1 + order.append(id) + + # add missing childs and update other ones + for i, child in enumerate(childs): + if order[i] < 0: + p = bpy.data.objects.new("Panel", child.data) + context.scene.objects.link(p) + p.lock_location[0] = True + p.lock_location[1] = True + p.lock_location[2] = True + p.lock_rotation[1] = True + p.lock_scale[0] = True + p.lock_scale[1] = True + p.lock_scale[2] = True + p.parent = linked + p.matrix_world = linked.matrix_world.copy() + + else: + p = l_childs[order[i]] + + # update handle + handle = self.find_handle(child) + h = self.find_handle(p) + if handle is not None: + if h is None: + h = create_handle(context, p, handle.data) + MaterialUtils.add_handle_materials(h) + h.location = handle.location.copy() + elif h is not None: + context.scene.objects.unlink(h) + bpy.data.objects.remove(h, do_unlink=True) + + p.location = child.location.copy() + + def _synch_hole(self, context, linked, hole): + l_hole = self.find_hole(linked) + if l_hole is None: + l_hole = bpy.data.objects.new("hole", hole.data) + l_hole['archipack_hole'] = True + context.scene.objects.link(l_hole) + l_hole.parent = linked + l_hole.matrix_world = linked.matrix_world.copy() + l_hole.location = hole.location.copy() + else: + l_hole.data = hole.data + + def synch_childs(self, context, o): + """ + synch childs nodes of linked objects + """ + bpy.ops.object.select_all(action='DESELECT') + o.select = True + context.scene.objects.active = o + childs = self.get_childs_panels(context, o) + hole = self.find_hole(o) + bpy.ops.object.select_linked(type='OBDATA') + for linked in context.selected_objects: + if linked != o: + self._synch_childs(context, o, linked, childs) + if hole is not None: + self._synch_hole(context, linked, hole) + + def update_childs(self, context, o): + """ + pass params to childrens + """ + self.update_rows(context, o) + childs = self.get_childs_panels(context, o) + n_childs = len(childs) + child_n = 0 + row_n = 0 + location_y = 0.5 * self.y - self.offset + 0.5 * self.frame_y + center, origin, size, radius = self.get_radius() + offset = Vector((0, 0)) + handle = 'NONE' + if self.shape != 'CIRCLE': + if self.handle_enable: + if self.z > 1.8: + handle = 'BOTH' + else: + handle = 'INSIDE' + is_circle = False + else: + is_circle = True + + if self.window_type == 'RAIL': + handle_model = 2 + else: + handle_model = 1 + + for row in self.rows: + row_n += 1 + if row_n < self.n_rows and not is_circle and self.window_type != 'RAIL': + z = row.height + shape = 'RECTANGLE' + else: + z = max(2 * self.frame_x + 0.001, self.z - offset.y) + shape = self.shape + + self.warning = bool(z > self.z - offset.y) + if self.warning: + break + size, origin, pivot = row.get_row(self.x, z) + # side materials + materials = [0 for i in range(row.cols)] + + handle_altitude = min( + max(4 * self.frame_x, self.handle_altitude - offset.y - self.altitude), + z - 4 * self.frame_x + ) + + if self.window_type == 'RAIL': + self.adjust_size_and_origin(size, origin, pivot, materials) + + for panel in range(row.cols): + child_n += 1 + + if row.fixed[panel]: + enable_handle = 'NONE' + else: + enable_handle = handle + + if child_n > n_childs: + bpy.ops.archipack.window_panel( + center=center, + origin=Vector((origin[panel].x, offset.y, 0)), + size=size[panel], + radius=radius, + pivot=pivot[panel], + shape=shape, + fixed=row.fixed[panel], + handle=enable_handle, + handle_model=handle_model, + handle_altitude=handle_altitude, + curve_steps=self.curve_steps, + side_material=materials[panel], + frame_x=self.frame_x, + frame_y=self.frame_y, + angle_y=self.angle_y, + ) + child = context.active_object + # parenting at 0, 0, 0 before set object matrix_world + # so location remains local from frame + child.parent = o + child.matrix_world = o.matrix_world.copy() + else: + child = childs[child_n - 1] + child.select = True + context.scene.objects.active = child + props = archipack_window_panel.datablock(child) + if props is not None: + props.origin = Vector((origin[panel].x, offset.y, 0)) + props.center = center + props.radius = radius + props.size = size[panel] + props.pivot = pivot[panel] + props.shape = shape + props.fixed = row.fixed[panel] + props.handle = enable_handle + props.handle_model = handle_model + props.handle_altitude = handle_altitude + props.side_material = materials[panel] + props.curve_steps = self.curve_steps + props.frame_x = self.frame_x + props.frame_y = self.frame_y + props.angle_y = self.angle_y + props.update(context) + # location y + frame width. frame depends on choosen profile (fixed or not) + # update linked childs location too + child.location = Vector((origin[panel].x, origin[panel].y + location_y + self.frame_y, + self.altitude + offset.y)) + + if not row.fixed[panel]: + handle = 'NONE' + + # only one single panel allowed for circle + if is_circle: + return + + # only one single row allowed for rail window + if self.window_type == 'RAIL': + return + offset.y += row.height + + def _get_tri_radius(self): + return Vector((0, self.y, 0)), Vector((0, 0, 0)), \ + Vector((self.x, self.z, 0)), Vector((self.x, 0, 0)) + + def _get_quad_radius(self): + fx_z = self.z / self.x + center_y = min(self.x / (self.x - self.frame_x) * self.z - self.frame_x * (1 + sqrt(1 + fx_z * fx_z)), + abs(tan(self.angle_y) * (self.x))) + if self.angle_y < 0: + center_x = 0.5 * self.x + else: + center_x = -0.5 * self.x + return Vector((center_x, center_y, 0)), Vector((0, 0, 0)), \ + Vector((self.x, self.z, 0)), Vector((self.x, 0, 0)) + + def _get_round_radius(self): + """ + bound radius to available space + return center, origin, size, radius + """ + x = 0.5 * self.x - self.frame_x + # minimum space available + y = self.z - sum([row.height for row in self.rows[:self.n_rows - 1]]) - 2 * self.frame_x + y = min(y, x) + # minimum radius inside + r = y + x * (x - (y * y / x)) / (2 * y) + radius = max(self.radius, 0.001 + self.frame_x + r) + return Vector((0, self.z - radius, 0)), Vector((0, 0, 0)), \ + Vector((self.x, self.z, 0)), Vector((radius, 0, 0)) + + def _get_circle_radius(self): + """ + return center, origin, size, radius + """ + return Vector((0, 0.5 * self.x, 0)), Vector((0, 0, 0)), \ + Vector((self.x, self.z, 0)), Vector((0.5 * self.x, 0, 0)) + + def _get_ellipsis_radius(self): + """ + return center, origin, size, radius + """ + y = self.z - sum([row.height for row in self.rows[:self.n_rows - 1]]) + radius_b = max(0, 0.001 - 2 * self.frame_x + min(y, self.elipsis_b)) + return Vector((0, self.z - radius_b, 0)), Vector((0, 0, 0)), \ + Vector((self.x, self.z, 0)), Vector((self.x / 2, radius_b, 0)) + + def get_radius(self): + """ + return center, origin, size, radius + """ + if self.shape == 'ROUND': + return self._get_round_radius() + elif self.shape == 'ELLIPSIS': + return self._get_ellipsis_radius() + elif self.shape == 'CIRCLE': + return self._get_circle_radius() + elif self.shape == 'QUADRI': + return self._get_quad_radius() + elif self.shape in ['TRIANGLE', 'PENTAGON']: + return self._get_tri_radius() + else: + return Vector((0, 0, 0)), Vector((0, 0, 0)), \ + Vector((self.x, self.z, 0)), Vector((0, 0, 0)) + + def update(self, context, childs_only=False): + # support for "copy to selected" + o = self.find_in_selection(context, self.auto_update) + + if o is None: + return + + self.setup_manipulators() + + if childs_only is False: + bmed.buildmesh(context, o, self.verts, self.faces, self.matids, self.uvs) + + self.update_childs(context, o) + + # update hole + if childs_only is False and self.find_hole(o) is not None: + self.interactive_hole(context, o) + + # support for instances childs, update at object level + self.synch_childs(context, o) + + # store 3d points for gl manipulators + x, y = 0.5 * self.x, 0.5 * self.y + self.manipulators[0].set_pts([(-x, -y, 0), (x, -y, 0), (1, 0, 0)]) + self.manipulators[1].set_pts([(-x, -y, 0), (-x, y, 0), (-1, 0, 0)]) + self.manipulators[2].set_pts([(x, -y, self.altitude), (x, -y, self.altitude + self.z), (-1, 0, 0)]) + self.manipulators[3].set_pts([(x, -y, 0), (x, -y, self.altitude), (-1, 0, 0)]) + + # restore context + self.restore_context(context) + + def find_hole(self, o): + for child in o.children: + if 'archipack_hole' in child: + return child + return None + + def interactive_hole(self, context, o): + hole_obj = self.find_hole(o) + + if hole_obj is None: + m = bpy.data.meshes.new("hole") + hole_obj = bpy.data.objects.new("hole", m) + context.scene.objects.link(hole_obj) + hole_obj['archipack_hole'] = True + hole_obj.parent = o + hole_obj.matrix_world = o.matrix_world.copy() + MaterialUtils.add_wall2_materials(hole_obj) + + hole = self.hole + center, origin, size, radius = self.get_radius() + + if self.out_frame is False: + x0 = 0 + else: + x0 = min(self.frame_x - 0.001, self.out_frame_y + self.out_frame_offset) + + if self.out_tablet_enable: + x0 -= min(self.frame_x - 0.001, self.out_tablet_z) + shape_z = [0, x0] + + verts = hole.vertices(self.curve_steps, Vector((0, self.altitude, 0)), center, origin, size, radius, + self.angle_y, 0, shape_z=shape_z, path_type=self.shape) + + faces = hole.faces(self.curve_steps, path_type=self.shape) + + matids = hole.mat(self.curve_steps, 2, 2, path_type=self.shape) + + uvs = hole.uv(self.curve_steps, center, origin, size, radius, + self.angle_y, 0, 0, self.frame_x, path_type=self.shape) + + bmed.buildmesh(context, hole_obj, verts, faces, matids=matids, uvs=uvs) + return hole_obj + + def robust_hole(self, context, tM): + hole = self.hole + center, origin, size, radius = self.get_radius() + + if self.out_frame is False: + x0 = 0 + else: + x0 = min(self.frame_x - 0.001, self.out_frame_y + self.out_frame_offset) + + if self.out_tablet_enable: + x0 -= min(self.frame_x - 0.001, self.out_tablet_z) + shape_z = [0, x0] + + m = bpy.data.meshes.new("hole") + o = bpy.data.objects.new("hole", m) + o['archipack_robusthole'] = True + context.scene.objects.link(o) + verts = hole.vertices(self.curve_steps, Vector((0, self.altitude, 0)), center, origin, size, radius, + self.angle_y, 0, shape_z=shape_z, path_type=self.shape) + + verts = [tM * Vector(v) for v in verts] + + faces = hole.faces(self.curve_steps, path_type=self.shape) + + matids = hole.mat(self.curve_steps, 2, 2, path_type=self.shape) + + uvs = hole.uv(self.curve_steps, center, origin, size, radius, + self.angle_y, 0, 0, self.frame_x, path_type=self.shape) + + bmed.buildmesh(context, o, verts, faces, matids=matids, uvs=uvs) + MaterialUtils.add_wall2_materials(o) + o.select = True + context.scene.objects.active = o + return o + + +class ARCHIPACK_PT_window(Panel): + bl_idname = "ARCHIPACK_PT_window" + bl_label = "Window" + bl_space_type = 'VIEW_3D' + bl_region_type = 'UI' + # bl_context = 'object' + bl_category = 'ArchiPack' + + # layout related + display_detail = BoolProperty( + default=False + ) + display_panels = BoolProperty( + default=True + ) + + @classmethod + def poll(cls, context): + return archipack_window.filter(context.active_object) + + def draw(self, context): + o = context.active_object + prop = archipack_window.datablock(o) + if prop is None: + return + layout = self.layout + layout.operator('archipack.window_manipulate', icon='HAND') + row = layout.row(align=True) + row.operator('archipack.window', text="Refresh", icon='FILE_REFRESH').mode = 'REFRESH' + if o.data.users > 1: + row.operator('archipack.window', text="Make unique", icon='UNLINKED').mode = 'UNIQUE' + row.operator('archipack.window', text="Delete", icon='ERROR').mode = 'DELETE' + box = layout.box() + # box.label(text="Styles") + row = box.row(align=True) + row.operator("archipack.window_preset_menu", text=bpy.types.ARCHIPACK_OT_window_preset_menu.bl_label) + row.operator("archipack.window_preset", text="", icon='ZOOMIN') + row.operator("archipack.window_preset", text="", icon='ZOOMOUT').remove_active = True + box = layout.box() + box.prop(prop, 'window_type') + box.prop(prop, 'x') + box.prop(prop, 'y') + if prop.window_shape != 'CIRCLE': + box.prop(prop, 'z') + if prop.warning: + box.label(text="Insufficient height", icon='ERROR') + box.prop(prop, 'altitude') + box.prop(prop, 'offset') + + if prop.window_type == 'FLAT': + box = layout.box() + box.prop(prop, 'window_shape') + if prop.window_shape in ['ROUND', 'CIRCLE', 'ELLIPSIS']: + box.prop(prop, 'curve_steps') + if prop.window_shape in ['ROUND']: + box.prop(prop, 'radius') + elif prop.window_shape == 'ELLIPSIS': + box.prop(prop, 'elipsis_b') + elif prop.window_shape == 'QUADRI': + box.prop(prop, 'angle_y') + + row = layout.row(align=True) + if prop.display_detail: + row.prop(prop, "display_detail", icon="TRIA_DOWN", icon_only=True, text="Components", emboss=False) + else: + row.prop(prop, "display_detail", icon="TRIA_RIGHT", icon_only=True, text="Components", emboss=False) + + if prop.display_detail: + box = layout.box() + box.label("Frame") + box.prop(prop, 'frame_x') + box.prop(prop, 'frame_y') + if prop.window_shape != 'CIRCLE': + box = layout.box() + row = box.row(align=True) + row.prop(prop, 'handle_enable') + if prop.handle_enable: + box.prop(prop, 'handle_altitude') + box = layout.box() + row = box.row(align=True) + row.prop(prop, 'out_frame') + if prop.out_frame: + box.prop(prop, 'out_frame_x') + box.prop(prop, 'out_frame_y2') + box.prop(prop, 'out_frame_y') + box.prop(prop, 'out_frame_offset') + if prop.window_shape != 'CIRCLE': + box = layout.box() + row = box.row(align=True) + row.prop(prop, 'out_tablet_enable') + if prop.out_tablet_enable: + box.prop(prop, 'out_tablet_x') + box.prop(prop, 'out_tablet_y') + box.prop(prop, 'out_tablet_z') + box = layout.box() + row = box.row(align=True) + row.prop(prop, 'in_tablet_enable') + if prop.in_tablet_enable: + box.prop(prop, 'in_tablet_x') + box.prop(prop, 'in_tablet_y') + box.prop(prop, 'in_tablet_z') + box = layout.box() + row = box.row(align=True) + row.prop(prop, 'blind_enable') + if prop.blind_enable: + box.prop(prop, 'blind_open') + box.prop(prop, 'blind_y') + box.prop(prop, 'blind_z') + if prop.window_shape != 'CIRCLE': + row = layout.row() + if prop.display_panels: + row.prop(prop, "display_panels", icon="TRIA_DOWN", icon_only=True, text="Rows", emboss=False) + else: + row.prop(prop, "display_panels", icon="TRIA_RIGHT", icon_only=True, text="Rows", emboss=False) + + if prop.display_panels: + if prop.window_type != 'RAIL': + row = layout.row() + row.prop(prop, 'n_rows') + last_row = prop.n_rows - 1 + for i, row in enumerate(prop.rows): + box = layout.box() + box.label(text="Row " + str(i + 1)) + row.draw(box, context, i == last_row) + else: + box = layout.box() + row = prop.rows[0] + row.draw(box, context, True) + + row = layout.row(align=True) + if prop.display_materials: + row.prop(prop, "display_materials", icon="TRIA_DOWN", icon_only=True, text="Materials", emboss=False) + else: + row.prop(prop, "display_materials", icon="TRIA_RIGHT", icon_only=True, text="Materials", emboss=False) + if prop.display_materials: + box = layout.box() + box.label("Hole") + box.prop(prop, 'hole_inside_mat') + box.prop(prop, 'hole_outside_mat') + + +class ARCHIPACK_PT_window_panel(Panel): + bl_idname = "ARCHIPACK_PT_window_panel" + bl_label = "Window panel" + bl_space_type = 'VIEW_3D' + bl_region_type = 'UI' + bl_category = 'ArchiPack' + + @classmethod + def poll(cls, context): + return archipack_window_panel.filter(context.active_object) + + def draw(self, context): + layout = self.layout + layout.operator("archipack.select_parent") + + +# ------------------------------------------------------------------ +# Define operator class to create object +# ------------------------------------------------------------------ + + +class ARCHIPACK_OT_window(ArchipackCreateTool, Operator): + bl_idname = "archipack.window" + bl_label = "Window" + bl_description = "Window" + bl_category = 'Archipack' + bl_options = {'REGISTER', 'UNDO'} + x = FloatProperty( + name='width', + min=0.1, max=10000, + default=2.0, precision=2, + description='Width' + ) + y = FloatProperty( + name='depth', + min=0.1, max=10000, + default=0.20, precision=2, + description='Depth' + ) + z = FloatProperty( + name='height', + min=0.1, max=10000, + default=1.2, precision=2, + description='height' + ) + altitude = FloatProperty( + name='altitude', + min=0.0, max=10000, + default=1.0, precision=2, + description='altitude' + ) + mode = EnumProperty( + items=( + ('CREATE', 'Create', '', 0), + ('DELETE', 'Delete', '', 1), + ('REFRESH', 'Refresh', '', 2), + ('UNIQUE', 'Make unique', '', 3), + ), + default='CREATE' + ) + # auto_manipulate = BoolProperty(default=True) + + def draw(self, context): + layout = self.layout + row = layout.row() + row.label("Use Properties panel (N) to define parms", icon='INFO') + + def create(self, context): + m = bpy.data.meshes.new("Window") + o = bpy.data.objects.new("Window", m) + d = m.archipack_window.add() + d.x = self.x + d.y = self.y + d.z = self.z + d.altitude = self.altitude + context.scene.objects.link(o) + o.select = True + context.scene.objects.active = o + self.load_preset(d) + self.add_material(o) + # select frame + o.select = True + context.scene.objects.active = o + return o + + def delete(self, context): + o = context.active_object + if archipack_window.filter(o): + bpy.ops.archipack.disable_manipulate() + for child in o.children: + if 'archipack_hole' in child: + context.scene.objects.unlink(child) + bpy.data.objects.remove(child, do_unlink=True) + elif child.data is not None and 'archipack_window_panel' in child.data: + for handle in child.children: + if 'archipack_handle' in handle: + context.scene.objects.unlink(handle) + bpy.data.objects.remove(handle, do_unlink=True) + context.scene.objects.unlink(child) + bpy.data.objects.remove(child, do_unlink=True) + context.scene.objects.unlink(o) + bpy.data.objects.remove(o, do_unlink=True) + + def update(self, context): + o = context.active_object + d = archipack_window.datablock(o) + if d is not None: + d.update(context) + bpy.ops.object.select_linked(type='OBDATA') + for linked in context.selected_objects: + if linked != o: + archipack_window.datablock(linked).update(context) + bpy.ops.object.select_all(action="DESELECT") + o.select = True + context.scene.objects.active = o + + def unique(self, context): + act = context.active_object + sel = [o for o in context.selected_objects] + bpy.ops.object.select_all(action="DESELECT") + for o in sel: + if archipack_window.filter(o): + o.select = True + for child in o.children: + if 'archipack_hole' in child or ( + child.data is not None and + 'archipack_window_panel' in child.data): + child.hide_select = False + child.select = True + if len(context.selected_objects) > 0: + bpy.ops.object.make_single_user(type='SELECTED_OBJECTS', object=True, + obdata=True, material=False, texture=False, animation=False) + for child in context.selected_objects: + if 'archipack_hole' in child: + child.hide_select = True + bpy.ops.object.select_all(action="DESELECT") + context.scene.objects.active = act + for o in sel: + o.select = True + + # ----------------------------------------------------- + # Execute + # ----------------------------------------------------- + def execute(self, context): + if context.mode == "OBJECT": + if self.mode == 'CREATE': + bpy.ops.object.select_all(action="DESELECT") + o = self.create(context) + o.location = bpy.context.scene.cursor_location + o.select = True + context.scene.objects.active = o + self.manipulate() + elif self.mode == 'DELETE': + self.delete(context) + elif self.mode == 'REFRESH': + self.update(context) + elif self.mode == 'UNIQUE': + self.unique(context) + return {'FINISHED'} + else: + self.report({'WARNING'}, "Archipack: Option only valid in Object mode") + return {'CANCELLED'} + + +class ARCHIPACK_OT_window_draw(ArchpackDrawTool, Operator): + bl_idname = "archipack.window_draw" + bl_label = "Draw Windows" + bl_description = "Draw Windows over walls" + bl_category = 'Archipack' + bl_options = {'REGISTER', 'UNDO'} + + filepath = StringProperty(default="") + feedback = None + stack = [] + + @classmethod + def poll(cls, context): + return True + + def draw(self, context): + layout = self.layout + row = layout.row() + row.label("Use Properties panel (N) to define parms", icon='INFO') + + def draw_callback(self, _self, context): + self.feedback.draw(context) + + def add_object(self, context, event): + o = context.active_object + bpy.ops.object.select_all(action="DESELECT") + + if archipack_window.filter(o): + + o.select = True + context.scene.objects.active = o + + if event.shift: + bpy.ops.archipack.window(mode="UNIQUE") + + new_w = o.copy() + new_w.data = o.data + context.scene.objects.link(new_w) + + o = new_w + o.select = True + context.scene.objects.active = o + + # synch subs from parent instance + bpy.ops.archipack.window(mode="REFRESH") + + else: + bpy.ops.archipack.window(auto_manipulate=False, filepath=self.filepath) + o = context.active_object + + bpy.ops.archipack.generate_hole('INVOKE_DEFAULT') + o.select = True + context.scene.objects.active = o + + def modal(self, context, event): + + context.area.tag_redraw() + o = context.active_object + d = archipack_window.datablock(o) + hole = None + if d is not None: + hole = d.find_hole(o) + + # hide hole from raycast + if hole is not None: + o.hide = True + hole.hide = True + + res, tM, wall, y = self.mouse_hover_wall(context, event) + + if hole is not None: + o.hide = False + hole.hide = False + + if res and d is not None: + o.matrix_world = tM + if d.y != wall.data.archipack_wall2[0].width: + d.y = wall.data.archipack_wall2[0].width + + if event.value == 'PRESS': + if event.type in {'LEFTMOUSE', 'RET', 'NUMPAD_ENTER', 'SPACE'}: + if wall is not None: + context.scene.objects.active = wall + wall.select = True + if bpy.ops.archipack.single_boolean.poll(): + bpy.ops.archipack.single_boolean() + wall.select = False + # o must be a window here + if d is not None: + context.scene.objects.active = o + self.stack.append(o) + self.add_object(context, event) + context.active_object.matrix_world = tM + return {'RUNNING_MODAL'} + # prevent selection of other object + if event.type in {'RIGHTMOUSE'}: + return {'RUNNING_MODAL'} + + if self.keymap.check(event, self.keymap.undo) or ( + event.type in {'BACK_SPACE'} and event.value == 'RELEASE' + ): + if len(self.stack) > 0: + last = self.stack.pop() + context.scene.objects.active = last + bpy.ops.archipack.window(mode="DELETE") + context.scene.objects.active = o + return {'RUNNING_MODAL'} + + if event.value == 'RELEASE': + + if event.type in {'ESC', 'RIGHTMOUSE'}: + bpy.ops.archipack.window(mode='DELETE') + self.feedback.disable() + bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW') + return {'FINISHED'} + + return {'PASS_THROUGH'} + + def invoke(self, context, event): + + if context.mode == "OBJECT": + o = None + self.stack = [] + self.keymap = Keymaps(context) + # exit manipulate_mode if any + bpy.ops.archipack.disable_manipulate() + # invoke with shift pressed will use current object as basis for linked copy + if self.filepath == '' and archipack_window.filter(context.active_object): + o = context.active_object + context.scene.objects.active = None + bpy.ops.object.select_all(action="DESELECT") + if o is not None: + o.select = True + context.scene.objects.active = o + self.add_object(context, event) + self.feedback = FeedbackPanel() + self.feedback.instructions(context, "Draw a window", "Click & Drag over a wall", [ + ('LEFTCLICK, RET, SPACE, ENTER', 'Create a window'), + ('BACKSPACE, CTRL+Z', 'undo last'), + ('SHIFT', 'Make independant copy'), + ('RIGHTCLICK or ESC', 'exit') + ]) + self.feedback.enable() + args = (self, context) + + self._handle = bpy.types.SpaceView3D.draw_handler_add(self.draw_callback, args, 'WINDOW', 'POST_PIXEL') + context.window_manager.modal_handler_add(self) + return {'RUNNING_MODAL'} + else: + self.report({'WARNING'}, "Archipack: Option only valid in Object mode") + return {'CANCELLED'} + + +# ------------------------------------------------------------------ +# Define operator class to create object +# ------------------------------------------------------------------ + + +class ARCHIPACK_OT_window_panel(Operator): + bl_idname = "archipack.window_panel" + bl_label = "Window panel" + bl_description = "Window panel" + bl_category = 'Archipack' + bl_options = {'REGISTER', 'UNDO'} + center = FloatVectorProperty( + subtype='XYZ' + ) + origin = FloatVectorProperty( + subtype='XYZ' + ) + size = FloatVectorProperty( + subtype='XYZ' + ) + radius = FloatVectorProperty( + subtype='XYZ' + ) + angle_y = FloatProperty( + name='angle', + unit='ROTATION', + subtype='ANGLE', + min=-1.5, max=1.5, + default=0, precision=2, + description='angle' + ) + frame_y = FloatProperty( + name='Depth', + min=0, max=100, + default=0.06, precision=2, + description='frame depth' + ) + frame_x = FloatProperty( + name='Width', + min=0, max=100, + default=0.06, precision=2, + description='frame width' + ) + curve_steps = IntProperty( + name="curve steps", + min=1, + max=128, + default=16 + ) + shape = EnumProperty( + name='Shape', + items=( + ('RECTANGLE', 'Rectangle', '', 0), + ('ROUND', 'Top Round', '', 1), + ('ELLIPSIS', 'Top Elliptic', '', 2), + ('QUADRI', 'Top oblique', '', 3), + ('CIRCLE', 'Full circle', '', 4) + ), + default='RECTANGLE' + ) + pivot = FloatProperty( + name='pivot', + min=-1, max=1, + default=-1, precision=2, + description='pivot' + ) + side_material = IntProperty( + name="side material", + min=0, + max=2, + default=0 + ) + handle = EnumProperty( + name='Handle', + items=( + ('NONE', 'No handle', '', 0), + ('INSIDE', 'Inside', '', 1), + ('BOTH', 'Inside and outside', '', 2) + ), + default='NONE' + ) + handle_model = IntProperty( + name="handle model", + default=1, + min=1, + max=2 + ) + handle_altitude = FloatProperty( + name='handle altitude', + min=0, max=1000, + default=0.2, precision=2, + description='handle altitude' + ) + fixed = BoolProperty( + name="Fixed", + default=False + ) + + def draw(self, context): + layout = self.layout + row = layout.row() + row.label("Use Properties panel (N) to define parms", icon='INFO') + + def create(self, context): + m = bpy.data.meshes.new("Window Panel") + o = bpy.data.objects.new("Window Panel", m) + d = m.archipack_window_panel.add() + d.center = self.center + d.origin = self.origin + d.size = self.size + d.radius = self.radius + d.frame_y = self.frame_y + d.frame_x = self.frame_x + d.curve_steps = self.curve_steps + d.shape = self.shape + d.fixed = self.fixed + d.pivot = self.pivot + d.angle_y = self.angle_y + d.side_material = self.side_material + d.handle = self.handle + d.handle_model = self.handle_model + d.handle_altitude = self.handle_altitude + context.scene.objects.link(o) + o.select = True + context.scene.objects.active = o + o.lock_location[0] = True + o.lock_location[1] = True + o.lock_location[2] = True + o.lock_rotation[1] = True + o.lock_scale[0] = True + o.lock_scale[1] = True + o.lock_scale[2] = True + d.update(context) + MaterialUtils.add_window_materials(o) + return o + + def execute(self, context): + if context.mode == "OBJECT": + o = self.create(context) + o.select = True + context.scene.objects.active = o + return {'FINISHED'} + else: + self.report({'WARNING'}, "Archipack: Option only valid in Object mode") + return {'CANCELLED'} + +# ------------------------------------------------------------------ +# Define operator class to manipulate object +# ------------------------------------------------------------------ + + +class ARCHIPACK_OT_window_manipulate(Operator): + bl_idname = "archipack.window_manipulate" + bl_label = "Manipulate" + bl_description = "Manipulate" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(self, context): + return archipack_window.filter(context.active_object) + + def invoke(self, context, event): + d = archipack_window.datablock(context.active_object) + d.manipulable_invoke(context) + return {'FINISHED'} + +# ------------------------------------------------------------------ +# Define operator class to load / save presets +# ------------------------------------------------------------------ + + +class ARCHIPACK_OT_window_preset_menu(PresetMenuOperator, Operator): + bl_idname = "archipack.window_preset_menu" + bl_label = "Window Presets" + preset_subdir = "archipack_window" + + +class ARCHIPACK_OT_window_preset(ArchipackPreset, Operator): + """Add a Window Preset""" + bl_idname = "archipack.window_preset" + bl_label = "Add Window Preset" + preset_menu = "ARCHIPACK_OT_window_preset_menu" + + @property + def blacklist(self): + # 'x', 'y', 'z', 'altitude', 'window_shape' + return ['manipulators'] + + +def register(): + bpy.utils.register_class(archipack_window_panelrow) + bpy.utils.register_class(archipack_window_panel) + Mesh.archipack_window_panel = CollectionProperty(type=archipack_window_panel) + bpy.utils.register_class(ARCHIPACK_PT_window_panel) + bpy.utils.register_class(ARCHIPACK_OT_window_panel) + bpy.utils.register_class(archipack_window) + Mesh.archipack_window = CollectionProperty(type=archipack_window) + bpy.utils.register_class(ARCHIPACK_OT_window_preset_menu) + bpy.utils.register_class(ARCHIPACK_PT_window) + bpy.utils.register_class(ARCHIPACK_OT_window) + bpy.utils.register_class(ARCHIPACK_OT_window_preset) + bpy.utils.register_class(ARCHIPACK_OT_window_draw) + bpy.utils.register_class(ARCHIPACK_OT_window_manipulate) + + +def unregister(): + bpy.utils.unregister_class(archipack_window_panelrow) + bpy.utils.unregister_class(archipack_window_panel) + bpy.utils.unregister_class(ARCHIPACK_PT_window_panel) + del Mesh.archipack_window_panel + bpy.utils.unregister_class(ARCHIPACK_OT_window_panel) + bpy.utils.unregister_class(archipack_window) + del Mesh.archipack_window + bpy.utils.unregister_class(ARCHIPACK_OT_window_preset_menu) + bpy.utils.unregister_class(ARCHIPACK_PT_window) + bpy.utils.unregister_class(ARCHIPACK_OT_window) + bpy.utils.unregister_class(ARCHIPACK_OT_window_preset) + bpy.utils.unregister_class(ARCHIPACK_OT_window_draw) + bpy.utils.unregister_class(ARCHIPACK_OT_window_manipulate) diff --git a/archipack/bitarray.py b/archipack/bitarray.py new file mode 100644 index 00000000..cf712610 --- /dev/null +++ b/archipack/bitarray.py @@ -0,0 +1,97 @@ + +import array + +# -*- coding:utf-8 -*- + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110- 1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +# + +# ---------------------------------------------------------- +# Author: Stephen Leger (s-leger) +# +# ---------------------------------------------------------- + + +class BitArray(): + + def __init__(self, bitSize, fill=0): + self.size = bitSize + intSize = bitSize >> 5 + if (bitSize & 31): + intSize += 1 + if fill == 1: + fill = 4294967295 + else: + fill = 0 + self.bitArray = array.array('I') + self.bitArray.extend((fill,) * intSize) + + def __str__(self): + return str(self.list) + + def bit_location(self, bit_num): + return bit_num >> 5, bit_num & 31 + + def test(self, bit_num): + record, offset = self.bit_location(bit_num) + mask = 1 << offset + return(self.bitArray[record] & mask) + + def set(self, bit_num): + record, offset = self.bit_location(bit_num) + mask = 1 << offset + self.bitArray[record] |= mask + + def clear(self, bit_num): + record, offset = self.bit_location(bit_num) + mask = ~(1 << offset) + self.bitArray[record] &= mask + + def toggle(self, bit_num): + record, offset = self.bit_location(bit_num) + mask = 1 << offset + self.bitArray[record] ^= mask + + @property + def len(self): + return len(self.bitArray) + + @property + def copy(self): + copy = BitArray(self.size) + for i in range(self.len): + copy.bitArray[i] = self.bitArray[i] + return copy + + @property + def list(self): + return [x for x in range(self.size) if self.test(x) > 0] + + def none(self): + for i in range(self.len): + self.bitArray[i] = 0 + + def reverse(self): + for i in range(self.len): + self.bitArray[i] = 4294967295 ^ self.bitArray[i] + + def all(self): + for i in range(self.len): + self.bitArray[i] = 4294967295 diff --git a/archipack/bmesh_utils.py b/archipack/bmesh_utils.py new file mode 100644 index 00000000..b49f4683 --- /dev/null +++ b/archipack/bmesh_utils.py @@ -0,0 +1,249 @@ +# -*- coding:utf-8 -*- + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110- 1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +# + +# ---------------------------------------------------------- +# Author: Stephen Leger (s-leger) +# +# ---------------------------------------------------------- +import bpy +import bmesh + + +class BmeshEdit(): + @staticmethod + def _start(context, o): + """ + private, start bmesh editing of active object + """ + o.select = True + context.scene.objects.active = o + bpy.ops.object.mode_set(mode='EDIT') + bm = bmesh.from_edit_mesh(o.data) + bm.verts.ensure_lookup_table() + bm.faces.ensure_lookup_table() + return bm + + @staticmethod + def _end(bm, o): + """ + private, end bmesh editing of active object + """ + bm.normal_update() + bmesh.update_edit_mesh(o.data, True) + bpy.ops.object.mode_set(mode='OBJECT') + bm.free() + + @staticmethod + def _matids(bm, matids): + for i, matid in enumerate(matids): + bm.faces[i].material_index = matid + + @staticmethod + def _uvs(bm, uvs): + layer = bm.loops.layers.uv.verify() + l_i = len(uvs) + for i, face in enumerate(bm.faces): + if i > l_i: + raise RuntimeError("Missing uvs for face {}".format(i)) + l_j = len(uvs[i]) + for j, loop in enumerate(face.loops): + if j > l_j: + raise RuntimeError("Missing uv {} for face {}".format(j, i)) + loop[layer].uv = uvs[i][j] + + @staticmethod + def _verts(bm, verts): + for i, v in enumerate(verts): + bm.verts[i].co = v + + @staticmethod + def buildmesh(context, o, verts, faces, matids=None, uvs=None, weld=False, clean=False, auto_smooth=True): + bm = BmeshEdit._start(context, o) + bm.clear() + for v in verts: + bm.verts.new(v) + bm.verts.ensure_lookup_table() + for f in faces: + bm.faces.new([bm.verts[i] for i in f]) + bm.faces.ensure_lookup_table() + if matids is not None: + BmeshEdit._matids(bm, matids) + if uvs is not None: + BmeshEdit._uvs(bm, uvs) + if weld: + bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.001) + BmeshEdit._end(bm, o) + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.mesh.select_all(action='SELECT') + if auto_smooth: + bpy.ops.mesh.faces_shade_smooth() + o.data.use_auto_smooth = True + else: + bpy.ops.mesh.faces_shade_flat() + if clean: + bpy.ops.mesh.delete_loose() + bpy.ops.object.mode_set(mode='OBJECT') + + @staticmethod + def addmesh(context, o, verts, faces, matids=None, uvs=None, weld=False, clean=False, auto_smooth=True): + bm = BmeshEdit._start(context, o) + nv = len(bm.verts) + nf = len(bm.faces) + + for v in verts: + bm.verts.new(v) + + bm.verts.ensure_lookup_table() + + for f in faces: + bm.faces.new([bm.verts[nv + i] for i in f]) + + bm.faces.ensure_lookup_table() + + if matids is not None: + for i, matid in enumerate(matids): + bm.faces[nf + i].material_index = matid + + if uvs is not None: + layer = bm.loops.layers.uv.verify() + for i, face in enumerate(bm.faces[nf:]): + for j, loop in enumerate(face.loops): + loop[layer].uv = uvs[i][j] + + if weld: + bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.001) + BmeshEdit._end(bm, o) + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.mesh.select_all(action='SELECT') + if auto_smooth: + bpy.ops.mesh.faces_shade_smooth() + o.data.use_auto_smooth = True + else: + bpy.ops.mesh.faces_shade_flat() + if clean: + bpy.ops.mesh.delete_loose() + bpy.ops.object.mode_set(mode='OBJECT') + + @staticmethod + def bevel(context, o, + offset, + offset_type=0, + segments=1, + profile=0.5, + vertex_only=False, + clamp_overlap=True, + material=-1, + use_selection=True): + """ + /* Bevel offset_type slot values */ + enum { + BEVEL_AMT_OFFSET, + BEVEL_AMT_WIDTH, + BEVEL_AMT_DEPTH, + BEVEL_AMT_PERCENT + }; + """ + bm = bmesh.new() + bm.from_mesh(o.data) + bm.verts.ensure_lookup_table() + if use_selection: + geom = [v for v in bm.verts if v.select] + geom.extend([ed for ed in bm.edges if ed.select]) + else: + geom = bm.verts[:] + geom.extend(bm.edges[:]) + + bmesh.ops.bevel(bm, + geom=geom, + offset=offset, + offset_type=offset_type, + segments=segments, + profile=profile, + vertex_only=vertex_only, + clamp_overlap=clamp_overlap, + material=material) + + bm.to_mesh(o.data) + bm.free() + + @staticmethod + def bissect(context, o, + plane_co, + plane_no, + dist=0.001, + use_snap_center=False, + clear_outer=True, + clear_inner=False + ): + + bm = bmesh.new() + bm.from_mesh(o.data) + bm.verts.ensure_lookup_table() + geom = bm.verts[:] + geom.extend(bm.edges[:]) + geom.extend(bm.faces[:]) + + bmesh.ops.bisect_plane(bm, + geom=geom, + dist=dist, + plane_co=plane_co, + plane_no=plane_no, + use_snap_center=False, + clear_outer=clear_outer, + clear_inner=clear_inner + ) + + bm.to_mesh(o.data) + bm.free() + + @staticmethod + def solidify(context, o, amt, floor_bottom=False, altitude=0): + bm = bmesh.new() + bm.from_mesh(o.data) + bm.verts.ensure_lookup_table() + geom = bm.faces[:] + bmesh.ops.solidify(bm, geom=geom, thickness=amt) + if floor_bottom: + for v in bm.verts: + if not v.select: + v.co.z = altitude + bm.to_mesh(o.data) + bm.free() + + @staticmethod + def verts(context, o, verts): + """ + update vertex position of active object + """ + bm = BmeshEdit._start(context, o) + BmeshEdit._verts(bm, verts) + BmeshEdit._end(bm, o) + + @staticmethod + def aspect(context, o, matids, uvs): + """ + update material id and uvmap of active object + """ + bm = BmeshEdit._start(context, o) + BmeshEdit._matids(bm, matids) + BmeshEdit._uvs(bm, uvs) + BmeshEdit._end(bm, o) diff --git a/archipack/icons/archipack.png b/archipack/icons/archipack.png new file mode 100644 index 00000000..92503c82 Binary files /dev/null and b/archipack/icons/archipack.png differ diff --git a/archipack/icons/detect.png b/archipack/icons/detect.png new file mode 100644 index 00000000..9c10f604 Binary files /dev/null and b/archipack/icons/detect.png differ diff --git a/archipack/icons/door.png b/archipack/icons/door.png new file mode 100644 index 00000000..dc975d4d Binary files /dev/null and b/archipack/icons/door.png differ diff --git a/archipack/icons/fence.png b/archipack/icons/fence.png new file mode 100644 index 00000000..f32dcc7e Binary files /dev/null and b/archipack/icons/fence.png differ diff --git a/archipack/icons/floor.png b/archipack/icons/floor.png new file mode 100644 index 00000000..1590c335 Binary files /dev/null and b/archipack/icons/floor.png differ diff --git a/archipack/icons/polygons.png b/archipack/icons/polygons.png new file mode 100644 index 00000000..b434068c Binary files /dev/null and b/archipack/icons/polygons.png differ diff --git a/archipack/icons/selection.png b/archipack/icons/selection.png new file mode 100644 index 00000000..e4a7e82b Binary files /dev/null and b/archipack/icons/selection.png differ diff --git a/archipack/icons/slab.png b/archipack/icons/slab.png new file mode 100644 index 00000000..292ea52e Binary files /dev/null and b/archipack/icons/slab.png differ diff --git a/archipack/icons/stair.png b/archipack/icons/stair.png new file mode 100644 index 00000000..5ce4d705 Binary files /dev/null and b/archipack/icons/stair.png differ diff --git a/archipack/icons/truss.png b/archipack/icons/truss.png new file mode 100644 index 00000000..72ca9157 Binary files /dev/null and b/archipack/icons/truss.png differ diff --git a/archipack/icons/union.png b/archipack/icons/union.png new file mode 100644 index 00000000..11b11472 Binary files /dev/null and b/archipack/icons/union.png differ diff --git a/archipack/icons/wall.png b/archipack/icons/wall.png new file mode 100644 index 00000000..1335a590 Binary files /dev/null and b/archipack/icons/wall.png differ diff --git a/archipack/icons/window.png b/archipack/icons/window.png new file mode 100644 index 00000000..74be2e0e Binary files /dev/null and b/archipack/icons/window.png differ diff --git a/archipack/materialutils.py b/archipack/materialutils.py new file mode 100644 index 00000000..92497924 --- /dev/null +++ b/archipack/materialutils.py @@ -0,0 +1,169 @@ +# -*- coding:utf-8 -*- + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110- 1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +# + +# ---------------------------------------------------------- +# Author: Stephen Leger (s-leger) +# +# ---------------------------------------------------------- +import bpy + + +class MaterialUtils(): + + @staticmethod + def build_default_mat(name, color=(1.0, 1.0, 1.0)): + midx = bpy.data.materials.find(name) + if midx < 0: + mat = bpy.data.materials.new(name) + mat.diffuse_color = color + else: + mat = bpy.data.materials[midx] + return mat + + @staticmethod + def add_wall2_materials(obj): + int_mat = MaterialUtils.build_default_mat('inside', (0.5, 1.0, 1.0)) + out_mat = MaterialUtils.build_default_mat('outside', (0.5, 1.0, 0.5)) + oth_mat = MaterialUtils.build_default_mat('cuts', (1.0, 0.2, 0.2)) + alt1_mat = MaterialUtils.build_default_mat('wall_alternative1', (1.0, 0.2, 0.2)) + alt2_mat = MaterialUtils.build_default_mat('wall_alternative2', (1.0, 0.2, 0.2)) + alt3_mat = MaterialUtils.build_default_mat('wall_alternative3', (1.0, 0.2, 0.2)) + alt4_mat = MaterialUtils.build_default_mat('wall_alternative4', (1.0, 0.2, 0.2)) + alt5_mat = MaterialUtils.build_default_mat('wall_alternative5', (1.0, 0.2, 0.2)) + obj.data.materials.append(out_mat) + obj.data.materials.append(int_mat) + obj.data.materials.append(oth_mat) + obj.data.materials.append(alt1_mat) + obj.data.materials.append(alt2_mat) + obj.data.materials.append(alt3_mat) + obj.data.materials.append(alt4_mat) + obj.data.materials.append(alt5_mat) + + @staticmethod + def add_wall_materials(obj): + int_mat = MaterialUtils.build_default_mat('inside', (0.5, 1.0, 1.0)) + out_mat = MaterialUtils.build_default_mat('outside', (0.5, 1.0, 0.5)) + oth_mat = MaterialUtils.build_default_mat('cuts', (1.0, 0.2, 0.2)) + obj.data.materials.append(out_mat) + obj.data.materials.append(int_mat) + obj.data.materials.append(oth_mat) + + @staticmethod + def add_slab_materials(obj): + out_mat = MaterialUtils.build_default_mat('Slab_bottom', (0.5, 1.0, 1.0)) + int_mat = MaterialUtils.build_default_mat('Slab_top', (1.0, 0.2, 0.2)) + oth_mat = MaterialUtils.build_default_mat('Slab_side', (0.5, 1.0, 0.5)) + obj.data.materials.append(out_mat) + obj.data.materials.append(int_mat) + obj.data.materials.append(oth_mat) + + @staticmethod + def add_stair_materials(obj): + cei_mat = MaterialUtils.build_default_mat('Stair_ceiling', (0.5, 1.0, 1.0)) + whi_mat = MaterialUtils.build_default_mat('Stair_white', (1.0, 1.0, 1.0)) + con_mat = MaterialUtils.build_default_mat('Stair_concrete', (0.5, 0.5, 0.5)) + wood_mat = MaterialUtils.build_default_mat('Stair_wood', (0.28, 0.2, 0.1)) + metal_mat = MaterialUtils.build_default_mat('Stair_metal', (0.4, 0.4, 0.4)) + glass_mat = MaterialUtils.build_default_mat('Stair_glass', (0.2, 0.2, 0.2)) + glass_mat.use_transparency = True + glass_mat.alpha = 0.5 + glass_mat.game_settings.alpha_blend = 'ADD' + obj.data.materials.append(cei_mat) + obj.data.materials.append(whi_mat) + obj.data.materials.append(con_mat) + obj.data.materials.append(wood_mat) + obj.data.materials.append(metal_mat) + obj.data.materials.append(glass_mat) + + @staticmethod + def add_fence_materials(obj): + wood_mat = MaterialUtils.build_default_mat('Fence_wood', (0.28, 0.2, 0.1)) + metal_mat = MaterialUtils.build_default_mat('Fence_metal', (0.4, 0.4, 0.4)) + glass_mat = MaterialUtils.build_default_mat('Fence_glass', (0.2, 0.2, 0.2)) + glass_mat.use_transparency = True + glass_mat.alpha = 0.5 + glass_mat.game_settings.alpha_blend = 'ADD' + obj.data.materials.append(wood_mat) + obj.data.materials.append(metal_mat) + obj.data.materials.append(glass_mat) + + @staticmethod + def add_floor_materials(obj): + con_mat = MaterialUtils.build_default_mat('Floor_grout', (0.5, 0.5, 0.5)) + alt1_mat = MaterialUtils.build_default_mat('Floor_alt1', (0.5, 1.0, 1.0)) + alt2_mat = MaterialUtils.build_default_mat('Floor_alt2', (1.0, 1.0, 1.0)) + alt3_mat = MaterialUtils.build_default_mat('Floor_alt3', (0.28, 0.2, 0.1)) + alt4_mat = MaterialUtils.build_default_mat('Floor_alt4', (0.5, 1.0, 1.0)) + alt5_mat = MaterialUtils.build_default_mat('Floor_alt5', (1.0, 1.0, 0.5)) + alt6_mat = MaterialUtils.build_default_mat('Floor_alt6', (0.28, 0.5, 0.1)) + alt7_mat = MaterialUtils.build_default_mat('Floor_alt7', (0.5, 1.0, 0.5)) + alt8_mat = MaterialUtils.build_default_mat('Floor_alt8', (1.0, 0.2, 1.0)) + alt9_mat = MaterialUtils.build_default_mat('Floor_alt9', (0.28, 0.2, 0.5)) + alt10_mat = MaterialUtils.build_default_mat('Floor_alt10', (0.5, 0.2, 0.1)) + obj.data.materials.append(con_mat) + obj.data.materials.append(alt1_mat) + obj.data.materials.append(alt2_mat) + obj.data.materials.append(alt3_mat) + obj.data.materials.append(alt4_mat) + obj.data.materials.append(alt5_mat) + obj.data.materials.append(alt6_mat) + obj.data.materials.append(alt7_mat) + obj.data.materials.append(alt8_mat) + obj.data.materials.append(alt9_mat) + obj.data.materials.append(alt10_mat) + + @staticmethod + def add_handle_materials(obj): + metal_mat = MaterialUtils.build_default_mat('metal', (0.4, 0.4, 0.4)) + obj.data.materials.append(metal_mat) + + @staticmethod + def add_door_materials(obj): + int_mat = MaterialUtils.build_default_mat('door_inside', (0.7, 0.2, 0.2)) + out_mat = MaterialUtils.build_default_mat('door_outside', (0.7, 0.2, 0.7)) + glass_mat = MaterialUtils.build_default_mat('glass', (0.2, 0.2, 0.2)) + metal_mat = MaterialUtils.build_default_mat('metal', (0.4, 0.4, 0.4)) + glass_mat.use_transparency = True + glass_mat.alpha = 0.5 + glass_mat.game_settings.alpha_blend = 'ADD' + obj.data.materials.append(out_mat) + obj.data.materials.append(int_mat) + obj.data.materials.append(glass_mat) + obj.data.materials.append(metal_mat) + + @staticmethod + def add_window_materials(obj): + int_mat = MaterialUtils.build_default_mat('window_inside', (0.7, 0.2, 0.2)) + out_mat = MaterialUtils.build_default_mat('window_outside', (0.7, 0.2, 0.7)) + glass_mat = MaterialUtils.build_default_mat('glass', (0.2, 0.2, 0.2)) + metal_mat = MaterialUtils.build_default_mat('metal', (0.4, 0.4, 0.4)) + tablet_mat = MaterialUtils.build_default_mat('tablet', (0.2, 0.2, 0.2)) + blind_mat = MaterialUtils.build_default_mat('blind', (0.2, 0.0, 0.0)) + glass_mat.use_transparency = True + glass_mat.alpha = 0.5 + glass_mat.game_settings.alpha_blend = 'ADD' + obj.data.materials.append(out_mat) + obj.data.materials.append(int_mat) + obj.data.materials.append(glass_mat) + obj.data.materials.append(metal_mat) + obj.data.materials.append(tablet_mat) + obj.data.materials.append(blind_mat) diff --git a/archipack/panel.py b/archipack/panel.py new file mode 100644 index 00000000..c8898fe5 --- /dev/null +++ b/archipack/panel.py @@ -0,0 +1,715 @@ +# -*- coding:utf-8 -*- + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110- 1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +# + +# ---------------------------------------------------------- +# Author: Stephen Leger (s-leger) +# +# ---------------------------------------------------------- + +from math import cos, sin, tan, sqrt, atan2, pi +from mathutils import Vector + + +class Panel(): + """ + Define a bevel profil + index: array associate each y with a coord circle and a x + x = array of x of unique points in the profil relative to origin (0, 0) is bottom left + y = array of y of all points in the profil relative to origin (0, 0) is bottom left + idmat = array of material index for each segment + when path is not closed, start and end caps are generated + + shape is the loft profile + path is the loft path + + Open shape: + + x = [0,1] + y = [0,1,1, 0] + index = [0, 0,1,1] + closed_shape = False + + 1 ____2 + | | + | | + | | + 0 3 + + Closed shape: + + x = [0,1] + y = [0,1,1, 0] + index = [0, 0,1,1] + closed_shape = True + + 1 ____2 + | | + | | + |____| + 0 3 + + Side Caps (like glass for window): + + x = [0,1] + y = [0,1,1, 0.75, 0.25, 0] + index = [0, 0,1,1,1,1] + closed_shape = True + side_caps = [3,4] + + 1 ____2 ____ + | 3|__cap__| | + | 4|_______| | + |____| |____| + 0 5 + + """ + def __init__(self, closed_shape, index, x, y, idmat, side_cap_front=-1, side_cap_back=-1, closed_path=True, + subdiv_x=0, subdiv_y=0, user_path_verts=0, user_path_uv_v=None): + + self.closed_shape = closed_shape + self.closed_path = closed_path + self.index = index + self.x = x + self.y = y + self.idmat = idmat + self.side_cap_front = side_cap_front + self.side_cap_back = side_cap_back + self.subdiv_x = subdiv_x + self.subdiv_y = subdiv_y + self.user_path_verts = user_path_verts + self.user_path_uv_v = user_path_uv_v + + @property + def n_pts(self): + return len(self.y) + + @property + def profil_faces(self): + """ + number of faces for each section + """ + if self.closed_shape: + return len(self.y) + else: + return len(self.y) - 1 + + @property + def uv_u(self): + """ + uvs of profil (absolute value) + """ + x = [self.x[i] for i in self.index] + x.append(x[0]) + y = [y for y in self.y] + y.append(y[0]) + uv_u = [] + uv = 0 + uv_u.append(uv) + for i in range(len(self.index)): + dx = x[i + 1] - x[i] + dy = y[i + 1] - y[i] + uv += sqrt(dx * dx + dy * dy) + uv_u.append(uv) + return uv_u + + def path_sections(self, steps, path_type): + """ + number of verts and faces sections along path + """ + n_path_verts = 2 + if path_type in ['QUADRI', 'RECTANGLE']: + n_path_verts = 4 + self.subdiv_x + 2 * self.subdiv_y + if self.closed_path: + n_path_verts += self.subdiv_x + elif path_type in ['ROUND', 'ELLIPSIS']: + n_path_verts = steps + 3 + elif path_type == 'CIRCLE': + n_path_verts = steps + elif path_type == 'TRIANGLE': + n_path_verts = 3 + elif path_type == 'PENTAGON': + n_path_verts = 5 + elif path_type == 'USER_DEFINED': + n_path_verts = self.user_path_verts + if self.closed_path: + n_path_faces = n_path_verts + else: + n_path_faces = n_path_verts - 1 + return n_path_verts, n_path_faces + + def n_verts(self, steps, path_type): + n_path_verts, n_path_faces = self.path_sections(steps, path_type) + return self.n_pts * n_path_verts + + ############################ + # Geomerty + ############################ + + def _intersect_line(self, center, basis, x): + """ upper intersection of line parallel to y axis and a triangle + where line is given by x origin + top by center, basis size as float + return float y of upper intersection point + + center.x and center.y are absolute + a 0 center.x lie on half size + a 0 center.y lie on basis + """ + if center.x > 0: + dx = x - center.x + else: + dx = center.x - x + p = center.y / basis + return center.y + dx * p + + def _intersect_triangle(self, center, basis, x): + """ upper intersection of line parallel to y axis and a triangle + where line is given by x origin + top by center, basis size as float + return float y of upper intersection point + + center.x and center.y are absolute + a 0 center.x lie on half size + a 0 center.y lie on basis + """ + if x > center.x: + dx = center.x - x + sx = 0.5 * basis - center.x + else: + dx = x - center.x + sx = 0.5 * basis + center.x + if sx == 0: + sx = basis + p = center.y / sx + return center.y + dx * p + + def _intersect_circle(self, center, radius, x): + """ upper intersection of line parallel to y axis and a circle + where line is given by x origin + circle by center, radius as float + return float y of upper intersection point, float angle + """ + dx = x - center.x + d = (radius * radius) - (dx * dx) + if d <= 0: + if x > center.x: + return center.y, 0 + else: + return center.y, pi + else: + y = sqrt(d) + return center.y + y, atan2(y, dx) + + def _intersect_elipsis(self, center, radius, x): + """ upper intersection of line parallel to y axis and an ellipsis + where line is given by x origin + circle by center, radius.x and radius.y semimajor and seminimor axis (half width and height) as float + return float y of upper intersection point, float angle + """ + dx = x - center.x + d2 = dx * dx + A = 1 / radius.y / radius.y + C = d2 / radius.x / radius.x - 1 + d = - 4 * A * C + if d <= 0: + if x > center.x: + return center.y, 0 + else: + return center.y, pi + else: + y0 = sqrt(d) / 2 / A + d = (radius.x * radius.x) - d2 + y = sqrt(d) + return center.y + y0, atan2(y, dx) + + def _intersect_arc(self, center, radius, x_left, x_right): + y0, a0 = self._intersect_circle(center, radius.x, x_left) + y1, a1 = self._intersect_circle(center, radius.x, x_right) + da = (a1 - a0) + if da < -pi: + da += 2 * pi + if da > pi: + da -= 2 * pi + return y0, y1, a0, da + + def _intersect_arc_elliptic(self, center, radius, x_left, x_right): + y0, a0 = self._intersect_elipsis(center, radius, x_left) + y1, a1 = self._intersect_elipsis(center, radius, x_right) + da = (a1 - a0) + if da < -pi: + da += 2 * pi + if da > pi: + da -= 2 * pi + return y0, y1, a0, da + + def _get_ellispe_coords(self, steps, offset, center, origin, size, radius, x, pivot, bottom_y=0): + """ + Rectangle with single arc on top + """ + x_left = size.x / 2 * (pivot - 1) + x + x_right = size.x / 2 * (pivot + 1) - x + cx = center.x - origin.x + cy = offset.y + center.y - origin.y + y0, y1, a0, da = self._intersect_arc_elliptic(center, radius, origin.x + x_left, origin.x + x_right) + da /= steps + coords = [] + # bottom left + if self.closed_path: + coords.append((offset.x + x_left, offset.y + x + bottom_y)) + else: + coords.append((offset.x + x_left, offset.y + bottom_y)) + # top left + coords.append((offset.x + x_left, offset.y + y0 - origin.y)) + for i in range(1, steps): + a = a0 + i * da + coords.append((offset.x + cx + cos(a) * radius.x, cy + sin(a) * radius.y)) + # top right + coords.append((offset.x + x_right, offset.y + y1 - origin.y)) + # bottom right + if self.closed_path: + coords.append((offset.x + x_right, offset.y + x + bottom_y)) + else: + coords.append((offset.x + x_right, offset.y + bottom_y)) + return coords + + def _get_arc_coords(self, steps, offset, center, origin, size, radius, x, pivot, bottom_y=0): + """ + Rectangle with single arc on top + """ + x_left = size.x / 2 * (pivot - 1) + x + x_right = size.x / 2 * (pivot + 1) - x + cx = offset.x + center.x - origin.x + cy = offset.y + center.y - origin.y + y0, y1, a0, da = self._intersect_arc(center, radius, origin.x + x_left, origin.x + x_right) + da /= steps + coords = [] + + # bottom left + if self.closed_path: + coords.append((offset.x + x_left, offset.y + x + bottom_y)) + else: + coords.append((offset.x + x_left, offset.y + bottom_y)) + + # top left + coords.append((offset.x + x_left, offset.y + y0 - origin.y)) + + for i in range(1, steps): + a = a0 + i * da + coords.append((cx + cos(a) * radius.x, cy + sin(a) * radius.x)) + + # top right + coords.append((offset.x + x_right, offset.y + y1 - origin.y)) + + # bottom right + if self.closed_path: + coords.append((offset.x + x_right, offset.y + x + bottom_y)) + else: + coords.append((offset.x + x_right, offset.y + bottom_y)) + + return coords + + def _get_circle_coords(self, steps, offset, center, origin, radius): + """ + Full circle + """ + cx = offset.x + center.x - origin.x + cy = offset.y + center.y - origin.y + a = -2 * pi / steps + return [(cx + cos(i * a) * radius.x, cy + sin(i * a) * radius.x) for i in range(steps)] + + def _get_rectangular_coords(self, offset, size, x, pivot, bottom_y=0): + coords = [] + + x_left = offset.x + size.x / 2 * (pivot - 1) + x + x_right = offset.x + size.x / 2 * (pivot + 1) - x + + if self.closed_path: + y0 = offset.y + x + bottom_y + else: + y0 = offset.y + bottom_y + y1 = offset.y + size.y - x + + dy = (y1 - y0) / (1 + self.subdiv_y) + dx = (x_right - x_left) / (1 + self.subdiv_x) + + # bottom left + # coords.append((x_left, y0)) + + # subdiv left + for i in range(self.subdiv_y + 1): + coords.append((x_left, y0 + i * dy)) + + # top left + # coords.append((x_left, y1)) + + # subdiv top + for i in range(self.subdiv_x + 1): + coords.append((x_left + dx * i, y1)) + + # top right + # coords.append((x_right, y1)) + # subdiv right + for i in range(self.subdiv_y + 1): + coords.append((x_right, y1 - i * dy)) + + # subdiv bottom + if self.closed_path: + for i in range(self.subdiv_x + 1): + coords.append((x_right - dx * i, y0)) + else: + # bottom right + coords.append((x_right, y0)) + + return coords + + def _get_vertical_rectangular_trapezoid_coords(self, offset, center, origin, size, basis, x, pivot, bottom_y=0): + """ + Rectangular trapezoid vertical + basis is the full width of a triangular area the trapezoid lie into + center.y is the height of triagular area from top + center.x is the offset from basis center + + |\ + | \ + |__| + """ + coords = [] + x_left = size.x / 2 * (pivot - 1) + x + x_right = size.x / 2 * (pivot + 1) - x + sx = x * sqrt(basis * basis + center.y * center.y) / basis + dy = size.y + offset.y - sx + y0 = self._intersect_line(center, basis, origin.x + x_left) + y1 = self._intersect_line(center, basis, origin.x + x_right) + # bottom left + if self.closed_path: + coords.append((offset.x + x_left, offset.y + x + bottom_y)) + else: + coords.append((offset.x + x_left, offset.y + bottom_y)) + # top left + coords.append((offset.x + x_left, dy - y0)) + # top right + coords.append((offset.x + x_right, dy - y1)) + # bottom right + if self.closed_path: + coords.append((offset.x + x_right, offset.y + x + bottom_y)) + else: + coords.append((offset.x + x_right, offset.y + bottom_y)) + return coords + + def _get_horizontal_rectangular_trapezoid_coords(self, offset, center, origin, size, basis, x, pivot, bottom_y=0): + """ + Rectangular trapeze horizontal + basis is the full width of a triangular area the trapezoid lie into + center.y is the height of triagular area from top to basis + center.x is the offset from basis center + ___ + | \ + |____\ + + TODO: correct implementation + """ + raise NotImplementedError + + def _get_pentagon_coords(self, offset, center, origin, size, basis, x, pivot, bottom_y=0): + """ + TODO: correct implementation + /\ + / \ + | | + |____| + """ + raise NotImplementedError + + def _get_triangle_coords(self, offset, center, origin, size, basis, x, pivot, bottom_y=0): + coords = [] + x_left = offset.x + size.x / 2 * (pivot - 1) + x + x_right = offset.x + size.x / 2 * (pivot + 1) - x + + # bottom left + if self.closed_path: + coords.append((x_left, offset.y + x + bottom_y)) + else: + coords.append((x_left, offset.y + bottom_y)) + # top center + coords.append((center.x, offset.y + center.y)) + # bottom right + if self.closed_path: + coords.append((x_right, offset.y + x + bottom_y)) + else: + coords.append((x_right, offset.y + bottom_y)) + return coords + + def _get_horizontal_coords(self, offset, size, x, pivot): + coords = [] + x_left = offset.x + size.x / 2 * (pivot - 1) + x_right = offset.x + size.x / 2 * (pivot + 1) + # left + coords.append((x_left, offset.y + x)) + # right + coords.append((x_right, offset.y + x)) + return coords + + def _get_vertical_coords(self, offset, size, x, pivot): + coords = [] + x_left = offset.x + size.x / 2 * (pivot - 1) + x + # top + coords.append((x_left, offset.y + size.y)) + # bottom + coords.append((x_left, offset.y)) + return coords + + def choose_a_shape_in_tri(self, center, origin, size, basis, pivot): + """ + Choose wich shape inside either a tri or a pentagon + """ + cx = (0.5 * basis + center.x) - origin.x + cy = center.y - origin.y + x_left = size.x / 2 * (pivot - 1) + x_right = size.x / 2 * (pivot + 1) + y0 = self.intersect_triangle(cx, cy, basis, x_left) + y1 = self.intersect_triangle(cx, cy, basis, x_right) + if (y0 == 0 and y1 == 0) or ((y0 == 0 or y1 == 0) and (y0 == cy or y1 == cy)): + return 'TRIANGLE' + elif x_right <= cx or x_left >= cx: + # single side of triangle + # may be horizontal or vertical rectangular trapezoid + # horizontal if size.y < center.y + return 'QUADRI' + else: + # both sides of triangle + # may be horizontal trapezoid or pentagon + # horizontal trapezoid if size.y < center.y + return 'PENTAGON' + + ############################ + # Vertices + ############################ + + def vertices(self, steps, offset, center, origin, size, radius, + angle_y, pivot, shape_z=None, path_type='ROUND', axis='XZ'): + + verts = [] + if shape_z is None: + shape_z = [0 for x in self.x] + if path_type == 'ROUND': + coords = [self._get_arc_coords(steps, offset, center, origin, + size, Vector((radius.x - x, 0)), x, pivot, shape_z[i]) for i, x in enumerate(self.x)] + elif path_type == 'ELLIPSIS': + coords = [self._get_ellispe_coords(steps, offset, center, origin, + size, Vector((radius.x - x, radius.y - x)), x, pivot, shape_z[i]) for i, x in enumerate(self.x)] + elif path_type == 'QUADRI': + coords = [self._get_vertical_rectangular_trapezoid_coords(offset, center, origin, + size, radius.x, x, pivot) for i, x in enumerate(self.x)] + elif path_type == 'HORIZONTAL': + coords = [self._get_horizontal_coords(offset, size, x, pivot) + for i, x in enumerate(self.x)] + elif path_type == 'VERTICAL': + coords = [self._get_vertical_coords(offset, size, x, pivot) + for i, x in enumerate(self.x)] + elif path_type == 'CIRCLE': + coords = [self._get_circle_coords(steps, offset, center, origin, Vector((radius.x - x, 0))) + for i, x in enumerate(self.x)] + else: + coords = [self._get_rectangular_coords(offset, size, x, pivot, shape_z[i]) + for i, x in enumerate(self.x)] + # vertical panel (as for windows) + if axis == 'XZ': + for i in range(len(coords[0])): + for j, p in enumerate(self.index): + x, z = coords[p][i] + y = self.y[j] + verts.append((x, y, z)) + # horizontal panel (table and so on) + elif axis == 'XY': + for i in range(len(coords[0])): + for j, p in enumerate(self.index): + x, y = coords[p][i] + z = self.y[j] + verts.append((x, y, z)) + return verts + + ############################ + # Faces + ############################ + + def _faces_cap(self, faces, n_path_verts, offset): + if self.closed_shape and not self.closed_path: + last_point = offset + self.n_pts * n_path_verts - 1 + faces.append(tuple([offset + i for i in range(self.n_pts)])) + faces.append(tuple([last_point - i for i in range(self.n_pts)])) + + def _faces_closed(self, n_path_faces, offset): + faces = [] + n_pts = self.n_pts + for i in range(n_path_faces): + k0 = offset + i * n_pts + if self.closed_path and i == n_path_faces - 1: + k1 = offset + else: + k1 = k0 + n_pts + for j in range(n_pts - 1): + faces.append((k1 + j, k1 + j + 1, k0 + j + 1, k0 + j)) + # close profile + faces.append((k1 + n_pts - 1, k1, k0, k0 + n_pts - 1)) + return faces + + def _faces_open(self, n_path_faces, offset): + faces = [] + n_pts = self.n_pts + for i in range(n_path_faces): + k0 = offset + i * n_pts + if self.closed_path and i == n_path_faces - 1: + k1 = offset + else: + k1 = k0 + n_pts + for j in range(n_pts - 1): + faces.append((k1 + j, k1 + j + 1, k0 + j + 1, k0 + j)) + return faces + + def _faces_side(self, faces, n_path_verts, start, reverse, offset): + n_pts = self.n_pts + vf = [offset + start + n_pts * f for f in range(n_path_verts)] + if reverse: + faces.append(tuple(reversed(vf))) + else: + faces.append(tuple(vf)) + + def faces(self, steps, offset=0, path_type='ROUND'): + n_path_verts, n_path_faces = self.path_sections(steps, path_type) + if self.closed_shape: + faces = self._faces_closed(n_path_faces, offset) + else: + faces = self._faces_open(n_path_faces, offset) + if self.side_cap_front > -1: + self._faces_side(faces, n_path_verts, self.side_cap_front, False, offset) + if self.side_cap_back > -1: + self._faces_side(faces, n_path_verts, self.side_cap_back, True, offset) + self._faces_cap(faces, n_path_verts, offset) + return faces + + ############################ + # Uvmaps + ############################ + + def uv(self, steps, center, origin, size, radius, angle_y, pivot, x, x_cap, path_type='ROUND'): + uvs = [] + n_path_verts, n_path_faces = self.path_sections(steps, path_type) + if path_type in ['ROUND', 'ELLIPSIS']: + x_left = size.x / 2 * (pivot - 1) + x + x_right = size.x / 2 * (pivot + 1) - x + if path_type == 'ELLIPSIS': + y0, y1, a0, da = self._intersect_arc_elliptic(center, radius, x_left, x_right) + else: + y0, y1, a0, da = self._intersect_arc(center, radius, x_left, x_right) + uv_r = abs(da) * radius.x / steps + uv_v = [uv_r for i in range(steps)] + uv_v.insert(0, y0 - origin.y) + uv_v.append(y1 - origin.y) + uv_v.append(size.x) + elif path_type == 'USER_DEFINED': + uv_v = self.user_path_uv_v + elif path_type == 'CIRCLE': + uv_r = 2 * pi * radius.x / steps + uv_v = [uv_r for i in range(steps + 1)] + elif path_type == 'QUADRI': + dy = 0.5 * tan(angle_y) * size.x + uv_v = [size.y - dy, size.x, size.y + dy, size.x] + elif path_type == 'HORIZONTAL': + uv_v = [size.y] + elif path_type == 'VERTICAL': + uv_v = [size.y] + else: + dx = size.x / (1 + self.subdiv_x) + dy = size.y / (1 + self.subdiv_y) + uv_v = [] + for i in range(self.subdiv_y + 1): + uv_v.append(dy * (i + 1)) + for i in range(self.subdiv_x + 1): + uv_v.append(dx * (i + 1)) + for i in range(self.subdiv_y + 1): + uv_v.append(dy * (i + 1)) + for i in range(self.subdiv_x + 1): + uv_v.append(dx * (i + 1)) + # uv_v = [size.y, size.x, size.y, size.x] + + uv_u = self.uv_u + if self.closed_shape: + n_pts = self.n_pts + else: + n_pts = self.n_pts - 1 + v0 = 0 + # uvs parties rondes + for i in range(n_path_faces): + v1 = v0 + uv_v[i] + for j in range(n_pts): + u0 = uv_u[j] + u1 = uv_u[j + 1] + uvs.append([(u0, v1), (u1, v1), (u1, v0), (u0, v0)]) + v0 = v1 + if self.side_cap_back > -1 or self.side_cap_front > -1: + if path_type == 'ROUND': + # rectangle with top part round + coords = self._get_arc_coords(steps, Vector((0, 0, 0)), center, + origin, size, Vector((radius.x - x_cap, 0)), x_cap, pivot, x_cap) + elif path_type == 'CIRCLE': + # full circle + coords = self._get_circle_coords(steps, Vector((0, 0, 0)), center, + origin, Vector((radius.x - x_cap, 0))) + elif path_type == 'ELLIPSIS': + coords = self._get_ellispe_coords(steps, Vector((0, 0, 0)), center, + origin, size, Vector((radius.x - x_cap, radius.y - x_cap)), x_cap, pivot, x_cap) + elif path_type == 'QUADRI': + coords = self._get_vertical_rectangular_trapezoid_coords(Vector((0, 0, 0)), center, + origin, size, radius.x, x_cap, pivot) + # coords = self._get_trapezoidal_coords(0, origin, size, angle_y, x_cap, pivot, x_cap) + else: + coords = self._get_rectangular_coords(Vector((0, 0, 0)), size, x_cap, pivot, 0) + if self.side_cap_front > -1: + uvs.append(list(coords)) + if self.side_cap_back > -1: + uvs.append(list(reversed(coords))) + + if self.closed_shape and not self.closed_path: + coords = [(self.x[self.index[i]], y) for i, y in enumerate(self.y)] + uvs.append(coords) + uvs.append(list(reversed(coords))) + return uvs + + ############################ + # Material indexes + ############################ + + def mat(self, steps, cap_front_id, cap_back_id, path_type='ROUND'): + n_path_verts, n_path_faces = self.path_sections(steps, path_type) + n_profil_faces = self.profil_faces + idmat = [] + for i in range(n_path_faces): + for mat in range(n_profil_faces): + idmat.append(self.idmat[mat]) + if self.side_cap_front > -1: + idmat.append(cap_front_id) + if self.side_cap_back > -1: + idmat.append(cap_back_id) + if self.closed_shape and not self.closed_path: + idmat.append(self.idmat[0]) + idmat.append(self.idmat[0]) + return idmat diff --git a/archipack/presets/archipack_door/160x200_dual.png b/archipack/presets/archipack_door/160x200_dual.png new file mode 100644 index 00000000..ef4fac84 Binary files /dev/null and b/archipack/presets/archipack_door/160x200_dual.png differ diff --git a/archipack/presets/archipack_door/160x200_dual.py b/archipack/presets/archipack_door/160x200_dual.py new file mode 100644 index 00000000..7a9e5ebc --- /dev/null +++ b/archipack/presets/archipack_door/160x200_dual.py @@ -0,0 +1,23 @@ +import bpy +d = bpy.context.active_object.data.archipack_door[0] + +d.handle = 'BOTH' +d.panels_distrib = 'REGULAR' +d.direction = 0 +d.frame_y = 0.029999999329447746 +d.door_y = 0.019999999552965164 +d.flip = False +d.panels_y = 3 +d.frame_x = 0.10000000149011612 +d.model = 2 +d.door_offset = 0.0 +d.x = 1.600000023841858 +d.z = 2.0 +d.hole_margin = 0.10000000149011612 +d.panel_border = 0.12999999523162842 +d.panels_x = 2 +d.panel_spacing = 0.10000000149011612 +d.chanfer = 0.004999999888241291 +d.panel_bottom = 0.17000000178813934 +d.n_panels = 2 +d.y = 0.20000000298023224 diff --git a/archipack/presets/archipack_door/400x240_garage.png b/archipack/presets/archipack_door/400x240_garage.png new file mode 100644 index 00000000..660b1d70 Binary files /dev/null and b/archipack/presets/archipack_door/400x240_garage.png differ diff --git a/archipack/presets/archipack_door/400x240_garage.py b/archipack/presets/archipack_door/400x240_garage.py new file mode 100644 index 00000000..2060cc3b --- /dev/null +++ b/archipack/presets/archipack_door/400x240_garage.py @@ -0,0 +1,23 @@ +import bpy +d = bpy.context.active_object.data.archipack_door[0] + +d.handle = 'NONE' +d.panels_distrib = 'REGULAR' +d.direction = 0 +d.frame_y = 0.029999999329447746 +d.door_y = 0.019999999552965164 +d.flip = False +d.panels_y = 1 +d.frame_x = 0.10000000149011612 +d.model = 1 +d.door_offset = 0.0 +d.x = 4.0 +d.z = 2.4000000953674316 +d.hole_margin = 0.10000000149011612 +d.panel_border = 0.0010000000474974513 +d.panels_x = 24 +d.panel_spacing = 0.0010000000474974513 +d.chanfer = 0.004999999888241291 +d.panel_bottom = 0.0 +d.n_panels = 1 +d.y = 0.20000000298023224 diff --git a/archipack/presets/archipack_door/80x200.png b/archipack/presets/archipack_door/80x200.png new file mode 100644 index 00000000..e2bf6f5c Binary files /dev/null and b/archipack/presets/archipack_door/80x200.png differ diff --git a/archipack/presets/archipack_door/80x200.py b/archipack/presets/archipack_door/80x200.py new file mode 100644 index 00000000..a29e3ddc --- /dev/null +++ b/archipack/presets/archipack_door/80x200.py @@ -0,0 +1,23 @@ +import bpy +d = bpy.context.active_object.data.archipack_door[0] + +d.handle = 'BOTH' +d.panels_distrib = 'REGULAR' +d.direction = 0 +d.frame_y = 0.029999999329447746 +d.door_y = 0.019999999552965164 +d.flip = False +d.panels_y = 1 +d.frame_x = 0.10000000149011612 +d.model = 0 +d.door_offset = 0.0 +d.x = 0.800000011920929 +d.z = 2.0 +d.hole_margin = 0.10000000149011612 +d.panel_border = 0.20000000298023224 +d.panels_x = 1 +d.panel_spacing = 0.10000000149011612 +d.chanfer = 0.004999999888241291 +d.panel_bottom = 0.0 +d.n_panels = 1 +d.y = 0.20000000298023224 diff --git a/archipack/presets/archipack_fence/glass_panels.png b/archipack/presets/archipack_fence/glass_panels.png new file mode 100644 index 00000000..4478afa6 Binary files /dev/null and b/archipack/presets/archipack_fence/glass_panels.png differ diff --git a/archipack/presets/archipack_fence/glass_panels.py b/archipack/presets/archipack_fence/glass_panels.py new file mode 100644 index 00000000..2d150b71 --- /dev/null +++ b/archipack/presets/archipack_fence/glass_panels.py @@ -0,0 +1,67 @@ +import bpy +d = bpy.context.active_object.data.archipack_fence[0] + +d.rail_expand = True +d.shape = 'RECTANGLE' +d.rail = False +d.radius = 0.699999988079071 +d.user_defined_resolution = 12 +d.handrail = False +d.handrail_x = 0.07999999076128006 +d.subs_alt = 0.10000000149011612 +d.handrail_extend = 0.0 +d.idmat_subs = '0' +d.rail_alt = (0.20000000298023224, 0.699999988079071, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0) +d.subs_x = 0.029999999329447746 +d.subs_offset_x = 0.0 +d.handrail_y = 0.03999999910593033 +d.user_defined_subs_enable = True +d.rail_x = (0.030000001192092896, 0.029999999329447746, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806) +d.post_y = 0.009999999776482582 +d.handrail_alt = 1.0 +d.subs_y = 0.09999999403953552 +d.idmat_panel = '2' +d.panel_expand = True +d.panel_x = 0.009999999776482582 +d.idmats_expand = True +d.idmat_post = '0' +d.idmat_handrail = '1' +d.user_defined_post_enable = True +d.x_offset = 0.0 +d.subs_z = 0.7999998927116394 +d.subs_bottom = 'STEP' +d.post_expand = True +d.subs_expand = False +d.rail_offset = (-0.009999999776482582, -0.009999999776482582, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0) +d.post = False +d.handrail_radius = 0.029999999329447746 +d.rail_n = 2 +d.rail_mat.clear() +item_sub_1 = d.rail_mat.add() +item_sub_1.name = '' +item_sub_1.index = '0' +item_sub_1 = d.rail_mat.add() +item_sub_1.name = '' +item_sub_1.index = '0' +d.parts_expand = False +d.angle_limit = 0.39269909262657166 +d.post_spacing = 1.5 +d.handrail_expand = True +d.subs = False +d.handrail_slice_right = True +d.panel_alt = 0.0 +d.user_defined_subs = '' +d.panel_dist = 0.009999999776482582 +d.handrail_slice = True +d.panel = True +d.subs_spacing = 0.07000000774860382 +d.panel_z = 1.0 +d.handrail_profil = 'CIRCLE' +d.handrail_offset = 0.0 +d.da = 1.5707963705062866 +d.post_z = 1.0 +d.rail_z = (0.07000000029802322, 0.07000000029802322, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806) +d.post_x = 0.03999999910593033 +d.user_defined_post = '' +d.panel_offset_x = 0.0 +d.post_alt = 0.0 diff --git a/archipack/presets/archipack_fence/inox_glass_concrete.png b/archipack/presets/archipack_fence/inox_glass_concrete.png new file mode 100644 index 00000000..e9031497 Binary files /dev/null and b/archipack/presets/archipack_fence/inox_glass_concrete.png differ diff --git a/archipack/presets/archipack_fence/inox_glass_concrete.py b/archipack/presets/archipack_fence/inox_glass_concrete.py new file mode 100644 index 00000000..80d3fb6c --- /dev/null +++ b/archipack/presets/archipack_fence/inox_glass_concrete.py @@ -0,0 +1,64 @@ +import bpy +d = bpy.context.active_object.data.archipack_fence[0] + +d.rail_expand = True +d.shape = 'RECTANGLE' +d.rail = True +d.radius = 0.699999988079071 +d.user_defined_resolution = 12 +d.handrail = True +d.handrail_x = 0.07999999076128006 +d.subs_alt = 0.10000000149011612 +d.handrail_extend = 0.0 +d.idmat_subs = '0' +d.rail_alt = (-0.2999999523162842, 0.699999988079071, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0) +d.subs_x = 0.029999999329447746 +d.subs_offset_x = 0.0 +d.handrail_y = 0.03999999910593033 +d.user_defined_subs_enable = True +d.rail_x = (0.19999998807907104, 0.029999999329447746, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806) +d.post_y = 0.009999999776482582 +d.handrail_alt = 1.0 +d.subs_y = 0.09999999403953552 +d.idmat_panel = '2' +d.panel_expand = True +d.panel_x = 0.009999999776482582 +d.idmats_expand = True +d.idmat_post = '0' +d.idmat_handrail = '1' +d.user_defined_post_enable = True +d.x_offset = 0.0 +d.subs_z = 0.7999998927116394 +d.subs_bottom = 'STEP' +d.post_expand = True +d.subs_expand = False +d.rail_offset = (-0.04999999701976776, -0.009999999776482582, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0) +d.post = False +d.handrail_radius = 0.029999999329447746 +d.rail_n = 1 +d.rail_mat.clear() +item_sub_1 = d.rail_mat.add() +item_sub_1.name = '' +item_sub_1.index = '0' +d.parts_expand = False +d.angle_limit = 0.39269909262657166 +d.post_spacing = 1.5 +d.handrail_expand = True +d.subs = False +d.handrail_slice_right = True +d.panel_alt = 0.0 +d.user_defined_subs = '' +d.panel_dist = 0.009999999776482582 +d.handrail_slice = True +d.panel = True +d.subs_spacing = 0.07000000774860382 +d.panel_z = 1.0 +d.handrail_profil = 'CIRCLE' +d.handrail_offset = 0.0 +d.da = 1.5707963705062866 +d.post_z = 1.0 +d.rail_z = (0.3199999928474426, 0.07000000029802322, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806) +d.post_x = 0.03999999910593033 +d.user_defined_post = '' +d.panel_offset_x = 0.0 +d.post_alt = 0.0 diff --git a/archipack/presets/archipack_fence/metal.png b/archipack/presets/archipack_fence/metal.png new file mode 100644 index 00000000..b6a24339 Binary files /dev/null and b/archipack/presets/archipack_fence/metal.png differ diff --git a/archipack/presets/archipack_fence/metal.py b/archipack/presets/archipack_fence/metal.py new file mode 100644 index 00000000..5e7ecbfd --- /dev/null +++ b/archipack/presets/archipack_fence/metal.py @@ -0,0 +1,67 @@ +import bpy +d = bpy.context.active_object.data.archipack_fence[0] + +d.rail_expand = True +d.shape = 'RECTANGLE' +d.rail = True +d.radius = 0.699999988079071 +d.user_defined_resolution = 12 +d.handrail = True +d.handrail_x = 0.03999999910593033 +d.subs_alt = 0.15000000596046448 +d.handrail_extend = 0.10000000149011612 +d.idmat_subs = '1' +d.rail_alt = (0.15000000596046448, 0.8500000238418579, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0) +d.subs_x = 0.019999999552965164 +d.subs_offset_x = 0.0 +d.handrail_y = 0.03999999910593033 +d.user_defined_subs_enable = True +d.rail_x = (0.030000001192092896, 0.029999999329447746, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806) +d.post_y = 0.03999999910593033 +d.handrail_alt = 1.0 +d.subs_y = 0.019999999552965164 +d.idmat_panel = '2' +d.panel_expand = False +d.panel_x = 0.009999999776482582 +d.idmats_expand = False +d.idmat_post = '1' +d.idmat_handrail = '0' +d.user_defined_post_enable = True +d.x_offset = 0.0 +d.subs_z = 0.699999988079071 +d.subs_bottom = 'STEP' +d.post_expand = False +d.subs_expand = True +d.rail_offset = (0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0) +d.post = True +d.handrail_radius = 0.019999999552965164 +d.rail_n = 2 +d.rail_mat.clear() +item_sub_1 = d.rail_mat.add() +item_sub_1.name = '' +item_sub_1.index = '1' +item_sub_1 = d.rail_mat.add() +item_sub_1.name = '' +item_sub_1.index = '1' +d.parts_expand = False +d.angle_limit = 0.39269909262657166 +d.post_spacing = 1.5 +d.handrail_expand = False +d.subs = True +d.handrail_slice_right = True +d.panel_alt = 0.20999997854232788 +d.user_defined_subs = '' +d.panel_dist = 0.03999999910593033 +d.handrail_slice = True +d.panel = False +d.subs_spacing = 0.10000000149011612 +d.panel_z = 0.6000000238418579 +d.handrail_profil = 'SQUARE' +d.handrail_offset = 0.0 +d.da = 1.5707963705062866 +d.post_z = 1.0 +d.rail_z = (0.019999999552965164, 0.019999999552965164, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806) +d.post_x = 0.03999999910593033 +d.user_defined_post = '' +d.panel_offset_x = 0.0 +d.post_alt = 0.0 diff --git a/archipack/presets/archipack_fence/metal_glass.png b/archipack/presets/archipack_fence/metal_glass.png new file mode 100644 index 00000000..16020ec4 Binary files /dev/null and b/archipack/presets/archipack_fence/metal_glass.png differ diff --git a/archipack/presets/archipack_fence/metal_glass.py b/archipack/presets/archipack_fence/metal_glass.py new file mode 100644 index 00000000..fb5149cb --- /dev/null +++ b/archipack/presets/archipack_fence/metal_glass.py @@ -0,0 +1,67 @@ +import bpy +d = bpy.context.active_object.data.archipack_fence[0] + +d.rail_expand = True +d.shape = 'RECTANGLE' +d.rail = True +d.radius = 0.699999988079071 +d.user_defined_resolution = 12 +d.handrail = True +d.handrail_x = 0.03999999910593033 +d.subs_alt = 0.0 +d.handrail_extend = 0.10000000149011612 +d.idmat_subs = '1' +d.rail_alt = (0.15000000596046448, 0.8500000238418579, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0) +d.subs_x = 0.019999999552965164 +d.subs_offset_x = 0.0 +d.handrail_y = 0.03999999910593033 +d.user_defined_subs_enable = True +d.rail_x = (0.030000001192092896, 0.029999999329447746, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806) +d.post_y = 0.03999999910593033 +d.handrail_alt = 1.0 +d.subs_y = 0.019999999552965164 +d.idmat_panel = '2' +d.panel_expand = False +d.panel_x = 0.009999999776482582 +d.idmats_expand = False +d.idmat_post = '1' +d.idmat_handrail = '0' +d.user_defined_post_enable = True +d.x_offset = 0.0 +d.subs_z = 1.0 +d.subs_bottom = 'STEP' +d.post_expand = True +d.subs_expand = False +d.rail_offset = (0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0) +d.post = True +d.handrail_radius = 0.019999999552965164 +d.rail_n = 2 +d.rail_mat.clear() +item_sub_1 = d.rail_mat.add() +item_sub_1.name = '' +item_sub_1.index = '1' +item_sub_1 = d.rail_mat.add() +item_sub_1.name = '' +item_sub_1.index = '1' +d.parts_expand = False +d.angle_limit = 0.39269909262657166 +d.post_spacing = 1.5 +d.handrail_expand = False +d.subs = False +d.handrail_slice_right = True +d.panel_alt = 0.20999997854232788 +d.user_defined_subs = '' +d.panel_dist = 0.03999999910593033 +d.handrail_slice = True +d.panel = True +d.subs_spacing = 0.10000000149011612 +d.panel_z = 0.6000000238418579 +d.handrail_profil = 'SQUARE' +d.handrail_offset = 0.0 +d.da = 1.5707963705062866 +d.post_z = 1.0 +d.rail_z = (0.019999999552965164, 0.019999999552965164, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806) +d.post_x = 0.03999999910593033 +d.user_defined_post = '' +d.panel_offset_x = 0.0 +d.post_alt = 0.0 diff --git a/archipack/presets/archipack_fence/wood.png b/archipack/presets/archipack_fence/wood.png new file mode 100644 index 00000000..a1706f29 Binary files /dev/null and b/archipack/presets/archipack_fence/wood.png differ diff --git a/archipack/presets/archipack_fence/wood.py b/archipack/presets/archipack_fence/wood.py new file mode 100644 index 00000000..9a9a42d9 --- /dev/null +++ b/archipack/presets/archipack_fence/wood.py @@ -0,0 +1,67 @@ +import bpy +d = bpy.context.active_object.data.archipack_fence[0] + +d.user_defined_post = '' +d.handrail_offset = 0.0 +d.post_spacing = 1.5 +d.post_z = 1.0 +d.idmats_expand = True +d.rail_alt = (0.20000000298023224, 0.699999988079071, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0) +d.idmat_handrail = '0' +d.post_alt = 0.0 +d.handrail_expand = True +d.panel_x = 0.009999999776482582 +d.idmat_panel = '2' +d.rail_z = (0.07000000029802322, 0.07000000029802322, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806) +d.subs_y = 0.09999999403953552 +d.handrail_radius = 0.019999999552965164 +d.handrail_extend = 0.10000000149011612 +d.subs_alt = 0.10000000149011612 +d.idmat_subs = '0' +d.handrail_y = 0.03999999910593033 +d.user_defined_post_enable = True +d.rail = True +d.handrail_profil = 'SQUARE' +d.post_x = 0.059999994933605194 +d.handrail = True +d.da = 1.5707963705062866 +d.user_defined_subs_enable = True +d.subs_expand = True +d.shape = 'RECTANGLE' +d.angle_limit = 0.39269909262657166 +d.panel_alt = 0.20999997854232788 +d.post_expand = True +d.subs_bottom = 'STEP' +d.handrail_slice_right = True +d.handrail_alt = 1.0 +d.subs_z = 0.7999998927116394 +d.user_defined_subs = '' +d.rail_x = (0.030000001192092896, 0.029999999329447746, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806) +d.parts_expand = False +d.idmat_post = '0' +d.panel_offset_x = 0.0 +d.rail_n = 2 +d.panel_z = 0.6000000238418579 +d.handrail_x = 0.07999999076128006 +d.subs_spacing = 0.14000000059604645 +d.post = True +d.rail_mat.clear() +item_sub_1 = d.rail_mat.add() +item_sub_1.name = '' +item_sub_1.index = '0' +item_sub_1 = d.rail_mat.add() +item_sub_1.name = '' +item_sub_1.index = '0' +d.handrail_slice = True +d.panel = False +d.x_offset = 0.0 +d.rail_expand = True +d.rail_offset = (0.009999999776482582, 0.009999999776482582, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0) +d.panel_dist = 0.03999999910593033 +d.post_y = 0.059999994933605194 +d.subs = True +d.user_defined_resolution = 12 +d.subs_x = 0.029999999329447746 +d.radius = 0.699999988079071 +d.subs_offset_x = 0.0 +d.panel_expand = False diff --git a/archipack/presets/archipack_floor/herringbone_50x10.png b/archipack/presets/archipack_floor/herringbone_50x10.png new file mode 100644 index 00000000..b6e7fe56 Binary files /dev/null and b/archipack/presets/archipack_floor/herringbone_50x10.png differ diff --git a/archipack/presets/archipack_floor/herringbone_50x10.py b/archipack/presets/archipack_floor/herringbone_50x10.py new file mode 100644 index 00000000..a1f196ef --- /dev/null +++ b/archipack/presets/archipack_floor/herringbone_50x10.py @@ -0,0 +1,34 @@ +import bpy +d = bpy.context.active_object.data.archipack_floor[0] + +d.space_l = 0.004999999888241291 +d.is_width_vary = False +d.offset_vary = 47.810237884521484 +d.is_ran_thickness = False +d.b_length = 2.0 +d.t_length = 0.30000001192092896 +d.space_w = 0.004999999888241291 +d.t_width_s = 0.10000000149011612 +d.b_length_s = 0.5 +d.is_grout = False +d.tile_types = '24' +d.offset = 50.0 +d.width_vary = 50.0 +d.spacing = 0.0010000000474974513 +d.is_offset = True +d.is_bevel = False +d.is_random_offset = True +d.bevel_amo = 0.001500000013038516 +d.thickness = 0.019999999552965164 +d.bevel_res = 1 +d.max_boards = 2 +d.b_width = 0.10000000149011612 +d.length_vary = 50.0 +d.ran_thickness = 50.0 +d.is_mat_vary = True +d.hb_direction = '1' +d.mat_vary = 3 +d.num_boards = 5 +d.t_width = 0.30000001192092896 +d.grout_depth = 0.0010000003967434168 +d.is_length_vary = False diff --git a/archipack/presets/archipack_floor/herringbone_p_50x10.png b/archipack/presets/archipack_floor/herringbone_p_50x10.png new file mode 100644 index 00000000..1a2b2370 Binary files /dev/null and b/archipack/presets/archipack_floor/herringbone_p_50x10.png differ diff --git a/archipack/presets/archipack_floor/herringbone_p_50x10.py b/archipack/presets/archipack_floor/herringbone_p_50x10.py new file mode 100644 index 00000000..088a22e4 --- /dev/null +++ b/archipack/presets/archipack_floor/herringbone_p_50x10.py @@ -0,0 +1,34 @@ +import bpy +d = bpy.context.active_object.data.archipack_floor[0] + +d.space_l = 0.004999999888241291 +d.is_width_vary = False +d.offset_vary = 47.810237884521484 +d.is_ran_thickness = False +d.b_length = 2.0 +d.t_length = 0.30000001192092896 +d.space_w = 0.004999999888241291 +d.t_width_s = 0.10000000149011612 +d.b_length_s = 0.5 +d.is_grout = False +d.tile_types = '23' +d.offset = 50.0 +d.width_vary = 50.0 +d.spacing = 0.0010000000474974513 +d.is_offset = True +d.is_bevel = False +d.is_random_offset = True +d.bevel_amo = 0.001500000013038516 +d.thickness = 0.019999999552965164 +d.bevel_res = 1 +d.max_boards = 2 +d.b_width = 0.10000000149011612 +d.length_vary = 50.0 +d.ran_thickness = 50.0 +d.is_mat_vary = True +d.hb_direction = '1' +d.mat_vary = 3 +d.num_boards = 5 +d.t_width = 0.30000001192092896 +d.grout_depth = 0.0010000003967434168 +d.is_length_vary = False diff --git a/archipack/presets/archipack_floor/parquet_15x3.png b/archipack/presets/archipack_floor/parquet_15x3.png new file mode 100644 index 00000000..2b35d58b Binary files /dev/null and b/archipack/presets/archipack_floor/parquet_15x3.png differ diff --git a/archipack/presets/archipack_floor/parquet_15x3.py b/archipack/presets/archipack_floor/parquet_15x3.py new file mode 100644 index 00000000..5711c93a --- /dev/null +++ b/archipack/presets/archipack_floor/parquet_15x3.py @@ -0,0 +1,34 @@ +import bpy +d = bpy.context.active_object.data.archipack_floor[0] + +d.bevel_res = 1 +d.b_width = 0.029999999329447746 +d.is_bevel = False +d.hb_direction = '1' +d.is_width_vary = False +d.b_length = 2.0 +d.spacing = 0.0010000000474974513 +d.is_grout = False +d.num_boards = 5 +d.is_length_vary = False +d.thickness = 0.019999999552965164 +d.is_ran_thickness = False +d.is_random_offset = True +d.offset_vary = 47.810237884521484 +d.is_mat_vary = True +d.tile_types = '22' +d.length_vary = 50.0 +d.space_w = 0.004999999888241291 +d.ran_thickness = 50.0 +d.max_boards = 2 +d.t_width_s = 0.10000000149011612 +d.t_width = 0.30000001192092896 +d.t_length = 0.30000001192092896 +d.width_vary = 50.0 +d.mat_vary = 3 +d.grout_depth = 0.0010000003967434168 +d.is_offset = True +d.space_l = 0.004999999888241291 +d.bevel_amo = 0.001500000013038516 +d.offset = 50.0 +d.b_length_s = 2.0 diff --git a/archipack/presets/archipack_floor/planks_200x20.png b/archipack/presets/archipack_floor/planks_200x20.png new file mode 100644 index 00000000..94a49c57 Binary files /dev/null and b/archipack/presets/archipack_floor/planks_200x20.png differ diff --git a/archipack/presets/archipack_floor/planks_200x20.py b/archipack/presets/archipack_floor/planks_200x20.py new file mode 100644 index 00000000..bbea2e66 --- /dev/null +++ b/archipack/presets/archipack_floor/planks_200x20.py @@ -0,0 +1,34 @@ +import bpy +d = bpy.context.active_object.data.archipack_floor[0] + +d.bevel_res = 1 +d.b_width = 0.2 +d.is_bevel = True +d.hb_direction = '1' +d.is_width_vary = False +d.b_length = 2.0 +d.spacing = 0.002 +d.is_grout = False +d.num_boards = 4 +d.is_length_vary = False +d.thickness = 0.02 +d.is_ran_thickness = False +d.is_random_offset = True +d.offset_vary = 47.81 +d.is_mat_vary = True +d.tile_types = '21' +d.length_vary = 50.0 +d.space_w = 0.002 +d.ran_thickness = 50.0 +d.max_boards = 2 +d.t_width_s = 0.1 +d.t_width = 0.3 +d.t_length = 0.3 +d.width_vary = 50.0 +d.mat_vary = 3 +d.grout_depth = 0.001 +d.is_offset = True +d.space_l = 0.002 +d.bevel_amo = 0.0015 +d.offset = 50.0 +d.b_length_s = 2.0 diff --git a/archipack/presets/archipack_floor/tiles_15x15.png b/archipack/presets/archipack_floor/tiles_15x15.png new file mode 100644 index 00000000..2a3d8633 Binary files /dev/null and b/archipack/presets/archipack_floor/tiles_15x15.png differ diff --git a/archipack/presets/archipack_floor/tiles_15x15.py b/archipack/presets/archipack_floor/tiles_15x15.py new file mode 100644 index 00000000..d3d244f9 --- /dev/null +++ b/archipack/presets/archipack_floor/tiles_15x15.py @@ -0,0 +1,34 @@ +import bpy +d = bpy.context.active_object.data.archipack_floor[0] + +d.b_width = 0.20000000298023224 +d.width_vary = 50.0 +d.t_width_s = 0.20000000298023224 +d.is_grout = True +d.tile_types = '1' +d.space_l = 0.004999999888241291 +d.is_length_vary = False +d.hb_direction = '1' +d.offset_vary = 50.0 +d.offset = 50.0 +d.spacing = 0.004999999888241291 +d.thickness = 0.10000000149011612 +d.bevel_res = 1 +d.is_offset = False +d.grout_depth = 0.0010000003967434168 +d.t_width = 0.15000000596046448 +d.is_ran_thickness = False +d.is_mat_vary = False +d.is_random_offset = False +d.space_w = 0.004999999888241291 +d.is_bevel = True +d.ran_thickness = 50.0 +d.max_boards = 2 +d.t_length = 0.15000000596046448 +d.b_length_s = 2.0 +d.bevel_amo = 0.001500000013038516 +d.is_width_vary = False +d.num_boards = 4 +d.length_vary = 50.0 +d.b_length = 0.800000011920929 +d.mat_vary = 1 diff --git a/archipack/presets/archipack_floor/tiles_60x30.png b/archipack/presets/archipack_floor/tiles_60x30.png new file mode 100644 index 00000000..16cdf0f1 Binary files /dev/null and b/archipack/presets/archipack_floor/tiles_60x30.png differ diff --git a/archipack/presets/archipack_floor/tiles_60x30.py b/archipack/presets/archipack_floor/tiles_60x30.py new file mode 100644 index 00000000..f8b66129 --- /dev/null +++ b/archipack/presets/archipack_floor/tiles_60x30.py @@ -0,0 +1,34 @@ +import bpy +d = bpy.context.active_object.data.archipack_floor[0] + +d.b_width = 0.20000000298023224 +d.width_vary = 50.0 +d.t_width_s = 0.20000000298023224 +d.is_grout = True +d.tile_types = '1' +d.space_l = 0.004999999888241291 +d.is_length_vary = False +d.hb_direction = '1' +d.offset_vary = 50.0 +d.offset = 50.0 +d.spacing = 0.004999999888241291 +d.thickness = 0.10000000149011612 +d.bevel_res = 1 +d.is_offset = False +d.grout_depth = 0.0010000003967434168 +d.t_width = 0.30000001192092896 +d.is_ran_thickness = False +d.is_mat_vary = False +d.is_random_offset = False +d.space_w = 0.004999999888241291 +d.is_bevel = True +d.ran_thickness = 50.0 +d.max_boards = 2 +d.t_length = 0.6000000238418579 +d.b_length_s = 2.0 +d.bevel_amo = 0.001500000013038516 +d.is_width_vary = False +d.num_boards = 4 +d.length_vary = 50.0 +d.b_length = 0.800000011920929 +d.mat_vary = 1 diff --git a/archipack/presets/archipack_floor/tiles_hex_10x10.png b/archipack/presets/archipack_floor/tiles_hex_10x10.png new file mode 100644 index 00000000..4d4c8ecf Binary files /dev/null and b/archipack/presets/archipack_floor/tiles_hex_10x10.png differ diff --git a/archipack/presets/archipack_floor/tiles_hex_10x10.py b/archipack/presets/archipack_floor/tiles_hex_10x10.py new file mode 100644 index 00000000..01086dc8 --- /dev/null +++ b/archipack/presets/archipack_floor/tiles_hex_10x10.py @@ -0,0 +1,34 @@ +import bpy +d = bpy.context.active_object.data.archipack_floor[0] + +d.bevel_res = 1 +d.b_width = 0.20000000298023224 +d.is_bevel = True +d.hb_direction = '1' +d.is_width_vary = False +d.b_length = 0.800000011920929 +d.spacing = 0.004999999888241291 +d.is_grout = True +d.num_boards = 4 +d.is_length_vary = False +d.thickness = 0.10000000149011612 +d.is_ran_thickness = False +d.is_random_offset = False +d.offset_vary = 50.0 +d.is_mat_vary = False +d.tile_types = '4' +d.length_vary = 50.0 +d.space_w = 0.004999999888241291 +d.ran_thickness = 50.0 +d.max_boards = 2 +d.t_width_s = 0.10000000149011612 +d.t_width = 0.30000001192092896 +d.t_length = 0.30000001192092896 +d.width_vary = 50.0 +d.mat_vary = 1 +d.grout_depth = 0.0010000003967434168 +d.is_offset = False +d.space_l = 0.004999999888241291 +d.bevel_amo = 0.001500000013038516 +d.offset = 50.0 +d.b_length_s = 2.0 diff --git a/archipack/presets/archipack_floor/tiles_l+ms_30x30_15x15.png b/archipack/presets/archipack_floor/tiles_l+ms_30x30_15x15.png new file mode 100644 index 00000000..07c6e266 Binary files /dev/null and b/archipack/presets/archipack_floor/tiles_l+ms_30x30_15x15.png differ diff --git a/archipack/presets/archipack_floor/tiles_l+ms_30x30_15x15.py b/archipack/presets/archipack_floor/tiles_l+ms_30x30_15x15.py new file mode 100644 index 00000000..3ee45a2d --- /dev/null +++ b/archipack/presets/archipack_floor/tiles_l+ms_30x30_15x15.py @@ -0,0 +1,34 @@ +import bpy +d = bpy.context.active_object.data.archipack_floor[0] + +d.bevel_res = 1 +d.b_width = 0.20000000298023224 +d.is_bevel = True +d.hb_direction = '1' +d.is_width_vary = False +d.b_length = 0.800000011920929 +d.spacing = 0.004999999888241291 +d.is_grout = True +d.num_boards = 4 +d.is_length_vary = False +d.thickness = 0.10000000149011612 +d.is_ran_thickness = False +d.is_random_offset = False +d.offset_vary = 50.0 +d.is_mat_vary = False +d.tile_types = '3' +d.length_vary = 50.0 +d.space_w = 0.004999999888241291 +d.ran_thickness = 50.0 +d.max_boards = 2 +d.t_width_s = 0.20000000298023224 +d.t_width = 0.30000001192092896 +d.t_length = 0.30000001192092896 +d.width_vary = 50.0 +d.mat_vary = 1 +d.grout_depth = 0.0010000003967434168 +d.is_offset = False +d.space_l = 0.004999999888241291 +d.bevel_amo = 0.001500000013038516 +d.offset = 50.0 +d.b_length_s = 2.0 diff --git a/archipack/presets/archipack_floor/tiles_l+s_30x30_15x15.png b/archipack/presets/archipack_floor/tiles_l+s_30x30_15x15.png new file mode 100644 index 00000000..33d28657 Binary files /dev/null and b/archipack/presets/archipack_floor/tiles_l+s_30x30_15x15.png differ diff --git a/archipack/presets/archipack_floor/tiles_l+s_30x30_15x15.py b/archipack/presets/archipack_floor/tiles_l+s_30x30_15x15.py new file mode 100644 index 00000000..8f4253fe --- /dev/null +++ b/archipack/presets/archipack_floor/tiles_l+s_30x30_15x15.py @@ -0,0 +1,34 @@ +import bpy +d = bpy.context.active_object.data.archipack_floor[0] + +d.b_width = 0.20000000298023224 +d.width_vary = 50.0 +d.t_width_s = 0.20000000298023224 +d.is_grout = True +d.tile_types = '2' +d.space_l = 0.004999999888241291 +d.is_length_vary = False +d.hb_direction = '1' +d.offset_vary = 50.0 +d.offset = 50.0 +d.spacing = 0.004999999888241291 +d.thickness = 0.10000000149011612 +d.bevel_res = 1 +d.is_offset = False +d.grout_depth = 0.0010000003967434168 +d.t_width = 0.30000001192092896 +d.is_ran_thickness = False +d.is_mat_vary = False +d.is_random_offset = False +d.space_w = 0.004999999888241291 +d.is_bevel = True +d.ran_thickness = 50.0 +d.max_boards = 2 +d.t_length = 0.30000001192092896 +d.b_length_s = 2.0 +d.bevel_amo = 0.001500000013038516 +d.is_width_vary = False +d.num_boards = 4 +d.length_vary = 50.0 +d.b_length = 0.800000011920929 +d.mat_vary = 1 diff --git a/archipack/presets/archipack_stair/i_wood_over_concrete.png b/archipack/presets/archipack_stair/i_wood_over_concrete.png new file mode 100644 index 00000000..9fb3d56c Binary files /dev/null and b/archipack/presets/archipack_stair/i_wood_over_concrete.png differ diff --git a/archipack/presets/archipack_stair/i_wood_over_concrete.py b/archipack/presets/archipack_stair/i_wood_over_concrete.py new file mode 100644 index 00000000..53b605cf --- /dev/null +++ b/archipack/presets/archipack_stair/i_wood_over_concrete.py @@ -0,0 +1,117 @@ +import bpy +d = bpy.context.active_object.data.archipack_stair[0] + +d.steps_type = 'CLOSED' +d.handrail_slice_right = True +d.total_angle = 6.2831854820251465 +d.user_defined_subs_enable = True +d.string_z = 0.30000001192092896 +d.nose_z = 0.029999999329447746 +d.user_defined_subs = '' +d.idmat_step_side = '3' +d.handrail_x = 0.03999999910593033 +d.right_post = True +d.left_post = True +d.width = 1.5 +d.subs_offset_x = 0.0 +d.rail_mat.clear() +item_sub_1 = d.rail_mat.add() +item_sub_1.name = '' +item_sub_1.index = '4' +d.step_depth = 0.30000001192092896 +d.rail_z = (0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806) +d.right_subs = False +d.left_panel = True +d.idmat_handrail = '3' +d.da = 3.1415927410125732 +d.post_alt = 0.0 +d.left_subs = False +d.n_parts = 1 +d.user_defined_post_enable = True +d.handrail_slice_left = True +d.handrail_profil = 'SQUARE' +d.handrail_expand = False +d.panel_alt = 0.25 +d.post_expand = False +d.subs_z = 1.0 +d.rail_alt = (1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0) +d.panel_dist = 0.05000000074505806 +d.panel_expand = False +d.x_offset = 0.0 +d.subs_expand = False +d.idmat_post = '4' +d.left_string = False +d.string_alt = -0.03999999910593033 +d.handrail_y = 0.03999999910593033 +d.radius = 1.0 +d.string_expand = False +d.post_z = 1.0 +d.idmat_top = '3' +d.idmat_bottom = '1' +d.parts.clear() +item_sub_1 = d.parts.add() +item_sub_1.name = '' +item_sub_1.manipulators.clear() +item_sub_2 = item_sub_1.manipulators.add() +item_sub_2.name = '' +item_sub_2.p0 = (0.0, 0.0, 2.700000047683716) +item_sub_2.prop1_name = 'length' +item_sub_2.p2 = (-1.0, 0.0, 0.0) +item_sub_2.normal = (0.0, 0.0, 1.0) +item_sub_2.pts_mode = 'SIZE' +item_sub_2.p1 = (0.0, 4.0, 2.700000047683716) +item_sub_2.prop2_name = '' +item_sub_2.type_key = 'SIZE' +item_sub_1.right_shape = 'RECTANGLE' +item_sub_1.radius = 0.699999988079071 +item_sub_1.type = 'S_STAIR' +item_sub_1.length = 4.0 +item_sub_1.left_shape = 'RECTANGLE' +item_sub_1.da = 1.5707963705062866 +d.subs_bottom = 'STEP' +d.user_defined_post = '' +d.panel_offset_x = 0.0 +d.idmat_side = '1' +d.right_string = False +d.idmat_raise = '1' +d.left_rail = False +d.parts_expand = False +d.panel_z = 0.6000000238418579 +d.bottom_z = 0.029999999329447746 +d.z_mode = 'STANDARD' +d.panel_x = 0.009999999776482582 +d.post_x = 0.03999999910593033 +d.presets = 'STAIR_I' +d.steps_expand = True +d.subs_x = 0.019999999552965164 +d.subs_spacing = 0.10000000149011612 +d.left_handrail = True +d.handrail_offset = 0.0 +d.right_rail = False +d.idmat_panel = '5' +d.post_offset_x = 0.019999999552965164 +d.idmat_step_front = '3' +d.rail_n = 1 +d.string_offset = 0.0 +d.subs_y = 0.019999999552965164 +d.handrail_alt = 1.0 +d.post_corners = False +d.rail_expand = False +d.rail_offset = (0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0) +d.rail_x = (0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806) +d.left_shape = 'RECTANGLE' +d.nose_y = 0.019999999552965164 +d.nose_type = 'STRAIGHT' +d.handrail_extend = 0.10000000149011612 +d.idmat_string = '3' +d.post_y = 0.03999999910593033 +d.subs_alt = 0.0 +d.right_handrail = True +d.idmats_expand = False +d.right_shape = 'RECTANGLE' +d.idmat_subs = '4' +d.handrail_radius = 0.019999999552965164 +d.right_panel = True +d.post_spacing = 1.0 +d.string_x = 0.019999999552965164 +d.height = 2.700000047683716 diff --git a/archipack/presets/archipack_stair/l_wood_over_concrete.png b/archipack/presets/archipack_stair/l_wood_over_concrete.png new file mode 100644 index 00000000..0e2ce6b6 Binary files /dev/null and b/archipack/presets/archipack_stair/l_wood_over_concrete.png differ diff --git a/archipack/presets/archipack_stair/l_wood_over_concrete.py b/archipack/presets/archipack_stair/l_wood_over_concrete.py new file mode 100644 index 00000000..d4fc1344 --- /dev/null +++ b/archipack/presets/archipack_stair/l_wood_over_concrete.py @@ -0,0 +1,155 @@ +import bpy +d = bpy.context.active_object.data.archipack_stair[0] + +d.steps_type = 'CLOSED' +d.handrail_slice_right = True +d.total_angle = 6.2831854820251465 +d.user_defined_subs_enable = True +d.string_z = 0.30000001192092896 +d.nose_z = 0.029999999329447746 +d.user_defined_subs = '' +d.idmat_step_side = '3' +d.handrail_x = 0.03999999910593033 +d.right_post = True +d.left_post = True +d.width = 1.5 +d.subs_offset_x = 0.0 +d.rail_mat.clear() +item_sub_1 = d.rail_mat.add() +item_sub_1.name = '' +item_sub_1.index = '4' +d.step_depth = 0.30000001192092896 +d.rail_z = (0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806) +d.right_subs = False +d.left_panel = True +d.idmat_handrail = '3' +d.da = 1.5707963705062866 +d.post_alt = 0.0 +d.left_subs = False +d.n_parts = 3 +d.user_defined_post_enable = True +d.handrail_slice_left = True +d.handrail_profil = 'SQUARE' +d.handrail_expand = False +d.panel_alt = 0.25 +d.post_expand = False +d.subs_z = 1.0 +d.rail_alt = (1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0) +d.panel_dist = 0.05000000074505806 +d.panel_expand = False +d.x_offset = 0.0 +d.subs_expand = False +d.idmat_post = '4' +d.left_string = False +d.string_alt = -0.03999999910593033 +d.handrail_y = 0.03999999910593033 +d.radius = 1.0 +d.string_expand = False +d.post_z = 1.0 +d.idmat_top = '3' +d.idmat_bottom = '1' +d.parts.clear() +item_sub_1 = d.parts.add() +item_sub_1.name = '' +item_sub_1.manipulators.clear() +item_sub_2 = item_sub_1.manipulators.add() +item_sub_2.name = '' +item_sub_2.p0 = (0.0, 0.0, 1.4040000438690186) +item_sub_2.prop1_name = 'length' +item_sub_2.p2 = (1.0, 0.0, 0.0) +item_sub_2.normal = (0.0, 0.0, 1.0) +item_sub_2.pts_mode = 'SIZE' +item_sub_2.p1 = (0.0, 4.0, 1.4040000438690186) +item_sub_2.prop2_name = '' +item_sub_2.type_key = 'SIZE' +item_sub_1.right_shape = 'RECTANGLE' +item_sub_1.radius = 0.699999988079071 +item_sub_1.type = 'S_STAIR' +item_sub_1.length = 4.0 +item_sub_1.left_shape = 'RECTANGLE' +item_sub_1.da = 1.5707963705062866 +item_sub_1 = d.parts.add() +item_sub_1.name = '' +item_sub_1.manipulators.clear() +item_sub_2 = item_sub_1.manipulators.add() +item_sub_2.name = '' +item_sub_2.p0 = (-1.0, 4.0, 1.944000005722046) +item_sub_2.prop1_name = 'da' +item_sub_2.p2 = (0.0, 1.0, 0.0) +item_sub_2.normal = (0.0, 0.0, 1.0) +item_sub_2.pts_mode = 'RADIUS' +item_sub_2.p1 = (1.0, 0.0, 0.0) +item_sub_2.prop2_name = 'radius' +item_sub_2.type_key = 'ARC_ANGLE_RADIUS' +item_sub_1.right_shape = 'RECTANGLE' +item_sub_1.radius = 0.699999988079071 +item_sub_1.type = 'C_STAIR' +item_sub_1.length = 2.0 +item_sub_1.left_shape = 'RECTANGLE' +item_sub_1.da = 1.5707963705062866 +item_sub_1 = d.parts.add() +item_sub_1.name = '' +item_sub_1.manipulators.clear() +item_sub_2 = item_sub_1.manipulators.add() +item_sub_2.name = '' +item_sub_2.p0 = (-1.0, 5.0, 2.700000047683716) +item_sub_2.prop1_name = 'length' +item_sub_2.p2 = (1.0, 0.0, 0.0) +item_sub_2.normal = (0.0, 0.0, 1.0) +item_sub_2.pts_mode = 'SIZE' +item_sub_2.p1 = (-3.0, 5.0, 2.700000047683716) +item_sub_2.prop2_name = '' +item_sub_2.type_key = 'SIZE' +item_sub_1.right_shape = 'RECTANGLE' +item_sub_1.radius = 0.699999988079071 +item_sub_1.type = 'S_STAIR' +item_sub_1.length = 2.0 +item_sub_1.left_shape = 'RECTANGLE' +item_sub_1.da = 1.5707963705062866 +d.subs_bottom = 'STEP' +d.user_defined_post = '' +d.panel_offset_x = 0.0 +d.idmat_side = '1' +d.right_string = False +d.idmat_raise = '1' +d.left_rail = False +d.parts_expand = False +d.panel_z = 0.6000000238418579 +d.bottom_z = 0.029999999329447746 +d.z_mode = 'STANDARD' +d.panel_x = 0.009999999776482582 +d.post_x = 0.03999999910593033 +d.presets = 'STAIR_L' +d.steps_expand = True +d.subs_x = 0.019999999552965164 +d.subs_spacing = 0.10000000149011612 +d.left_handrail = True +d.handrail_offset = 0.0 +d.right_rail = False +d.idmat_panel = '5' +d.post_offset_x = 0.019999999552965164 +d.idmat_step_front = '3' +d.rail_n = 1 +d.string_offset = 0.0 +d.subs_y = 0.019999999552965164 +d.handrail_alt = 1.0 +d.post_corners = False +d.rail_expand = False +d.rail_offset = (0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0) +d.rail_x = (0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806) +d.left_shape = 'RECTANGLE' +d.nose_y = 0.019999999552965164 +d.nose_type = 'STRAIGHT' +d.handrail_extend = 0.10000000149011612 +d.idmat_string = '3' +d.post_y = 0.03999999910593033 +d.subs_alt = 0.0 +d.right_handrail = True +d.idmats_expand = False +d.right_shape = 'RECTANGLE' +d.idmat_subs = '4' +d.handrail_radius = 0.019999999552965164 +d.right_panel = True +d.post_spacing = 1.0 +d.string_x = 0.019999999552965164 +d.height = 2.700000047683716 diff --git a/archipack/presets/archipack_stair/o_wood_over_concrete.png b/archipack/presets/archipack_stair/o_wood_over_concrete.png new file mode 100644 index 00000000..215d42b9 Binary files /dev/null and b/archipack/presets/archipack_stair/o_wood_over_concrete.png differ diff --git a/archipack/presets/archipack_stair/o_wood_over_concrete.py b/archipack/presets/archipack_stair/o_wood_over_concrete.py new file mode 100644 index 00000000..586aa990 --- /dev/null +++ b/archipack/presets/archipack_stair/o_wood_over_concrete.py @@ -0,0 +1,136 @@ +import bpy +d = bpy.context.active_object.data.archipack_stair[0] + +d.steps_type = 'CLOSED' +d.handrail_slice_right = True +d.total_angle = 6.2831854820251465 +d.user_defined_subs_enable = True +d.string_z = 0.30000001192092896 +d.nose_z = 0.029999999329447746 +d.user_defined_subs = '' +d.idmat_step_side = '3' +d.handrail_x = 0.03999999910593033 +d.right_post = True +d.left_post = True +d.width = 1.5 +d.subs_offset_x = 0.0 +d.rail_mat.clear() +item_sub_1 = d.rail_mat.add() +item_sub_1.name = '' +item_sub_1.index = '4' +d.step_depth = 0.30000001192092896 +d.rail_z = (0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806) +d.right_subs = False +d.left_panel = True +d.idmat_handrail = '3' +d.da = 3.1415927410125732 +d.post_alt = 0.0 +d.left_subs = False +d.n_parts = 2 +d.user_defined_post_enable = True +d.handrail_slice_left = True +d.handrail_profil = 'SQUARE' +d.handrail_expand = False +d.panel_alt = 0.25 +d.post_expand = False +d.subs_z = 1.0 +d.rail_alt = (1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0) +d.panel_dist = 0.05000000074505806 +d.panel_expand = False +d.x_offset = 0.0 +d.subs_expand = False +d.idmat_post = '4' +d.left_string = False +d.string_alt = -0.03999999910593033 +d.handrail_y = 0.03999999910593033 +d.radius = 1.0 +d.string_expand = False +d.post_z = 1.0 +d.idmat_top = '3' +d.idmat_bottom = '1' +d.parts.clear() +item_sub_1 = d.parts.add() +item_sub_1.name = '' +item_sub_1.manipulators.clear() +item_sub_2 = item_sub_1.manipulators.add() +item_sub_2.name = '' +item_sub_2.p0 = (-1.0, 0.0, 1.350000023841858) +item_sub_2.prop1_name = 'da' +item_sub_2.p2 = (-1.0, 1.2246468525851679e-16, 0.0) +item_sub_2.normal = (0.0, 0.0, 1.0) +item_sub_2.pts_mode = 'SIZE' +item_sub_2.p1 = (1.0, 0.0, 0.0) +item_sub_2.prop2_name = 'radius' +item_sub_2.type_key = 'ARC_ANGLE_RADIUS' +item_sub_1.right_shape = 'RECTANGLE' +item_sub_1.radius = 0.699999988079071 +item_sub_1.type = 'D_STAIR' +item_sub_1.length = 4.0 +item_sub_1.left_shape = 'RECTANGLE' +item_sub_1.da = 1.5707963705062866 +item_sub_1 = d.parts.add() +item_sub_1.name = '' +item_sub_1.manipulators.clear() +item_sub_2 = item_sub_1.manipulators.add() +item_sub_2.name = '' +item_sub_2.p0 = (-1.0, 0.0, 2.700000047683716) +item_sub_2.prop1_name = 'da' +item_sub_2.p2 = (1.0, -2.4492937051703357e-16, 0.0) +item_sub_2.normal = (0.0, 0.0, 1.0) +item_sub_2.pts_mode = 'RADIUS' +item_sub_2.p1 = (-1.0, 1.2246468525851679e-16, 0.0) +item_sub_2.prop2_name = 'radius' +item_sub_2.type_key = 'ARC_ANGLE_RADIUS' +item_sub_1.right_shape = 'RECTANGLE' +item_sub_1.radius = 0.699999988079071 +item_sub_1.type = 'D_STAIR' +item_sub_1.length = 2.0 +item_sub_1.left_shape = 'RECTANGLE' +item_sub_1.da = 1.5707963705062866 +d.subs_bottom = 'STEP' +d.user_defined_post = '' +d.panel_offset_x = 0.0 +d.idmat_side = '1' +d.right_string = False +d.idmat_raise = '1' +d.left_rail = False +d.parts_expand = True +d.panel_z = 0.6000000238418579 +d.bottom_z = 0.029999999329447746 +d.z_mode = 'STANDARD' +d.panel_x = 0.009999999776482582 +d.post_x = 0.03999999910593033 +d.presets = 'STAIR_O' +d.steps_expand = True +d.subs_x = 0.019999999552965164 +d.subs_spacing = 0.10000000149011612 +d.left_handrail = True +d.handrail_offset = 0.0 +d.right_rail = False +d.idmat_panel = '5' +d.post_offset_x = 0.019999999552965164 +d.idmat_step_front = '3' +d.rail_n = 1 +d.string_offset = 0.0 +d.subs_y = 0.019999999552965164 +d.handrail_alt = 1.0 +d.post_corners = False +d.rail_expand = False +d.rail_offset = (0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0) +d.rail_x = (0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806) +d.left_shape = 'CIRCLE' +d.nose_y = 0.019999999552965164 +d.nose_type = 'STRAIGHT' +d.handrail_extend = 0.10000000149011612 +d.idmat_string = '3' +d.post_y = 0.03999999910593033 +d.subs_alt = 0.0 +d.right_handrail = True +d.idmats_expand = False +d.right_shape = 'CIRCLE' +d.idmat_subs = '4' +d.handrail_radius = 0.019999999552965164 +d.right_panel = True +d.post_spacing = 1.0 +d.string_x = 0.019999999552965164 +d.height = 2.700000047683716 diff --git a/archipack/presets/archipack_stair/u_wood_over_concrete.png b/archipack/presets/archipack_stair/u_wood_over_concrete.png new file mode 100644 index 00000000..aab27159 Binary files /dev/null and b/archipack/presets/archipack_stair/u_wood_over_concrete.png differ diff --git a/archipack/presets/archipack_stair/u_wood_over_concrete.py b/archipack/presets/archipack_stair/u_wood_over_concrete.py new file mode 100644 index 00000000..b523dcde --- /dev/null +++ b/archipack/presets/archipack_stair/u_wood_over_concrete.py @@ -0,0 +1,155 @@ +import bpy +d = bpy.context.active_object.data.archipack_stair[0] + +d.steps_type = 'CLOSED' +d.handrail_slice_right = True +d.total_angle = 6.2831854820251465 +d.user_defined_subs_enable = True +d.string_z = 0.30000001192092896 +d.nose_z = 0.029999999329447746 +d.user_defined_subs = '' +d.idmat_step_side = '3' +d.handrail_x = 0.03999999910593033 +d.right_post = True +d.left_post = True +d.width = 1.5 +d.subs_offset_x = 0.0 +d.rail_mat.clear() +item_sub_1 = d.rail_mat.add() +item_sub_1.name = '' +item_sub_1.index = '4' +d.step_depth = 0.30000001192092896 +d.rail_z = (0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806) +d.right_subs = False +d.left_panel = True +d.idmat_handrail = '3' +d.da = 3.1415927410125732 +d.post_alt = 0.0 +d.left_subs = False +d.n_parts = 3 +d.user_defined_post_enable = True +d.handrail_slice_left = True +d.handrail_profil = 'SQUARE' +d.handrail_expand = False +d.panel_alt = 0.25 +d.post_expand = False +d.subs_z = 1.0 +d.rail_alt = (1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0) +d.panel_dist = 0.05000000074505806 +d.panel_expand = False +d.x_offset = 0.0 +d.subs_expand = False +d.idmat_post = '4' +d.left_string = False +d.string_alt = -0.03999999910593033 +d.handrail_y = 0.03999999910593033 +d.radius = 1.0 +d.string_expand = False +d.post_z = 1.0 +d.idmat_top = '3' +d.idmat_bottom = '1' +d.parts.clear() +item_sub_1 = d.parts.add() +item_sub_1.name = '' +item_sub_1.manipulators.clear() +item_sub_2 = item_sub_1.manipulators.add() +item_sub_2.name = '' +item_sub_2.p0 = (0.0, 0.0, 0.7875000238418579) +item_sub_2.prop1_name = 'length' +item_sub_2.p2 = (1.0, 0.0, 0.0) +item_sub_2.normal = (0.0, 0.0, 1.0) +item_sub_2.pts_mode = 'SIZE' +item_sub_2.p1 = (0.0, 2.0, 0.7875000238418579) +item_sub_2.prop2_name = 'radius' +item_sub_2.type_key = 'SIZE' +item_sub_1.right_shape = 'RECTANGLE' +item_sub_1.radius = 0.699999988079071 +item_sub_1.type = 'S_STAIR' +item_sub_1.length = 2.0 +item_sub_1.left_shape = 'RECTANGLE' +item_sub_1.da = 1.5707963705062866 +item_sub_1 = d.parts.add() +item_sub_1.name = '' +item_sub_1.manipulators.clear() +item_sub_2 = item_sub_1.manipulators.add() +item_sub_2.name = '' +item_sub_2.p0 = (-1.0, 2.0, 1.912500023841858) +item_sub_2.prop1_name = 'da' +item_sub_2.p2 = (-1.0, -1.1920928955078125e-07, 0.0) +item_sub_2.normal = (0.0, 0.0, 1.0) +item_sub_2.pts_mode = 'RADIUS' +item_sub_2.p1 = (1.0, 0.0, 0.0) +item_sub_2.prop2_name = 'radius' +item_sub_2.type_key = 'ARC_ANGLE_RADIUS' +item_sub_1.right_shape = 'RECTANGLE' +item_sub_1.radius = 0.699999988079071 +item_sub_1.type = 'D_STAIR' +item_sub_1.length = 2.0 +item_sub_1.left_shape = 'RECTANGLE' +item_sub_1.da = 1.5707963705062866 +item_sub_1 = d.parts.add() +item_sub_1.name = '' +item_sub_1.manipulators.clear() +item_sub_2 = item_sub_1.manipulators.add() +item_sub_2.name = '' +item_sub_2.p0 = (-2.0, 1.9999998807907104, 2.700000047683716) +item_sub_2.prop1_name = 'length' +item_sub_2.p2 = (1.0, 0.0, 0.0) +item_sub_2.normal = (0.0, 0.0, 1.0) +item_sub_2.pts_mode = 'SIZE' +item_sub_2.p1 = (-1.9999998807907104, -1.1920928955078125e-07, 2.700000047683716) +item_sub_2.prop2_name = '' +item_sub_2.type_key = 'SIZE' +item_sub_1.right_shape = 'RECTANGLE' +item_sub_1.radius = 0.699999988079071 +item_sub_1.type = 'S_STAIR' +item_sub_1.length = 2.0 +item_sub_1.left_shape = 'RECTANGLE' +item_sub_1.da = 1.5707963705062866 +d.subs_bottom = 'STEP' +d.user_defined_post = '' +d.panel_offset_x = 0.0 +d.idmat_side = '1' +d.right_string = False +d.idmat_raise = '1' +d.left_rail = False +d.parts_expand = False +d.panel_z = 0.6000000238418579 +d.bottom_z = 0.029999999329447746 +d.z_mode = 'STANDARD' +d.panel_x = 0.009999999776482582 +d.post_x = 0.03999999910593033 +d.presets = 'STAIR_U' +d.steps_expand = True +d.subs_x = 0.019999999552965164 +d.subs_spacing = 0.10000000149011612 +d.left_handrail = True +d.handrail_offset = 0.0 +d.right_rail = False +d.idmat_panel = '5' +d.post_offset_x = 0.019999999552965164 +d.idmat_step_front = '3' +d.rail_n = 1 +d.string_offset = 0.0 +d.subs_y = 0.019999999552965164 +d.handrail_alt = 1.0 +d.post_corners = False +d.rail_expand = False +d.rail_offset = (0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0) +d.rail_x = (0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806) +d.left_shape = 'RECTANGLE' +d.nose_y = 0.019999999552965164 +d.nose_type = 'STRAIGHT' +d.handrail_extend = 0.10000000149011612 +d.idmat_string = '3' +d.post_y = 0.03999999910593033 +d.subs_alt = 0.0 +d.right_handrail = True +d.idmats_expand = False +d.right_shape = 'RECTANGLE' +d.idmat_subs = '4' +d.handrail_radius = 0.019999999552965164 +d.right_panel = True +d.post_spacing = 1.0 +d.string_x = 0.019999999552965164 +d.height = 2.700000047683716 diff --git a/archipack/presets/archipack_window/120x110_flat_2.png b/archipack/presets/archipack_window/120x110_flat_2.png new file mode 100644 index 00000000..25f21c0a Binary files /dev/null and b/archipack/presets/archipack_window/120x110_flat_2.png differ diff --git a/archipack/presets/archipack_window/120x110_flat_2.py b/archipack/presets/archipack_window/120x110_flat_2.py new file mode 100644 index 00000000..7c7dcf9b --- /dev/null +++ b/archipack/presets/archipack_window/120x110_flat_2.py @@ -0,0 +1,50 @@ +import bpy +d = bpy.context.active_object.data.archipack_window[0] + +d.frame_y = 0.05999999865889549 +d.flip = False +d.blind_z = 0.029999999329447746 +d.blind_open = 80.0 +d.hole_margin = 0.10000000149011612 +d.out_frame_y = 0.019999999552965164 +d.blind_y = 0.0020000000949949026 +d.in_tablet_x = 0.03999999910593033 +d.in_tablet_enable = True +d.n_rows = 1 +d.radius = 2.5 +d.rows.clear() +item_sub_1 = d.rows.add() +item_sub_1.name = '' +item_sub_1.width = (50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0) +item_sub_1.fixed = (False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False) +item_sub_1.auto_update = True +item_sub_1.n_cols = 2 +item_sub_1.cols = 2 +item_sub_1.height = 1.0 +d.out_tablet_x = 0.03999999910593033 +d.out_frame = False +d.y = 0.20000000298023224 +d.in_tablet_z = 0.029999999329447746 +d.handle_altitude = 1.399999976158142 +d.out_frame_y2 = 0.019999999552965164 +d.out_tablet_y = 0.03999999910593033 +d.in_tablet_y = 0.03999999910593033 +d.out_frame_x = 0.10000000149011612 +d.offset = 0.10000000149011612 +d.window_shape = 'RECTANGLE' +d.frame_x = 0.05999999865889549 +d.x = 1.2000000476837158 +d.z = 1.100000023841858 +d.hole_inside_mat = 1 +d.curve_steps = 16 +d.handle_enable = True +d.hole_outside_mat = 0 +d.out_tablet_z = 0.029999999329447746 +d.window_type = 'FLAT' +d.angle_y = 0.0 +d.elipsis_b = 0.5 +d.out_tablet_enable = True +d.out_frame_offset = 0.0 +d.warning = False +d.altitude = 1.0 +d.blind_enable = False diff --git a/archipack/presets/archipack_window/120x110_flat_2_elliptic.png b/archipack/presets/archipack_window/120x110_flat_2_elliptic.png new file mode 100644 index 00000000..6809b6fb Binary files /dev/null and b/archipack/presets/archipack_window/120x110_flat_2_elliptic.png differ diff --git a/archipack/presets/archipack_window/120x110_flat_2_elliptic.py b/archipack/presets/archipack_window/120x110_flat_2_elliptic.py new file mode 100644 index 00000000..312f7299 --- /dev/null +++ b/archipack/presets/archipack_window/120x110_flat_2_elliptic.py @@ -0,0 +1,58 @@ +import bpy +d = bpy.context.active_object.data.archipack_window[0] + +d.frame_y = 0.05999999865889549 +d.flip = False +d.blind_z = 0.029999999329447746 +d.blind_open = 80.0 +d.hole_margin = 0.10000000149011612 +d.out_frame_y = 0.019999999552965164 +d.blind_y = 0.0020000000949949026 +d.in_tablet_x = 0.03999999910593033 +d.in_tablet_enable = True +d.n_rows = 2 +d.radius = 0.9599999785423279 +d.rows.clear() +item_sub_1 = d.rows.add() +item_sub_1.name = '' +item_sub_1.width = (50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0) +item_sub_1.fixed = (False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False) +item_sub_1.auto_update = True +item_sub_1.n_cols = 2 +item_sub_1.cols = 2 +item_sub_1.height = 0.800000011920929 +item_sub_1 = d.rows.add() +item_sub_1.name = '' +item_sub_1.width = (50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0) +item_sub_1.fixed = (False, True, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False) +item_sub_1.auto_update = True +item_sub_1.n_cols = 1 +item_sub_1.cols = 1 +item_sub_1.height = 1.0 +d.out_tablet_x = 0.03999999910593033 +d.out_frame = False +d.y = 0.20000000298023224 +d.in_tablet_z = 0.029999999329447746 +d.handle_altitude = 1.399999976158142 +d.out_frame_y2 = 0.019999999552965164 +d.out_tablet_y = 0.03999999910593033 +d.in_tablet_y = 0.03999999910593033 +d.out_frame_x = 0.10000000149011612 +d.offset = 0.10000000149011612 +d.window_shape = 'ELLIPSIS' +d.frame_x = 0.05999999865889549 +d.x = 1.2000000476837158 +d.z = 1.100000023841858 +d.hole_inside_mat = 1 +d.curve_steps = 32 +d.handle_enable = True +d.hole_outside_mat = 0 +d.out_tablet_z = 0.029999999329447746 +d.window_type = 'FLAT' +d.angle_y = 0.0 +d.elipsis_b = 0.5 +d.out_tablet_enable = True +d.out_frame_offset = 0.0 +d.warning = False +d.altitude = 1.0 +d.blind_enable = False diff --git a/archipack/presets/archipack_window/120x110_flat_2_oblique.png b/archipack/presets/archipack_window/120x110_flat_2_oblique.png new file mode 100644 index 00000000..e775b887 Binary files /dev/null and b/archipack/presets/archipack_window/120x110_flat_2_oblique.png differ diff --git a/archipack/presets/archipack_window/120x110_flat_2_oblique.py b/archipack/presets/archipack_window/120x110_flat_2_oblique.py new file mode 100644 index 00000000..010b4073 --- /dev/null +++ b/archipack/presets/archipack_window/120x110_flat_2_oblique.py @@ -0,0 +1,50 @@ +import bpy +d = bpy.context.active_object.data.archipack_window[0] + +d.frame_y = 0.05999999865889549 +d.flip = False +d.blind_z = 0.029999999329447746 +d.blind_open = 80.0 +d.hole_margin = 0.10000000149011612 +d.out_frame_y = 0.019999999552965164 +d.blind_y = 0.0020000000949949026 +d.in_tablet_x = 0.03999999910593033 +d.in_tablet_enable = True +d.n_rows = 1 +d.radius = 0.9599999785423279 +d.rows.clear() +item_sub_1 = d.rows.add() +item_sub_1.name = '' +item_sub_1.width = (50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0) +item_sub_1.fixed = (False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False) +item_sub_1.auto_update = True +item_sub_1.n_cols = 2 +item_sub_1.cols = 2 +item_sub_1.height = 0.800000011920929 +d.out_tablet_x = 0.03999999910593033 +d.out_frame = False +d.y = 0.20000000298023224 +d.in_tablet_z = 0.029999999329447746 +d.handle_altitude = 1.399999976158142 +d.out_frame_y2 = 0.019999999552965164 +d.out_tablet_y = 0.03999999910593033 +d.in_tablet_y = 0.03999999910593033 +d.out_frame_x = 0.10000000149011612 +d.offset = 0.10000000149011612 +d.window_shape = 'QUADRI' +d.frame_x = 0.05999999865889549 +d.x = 1.2000000476837158 +d.z = 1.100000023841858 +d.hole_inside_mat = 1 +d.curve_steps = 32 +d.handle_enable = True +d.hole_outside_mat = 0 +d.out_tablet_z = 0.029999999329447746 +d.window_type = 'FLAT' +d.angle_y = 0.39269909262657166 +d.elipsis_b = 0.5 +d.out_tablet_enable = True +d.out_frame_offset = 0.0 +d.warning = False +d.altitude = 1.0 +d.blind_enable = False diff --git a/archipack/presets/archipack_window/120x110_flat_2_round.png b/archipack/presets/archipack_window/120x110_flat_2_round.png new file mode 100644 index 00000000..5ae472dc Binary files /dev/null and b/archipack/presets/archipack_window/120x110_flat_2_round.png differ diff --git a/archipack/presets/archipack_window/120x110_flat_2_round.py b/archipack/presets/archipack_window/120x110_flat_2_round.py new file mode 100644 index 00000000..3d0fd325 --- /dev/null +++ b/archipack/presets/archipack_window/120x110_flat_2_round.py @@ -0,0 +1,58 @@ +import bpy +d = bpy.context.active_object.data.archipack_window[0] + +d.frame_y = 0.05999999865889549 +d.flip = False +d.blind_z = 0.029999999329447746 +d.blind_open = 80.0 +d.hole_margin = 0.10000000149011612 +d.out_frame_y = 0.019999999552965164 +d.blind_y = 0.0020000000949949026 +d.in_tablet_x = 0.03999999910593033 +d.in_tablet_enable = True +d.n_rows = 2 +d.radius = 0.9599999785423279 +d.rows.clear() +item_sub_1 = d.rows.add() +item_sub_1.name = '' +item_sub_1.width = (50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0) +item_sub_1.fixed = (False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False) +item_sub_1.auto_update = True +item_sub_1.n_cols = 2 +item_sub_1.cols = 2 +item_sub_1.height = 0.800000011920929 +item_sub_1 = d.rows.add() +item_sub_1.name = '' +item_sub_1.width = (50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0) +item_sub_1.fixed = (False, True, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False) +item_sub_1.auto_update = True +item_sub_1.n_cols = 1 +item_sub_1.cols = 1 +item_sub_1.height = 1.0 +d.out_tablet_x = 0.03999999910593033 +d.out_frame = False +d.y = 0.20000000298023224 +d.in_tablet_z = 0.029999999329447746 +d.handle_altitude = 1.399999976158142 +d.out_frame_y2 = 0.019999999552965164 +d.out_tablet_y = 0.03999999910593033 +d.in_tablet_y = 0.03999999910593033 +d.out_frame_x = 0.10000000149011612 +d.offset = 0.10000000149011612 +d.window_shape = 'ROUND' +d.frame_x = 0.05999999865889549 +d.x = 1.2000000476837158 +d.z = 1.100000023841858 +d.hole_inside_mat = 1 +d.curve_steps = 16 +d.handle_enable = True +d.hole_outside_mat = 0 +d.out_tablet_z = 0.029999999329447746 +d.window_type = 'FLAT' +d.angle_y = 0.0 +d.elipsis_b = 0.5 +d.out_tablet_enable = True +d.out_frame_offset = 0.0 +d.warning = False +d.altitude = 1.0 +d.blind_enable = False diff --git a/archipack/presets/archipack_window/180x110_flat_3.png b/archipack/presets/archipack_window/180x110_flat_3.png new file mode 100644 index 00000000..22845518 Binary files /dev/null and b/archipack/presets/archipack_window/180x110_flat_3.png differ diff --git a/archipack/presets/archipack_window/180x110_flat_3.py b/archipack/presets/archipack_window/180x110_flat_3.py new file mode 100644 index 00000000..3ae2748a --- /dev/null +++ b/archipack/presets/archipack_window/180x110_flat_3.py @@ -0,0 +1,50 @@ +import bpy +d = bpy.context.active_object.data.archipack_window[0] + +d.frame_y = 0.05999999865889549 +d.flip = False +d.blind_z = 0.029999999329447746 +d.blind_open = 80.0 +d.hole_margin = 0.10000000149011612 +d.out_frame_y = 0.019999999552965164 +d.blind_y = 0.0020000000949949026 +d.in_tablet_x = 0.03999999910593033 +d.in_tablet_enable = True +d.n_rows = 1 +d.radius = 2.5 +d.rows.clear() +item_sub_1 = d.rows.add() +item_sub_1.name = '' +item_sub_1.width = (33.33333206176758, 33.33333206176758, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0) +item_sub_1.fixed = (False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False) +item_sub_1.auto_update = True +item_sub_1.n_cols = 3 +item_sub_1.cols = 3 +item_sub_1.height = 1.0 +d.out_tablet_x = 0.03999999910593033 +d.out_frame = False +d.y = 0.20000000298023224 +d.in_tablet_z = 0.029999999329447746 +d.handle_altitude = 1.399999976158142 +d.out_frame_y2 = 0.019999999552965164 +d.out_tablet_y = 0.03999999910593033 +d.in_tablet_y = 0.03999999910593033 +d.out_frame_x = 0.10000000149011612 +d.offset = 0.10000000149011612 +d.window_shape = 'RECTANGLE' +d.frame_x = 0.05999999865889549 +d.x = 1.7999999523162842 +d.z = 1.100000023841858 +d.hole_inside_mat = 1 +d.curve_steps = 16 +d.handle_enable = True +d.hole_outside_mat = 0 +d.out_tablet_z = 0.029999999329447746 +d.window_type = 'FLAT' +d.angle_y = 0.0 +d.elipsis_b = 0.5 +d.out_tablet_enable = True +d.out_frame_offset = 0.0 +d.warning = False +d.altitude = 1.0 +d.blind_enable = False diff --git a/archipack/presets/archipack_window/180x210_flat_3.png b/archipack/presets/archipack_window/180x210_flat_3.png new file mode 100644 index 00000000..354e9be9 Binary files /dev/null and b/archipack/presets/archipack_window/180x210_flat_3.png differ diff --git a/archipack/presets/archipack_window/180x210_flat_3.py b/archipack/presets/archipack_window/180x210_flat_3.py new file mode 100644 index 00000000..df26b7a5 --- /dev/null +++ b/archipack/presets/archipack_window/180x210_flat_3.py @@ -0,0 +1,50 @@ +import bpy +d = bpy.context.active_object.data.archipack_window[0] + +d.frame_y = 0.05999999865889549 +d.flip = False +d.blind_z = 0.029999999329447746 +d.blind_open = 80.0 +d.hole_margin = 0.10000000149011612 +d.out_frame_y = 0.019999999552965164 +d.blind_y = 0.0020000000949949026 +d.in_tablet_x = 0.03999999910593033 +d.in_tablet_enable = True +d.n_rows = 1 +d.radius = 2.5 +d.rows.clear() +item_sub_1 = d.rows.add() +item_sub_1.name = '' +item_sub_1.width = (33.33333206176758, 33.33333206176758, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0) +item_sub_1.fixed = (False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False) +item_sub_1.auto_update = True +item_sub_1.n_cols = 3 +item_sub_1.cols = 3 +item_sub_1.height = 1.0 +d.out_tablet_x = 0.03999999910593033 +d.out_frame = False +d.y = 0.20000000298023224 +d.in_tablet_z = 0.029999999329447746 +d.handle_altitude = 1.399999976158142 +d.out_frame_y2 = 0.019999999552965164 +d.out_tablet_y = 0.03999999910593033 +d.in_tablet_y = 0.03999999910593033 +d.out_frame_x = 0.10000000149011612 +d.offset = 0.10000000149011612 +d.window_shape = 'RECTANGLE' +d.frame_x = 0.05999999865889549 +d.x = 1.7999999523162842 +d.z = 2.0999999046325684 +d.hole_inside_mat = 1 +d.curve_steps = 16 +d.handle_enable = True +d.hole_outside_mat = 0 +d.out_tablet_z = 0.029999999329447746 +d.window_type = 'FLAT' +d.angle_y = 0.0 +d.elipsis_b = 0.5 +d.out_tablet_enable = True +d.out_frame_offset = 0.0 +d.warning = False +d.altitude = 0.0 +d.blind_enable = False diff --git a/archipack/presets/archipack_window/180x210_rail_2.png b/archipack/presets/archipack_window/180x210_rail_2.png new file mode 100644 index 00000000..b7808c27 Binary files /dev/null and b/archipack/presets/archipack_window/180x210_rail_2.png differ diff --git a/archipack/presets/archipack_window/180x210_rail_2.py b/archipack/presets/archipack_window/180x210_rail_2.py new file mode 100644 index 00000000..d9f2cb89 --- /dev/null +++ b/archipack/presets/archipack_window/180x210_rail_2.py @@ -0,0 +1,50 @@ +import bpy +d = bpy.context.active_object.data.archipack_window[0] + +d.frame_y = 0.05999999865889549 +d.flip = False +d.blind_z = 0.029999999329447746 +d.blind_open = 80.0 +d.hole_margin = 0.10000000149011612 +d.out_frame_y = 0.019999999552965164 +d.blind_y = 0.0020000000949949026 +d.in_tablet_x = 0.03999999910593033 +d.in_tablet_enable = True +d.n_rows = 1 +d.radius = 2.5 +d.rows.clear() +item_sub_1 = d.rows.add() +item_sub_1.name = '' +item_sub_1.width = (50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0) +item_sub_1.fixed = (False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False) +item_sub_1.auto_update = True +item_sub_1.n_cols = 2 +item_sub_1.cols = 2 +item_sub_1.height = 1.0 +d.out_tablet_x = 0.03999999910593033 +d.out_frame = False +d.y = 0.20000000298023224 +d.in_tablet_z = 0.029999999329447746 +d.handle_altitude = 1.399999976158142 +d.out_frame_y2 = 0.019999999552965164 +d.out_tablet_y = 0.03999999910593033 +d.in_tablet_y = 0.03999999910593033 +d.out_frame_x = 0.10000000149011612 +d.offset = 0.10000000149011612 +d.window_shape = 'RECTANGLE' +d.frame_x = 0.05999999865889549 +d.x = 1.7999999523162842 +d.z = 2.0999999046325684 +d.hole_inside_mat = 1 +d.curve_steps = 16 +d.handle_enable = True +d.hole_outside_mat = 0 +d.out_tablet_z = 0.029999999329447746 +d.window_type = 'RAIL' +d.angle_y = 0.0 +d.elipsis_b = 0.5 +d.out_tablet_enable = True +d.out_frame_offset = 0.0 +d.warning = False +d.altitude = 0.0 +d.blind_enable = False diff --git a/archipack/presets/archipack_window/240x210_rail_3.png b/archipack/presets/archipack_window/240x210_rail_3.png new file mode 100644 index 00000000..1201622a Binary files /dev/null and b/archipack/presets/archipack_window/240x210_rail_3.png differ diff --git a/archipack/presets/archipack_window/240x210_rail_3.py b/archipack/presets/archipack_window/240x210_rail_3.py new file mode 100644 index 00000000..4cec930b --- /dev/null +++ b/archipack/presets/archipack_window/240x210_rail_3.py @@ -0,0 +1,50 @@ +import bpy +d = bpy.context.active_object.data.archipack_window[0] + +d.frame_y = 0.05999999865889549 +d.flip = False +d.blind_z = 0.029999999329447746 +d.blind_open = 80.0 +d.hole_margin = 0.10000000149011612 +d.out_frame_y = 0.019999999552965164 +d.blind_y = 0.0020000000949949026 +d.in_tablet_x = 0.03999999910593033 +d.in_tablet_enable = True +d.n_rows = 1 +d.radius = 2.5 +d.rows.clear() +item_sub_1 = d.rows.add() +item_sub_1.name = '' +item_sub_1.width = (33.33333206176758, 33.33333206176758, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0) +item_sub_1.fixed = (False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False) +item_sub_1.auto_update = True +item_sub_1.n_cols = 3 +item_sub_1.cols = 3 +item_sub_1.height = 1.0 +d.out_tablet_x = 0.03999999910593033 +d.out_frame = False +d.y = 0.20000000298023224 +d.in_tablet_z = 0.029999999329447746 +d.handle_altitude = 1.399999976158142 +d.out_frame_y2 = 0.019999999552965164 +d.out_tablet_y = 0.03999999910593033 +d.in_tablet_y = 0.03999999910593033 +d.out_frame_x = 0.10000000149011612 +d.offset = 0.10000000149011612 +d.window_shape = 'RECTANGLE' +d.frame_x = 0.05999999865889549 +d.x = 2.4000000953674316 +d.z = 2.0999999046325684 +d.hole_inside_mat = 1 +d.curve_steps = 16 +d.handle_enable = True +d.hole_outside_mat = 0 +d.out_tablet_z = 0.029999999329447746 +d.window_type = 'RAIL' +d.angle_y = 0.0 +d.elipsis_b = 0.5 +d.out_tablet_enable = True +d.out_frame_offset = 0.0 +d.warning = False +d.altitude = 0.0 +d.blind_enable = False diff --git a/archipack/presets/archipack_window/80x80_flat_1.png b/archipack/presets/archipack_window/80x80_flat_1.png new file mode 100644 index 00000000..8568fac8 Binary files /dev/null and b/archipack/presets/archipack_window/80x80_flat_1.png differ diff --git a/archipack/presets/archipack_window/80x80_flat_1.py b/archipack/presets/archipack_window/80x80_flat_1.py new file mode 100644 index 00000000..caf2980b --- /dev/null +++ b/archipack/presets/archipack_window/80x80_flat_1.py @@ -0,0 +1,50 @@ +import bpy +d = bpy.context.active_object.data.archipack_window[0] + +d.frame_y = 0.05999999865889549 +d.flip = False +d.blind_z = 0.029999999329447746 +d.blind_open = 80.0 +d.hole_margin = 0.10000000149011612 +d.out_frame_y = 0.019999999552965164 +d.blind_y = 0.0020000000949949026 +d.in_tablet_x = 0.03999999910593033 +d.in_tablet_enable = True +d.n_rows = 1 +d.radius = 2.5 +d.rows.clear() +item_sub_1 = d.rows.add() +item_sub_1.name = '' +item_sub_1.width = (50.0, 33.33333206176758, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0) +item_sub_1.fixed = (False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False) +item_sub_1.auto_update = True +item_sub_1.n_cols = 1 +item_sub_1.cols = 1 +item_sub_1.height = 1.0 +d.out_tablet_x = 0.03999999910593033 +d.out_frame = False +d.y = 0.20000000298023224 +d.in_tablet_z = 0.029999999329447746 +d.handle_altitude = 1.399999976158142 +d.out_frame_y2 = 0.019999999552965164 +d.out_tablet_y = 0.03999999910593033 +d.in_tablet_y = 0.03999999910593033 +d.out_frame_x = 0.10000000149011612 +d.offset = 0.10000000149011612 +d.window_shape = 'RECTANGLE' +d.frame_x = 0.05999999865889549 +d.x = 0.800000011920929 +d.z = 0.800000011920929 +d.hole_inside_mat = 1 +d.curve_steps = 16 +d.handle_enable = True +d.hole_outside_mat = 0 +d.out_tablet_z = 0.029999999329447746 +d.window_type = 'FLAT' +d.angle_y = 0.0 +d.elipsis_b = 0.5 +d.out_tablet_enable = True +d.out_frame_offset = 0.0 +d.warning = False +d.altitude = 1.2000000476837158 +d.blind_enable = False diff --git a/archipack/presets/archipack_window/80x80_flat_1_circle.png b/archipack/presets/archipack_window/80x80_flat_1_circle.png new file mode 100644 index 00000000..bd856b37 Binary files /dev/null and b/archipack/presets/archipack_window/80x80_flat_1_circle.png differ diff --git a/archipack/presets/archipack_window/80x80_flat_1_circle.py b/archipack/presets/archipack_window/80x80_flat_1_circle.py new file mode 100644 index 00000000..18f5c8bc --- /dev/null +++ b/archipack/presets/archipack_window/80x80_flat_1_circle.py @@ -0,0 +1,58 @@ +import bpy +d = bpy.context.active_object.data.archipack_window[0] + +d.frame_y = 0.05999999865889549 +d.flip = False +d.blind_z = 0.029999999329447746 +d.blind_open = 80.0 +d.hole_margin = 0.10000000149011612 +d.out_frame_y = 0.019999999552965164 +d.blind_y = 0.0020000000949949026 +d.in_tablet_x = 0.03999999910593033 +d.in_tablet_enable = True +d.n_rows = 2 +d.radius = 0.9599999785423279 +d.rows.clear() +item_sub_1 = d.rows.add() +item_sub_1.name = '' +item_sub_1.width = (50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0) +item_sub_1.fixed = (False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False) +item_sub_1.auto_update = True +item_sub_1.n_cols = 2 +item_sub_1.cols = 2 +item_sub_1.height = 0.800000011920929 +item_sub_1 = d.rows.add() +item_sub_1.name = '' +item_sub_1.width = (50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0) +item_sub_1.fixed = (False, True, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False) +item_sub_1.auto_update = True +item_sub_1.n_cols = 1 +item_sub_1.cols = 1 +item_sub_1.height = 1.0 +d.out_tablet_x = 0.03999999910593033 +d.out_frame = False +d.y = 0.800000011920929 +d.in_tablet_z = 0.029999999329447746 +d.handle_altitude = 1.399999976158142 +d.out_frame_y2 = 0.019999999552965164 +d.out_tablet_y = 0.03999999910593033 +d.in_tablet_y = 0.03999999910593033 +d.out_frame_x = 0.10000000149011612 +d.offset = 0.10000000149011612 +d.window_shape = 'CIRCLE' +d.frame_x = 0.05999999865889549 +d.x = 0.800000011920929 +d.z = 1.100000023841858 +d.hole_inside_mat = 1 +d.curve_steps = 32 +d.handle_enable = True +d.hole_outside_mat = 0 +d.out_tablet_z = 0.029999999329447746 +d.window_type = 'FLAT' +d.angle_y = 0.0 +d.elipsis_b = 0.5 +d.out_tablet_enable = True +d.out_frame_offset = 0.0 +d.warning = False +d.altitude = 1.0 +d.blind_enable = False diff --git a/archipack/presets/missing.png b/archipack/presets/missing.png new file mode 100644 index 00000000..7881102a Binary files /dev/null and b/archipack/presets/missing.png differ diff --git a/archipack/pyqtree.py b/archipack/pyqtree.py new file mode 100644 index 00000000..80b75727 --- /dev/null +++ b/archipack/pyqtree.py @@ -0,0 +1,187 @@ +# -*- coding:utf-8 -*- + +# + +""" +# Pyqtree + +Pyqtree is a pure Python spatial index for GIS or rendering usage. +It stores and quickly retrieves items from a 2x2 rectangular grid area, +and grows in depth and detail as more items are added. +The actual quad tree implementation is adapted from +[Matt Rasmussen's compbio library](https://github.com/mdrasmus/compbio/blob/master/rasmus/quadtree.py) +and extended for geospatial use. + + +## Platforms + +Python 2 and 3. + + +## Dependencies + +Pyqtree is written in pure Python and has no dependencies. + + +## Installing It + +Installing Pyqtree can be done by opening your terminal or commandline and typing: + + pip install pyqtree + +Alternatively, you can simply download the "pyqtree.py" file and place +it anywhere Python can import it, such as the Python site-packages folder. + + +## Example Usage + +Start your script by importing the quad tree. + + from pyqtree import Index + +Setup the spatial index, giving it a bounding box area to keep track of. +The bounding box being in a four-tuple: (xmin, ymin, xmax, ymax). + + spindex = Index(bbox=(0, 0, 100, 100)) + +Populate the index with items that you want to be retrieved at a later point, +along with each item's geographic bbox. + + # this example assumes you have a list of items with bbox attribute + for item in items: + spindex.insert(item, item.bbox) + +Then when you have a region of interest and you wish to retrieve items from that region, +just use the index's intersect method. This quickly gives you a list of the stored items +whose bboxes intersects your region of interests. + + overlapbbox = (51, 51, 86, 86) + matches = spindex.intersect(overlapbbox) + +There are other things that can be done as well, but that's it for the main usage! + + +## More Information: + +- [Home Page](http://github.com/karimbahgat/Pyqtree) +- [API Documentation](http://pythonhosted.org/Pyqtree) + + +## License: + +This code is free to share, use, reuse, and modify according to the MIT license, see LICENSE.txt. + + +## Credits: + +- Karim Bahgat (2015) +- Joschua Gandert (2016) + +""" + + +__version__ = "0.25.0" + +# PYTHON VERSION CHECK +import sys + + +PYTHON3 = int(sys.version[0]) == 3 +if PYTHON3: + xrange = range + + +class _QuadNode(object): + def __init__(self, item, rect): + self.item = item + self.rect = rect + + +class _QuadTree(object): + """ + Internal backend version of the index. + The index being used behind the scenes. Has all the same methods as the user + index, but requires more technical arguments when initiating it than the + user-friendly version. + """ + def __init__(self, x, y, width, height, max_items, max_depth, _depth=0): + self.nodes = [] + self.children = [] + self.center = (x, y) + self.width, self.height = width, height + self.max_items = max_items + self.max_depth = max_depth + self._depth = _depth + + def _insert(self, item, bbox): + if len(self.children) == 0: + node = _QuadNode(item, bbox) + self.nodes.append(node) + if len(self.nodes) > self.max_items and self._depth < self.max_depth: + self._split() + else: + self._insert_into_children(item, bbox) + + def _intersect(self, rect, results=None): + if results is None: + results = set() + # search children + if self.children: + if rect[0] <= self.center[0]: + if rect[1] <= self.center[1]: + self.children[0]._intersect(rect, results) + if rect[3] >= self.center[1]: + self.children[1]._intersect(rect, results) + if rect[2] >= self.center[0]: + if rect[1] <= self.center[1]: + self.children[2]._intersect(rect, results) + if rect[3] >= self.center[1]: + self.children[3]._intersect(rect, results) + # search node at this level + for node in self.nodes: + if (node.rect[2] >= rect[0] and node.rect[0] <= rect[2] and + node.rect[3] >= rect[1] and node.rect[1] <= rect[3]): + results.add(node.item) + return results + + def _insert_into_children(self, item, rect): + # if rect spans center then insert here + if (rect[0] <= self.center[0] and rect[2] >= self.center[0] and + rect[1] <= self.center[1] and rect[3] >= self.center[1]): + node = _QuadNode(item, rect) + self.nodes.append(node) + else: + # try to insert into children + if rect[0] <= self.center[0]: + if rect[1] <= self.center[1]: + self.children[0]._insert(item, rect) + if rect[3] >= self.center[1]: + self.children[1]._insert(item, rect) + if rect[2] > self.center[0]: + if rect[1] <= self.center[1]: + self.children[2]._insert(item, rect) + if rect[3] >= self.center[1]: + self.children[3]._insert(item, rect) + + def _split(self): + quartwidth = self.width / 4.0 + quartheight = self.height / 4.0 + halfwidth = self.width / 2.0 + halfheight = self.height / 2.0 + x1 = self.center[0] - quartwidth + x2 = self.center[0] + quartwidth + y1 = self.center[1] - quartheight + y2 = self.center[1] + quartheight + new_depth = self._depth + 1 + self.children = [_QuadTree(x1, y1, halfwidth, halfheight, + self.max_items, self.max_depth, new_depth), + _QuadTree(x1, y2, halfwidth, halfheight, + self.max_items, self.max_depth, new_depth), + _QuadTree(x2, y1, halfwidth, halfheight, + self.max_items, self.max_depth, new_depth), + _QuadTree(x2, y2, halfwidth, halfheight, + self.max_items, self.max_depth, new_depth)] + nodes = self.nodes + self.nodes = [] + for node in nodes: + self._insert_into_children(node.item, node.rect) -- cgit v1.2.3