From c1ab9b4b9c6c0226f8d7789b92efda9b0f33cfd1 Mon Sep 17 00:00:00 2001
From: Stephen Leger <stephen@3dservices.ch>
Date: Sat, 22 Jul 2017 13:25:28 +0200
Subject: [PATCH] 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 +++++
 .../presets/archipack_door/160x200_dual.png   |  Bin 0 -> 10252 bytes
 .../presets/archipack_door/160x200_dual.py    |   23 +
 .../presets/archipack_door/400x240_garage.png |  Bin 0 -> 10492 bytes
 .../presets/archipack_door/400x240_garage.py  |   23 +
 archipack/presets/archipack_door/80x200.png   |  Bin 0 -> 7840 bytes
 archipack/presets/archipack_door/80x200.py    |   23 +
 .../presets/archipack_fence/glass_panels.png  |  Bin 0 -> 7106 bytes
 .../presets/archipack_fence/glass_panels.py   |   67 +
 .../archipack_fence/inox_glass_concrete.png   |  Bin 0 -> 7835 bytes
 .../archipack_fence/inox_glass_concrete.py    |   64 +
 archipack/presets/archipack_fence/metal.png   |  Bin 0 -> 10234 bytes
 archipack/presets/archipack_fence/metal.py    |   67 +
 .../presets/archipack_fence/metal_glass.png   |  Bin 0 -> 9582 bytes
 .../presets/archipack_fence/metal_glass.py    |   67 +
 archipack/presets/archipack_fence/wood.png    |  Bin 0 -> 13183 bytes
 archipack/presets/archipack_fence/wood.py     |   67 +
 .../archipack_floor/herringbone_50x10.png     |  Bin 0 -> 11148 bytes
 .../archipack_floor/herringbone_50x10.py      |   34 +
 .../archipack_floor/herringbone_p_50x10.png   |  Bin 0 -> 10924 bytes
 .../archipack_floor/herringbone_p_50x10.py    |   34 +
 .../presets/archipack_floor/parquet_15x3.png  |  Bin 0 -> 13445 bytes
 .../presets/archipack_floor/parquet_15x3.py   |   34 +
 .../presets/archipack_floor/planks_200x20.png |  Bin 0 -> 11644 bytes
 .../presets/archipack_floor/planks_200x20.py  |   34 +
 .../presets/archipack_floor/tiles_15x15.png   |  Bin 0 -> 12939 bytes
 .../presets/archipack_floor/tiles_15x15.py    |   34 +
 .../presets/archipack_floor/tiles_60x30.png   |  Bin 0 -> 11379 bytes
 .../presets/archipack_floor/tiles_60x30.py    |   34 +
 .../archipack_floor/tiles_hex_10x10.png       |  Bin 0 -> 13663 bytes
 .../archipack_floor/tiles_hex_10x10.py        |   34 +
 .../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 +
 .../archipack_window/120x110_flat_2.png       |  Bin 0 -> 8410 bytes
 .../archipack_window/120x110_flat_2.py        |   50 +
 .../120x110_flat_2_elliptic.png               |  Bin 0 -> 8593 bytes
 .../120x110_flat_2_elliptic.py                |   58 +
 .../120x110_flat_2_oblique.png                |  Bin 0 -> 7969 bytes
 .../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 +
 .../archipack_window/180x110_flat_3.png       |  Bin 0 -> 9492 bytes
 .../archipack_window/180x110_flat_3.py        |   50 +
 .../archipack_window/180x210_flat_3.png       |  Bin 0 -> 10314 bytes
 .../archipack_window/180x210_flat_3.py        |   50 +
 .../archipack_window/180x210_rail_2.png       |  Bin 0 -> 9362 bytes
 .../archipack_window/180x210_rail_2.py        |   50 +
 .../archipack_window/240x210_rail_3.png       |  Bin 0 -> 10360 bytes
 .../archipack_window/240x210_rail_3.py        |   50 +
 .../presets/archipack_window/80x80_flat_1.png |  Bin 0 -> 7291 bytes
 .../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 000000000..79ac9879f
--- /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 #####
+
+# <pep8 compliant>
+
+# ----------------------------------------------------------
+# 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 000000000..912e3cb85
--- /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 #####
+
+# <pep8 compliant>
+
+# ----------------------------------------------------------
+# 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 000000000..a171532cf
--- /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 #####
+
+# <pep8 compliant>
+
+# ----------------------------------------------------------
+# 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 000000000..f29c44d14
--- /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 #####
+
+# <pep8 compliant>
+
+# ----------------------------------------------------------
+# 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 000000000..961b516eb
--- /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 #####
+
+# <pep8 compliant>
+
+# ----------------------------------------------------------
+# 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 000000000..24917c169
--- /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 #####
+
+# <pep8 compliant>
+
+# ----------------------------------------------------------
+# 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 000000000..fc1f8c03f
--- /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 #####
+
+# <pep8 compliant>
+
+# ----------------------------------------------------------
+# 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 000000000..852fe2b6e
--- /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 #####
+
+# <pep8 compliant>
+
+# ----------------------------------------------------------
+# 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 000000000..65b295bfd
--- /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 #####
+
+# <pep8 compliant>
+
+# ----------------------------------------------------------
+# 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 000000000..c3e0fc24b
--- /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 #####
+
+# <pep8 compliant>
+
+# ----------------------------------------------------------
+# 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 000000000..18ae43e5e
--- /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 #####
+
+# <pep8 compliant>
+
+# ----------------------------------------------------------
+# 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 000000000..886029ba9
--- /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 #####
+
+# <pep8 compliant>
+
+# ----------------------------------------------------------
+# 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 000000000..c5fe94460
--- /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 #####
+
+# <pep8 compliant>
+
+# ----------------------------------------------------------
+# 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 000000000..d81a6839a
--- /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 #####
+
+# <pep8 compliant>
+
+# ----------------------------------------------------------
+# 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 000000000..3d86d4d8b
--- /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 #####
+
+# <pep8 compliant>
+
+# ----------------------------------------------------------
+# 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 000000000..d29c16784
--- /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 #####
+
+# <pep8 compliant>
+
+# ----------------------------------------------------------
+# 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 000000000..936a07d82
--- /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 #####
+
+# <pep8 compliant>
+
+# ----------------------------------------------------------
+# 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 000000000..c7e7f02c1
--- /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 #####
+
+# <pep8 compliant>
+
+# ----------------------------------------------------------
+# 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 000000000..b8056daab
--- /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 #####
+
+# <pep8 compliant>
+
+# ----------------------------------------------------------
+# 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 000000000..5adf92c2e
--- /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 #####
+
+# <pep8 compliant>
+
+# ----------------------------------------------------------
+# 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 000000000..4944f59fe
--- /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 #####
+
+# <pep8 compliant>
+
+# ----------------------------------------------------------
+# 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 000000000..2be559474
--- /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 #####
+
+# <pep8 compliant>
+
+# ----------------------------------------------------------
+# 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 000000000..cf7126107
--- /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 #####
+
+# <pep8 compliant>
+
+# ----------------------------------------------------------
+# 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 000000000..b49f46831
--- /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 #####
+
+# <pep8 compliant>
+
+# ----------------------------------------------------------
+# 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 0000000000000000000000000000000000000000..92503c824dbba4bca5148055310822c93ef66fec
GIT binary patch
literal 1364
zcmeAS@N?(olHy`uVBq!ia0y~yU{C;I4mJh`hT^KKFANL}Y)RhkE)4%caKYZ?lNlHo
zI14-?iy0WWg+Z8+Vb&Z81_lQ95>H=O_J?fZeBvgxua{~tFt9xIba4!cIQ({+Z$@yW
z$npB`wO>^A^)rJmhxkY@I{N6k)9dRBVeIU44seM1hCOnM*&)cf@Pg}fzI80$yPB*g
zK5$~&RiNm!bc^1qM#<G7Ep-__(Q}sN{)+ollzvfdvftXMbQy_b%>BprzQ6n1{(Jqo
zozMC2znT5_zHiOa+ENE*DF)VphBxZrbM0!YcI@8$d;5N$)Ab)B6TRLZ*!K2U?(J=^
z)7r!omM>pEF{tk2<dZ2D#m{*7<!n~SxW!Iw{PykJL22{6GvPM>tkZ69+f)3!?@NV^
z(1eEt5z8*W<Pv^u_rgJG;*YP-&dwH^urtQ*-1+nMf)#qxC)?WE7F}NEduh{}ERH>K
z>-SWAOiGnmt+J5e)cJX~r<YxR>2|zu%HekY%NH*;X3ok!WpllB=8PE&cJH43{QUgI
znNmxGI%m$5Ja+uJrU=)IUFS9?AKy{_Ud|zg;nTObw<D&tu6tclQW6j!zyH~m^Upu0
z7)kEhwd=$0zjJJ>#SR<nsQ#{(e{avjzrVlt^!83Xo92DMLuq2i*Kgmdjta@zzrVNl
z_iyKRzS`^8t}V;Exv4e7#7jZo!1K?Ek=NW0ZoaAWvSill>+3fcKlck-DY7vlXJgBf
zCm$al|MK;#p_SFC!|nXpr{e{;J{9afn&x|XON&6u;lx|FZr#{dYke_8<n*Qv7bO=Z
z!9^D}R;_wu!T0#_<CwiwQyUu@g*sW>`{h(cxD=m0+UCR|;;K0P^v5qRgY{x|Y<Tl;
z<=&1b`)YsJvA=r$?AeCg*)?|lD?^k7I0X8S-`tqYuB)rNW8LXSW_AOf=6UnvVy>&s
z^x2etUXJIm!(|hZr{}rEofMY3a2&k0HhTKQtgRt#i!W{xX!`lHQcO&2L$37gZJHun
zdn!IQsr%1cpuzRAVvdj6;S(n~e0_cASQIvW`&QN>aOLXNhaW2nV#RN73lw2h6#E!b
z`c!no8OGS_r+s~WE&816>+LUJzPx4276GnSj`qVl)}4OyMyI&A_{;b2#ZOKM_Q~7(
zWoK)zdmZ+2U;TeOyZQai?EGQ=dP`j{Z|T3^^8Vi5)30~=DF{5cx;nh4ug}QF#>d&2
z`C-9~1Ot!cWaYDG&%U{}wL5sZ-^RknZl|6aty;C{wbk`?v5)`!{QTwX*U--nUOx|B
zUZ0>a{dDijRZ`)hQv)<y?B=IuWpQ0L`B=4Aq}!FNmC4xHn5Ef~snNmonrOq`>hEFB
zfwP&P7X7@;<KySIZ{_OMhf8CvtgSm;#BLwDaz&&q@xkYx9Bj=;k_@G!r2|7n6<tJQ
zu9vd0vri0qWa0O<TGS`d?wo_!?6-4g%=oc!=T1w7mIrrs7K?DPe){>R>u6izf>p0J
zsZ0*iOI~2K{<W18hn#Jd37<=Nu>-@WYipx(?(O+GJ;mr}@Wu!h1p$r+yWSaX;MzL1
z@af*f!)!XIS6zDh{kNo))GTimp?#_a8?!jH_C9=idb)k=&Z5@s`S-2l`j>mftDeqI
zj(hF0RDQ*(uKVxV6aN1Cs?)eyUdUm7kc-fROG~{~eJ^kE>KAuPn5DKdgzK=us+;Rp
zX*oMP|D3Gu&&Ob2!*}kvml5ZMt5;hSB`o@yGiRwx_AH5Y4;1maoRX8XW=sA5x?J7r
z^{&F}nAI5d;`jM%j8K{C_3-;|&8c2nwrvah_nC=1KKO{67SrReudi>dI{#z4XzBAT
hr_ZzhE`P&6x3}Wp>ebRO7#J8BJYD@<);T3K0RV{^mn;AP

literal 0
HcmV?d00001

diff --git a/archipack/icons/detect.png b/archipack/icons/detect.png
new file mode 100644
index 0000000000000000000000000000000000000000..9c10f604ff74be249665798ab03ec4ccfdd01fb1
GIT binary patch
literal 281
zcmeAS@N?(olHy`uVBq!ia0y~yU{C;I4mJh`hT^KKFANL}Y)RhkE(~TMXi@j;5d#AQ
zXMsm#F#`k3dk|(+zkWxafq{X&#M9T6{UMVy535nYy1h3T7#Nm$x;TbJ9DY0PAlDHC
z4wvJq_f7u(fBY->+a(>_tx{W39rK%JIMzD1ELo@!5*pJg%A{m#Utt;Lz@5Rko?Bw(
zS)L7t+4@BU9OULUFl?H*oxffE!KU!g30u}{nO#bo_vrS#T|1j1e@CBmJwI_T6Z4xI
z=KdgshB^5MirL>XFK&AKh12CGpZ^1)jiTGM-&{Ri_T{~W)(yYCjiTFr+h1UcIuOrU
eU>uve-#Wb3{aH)w>SzWA1_n=8KbLh*2~7Z;&uF#)

literal 0
HcmV?d00001

diff --git a/archipack/icons/door.png b/archipack/icons/door.png
new file mode 100644
index 0000000000000000000000000000000000000000..dc975d4d0d904cd54f0e3d76176143dcaf726555
GIT binary patch
literal 414
zcmeAS@N?(olHy`uVBq!ia0y~yU{C;I4mJh`hT^KKFANL}Y)RhkE)4%caKYZ?lNlHo
zI14-?iy0WWg+Z8+Vb&Z81_lQ95>H=O_J^zj+#=fSMb&c{7#L(TLn2C?^K)}k^GX;%
zz_}<ju_QG`p**uBL&4qCHy}kXm7Rfsk;&7=F(l&f+v^+onhXTk9%x@Y!pGNR*RQ`&
zS=mM)FV#gYzUhOM-!Wfyb~d4Uc16d=#UdA$PA_@bP?!5+qVN%R4}BKNw!^-1+O8Gs
zwY};MOb&bx0-bgyvOEZTE^u*Dhl=3Z=!UC1j(Kgg-g)7!R`{uNcWlq^4w}91dloZa
z;)>p6hi442bL3f?=gIQisEm7j%djh>FW7+ND%)?#1fPfo)`$kd15+pcH9r%@`FiQ~
zsYL;I=kp{)eO3F<wyfbsZ~~)P0%JY<i)l$AvXcbn_X;sw`IO`(bNjif=O(YqCHKqt
z8@z7@D}}AvZZ9JmIW1SZ=mE<Ewg)jHt>+n!_~^Yj_nB$ep(%$VWHb+fg2~g>&t;uc
GLK6Vj0hKWT

literal 0
HcmV?d00001

diff --git a/archipack/icons/fence.png b/archipack/icons/fence.png
new file mode 100644
index 0000000000000000000000000000000000000000..f32dcc7eb4a2bb12c4c6029d626cd2326288a849
GIT binary patch
literal 1779
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

literal 0
HcmV?d00001

diff --git a/archipack/icons/floor.png b/archipack/icons/floor.png
new file mode 100644
index 0000000000000000000000000000000000000000..1590c335ae63191921ed621d2f8844ded23c8230
GIT binary patch
literal 1457
zcmeAS@N?(olHy`uVBq!ia0y~yU{C;I4mJh`hT^KKFANL}Y)RhkE)4%caKYZ?lNlHo
zI14-?iy0WWg+Z8+Vb&Z81_lQ95>H=O_J?fZeDWq+jr0{57+CE*T^vIq4!@li?=v}E
z;@JH4_p>yY&bySAyX~4A^ED>NN7H(8nk5<;KJq_w<1sY8#FA^IXgurWr4*Ij0wO)#
z4-ci}ajSW+%{!LTBA~QnlHTFcWe3t$TroJ_#`@;M)~Ee<E6S2P&&68IiT0_Oq;F}i
zzW=-J^PlH_Gn|gP#;s%8F!eus1ltDw1O5z$816AHQ29CkKsO@`LyfLy&_uVzh0mWp
z{ktpaXjdUaKJ$cm;y+9oY$vJkYESh#zAg8*T}Dn$O~Uruk#TY7Cae2jN-??{sx_6d
z*KM=u&XsG|zMXk6jNvWAv6RSv8<s42GUM{gIa)e8PYyP-i|^XKJ2E!bSCK_mjC*_T
z{e25AW-Qwp_12uBh+)CTjfI{G%nfP`<*AYXzMPn-oY#K1(0%>&&D*z6-@AA3o|2bB
ztpXxW0n0DzuJiWtdQr6VUG~;n6;)Ncq#It&7n#KMz?LC6Gc$8u@N&OcL8l`<Zpy#c
z$0~cKrKJUN99*_c?O2lGv}x1y4&9aG_Y7KoIdJt=y^zpQ+Y1>cn>XofXS(oD*U;8h
zmO+Hy_e*!s$~POL*2Z%wMuf+oFR(bnEv|25Z$Dpca%ZDMgWddoU0q!frw?y#ZkCae
zS#tk<dRdtnHy787@4sKZex3aJ<Kvi}MXL9;G(1*>tomKG_n$_?bOr^verEpI-)s&2
zs-9h|ufDQwZEdx_XIsK?pPyGzu3uS%>*1%Tr~5_sR)5cX^7Lt9QWDc76-8B5)=n2D
z<Kp58s+>+76*h7#%?FR#Mn*<T$ji^~bW!?lWMm}8)aX#X>GO5Q6RxhVpTky{x)&B2
zwzaiQm^!uf>+9?GjO+~s7Cf`hvduo*rW?I&!QQ>HlT;QiS-}y#J<qi!?U|tk!<`+4
z%xs5Wg>1k5x5z@qj&p(dfz>D5pU&`F`fKg7Wo=WZP8D&wkYe=l+uPf7=FUwmEHrEt
zh}oYf+pMhI<F<Iq%^VS@4VyMiQmK4*$MV(Js#jmD=2;fEsqC!${LHm}%eHNkRE{4z
zma+YIGXon#ifz;rzQ}2dpFVxs+1AEZwKwk8+p<?*tN3_$3Q9_*FgG`wNbu<B>ONf?
zz4*<mSC6i)4%ci+C@waxuBy6m_pWdMap$>y$K%#dpF4N%p1QwQo|_ggPS(@a)&2D0
zx}R?uKSROJ81ubx@5L7{UMxC!^5lA!=7U#OuG9<(2}vj`6YF$gs@pHm)ykB%Su(4X
z>(;GX1&@w!GC0J?%h%Y+YiVg+xOtP4VaD8<jh9~@nPr;&pv0=VonL<1^XJdMPs}OX
zeYev?<<pP5dAGK3GHl^$eWX3tPki0lwNGDOUY=<nq0zG7{`=>5?#QSwTb5xWCEovi
z{r&W}Z&h|WOtPD=pJpPJ?=|TO>xWbK_Eyh-WjldEW@@TsP-y7UzrVk4-oJl7Uwd=*
z^>wMSwYJR%6?DZ;3x8O*PH$3*j@aVUPo<`)WC}SvUcCHo$f=Vj85@ciCjR;JXSe5$
zlPQZXzkK4M;WBB>x^>T1g|3c>3Olx>t-byI(`V1VF-ow0c-F=%J<W{cOqwx63d4_s
zIcDvzUcC~VHEUMI!$Yk{``X*vfBrti<XKczHS78L`QKNDtP*69W7w5G&#n5~8_%}>
zvZ-EQlYM=CYp$=0-4eAnCo?nAL`v1O&1vDwxz^=%ObJX5x}G_Ac6_`)^XSf<J5Q#r
zRq>phnwqN2%gfv2wz%hL(wi4AKCHg_N}FLH>x7pD7W1+-G+7MtHs54pYIJyWppkh#
zvjp=8!3NRuwnC}t>FULY%DGyV7#^fEsCt{-3QE}+F==0g{DD_bYxb=XkE&<-$uxPM
UrS>Eb1_lNOPgg&ebxsLQ02xA^o&W#<

literal 0
HcmV?d00001

diff --git a/archipack/icons/polygons.png b/archipack/icons/polygons.png
new file mode 100644
index 0000000000000000000000000000000000000000..b434068cf201cc7dedda0f02bc59dd2fecee1e98
GIT binary patch
literal 242
zcmeAS@N?(olHy`uVBq!ia0y~yU{C;I4mJh`hT^KKFANL}Y)RhkE(~TMXi@j;5d#AQ
zXMsm#F#`k3dk|(+zkWxafq{X&#M9T6{UMVy51W{K1J@}A28Mc17srr@!*8b<avd<>
zVV?TP{N{i8*Zy0br~A1!c9cv=x3=W4N>6#l>R9?c;TIFbxy{q#;-;3oZ#<jIz$}qq
z?XYG_7fXWMY9j`Q8FwY`+Rm6c!*a@9$@l5MXRY*Fet?0kBI(2}zr>>ny302$JkxTT
nQDlEQNcaaUL$3MMj=Wi06ldz1&5dDTU|{fc^>bP0l+XkKg+Nm1

literal 0
HcmV?d00001

diff --git a/archipack/icons/selection.png b/archipack/icons/selection.png
new file mode 100644
index 0000000000000000000000000000000000000000..e4a7e82bb49b6bbb0a21214c8cbd5915aecfcf83
GIT binary patch
literal 1021
zcmeAS@N?(olHy`uVBq!ia0y~yU{C;I4mJh`hT^KKFANL}Y)RhkE(~TMXi@j;5d#AQ
zXMsm#F#`k3dk|(+zkWxafq{X&#M9T6{UMVy4~xlyYnp2q7?`^}T^vIq4!@o5?~xoX
za;*Nn>Y|_*Cax_#kuMZFj3Oq@I%B56vP8k_?zD+FdG(g)ZJKv8;z;dG*M%M2qPqe#
zgH{KcZ}VNIwRY+<16jq5zGr)8Wm<Qh^uG5p=^gh$sbe#?1iffH{k!<w<C>c1^WM)}
z{<FsJq5K99l|`{?=lBHp+_Pr=IVXF6X>#ehoB!B9+-E8HZ>J)3okd}KvwQ!8XTB95
z9!N3FP%A0jyQ}_(z^qvb`S0V?7VMUml9s-C>z2~|{f_14clL6(1zIvxFI|*0OYLOJ
zBp)?5waJQ8y##sKX3m<Gl;*a0?b_aD%eVx7)Yu6we6!18)!PHs4LyB*U8(Hs?2pQJ
zvkD|gSFBd!nZm>>AXv0h=3>T_sZ*ye+P`({)VXtImn~l|DJl7pVY%LP?&X&|mt|fx
zF6iTAH(>U#nmg_DwDZrG?%#huNtv(W)YE;d1m-fnW{{Sat}nioV>WI2bk0^M3mLxY
zS|^w9T6a5ZX$#v2i3vAv-rVH4ckkXgbLI%}ura#%Tt36_o;hkO1J?n~4=N&Dd(A3}
ziY8Uq_=Sgu%YKj)2uxk`N~eM|M@5J;Xyujc=2$)P-@kwV_*>T?_h4S;tZ+7kEcupG
zZ<{}x85%Yoewa{PEc`%1;IM(-{WkFkxf{$qKY#w*wQ%7=?&fRPufKf#db8YoW_xDN
z12fO2#WU`B!x4I`OQ|VQ!ew#bwQJW-)jj_HyKwj2H}BqwHOL;lBY$A!x^Q#ej03z6
zk{$Z``ns$NYHD;E9Txnksi@H4<>h6X!ywNn&+xv=MsB6Yrt|CP&6CT_&lh)i_Uu^~
zXLM8)%Y=;^H$K{Z^r)-%3_JE1hvw_A*YY?>YQASNYHn^;Qo3~UVxsCV(Fv`stx6pq
zKNg-Y=id<e#XCSPf_ct?^0Q~p8W|WQsB$o$c38D<U)-aQJ32a!BpI$2VSO;|`s=At
zXIk_d1B_;-WM*<cV1Ho#Kyt&RvuWIi4<BB$Zr!DuH&5QYIdkQa7rBBfSMloFo?wb+
zl3(@2zfocX_lyJ54-0<mbx>%y_4oEF;k@Ucf39MTsQMuC!)fD3o*!OQ4_^8cePFhi
zd67d@Y&O#YmN<su1Gx{XA7pRkG<^{LKv;EuY1iE22e#eKThGbB_>X0gS5iGoU7)(!
z0mcu#R^=t3vuYpgT=&|PG2tw`J)?if+|R1}wlIC&ous|+9ru)Zr^9ocy_QPuDEPDK
gnD*&-sn6o!*WSLrv_x8%fq{X+)78&qol`;+0AcLS_W%F@

literal 0
HcmV?d00001

diff --git a/archipack/icons/slab.png b/archipack/icons/slab.png
new file mode 100644
index 0000000000000000000000000000000000000000..292ea52efa442feef358e2da68a6468829558646
GIT binary patch
literal 1620
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

literal 0
HcmV?d00001

diff --git a/archipack/icons/stair.png b/archipack/icons/stair.png
new file mode 100644
index 0000000000000000000000000000000000000000..5ce4d7054f3bb1e15262bbf51e014ca2fff11208
GIT binary patch
literal 1486
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

literal 0
HcmV?d00001

diff --git a/archipack/icons/truss.png b/archipack/icons/truss.png
new file mode 100644
index 0000000000000000000000000000000000000000..72ca91579c1c627ec088935dfa44e05cb0534fdf
GIT binary patch
literal 1462
zcmeAS@N?(olHy`uVBq!ia0y~yU{C;I4mJh`hT^KKFANL}Y)RhkE(~Ds(|LD20|NtR
zfk$L90|U1(2s1Lwnj^u$z`$PO>Fdh=kd1{&N57$~=_LaLgKTC<M2T~LZf<H`30S{#
zQEFmIYKlU6W=V#EyQgnJie4%^0|Tp+r;B4q#NoZuV>2eFiyW_4ue^Ni$Sia3U9mg1
z{RrH1A*4O7$5ocqaj)Q!hpz7L*_ukEOB6W1E~+q;+<l36Tj#yV56M+ex1I7)Unc(1
zX@&DgW!;GGt=oQ9?A~U0cJ`du8B>;AlM3F>+<2gR-sd^R|DOLjUl|bURqvp_z<<77
zXy{cB6`}o=pVI<DIol8GTv#6(dfiDnZoT*eZwGfbw@0h8_<49#^6u=oxGUhgqo|V7
zT%|?H$;tW+I~yAtD^!Fy^_IOZv3kvr?%9wUG?iJ@>!aD?2V11t7XDw({FyWQ)nn^o
zh56_A^XVnqsfIdzn3pI~zUbnLDjT`l4-XpO-QWK|<lv7ze!jkE=Vq9!+Maj!Qie&?
zD&_UNYvkr1P07pSYY5Qza%%hO>H6pY`!BzI<=VAPxwp3&&0QP4z3Azwsn_HFxUpwt
zWijli`l|K&+uLZ9qr$?%EKH2Yjvp_yn%n(y#p$O(2NadAItp;G%$PUNEwrAm{qUzZ
zH;pw5Sa0TtG0Z<dz3XVw?vT>mtLBAW{V`G5ok3;xS*uk|3v_y>xASk^y0w-qhjCJ>
z8_(g*X=i829q>^RlD@t++FffZ(}AZ&8`tPn{QF}m*WZ43cRBNqM)la(*ypEuEM)i|
zG(SGxZ~pu5KNqd3Sxk2NXD>xxcL@AbS059)z2k(Rpyl@$7nP-?q)wF_JAQnw(x$r4
z&(89Pd}T7wQ!1UBa(#uu-%I!JzYo58Z`-zQ|E5iwW~UG_>z;$cfiq`#^0rqyC$fDJ
z-_#m=$Sq1&&~mOH!>`}JjaRL@d-v{?W#x=MY69<Gz52Dz#3`0d_|=CG8$u75*jZWK
zva+!7&=eF+$l6o(_SSPHCmk{ELrYf8(t9;cApEI-MBBss{QUYk9x4?-J}55BOp-XJ
zZQ!aTIDgf*t`qNdb#?0**Rsw?i8M{PZMHrC{<3}h<W6t8u`!u_Tdw%r=rw7RmSyH`
z(^)NSk$SuA?xwWHj)Yli>V9()&d;+gyuD3YJ9lIGCDxNElL9najz9hx9J=4!xA@AY
zHB&`z#BokI{d8GFXv32t%avVS>1k;*=FPjuI>&oykUZ06Z<&uvgH|4V_wHSO<DON|
zrY(BZHM8LTy}cqzJ1c&Cc$mz+aDVmpJfRGBA%|?Mxywpq!g(E?ot>RR%ezjfD}D0T
z5&OMk<HnCu{a(I!QNc99LnUH&neMby#(?PP-Ftd9=AS?P_xJbLp<lOG*!=tY>+5Ux
zemUDHvkVuO?YDjX{rT(m+vi1wDPLf;NJ~$@?653LuE0X(+_PuTdJkN9@Zf=GsI8{b
z+yg6<#C?2xM0)23U0v^U`N$@ndc_9u12PKw>DO*=UzmNgOZ0Z*vKqVnMHv|{%BriY
zZ+eMwuo$iSwR#mF^L(+suq%uExH>Yv{rdX)_@t9h+G6y+pI_!X+wRrdx4O!UV)UlV
zNlRPv{^6}De1C86kw#|rI-N6%-fZd$aQU-3e0^QfiwlZ+aeGeu{QUgo>(`Ip+}!-R
zXT_>#kB|5Nmyncnboo%V_vo7yxwl>AR>bl!EuQ3g<I0sIt=!_zUVeFSuz8MUF<XO=
zT63ZVOS9vhJaY%R4NV6V?k@G7-cw>_%X)^P^k+@ny<3eZPkO$3{d#(gm-q_{hWL$q
zHgf*50iSnmKP;Vn;Q43YqmMt9g~p2VwY!Ec-B5DoJkw>9Zhrs3(2rGnnQEAKY`VE-
z;}NTXn?buy9Qn=ogR??@26wB|WbYTR>c5&4GbtuS7v9iGvz~m?hbg#0#C&zoPw_LG
V=H~7$u{8pfpPsIMF6*2UngGYIut)#^

literal 0
HcmV?d00001

diff --git a/archipack/icons/union.png b/archipack/icons/union.png
new file mode 100644
index 0000000000000000000000000000000000000000..11b114724061362dae1ecb94b11aa3ac9eede927
GIT binary patch
literal 1102
zcmeAS@N?(olHy`uVBq!ia0y~yU{C;I4mJh`hT^KKFANL}Y)RhkE(~TMXi@j;5d#AQ
zXMsm#F#`k3dk|(+zkWxafq{X&#M9T6{UMVy535+t+DJ|Y2IkwIE{-7)hu==~_XvrW
zId1>`+>M^aFK4H7nzI`U`mAj%oHJ8s(aHoTroxA6LPeLIVmO+FcC75Zy)BnZgsD|Q
zROrZlY4NU~N}K0$^r>+DVpDqVTfS@mb_=U-=dYVQ_LkctAM<YK_dn+>?|=S1_x|^q
ztqBGif4I%;?bk1ToX$RD_H6r0N1YcY+}dY#Jm)|2k9?+#?B?FyWTp=vm2Ik6($5|E
z`};V<flUi`MQxS0X$W4Pkoi*F{lM$iJ9lEz($hWFxIb1sJSS^#Hl9I2o&D8oMY(?A
z{^OlTlS)cUFW%>`to-ohrN5s951VVt7WvCAaeJK@GV}AN*G9+2iq1c;-moJ^kFm~s
zcGu-iwrSfL_cK2|aN|bAs@-eW^c+3v`s>%PjNDw-AD170OpsukwM^EL#UkVV8-?xp
zt2N%ddw1&kG#|C43l};ve5{zz+WP*yBoCXavRr}H!&|0T=gph<Z`u8{&4yN1r=C1f
zk+<8ia&_~yupeJub|@vxI&o=ImxH%=_n#BHcJF2t*ezT6jc4PUW{wn=1Dhs%EnTvR
z$&knS(#8xEt*ckBGIiWve`!V`m&4RMS66S?xY1EZ?DxX`#~&wFR?aN2ICFaXhbNp7
z)27~xU6A=oCT4%#56d67s;a9Cci%m8=1f4yl4l0K#-DFr$P4e8F`2o!xw-w9udnaI
zOetGi+Z#ECPKrBfB4g7!U7S8TL<wne6yItr+04l5z-nM<SomT?fkn^x=a0X>w*T-u
z^oid*mhI9dvFGO)>T-7;ov_U;ly~;**(;g1@7=3A(L?3y-hKP@{FVpb$$N6ocFF$u
zLg(DvwJEMQKU#eHvy;idz+ghuh9yf}7+$`3;qg~mPEO1r@1Dz|KvOdTjuiF-CKI>k
z3$`<7W@QNpPCRwWi}8qo$cBXr7m9S8JLkvv<YThR%sY=8a~LATKE0ED^i}ET(W94c
z+|X!XVe)Wh4hm?nkm2ieTdbtCY1=k6zvaOtvXPO2f+wPcpFLe!bm!Ksq?DAF&(9AU
zJou8iqmbFh*s-|S`0?Y%A)%oYJ^r{B8^3v@k+wN~^NwYWhZ!ew*wk>Cng+!2EnWKH
zbK|<h@AURm7`m$nDcZ}giwT_i;Tf;W<dbh0-h6hf+<7avZr`oSTH6*Ug=)#T$j0yQ
zA0A@0C}fJ!`})t)wqVAROA9v$<lPP0{i*8VB~{fZ*9GhN-rZ|$WmUcD<*g{duwVX7
zS@kyt6W_Og?pSJQHLcRp&<y0^Ecm7qvuDMTHK{AFT)CoQxcYU~H->Lt9aB|9LNumr
zt~XnKdZ~k`$chZfwyfJZ@f>Tvocb{TSE~NQ`;+5;m+rp*f9Lu8E`M*IigB~AV_;xl
N@O1TaS?83{1ORkW16lw8

literal 0
HcmV?d00001

diff --git a/archipack/icons/wall.png b/archipack/icons/wall.png
new file mode 100644
index 0000000000000000000000000000000000000000..1335a590b819908c8180f1b38e1e762349590dcc
GIT binary patch
literal 637
zcmeAS@N?(olHy`uVBq!ia0y~yU{C;I4mJh`hT^KKFANL}Y)RhkE)4%caKYZ?lNlHo
zI14-?iy0WWg+Z8+Vb&Z81_lQ95>H=O_J^zj+#-7A68*vq3=FcFArU3c`MJ5Nc_j=W
z;9QiNSdyBeP@Y+mq2TW68<3)x%Fe*R_|VhEF(l&f(Xfqqha7ln%NFPG`1Cl4tPFW|
zr*Ucgfh)Xb)3`obzdgXdq4Xj5jex8A35#QAwajfa`BbXFsXJl9Zk9yNORm%2rvLtV
zuB9f>(#nao=v-%%-gI@*xqi!q5{@x!Qel>mWDf9JdY0o4L)?Rd-)pwa-KMu<^2Rlb
z{6c)#`}X%AwccmTfAH3ED-MAQxtjaOZ|6+AwPRhkMpw~aF*XOox}UST-+$Gg>{(SN
z!+!g%*cqPN+g3A9=u$bX$k6coqDJV}DBXx@tZ8kjqQ$W?4U1wA@pLfB?Yg`8!0fZn
za*nkhI{MtzF@k|{M#J(r{_dYL-9KxdcSvrH^4+~)O>pDhqxDX&({fo7%vpKbp4{)B
z&g9^d!00036wbhKM5dvE*UP~rWD;{HYy5K7-LpHiYuI@`PAE0Rbhl{j`+Z{#)8U67
zmZfk%`p50eBal)MFujE-tzo^>p(lz$n;pJZ?N#BtEX39F#rwf7y{T%7GXlTLWZi8M
zV2s$Full{w`CD7!kE&|*?+kLKOr}!2i|2kgabjEUV#yO#n|xP!s;NY*4YLnr)KCc&
zX|4Z%{PD(BX_~rTdZ#y8&Ykt9E0)D!;sJ5S1;RUm7}OezdD)sBKhJqsV)fVO+~2zW
hGq)C{-l$W39)G91=fJwliD{rz;OXk;vd$@?2>@~#0qFn$

literal 0
HcmV?d00001

diff --git a/archipack/icons/window.png b/archipack/icons/window.png
new file mode 100644
index 0000000000000000000000000000000000000000..74be2e0ee154c9994b03593b4e0652ff108b0037
GIT binary patch
literal 579
zcmeAS@N?(olHy`uVBq!ia0y~yU{C;I4mJh`hT^KKFANL}Y)RhkE)4%caKYZ?lNlHo
zI14-?iy0WWg+Z8+Vb&Z81_lQ95>H=O_J^zj+#+g~huIYw7#L(TLn2C?^K)}k^GX;%
zz_}<ju_QG`p**uBL&4qCHy}kXm7Rfsak;09V@Sl|x6}9M9WoF&w%_=nzjmpoo7t7#
zjq5LV>Q<{?JHrvW$60OLJD*xcxdqD6OZDD{l|{9!_ik#kWS^|Ilv(|*!0Ata|J?j|
zmyyAP>s-c`xhdb*H+hQ{)CZI}dVF(vvYL}2D*4wFwgW{CybW<D1sM;RH{{(-aQwJf
z-u2Zl`PZg02RReGH$7&!ZJ{q$$^F1q<K3r1vtae?1j#83FD#OBJh#PWb)Lu$WSn*~
zrP)_qxI*LlIfb`#pKrY9k|J<Jc-dsmxUkiGH$Fegw46aMP;b9MbF=k>O}{Um;6B2>
zM&4CJ>v1WIU6`w7KI^Kebup4XE4>?6eamOaJIC9=eSq~r%rZNd^1~1BZU1}7lxxA>
z`<4^5UtVGRU?3;&Gh=zCa^u#NCD%fwrkI6&ebJQM`XWClU+~1zMN^(K?vAlP`YiL6
z&+SUv4cg~()HA1@e!BJAmM5t-vSG7WJZGtRMujkBGyP_92)@)}pnUO``ANBrbB`C+
zs488!lX&stvWZhvJfqZ2*0X2X9xXh-bVj(WP}@Sq%vJw1uc_sHk2}(mn6ZjQJ@b^v
ZAI2pg+`gGNeR~OtOHWrnmvv4FO#tD$?Tr8c

literal 0
HcmV?d00001

diff --git a/archipack/materialutils.py b/archipack/materialutils.py
new file mode 100644
index 000000000..92497924b
--- /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 #####
+
+# <pep8 compliant>
+
+# ----------------------------------------------------------
+# 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 000000000..c8898fe56
--- /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 #####
+
+# <pep8 compliant>
+
+# ----------------------------------------------------------
+# 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 0000000000000000000000000000000000000000..ef4fac84e9bab47038f87b58acf89e32d91bd6b8
GIT binary patch
literal 10252
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
PU|{fc^>bP0l+XkK1!8qE

literal 0
HcmV?d00001

diff --git a/archipack/presets/archipack_door/160x200_dual.py b/archipack/presets/archipack_door/160x200_dual.py
new file mode 100644
index 000000000..7a9e5ebc2
--- /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 0000000000000000000000000000000000000000..660b1d705443662baebbb1fb58ae680b6e257d57
GIT binary patch
literal 10492
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
JS?83{1OQqEa$Eoa

literal 0
HcmV?d00001

diff --git a/archipack/presets/archipack_door/400x240_garage.py b/archipack/presets/archipack_door/400x240_garage.py
new file mode 100644
index 000000000..2060cc3b1
--- /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 0000000000000000000000000000000000000000..e2bf6f5ccc444f72e8e6f4e442d6f758c9690efd
GIT binary patch
literal 7840
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

literal 0
HcmV?d00001

diff --git a/archipack/presets/archipack_door/80x200.py b/archipack/presets/archipack_door/80x200.py
new file mode 100644
index 000000000..a29e3ddc0
--- /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 0000000000000000000000000000000000000000..4478afa6ff78bfd2838e843190d1462ed3b7b7db
GIT binary patch
literal 7106
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

literal 0
HcmV?d00001

diff --git a/archipack/presets/archipack_fence/glass_panels.py b/archipack/presets/archipack_fence/glass_panels.py
new file mode 100644
index 000000000..2d150b71e
--- /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 0000000000000000000000000000000000000000..e90314975618f03cda32cfee5ddd1ad0db81abf0
GIT binary patch
literal 7835
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
Mr>mdKI;Vst04$TN`2YX_

literal 0
HcmV?d00001

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 000000000..80d3fb6c3
--- /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 0000000000000000000000000000000000000000..b6a24339f99cc5aca6383ee4511a2209409a9b0e
GIT binary patch
literal 10234
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

literal 0
HcmV?d00001

diff --git a/archipack/presets/archipack_fence/metal.py b/archipack/presets/archipack_fence/metal.py
new file mode 100644
index 000000000..5e7ecbfdd
--- /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 0000000000000000000000000000000000000000..16020ec483dcdbdbf55ff592a0c92dfe7e51e6ee
GIT binary patch
literal 9582
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%0D+sYyb0!(T~FA
zB7eQtN&b57vBPYo29~*XO-}EBFI?Iyo11a_mX`HZ_tkZ;Ej|^Sl&Jpt^=ReNPuo5&
zjxNozuPKvh-r6!<Y{j45$N&B4J~t=!&ne?R-`b_0s^fmFHVzZn>Q%aF;hvvs_OJ-d
z+E}-={g~6Q;<t$@FTU6%tM9S8=XN;Ui2d&w|M-<Qk59!v%DsGa{nA^m>AU56L(5L)
zpI;oF`~8pQy=y(8yy41I*X>{HV%{kEdim1%byL@F(c16ZwpaD%hpkt`Wc^Qu%-SbZ
z3o_~ERQGS!FT{TOX=bRKrqj0BAXD4de(5J?@91Xr>4m>CPx^m#o}Kl0asJO0pMEU#
z{kkcs)`Wq9#UHd5;NppP^RjIhlqUa_INnyb^!uLNKZ`HLZo2*X*Np0amtyRBHs`Ot
z_(97z{^06Ayk8%r>jxjd=qr7;=YHtvyd2-J)$=}@MfGl--rs*=_1fI3y?IZvt3}GD
z=j3&-ye=F6IPFqq@{(=Af3^DeuRT)vWYK@`Emx0+#QVH>cva$ghV9;47FzcLZ=S8W
z{AsdORPN!~-Otm@bxsNTMQ7IDk65`$`qb?6D@CNFZ2vCKaSf`KI3EA&mek~pn_LdB
z|J<jut4L(e;!V{zBA!38oUn0KK&i>~{+g$Yzq0P1zVh*x{#By>TWzoQ`&7(q{wR|h
zUbXe^>opJe_U1jCJZ<fj3zypVU6cR&V&A^x)`;7=e>IPnJoB2<7XS6Keb=__C+jm_
z>h~^Bxgb&%TJBuyTt9t}YW>vjdbMeF*|~B*!e%^p^(g1H|3_zU>r$5%wFh66<!|lI
zy18~+&rcWccx&@{lU8n7b^CHXZ`{|L>))>1vwiOIlw+&TP5<>U)wyRmSDk+F@t4cz
zZra!@c6aBrx{Bbx^TLl$oWJ<3*ZQ)mx!-%kA76am{bTtv2fn{w`>R)7(EBU#LRPPL
z_1!r8J%_hleLtnwYxXPkc^{S?E8Y_O;;LRHt8S@t<!8xX_N(r_d1@<sKIAv&x5p=P
zv#a;k#r$pCzHRQNz)bJI^QIRj8HDu&n@>5Lp0W4Oq;vMl`CkKPxZcotJp25v1-1Ws
zuPig)`obdk%Y|#(uEv#U>;C<)<>hyq<fYSZ25i5)`fl8Fjf#(fw|Cw+DZ`roHSmW1
zk)VK?H>UV(HNLujOO`tKM(^9Fk1R9$t2Mdq{On2ZGeft(TC6=W{!y+9f1TTVg;L{L
zk6ZUWdA*{t-HyvQS8K;gR*tpP*FRoamywygeA`3ct+u^!8IZuZ-x`)${qxl2^{qKs
zOJBF|OL?zRQ7dCQP4(WwzC#l8jrR0j^WWE(ZC~azJ6n1A+th6r5B0|#yCt&clzaT5
zubYZLP2IPoI704s?aVG~qvAamt{>a@N@I=VlKL6j_&3RK-s!@<K7Vywh4&WCnJ2>U
zN!^;0v-JBmZU6oHPnMUQIQ!9Q{-x^ZIQjb_GVA|`Nj#MDc<cYsx%;8jy1zT8m)w5w
z=ecW|{_W33emZ;OenhE$bLx*fzxuJ}Ov|XH-_5l23m0g1`u~spTvdJbzSKQE!+qx>
zu6o`#4_kGRH}zj9r+roNu8FJv+<O1B{i$l-y*U5*(%<WPpJ&+GnoWP<z2`_w^rv8}
z{MXs1o+Z}oWeYQn_<fsc?c}@H-oO3!Dfo}-t#x~}wqJQ)S!%PLFQsay<xSOJLi%>o
z{^)$X@y<KvTghX~xEJ304n_Y7lThFL+2X~^-93*jHvO&WlAF0D<<k4xnsG~gza}q!
zo*MHrJEM5h)O)w@O?6+VpYvDf+mE@QMen`YeuyI@dnVVOPvtMao4u-7C%=2@{mWYV
zf48JO{}sGP>-OhfQKR4@+qI|kKDsTwdwfcN{ONnMZv2-Q@fB+ES!TG9fq{X+)78&q
Iol`;+08{kEhyVZp

literal 0
HcmV?d00001

diff --git a/archipack/presets/archipack_fence/metal_glass.py b/archipack/presets/archipack_fence/metal_glass.py
new file mode 100644
index 000000000..fb5149cb7
--- /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 0000000000000000000000000000000000000000..a1706f297fc24530193f0c7290e716cee43f917b
GIT binary patch
literal 13183
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

literal 0
HcmV?d00001

diff --git a/archipack/presets/archipack_fence/wood.py b/archipack/presets/archipack_fence/wood.py
new file mode 100644
index 000000000..9a9a42d95
--- /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 0000000000000000000000000000000000000000..b6e7fe56713f2bdcc5ee2a65de012ce461fb2164
GIT binary patch
literal 11148
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%0D?=DZ=C}Bz@ZHT87o6Y}R)H*#(j_%l55(*%BAP
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
CKa36l

literal 0
HcmV?d00001

diff --git a/archipack/presets/archipack_floor/herringbone_50x10.py b/archipack/presets/archipack_floor/herringbone_50x10.py
new file mode 100644
index 000000000..a1f196ef2
--- /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 0000000000000000000000000000000000000000..1a2b2370eaaa413f5f2984c44fd1a52815615116
GIT binary patch
literal 10924
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

literal 0
HcmV?d00001

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 000000000..088a22e41
--- /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 0000000000000000000000000000000000000000..2b35d58b765186284c61c0d0f14e7cf28c58e79b
GIT binary patch
literal 13445
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
a_{pF64GH{{s(kVpK)}=0&t;ucLK6T8z8Rwc

literal 0
HcmV?d00001

diff --git a/archipack/presets/archipack_floor/parquet_15x3.py b/archipack/presets/archipack_floor/parquet_15x3.py
new file mode 100644
index 000000000..5711c93ae
--- /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 0000000000000000000000000000000000000000..94a49c5789a2fe9d499563334f017b027a22728b
GIT binary patch
literal 11644
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
bk9$Xg=)3)Hr*<<iFfe$!`njxgN@xNAFN`0i

literal 0
HcmV?d00001

diff --git a/archipack/presets/archipack_floor/planks_200x20.py b/archipack/presets/archipack_floor/planks_200x20.py
new file mode 100644
index 000000000..bbea2e66c
--- /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 0000000000000000000000000000000000000000..2a3d8633e632f4ae65e8bea9b7682ca50ca8cf79
GIT binary patch
literal 12939
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

literal 0
HcmV?d00001

diff --git a/archipack/presets/archipack_floor/tiles_15x15.py b/archipack/presets/archipack_floor/tiles_15x15.py
new file mode 100644
index 000000000..d3d244f90
--- /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 0000000000000000000000000000000000000000..16cdf0f154ab28b4ccb7952fa031f4683cbcf59c
GIT binary patch
literal 11379
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

literal 0
HcmV?d00001

diff --git a/archipack/presets/archipack_floor/tiles_60x30.py b/archipack/presets/archipack_floor/tiles_60x30.py
new file mode 100644
index 000000000..f8b66129f
--- /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 0000000000000000000000000000000000000000..4d4c8ecf612c11735572e97f4ba3daee0fe1ed0c
GIT binary patch
literal 13663
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
N!PC{xWt~$(698@$4?X|@

literal 0
HcmV?d00001

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 000000000..01086dc8b
--- /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 0000000000000000000000000000000000000000..07c6e266b92570941291cc0bdf12563f38fcc9b2
GIT binary patch
literal 12511
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
Zrxirpp5e3UI0FL%gQu&X%Q~loCIH%t3oZZv

literal 0
HcmV?d00001

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 000000000..3ee45a2d4
--- /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 0000000000000000000000000000000000000000..33d286573abb0ff831ffb30ba7d3dad7e2616bff
GIT binary patch
literal 11631
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
bKk-lY7zEtBTi?vUz`)??>gTe~DWM4fIrEv`

literal 0
HcmV?d00001

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 000000000..8f4253fe6
--- /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 0000000000000000000000000000000000000000..9fb3d56cff3a4de9748f50e359b8b67d9740888c
GIT binary patch
literal 15606
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

literal 0
HcmV?d00001

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 000000000..53b605cfb
--- /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 0000000000000000000000000000000000000000..0e2ce6b66e6997848908e6b15aae6ceabc4ea602
GIT binary patch
literal 18279
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

literal 0
HcmV?d00001

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 000000000..d4fc1344a
--- /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 0000000000000000000000000000000000000000..215d42b929fed4d739913e0195d8f159956271dd
GIT binary patch
literal 13886
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
P7#KWV{an^LB{Ts5pu5xW

literal 0
HcmV?d00001

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 000000000..586aa9901
--- /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 0000000000000000000000000000000000000000..aab27159f2a32a05d32a3f262ecff34a72cb682f
GIT binary patch
literal 18165
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

literal 0
HcmV?d00001

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 000000000..b523dcde0
--- /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 0000000000000000000000000000000000000000..25f21c0a5ba0eae99987b6e06c6a1d17308ad841
GIT binary patch
literal 8410
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
CsUZCT

literal 0
HcmV?d00001

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 000000000..7c7dcf9b7
--- /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 0000000000000000000000000000000000000000..6809b6fbdfc91fba2e51e133457d6fcc0f010bc9
GIT binary patch
literal 8593
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

literal 0
HcmV?d00001

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 000000000..312f72995
--- /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 0000000000000000000000000000000000000000..e775b887e1a189ab98f867c34545b28b3d68d452
GIT binary patch
literal 7969
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
Aj{pDw

literal 0
HcmV?d00001

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 000000000..010b40731
--- /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 0000000000000000000000000000000000000000..5ae472dc47d0d6472fcf29e44277e5df8b005ff5
GIT binary patch
literal 8571
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
L`njxgN@xNAZGbB&

literal 0
HcmV?d00001

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 000000000..3d0fd325f
--- /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 0000000000000000000000000000000000000000..228455187596399ab26b4d021823258567da58cb
GIT binary patch
literal 9492
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

literal 0
HcmV?d00001

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 000000000..3ae2748a1
--- /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 0000000000000000000000000000000000000000..354e9be9888608fa87ac0ec7787eaf25c2482ea9
GIT binary patch
literal 10314
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

literal 0
HcmV?d00001

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 000000000..df26b7a54
--- /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 0000000000000000000000000000000000000000..b7808c27cc7818c683155198e32fc74b4ec17d73
GIT binary patch
literal 9362
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
U9}Ql>z`(%Z>FVdQ&MBb@06aq9s{jB1

literal 0
HcmV?d00001

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 000000000..d9f2cb89f
--- /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 0000000000000000000000000000000000000000..1201622abbc396b154e0d9f15fc0dc4a242833ec
GIT binary patch
literal 10360
zcmeAS@N?(olHy`uVBq!ia0y~yV3@|hz>vbh1|p+Reqdl=;4JWnEM{Qf76xHPhFNnY
z7#J8tOI#yL+%j`g8C<Ml3W`#TQ%j2Vl5$e>QVvMQ&Szj?kN_!gNi0caFfuSS*EcZJ
zH?UAJG_x`=u`;kc+0ihWfq_8)q$VUYH<iJ_zzT{C-yBvu1hN$*=T?*mmNYyVD5?Z<
zBS_FWF*mg+kpV(w{D1$Ffq{V=BoUmPnwQD|CZ8(Cg8U&25)MkuOGzz4SfgiPdGqzN
zrwj}V44y8IAr*{o=SEIev7R+ev|4ZL+*O-P<Y(+ll4|D8TcFRT_k;cKdc%K=*P0t8
zbDfhfe~HXGt7;^6&s{G6$#kuq?bRtMUuPPIh-$vP{l&A&dD7{pa*OZ(|M&fNNXx~H
zkbqe^(z1Et=a2n5A)}l<H@;}k-MVXM+;{$tus;5Z|LW|%<B!=z-@N*hTq$y4>F)!L
zjhmLfi?_aijnVm)zpPyL^N7F6U$U(BHeS7I5Njj#Qb2yDMB6F%yZTetnX0AofBkD%
zcB_Q{XJOXfsJZ{`H}ENJTq~T_do%TwVOif*TetPT%emK>&W%^uaL3Wv`h1o1<EX<o
zjs?iy%8}i4d0D%B?5^W4YB}e|n{2qVvt7#k)X|b~X1AT|`nrq!%5U1HtoGgRz229-
z{??baS0yL6PrbbKvunfEXA)nQ^W^s4TYhGpn{~=}iMtD}Vn1D3|15d>xqzR}sWohC
z%v$w+DazHfN3ovyJ*hBU`SEc@``uq|m$bi{6SOaEx!!Nn%8c4lHpbO!4PNl&_GZ=T
z+PN)HW52bqEBbxF%=R}|zO=vk6FhnO+f!BBuO@aSXx6Owc>BrP@M48e^Nj8uH$Ro1
zE$};RvvJ7t`MYg$MQ>|fE4NQ_Wi-!QDe=;8`^%Z}$DE&f|6NzJCStnGo>}tkU%clo
zpJJb~d(B~e-#UJsa+jMG%chHVewp#^Qh$;E#CKZN8HXR=SDd?PkK(80Q;uGLaa!?q
zywkrGD+P{oY&7CsdO!a4*UW}GF@EozKHlFntzYp>{GWX)$o}`;<NeDg+5hYil~uhf
zwnNo=p3kM9KPzYXrf2SrTlzL_w%P2`u$wt%l}8lRE_mdvFgnMy+y29y4HM&*=_W_3
z?mxpn@xSVcNB;%v{dDhJCLKNGv7I%1L2vxx_$8%Pdp$Dk?k_nTG5@AYuG?jmA4}e5
z%rTnNvd=fmFG@g~Ww!1DVgCiqx1YG4*iy<Ia8h8Kb;BgHh+Ori4NENh<9M83b>+;S
zn=$!IKxE0hL)&U4v-VH<pL3+E&2xLJB5%yC3o|CA&3^sm*K_*|pR=xZx?JIWJTs}m
zG&D@U^=!(*Jr3qGpZ_Vd5f?bgJvGw(<;GOjGb<f5tq(=8X{0h<nUj7+d44eiZ`NhU
z=mV>43={XNsjcrnkQs2OdvV5@$$Hk+9@dfDwlv<2s_-%hx+m~^X^q^*MVHsbEIIl`
zv2IGPcXqS#-M;ges!aSRmrUPXX>)7Y$8UaGuinVK-*R2PNU2%zaRNKr3YE_d+pO)=
z8s0hV6lGw1na0C0k#&Oc)8~?FUaT{ANadQxz$O1-=}C>}#u|)j48{&e&;LA@Wvn%6
znva~Vu|uKXPlf;nNykiv)aV=6+XbwzY&oOxPeDADyUg_W(kI^!EIQ!4u>Xvr^fU9z
zcOwpXy{!ND`1Y4?mvgP=_g_AK)h2dN#pH7)9GOq2iV3?39lhL{5}vtEP&4;>=AsOx
z`RY;2GkF5TFQ0fVbJ)Kjfj8jgz5bv1!Om6&la1FprZr#iIrpIDyvK6EfJ(a-8?Q75
zqbe>F29x>e(wqw#L<Nkix~4O(x>)=yFyh~d`@UJ{_6LSNb~A2DD`51vD9^M-(oxxA
zV(i*5snFS9HMIOC`xqY|G@T}Er@CUpPEnt0i(O~s=U+N6pMQCN-LDz{-fh3X)PB!j
zt+~1JOU|x6z;q)sQ=o9MpzquhOWY3keNnWPUipx9&%|XLS}$=k=4(03OHa_dY@WVL
zV?yK32|K3EVe2&!^*Am$<yFPae20_qP24$d7jh+Rm%sMk9jIDfKP}fGOZLk~CHeA%
zJAFKB*cMcNIdgT=cc$cd+sZfnzBqa1yo-~U9+Y^%Il<V0&*EoN?&aJ4;j8>NAG2BZ
zc(<EV*2fzkt}{=Q%COw&^L+O9m#fpezr_FhP=BfZ*Y*3H62Y;mX4%UkcHR)+WoS`1
z{lM$Qqu~`V{F2!vZ2Rhxb?1ImeKL!!t(yOD$1b1g=kIYepEkL)y;kW!%$emsXYb#$
z+k5d@qq%2~y$o9X%wykOo#|4d?wMK+Oowh?joMhT=xM~3nqOh8Od+SlwJQ`@p6eeo
z`u*f(?e3&6Dkkw0)H<*Cgz;)f7j!W_NH{2Z!KO;-d*Uzs%T-m^4C))}S9gD(HGjF>
z#*D+hc{_5eN;i63_qNKLBKE3)!T8CElC$?a874X|SXs1G^D%#T`pcPa4HK`GO0q0)
zSTV<di|e>gKvdY$mr=GW{|l$8MSagyU2!o$#dmXbZs(aFJ=-(aWlH`_s-9Z+=cU=-
z?e_PVUElvp`eeo}x%HPrww+@QVX&ThM@H7o>uSHDhxkFs9aA_1mi;(fzxCkX8!MY!
zl?{sW%zv%Oo#AJGgKfu5b?)6IYBE12yZ>SQ#C|69W@W~U*CFvcvpiJ;MR(7hvvMNC
z#C1ZZt!qv7Z7;w0zi*1IQvZ6N;_|4Kx>rscTU~wr`>3#XyQmYFoM5qEy&IGB<a3kP
zt7NRoWV1hT<lpL}Dt~`>+b@^D7r%V}e-rQhcjqnbJSC|8dR;#2u{lf(2izH^@lR<D
zU6%hW?b8}{(@3s{S?9hkosknhv))WFw(I<{>o1=^oynjPuyF0mPfPwP-oJIo(BX}(
z=rJe%U`1d3%iN|*%{)yz_E^kcy85ZtYZbA3B?mJPWm!+FEj`3un&e(Ex9H^VNAEAU
z+A{<v+{k@;D!w=8bLG54-yeiV_Ou!9Xzt+bn|eW9Vd>Hn*R{BQ)#dy3?40oR{W5!#
z1!3K}Gk%vJ*mO2$!}0kGKiT?(xw6HIUenp(oVCY!%hqLTQkiG2&ary!b~({2Jkwg3
z<I~l9irjZ=qzZS=ReNxH>eH9fq75h8+0^G-25;;A-1l+0?u_;m+`T>P#b+rT+|IE@
zy1Qc2wySCCm-<)!-)L2xaI;adFG~HG^Ov0==|K->|9w?7aoxLXFOxRf-0isf#bsaB
zqiGi}x3ER#RP(H~-WPFCq3hpF_N>_^&XPj6vMy{p=8?;`;aA&2hK>H;>tysaU)8Yb
zTxplEj){qFa(h@*Wm-G!T$ugR`1;a`_cHZg@18zA%X*vCmPapsxLsc>7E*e|>-D0U
z*K{`XY+9DrzfHP3Q1V7}hL(-lj=i`4T(XLdx4)L{v)1R}EUwV`b_X{3PQQ5Ey`=3#
z^5!cmnNGLzo96Akw^1tntlHv+Bchl2-<Qr@wpw1$u6WD#vkE5;{q&oAQ1p64_e1C2
zoH~Px6LWS=HM;Qm)~Q^xGhx9#v)}o+Cgyuj?7DtS`gF9M)^6<u$+KgNp51&N{Ne@I
z{GGf16*lJnIezKqwT;zlubqnKtN(Ml`t6QvuZyQ%t2n;hcgHNe`T4I&!Exa$t}YLi
z3qE*H^*6VoOLXHM?aWy2&3h*v){9zsRwuZ7ZTRD>8O~2PJ`C8*zP9+trcIACE^Ki>
zT9jZ}J#CBV67QFlQR~cJ`u%nYzS|XfUTkmMuXCS5>tnYpUz&3->+t0K-xmU|-MpXm
z=E<|OYNn!D#@lpnxLwJe+*|U+;Jj?>vF~?ZoRFJa@%`@YjGOyTcW=vjx$x7C7psjk
z<WHabT6*Wx+l|N6-mP5u^@dDpvPi+%tz9RqU%l<xYhm)o>+^A!>Ce(9-_@(tKU=o2
z_HMaCs@9$J9;=+gZ@l(8A$vu2ex>9NjTigR1T$p^=j=4O-Sutv8#(R%v-?utxUbpA
z<=j&qdd{Zr@CFUr>u*KvwliHzs+qlROQGe}Wh<o*emeXp_A)n1@Y=_6>8lx?*Ur6`
zQ}WJg@A~>}#V6D^oO+gdVEQ{lCY`BUly8NF-`qII?$eD;e(^t7Y-SfbqR7^EB5Aj7
z!T#*UvOjKqiwbXf{_+{0!`&yh4PVGa`{jw4?mP1`<GPOR?E5Nw4-(GvUVA27;>TBT
z<me}zb0_`FzZ-6Rc<x!>!HtU7*1wwm=jpOt(;iBC)NtA+ebu~dmTVbYHO>8*p%s6_
z)$0-WnRaVUIQ^)9nP%DjpG=$PmoES8{k-^^vq3-qP508eeQT9xZ{I#E=DgJ6*ngKd
zG-Rzbjm$}@K2#{$bmWqg_KvqxO8vd;<L)lYKUXSmz_04N{7wG-6>&EnG4o1Z>dT$I
zV@V-*;B2Ev7LDh(Ers7En;x1O_ph_=_WU5VXYHpJS9_!zZ1`m%HP>#2*aox9pH1%9
z_}t&S>%{ibxVTNi^#PmP8~iK{ynUblZ2O?N=2Aq%E5GHk9><RrCoW4oUA=A#r@G38
z;@=i-_cC)NB9<(4x2tTvXtidETC|^s3GbqhG1+UMb$9TxznQY@+0~@$%=1pKI_Y{#
zV6NkVSFasPo(0T`7GC|c+hq6O;(I6leBQnK%k1mVD{n1aC@0MDJc@T+F^7-BO_9<L
z-jv@L9(jaL-uR~Wj>FXAK$em_Zh`j8G#6{T{9j=|dw25O2^j^d@}ZOY=i9w>Y0KbZ
z6aJXAF!_#k!_EF}oA^Fn?5>+Ezv!}bL~T#dj}Hs_p55t^YPoIo|Lgw$$-nyRf0s%<
zedyq9_hLoMrc0s|eJ0)f!&|@aLdh0p2j6L5h3EF&y|*ouBV(<})o0N<{a-})C*Az9
z_@n3S-De-oofg3vF@N38w7hk*Gv!|wM0c7lxXrU+qS=oJ9~Z8>kknCi!}<KST>VqW
zx14TIyBT+2Q-O)t<=8(F8CQ1tXU|SNmYei8H8x{$Bj2;G0A(LzxjWv+Lc(XBczZha
z$|T!uMnA&2Pi!iCwE6mKPB*D#W%5dg_iwM;a&=PPp4@+CN<I6cPMwZFtTgR<S<K9B
zPJKJp-d%9Mq(|z2;ZoyYk#f6!9@*&grzxzB{lF^DD;c@RydDRxD|r?db|5NdRlmj4
z4PNs<NviRFyrmNRR_bxww)#_b%&WG_Eqk!-+}%x))-&5p_w8+VoO;(_j>zfr&)(G5
z|F7~qRkF|jMBt*>6W=-Kt~|1}%<OF8O1J6*$t@+b%nBRKuJ64NzCP~l!L+#OJDh87
zFq#D{o_Sp~di|Q1V^-m9t8UEvwsBu&;WFPK*^0}%9_`DDoyPw;BH6bjx@EDkVSpO%
zi=$OfzWP?%Fh<-cw*CF8{B+^6_~1yn?97#EdvEmg>+QVlByRre)t2AKH-5Fviab|x
zeCZ##h^0<5k300H9adehqj75Lp0{s&roR=h+VDurzy1CC(sz?}*-f)D#jTg_%e|hI
zVwqNSFmb>CS*6=WJz))9HxfH?UkiwD=Td(C!sNotmjVm5bUaKipPl=vs4F1*`mER|
z5^MGJ_2qj`rCLw@R>inF<WQrOu9eu&T)#IjZ+YH(@#D;ocftF(c0G)lW@d7KYmMgO
z-03^FrykNf#`0Q7u1lcl+hVT6`y8(>i=I(6;qLBbEw}9EZhZKKrE&8o!#f}698h*z
z`}Wou#p<%MX>t2%cHWoR_D0b<G{JT0jAI!y%N=7kmPT1mn0K+n<Nk)}U%y>8ik+AF
zJ+`tW`m57IJ>>~Sla3!TyLIV7bp7@{XSZ!&UonI2_YJ*VtNSs0FV_E>XZXG_Hcr0t
zGjB$3IE$O^Ht8!$miAx#qoudrOl183=H_Ph@L9Q=QmxYxjxyc8B++9d9<X?3xL8``
z<&)31Sk)DO(^~5P-&%hDPx-2buiJkn&RWLkZF~E@cyODQ%j?yybrm7XGZ>Dl`!)W}
zxw`bz_oN36Pt8_D-w7=L!5GjGVY}dS>hXiub8ke(C2UGNqn~p#R+BBvZ`!}<->R8c
zhv<AxJ@{m?PYKJe&z8Rb{-owvnX%a4yOVi#kzvE$HFx*!)T`dQ>3ycu+%0-DUzwdK
ztDKsbmsfPNpKU5TZ&K|iMH^M+eKE(Y<Jp#_^eiun5!qgR<9Ju<myHs?7p}Ka7Cn$u
zzglcpPSAVy^P+AK?OtwjD7huI`HAhB)hkPGv)&cIb-HT#@4TIc<?2F<^z}9`**f)f
z&8qH<l`pxxjUTOb)9QO+V`q|a$p72ab*1k;e(zrU)qb1Jx6eH~4M(4AGQX8te!wnu
zYHok;Uny?pcRI%>hAqqfDWxmcb$5pQyaRtXENQO2!~D2&Zv6%31vb4l7lTjVe!J7`
z?W?NZvd`wfc#b}0P?Tdl?Os{lU4EeK)q&5QQzCzToKf_lXx+CAH=R6(*ZW>M+;9JN
zSmW!TpP$c_PQ87tboJDY>|rTZZzY&>Go|yp*QTeRxPB>f;oWk%H8b{m-`^jVu{b2%
z-hI30`MjqI)+uZ57rd(1^<REx<L7lR3zmuZe2}QSe_{EYJ1?281hcqZn`zb?nY{9<
z2`^Vf4d;9fgSU)Y>sTJ^&fg?^XWLTgz<0(8uByvrlQov*?r`gFjW}=e#NK<e=c?+q
zJ$oMa-g~N`^F+KYCUZ*X)GfM6w-xS6%~%!@p2>ES{n?cFORr~bNMu<0?B&cf{x>F<
zvm-ND)|4Il`n2Jac+}Q9P49wvM}KvT>)SDl{xP#owRYPXmf)(mT=w=%qx`B8HL<=u
zpAF}Jjfgp4`tR?v<9nWe`B$|x|Gd`SH+f5<)`ziek&>4>9$E4H;E$Ug?SI`~9=~^2
z$a~fD*|LRGeapYUeY<Pr`4tQt8;sm`=AAY1a1<<<zmiYw!_m|wk1m&HpQ_u@u<qaa
z16@ph(+!XLXkXS_Ar^hyZ({kaPcxUzyBW8~@A3EgE5>cpU%tI+G<W_>&F;G<!M8T9
zOa44}InOqcy!`x^Qo0*vS5H)DZun(ZzW(2bysa<iTo;{daH9U+|D&H<_;l`Qex4$+
z?Cy=w{X2G?SkU!0;<fs3p~pdd_m~rIu1IE?QznzSL{>>YYOTR(E4Sxyey=%tqxP8e
z2J`)YtuJqKXvvr6ltpt+@4UWw>u-sBav`@8<Fh}WcNUGlTYo1*x_SDmd0GZhPp^Bu
z+sEMirtB)C^&K^-&2J|;Z$5S_)lbMUXye)sSHt6L*|fL{qNle`t-Jojs!>#QPT8A7
zr)rkCvrj0tTPA;EzWwzVi?(*p)BW@Eaqqp<rMc|!CnF2Qj~C@$SGuz<aS2ll%eCxs
zX}!isw)t9bj`6wXBu-iQW#tq3Q^|8p=bWq4xSC^Fko{^(qwBAQ+773*Uklx~`z8Fe
zicwoC%lkOfoNa5kRo-t&eV5Gp`+3#m`S;?#esT)W-=+2B<%>V7<T<Bqd-qlD6NBjU
ztB*Tb&n4Y``BZ`1mPssaT1HaAtiJMu4MA7Sf|H*_JlbJ#*z?(^XR9BDKK1xiIIZ&e
zDHZ?5H<3rX7f1cy@isZK{RtbRwwS=>jR*E+eknXxc<u42fd3WuHDq09e6YJH5R<ny
zse^IVt!9A|^Y{9^?z5Y&Do-xi6|ytBU|BdX<Ac4A-DlldeNsff?6jj`vhHnO>mToq
z_IXxtrAX<Bzs|dVDevaJ_9y!|HYimj?JB9;b73`q`uWmR4|DCl{tR4vEyz7{+2_xD
zCiH$<7qP}*vB-i|ch^O}&YsV2vEuXWPSvR3a>lO4gHnC$tIE4Rvp$~7%NE4EX4dn$
zX?Dq%@_g#o$L~L<?q)B1L-5qMf@=4lL2UD$`f=$<uN9S$&GPQw`g>l9aaGxGz1qKz
zVjf)D*!%Tt^W-H<b5FG#t9te8m(~0xowtYg-+htD%=hbK!Ahsh<yT%#U;ev%Llskr
zZpr)!4w7$nt!MXZy>hYk(XR^OE9~EW^lvY|a=K0=e&4K~r+rLeXA_o8TxS_|DQ2aF
zd3f8|f1lXz_RQ;9w`JRo_@e7M%vyPRoA+rkr(Sw~=UU~#$yX;mj65*8b?(aF(MOkW
z^yz)RODScE(uS)S?tDITbMe0oYY&JjY|l-qPh4}xS^M|u{+?W0&J6~2J`X&ne|$RI
z<lo2lpLgo??T+brKPm6cldj#1)+tmj+gr5p-6mcCI?d<@pHrTHWHNjI#Vs|@>ACr>
z9Up{`{N#KQ!?%4&ME$+`gKaaG%Sx<d5Sd>Tn;N}w?}D9k&#J%E{+x2)(8^1lFA_|3
z=YBleeI`l&wS3)=8ULo<TXE;woEz!Uiup}k32PnaZoblSH1yfaI7!d`)qOhy-aXw~
zdGl3h+Lo<?Z;$h4u*<QWW*75)A(<?%U>Nk#fF+Z8na=Gm8NVWCsxya`tkeA~`Qyrr
zJ?V|&8_!;0c)x!7S>Fg#zJsF6H`Pe2oWYdc{dJS0!NJ9;KX-XOon!RodZba*wCWPe
zD}vR(7~;>pz5hPr>-JaL4h~0mX|Zizo@si&f8xP7HZe|~8z&at_;kcDf?vw-Sk08;
zU24XAYZh)_Ip<}}w7Ao{2_LoIK67i^I%|fugV<}+0NW`!MQ304m|T5zwr<Y+NyVlc
z?`^sB^4-sSQGZ!KpKROkvh2mi^|MlM&DTC)o_{YTVCvsMrj<{UKTAk{kz?RyIu`2?
zon!fETSDyTYz3q9hJQ`wWuJCD&#1S|YVx|-1@h~+HrH*uqWtu%^W+o9v=3~0^dZ~i
zp4DMK8-=TvQ=1DyH@jQ5*sLok^Qo4seRU&4_TTBKf=8PpekmD<^6}clGOwuNS+>Ug
zruz;1sXn3+A`hHyKhkjirQ*nIaZN4rlJ(|{8>f%z%*{@pF}=*1VO7QMV_%#Nx0k3z
z9VnWea?>bcYH!tvXU=uq7u%io7X9R2re9HapGA_j>~1^Dvc38Hex~tjmE4_v(J%6O
z(6T4j-&{%gbe$pD?1uR|Q2|*yGuwIkm!90*o>}&HvV*Cw{p-%TZ(Z`YSyfN-t=bs3
z+9!Nw<*&<GshfV^6yT3^emf=AVD+=sfX~ZQ<fl(R`)p14=8ET86HLSIA7GLVxY~Dc
z%Gc8R+us!JifU7qz4^Q0z~jeGzmAC{MPABfbk})a65VsB<MNWI1Lv<g{5hsz|KCyg
ze&)uy7hD_GY^hE9l|0wz>S>b+nUgpFIcU+Z^3#9a?U%>2z3%DWG1NbNBTw#k({<TB
z5zjBgy>;aH-n@3o+kj6KtyO|QU7T5zI@LN@)=S~7_a%-U9D8(jOfH?Z<LpW)+gG+e
zt7{m$GaqdAn{@A``{Z-x;V;#5wWowPY~`}u^3`Dbw6!(CyAK|AekN#n=E=VB%O9)F
zm-E;#`bwMBOK$w-+se>&an+s3?fI#*r`864x@fHu{P_Lxa2_RxGa7EU!ehcz%5FU{
z?!DnqqPi%!?8dLaW0}7VF7qcAnax!%`}cnL>o2Q+E-x{?=)X|L_EJXkL}jK|bBh1I
z5!=u8?Z*Wk^__c5LLK(DJ-GTd@H4aZ*;*&nDGwEe-PWvYu=Ufb`TO-c|FzIm%hcce
zPaiwWOY@7p4=adS-68w&?PLw<%P*>?7~eJg5_O{H>(4KrmbNkI-cqkRxwdXc=lpc$
zCp#ZsX~;de@9K{^HYcL_{^ZQfnfqx&d~lp`E$82@kJ>M$DDB_uX!=Zkd+{HW?NwW|
zPIs4<T}%GG>+IT)Z5NY&-dKCHf?<tD%EN6IrzWXQ@sqCM-TpKBRKk4zFEf_a$Gu2D
z`0vl>-(Sk>f5-kZukt+M^*j3g*TbUTdP1d7E0$gt`p8!vs{3E<Az!6s{I+_#$xB7A
z&1`F$xK2P`+NkCWyS@F(d1}FBr#H`a7Y(&r@oIVT9X~U#c{iIUp0a-ZOz`@x-<m&f
zER+8p-m2zMvP;Z8YNlE|ck0s#hG#FY{ag}ZcVm&f*bM)5mv4T~zx3?U?k_>*JYUsT
ze>od-d5fP-&$H>j_By}av%Yj%?EC9g%iHzsUMW2HO21t>^?T{PHCfxj{$_tYz4g_x
zmuwd<DBoC~7uegP)o(PR{CDYVnFptQ=V|QX|EkaQlu_&3s#|>rr~I2zTBWs5Pv%RB
zPez7#$gz-3ZpBZ}7SFp;c2?Pcm$uksOB3Ds|9k7=-o5@Z|Nr6KT{R2MFTR*zw13Ji
znb2#qleTp3l3f3V+288#hFyO7U$jodpWX3Dc-I!Sox6kA1ip1Uo@#d|=gZ_vRcGp|
zSN}D<7X50%iwuw3&mCU*NG(dvov>@(T&~&~MyC|*^A3GBeY{b>KVrpg_hmL>ukY9W
z&aJx|aCS@TS?$^yt$$NuZ$_o>W=cKSV=!&|<|Abmr)H-`8_Jg|%g>ZPTWWtcetECl
zG6shFlg_J`NxgM^d3>JhWkri~hxNA}ng422l)sO$&|1@~yD!r|hrQnX=TrE}59iwA
z&wh>g>*gQs{VeM8v&gvg!dZJ}w#~L(_uJ~q@%BT>rFCpq`6Bn0a0$mo>aj{)K3nTl
ztNJxRe1=Ho@>`+_IrVR*=iWQ2U%B+3z`uWYdE-9Z6g<AP@cC&?o=UF1*IJ*ZIj<L+
zP#9VE$G`qp@UMsLe;2*T%jdOowwu3a{nCn6D+`v$w?CODb^rNCH-Db&@Va&1H~PH)
z!u|fxEf@X+Om1oWt>-RHzxV&yPm{g(_MLyec^&H!lgo?KsxIEX>Q!BN>siI}ua|n$
z7EXM}uwhDa*XK}`2OHPFdir}ov{lKTzxPky@#gVb>GivO_u`1%{wI>=zU(&+duN?;
zded8B8>c7bA1BoE{D1Js)=GHps@#2w-mS?SirHi;?HKfq`8hv#jh}KfO~ZWl`gQ9*
zvG3l-d|$ri<kMBn%P+gm%AQ_VQpdlm(_wqu-Ire{SMl$5*dD7pb)WC~>+`4lThMOK
z!tJ|o@k9pm%gy4m?93l;E?Io@%U#{?FC(`5URGaw@rFN>%bL|;xAr9QNOB$!-?e_b
zg|+|ns?3<U;<x(@qE9k9JvP{un6rFFnt95$-To(j@14PwykMQqHOY;irt2DH9+ya}
zjI}9|JFH)7AH65IVQyhf2itz#8*Rx?-mnzTWQ*AU^KAaN1<L2u*1hMiyJUUNzUIe-
z&lebEm&<hQ?C-GMcktf@&O1F%1s;aD&wu{>j?^WM<)(tuzhuAk`Fh}3h&W5K@7meB
zrEPE3uU`4hYrR0!(s1K@9;Lb)az4dLh>K<WZkK;2VY{-sZf)G!ANl%q*Y=+6f0?#O
z`I7YCX)O&WpR31)Fr2YoJ3a3!WBQtpv#Q=dSf;UDVwH$Q>*wZGlUUWxI*ZJl+IWOb
zWB#q_yRV(=eO9~Q>ixFzecs|T_3EA|Ru>%U;>vxwDc}=dncb^d?mV)RpBY&y?P{bu
z8QGof8f_R47*_pc<Z@trac_V9{6>xoc6EOuSY>~)#TXxbHCOOf%$Wt3J+3$SIn8o*
zuc|hFz4Nv-qi^=+pI-X!ud^QZX3SWXS-!aV%ky24`(kQz4NjZyy-~kR@L5p*EZ=MS
zUyU`Q?q_n`%DS@Wa>wzHkE9vjANDE;wGp~;|Mfz_y*g373&Rsv*G)Me;o%z@`O{7@
z_ob=9KL!T&NYGk<!x`rm+Wsk@?Wx1Qz4x>8kL6dE{&DY(kkFXhF6R8?c4fwr<Q~5#
z)6E{Z%1TMrHXLN-Xf$olW!S@E)xh>*hwc&f3ydxdo(}x8S=^Z1EORzb?D~82sQF7b
z=2Ie4v3eJLS$6Go_<pz5Vfpq$OLq73b{u*9?V?-otDyC#CA0tME;4_vGjkQ|lIOEu
ze{rg^={9@nv;Tm`N1Iu-dmg*-g>Tpua5CHcqUh>Xk|F#5eO>?gV^Tfi=EJ+~w{ClP
z+J36u(~Hy8r8gQ#%q?a-!uR3sN55$Hs+neIH@+2^Zt}@6fxF;?;)co}f4|>Pw{>7&
zFvH3AsL|ei)!+5{n+mI3zrQ_ao>R4CaWbQv@?DSJcg>|zcVyZnHmdwij@`S=I<_+_
z*8E!836I5BL%vL8*eclj-FA(-&YseV3+7+E{VM!IT2;lPr!oFgnfqd1x9*?ZyUkx>
zqsi1D{%@-vo!fPrTR!`1`@0KA-#=P-|EPZDiO1I;2%hdb(h{+=t?0k`r#B6mPdwds
zWkwY2(v?$6j!a^D$|PWSF1XL$^n&57pI5XRzIde9Iy~#{;IZnh>Tb<j|AN=*@x|FD
z*Y!6zJzpMkY*)(NJ4|M>30H3QF&e}-ud_&7%iOoC>FK1BMVV!*Gp8Qzzn`@vNot7<
z)4pR5%1qaG+qbmu&Dg#E-9bTZsXkU0tC!9Pca`<nWFCKEEt{&WdrE%N!MN#rej2Lv
z-W4jjAo+C9I_3zUr7TGf$1~XF<;0~MIvz|syWp_h;yXzTmrppqWyN(5_Wt=-qF<hx
z`Z>vHidj|st03jOpPt(<4bR`_mv3@b`T2ui*6+(o?LPHCO?(}3_naX2;j#;bOD2|e
z)LfQve?Fo3Ugq%=*Do!*nlwG|*c8F14`+vYJ3HUoGv|W*Z!ftC9}=S;3Cumnyg=gY
z1lEu>cXw=1U@KtYmt1zE!l(0-L?chir#S&9+&jN^XFT4#{?gIU?=O|#|KF{X{QraM
zFV}yX75y7;TP3+JzW8!RpV5-rO{I4v4E?2cCEqUi7h+hJyLwt%*strZ+PRgw>;?>T
zf8JmzVOY%~+who~L-~b<_yr@UzwgTTdvmXS_(bMO?SXyGf6M-9R@{(F+HRHPs(djb
z`j)7bNc4SP$9Z3-cYHjXdiBx;um5}(bJ}<COnF*WrOW=}#+8x`>6bQpnRcw%ovr-(
z=DN?{X8)P~An@EmBYWwMi-JP(yp{bs{<IbS-yRlu`GWG_DTeco7@gQ{v%GV${}aww
zlOG%JO>6aA@o^E)pKTNQE->ooUz6@Wdw*8!=lv%AMcJv(MAK3%-tGwLDf#Rr^+n>r
zbR)y}UxJVR5X!xNX`12rf0OeUe_iT+duiY4LY@T-@<l$aM?NN79^bDbXLnQnuI-Nb
ztLK*G9E|9k(O0yr{OAeyxly;a1Rq<fTVrze<+>VQHYvuvD{cq8f4Tfg=~VH`<vN$Y
zN@smOD0TYW(PNfV)_wFXmT9lgu*i(^iN3cb_~=HT93dMm3tyQ^!}!UaTg)O~u-sOC
zTQynDhE@NT?&;U3o!4EFeUx2#q^q@j>Dg{pCvE0GL2A$Dmc_jKelmKy?y<JYttBVB
zD^G8~`0LW*xk1mTAM5@%L67<0^;ZotU;YO32CneG#deBm_os__EA#JdEY#VarhF+Y
z^LKKb?tQn{&i!Q*)g3-BUwG%g>EHRQ;$A<xD_S0Unrm9+Y1uCCy+-n<P4hEiHVbCP
zzMS-YPpyK#!h3(`%zv}OPL;p>Q~ZQ?9@kwCU*miKdwHXly<a*jWbMU=S6(dR?Okm9
z+HKb5XM)zTpH2vS&1|chA@_EgmH)wFNyEI)vNM-I;GM^Hy6r{LDbdv@U97j3EOE8Y
z>#!@-OrCwj&s}o4cbxCOC9Zc@@Xt;7sTZgG^TYbDazV3Wq|bj@w`tz2@-HDCSF_Gt
zl{&B`$nIHo=4F}Nem!SbA64bGy`1%OlKAIo@146hEv<Pnv5zsbUTN-@m$BdGHHH5Y
ztbO@N)>QOX#n}&vQIS7q^k?2ST&ufHrckrEBW>T;IqGE%;y*9!&RCi=yJkuGOPSM$
z>m;-4qL=li{|c~Y^KHGzxxByATm7_++3t*IQwy&cukQ|<ys_p9@4~Go-KsLJKY2`N
z{5gxSDA(q4c2)n=ssFD2xp7~6OF@asvh146ITp{7ciqX@{72F1sq$lsTPACJ7UzEU
zlB%2@duqy_8S1??@=;TCpDLfPb5Oov_cHjiSM|m{ihJzr-np-d@R+IQ-@zvB_1pYd
z&Fif%qx3!{?|LGCdb9k}y_4=u(?6|x-%b1Fwwtc+AKiQ|p&Wm?GAGh>_VFoO*~3gE
zF1O3{PPLt)_jcMR(>n3B`ue9zmwLyY*}j$8Xlcys9Pz!~&y9ASdif}EH_x`nODDH~
z^m(s!de6DlPU1mg@0h%*=I`O&mNRqxMcu~|lFLu;IM%%->Xw<;yw4NuTdt`|zg+ru
zpUsm~4Quri`-^U$GFtz2)di*_hF4_z|Jc2}<aTEJ(p>XT-ce61-%8xvR?;h;>TOra
wWMsN?l342Qiklf9mxmpTKYdT&{(t$emt$v5ZCk#Mfq{X+)78&qol`;+05n_CGXMYp

literal 0
HcmV?d00001

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 000000000..4cec930b7
--- /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 0000000000000000000000000000000000000000..8568fac888efad1e5a91d2074e9553a0e1396344
GIT binary patch
literal 7291
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
V#oH@87#J8BJYD@<);T3K0RWcJ;y3^R

literal 0
HcmV?d00001

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 000000000..caf2980b7
--- /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 0000000000000000000000000000000000000000..bd856b377d67a52937a4d7625887e8f7f73d7d17
GIT binary patch
literal 6914
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

literal 0
HcmV?d00001

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 000000000..18f5c8bc3
--- /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 0000000000000000000000000000000000000000..7881102a1884a6bfb7b78c94bdf25d142e2b69ec
GIT binary patch
literal 3874
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
X$)u&fb}wgOU|{fc^>bP0l+XkKSz(M%

literal 0
HcmV?d00001

diff --git a/archipack/pyqtree.py b/archipack/pyqtree.py
new file mode 100644
index 000000000..80b757275
--- /dev/null
+++ b/archipack/pyqtree.py
@@ -0,0 +1,187 @@
+# -*- coding:utf-8 -*-
+
+# <pep8 compliant>
+
+"""
+# 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)
-- 
GitLab