diff --git a/archipack/__init__.py b/archipack/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..79ac9879faf262b7a524f0fc6771f9787262e198
--- /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 0000000000000000000000000000000000000000..912e3cb85eb1cbefa5ec97b3e109458011ad26f3
--- /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 0000000000000000000000000000000000000000..a171532cf1200490a4dbab2ddb654015c31153cc
--- /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 0000000000000000000000000000000000000000..f29c44d14150e40c4d5aa6075f295d6a99da1e54
--- /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 0000000000000000000000000000000000000000..961b516ebd9542bdeb6f06abd2c791ce80d803e9
--- /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 0000000000000000000000000000000000000000..24917c16956fac2ba6ef5bebe4fe1c8e115a749f
--- /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 0000000000000000000000000000000000000000..fc1f8c03f51d056894aac78a969c7aacb1dc1eea
--- /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 0000000000000000000000000000000000000000..852fe2b6eda41d6cee545d4abbd54e84c05611e7
--- /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 0000000000000000000000000000000000000000..65b295bfde9cab16d0e6b2007e78bd5bc8c3b7d4
--- /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 0000000000000000000000000000000000000000..c3e0fc24b997b504d68d09ff6fabd59aeea080c7
--- /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 0000000000000000000000000000000000000000..18ae43e5e719cd692d8a10fc02be6e857ffd8a7f
--- /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 0000000000000000000000000000000000000000..886029ba94d70d599b6063a6721a109df7de954e
--- /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 0000000000000000000000000000000000000000..c5fe94460619edf9da64abb5d70392eff02f3516
--- /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 0000000000000000000000000000000000000000..d81a6839aba73831d310bff1def4f18053118a96
--- /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 0000000000000000000000000000000000000000..3d86d4d8b04787c486e4e654797da1c635653749
--- /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 0000000000000000000000000000000000000000..d29c167846403f10f0c72619a23745ffd6470e59
--- /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 0000000000000000000000000000000000000000..936a07d82ecfadbdd20e907853136ca54ad0285c
--- /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 0000000000000000000000000000000000000000..c7e7f02c14d8a9156a1fb77fd85b760acb222650
--- /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 0000000000000000000000000000000000000000..b8056daab217247310c1e6e7039c66e29389e8ef
--- /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 0000000000000000000000000000000000000000..5adf92c2ec004e4967ab55467eff024a439039df
--- /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 0000000000000000000000000000000000000000..4944f59feb9c621b35f2caa834dd3421f9328080
--- /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 0000000000000000000000000000000000000000..2be559474d5f44eb67c6658adb9cc86a5a910596
--- /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 0000000000000000000000000000000000000000..cf71261073ee6462182cbde2203828f9622ecce4
--- /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 0000000000000000000000000000000000000000..b49f46831427080f9bfb30baea7d6ec018e2fb09
--- /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
Binary files /dev/null and b/archipack/icons/archipack.png differ
diff --git a/archipack/icons/detect.png b/archipack/icons/detect.png
new file mode 100644
index 0000000000000000000000000000000000000000..9c10f604ff74be249665798ab03ec4ccfdd01fb1
Binary files /dev/null and b/archipack/icons/detect.png differ
diff --git a/archipack/icons/door.png b/archipack/icons/door.png
new file mode 100644
index 0000000000000000000000000000000000000000..dc975d4d0d904cd54f0e3d76176143dcaf726555
Binary files /dev/null and b/archipack/icons/door.png differ
diff --git a/archipack/icons/fence.png b/archipack/icons/fence.png
new file mode 100644
index 0000000000000000000000000000000000000000..f32dcc7eb4a2bb12c4c6029d626cd2326288a849
Binary files /dev/null and b/archipack/icons/fence.png differ
diff --git a/archipack/icons/floor.png b/archipack/icons/floor.png
new file mode 100644
index 0000000000000000000000000000000000000000..1590c335ae63191921ed621d2f8844ded23c8230
Binary files /dev/null and b/archipack/icons/floor.png differ
diff --git a/archipack/icons/polygons.png b/archipack/icons/polygons.png
new file mode 100644
index 0000000000000000000000000000000000000000..b434068cf201cc7dedda0f02bc59dd2fecee1e98
Binary files /dev/null and b/archipack/icons/polygons.png differ
diff --git a/archipack/icons/selection.png b/archipack/icons/selection.png
new file mode 100644
index 0000000000000000000000000000000000000000..e4a7e82bb49b6bbb0a21214c8cbd5915aecfcf83
Binary files /dev/null and b/archipack/icons/selection.png differ
diff --git a/archipack/icons/slab.png b/archipack/icons/slab.png
new file mode 100644
index 0000000000000000000000000000000000000000..292ea52efa442feef358e2da68a6468829558646
Binary files /dev/null and b/archipack/icons/slab.png differ
diff --git a/archipack/icons/stair.png b/archipack/icons/stair.png
new file mode 100644
index 0000000000000000000000000000000000000000..5ce4d7054f3bb1e15262bbf51e014ca2fff11208
Binary files /dev/null and b/archipack/icons/stair.png differ
diff --git a/archipack/icons/truss.png b/archipack/icons/truss.png
new file mode 100644
index 0000000000000000000000000000000000000000..72ca91579c1c627ec088935dfa44e05cb0534fdf
Binary files /dev/null and b/archipack/icons/truss.png differ
diff --git a/archipack/icons/union.png b/archipack/icons/union.png
new file mode 100644
index 0000000000000000000000000000000000000000..11b114724061362dae1ecb94b11aa3ac9eede927
Binary files /dev/null and b/archipack/icons/union.png differ
diff --git a/archipack/icons/wall.png b/archipack/icons/wall.png
new file mode 100644
index 0000000000000000000000000000000000000000..1335a590b819908c8180f1b38e1e762349590dcc
Binary files /dev/null and b/archipack/icons/wall.png differ
diff --git a/archipack/icons/window.png b/archipack/icons/window.png
new file mode 100644
index 0000000000000000000000000000000000000000..74be2e0ee154c9994b03593b4e0652ff108b0037
Binary files /dev/null and b/archipack/icons/window.png differ
diff --git a/archipack/materialutils.py b/archipack/materialutils.py
new file mode 100644
index 0000000000000000000000000000000000000000..92497924b958919c6741032d3de44eb99d39e935
--- /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 0000000000000000000000000000000000000000..c8898fe56f51d492a7f503260e3beab2f4c53bbf
--- /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
Binary files /dev/null and b/archipack/presets/archipack_door/160x200_dual.png differ
diff --git a/archipack/presets/archipack_door/160x200_dual.py b/archipack/presets/archipack_door/160x200_dual.py
new file mode 100644
index 0000000000000000000000000000000000000000..7a9e5ebc2d72283079fcd2c741f5ea3b22960cb3
--- /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
Binary files /dev/null and b/archipack/presets/archipack_door/400x240_garage.png differ
diff --git a/archipack/presets/archipack_door/400x240_garage.py b/archipack/presets/archipack_door/400x240_garage.py
new file mode 100644
index 0000000000000000000000000000000000000000..2060cc3b19c5aec9638d3ce7bff08bc197008329
--- /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
Binary files /dev/null and b/archipack/presets/archipack_door/80x200.png differ
diff --git a/archipack/presets/archipack_door/80x200.py b/archipack/presets/archipack_door/80x200.py
new file mode 100644
index 0000000000000000000000000000000000000000..a29e3ddc01eaf234730f195feb5e500b1e53066d
--- /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
Binary files /dev/null and b/archipack/presets/archipack_fence/glass_panels.png differ
diff --git a/archipack/presets/archipack_fence/glass_panels.py b/archipack/presets/archipack_fence/glass_panels.py
new file mode 100644
index 0000000000000000000000000000000000000000..2d150b71e52114e797b06ff3cb7c283359615ade
--- /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
Binary files /dev/null and b/archipack/presets/archipack_fence/inox_glass_concrete.png differ
diff --git a/archipack/presets/archipack_fence/inox_glass_concrete.py b/archipack/presets/archipack_fence/inox_glass_concrete.py
new file mode 100644
index 0000000000000000000000000000000000000000..80d3fb6c3e3e1734cd5b2e3cf93c669f692df0ca
--- /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
Binary files /dev/null and b/archipack/presets/archipack_fence/metal.png differ
diff --git a/archipack/presets/archipack_fence/metal.py b/archipack/presets/archipack_fence/metal.py
new file mode 100644
index 0000000000000000000000000000000000000000..5e7ecbfddcce207b0be0595b4d494ed358d36b53
--- /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
Binary files /dev/null and b/archipack/presets/archipack_fence/metal_glass.png differ
diff --git a/archipack/presets/archipack_fence/metal_glass.py b/archipack/presets/archipack_fence/metal_glass.py
new file mode 100644
index 0000000000000000000000000000000000000000..fb5149cb7f06f8ab7140eff5eb002c712d7fad19
--- /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
Binary files /dev/null and b/archipack/presets/archipack_fence/wood.png differ
diff --git a/archipack/presets/archipack_fence/wood.py b/archipack/presets/archipack_fence/wood.py
new file mode 100644
index 0000000000000000000000000000000000000000..9a9a42d95b1961e5f42e566187b353c94a178e11
--- /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
Binary files /dev/null and b/archipack/presets/archipack_floor/herringbone_50x10.png differ
diff --git a/archipack/presets/archipack_floor/herringbone_50x10.py b/archipack/presets/archipack_floor/herringbone_50x10.py
new file mode 100644
index 0000000000000000000000000000000000000000..a1f196ef215717e3b3313d2e3e4d6d3e508636f2
--- /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
Binary files /dev/null and b/archipack/presets/archipack_floor/herringbone_p_50x10.png differ
diff --git a/archipack/presets/archipack_floor/herringbone_p_50x10.py b/archipack/presets/archipack_floor/herringbone_p_50x10.py
new file mode 100644
index 0000000000000000000000000000000000000000..088a22e41df0390495e66c6fc0b634db58e94823
--- /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
Binary files /dev/null and b/archipack/presets/archipack_floor/parquet_15x3.png differ
diff --git a/archipack/presets/archipack_floor/parquet_15x3.py b/archipack/presets/archipack_floor/parquet_15x3.py
new file mode 100644
index 0000000000000000000000000000000000000000..5711c93ae59574dd441849e13745f70d20fc7fce
--- /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
Binary files /dev/null and b/archipack/presets/archipack_floor/planks_200x20.png differ
diff --git a/archipack/presets/archipack_floor/planks_200x20.py b/archipack/presets/archipack_floor/planks_200x20.py
new file mode 100644
index 0000000000000000000000000000000000000000..bbea2e66cafe6ec60974e8bd928a34ad3d7a4fa4
--- /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
Binary files /dev/null and b/archipack/presets/archipack_floor/tiles_15x15.png differ
diff --git a/archipack/presets/archipack_floor/tiles_15x15.py b/archipack/presets/archipack_floor/tiles_15x15.py
new file mode 100644
index 0000000000000000000000000000000000000000..d3d244f90b078cab3306a0a4120fb7dfdaecd501
--- /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
Binary files /dev/null and b/archipack/presets/archipack_floor/tiles_60x30.png differ
diff --git a/archipack/presets/archipack_floor/tiles_60x30.py b/archipack/presets/archipack_floor/tiles_60x30.py
new file mode 100644
index 0000000000000000000000000000000000000000..f8b66129fba5bc6d373ae6a08176d7a05194a7ab
--- /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
Binary files /dev/null and b/archipack/presets/archipack_floor/tiles_hex_10x10.png differ
diff --git a/archipack/presets/archipack_floor/tiles_hex_10x10.py b/archipack/presets/archipack_floor/tiles_hex_10x10.py
new file mode 100644
index 0000000000000000000000000000000000000000..01086dc8b02d5443dd0c18acfc61a7abb1e0c5de
--- /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
Binary files /dev/null and b/archipack/presets/archipack_floor/tiles_l+ms_30x30_15x15.png differ
diff --git a/archipack/presets/archipack_floor/tiles_l+ms_30x30_15x15.py b/archipack/presets/archipack_floor/tiles_l+ms_30x30_15x15.py
new file mode 100644
index 0000000000000000000000000000000000000000..3ee45a2d44d881c2bc1e717fafe5a400f5ddffc0
--- /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
Binary files /dev/null and b/archipack/presets/archipack_floor/tiles_l+s_30x30_15x15.png differ
diff --git a/archipack/presets/archipack_floor/tiles_l+s_30x30_15x15.py b/archipack/presets/archipack_floor/tiles_l+s_30x30_15x15.py
new file mode 100644
index 0000000000000000000000000000000000000000..8f4253fe6a360fb198a43ad00372582b0e2e8b54
--- /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
Binary files /dev/null and b/archipack/presets/archipack_stair/i_wood_over_concrete.png differ
diff --git a/archipack/presets/archipack_stair/i_wood_over_concrete.py b/archipack/presets/archipack_stair/i_wood_over_concrete.py
new file mode 100644
index 0000000000000000000000000000000000000000..53b605cfbb684a97bea8934be0d3c84285b0f9ea
--- /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
Binary files /dev/null and b/archipack/presets/archipack_stair/l_wood_over_concrete.png differ
diff --git a/archipack/presets/archipack_stair/l_wood_over_concrete.py b/archipack/presets/archipack_stair/l_wood_over_concrete.py
new file mode 100644
index 0000000000000000000000000000000000000000..d4fc1344a54ccd723bdcb01aad8a5764c427b8b4
--- /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
Binary files /dev/null and b/archipack/presets/archipack_stair/o_wood_over_concrete.png differ
diff --git a/archipack/presets/archipack_stair/o_wood_over_concrete.py b/archipack/presets/archipack_stair/o_wood_over_concrete.py
new file mode 100644
index 0000000000000000000000000000000000000000..586aa9901b5414f45cb610201759040da053fcc8
--- /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
Binary files /dev/null and b/archipack/presets/archipack_stair/u_wood_over_concrete.png differ
diff --git a/archipack/presets/archipack_stair/u_wood_over_concrete.py b/archipack/presets/archipack_stair/u_wood_over_concrete.py
new file mode 100644
index 0000000000000000000000000000000000000000..b523dcde046cda153df4bfe6f168b56b5b28b713
--- /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
Binary files /dev/null and b/archipack/presets/archipack_window/120x110_flat_2.png differ
diff --git a/archipack/presets/archipack_window/120x110_flat_2.py b/archipack/presets/archipack_window/120x110_flat_2.py
new file mode 100644
index 0000000000000000000000000000000000000000..7c7dcf9b7940d6812633ec59dd4e114eb6eb47de
--- /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
Binary files /dev/null and b/archipack/presets/archipack_window/120x110_flat_2_elliptic.png differ
diff --git a/archipack/presets/archipack_window/120x110_flat_2_elliptic.py b/archipack/presets/archipack_window/120x110_flat_2_elliptic.py
new file mode 100644
index 0000000000000000000000000000000000000000..312f72995e0c941b001dec6937ce4e056249de55
--- /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
Binary files /dev/null and b/archipack/presets/archipack_window/120x110_flat_2_oblique.png differ
diff --git a/archipack/presets/archipack_window/120x110_flat_2_oblique.py b/archipack/presets/archipack_window/120x110_flat_2_oblique.py
new file mode 100644
index 0000000000000000000000000000000000000000..010b40731b12d3a0120031de59af25537f391329
--- /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
Binary files /dev/null and b/archipack/presets/archipack_window/120x110_flat_2_round.png differ
diff --git a/archipack/presets/archipack_window/120x110_flat_2_round.py b/archipack/presets/archipack_window/120x110_flat_2_round.py
new file mode 100644
index 0000000000000000000000000000000000000000..3d0fd325f286cb2422191e89cbb12d082fe4364a
--- /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
Binary files /dev/null and b/archipack/presets/archipack_window/180x110_flat_3.png differ
diff --git a/archipack/presets/archipack_window/180x110_flat_3.py b/archipack/presets/archipack_window/180x110_flat_3.py
new file mode 100644
index 0000000000000000000000000000000000000000..3ae2748a11ec1f444529d0a53f604af404dac9b7
--- /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
Binary files /dev/null and b/archipack/presets/archipack_window/180x210_flat_3.png differ
diff --git a/archipack/presets/archipack_window/180x210_flat_3.py b/archipack/presets/archipack_window/180x210_flat_3.py
new file mode 100644
index 0000000000000000000000000000000000000000..df26b7a54ef845702e719ece6945ec380cd0f44b
--- /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
Binary files /dev/null and b/archipack/presets/archipack_window/180x210_rail_2.png differ
diff --git a/archipack/presets/archipack_window/180x210_rail_2.py b/archipack/presets/archipack_window/180x210_rail_2.py
new file mode 100644
index 0000000000000000000000000000000000000000..d9f2cb89f3ee9c85b7ee9b9f71ffe1ad3100d28c
--- /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
Binary files /dev/null and b/archipack/presets/archipack_window/240x210_rail_3.png differ
diff --git a/archipack/presets/archipack_window/240x210_rail_3.py b/archipack/presets/archipack_window/240x210_rail_3.py
new file mode 100644
index 0000000000000000000000000000000000000000..4cec930b708c14b7108d879ba370a42784ba56a2
--- /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
Binary files /dev/null and b/archipack/presets/archipack_window/80x80_flat_1.png differ
diff --git a/archipack/presets/archipack_window/80x80_flat_1.py b/archipack/presets/archipack_window/80x80_flat_1.py
new file mode 100644
index 0000000000000000000000000000000000000000..caf2980b75d0e9c2be35d319e2364684e9d8c991
--- /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
Binary files /dev/null and b/archipack/presets/archipack_window/80x80_flat_1_circle.png differ
diff --git a/archipack/presets/archipack_window/80x80_flat_1_circle.py b/archipack/presets/archipack_window/80x80_flat_1_circle.py
new file mode 100644
index 0000000000000000000000000000000000000000..18f5c8bc39993a75d8f6d2dd2148c011090a6ea1
--- /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
Binary files /dev/null and b/archipack/presets/missing.png differ
diff --git a/archipack/pyqtree.py b/archipack/pyqtree.py
new file mode 100644
index 0000000000000000000000000000000000000000..80b7572751fff34593e657ef2f4ae34f19257219
--- /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)