Welcome to mirror list, hosted at ThFree Co, Russian Federation.

git.blender.org/blender-addons.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorStephen Leger <stephen@3dservices.ch>2017-07-22 14:25:28 +0300
committerStephen Leger <stephen@3dservices.ch>2017-07-22 14:26:04 +0300
commitc1ab9b4b9c6c0226f8d7789b92efda9b0f33cfd1 (patch)
tree37d5a97c758fa9af48d1dfb5428edd72072d882a
parent5638a8783502138500912061dde0e8ee476d7fca (diff)
archipack: T52120 release to official
-rw-r--r--archipack/__init__.py646
-rw-r--r--archipack/archipack_2d.py893
-rw-r--r--archipack/archipack_autoboolean.py678
-rw-r--r--archipack/archipack_door.py1847
-rw-r--r--archipack/archipack_fence.py1782
-rw-r--r--archipack/archipack_floor.py1190
-rw-r--r--archipack/archipack_gl.py1228
-rw-r--r--archipack/archipack_handle.py178
-rw-r--r--archipack/archipack_keymaps.py108
-rw-r--r--archipack/archipack_manipulator.py2446
-rw-r--r--archipack/archipack_object.py237
-rw-r--r--archipack/archipack_polylib.py2274
-rw-r--r--archipack/archipack_preset.py578
-rw-r--r--archipack/archipack_reference_point.py368
-rw-r--r--archipack/archipack_rendering.py529
-rw-r--r--archipack/archipack_slab.py1505
-rw-r--r--archipack/archipack_snap.py309
-rw-r--r--archipack/archipack_stair.py2849
-rw-r--r--archipack/archipack_truss.py380
-rw-r--r--archipack/archipack_wall.py137
-rw-r--r--archipack/archipack_wall2.py2220
-rw-r--r--archipack/archipack_window.py2098
-rw-r--r--archipack/bitarray.py97
-rw-r--r--archipack/bmesh_utils.py249
-rw-r--r--archipack/icons/archipack.pngbin0 -> 1364 bytes
-rw-r--r--archipack/icons/detect.pngbin0 -> 281 bytes
-rw-r--r--archipack/icons/door.pngbin0 -> 414 bytes
-rw-r--r--archipack/icons/fence.pngbin0 -> 1779 bytes
-rw-r--r--archipack/icons/floor.pngbin0 -> 1457 bytes
-rw-r--r--archipack/icons/polygons.pngbin0 -> 242 bytes
-rw-r--r--archipack/icons/selection.pngbin0 -> 1021 bytes
-rw-r--r--archipack/icons/slab.pngbin0 -> 1620 bytes
-rw-r--r--archipack/icons/stair.pngbin0 -> 1486 bytes
-rw-r--r--archipack/icons/truss.pngbin0 -> 1462 bytes
-rw-r--r--archipack/icons/union.pngbin0 -> 1102 bytes
-rw-r--r--archipack/icons/wall.pngbin0 -> 637 bytes
-rw-r--r--archipack/icons/window.pngbin0 -> 579 bytes
-rw-r--r--archipack/materialutils.py169
-rw-r--r--archipack/panel.py715
-rw-r--r--archipack/presets/archipack_door/160x200_dual.pngbin0 -> 10252 bytes
-rw-r--r--archipack/presets/archipack_door/160x200_dual.py23
-rw-r--r--archipack/presets/archipack_door/400x240_garage.pngbin0 -> 10492 bytes
-rw-r--r--archipack/presets/archipack_door/400x240_garage.py23
-rw-r--r--archipack/presets/archipack_door/80x200.pngbin0 -> 7840 bytes
-rw-r--r--archipack/presets/archipack_door/80x200.py23
-rw-r--r--archipack/presets/archipack_fence/glass_panels.pngbin0 -> 7106 bytes
-rw-r--r--archipack/presets/archipack_fence/glass_panels.py67
-rw-r--r--archipack/presets/archipack_fence/inox_glass_concrete.pngbin0 -> 7835 bytes
-rw-r--r--archipack/presets/archipack_fence/inox_glass_concrete.py64
-rw-r--r--archipack/presets/archipack_fence/metal.pngbin0 -> 10234 bytes
-rw-r--r--archipack/presets/archipack_fence/metal.py67
-rw-r--r--archipack/presets/archipack_fence/metal_glass.pngbin0 -> 9582 bytes
-rw-r--r--archipack/presets/archipack_fence/metal_glass.py67
-rw-r--r--archipack/presets/archipack_fence/wood.pngbin0 -> 13183 bytes
-rw-r--r--archipack/presets/archipack_fence/wood.py67
-rw-r--r--archipack/presets/archipack_floor/herringbone_50x10.pngbin0 -> 11148 bytes
-rw-r--r--archipack/presets/archipack_floor/herringbone_50x10.py34
-rw-r--r--archipack/presets/archipack_floor/herringbone_p_50x10.pngbin0 -> 10924 bytes
-rw-r--r--archipack/presets/archipack_floor/herringbone_p_50x10.py34
-rw-r--r--archipack/presets/archipack_floor/parquet_15x3.pngbin0 -> 13445 bytes
-rw-r--r--archipack/presets/archipack_floor/parquet_15x3.py34
-rw-r--r--archipack/presets/archipack_floor/planks_200x20.pngbin0 -> 11644 bytes
-rw-r--r--archipack/presets/archipack_floor/planks_200x20.py34
-rw-r--r--archipack/presets/archipack_floor/tiles_15x15.pngbin0 -> 12939 bytes
-rw-r--r--archipack/presets/archipack_floor/tiles_15x15.py34
-rw-r--r--archipack/presets/archipack_floor/tiles_60x30.pngbin0 -> 11379 bytes
-rw-r--r--archipack/presets/archipack_floor/tiles_60x30.py34
-rw-r--r--archipack/presets/archipack_floor/tiles_hex_10x10.pngbin0 -> 13663 bytes
-rw-r--r--archipack/presets/archipack_floor/tiles_hex_10x10.py34
-rw-r--r--archipack/presets/archipack_floor/tiles_l+ms_30x30_15x15.pngbin0 -> 12511 bytes
-rw-r--r--archipack/presets/archipack_floor/tiles_l+ms_30x30_15x15.py34
-rw-r--r--archipack/presets/archipack_floor/tiles_l+s_30x30_15x15.pngbin0 -> 11631 bytes
-rw-r--r--archipack/presets/archipack_floor/tiles_l+s_30x30_15x15.py34
-rw-r--r--archipack/presets/archipack_stair/i_wood_over_concrete.pngbin0 -> 15606 bytes
-rw-r--r--archipack/presets/archipack_stair/i_wood_over_concrete.py117
-rw-r--r--archipack/presets/archipack_stair/l_wood_over_concrete.pngbin0 -> 18279 bytes
-rw-r--r--archipack/presets/archipack_stair/l_wood_over_concrete.py155
-rw-r--r--archipack/presets/archipack_stair/o_wood_over_concrete.pngbin0 -> 13886 bytes
-rw-r--r--archipack/presets/archipack_stair/o_wood_over_concrete.py136
-rw-r--r--archipack/presets/archipack_stair/u_wood_over_concrete.pngbin0 -> 18165 bytes
-rw-r--r--archipack/presets/archipack_stair/u_wood_over_concrete.py155
-rw-r--r--archipack/presets/archipack_window/120x110_flat_2.pngbin0 -> 8410 bytes
-rw-r--r--archipack/presets/archipack_window/120x110_flat_2.py50
-rw-r--r--archipack/presets/archipack_window/120x110_flat_2_elliptic.pngbin0 -> 8593 bytes
-rw-r--r--archipack/presets/archipack_window/120x110_flat_2_elliptic.py58
-rw-r--r--archipack/presets/archipack_window/120x110_flat_2_oblique.pngbin0 -> 7969 bytes
-rw-r--r--archipack/presets/archipack_window/120x110_flat_2_oblique.py50
-rw-r--r--archipack/presets/archipack_window/120x110_flat_2_round.pngbin0 -> 8571 bytes
-rw-r--r--archipack/presets/archipack_window/120x110_flat_2_round.py58
-rw-r--r--archipack/presets/archipack_window/180x110_flat_3.pngbin0 -> 9492 bytes
-rw-r--r--archipack/presets/archipack_window/180x110_flat_3.py50
-rw-r--r--archipack/presets/archipack_window/180x210_flat_3.pngbin0 -> 10314 bytes
-rw-r--r--archipack/presets/archipack_window/180x210_flat_3.py50
-rw-r--r--archipack/presets/archipack_window/180x210_rail_2.pngbin0 -> 9362 bytes
-rw-r--r--archipack/presets/archipack_window/180x210_rail_2.py50
-rw-r--r--archipack/presets/archipack_window/240x210_rail_3.pngbin0 -> 10360 bytes
-rw-r--r--archipack/presets/archipack_window/240x210_rail_3.py50
-rw-r--r--archipack/presets/archipack_window/80x80_flat_1.pngbin0 -> 7291 bytes
-rw-r--r--archipack/presets/archipack_window/80x80_flat_1.py50
-rw-r--r--archipack/presets/archipack_window/80x80_flat_1_circle.pngbin0 -> 6914 bytes
-rw-r--r--archipack/presets/archipack_window/80x80_flat_1_circle.py58
-rw-r--r--archipack/presets/missing.pngbin0 -> 3874 bytes
-rw-r--r--archipack/pyqtree.py187
103 files changed, 27691 insertions, 0 deletions
diff --git a/archipack/__init__.py b/archipack/__init__.py
new file mode 100644
index 00000000..79ac9879
--- /dev/null
+++ b/archipack/__init__.py
@@ -0,0 +1,646 @@
+# -*- coding:utf-8 -*-
+
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+# <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 00000000..912e3cb8
--- /dev/null
+++ b/archipack/archipack_2d.py
@@ -0,0 +1,893 @@
+# -*- coding:utf-8 -*-
+
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110- 1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+# <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 00000000..a171532c
--- /dev/null
+++ b/archipack/archipack_autoboolean.py
@@ -0,0 +1,678 @@
+# -*- coding:utf-8 -*-
+
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+# <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 00000000..f29c44d1
--- /dev/null
+++ b/archipack/archipack_door.py
@@ -0,0 +1,1847 @@
+# -*- coding:utf-8 -*-
+
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+# <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 00000000..961b516e
--- /dev/null
+++ b/archipack/archipack_fence.py
@@ -0,0 +1,1782 @@
+# -*- coding:utf-8 -*-
+
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110- 1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+# <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 00000000..24917c16
--- /dev/null
+++ b/archipack/archipack_floor.py
@@ -0,0 +1,1190 @@
+# -*- coding:utf-8 -*-
+
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110- 1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+# <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 00000000..fc1f8c03
--- /dev/null
+++ b/archipack/archipack_gl.py
@@ -0,0 +1,1228 @@
+# -*- coding:utf-8 -*-
+
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+# <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 00000000..852fe2b6
--- /dev/null
+++ b/archipack/archipack_handle.py
@@ -0,0 +1,178 @@
+# -*- coding:utf-8 -*-
+
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+# <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 00000000..65b295bf
--- /dev/null
+++ b/archipack/archipack_keymaps.py
@@ -0,0 +1,108 @@
+# -*- coding:utf-8 -*-
+
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110- 1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+# <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 00000000..c3e0fc24
--- /dev/null
+++ b/archipack/archipack_manipulator.py
@@ -0,0 +1,2446 @@
+# -*- coding:utf-8 -*-
+
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+# <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 00000000..18ae43e5
--- /dev/null
+++ b/archipack/archipack_object.py
@@ -0,0 +1,237 @@
+# -*- coding:utf-8 -*-
+
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110- 1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+# <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 00000000..886029ba
--- /dev/null
+++ b/archipack/archipack_polylib.py
@@ -0,0 +1,2274 @@
+# -*- coding:utf-8 -*-
+
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110- 1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+# <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 00000000..c5fe9446
--- /dev/null
+++ b/archipack/archipack_preset.py
@@ -0,0 +1,578 @@
+# -*- coding:utf-8 -*-
+
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110- 1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+# <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 00000000..d81a6839
--- /dev/null
+++ b/archipack/archipack_reference_point.py
@@ -0,0 +1,368 @@
+# -*- coding:utf-8 -*-
+
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110- 1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+# <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 00000000..3d86d4d8
--- /dev/null
+++ b/archipack/archipack_rendering.py
@@ -0,0 +1,529 @@
+# -*- coding:utf-8 -*-
+
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110- 1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+# <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 00000000..d29c1678
--- /dev/null
+++ b/archipack/archipack_slab.py
@@ -0,0 +1,1505 @@
+# -*- coding:utf-8 -*-
+
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110- 1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+# <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 00000000..936a07d8
--- /dev/null
+++ b/archipack/archipack_snap.py
@@ -0,0 +1,309 @@
+# -*- coding:utf-8 -*-
+
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110- 1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+# <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 00000000..c7e7f02c
--- /dev/null
+++ b/archipack/archipack_stair.py
@@ -0,0 +1,2849 @@
+# -*- coding:utf-8 -*-
+
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110- 1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+# <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 00000000..b8056daa
--- /dev/null
+++ b/archipack/archipack_truss.py
@@ -0,0 +1,380 @@
+# -*- coding:utf-8 -*-
+
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110- 1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+# <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 00000000..5adf92c2
--- /dev/null
+++ b/archipack/archipack_wall.py
@@ -0,0 +1,137 @@
+# -*- coding:utf-8 -*-
+
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110- 1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+# <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 00000000..4944f59f
--- /dev/null
+++ b/archipack/archipack_wall2.py
@@ -0,0 +1,2220 @@
+# -*- coding:utf-8 -*-
+
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110- 1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+# <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 00000000..2be55947
--- /dev/null
+++ b/archipack/archipack_window.py
@@ -0,0 +1,2098 @@
+# -*- coding:utf-8 -*-
+
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110- 1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+# <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 00000000..cf712610
--- /dev/null
+++ b/archipack/bitarray.py
@@ -0,0 +1,97 @@
+
+import array
+
+# -*- coding:utf-8 -*-
+
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110- 1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+# <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 00000000..b49f4683
--- /dev/null
+++ b/archipack/bmesh_utils.py
@@ -0,0 +1,249 @@
+# -*- coding:utf-8 -*-
+
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110- 1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+# <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 00000000..92503c82
--- /dev/null
+++ b/archipack/icons/archipack.png
Binary files differ
diff --git a/archipack/icons/detect.png b/archipack/icons/detect.png
new file mode 100644
index 00000000..9c10f604
--- /dev/null
+++ b/archipack/icons/detect.png
Binary files differ
diff --git a/archipack/icons/door.png b/archipack/icons/door.png
new file mode 100644
index 00000000..dc975d4d
--- /dev/null
+++ b/archipack/icons/door.png
Binary files differ
diff --git a/archipack/icons/fence.png b/archipack/icons/fence.png
new file mode 100644
index 00000000..f32dcc7e
--- /dev/null
+++ b/archipack/icons/fence.png
Binary files differ
diff --git a/archipack/icons/floor.png b/archipack/icons/floor.png
new file mode 100644
index 00000000..1590c335
--- /dev/null
+++ b/archipack/icons/floor.png
Binary files differ
diff --git a/archipack/icons/polygons.png b/archipack/icons/polygons.png
new file mode 100644
index 00000000..b434068c
--- /dev/null
+++ b/archipack/icons/polygons.png
Binary files differ
diff --git a/archipack/icons/selection.png b/archipack/icons/selection.png
new file mode 100644
index 00000000..e4a7e82b
--- /dev/null
+++ b/archipack/icons/selection.png
Binary files differ
diff --git a/archipack/icons/slab.png b/archipack/icons/slab.png
new file mode 100644
index 00000000..292ea52e
--- /dev/null
+++ b/archipack/icons/slab.png
Binary files differ
diff --git a/archipack/icons/stair.png b/archipack/icons/stair.png
new file mode 100644
index 00000000..5ce4d705
--- /dev/null
+++ b/archipack/icons/stair.png
Binary files differ
diff --git a/archipack/icons/truss.png b/archipack/icons/truss.png
new file mode 100644
index 00000000..72ca9157
--- /dev/null
+++ b/archipack/icons/truss.png
Binary files differ
diff --git a/archipack/icons/union.png b/archipack/icons/union.png
new file mode 100644
index 00000000..11b11472
--- /dev/null
+++ b/archipack/icons/union.png
Binary files differ
diff --git a/archipack/icons/wall.png b/archipack/icons/wall.png
new file mode 100644
index 00000000..1335a590
--- /dev/null
+++ b/archipack/icons/wall.png
Binary files differ
diff --git a/archipack/icons/window.png b/archipack/icons/window.png
new file mode 100644
index 00000000..74be2e0e
--- /dev/null
+++ b/archipack/icons/window.png
Binary files differ
diff --git a/archipack/materialutils.py b/archipack/materialutils.py
new file mode 100644
index 00000000..92497924
--- /dev/null
+++ b/archipack/materialutils.py
@@ -0,0 +1,169 @@
+# -*- coding:utf-8 -*-
+
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110- 1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+# <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 00000000..c8898fe5
--- /dev/null
+++ b/archipack/panel.py
@@ -0,0 +1,715 @@
+# -*- coding:utf-8 -*-
+
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110- 1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+# <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 00000000..ef4fac84
--- /dev/null
+++ b/archipack/presets/archipack_door/160x200_dual.png
Binary files differ
diff --git a/archipack/presets/archipack_door/160x200_dual.py b/archipack/presets/archipack_door/160x200_dual.py
new file mode 100644
index 00000000..7a9e5ebc
--- /dev/null
+++ b/archipack/presets/archipack_door/160x200_dual.py
@@ -0,0 +1,23 @@
+import bpy
+d = bpy.context.active_object.data.archipack_door[0]
+
+d.handle = 'BOTH'
+d.panels_distrib = 'REGULAR'
+d.direction = 0
+d.frame_y = 0.029999999329447746
+d.door_y = 0.019999999552965164
+d.flip = False
+d.panels_y = 3
+d.frame_x = 0.10000000149011612
+d.model = 2
+d.door_offset = 0.0
+d.x = 1.600000023841858
+d.z = 2.0
+d.hole_margin = 0.10000000149011612
+d.panel_border = 0.12999999523162842
+d.panels_x = 2
+d.panel_spacing = 0.10000000149011612
+d.chanfer = 0.004999999888241291
+d.panel_bottom = 0.17000000178813934
+d.n_panels = 2
+d.y = 0.20000000298023224
diff --git a/archipack/presets/archipack_door/400x240_garage.png b/archipack/presets/archipack_door/400x240_garage.png
new file mode 100644
index 00000000..660b1d70
--- /dev/null
+++ b/archipack/presets/archipack_door/400x240_garage.png
Binary files differ
diff --git a/archipack/presets/archipack_door/400x240_garage.py b/archipack/presets/archipack_door/400x240_garage.py
new file mode 100644
index 00000000..2060cc3b
--- /dev/null
+++ b/archipack/presets/archipack_door/400x240_garage.py
@@ -0,0 +1,23 @@
+import bpy
+d = bpy.context.active_object.data.archipack_door[0]
+
+d.handle = 'NONE'
+d.panels_distrib = 'REGULAR'
+d.direction = 0
+d.frame_y = 0.029999999329447746
+d.door_y = 0.019999999552965164
+d.flip = False
+d.panels_y = 1
+d.frame_x = 0.10000000149011612
+d.model = 1
+d.door_offset = 0.0
+d.x = 4.0
+d.z = 2.4000000953674316
+d.hole_margin = 0.10000000149011612
+d.panel_border = 0.0010000000474974513
+d.panels_x = 24
+d.panel_spacing = 0.0010000000474974513
+d.chanfer = 0.004999999888241291
+d.panel_bottom = 0.0
+d.n_panels = 1
+d.y = 0.20000000298023224
diff --git a/archipack/presets/archipack_door/80x200.png b/archipack/presets/archipack_door/80x200.png
new file mode 100644
index 00000000..e2bf6f5c
--- /dev/null
+++ b/archipack/presets/archipack_door/80x200.png
Binary files differ
diff --git a/archipack/presets/archipack_door/80x200.py b/archipack/presets/archipack_door/80x200.py
new file mode 100644
index 00000000..a29e3ddc
--- /dev/null
+++ b/archipack/presets/archipack_door/80x200.py
@@ -0,0 +1,23 @@
+import bpy
+d = bpy.context.active_object.data.archipack_door[0]
+
+d.handle = 'BOTH'
+d.panels_distrib = 'REGULAR'
+d.direction = 0
+d.frame_y = 0.029999999329447746
+d.door_y = 0.019999999552965164
+d.flip = False
+d.panels_y = 1
+d.frame_x = 0.10000000149011612
+d.model = 0
+d.door_offset = 0.0
+d.x = 0.800000011920929
+d.z = 2.0
+d.hole_margin = 0.10000000149011612
+d.panel_border = 0.20000000298023224
+d.panels_x = 1
+d.panel_spacing = 0.10000000149011612
+d.chanfer = 0.004999999888241291
+d.panel_bottom = 0.0
+d.n_panels = 1
+d.y = 0.20000000298023224
diff --git a/archipack/presets/archipack_fence/glass_panels.png b/archipack/presets/archipack_fence/glass_panels.png
new file mode 100644
index 00000000..4478afa6
--- /dev/null
+++ b/archipack/presets/archipack_fence/glass_panels.png
Binary files differ
diff --git a/archipack/presets/archipack_fence/glass_panels.py b/archipack/presets/archipack_fence/glass_panels.py
new file mode 100644
index 00000000..2d150b71
--- /dev/null
+++ b/archipack/presets/archipack_fence/glass_panels.py
@@ -0,0 +1,67 @@
+import bpy
+d = bpy.context.active_object.data.archipack_fence[0]
+
+d.rail_expand = True
+d.shape = 'RECTANGLE'
+d.rail = False
+d.radius = 0.699999988079071
+d.user_defined_resolution = 12
+d.handrail = False
+d.handrail_x = 0.07999999076128006
+d.subs_alt = 0.10000000149011612
+d.handrail_extend = 0.0
+d.idmat_subs = '0'
+d.rail_alt = (0.20000000298023224, 0.699999988079071, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0)
+d.subs_x = 0.029999999329447746
+d.subs_offset_x = 0.0
+d.handrail_y = 0.03999999910593033
+d.user_defined_subs_enable = True
+d.rail_x = (0.030000001192092896, 0.029999999329447746, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806)
+d.post_y = 0.009999999776482582
+d.handrail_alt = 1.0
+d.subs_y = 0.09999999403953552
+d.idmat_panel = '2'
+d.panel_expand = True
+d.panel_x = 0.009999999776482582
+d.idmats_expand = True
+d.idmat_post = '0'
+d.idmat_handrail = '1'
+d.user_defined_post_enable = True
+d.x_offset = 0.0
+d.subs_z = 0.7999998927116394
+d.subs_bottom = 'STEP'
+d.post_expand = True
+d.subs_expand = False
+d.rail_offset = (-0.009999999776482582, -0.009999999776482582, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0)
+d.post = False
+d.handrail_radius = 0.029999999329447746
+d.rail_n = 2
+d.rail_mat.clear()
+item_sub_1 = d.rail_mat.add()
+item_sub_1.name = ''
+item_sub_1.index = '0'
+item_sub_1 = d.rail_mat.add()
+item_sub_1.name = ''
+item_sub_1.index = '0'
+d.parts_expand = False
+d.angle_limit = 0.39269909262657166
+d.post_spacing = 1.5
+d.handrail_expand = True
+d.subs = False
+d.handrail_slice_right = True
+d.panel_alt = 0.0
+d.user_defined_subs = ''
+d.panel_dist = 0.009999999776482582
+d.handrail_slice = True
+d.panel = True
+d.subs_spacing = 0.07000000774860382
+d.panel_z = 1.0
+d.handrail_profil = 'CIRCLE'
+d.handrail_offset = 0.0
+d.da = 1.5707963705062866
+d.post_z = 1.0
+d.rail_z = (0.07000000029802322, 0.07000000029802322, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806)
+d.post_x = 0.03999999910593033
+d.user_defined_post = ''
+d.panel_offset_x = 0.0
+d.post_alt = 0.0
diff --git a/archipack/presets/archipack_fence/inox_glass_concrete.png b/archipack/presets/archipack_fence/inox_glass_concrete.png
new file mode 100644
index 00000000..e9031497
--- /dev/null
+++ b/archipack/presets/archipack_fence/inox_glass_concrete.png
Binary files differ
diff --git a/archipack/presets/archipack_fence/inox_glass_concrete.py b/archipack/presets/archipack_fence/inox_glass_concrete.py
new file mode 100644
index 00000000..80d3fb6c
--- /dev/null
+++ b/archipack/presets/archipack_fence/inox_glass_concrete.py
@@ -0,0 +1,64 @@
+import bpy
+d = bpy.context.active_object.data.archipack_fence[0]
+
+d.rail_expand = True
+d.shape = 'RECTANGLE'
+d.rail = True
+d.radius = 0.699999988079071
+d.user_defined_resolution = 12
+d.handrail = True
+d.handrail_x = 0.07999999076128006
+d.subs_alt = 0.10000000149011612
+d.handrail_extend = 0.0
+d.idmat_subs = '0'
+d.rail_alt = (-0.2999999523162842, 0.699999988079071, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0)
+d.subs_x = 0.029999999329447746
+d.subs_offset_x = 0.0
+d.handrail_y = 0.03999999910593033
+d.user_defined_subs_enable = True
+d.rail_x = (0.19999998807907104, 0.029999999329447746, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806)
+d.post_y = 0.009999999776482582
+d.handrail_alt = 1.0
+d.subs_y = 0.09999999403953552
+d.idmat_panel = '2'
+d.panel_expand = True
+d.panel_x = 0.009999999776482582
+d.idmats_expand = True
+d.idmat_post = '0'
+d.idmat_handrail = '1'
+d.user_defined_post_enable = True
+d.x_offset = 0.0
+d.subs_z = 0.7999998927116394
+d.subs_bottom = 'STEP'
+d.post_expand = True
+d.subs_expand = False
+d.rail_offset = (-0.04999999701976776, -0.009999999776482582, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0)
+d.post = False
+d.handrail_radius = 0.029999999329447746
+d.rail_n = 1
+d.rail_mat.clear()
+item_sub_1 = d.rail_mat.add()
+item_sub_1.name = ''
+item_sub_1.index = '0'
+d.parts_expand = False
+d.angle_limit = 0.39269909262657166
+d.post_spacing = 1.5
+d.handrail_expand = True
+d.subs = False
+d.handrail_slice_right = True
+d.panel_alt = 0.0
+d.user_defined_subs = ''
+d.panel_dist = 0.009999999776482582
+d.handrail_slice = True
+d.panel = True
+d.subs_spacing = 0.07000000774860382
+d.panel_z = 1.0
+d.handrail_profil = 'CIRCLE'
+d.handrail_offset = 0.0
+d.da = 1.5707963705062866
+d.post_z = 1.0
+d.rail_z = (0.3199999928474426, 0.07000000029802322, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806)
+d.post_x = 0.03999999910593033
+d.user_defined_post = ''
+d.panel_offset_x = 0.0
+d.post_alt = 0.0
diff --git a/archipack/presets/archipack_fence/metal.png b/archipack/presets/archipack_fence/metal.png
new file mode 100644
index 00000000..b6a24339
--- /dev/null
+++ b/archipack/presets/archipack_fence/metal.png
Binary files differ
diff --git a/archipack/presets/archipack_fence/metal.py b/archipack/presets/archipack_fence/metal.py
new file mode 100644
index 00000000..5e7ecbfd
--- /dev/null
+++ b/archipack/presets/archipack_fence/metal.py
@@ -0,0 +1,67 @@
+import bpy
+d = bpy.context.active_object.data.archipack_fence[0]
+
+d.rail_expand = True
+d.shape = 'RECTANGLE'
+d.rail = True
+d.radius = 0.699999988079071
+d.user_defined_resolution = 12
+d.handrail = True
+d.handrail_x = 0.03999999910593033
+d.subs_alt = 0.15000000596046448
+d.handrail_extend = 0.10000000149011612
+d.idmat_subs = '1'
+d.rail_alt = (0.15000000596046448, 0.8500000238418579, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0)
+d.subs_x = 0.019999999552965164
+d.subs_offset_x = 0.0
+d.handrail_y = 0.03999999910593033
+d.user_defined_subs_enable = True
+d.rail_x = (0.030000001192092896, 0.029999999329447746, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806)
+d.post_y = 0.03999999910593033
+d.handrail_alt = 1.0
+d.subs_y = 0.019999999552965164
+d.idmat_panel = '2'
+d.panel_expand = False
+d.panel_x = 0.009999999776482582
+d.idmats_expand = False
+d.idmat_post = '1'
+d.idmat_handrail = '0'
+d.user_defined_post_enable = True
+d.x_offset = 0.0
+d.subs_z = 0.699999988079071
+d.subs_bottom = 'STEP'
+d.post_expand = False
+d.subs_expand = True
+d.rail_offset = (0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0)
+d.post = True
+d.handrail_radius = 0.019999999552965164
+d.rail_n = 2
+d.rail_mat.clear()
+item_sub_1 = d.rail_mat.add()
+item_sub_1.name = ''
+item_sub_1.index = '1'
+item_sub_1 = d.rail_mat.add()
+item_sub_1.name = ''
+item_sub_1.index = '1'
+d.parts_expand = False
+d.angle_limit = 0.39269909262657166
+d.post_spacing = 1.5
+d.handrail_expand = False
+d.subs = True
+d.handrail_slice_right = True
+d.panel_alt = 0.20999997854232788
+d.user_defined_subs = ''
+d.panel_dist = 0.03999999910593033
+d.handrail_slice = True
+d.panel = False
+d.subs_spacing = 0.10000000149011612
+d.panel_z = 0.6000000238418579
+d.handrail_profil = 'SQUARE'
+d.handrail_offset = 0.0
+d.da = 1.5707963705062866
+d.post_z = 1.0
+d.rail_z = (0.019999999552965164, 0.019999999552965164, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806)
+d.post_x = 0.03999999910593033
+d.user_defined_post = ''
+d.panel_offset_x = 0.0
+d.post_alt = 0.0
diff --git a/archipack/presets/archipack_fence/metal_glass.png b/archipack/presets/archipack_fence/metal_glass.png
new file mode 100644
index 00000000..16020ec4
--- /dev/null
+++ b/archipack/presets/archipack_fence/metal_glass.png
Binary files differ
diff --git a/archipack/presets/archipack_fence/metal_glass.py b/archipack/presets/archipack_fence/metal_glass.py
new file mode 100644
index 00000000..fb5149cb
--- /dev/null
+++ b/archipack/presets/archipack_fence/metal_glass.py
@@ -0,0 +1,67 @@
+import bpy
+d = bpy.context.active_object.data.archipack_fence[0]
+
+d.rail_expand = True
+d.shape = 'RECTANGLE'
+d.rail = True
+d.radius = 0.699999988079071
+d.user_defined_resolution = 12
+d.handrail = True
+d.handrail_x = 0.03999999910593033
+d.subs_alt = 0.0
+d.handrail_extend = 0.10000000149011612
+d.idmat_subs = '1'
+d.rail_alt = (0.15000000596046448, 0.8500000238418579, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0)
+d.subs_x = 0.019999999552965164
+d.subs_offset_x = 0.0
+d.handrail_y = 0.03999999910593033
+d.user_defined_subs_enable = True
+d.rail_x = (0.030000001192092896, 0.029999999329447746, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806)
+d.post_y = 0.03999999910593033
+d.handrail_alt = 1.0
+d.subs_y = 0.019999999552965164
+d.idmat_panel = '2'
+d.panel_expand = False
+d.panel_x = 0.009999999776482582
+d.idmats_expand = False
+d.idmat_post = '1'
+d.idmat_handrail = '0'
+d.user_defined_post_enable = True
+d.x_offset = 0.0
+d.subs_z = 1.0
+d.subs_bottom = 'STEP'
+d.post_expand = True
+d.subs_expand = False
+d.rail_offset = (0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0)
+d.post = True
+d.handrail_radius = 0.019999999552965164
+d.rail_n = 2
+d.rail_mat.clear()
+item_sub_1 = d.rail_mat.add()
+item_sub_1.name = ''
+item_sub_1.index = '1'
+item_sub_1 = d.rail_mat.add()
+item_sub_1.name = ''
+item_sub_1.index = '1'
+d.parts_expand = False
+d.angle_limit = 0.39269909262657166
+d.post_spacing = 1.5
+d.handrail_expand = False
+d.subs = False
+d.handrail_slice_right = True
+d.panel_alt = 0.20999997854232788
+d.user_defined_subs = ''
+d.panel_dist = 0.03999999910593033
+d.handrail_slice = True
+d.panel = True
+d.subs_spacing = 0.10000000149011612
+d.panel_z = 0.6000000238418579
+d.handrail_profil = 'SQUARE'
+d.handrail_offset = 0.0
+d.da = 1.5707963705062866
+d.post_z = 1.0
+d.rail_z = (0.019999999552965164, 0.019999999552965164, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806)
+d.post_x = 0.03999999910593033
+d.user_defined_post = ''
+d.panel_offset_x = 0.0
+d.post_alt = 0.0
diff --git a/archipack/presets/archipack_fence/wood.png b/archipack/presets/archipack_fence/wood.png
new file mode 100644
index 00000000..a1706f29
--- /dev/null
+++ b/archipack/presets/archipack_fence/wood.png
Binary files differ
diff --git a/archipack/presets/archipack_fence/wood.py b/archipack/presets/archipack_fence/wood.py
new file mode 100644
index 00000000..9a9a42d9
--- /dev/null
+++ b/archipack/presets/archipack_fence/wood.py
@@ -0,0 +1,67 @@
+import bpy
+d = bpy.context.active_object.data.archipack_fence[0]
+
+d.user_defined_post = ''
+d.handrail_offset = 0.0
+d.post_spacing = 1.5
+d.post_z = 1.0
+d.idmats_expand = True
+d.rail_alt = (0.20000000298023224, 0.699999988079071, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0)
+d.idmat_handrail = '0'
+d.post_alt = 0.0
+d.handrail_expand = True
+d.panel_x = 0.009999999776482582
+d.idmat_panel = '2'
+d.rail_z = (0.07000000029802322, 0.07000000029802322, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806)
+d.subs_y = 0.09999999403953552
+d.handrail_radius = 0.019999999552965164
+d.handrail_extend = 0.10000000149011612
+d.subs_alt = 0.10000000149011612
+d.idmat_subs = '0'
+d.handrail_y = 0.03999999910593033
+d.user_defined_post_enable = True
+d.rail = True
+d.handrail_profil = 'SQUARE'
+d.post_x = 0.059999994933605194
+d.handrail = True
+d.da = 1.5707963705062866
+d.user_defined_subs_enable = True
+d.subs_expand = True
+d.shape = 'RECTANGLE'
+d.angle_limit = 0.39269909262657166
+d.panel_alt = 0.20999997854232788
+d.post_expand = True
+d.subs_bottom = 'STEP'
+d.handrail_slice_right = True
+d.handrail_alt = 1.0
+d.subs_z = 0.7999998927116394
+d.user_defined_subs = ''
+d.rail_x = (0.030000001192092896, 0.029999999329447746, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806)
+d.parts_expand = False
+d.idmat_post = '0'
+d.panel_offset_x = 0.0
+d.rail_n = 2
+d.panel_z = 0.6000000238418579
+d.handrail_x = 0.07999999076128006
+d.subs_spacing = 0.14000000059604645
+d.post = True
+d.rail_mat.clear()
+item_sub_1 = d.rail_mat.add()
+item_sub_1.name = ''
+item_sub_1.index = '0'
+item_sub_1 = d.rail_mat.add()
+item_sub_1.name = ''
+item_sub_1.index = '0'
+d.handrail_slice = True
+d.panel = False
+d.x_offset = 0.0
+d.rail_expand = True
+d.rail_offset = (0.009999999776482582, 0.009999999776482582, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0)
+d.panel_dist = 0.03999999910593033
+d.post_y = 0.059999994933605194
+d.subs = True
+d.user_defined_resolution = 12
+d.subs_x = 0.029999999329447746
+d.radius = 0.699999988079071
+d.subs_offset_x = 0.0
+d.panel_expand = False
diff --git a/archipack/presets/archipack_floor/herringbone_50x10.png b/archipack/presets/archipack_floor/herringbone_50x10.png
new file mode 100644
index 00000000..b6e7fe56
--- /dev/null
+++ b/archipack/presets/archipack_floor/herringbone_50x10.png
Binary files differ
diff --git a/archipack/presets/archipack_floor/herringbone_50x10.py b/archipack/presets/archipack_floor/herringbone_50x10.py
new file mode 100644
index 00000000..a1f196ef
--- /dev/null
+++ b/archipack/presets/archipack_floor/herringbone_50x10.py
@@ -0,0 +1,34 @@
+import bpy
+d = bpy.context.active_object.data.archipack_floor[0]
+
+d.space_l = 0.004999999888241291
+d.is_width_vary = False
+d.offset_vary = 47.810237884521484
+d.is_ran_thickness = False
+d.b_length = 2.0
+d.t_length = 0.30000001192092896
+d.space_w = 0.004999999888241291
+d.t_width_s = 0.10000000149011612
+d.b_length_s = 0.5
+d.is_grout = False
+d.tile_types = '24'
+d.offset = 50.0
+d.width_vary = 50.0
+d.spacing = 0.0010000000474974513
+d.is_offset = True
+d.is_bevel = False
+d.is_random_offset = True
+d.bevel_amo = 0.001500000013038516
+d.thickness = 0.019999999552965164
+d.bevel_res = 1
+d.max_boards = 2
+d.b_width = 0.10000000149011612
+d.length_vary = 50.0
+d.ran_thickness = 50.0
+d.is_mat_vary = True
+d.hb_direction = '1'
+d.mat_vary = 3
+d.num_boards = 5
+d.t_width = 0.30000001192092896
+d.grout_depth = 0.0010000003967434168
+d.is_length_vary = False
diff --git a/archipack/presets/archipack_floor/herringbone_p_50x10.png b/archipack/presets/archipack_floor/herringbone_p_50x10.png
new file mode 100644
index 00000000..1a2b2370
--- /dev/null
+++ b/archipack/presets/archipack_floor/herringbone_p_50x10.png
Binary files differ
diff --git a/archipack/presets/archipack_floor/herringbone_p_50x10.py b/archipack/presets/archipack_floor/herringbone_p_50x10.py
new file mode 100644
index 00000000..088a22e4
--- /dev/null
+++ b/archipack/presets/archipack_floor/herringbone_p_50x10.py
@@ -0,0 +1,34 @@
+import bpy
+d = bpy.context.active_object.data.archipack_floor[0]
+
+d.space_l = 0.004999999888241291
+d.is_width_vary = False
+d.offset_vary = 47.810237884521484
+d.is_ran_thickness = False
+d.b_length = 2.0
+d.t_length = 0.30000001192092896
+d.space_w = 0.004999999888241291
+d.t_width_s = 0.10000000149011612
+d.b_length_s = 0.5
+d.is_grout = False
+d.tile_types = '23'
+d.offset = 50.0
+d.width_vary = 50.0
+d.spacing = 0.0010000000474974513
+d.is_offset = True
+d.is_bevel = False
+d.is_random_offset = True
+d.bevel_amo = 0.001500000013038516
+d.thickness = 0.019999999552965164
+d.bevel_res = 1
+d.max_boards = 2
+d.b_width = 0.10000000149011612
+d.length_vary = 50.0
+d.ran_thickness = 50.0
+d.is_mat_vary = True
+d.hb_direction = '1'
+d.mat_vary = 3
+d.num_boards = 5
+d.t_width = 0.30000001192092896
+d.grout_depth = 0.0010000003967434168
+d.is_length_vary = False
diff --git a/archipack/presets/archipack_floor/parquet_15x3.png b/archipack/presets/archipack_floor/parquet_15x3.png
new file mode 100644
index 00000000..2b35d58b
--- /dev/null
+++ b/archipack/presets/archipack_floor/parquet_15x3.png
Binary files differ
diff --git a/archipack/presets/archipack_floor/parquet_15x3.py b/archipack/presets/archipack_floor/parquet_15x3.py
new file mode 100644
index 00000000..5711c93a
--- /dev/null
+++ b/archipack/presets/archipack_floor/parquet_15x3.py
@@ -0,0 +1,34 @@
+import bpy
+d = bpy.context.active_object.data.archipack_floor[0]
+
+d.bevel_res = 1
+d.b_width = 0.029999999329447746
+d.is_bevel = False
+d.hb_direction = '1'
+d.is_width_vary = False
+d.b_length = 2.0
+d.spacing = 0.0010000000474974513
+d.is_grout = False
+d.num_boards = 5
+d.is_length_vary = False
+d.thickness = 0.019999999552965164
+d.is_ran_thickness = False
+d.is_random_offset = True
+d.offset_vary = 47.810237884521484
+d.is_mat_vary = True
+d.tile_types = '22'
+d.length_vary = 50.0
+d.space_w = 0.004999999888241291
+d.ran_thickness = 50.0
+d.max_boards = 2
+d.t_width_s = 0.10000000149011612
+d.t_width = 0.30000001192092896
+d.t_length = 0.30000001192092896
+d.width_vary = 50.0
+d.mat_vary = 3
+d.grout_depth = 0.0010000003967434168
+d.is_offset = True
+d.space_l = 0.004999999888241291
+d.bevel_amo = 0.001500000013038516
+d.offset = 50.0
+d.b_length_s = 2.0
diff --git a/archipack/presets/archipack_floor/planks_200x20.png b/archipack/presets/archipack_floor/planks_200x20.png
new file mode 100644
index 00000000..94a49c57
--- /dev/null
+++ b/archipack/presets/archipack_floor/planks_200x20.png
Binary files differ
diff --git a/archipack/presets/archipack_floor/planks_200x20.py b/archipack/presets/archipack_floor/planks_200x20.py
new file mode 100644
index 00000000..bbea2e66
--- /dev/null
+++ b/archipack/presets/archipack_floor/planks_200x20.py
@@ -0,0 +1,34 @@
+import bpy
+d = bpy.context.active_object.data.archipack_floor[0]
+
+d.bevel_res = 1
+d.b_width = 0.2
+d.is_bevel = True
+d.hb_direction = '1'
+d.is_width_vary = False
+d.b_length = 2.0
+d.spacing = 0.002
+d.is_grout = False
+d.num_boards = 4
+d.is_length_vary = False
+d.thickness = 0.02
+d.is_ran_thickness = False
+d.is_random_offset = True
+d.offset_vary = 47.81
+d.is_mat_vary = True
+d.tile_types = '21'
+d.length_vary = 50.0
+d.space_w = 0.002
+d.ran_thickness = 50.0
+d.max_boards = 2
+d.t_width_s = 0.1
+d.t_width = 0.3
+d.t_length = 0.3
+d.width_vary = 50.0
+d.mat_vary = 3
+d.grout_depth = 0.001
+d.is_offset = True
+d.space_l = 0.002
+d.bevel_amo = 0.0015
+d.offset = 50.0
+d.b_length_s = 2.0
diff --git a/archipack/presets/archipack_floor/tiles_15x15.png b/archipack/presets/archipack_floor/tiles_15x15.png
new file mode 100644
index 00000000..2a3d8633
--- /dev/null
+++ b/archipack/presets/archipack_floor/tiles_15x15.png
Binary files differ
diff --git a/archipack/presets/archipack_floor/tiles_15x15.py b/archipack/presets/archipack_floor/tiles_15x15.py
new file mode 100644
index 00000000..d3d244f9
--- /dev/null
+++ b/archipack/presets/archipack_floor/tiles_15x15.py
@@ -0,0 +1,34 @@
+import bpy
+d = bpy.context.active_object.data.archipack_floor[0]
+
+d.b_width = 0.20000000298023224
+d.width_vary = 50.0
+d.t_width_s = 0.20000000298023224
+d.is_grout = True
+d.tile_types = '1'
+d.space_l = 0.004999999888241291
+d.is_length_vary = False
+d.hb_direction = '1'
+d.offset_vary = 50.0
+d.offset = 50.0
+d.spacing = 0.004999999888241291
+d.thickness = 0.10000000149011612
+d.bevel_res = 1
+d.is_offset = False
+d.grout_depth = 0.0010000003967434168
+d.t_width = 0.15000000596046448
+d.is_ran_thickness = False
+d.is_mat_vary = False
+d.is_random_offset = False
+d.space_w = 0.004999999888241291
+d.is_bevel = True
+d.ran_thickness = 50.0
+d.max_boards = 2
+d.t_length = 0.15000000596046448
+d.b_length_s = 2.0
+d.bevel_amo = 0.001500000013038516
+d.is_width_vary = False
+d.num_boards = 4
+d.length_vary = 50.0
+d.b_length = 0.800000011920929
+d.mat_vary = 1
diff --git a/archipack/presets/archipack_floor/tiles_60x30.png b/archipack/presets/archipack_floor/tiles_60x30.png
new file mode 100644
index 00000000..16cdf0f1
--- /dev/null
+++ b/archipack/presets/archipack_floor/tiles_60x30.png
Binary files differ
diff --git a/archipack/presets/archipack_floor/tiles_60x30.py b/archipack/presets/archipack_floor/tiles_60x30.py
new file mode 100644
index 00000000..f8b66129
--- /dev/null
+++ b/archipack/presets/archipack_floor/tiles_60x30.py
@@ -0,0 +1,34 @@
+import bpy
+d = bpy.context.active_object.data.archipack_floor[0]
+
+d.b_width = 0.20000000298023224
+d.width_vary = 50.0
+d.t_width_s = 0.20000000298023224
+d.is_grout = True
+d.tile_types = '1'
+d.space_l = 0.004999999888241291
+d.is_length_vary = False
+d.hb_direction = '1'
+d.offset_vary = 50.0
+d.offset = 50.0
+d.spacing = 0.004999999888241291
+d.thickness = 0.10000000149011612
+d.bevel_res = 1
+d.is_offset = False
+d.grout_depth = 0.0010000003967434168
+d.t_width = 0.30000001192092896
+d.is_ran_thickness = False
+d.is_mat_vary = False
+d.is_random_offset = False
+d.space_w = 0.004999999888241291
+d.is_bevel = True
+d.ran_thickness = 50.0
+d.max_boards = 2
+d.t_length = 0.6000000238418579
+d.b_length_s = 2.0
+d.bevel_amo = 0.001500000013038516
+d.is_width_vary = False
+d.num_boards = 4
+d.length_vary = 50.0
+d.b_length = 0.800000011920929
+d.mat_vary = 1
diff --git a/archipack/presets/archipack_floor/tiles_hex_10x10.png b/archipack/presets/archipack_floor/tiles_hex_10x10.png
new file mode 100644
index 00000000..4d4c8ecf
--- /dev/null
+++ b/archipack/presets/archipack_floor/tiles_hex_10x10.png
Binary files differ
diff --git a/archipack/presets/archipack_floor/tiles_hex_10x10.py b/archipack/presets/archipack_floor/tiles_hex_10x10.py
new file mode 100644
index 00000000..01086dc8
--- /dev/null
+++ b/archipack/presets/archipack_floor/tiles_hex_10x10.py
@@ -0,0 +1,34 @@
+import bpy
+d = bpy.context.active_object.data.archipack_floor[0]
+
+d.bevel_res = 1
+d.b_width = 0.20000000298023224
+d.is_bevel = True
+d.hb_direction = '1'
+d.is_width_vary = False
+d.b_length = 0.800000011920929
+d.spacing = 0.004999999888241291
+d.is_grout = True
+d.num_boards = 4
+d.is_length_vary = False
+d.thickness = 0.10000000149011612
+d.is_ran_thickness = False
+d.is_random_offset = False
+d.offset_vary = 50.0
+d.is_mat_vary = False
+d.tile_types = '4'
+d.length_vary = 50.0
+d.space_w = 0.004999999888241291
+d.ran_thickness = 50.0
+d.max_boards = 2
+d.t_width_s = 0.10000000149011612
+d.t_width = 0.30000001192092896
+d.t_length = 0.30000001192092896
+d.width_vary = 50.0
+d.mat_vary = 1
+d.grout_depth = 0.0010000003967434168
+d.is_offset = False
+d.space_l = 0.004999999888241291
+d.bevel_amo = 0.001500000013038516
+d.offset = 50.0
+d.b_length_s = 2.0
diff --git a/archipack/presets/archipack_floor/tiles_l+ms_30x30_15x15.png b/archipack/presets/archipack_floor/tiles_l+ms_30x30_15x15.png
new file mode 100644
index 00000000..07c6e266
--- /dev/null
+++ b/archipack/presets/archipack_floor/tiles_l+ms_30x30_15x15.png
Binary files differ
diff --git a/archipack/presets/archipack_floor/tiles_l+ms_30x30_15x15.py b/archipack/presets/archipack_floor/tiles_l+ms_30x30_15x15.py
new file mode 100644
index 00000000..3ee45a2d
--- /dev/null
+++ b/archipack/presets/archipack_floor/tiles_l+ms_30x30_15x15.py
@@ -0,0 +1,34 @@
+import bpy
+d = bpy.context.active_object.data.archipack_floor[0]
+
+d.bevel_res = 1
+d.b_width = 0.20000000298023224
+d.is_bevel = True
+d.hb_direction = '1'
+d.is_width_vary = False
+d.b_length = 0.800000011920929
+d.spacing = 0.004999999888241291
+d.is_grout = True
+d.num_boards = 4
+d.is_length_vary = False
+d.thickness = 0.10000000149011612
+d.is_ran_thickness = False
+d.is_random_offset = False
+d.offset_vary = 50.0
+d.is_mat_vary = False
+d.tile_types = '3'
+d.length_vary = 50.0
+d.space_w = 0.004999999888241291
+d.ran_thickness = 50.0
+d.max_boards = 2
+d.t_width_s = 0.20000000298023224
+d.t_width = 0.30000001192092896
+d.t_length = 0.30000001192092896
+d.width_vary = 50.0
+d.mat_vary = 1
+d.grout_depth = 0.0010000003967434168
+d.is_offset = False
+d.space_l = 0.004999999888241291
+d.bevel_amo = 0.001500000013038516
+d.offset = 50.0
+d.b_length_s = 2.0
diff --git a/archipack/presets/archipack_floor/tiles_l+s_30x30_15x15.png b/archipack/presets/archipack_floor/tiles_l+s_30x30_15x15.png
new file mode 100644
index 00000000..33d28657
--- /dev/null
+++ b/archipack/presets/archipack_floor/tiles_l+s_30x30_15x15.png
Binary files differ
diff --git a/archipack/presets/archipack_floor/tiles_l+s_30x30_15x15.py b/archipack/presets/archipack_floor/tiles_l+s_30x30_15x15.py
new file mode 100644
index 00000000..8f4253fe
--- /dev/null
+++ b/archipack/presets/archipack_floor/tiles_l+s_30x30_15x15.py
@@ -0,0 +1,34 @@
+import bpy
+d = bpy.context.active_object.data.archipack_floor[0]
+
+d.b_width = 0.20000000298023224
+d.width_vary = 50.0
+d.t_width_s = 0.20000000298023224
+d.is_grout = True
+d.tile_types = '2'
+d.space_l = 0.004999999888241291
+d.is_length_vary = False
+d.hb_direction = '1'
+d.offset_vary = 50.0
+d.offset = 50.0
+d.spacing = 0.004999999888241291
+d.thickness = 0.10000000149011612
+d.bevel_res = 1
+d.is_offset = False
+d.grout_depth = 0.0010000003967434168
+d.t_width = 0.30000001192092896
+d.is_ran_thickness = False
+d.is_mat_vary = False
+d.is_random_offset = False
+d.space_w = 0.004999999888241291
+d.is_bevel = True
+d.ran_thickness = 50.0
+d.max_boards = 2
+d.t_length = 0.30000001192092896
+d.b_length_s = 2.0
+d.bevel_amo = 0.001500000013038516
+d.is_width_vary = False
+d.num_boards = 4
+d.length_vary = 50.0
+d.b_length = 0.800000011920929
+d.mat_vary = 1
diff --git a/archipack/presets/archipack_stair/i_wood_over_concrete.png b/archipack/presets/archipack_stair/i_wood_over_concrete.png
new file mode 100644
index 00000000..9fb3d56c
--- /dev/null
+++ b/archipack/presets/archipack_stair/i_wood_over_concrete.png
Binary files differ
diff --git a/archipack/presets/archipack_stair/i_wood_over_concrete.py b/archipack/presets/archipack_stair/i_wood_over_concrete.py
new file mode 100644
index 00000000..53b605cf
--- /dev/null
+++ b/archipack/presets/archipack_stair/i_wood_over_concrete.py
@@ -0,0 +1,117 @@
+import bpy
+d = bpy.context.active_object.data.archipack_stair[0]
+
+d.steps_type = 'CLOSED'
+d.handrail_slice_right = True
+d.total_angle = 6.2831854820251465
+d.user_defined_subs_enable = True
+d.string_z = 0.30000001192092896
+d.nose_z = 0.029999999329447746
+d.user_defined_subs = ''
+d.idmat_step_side = '3'
+d.handrail_x = 0.03999999910593033
+d.right_post = True
+d.left_post = True
+d.width = 1.5
+d.subs_offset_x = 0.0
+d.rail_mat.clear()
+item_sub_1 = d.rail_mat.add()
+item_sub_1.name = ''
+item_sub_1.index = '4'
+d.step_depth = 0.30000001192092896
+d.rail_z = (0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806)
+d.right_subs = False
+d.left_panel = True
+d.idmat_handrail = '3'
+d.da = 3.1415927410125732
+d.post_alt = 0.0
+d.left_subs = False
+d.n_parts = 1
+d.user_defined_post_enable = True
+d.handrail_slice_left = True
+d.handrail_profil = 'SQUARE'
+d.handrail_expand = False
+d.panel_alt = 0.25
+d.post_expand = False
+d.subs_z = 1.0
+d.rail_alt = (1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0)
+d.panel_dist = 0.05000000074505806
+d.panel_expand = False
+d.x_offset = 0.0
+d.subs_expand = False
+d.idmat_post = '4'
+d.left_string = False
+d.string_alt = -0.03999999910593033
+d.handrail_y = 0.03999999910593033
+d.radius = 1.0
+d.string_expand = False
+d.post_z = 1.0
+d.idmat_top = '3'
+d.idmat_bottom = '1'
+d.parts.clear()
+item_sub_1 = d.parts.add()
+item_sub_1.name = ''
+item_sub_1.manipulators.clear()
+item_sub_2 = item_sub_1.manipulators.add()
+item_sub_2.name = ''
+item_sub_2.p0 = (0.0, 0.0, 2.700000047683716)
+item_sub_2.prop1_name = 'length'
+item_sub_2.p2 = (-1.0, 0.0, 0.0)
+item_sub_2.normal = (0.0, 0.0, 1.0)
+item_sub_2.pts_mode = 'SIZE'
+item_sub_2.p1 = (0.0, 4.0, 2.700000047683716)
+item_sub_2.prop2_name = ''
+item_sub_2.type_key = 'SIZE'
+item_sub_1.right_shape = 'RECTANGLE'
+item_sub_1.radius = 0.699999988079071
+item_sub_1.type = 'S_STAIR'
+item_sub_1.length = 4.0
+item_sub_1.left_shape = 'RECTANGLE'
+item_sub_1.da = 1.5707963705062866
+d.subs_bottom = 'STEP'
+d.user_defined_post = ''
+d.panel_offset_x = 0.0
+d.idmat_side = '1'
+d.right_string = False
+d.idmat_raise = '1'
+d.left_rail = False
+d.parts_expand = False
+d.panel_z = 0.6000000238418579
+d.bottom_z = 0.029999999329447746
+d.z_mode = 'STANDARD'
+d.panel_x = 0.009999999776482582
+d.post_x = 0.03999999910593033
+d.presets = 'STAIR_I'
+d.steps_expand = True
+d.subs_x = 0.019999999552965164
+d.subs_spacing = 0.10000000149011612
+d.left_handrail = True
+d.handrail_offset = 0.0
+d.right_rail = False
+d.idmat_panel = '5'
+d.post_offset_x = 0.019999999552965164
+d.idmat_step_front = '3'
+d.rail_n = 1
+d.string_offset = 0.0
+d.subs_y = 0.019999999552965164
+d.handrail_alt = 1.0
+d.post_corners = False
+d.rail_expand = False
+d.rail_offset = (0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0)
+d.rail_x = (0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806)
+d.left_shape = 'RECTANGLE'
+d.nose_y = 0.019999999552965164
+d.nose_type = 'STRAIGHT'
+d.handrail_extend = 0.10000000149011612
+d.idmat_string = '3'
+d.post_y = 0.03999999910593033
+d.subs_alt = 0.0
+d.right_handrail = True
+d.idmats_expand = False
+d.right_shape = 'RECTANGLE'
+d.idmat_subs = '4'
+d.handrail_radius = 0.019999999552965164
+d.right_panel = True
+d.post_spacing = 1.0
+d.string_x = 0.019999999552965164
+d.height = 2.700000047683716
diff --git a/archipack/presets/archipack_stair/l_wood_over_concrete.png b/archipack/presets/archipack_stair/l_wood_over_concrete.png
new file mode 100644
index 00000000..0e2ce6b6
--- /dev/null
+++ b/archipack/presets/archipack_stair/l_wood_over_concrete.png
Binary files differ
diff --git a/archipack/presets/archipack_stair/l_wood_over_concrete.py b/archipack/presets/archipack_stair/l_wood_over_concrete.py
new file mode 100644
index 00000000..d4fc1344
--- /dev/null
+++ b/archipack/presets/archipack_stair/l_wood_over_concrete.py
@@ -0,0 +1,155 @@
+import bpy
+d = bpy.context.active_object.data.archipack_stair[0]
+
+d.steps_type = 'CLOSED'
+d.handrail_slice_right = True
+d.total_angle = 6.2831854820251465
+d.user_defined_subs_enable = True
+d.string_z = 0.30000001192092896
+d.nose_z = 0.029999999329447746
+d.user_defined_subs = ''
+d.idmat_step_side = '3'
+d.handrail_x = 0.03999999910593033
+d.right_post = True
+d.left_post = True
+d.width = 1.5
+d.subs_offset_x = 0.0
+d.rail_mat.clear()
+item_sub_1 = d.rail_mat.add()
+item_sub_1.name = ''
+item_sub_1.index = '4'
+d.step_depth = 0.30000001192092896
+d.rail_z = (0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806)
+d.right_subs = False
+d.left_panel = True
+d.idmat_handrail = '3'
+d.da = 1.5707963705062866
+d.post_alt = 0.0
+d.left_subs = False
+d.n_parts = 3
+d.user_defined_post_enable = True
+d.handrail_slice_left = True
+d.handrail_profil = 'SQUARE'
+d.handrail_expand = False
+d.panel_alt = 0.25
+d.post_expand = False
+d.subs_z = 1.0
+d.rail_alt = (1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0)
+d.panel_dist = 0.05000000074505806
+d.panel_expand = False
+d.x_offset = 0.0
+d.subs_expand = False
+d.idmat_post = '4'
+d.left_string = False
+d.string_alt = -0.03999999910593033
+d.handrail_y = 0.03999999910593033
+d.radius = 1.0
+d.string_expand = False
+d.post_z = 1.0
+d.idmat_top = '3'
+d.idmat_bottom = '1'
+d.parts.clear()
+item_sub_1 = d.parts.add()
+item_sub_1.name = ''
+item_sub_1.manipulators.clear()
+item_sub_2 = item_sub_1.manipulators.add()
+item_sub_2.name = ''
+item_sub_2.p0 = (0.0, 0.0, 1.4040000438690186)
+item_sub_2.prop1_name = 'length'
+item_sub_2.p2 = (1.0, 0.0, 0.0)
+item_sub_2.normal = (0.0, 0.0, 1.0)
+item_sub_2.pts_mode = 'SIZE'
+item_sub_2.p1 = (0.0, 4.0, 1.4040000438690186)
+item_sub_2.prop2_name = ''
+item_sub_2.type_key = 'SIZE'
+item_sub_1.right_shape = 'RECTANGLE'
+item_sub_1.radius = 0.699999988079071
+item_sub_1.type = 'S_STAIR'
+item_sub_1.length = 4.0
+item_sub_1.left_shape = 'RECTANGLE'
+item_sub_1.da = 1.5707963705062866
+item_sub_1 = d.parts.add()
+item_sub_1.name = ''
+item_sub_1.manipulators.clear()
+item_sub_2 = item_sub_1.manipulators.add()
+item_sub_2.name = ''
+item_sub_2.p0 = (-1.0, 4.0, 1.944000005722046)
+item_sub_2.prop1_name = 'da'
+item_sub_2.p2 = (0.0, 1.0, 0.0)
+item_sub_2.normal = (0.0, 0.0, 1.0)
+item_sub_2.pts_mode = 'RADIUS'
+item_sub_2.p1 = (1.0, 0.0, 0.0)
+item_sub_2.prop2_name = 'radius'
+item_sub_2.type_key = 'ARC_ANGLE_RADIUS'
+item_sub_1.right_shape = 'RECTANGLE'
+item_sub_1.radius = 0.699999988079071
+item_sub_1.type = 'C_STAIR'
+item_sub_1.length = 2.0
+item_sub_1.left_shape = 'RECTANGLE'
+item_sub_1.da = 1.5707963705062866
+item_sub_1 = d.parts.add()
+item_sub_1.name = ''
+item_sub_1.manipulators.clear()
+item_sub_2 = item_sub_1.manipulators.add()
+item_sub_2.name = ''
+item_sub_2.p0 = (-1.0, 5.0, 2.700000047683716)
+item_sub_2.prop1_name = 'length'
+item_sub_2.p2 = (1.0, 0.0, 0.0)
+item_sub_2.normal = (0.0, 0.0, 1.0)
+item_sub_2.pts_mode = 'SIZE'
+item_sub_2.p1 = (-3.0, 5.0, 2.700000047683716)
+item_sub_2.prop2_name = ''
+item_sub_2.type_key = 'SIZE'
+item_sub_1.right_shape = 'RECTANGLE'
+item_sub_1.radius = 0.699999988079071
+item_sub_1.type = 'S_STAIR'
+item_sub_1.length = 2.0
+item_sub_1.left_shape = 'RECTANGLE'
+item_sub_1.da = 1.5707963705062866
+d.subs_bottom = 'STEP'
+d.user_defined_post = ''
+d.panel_offset_x = 0.0
+d.idmat_side = '1'
+d.right_string = False
+d.idmat_raise = '1'
+d.left_rail = False
+d.parts_expand = False
+d.panel_z = 0.6000000238418579
+d.bottom_z = 0.029999999329447746
+d.z_mode = 'STANDARD'
+d.panel_x = 0.009999999776482582
+d.post_x = 0.03999999910593033
+d.presets = 'STAIR_L'
+d.steps_expand = True
+d.subs_x = 0.019999999552965164
+d.subs_spacing = 0.10000000149011612
+d.left_handrail = True
+d.handrail_offset = 0.0
+d.right_rail = False
+d.idmat_panel = '5'
+d.post_offset_x = 0.019999999552965164
+d.idmat_step_front = '3'
+d.rail_n = 1
+d.string_offset = 0.0
+d.subs_y = 0.019999999552965164
+d.handrail_alt = 1.0
+d.post_corners = False
+d.rail_expand = False
+d.rail_offset = (0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0)
+d.rail_x = (0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806)
+d.left_shape = 'RECTANGLE'
+d.nose_y = 0.019999999552965164
+d.nose_type = 'STRAIGHT'
+d.handrail_extend = 0.10000000149011612
+d.idmat_string = '3'
+d.post_y = 0.03999999910593033
+d.subs_alt = 0.0
+d.right_handrail = True
+d.idmats_expand = False
+d.right_shape = 'RECTANGLE'
+d.idmat_subs = '4'
+d.handrail_radius = 0.019999999552965164
+d.right_panel = True
+d.post_spacing = 1.0
+d.string_x = 0.019999999552965164
+d.height = 2.700000047683716
diff --git a/archipack/presets/archipack_stair/o_wood_over_concrete.png b/archipack/presets/archipack_stair/o_wood_over_concrete.png
new file mode 100644
index 00000000..215d42b9
--- /dev/null
+++ b/archipack/presets/archipack_stair/o_wood_over_concrete.png
Binary files differ
diff --git a/archipack/presets/archipack_stair/o_wood_over_concrete.py b/archipack/presets/archipack_stair/o_wood_over_concrete.py
new file mode 100644
index 00000000..586aa990
--- /dev/null
+++ b/archipack/presets/archipack_stair/o_wood_over_concrete.py
@@ -0,0 +1,136 @@
+import bpy
+d = bpy.context.active_object.data.archipack_stair[0]
+
+d.steps_type = 'CLOSED'
+d.handrail_slice_right = True
+d.total_angle = 6.2831854820251465
+d.user_defined_subs_enable = True
+d.string_z = 0.30000001192092896
+d.nose_z = 0.029999999329447746
+d.user_defined_subs = ''
+d.idmat_step_side = '3'
+d.handrail_x = 0.03999999910593033
+d.right_post = True
+d.left_post = True
+d.width = 1.5
+d.subs_offset_x = 0.0
+d.rail_mat.clear()
+item_sub_1 = d.rail_mat.add()
+item_sub_1.name = ''
+item_sub_1.index = '4'
+d.step_depth = 0.30000001192092896
+d.rail_z = (0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806)
+d.right_subs = False
+d.left_panel = True
+d.idmat_handrail = '3'
+d.da = 3.1415927410125732
+d.post_alt = 0.0
+d.left_subs = False
+d.n_parts = 2
+d.user_defined_post_enable = True
+d.handrail_slice_left = True
+d.handrail_profil = 'SQUARE'
+d.handrail_expand = False
+d.panel_alt = 0.25
+d.post_expand = False
+d.subs_z = 1.0
+d.rail_alt = (1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0)
+d.panel_dist = 0.05000000074505806
+d.panel_expand = False
+d.x_offset = 0.0
+d.subs_expand = False
+d.idmat_post = '4'
+d.left_string = False
+d.string_alt = -0.03999999910593033
+d.handrail_y = 0.03999999910593033
+d.radius = 1.0
+d.string_expand = False
+d.post_z = 1.0
+d.idmat_top = '3'
+d.idmat_bottom = '1'
+d.parts.clear()
+item_sub_1 = d.parts.add()
+item_sub_1.name = ''
+item_sub_1.manipulators.clear()
+item_sub_2 = item_sub_1.manipulators.add()
+item_sub_2.name = ''
+item_sub_2.p0 = (-1.0, 0.0, 1.350000023841858)
+item_sub_2.prop1_name = 'da'
+item_sub_2.p2 = (-1.0, 1.2246468525851679e-16, 0.0)
+item_sub_2.normal = (0.0, 0.0, 1.0)
+item_sub_2.pts_mode = 'SIZE'
+item_sub_2.p1 = (1.0, 0.0, 0.0)
+item_sub_2.prop2_name = 'radius'
+item_sub_2.type_key = 'ARC_ANGLE_RADIUS'
+item_sub_1.right_shape = 'RECTANGLE'
+item_sub_1.radius = 0.699999988079071
+item_sub_1.type = 'D_STAIR'
+item_sub_1.length = 4.0
+item_sub_1.left_shape = 'RECTANGLE'
+item_sub_1.da = 1.5707963705062866
+item_sub_1 = d.parts.add()
+item_sub_1.name = ''
+item_sub_1.manipulators.clear()
+item_sub_2 = item_sub_1.manipulators.add()
+item_sub_2.name = ''
+item_sub_2.p0 = (-1.0, 0.0, 2.700000047683716)
+item_sub_2.prop1_name = 'da'
+item_sub_2.p2 = (1.0, -2.4492937051703357e-16, 0.0)
+item_sub_2.normal = (0.0, 0.0, 1.0)
+item_sub_2.pts_mode = 'RADIUS'
+item_sub_2.p1 = (-1.0, 1.2246468525851679e-16, 0.0)
+item_sub_2.prop2_name = 'radius'
+item_sub_2.type_key = 'ARC_ANGLE_RADIUS'
+item_sub_1.right_shape = 'RECTANGLE'
+item_sub_1.radius = 0.699999988079071
+item_sub_1.type = 'D_STAIR'
+item_sub_1.length = 2.0
+item_sub_1.left_shape = 'RECTANGLE'
+item_sub_1.da = 1.5707963705062866
+d.subs_bottom = 'STEP'
+d.user_defined_post = ''
+d.panel_offset_x = 0.0
+d.idmat_side = '1'
+d.right_string = False
+d.idmat_raise = '1'
+d.left_rail = False
+d.parts_expand = True
+d.panel_z = 0.6000000238418579
+d.bottom_z = 0.029999999329447746
+d.z_mode = 'STANDARD'
+d.panel_x = 0.009999999776482582
+d.post_x = 0.03999999910593033
+d.presets = 'STAIR_O'
+d.steps_expand = True
+d.subs_x = 0.019999999552965164
+d.subs_spacing = 0.10000000149011612
+d.left_handrail = True
+d.handrail_offset = 0.0
+d.right_rail = False
+d.idmat_panel = '5'
+d.post_offset_x = 0.019999999552965164
+d.idmat_step_front = '3'
+d.rail_n = 1
+d.string_offset = 0.0
+d.subs_y = 0.019999999552965164
+d.handrail_alt = 1.0
+d.post_corners = False
+d.rail_expand = False
+d.rail_offset = (0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0)
+d.rail_x = (0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806)
+d.left_shape = 'CIRCLE'
+d.nose_y = 0.019999999552965164
+d.nose_type = 'STRAIGHT'
+d.handrail_extend = 0.10000000149011612
+d.idmat_string = '3'
+d.post_y = 0.03999999910593033
+d.subs_alt = 0.0
+d.right_handrail = True
+d.idmats_expand = False
+d.right_shape = 'CIRCLE'
+d.idmat_subs = '4'
+d.handrail_radius = 0.019999999552965164
+d.right_panel = True
+d.post_spacing = 1.0
+d.string_x = 0.019999999552965164
+d.height = 2.700000047683716
diff --git a/archipack/presets/archipack_stair/u_wood_over_concrete.png b/archipack/presets/archipack_stair/u_wood_over_concrete.png
new file mode 100644
index 00000000..aab27159
--- /dev/null
+++ b/archipack/presets/archipack_stair/u_wood_over_concrete.png
Binary files differ
diff --git a/archipack/presets/archipack_stair/u_wood_over_concrete.py b/archipack/presets/archipack_stair/u_wood_over_concrete.py
new file mode 100644
index 00000000..b523dcde
--- /dev/null
+++ b/archipack/presets/archipack_stair/u_wood_over_concrete.py
@@ -0,0 +1,155 @@
+import bpy
+d = bpy.context.active_object.data.archipack_stair[0]
+
+d.steps_type = 'CLOSED'
+d.handrail_slice_right = True
+d.total_angle = 6.2831854820251465
+d.user_defined_subs_enable = True
+d.string_z = 0.30000001192092896
+d.nose_z = 0.029999999329447746
+d.user_defined_subs = ''
+d.idmat_step_side = '3'
+d.handrail_x = 0.03999999910593033
+d.right_post = True
+d.left_post = True
+d.width = 1.5
+d.subs_offset_x = 0.0
+d.rail_mat.clear()
+item_sub_1 = d.rail_mat.add()
+item_sub_1.name = ''
+item_sub_1.index = '4'
+d.step_depth = 0.30000001192092896
+d.rail_z = (0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806)
+d.right_subs = False
+d.left_panel = True
+d.idmat_handrail = '3'
+d.da = 3.1415927410125732
+d.post_alt = 0.0
+d.left_subs = False
+d.n_parts = 3
+d.user_defined_post_enable = True
+d.handrail_slice_left = True
+d.handrail_profil = 'SQUARE'
+d.handrail_expand = False
+d.panel_alt = 0.25
+d.post_expand = False
+d.subs_z = 1.0
+d.rail_alt = (1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0)
+d.panel_dist = 0.05000000074505806
+d.panel_expand = False
+d.x_offset = 0.0
+d.subs_expand = False
+d.idmat_post = '4'
+d.left_string = False
+d.string_alt = -0.03999999910593033
+d.handrail_y = 0.03999999910593033
+d.radius = 1.0
+d.string_expand = False
+d.post_z = 1.0
+d.idmat_top = '3'
+d.idmat_bottom = '1'
+d.parts.clear()
+item_sub_1 = d.parts.add()
+item_sub_1.name = ''
+item_sub_1.manipulators.clear()
+item_sub_2 = item_sub_1.manipulators.add()
+item_sub_2.name = ''
+item_sub_2.p0 = (0.0, 0.0, 0.7875000238418579)
+item_sub_2.prop1_name = 'length'
+item_sub_2.p2 = (1.0, 0.0, 0.0)
+item_sub_2.normal = (0.0, 0.0, 1.0)
+item_sub_2.pts_mode = 'SIZE'
+item_sub_2.p1 = (0.0, 2.0, 0.7875000238418579)
+item_sub_2.prop2_name = 'radius'
+item_sub_2.type_key = 'SIZE'
+item_sub_1.right_shape = 'RECTANGLE'
+item_sub_1.radius = 0.699999988079071
+item_sub_1.type = 'S_STAIR'
+item_sub_1.length = 2.0
+item_sub_1.left_shape = 'RECTANGLE'
+item_sub_1.da = 1.5707963705062866
+item_sub_1 = d.parts.add()
+item_sub_1.name = ''
+item_sub_1.manipulators.clear()
+item_sub_2 = item_sub_1.manipulators.add()
+item_sub_2.name = ''
+item_sub_2.p0 = (-1.0, 2.0, 1.912500023841858)
+item_sub_2.prop1_name = 'da'
+item_sub_2.p2 = (-1.0, -1.1920928955078125e-07, 0.0)
+item_sub_2.normal = (0.0, 0.0, 1.0)
+item_sub_2.pts_mode = 'RADIUS'
+item_sub_2.p1 = (1.0, 0.0, 0.0)
+item_sub_2.prop2_name = 'radius'
+item_sub_2.type_key = 'ARC_ANGLE_RADIUS'
+item_sub_1.right_shape = 'RECTANGLE'
+item_sub_1.radius = 0.699999988079071
+item_sub_1.type = 'D_STAIR'
+item_sub_1.length = 2.0
+item_sub_1.left_shape = 'RECTANGLE'
+item_sub_1.da = 1.5707963705062866
+item_sub_1 = d.parts.add()
+item_sub_1.name = ''
+item_sub_1.manipulators.clear()
+item_sub_2 = item_sub_1.manipulators.add()
+item_sub_2.name = ''
+item_sub_2.p0 = (-2.0, 1.9999998807907104, 2.700000047683716)
+item_sub_2.prop1_name = 'length'
+item_sub_2.p2 = (1.0, 0.0, 0.0)
+item_sub_2.normal = (0.0, 0.0, 1.0)
+item_sub_2.pts_mode = 'SIZE'
+item_sub_2.p1 = (-1.9999998807907104, -1.1920928955078125e-07, 2.700000047683716)
+item_sub_2.prop2_name = ''
+item_sub_2.type_key = 'SIZE'
+item_sub_1.right_shape = 'RECTANGLE'
+item_sub_1.radius = 0.699999988079071
+item_sub_1.type = 'S_STAIR'
+item_sub_1.length = 2.0
+item_sub_1.left_shape = 'RECTANGLE'
+item_sub_1.da = 1.5707963705062866
+d.subs_bottom = 'STEP'
+d.user_defined_post = ''
+d.panel_offset_x = 0.0
+d.idmat_side = '1'
+d.right_string = False
+d.idmat_raise = '1'
+d.left_rail = False
+d.parts_expand = False
+d.panel_z = 0.6000000238418579
+d.bottom_z = 0.029999999329447746
+d.z_mode = 'STANDARD'
+d.panel_x = 0.009999999776482582
+d.post_x = 0.03999999910593033
+d.presets = 'STAIR_U'
+d.steps_expand = True
+d.subs_x = 0.019999999552965164
+d.subs_spacing = 0.10000000149011612
+d.left_handrail = True
+d.handrail_offset = 0.0
+d.right_rail = False
+d.idmat_panel = '5'
+d.post_offset_x = 0.019999999552965164
+d.idmat_step_front = '3'
+d.rail_n = 1
+d.string_offset = 0.0
+d.subs_y = 0.019999999552965164
+d.handrail_alt = 1.0
+d.post_corners = False
+d.rail_expand = False
+d.rail_offset = (0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0)
+d.rail_x = (0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806, 0.05000000074505806)
+d.left_shape = 'RECTANGLE'
+d.nose_y = 0.019999999552965164
+d.nose_type = 'STRAIGHT'
+d.handrail_extend = 0.10000000149011612
+d.idmat_string = '3'
+d.post_y = 0.03999999910593033
+d.subs_alt = 0.0
+d.right_handrail = True
+d.idmats_expand = False
+d.right_shape = 'RECTANGLE'
+d.idmat_subs = '4'
+d.handrail_radius = 0.019999999552965164
+d.right_panel = True
+d.post_spacing = 1.0
+d.string_x = 0.019999999552965164
+d.height = 2.700000047683716
diff --git a/archipack/presets/archipack_window/120x110_flat_2.png b/archipack/presets/archipack_window/120x110_flat_2.png
new file mode 100644
index 00000000..25f21c0a
--- /dev/null
+++ b/archipack/presets/archipack_window/120x110_flat_2.png
Binary files differ
diff --git a/archipack/presets/archipack_window/120x110_flat_2.py b/archipack/presets/archipack_window/120x110_flat_2.py
new file mode 100644
index 00000000..7c7dcf9b
--- /dev/null
+++ b/archipack/presets/archipack_window/120x110_flat_2.py
@@ -0,0 +1,50 @@
+import bpy
+d = bpy.context.active_object.data.archipack_window[0]
+
+d.frame_y = 0.05999999865889549
+d.flip = False
+d.blind_z = 0.029999999329447746
+d.blind_open = 80.0
+d.hole_margin = 0.10000000149011612
+d.out_frame_y = 0.019999999552965164
+d.blind_y = 0.0020000000949949026
+d.in_tablet_x = 0.03999999910593033
+d.in_tablet_enable = True
+d.n_rows = 1
+d.radius = 2.5
+d.rows.clear()
+item_sub_1 = d.rows.add()
+item_sub_1.name = ''
+item_sub_1.width = (50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0)
+item_sub_1.fixed = (False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False)
+item_sub_1.auto_update = True
+item_sub_1.n_cols = 2
+item_sub_1.cols = 2
+item_sub_1.height = 1.0
+d.out_tablet_x = 0.03999999910593033
+d.out_frame = False
+d.y = 0.20000000298023224
+d.in_tablet_z = 0.029999999329447746
+d.handle_altitude = 1.399999976158142
+d.out_frame_y2 = 0.019999999552965164
+d.out_tablet_y = 0.03999999910593033
+d.in_tablet_y = 0.03999999910593033
+d.out_frame_x = 0.10000000149011612
+d.offset = 0.10000000149011612
+d.window_shape = 'RECTANGLE'
+d.frame_x = 0.05999999865889549
+d.x = 1.2000000476837158
+d.z = 1.100000023841858
+d.hole_inside_mat = 1
+d.curve_steps = 16
+d.handle_enable = True
+d.hole_outside_mat = 0
+d.out_tablet_z = 0.029999999329447746
+d.window_type = 'FLAT'
+d.angle_y = 0.0
+d.elipsis_b = 0.5
+d.out_tablet_enable = True
+d.out_frame_offset = 0.0
+d.warning = False
+d.altitude = 1.0
+d.blind_enable = False
diff --git a/archipack/presets/archipack_window/120x110_flat_2_elliptic.png b/archipack/presets/archipack_window/120x110_flat_2_elliptic.png
new file mode 100644
index 00000000..6809b6fb
--- /dev/null
+++ b/archipack/presets/archipack_window/120x110_flat_2_elliptic.png
Binary files differ
diff --git a/archipack/presets/archipack_window/120x110_flat_2_elliptic.py b/archipack/presets/archipack_window/120x110_flat_2_elliptic.py
new file mode 100644
index 00000000..312f7299
--- /dev/null
+++ b/archipack/presets/archipack_window/120x110_flat_2_elliptic.py
@@ -0,0 +1,58 @@
+import bpy
+d = bpy.context.active_object.data.archipack_window[0]
+
+d.frame_y = 0.05999999865889549
+d.flip = False
+d.blind_z = 0.029999999329447746
+d.blind_open = 80.0
+d.hole_margin = 0.10000000149011612
+d.out_frame_y = 0.019999999552965164
+d.blind_y = 0.0020000000949949026
+d.in_tablet_x = 0.03999999910593033
+d.in_tablet_enable = True
+d.n_rows = 2
+d.radius = 0.9599999785423279
+d.rows.clear()
+item_sub_1 = d.rows.add()
+item_sub_1.name = ''
+item_sub_1.width = (50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0)
+item_sub_1.fixed = (False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False)
+item_sub_1.auto_update = True
+item_sub_1.n_cols = 2
+item_sub_1.cols = 2
+item_sub_1.height = 0.800000011920929
+item_sub_1 = d.rows.add()
+item_sub_1.name = ''
+item_sub_1.width = (50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0)
+item_sub_1.fixed = (False, True, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False)
+item_sub_1.auto_update = True
+item_sub_1.n_cols = 1
+item_sub_1.cols = 1
+item_sub_1.height = 1.0
+d.out_tablet_x = 0.03999999910593033
+d.out_frame = False
+d.y = 0.20000000298023224
+d.in_tablet_z = 0.029999999329447746
+d.handle_altitude = 1.399999976158142
+d.out_frame_y2 = 0.019999999552965164
+d.out_tablet_y = 0.03999999910593033
+d.in_tablet_y = 0.03999999910593033
+d.out_frame_x = 0.10000000149011612
+d.offset = 0.10000000149011612
+d.window_shape = 'ELLIPSIS'
+d.frame_x = 0.05999999865889549
+d.x = 1.2000000476837158
+d.z = 1.100000023841858
+d.hole_inside_mat = 1
+d.curve_steps = 32
+d.handle_enable = True
+d.hole_outside_mat = 0
+d.out_tablet_z = 0.029999999329447746
+d.window_type = 'FLAT'
+d.angle_y = 0.0
+d.elipsis_b = 0.5
+d.out_tablet_enable = True
+d.out_frame_offset = 0.0
+d.warning = False
+d.altitude = 1.0
+d.blind_enable = False
diff --git a/archipack/presets/archipack_window/120x110_flat_2_oblique.png b/archipack/presets/archipack_window/120x110_flat_2_oblique.png
new file mode 100644
index 00000000..e775b887
--- /dev/null
+++ b/archipack/presets/archipack_window/120x110_flat_2_oblique.png
Binary files differ
diff --git a/archipack/presets/archipack_window/120x110_flat_2_oblique.py b/archipack/presets/archipack_window/120x110_flat_2_oblique.py
new file mode 100644
index 00000000..010b4073
--- /dev/null
+++ b/archipack/presets/archipack_window/120x110_flat_2_oblique.py
@@ -0,0 +1,50 @@
+import bpy
+d = bpy.context.active_object.data.archipack_window[0]
+
+d.frame_y = 0.05999999865889549
+d.flip = False
+d.blind_z = 0.029999999329447746
+d.blind_open = 80.0
+d.hole_margin = 0.10000000149011612
+d.out_frame_y = 0.019999999552965164
+d.blind_y = 0.0020000000949949026
+d.in_tablet_x = 0.03999999910593033
+d.in_tablet_enable = True
+d.n_rows = 1
+d.radius = 0.9599999785423279
+d.rows.clear()
+item_sub_1 = d.rows.add()
+item_sub_1.name = ''
+item_sub_1.width = (50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0)
+item_sub_1.fixed = (False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False)
+item_sub_1.auto_update = True
+item_sub_1.n_cols = 2
+item_sub_1.cols = 2
+item_sub_1.height = 0.800000011920929
+d.out_tablet_x = 0.03999999910593033
+d.out_frame = False
+d.y = 0.20000000298023224
+d.in_tablet_z = 0.029999999329447746
+d.handle_altitude = 1.399999976158142
+d.out_frame_y2 = 0.019999999552965164
+d.out_tablet_y = 0.03999999910593033
+d.in_tablet_y = 0.03999999910593033
+d.out_frame_x = 0.10000000149011612
+d.offset = 0.10000000149011612
+d.window_shape = 'QUADRI'
+d.frame_x = 0.05999999865889549
+d.x = 1.2000000476837158
+d.z = 1.100000023841858
+d.hole_inside_mat = 1
+d.curve_steps = 32
+d.handle_enable = True
+d.hole_outside_mat = 0
+d.out_tablet_z = 0.029999999329447746
+d.window_type = 'FLAT'
+d.angle_y = 0.39269909262657166
+d.elipsis_b = 0.5
+d.out_tablet_enable = True
+d.out_frame_offset = 0.0
+d.warning = False
+d.altitude = 1.0
+d.blind_enable = False
diff --git a/archipack/presets/archipack_window/120x110_flat_2_round.png b/archipack/presets/archipack_window/120x110_flat_2_round.png
new file mode 100644
index 00000000..5ae472dc
--- /dev/null
+++ b/archipack/presets/archipack_window/120x110_flat_2_round.png
Binary files differ
diff --git a/archipack/presets/archipack_window/120x110_flat_2_round.py b/archipack/presets/archipack_window/120x110_flat_2_round.py
new file mode 100644
index 00000000..3d0fd325
--- /dev/null
+++ b/archipack/presets/archipack_window/120x110_flat_2_round.py
@@ -0,0 +1,58 @@
+import bpy
+d = bpy.context.active_object.data.archipack_window[0]
+
+d.frame_y = 0.05999999865889549
+d.flip = False
+d.blind_z = 0.029999999329447746
+d.blind_open = 80.0
+d.hole_margin = 0.10000000149011612
+d.out_frame_y = 0.019999999552965164
+d.blind_y = 0.0020000000949949026
+d.in_tablet_x = 0.03999999910593033
+d.in_tablet_enable = True
+d.n_rows = 2
+d.radius = 0.9599999785423279
+d.rows.clear()
+item_sub_1 = d.rows.add()
+item_sub_1.name = ''
+item_sub_1.width = (50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0)
+item_sub_1.fixed = (False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False)
+item_sub_1.auto_update = True
+item_sub_1.n_cols = 2
+item_sub_1.cols = 2
+item_sub_1.height = 0.800000011920929
+item_sub_1 = d.rows.add()
+item_sub_1.name = ''
+item_sub_1.width = (50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0)
+item_sub_1.fixed = (False, True, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False)
+item_sub_1.auto_update = True
+item_sub_1.n_cols = 1
+item_sub_1.cols = 1
+item_sub_1.height = 1.0
+d.out_tablet_x = 0.03999999910593033
+d.out_frame = False
+d.y = 0.20000000298023224
+d.in_tablet_z = 0.029999999329447746
+d.handle_altitude = 1.399999976158142
+d.out_frame_y2 = 0.019999999552965164
+d.out_tablet_y = 0.03999999910593033
+d.in_tablet_y = 0.03999999910593033
+d.out_frame_x = 0.10000000149011612
+d.offset = 0.10000000149011612
+d.window_shape = 'ROUND'
+d.frame_x = 0.05999999865889549
+d.x = 1.2000000476837158
+d.z = 1.100000023841858
+d.hole_inside_mat = 1
+d.curve_steps = 16
+d.handle_enable = True
+d.hole_outside_mat = 0
+d.out_tablet_z = 0.029999999329447746
+d.window_type = 'FLAT'
+d.angle_y = 0.0
+d.elipsis_b = 0.5
+d.out_tablet_enable = True
+d.out_frame_offset = 0.0
+d.warning = False
+d.altitude = 1.0
+d.blind_enable = False
diff --git a/archipack/presets/archipack_window/180x110_flat_3.png b/archipack/presets/archipack_window/180x110_flat_3.png
new file mode 100644
index 00000000..22845518
--- /dev/null
+++ b/archipack/presets/archipack_window/180x110_flat_3.png
Binary files differ
diff --git a/archipack/presets/archipack_window/180x110_flat_3.py b/archipack/presets/archipack_window/180x110_flat_3.py
new file mode 100644
index 00000000..3ae2748a
--- /dev/null
+++ b/archipack/presets/archipack_window/180x110_flat_3.py
@@ -0,0 +1,50 @@
+import bpy
+d = bpy.context.active_object.data.archipack_window[0]
+
+d.frame_y = 0.05999999865889549
+d.flip = False
+d.blind_z = 0.029999999329447746
+d.blind_open = 80.0
+d.hole_margin = 0.10000000149011612
+d.out_frame_y = 0.019999999552965164
+d.blind_y = 0.0020000000949949026
+d.in_tablet_x = 0.03999999910593033
+d.in_tablet_enable = True
+d.n_rows = 1
+d.radius = 2.5
+d.rows.clear()
+item_sub_1 = d.rows.add()
+item_sub_1.name = ''
+item_sub_1.width = (33.33333206176758, 33.33333206176758, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0)
+item_sub_1.fixed = (False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False)
+item_sub_1.auto_update = True
+item_sub_1.n_cols = 3
+item_sub_1.cols = 3
+item_sub_1.height = 1.0
+d.out_tablet_x = 0.03999999910593033
+d.out_frame = False
+d.y = 0.20000000298023224
+d.in_tablet_z = 0.029999999329447746
+d.handle_altitude = 1.399999976158142
+d.out_frame_y2 = 0.019999999552965164
+d.out_tablet_y = 0.03999999910593033
+d.in_tablet_y = 0.03999999910593033
+d.out_frame_x = 0.10000000149011612
+d.offset = 0.10000000149011612
+d.window_shape = 'RECTANGLE'
+d.frame_x = 0.05999999865889549
+d.x = 1.7999999523162842
+d.z = 1.100000023841858
+d.hole_inside_mat = 1
+d.curve_steps = 16
+d.handle_enable = True
+d.hole_outside_mat = 0
+d.out_tablet_z = 0.029999999329447746
+d.window_type = 'FLAT'
+d.angle_y = 0.0
+d.elipsis_b = 0.5
+d.out_tablet_enable = True
+d.out_frame_offset = 0.0
+d.warning = False
+d.altitude = 1.0
+d.blind_enable = False
diff --git a/archipack/presets/archipack_window/180x210_flat_3.png b/archipack/presets/archipack_window/180x210_flat_3.png
new file mode 100644
index 00000000..354e9be9
--- /dev/null
+++ b/archipack/presets/archipack_window/180x210_flat_3.png
Binary files differ
diff --git a/archipack/presets/archipack_window/180x210_flat_3.py b/archipack/presets/archipack_window/180x210_flat_3.py
new file mode 100644
index 00000000..df26b7a5
--- /dev/null
+++ b/archipack/presets/archipack_window/180x210_flat_3.py
@@ -0,0 +1,50 @@
+import bpy
+d = bpy.context.active_object.data.archipack_window[0]
+
+d.frame_y = 0.05999999865889549
+d.flip = False
+d.blind_z = 0.029999999329447746
+d.blind_open = 80.0
+d.hole_margin = 0.10000000149011612
+d.out_frame_y = 0.019999999552965164
+d.blind_y = 0.0020000000949949026
+d.in_tablet_x = 0.03999999910593033
+d.in_tablet_enable = True
+d.n_rows = 1
+d.radius = 2.5
+d.rows.clear()
+item_sub_1 = d.rows.add()
+item_sub_1.name = ''
+item_sub_1.width = (33.33333206176758, 33.33333206176758, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0)
+item_sub_1.fixed = (False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False)
+item_sub_1.auto_update = True
+item_sub_1.n_cols = 3
+item_sub_1.cols = 3
+item_sub_1.height = 1.0
+d.out_tablet_x = 0.03999999910593033
+d.out_frame = False
+d.y = 0.20000000298023224
+d.in_tablet_z = 0.029999999329447746
+d.handle_altitude = 1.399999976158142
+d.out_frame_y2 = 0.019999999552965164
+d.out_tablet_y = 0.03999999910593033
+d.in_tablet_y = 0.03999999910593033
+d.out_frame_x = 0.10000000149011612
+d.offset = 0.10000000149011612
+d.window_shape = 'RECTANGLE'
+d.frame_x = 0.05999999865889549
+d.x = 1.7999999523162842
+d.z = 2.0999999046325684
+d.hole_inside_mat = 1
+d.curve_steps = 16
+d.handle_enable = True
+d.hole_outside_mat = 0
+d.out_tablet_z = 0.029999999329447746
+d.window_type = 'FLAT'
+d.angle_y = 0.0
+d.elipsis_b = 0.5
+d.out_tablet_enable = True
+d.out_frame_offset = 0.0
+d.warning = False
+d.altitude = 0.0
+d.blind_enable = False
diff --git a/archipack/presets/archipack_window/180x210_rail_2.png b/archipack/presets/archipack_window/180x210_rail_2.png
new file mode 100644
index 00000000..b7808c27
--- /dev/null
+++ b/archipack/presets/archipack_window/180x210_rail_2.png
Binary files differ
diff --git a/archipack/presets/archipack_window/180x210_rail_2.py b/archipack/presets/archipack_window/180x210_rail_2.py
new file mode 100644
index 00000000..d9f2cb89
--- /dev/null
+++ b/archipack/presets/archipack_window/180x210_rail_2.py
@@ -0,0 +1,50 @@
+import bpy
+d = bpy.context.active_object.data.archipack_window[0]
+
+d.frame_y = 0.05999999865889549
+d.flip = False
+d.blind_z = 0.029999999329447746
+d.blind_open = 80.0
+d.hole_margin = 0.10000000149011612
+d.out_frame_y = 0.019999999552965164
+d.blind_y = 0.0020000000949949026
+d.in_tablet_x = 0.03999999910593033
+d.in_tablet_enable = True
+d.n_rows = 1
+d.radius = 2.5
+d.rows.clear()
+item_sub_1 = d.rows.add()
+item_sub_1.name = ''
+item_sub_1.width = (50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0)
+item_sub_1.fixed = (False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False)
+item_sub_1.auto_update = True
+item_sub_1.n_cols = 2
+item_sub_1.cols = 2
+item_sub_1.height = 1.0
+d.out_tablet_x = 0.03999999910593033
+d.out_frame = False
+d.y = 0.20000000298023224
+d.in_tablet_z = 0.029999999329447746
+d.handle_altitude = 1.399999976158142
+d.out_frame_y2 = 0.019999999552965164
+d.out_tablet_y = 0.03999999910593033
+d.in_tablet_y = 0.03999999910593033
+d.out_frame_x = 0.10000000149011612
+d.offset = 0.10000000149011612
+d.window_shape = 'RECTANGLE'
+d.frame_x = 0.05999999865889549
+d.x = 1.7999999523162842
+d.z = 2.0999999046325684
+d.hole_inside_mat = 1
+d.curve_steps = 16
+d.handle_enable = True
+d.hole_outside_mat = 0
+d.out_tablet_z = 0.029999999329447746
+d.window_type = 'RAIL'
+d.angle_y = 0.0
+d.elipsis_b = 0.5
+d.out_tablet_enable = True
+d.out_frame_offset = 0.0
+d.warning = False
+d.altitude = 0.0
+d.blind_enable = False
diff --git a/archipack/presets/archipack_window/240x210_rail_3.png b/archipack/presets/archipack_window/240x210_rail_3.png
new file mode 100644
index 00000000..1201622a
--- /dev/null
+++ b/archipack/presets/archipack_window/240x210_rail_3.png
Binary files differ
diff --git a/archipack/presets/archipack_window/240x210_rail_3.py b/archipack/presets/archipack_window/240x210_rail_3.py
new file mode 100644
index 00000000..4cec930b
--- /dev/null
+++ b/archipack/presets/archipack_window/240x210_rail_3.py
@@ -0,0 +1,50 @@
+import bpy
+d = bpy.context.active_object.data.archipack_window[0]
+
+d.frame_y = 0.05999999865889549
+d.flip = False
+d.blind_z = 0.029999999329447746
+d.blind_open = 80.0
+d.hole_margin = 0.10000000149011612
+d.out_frame_y = 0.019999999552965164
+d.blind_y = 0.0020000000949949026
+d.in_tablet_x = 0.03999999910593033
+d.in_tablet_enable = True
+d.n_rows = 1
+d.radius = 2.5
+d.rows.clear()
+item_sub_1 = d.rows.add()
+item_sub_1.name = ''
+item_sub_1.width = (33.33333206176758, 33.33333206176758, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0)
+item_sub_1.fixed = (False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False)
+item_sub_1.auto_update = True
+item_sub_1.n_cols = 3
+item_sub_1.cols = 3
+item_sub_1.height = 1.0
+d.out_tablet_x = 0.03999999910593033
+d.out_frame = False
+d.y = 0.20000000298023224
+d.in_tablet_z = 0.029999999329447746
+d.handle_altitude = 1.399999976158142
+d.out_frame_y2 = 0.019999999552965164
+d.out_tablet_y = 0.03999999910593033
+d.in_tablet_y = 0.03999999910593033
+d.out_frame_x = 0.10000000149011612
+d.offset = 0.10000000149011612
+d.window_shape = 'RECTANGLE'
+d.frame_x = 0.05999999865889549
+d.x = 2.4000000953674316
+d.z = 2.0999999046325684
+d.hole_inside_mat = 1
+d.curve_steps = 16
+d.handle_enable = True
+d.hole_outside_mat = 0
+d.out_tablet_z = 0.029999999329447746
+d.window_type = 'RAIL'
+d.angle_y = 0.0
+d.elipsis_b = 0.5
+d.out_tablet_enable = True
+d.out_frame_offset = 0.0
+d.warning = False
+d.altitude = 0.0
+d.blind_enable = False
diff --git a/archipack/presets/archipack_window/80x80_flat_1.png b/archipack/presets/archipack_window/80x80_flat_1.png
new file mode 100644
index 00000000..8568fac8
--- /dev/null
+++ b/archipack/presets/archipack_window/80x80_flat_1.png
Binary files differ
diff --git a/archipack/presets/archipack_window/80x80_flat_1.py b/archipack/presets/archipack_window/80x80_flat_1.py
new file mode 100644
index 00000000..caf2980b
--- /dev/null
+++ b/archipack/presets/archipack_window/80x80_flat_1.py
@@ -0,0 +1,50 @@
+import bpy
+d = bpy.context.active_object.data.archipack_window[0]
+
+d.frame_y = 0.05999999865889549
+d.flip = False
+d.blind_z = 0.029999999329447746
+d.blind_open = 80.0
+d.hole_margin = 0.10000000149011612
+d.out_frame_y = 0.019999999552965164
+d.blind_y = 0.0020000000949949026
+d.in_tablet_x = 0.03999999910593033
+d.in_tablet_enable = True
+d.n_rows = 1
+d.radius = 2.5
+d.rows.clear()
+item_sub_1 = d.rows.add()
+item_sub_1.name = ''
+item_sub_1.width = (50.0, 33.33333206176758, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0)
+item_sub_1.fixed = (False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False)
+item_sub_1.auto_update = True
+item_sub_1.n_cols = 1
+item_sub_1.cols = 1
+item_sub_1.height = 1.0
+d.out_tablet_x = 0.03999999910593033
+d.out_frame = False
+d.y = 0.20000000298023224
+d.in_tablet_z = 0.029999999329447746
+d.handle_altitude = 1.399999976158142
+d.out_frame_y2 = 0.019999999552965164
+d.out_tablet_y = 0.03999999910593033
+d.in_tablet_y = 0.03999999910593033
+d.out_frame_x = 0.10000000149011612
+d.offset = 0.10000000149011612
+d.window_shape = 'RECTANGLE'
+d.frame_x = 0.05999999865889549
+d.x = 0.800000011920929
+d.z = 0.800000011920929
+d.hole_inside_mat = 1
+d.curve_steps = 16
+d.handle_enable = True
+d.hole_outside_mat = 0
+d.out_tablet_z = 0.029999999329447746
+d.window_type = 'FLAT'
+d.angle_y = 0.0
+d.elipsis_b = 0.5
+d.out_tablet_enable = True
+d.out_frame_offset = 0.0
+d.warning = False
+d.altitude = 1.2000000476837158
+d.blind_enable = False
diff --git a/archipack/presets/archipack_window/80x80_flat_1_circle.png b/archipack/presets/archipack_window/80x80_flat_1_circle.png
new file mode 100644
index 00000000..bd856b37
--- /dev/null
+++ b/archipack/presets/archipack_window/80x80_flat_1_circle.png
Binary files differ
diff --git a/archipack/presets/archipack_window/80x80_flat_1_circle.py b/archipack/presets/archipack_window/80x80_flat_1_circle.py
new file mode 100644
index 00000000..18f5c8bc
--- /dev/null
+++ b/archipack/presets/archipack_window/80x80_flat_1_circle.py
@@ -0,0 +1,58 @@
+import bpy
+d = bpy.context.active_object.data.archipack_window[0]
+
+d.frame_y = 0.05999999865889549
+d.flip = False
+d.blind_z = 0.029999999329447746
+d.blind_open = 80.0
+d.hole_margin = 0.10000000149011612
+d.out_frame_y = 0.019999999552965164
+d.blind_y = 0.0020000000949949026
+d.in_tablet_x = 0.03999999910593033
+d.in_tablet_enable = True
+d.n_rows = 2
+d.radius = 0.9599999785423279
+d.rows.clear()
+item_sub_1 = d.rows.add()
+item_sub_1.name = ''
+item_sub_1.width = (50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0)
+item_sub_1.fixed = (False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False)
+item_sub_1.auto_update = True
+item_sub_1.n_cols = 2
+item_sub_1.cols = 2
+item_sub_1.height = 0.800000011920929
+item_sub_1 = d.rows.add()
+item_sub_1.name = ''
+item_sub_1.width = (50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0)
+item_sub_1.fixed = (False, True, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False)
+item_sub_1.auto_update = True
+item_sub_1.n_cols = 1
+item_sub_1.cols = 1
+item_sub_1.height = 1.0
+d.out_tablet_x = 0.03999999910593033
+d.out_frame = False
+d.y = 0.800000011920929
+d.in_tablet_z = 0.029999999329447746
+d.handle_altitude = 1.399999976158142
+d.out_frame_y2 = 0.019999999552965164
+d.out_tablet_y = 0.03999999910593033
+d.in_tablet_y = 0.03999999910593033
+d.out_frame_x = 0.10000000149011612
+d.offset = 0.10000000149011612
+d.window_shape = 'CIRCLE'
+d.frame_x = 0.05999999865889549
+d.x = 0.800000011920929
+d.z = 1.100000023841858
+d.hole_inside_mat = 1
+d.curve_steps = 32
+d.handle_enable = True
+d.hole_outside_mat = 0
+d.out_tablet_z = 0.029999999329447746
+d.window_type = 'FLAT'
+d.angle_y = 0.0
+d.elipsis_b = 0.5
+d.out_tablet_enable = True
+d.out_frame_offset = 0.0
+d.warning = False
+d.altitude = 1.0
+d.blind_enable = False
diff --git a/archipack/presets/missing.png b/archipack/presets/missing.png
new file mode 100644
index 00000000..7881102a
--- /dev/null
+++ b/archipack/presets/missing.png
Binary files differ
diff --git a/archipack/pyqtree.py b/archipack/pyqtree.py
new file mode 100644
index 00000000..80b75727
--- /dev/null
+++ b/archipack/pyqtree.py
@@ -0,0 +1,187 @@
+# -*- coding:utf-8 -*-
+
+# <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)