From 45cad6756f10eb708d1a17dae4a70723accc1928 Mon Sep 17 00:00:00 2001 From: Stephen Leger Date: Tue, 1 Aug 2017 03:48:42 +0200 Subject: archipack: update to 1.2.8 add roof and freeform floors --- archipack/__init__.py | 49 +- archipack/archipack_2d.py | 44 +- archipack/archipack_autoboolean.py | 97 +- archipack/archipack_cutter.py | 910 ++++ archipack/archipack_door.py | 52 +- archipack/archipack_floor.py | 2720 ++++++---- archipack/archipack_gl.py | 125 +- archipack/archipack_handle.py | 6 + archipack/archipack_manipulator.py | 6 + archipack/archipack_material.py | 575 +++ archipack/archipack_object.py | 19 +- archipack/archipack_preset.py | 8 +- archipack/archipack_progressbar.py | 87 + archipack/archipack_reference_point.py | 162 +- archipack/archipack_roof.py | 5376 ++++++++++++++++++++ archipack/archipack_slab.py | 307 +- archipack/archipack_snap.py | 5 +- archipack/archipack_stair.py | 5 - archipack/archipack_wall2.py | 222 +- archipack/archipack_window.py | 169 +- archipack/bmesh_utils.py | 72 +- archipack/icons/roof.png | Bin 0 -> 1483 bytes archipack/materialutils.py | 169 - .../presets/archipack_floor/boards_200x20.png | Bin 0 -> 11237 bytes archipack/presets/archipack_floor/boards_200x20.py | 30 + .../presets/archipack_floor/herringbone_50x10.png | Bin 11148 -> 11228 bytes .../presets/archipack_floor/herringbone_50x10.py | 58 +- .../archipack_floor/herringbone_p_50x10.png | Bin 10924 -> 11099 bytes .../presets/archipack_floor/herringbone_p_50x10.py | 58 +- archipack/presets/archipack_floor/hexagon_10.png | Bin 0 -> 14395 bytes archipack/presets/archipack_floor/hexagon_10.py | 30 + .../presets/archipack_floor/hopscotch_30x30.png | Bin 0 -> 12816 bytes .../presets/archipack_floor/hopscotch_30x30.py | 30 + archipack/presets/archipack_floor/parquet_15x3.png | Bin 13445 -> 13697 bytes archipack/presets/archipack_floor/parquet_15x3.py | 58 +- .../presets/archipack_floor/planks_200x20.png | Bin 11644 -> 0 bytes archipack/presets/archipack_floor/planks_200x20.py | 34 - .../archipack_floor/stepping_stone_30x30.png | Bin 0 -> 13287 bytes .../archipack_floor/stepping_stone_30x30.py | 30 + archipack/presets/archipack_floor/tile_30x60.png | Bin 0 -> 12081 bytes archipack/presets/archipack_floor/tile_30x60.py | 30 + archipack/presets/archipack_floor/tiles_15x15.png | Bin 12939 -> 0 bytes archipack/presets/archipack_floor/tiles_15x15.py | 34 - archipack/presets/archipack_floor/tiles_60x30.png | Bin 11379 -> 0 bytes archipack/presets/archipack_floor/tiles_60x30.py | 34 - .../presets/archipack_floor/tiles_hex_10x10.png | Bin 13663 -> 0 bytes .../presets/archipack_floor/tiles_hex_10x10.py | 34 - .../archipack_floor/tiles_l+ms_30x30_15x15.png | Bin 12511 -> 0 bytes .../archipack_floor/tiles_l+ms_30x30_15x15.py | 34 - .../archipack_floor/tiles_l+s_30x30_15x15.png | Bin 11631 -> 0 bytes .../archipack_floor/tiles_l+s_30x30_15x15.py | 34 - .../presets/archipack_floor/windmill_30x30.png | Bin 0 -> 13477 bytes .../presets/archipack_floor/windmill_30x30.py | 30 + archipack/presets/archipack_materials/door.txt | 4 + archipack/presets/archipack_materials/fence.txt | 4 + archipack/presets/archipack_materials/floor.txt | 11 + archipack/presets/archipack_materials/handle.txt | 2 + archipack/presets/archipack_materials/roof.txt | 12 + archipack/presets/archipack_materials/slab.txt | 3 + archipack/presets/archipack_materials/stair.txt | 6 + archipack/presets/archipack_materials/truss.txt | 1 + archipack/presets/archipack_materials/wall2.txt | 8 + archipack/presets/archipack_materials/window.txt | 6 + archipack/presets/archipack_roof/braas_1.png | Bin 0 -> 23745 bytes archipack/presets/archipack_roof/braas_1.py | 30 + archipack/presets/archipack_roof/braas_2.png | Bin 0 -> 23796 bytes archipack/presets/archipack_roof/braas_2.py | 30 + archipack/presets/archipack_roof/eternit.png | Bin 0 -> 21808 bytes archipack/presets/archipack_roof/eternit.py | 30 + archipack/presets/archipack_roof/lauze.png | Bin 0 -> 21626 bytes archipack/presets/archipack_roof/lauze.py | 30 + archipack/presets/archipack_roof/metal.png | Bin 0 -> 20668 bytes archipack/presets/archipack_roof/metal.py | 30 + archipack/presets/archipack_roof/ondule.png | Bin 0 -> 22270 bytes archipack/presets/archipack_roof/ondule.py | 29 + archipack/presets/archipack_roof/roman.png | Bin 0 -> 25294 bytes archipack/presets/archipack_roof/roman.py | 29 + archipack/presets/archipack_roof/round.png | Bin 0 -> 22493 bytes archipack/presets/archipack_roof/round.py | 30 + archipack/presets/archipack_roof/square.png | Bin 0 -> 21214 bytes archipack/presets/archipack_roof/square.py | 30 + .../archipack_stair/u_wood_over_concrete.py | 2 +- 82 files changed, 10391 insertions(+), 1679 deletions(-) create mode 100644 archipack/archipack_cutter.py create mode 100644 archipack/archipack_material.py create mode 100644 archipack/archipack_progressbar.py create mode 100644 archipack/archipack_roof.py create mode 100644 archipack/icons/roof.png delete mode 100644 archipack/materialutils.py create mode 100644 archipack/presets/archipack_floor/boards_200x20.png create mode 100644 archipack/presets/archipack_floor/boards_200x20.py create mode 100644 archipack/presets/archipack_floor/hexagon_10.png create mode 100644 archipack/presets/archipack_floor/hexagon_10.py create mode 100644 archipack/presets/archipack_floor/hopscotch_30x30.png create mode 100644 archipack/presets/archipack_floor/hopscotch_30x30.py delete mode 100644 archipack/presets/archipack_floor/planks_200x20.png delete mode 100644 archipack/presets/archipack_floor/planks_200x20.py create mode 100644 archipack/presets/archipack_floor/stepping_stone_30x30.png create mode 100644 archipack/presets/archipack_floor/stepping_stone_30x30.py create mode 100644 archipack/presets/archipack_floor/tile_30x60.png create mode 100644 archipack/presets/archipack_floor/tile_30x60.py delete mode 100644 archipack/presets/archipack_floor/tiles_15x15.png delete mode 100644 archipack/presets/archipack_floor/tiles_15x15.py delete mode 100644 archipack/presets/archipack_floor/tiles_60x30.png delete mode 100644 archipack/presets/archipack_floor/tiles_60x30.py delete mode 100644 archipack/presets/archipack_floor/tiles_hex_10x10.png delete mode 100644 archipack/presets/archipack_floor/tiles_hex_10x10.py delete mode 100644 archipack/presets/archipack_floor/tiles_l+ms_30x30_15x15.png delete mode 100644 archipack/presets/archipack_floor/tiles_l+ms_30x30_15x15.py delete mode 100644 archipack/presets/archipack_floor/tiles_l+s_30x30_15x15.png delete mode 100644 archipack/presets/archipack_floor/tiles_l+s_30x30_15x15.py create mode 100644 archipack/presets/archipack_floor/windmill_30x30.png create mode 100644 archipack/presets/archipack_floor/windmill_30x30.py create mode 100644 archipack/presets/archipack_materials/door.txt create mode 100644 archipack/presets/archipack_materials/fence.txt create mode 100644 archipack/presets/archipack_materials/floor.txt create mode 100644 archipack/presets/archipack_materials/handle.txt create mode 100644 archipack/presets/archipack_materials/roof.txt create mode 100644 archipack/presets/archipack_materials/slab.txt create mode 100644 archipack/presets/archipack_materials/stair.txt create mode 100644 archipack/presets/archipack_materials/truss.txt create mode 100644 archipack/presets/archipack_materials/wall2.txt create mode 100644 archipack/presets/archipack_materials/window.txt create mode 100644 archipack/presets/archipack_roof/braas_1.png create mode 100644 archipack/presets/archipack_roof/braas_1.py create mode 100644 archipack/presets/archipack_roof/braas_2.png create mode 100644 archipack/presets/archipack_roof/braas_2.py create mode 100644 archipack/presets/archipack_roof/eternit.png create mode 100644 archipack/presets/archipack_roof/eternit.py create mode 100644 archipack/presets/archipack_roof/lauze.png create mode 100644 archipack/presets/archipack_roof/lauze.py create mode 100644 archipack/presets/archipack_roof/metal.png create mode 100644 archipack/presets/archipack_roof/metal.py create mode 100644 archipack/presets/archipack_roof/ondule.png create mode 100644 archipack/presets/archipack_roof/ondule.py create mode 100644 archipack/presets/archipack_roof/roman.png create mode 100644 archipack/presets/archipack_roof/roman.py create mode 100644 archipack/presets/archipack_roof/round.png create mode 100644 archipack/presets/archipack_roof/round.py create mode 100644 archipack/presets/archipack_roof/square.png create mode 100644 archipack/presets/archipack_roof/square.py diff --git a/archipack/__init__.py b/archipack/__init__.py index 5bdf9b10..93a50e66 100644 --- a/archipack/__init__.py +++ b/archipack/__init__.py @@ -31,7 +31,7 @@ bl_info = { 'author': 's-leger', 'license': 'GPL', 'deps': '', - 'version': (1, 2, 6), + 'version': (1, 2, 8), 'blender': (2, 7, 8), 'location': 'View3D > Tools > Create > Archipack', 'warning': '', @@ -46,6 +46,8 @@ import os if "bpy" in locals(): import importlib as imp + imp.reload(archipack_progressbar) + imp.reload(archipack_material) imp.reload(archipack_snap) imp.reload(archipack_manipulator) imp.reload(archipack_reference_point) @@ -54,6 +56,7 @@ if "bpy" in locals(): imp.reload(archipack_window) imp.reload(archipack_stair) imp.reload(archipack_wall2) + imp.reload(archipack_roof) imp.reload(archipack_slab) imp.reload(archipack_fence) imp.reload(archipack_truss) @@ -62,6 +65,8 @@ if "bpy" in locals(): print("archipack: reload ready") else: + from . import archipack_progressbar + from . import archipack_material from . import archipack_snap from . import archipack_manipulator from . import archipack_reference_point @@ -70,6 +75,7 @@ else: from . import archipack_window from . import archipack_stair from . import archipack_wall2 + from . import archipack_roof from . import archipack_slab from . import archipack_fence from . import archipack_truss @@ -78,7 +84,6 @@ else: print("archipack: ready") - # noinspection PyUnresolvedReferences import bpy # noinspection PyUnresolvedReferences @@ -135,7 +140,7 @@ class Archipack_Pref(AddonPreferences): ) max_style_draw_tool = BoolProperty( name="Draw a wall use 3dsmax style", - description="Reverse clic / release cycle for Draw a wall", + description="Reverse clic / release & drag cycle for Draw a wall", default=True ) # Arrow sizes (world units) @@ -151,6 +156,11 @@ class Archipack_Pref(AddonPreferences): min=2, default=10 ) + matlib_path = StringProperty( + name="Folder path", + description="absolute path to material library folder", + default="" + ) # Font sizes and basic colour scheme feedback_size_main = IntProperty( name="Main", @@ -225,6 +235,11 @@ class Archipack_Pref(AddonPreferences): box.prop(self, "max_style_draw_tool") box = layout.box() row = box.row() + col = row.column() + col.label(text="Material library:") + col.prop(self, "matlib_path") + box = layout.box() + row = box.row() split = row.split(percentage=0.5) col = split.column() col.label(text="Colors:") @@ -296,6 +311,7 @@ class TOOLS_PT_Archipack_Create(Panel): def draw(self, context): global icons_collection + icons = icons_collection["main"] layout = self.layout row = layout.row(align=True) @@ -356,10 +372,22 @@ class TOOLS_PT_Archipack_Create(Panel): icon_value=icons["slab"].icon_id ).ceiling = True row = box.row(align=True) + row.operator("archipack.roof_preset_menu", + text="Roof", + icon_value=icons["roof"].icon_id + ).preset_operator = "archipack.roof" + row = box.row(align=True) row.operator("archipack.floor_preset_menu", text="Floor", icon_value=icons["floor"].icon_id ).preset_operator = "archipack.floor" + row.operator("archipack.floor_preset_menu", + text="->Wall", + icon_value=icons["floor"].icon_id + ).preset_operator = "archipack.floor_from_wall" + row.operator("archipack.floor_preset_menu", + text="", + icon='CURVE_DATA').preset_operator = "archipack.floor_from_curve" # ---------------------------------------------------- @@ -400,13 +428,16 @@ def draw_menu(self, context): layout.operator("archipack.floor_preset_menu", text="Floor", icon_value=icons["floor"].icon_id - ) + ).preset_operator = "archipack.floor" + layout.operator("archipack.roof_preset_menu", + text="Roof", + icon_value=icons["roof"].icon_id + ).preset_operator = "archipack.roof" class ARCHIPACK_create_menu(Menu): bl_label = 'Archipack' bl_idname = 'ARCHIPACK_create_menu' - bl_context = "objectmode" def draw(self, context): draw_menu(self, context) @@ -454,6 +485,8 @@ def register(): icons.load(name, os.path.join(icons_dir, icon), 'IMAGE') icons_collection["main"] = icons + archipack_progressbar.register() + archipack_material.register() archipack_snap.register() archipack_manipulator.register() archipack_reference_point.register() @@ -462,6 +495,7 @@ def register(): archipack_window.register() archipack_stair.register() archipack_wall2.register() + archipack_roof.register() archipack_slab.register() archipack_fence.register() archipack_truss.register() @@ -484,8 +518,8 @@ def unregister(): 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_progressbar.unregister() + archipack_material.unregister() archipack_snap.unregister() archipack_manipulator.unregister() archipack_reference_point.unregister() @@ -494,6 +528,7 @@ def unregister(): archipack_window.unregister() archipack_stair.unregister() archipack_wall2.unregister() + archipack_roof.unregister() archipack_slab.unregister() archipack_fence.unregister() archipack_truss.unregister() diff --git a/archipack/archipack_2d.py b/archipack/archipack_2d.py index 912e3cb8..fcd578da 100644 --- a/archipack/archipack_2d.py +++ b/archipack/archipack_2d.py @@ -117,6 +117,10 @@ class Line(Projection): self.p = Vector((0, 0)) self.v = Vector((0, 0)) + @property + def copy(self): + return Line(self.p.copy(), self.v.copy()) + @property def p0(self): return self.p @@ -250,6 +254,20 @@ class Line(Projection): t = (c * (line.p - self.p)) / d return True, self.lerp(t), t + def intersect_ext(self, line): + """ + same as intersect, but return param t on both lines + """ + c = line.cross_z + d = self.v * c + if d == 0: + return False, 0, 0, 0 + dp = line.p - self.p + c2 = self.cross_z + u = (c * dp) / d + v = (c2 * dp) / d + return u > 0 and v > 0 and u < 1 and v < 1, self.lerp(u), u, v + def point_sur_segment(self, pt): """ _point_sur_segment point: Vector 2d @@ -258,6 +276,8 @@ class Line(Projection): """ dp = pt - self.p dl = self.length + if dl == 0: + return dp.length < 0.00001, 0, 0 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 @@ -318,7 +338,19 @@ class Line(Projection): Draw Line with open gl in screen space aka: coords are in pixels """ - raise NotImplementedError + curve = bpy.data.curves.new('LINE', 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, z = p + spline.points[i].co = (x, y, 0, 1) + curve_obj = bpy.data.objects.new('LINE', curve) + context.scene.objects.link(curve_obj) + curve_obj.select = True def make_offset(self, offset, last=None): """ @@ -581,6 +613,16 @@ class Arc(Circle): steps = max(1, round(self.length / length, 0)) return 1.0 / steps, int(steps) + def intersect_ext(self, line): + """ + same as intersect, but return param t on both lines + """ + res, p, v = self.intersect(line) + v0 = self.p0 - self.c + v1 = p - self.c + u = self.signed_angle(v0, v1) / self.da + return res and u > 0 and v > 0 and u < 1 and v < 1, p, u, v + # this is for wall def steps_by_angle(self, step_angle): steps = max(1, round(abs(self.da) / step_angle, 0)) diff --git a/archipack/archipack_autoboolean.py b/archipack/archipack_autoboolean.py index a171532c..f304ced0 100644 --- a/archipack/archipack_autoboolean.py +++ b/archipack/archipack_autoboolean.py @@ -28,16 +28,16 @@ import bpy from bpy.types import Operator from bpy.props import EnumProperty from mathutils import Vector -from .materialutils import MaterialUtils - -from os import path +""" +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(): @@ -95,7 +95,7 @@ class ArchipackBoolManager(): 'archipack_robusthole' in wall or 'archipack_handle' in wall) - def datablock(self, o): + def datablock(self, o, basis='WALL'): """ get datablock from windows and doors return @@ -104,10 +104,14 @@ class ArchipackBoolManager(): """ 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] + if basis == 'WALL': + 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 basis == 'ROOF': + if "archipack_roof" in o.data: + d = o.data.archipack_roof[0] return d def prepare_hole(self, hole): @@ -131,7 +135,7 @@ class ArchipackBoolManager(): return hole return None - def _generate_hole(self, context, o): + def _generate_hole(self, context, o, basis='WALL'): # use existing one if self.mode != 'ROBUST': hole = self.get_child_hole(o) @@ -139,7 +143,7 @@ class ArchipackBoolManager(): # print("_generate_hole Use existing hole %s" % (hole.name)) return hole # generate single hole from archipack primitives - d = self.datablock(o) + d = self.datablock(o, basis) hole = None if d is not None: if (self.itM is not None and ( @@ -224,7 +228,9 @@ class ArchipackBoolManager(): 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) + for mat in wall.data.materials: + hole_obj.data.materials.append(mat) + # MaterialUtils.add_wall2_materials(hole_obj) return hole_obj def update_hybrid(self, context, wall, childs, holes): @@ -268,10 +274,10 @@ class ArchipackBoolManager(): 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 @@ -284,12 +290,11 @@ class ArchipackBoolManager(): # 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): @@ -385,6 +390,16 @@ class ArchipackBoolManager(): elif modif is not None: wall.modifiers.remove(modif) + def get_basis_type(self, o): + if o.data is not None: + if "archipack_wall2" in o.data: + return 'WALL' + elif "archipack_roof" in o.data: + return 'ROOF' + elif "archipack_wall" in o.data: + return 'WALL' + return 'DEFAULT' + def autoboolean(self, context, wall): """ Entry point for multi-boolean operations like @@ -397,19 +412,26 @@ class ArchipackBoolManager(): # get wall bounds to find what's inside self._get_bounding_box(wall) + # filter roofs when wall is roof + basis = self.get_basis_type(wall) + # either generate hole or get existing one for o in context.scene.objects: - h = self._generate_hole(context, o) + h = self._generate_hole(context, o, basis) 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: + # copy wall material to hole + hole.data.materials.clear() + for mat in wall.data.materials: + hole.data.materials.append(mat) + self.prepare_hole(hole) - # debug_using_gl(context, "401") # update / remove / add boolean modifier if self.mode == 'INTERACTIVE': @@ -430,20 +452,18 @@ class ArchipackBoolManager(): 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: @@ -461,9 +481,12 @@ class ArchipackBoolManager(): 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) + basis = self.get_basis_type(wall) + d = self.datablock(o, basis) + hole = None hole_obj = None # default mode defined by __init__ @@ -477,6 +500,10 @@ class ArchipackBoolManager(): if hole is None: return + hole.data.materials.clear() + for mat in wall.data.materials: + hole.data.materials.append(mat) + self.prepare_hole(hole) if self.mode == 'INTERACTIVE': @@ -518,11 +545,13 @@ class ArchipackBoolManager(): 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 "archipack_wall2" in wall.data: + d = wall.data.archipack_wall2[0] + g = d.get_generator() + d.setup_childs(wall, g) + d.relocate_childs(context, wall, g) + elif "archipack_roof" in wall.data: + pass if hole_obj is not None: self.prepare_hole(hole_obj) @@ -559,7 +588,9 @@ class ARCHIPACK_OT_single_boolean(Operator): def poll(cls, context): w = context.active_object return (w.data is not None and - "archipack_wall2" in w.data and + ("archipack_wall2" in w.data or + "archipack_wall" in w.data or + "archipack_roof" in w.data) and len(context.selected_objects) == 2 ) @@ -648,14 +679,18 @@ class ARCHIPACK_OT_generate_hole(Operator): if context.mode == "OBJECT": manager = ArchipackBoolManager(mode='HYBRID') o = context.active_object - d = manager.datablock(o) + + # filter roofs when o is roof + basis = manager.get_basis_type(o) + + d = manager.datablock(o, basis) if d is None: - self.report({'WARNING'}, "Archipack: active object must be a door or a window") + self.report({'WARNING'}, "Archipack: active object must be a door, a window or a roof") return {'CANCELLED'} bpy.ops.object.select_all(action='DESELECT') o.select = True context.scene.objects.active = o - hole = manager._generate_hole(context, o) + hole = manager._generate_hole(context, o, basis) manager.prepare_hole(hole) hole.select = False o.select = True diff --git a/archipack/archipack_cutter.py b/archipack/archipack_cutter.py new file mode 100644 index 00000000..bce82008 --- /dev/null +++ b/archipack/archipack_cutter.py @@ -0,0 +1,910 @@ +# -*- coding:utf-8 -*- + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110- 1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +# + +# ---------------------------------------------------------- +# Author: Stephen Leger (s-leger) +# Cutter / CutAble shared by roof, slab, and floor +# ---------------------------------------------------------- +from mathutils import Vector, Matrix +from mathutils.geometry import interpolate_bezier +from math import cos, sin, pi, atan2 +import bmesh +from bpy.props import ( + FloatProperty, IntProperty, BoolProperty, + StringProperty, EnumProperty + ) +from .archipack_2d import Line + + +class CutterSegment(Line): + + def __init__(self, p, v, type='DEFAULT'): + Line.__init__(self, p, v) + self.type = type + + @property + def copy(self): + return CutterSegment(self.p.copy(), self.v.copy(), self.type) + + def straight(self, length, t=1): + s = self.copy + s.p = self.lerp(t) + s.v = self.v.normalized() * length + return s + + def set_offset(self, offset, last=None): + """ + Offset line and compute intersection point + between segments + """ + self.line = self.make_offset(offset, last) + + def offset(self, offset): + s = self.copy + s.p += offset * self.cross_z.normalized() + return s + + @property + def oposite(self): + s = self.copy + s.p += s.v + s.v = -s.v + return s + + +class CutterGenerator(): + + def __init__(self, d): + self.parts = d.parts + self.operation = d.operation + self.segs = [] + + def add_part(self, part): + + if len(self.segs) < 1: + s = None + else: + s = self.segs[-1] + + # start a new Cutter + if s is None: + v = part.length * Vector((cos(part.a0), sin(part.a0))) + s = CutterSegment(Vector((0, 0)), v, part.type) + else: + s = s.straight(part.length).rotate(part.a0) + s.type = part.type + + self.segs.append(s) + + 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): + # Make last segment implicit closing one + s0 = self.segs[-1] + s1 = self.segs[0] + dp = s1.p0 - s0.p0 + s0.v = dp + + if len(self.segs) > 1: + s0.line = s0.make_offset(self.parts[-1].offset, self.segs[-2].line) + + p1 = s1.line.p1 + s1.line = s1.make_offset(self.parts[0].offset, s0.line) + s1.line.p1 = p1 + + def locate_manipulators(self): + if self.operation == 'DIFFERENCE': + side = -1 + else: + side = 1 + 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: + + 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 = f.straight(side, 0).v.to_3d() + manipulators[0].set_pts([p0, v0, v1]) + + # segment length + manipulators[1].type_key = 'SIZE' + manipulators[1].prop1_name = "length" + manipulators[1].set_pts([p0, p1, (side, 0, 0)]) + + # snap manipulator, dont change index ! + manipulators[2].set_pts([p0, p1, (side, 0, 0)]) + # dumb segment id + manipulators[3].set_pts([p0, p1, (side, 0, 0)]) + + # offset + manipulators[4].set_pts([ + p0, + p0 + f.sized_normal(0, max(0.0001, self.parts[i].offset)).v.to_3d(), + (0.5, 0, 0) + ]) + + def change_coordsys(self, fromTM, toTM): + """ + move shape fromTM into toTM coordsys + """ + dp = (toTM.inverted() * fromTM.translation).to_2d() + da = toTM.row[1].to_2d().angle_signed(fromTM.row[1].to_2d()) + ca = cos(da) + sa = sin(da) + rM = Matrix([ + [ca, -sa], + [sa, ca] + ]) + for s in self.segs: + tp = (rM * s.p0) - s.p0 + dp + s.rotate(da) + s.translate(tp) + + def get_index(self, index): + n_segs = len(self.segs) + if index >= n_segs: + index -= n_segs + return index + + def next_seg(self, index): + idx = self.get_index(index + 1) + return self.segs[idx] + + def last_seg(self, index): + return self.segs[index - 1] + + def get_verts(self, verts, edges): + + n_segs = len(self.segs) - 1 + + for s in self.segs: + verts.append(s.line.p0.to_3d()) + + for i in range(n_segs): + edges.append([i, i + 1]) + + +class CutAblePolygon(): + """ + Simple boolean operations + Cutable generator / polygon + Object MUST have properties + - segs + - holes + - convex + """ + + def inside(self, pt, segs=None): + """ + Point inside poly (raycast method) + support concave polygons + TODO: + make s1 angle different than all othr segs + """ + s1 = Line(pt, Vector((100 * self.xsize, 0.1))) + counter = 0 + if segs is None: + segs = self.segs + for s in segs: + res, p, t, u = s.intersect_ext(s1) + if res: + counter += 1 + return counter % 2 == 1 + + def get_index(self, index): + n_segs = len(self.segs) + if index >= n_segs: + index -= n_segs + return index + + def is_convex(self): + n_segs = len(self.segs) + self.convex = True + sign = False + s0 = self.segs[-1] + for i in range(n_segs): + s1 = self.segs[i] + c = s0.v.cross(s1.v) + if i == 0: + sign = (c > 0) + elif sign != (c > 0): + self.convex = False + return + s0 = s1 + + def get_intersections(self, border, cutter, s_start, segs, start_by_hole): + """ + Detect all intersections + for boundary: store intersection point, t, idx of segment, idx of cutter + sort by t + """ + s_segs = border.segs + b_segs = cutter.segs + s_nsegs = len(s_segs) + b_nsegs = len(b_segs) + inter = [] + + # find all intersections + for idx in range(s_nsegs): + s_idx = border.get_index(s_start + idx) + s = s_segs[s_idx] + for b_idx, b in enumerate(b_segs): + res, p, u, v = s.intersect_ext(b) + if res: + inter.append((s_idx, u, b_idx, v, p)) + + # print("%s" % (self.side)) + # print("%s" % (inter)) + + if len(inter) < 1: + return True + + # sort by seg and param t of seg + inter.sort() + + # reorder so we realy start from s_start + for i, it in enumerate(inter): + if it[0] >= s_start: + order = i + break + + inter = inter[order:] + inter[:order] + + # print("%s" % (inter)) + p0 = border.segs[s_start].p0 + + n_inter = len(inter) - 1 + + for i in range(n_inter): + s_end, u, b_start, v, p = inter[i] + s_idx = border.get_index(s_start) + s = s_segs[s_idx].copy + s.is_hole = not start_by_hole + segs.append(s) + idx = s_idx + max_iter = s_nsegs + # walk through s_segs until intersection + while s_idx != s_end and max_iter > 0: + idx += 1 + s_idx = border.get_index(idx) + s = s_segs[s_idx].copy + s.is_hole = not start_by_hole + segs.append(s) + max_iter -= 1 + segs[-1].p1 = p + + s_start, u, b_end, v, p = inter[i + 1] + b_idx = cutter.get_index(b_start) + s = b_segs[b_idx].copy + s.is_hole = start_by_hole + segs.append(s) + idx = b_idx + max_iter = b_nsegs + # walk through b_segs until intersection + while b_idx != b_end and max_iter > 0: + idx += 1 + b_idx = cutter.get_index(idx) + s = b_segs[b_idx].copy + s.is_hole = start_by_hole + segs.append(s) + max_iter -= 1 + segs[-1].p1 = p + + # add part between last intersection and start point + idx = s_start + s_idx = border.get_index(s_start) + s = s_segs[s_idx].copy + s.is_hole = not start_by_hole + segs.append(s) + max_iter = s_nsegs + # go until end of segment is near start of first one + while (s_segs[s_idx].p1 - p0).length > 0.0001 and max_iter > 0: + idx += 1 + s_idx = border.get_index(idx) + s = s_segs[s_idx].copy + s.is_hole = not start_by_hole + segs.append(s) + max_iter -= 1 + + if len(segs) > s_nsegs + b_nsegs + 1: + # print("slice failed found:%s of:%s" % (len(segs), s_nsegs + b_nsegs)) + return False + + for i, s in enumerate(segs): + s.p0 = segs[i - 1].p1 + + return True + + def slice(self, cutter): + """ + Simple 2d Boolean between boundary and roof part + Dosen't handle slicing roof into multiple parts + + 4 cases: + 1 pitch has point in boundary -> start from this point + 2 boundary has point in pitch -> start from this point + 3 no points inside -> find first crossing segment + 4 not points inside and no crossing segments + """ + # print("************") + + # keep inside or cut inside + # keep inside must be CCW + # cut inside must be CW + keep_inside = (cutter.operation == 'INTERSECTION') + + start = -1 + + f_segs = self.segs + c_segs = cutter.segs + store = [] + + slice_res = True + is_inside = False + + # find if either a cutter or + # cutter intersects + # (at least one point of any must be inside other one) + + # find a point of this pitch inside cutter + for i, s in enumerate(f_segs): + res = self.inside(s.p0, c_segs) + if res: + is_inside = True + if res == keep_inside: + start = i + # print("pitch pt %sside f_start:%s %s" % (in_out, start, self.side)) + slice_res = self.get_intersections(self, cutter, start, store, True) + break + + # seek for point of cutter inside pitch + for i, s in enumerate(c_segs): + res = self.inside(s.p0) + if res: + is_inside = True + # no pitch point found inside cutter + if start < 0 and res == keep_inside: + start = i + # print("cutter pt %sside c_start:%s %s" % (in_out, start, self.side)) + # swap cutter / pitch so we start from cutter + slice_res = self.get_intersections(cutter, self, start, store, False) + break + + # no points found at all + if start < 0: + # print("no pt inside") + return + + if not slice_res: + # print("slice fails") + # found more segments than input + # cutter made more than one loop + return + + if len(store) < 1: + if is_inside: + # print("not touching, add as hole") + self.holes.append(cutter) + return + + self.segs = store + self.is_convex() + + +class CutAbleGenerator(): + + def bissect(self, bm, + plane_co, + plane_no, + dist=0.001, + use_snap_center=False, + clear_outer=True, + clear_inner=False + ): + 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 + ) + + def cut_holes(self, bm, cutable, offset={'DEFAULT': 0}): + o_keys = offset.keys() + has_offset = len(o_keys) > 1 or offset['DEFAULT'] != 0 + # cut holes + for hole in cutable.holes: + + if has_offset: + + for s in hole.segs: + if s.length > 0: + if s.type in o_keys: + of = offset[s.type] + else: + of = offset['DEFAULT'] + p0 = s.p0 + s.cross_z.normalized() * of + self.bissect(bm, p0.to_3d(), s.cross_z.to_3d(), clear_outer=False) + + # compute boundary with offset + new_s = None + segs = [] + for s in hole.segs: + if s.length > 0: + if s.type in o_keys: + of = offset[s.type] + else: + of = offset['DEFAULT'] + new_s = s.make_offset(of, new_s) + segs.append(new_s) + # last / first intersection + if len(segs) > 0: + res, p0, t = segs[0].intersect(segs[-1]) + if res: + segs[0].p0 = p0 + segs[-1].p1 = p0 + + else: + for s in hole.segs: + if s.length > 0: + self.bissect(bm, s.p0.to_3d(), s.cross_z.to_3d(), clear_outer=False) + # use hole boundary + segs = hole.segs + if len(segs) > 0: + # when hole segs are found clear parts inside hole + f_geom = [f for f in bm.faces + if cutable.inside( + f.calc_center_median().to_2d(), + segs=segs)] + if len(f_geom) > 0: + bmesh.ops.delete(bm, geom=f_geom, context=5) + + def cut_boundary(self, bm, cutable, offset={'DEFAULT': 0}): + o_keys = offset.keys() + has_offset = len(o_keys) > 1 or offset['DEFAULT'] != 0 + # cut outside parts + if has_offset: + for s in cutable.segs: + if s.length > 0: + if s.type in o_keys: + of = offset[s.type] + else: + of = offset['DEFAULT'] + p0 = s.p0 + s.cross_z.normalized() * of + self.bissect(bm, p0.to_3d(), s.cross_z.to_3d(), clear_outer=cutable.convex) + else: + for s in cutable.segs: + if s.length > 0: + self.bissect(bm, s.p0.to_3d(), s.cross_z.to_3d(), clear_outer=cutable.convex) + + if not cutable.convex: + f_geom = [f for f in bm.faces + if not cutable.inside(f.calc_center_median().to_2d())] + if len(f_geom) > 0: + bmesh.ops.delete(bm, geom=f_geom, context=5) + + +def update_hole(self, context): + # update parent's only when manipulated + self.update(context, update_parent=True) + + +class ArchipackCutterPart(): + """ + Cutter segment PropertyGroup + + Childs MUST implements + -find_in_selection + Childs MUST define + -type EnumProperty + """ + length = FloatProperty( + name="length", + min=0.01, + max=1000.0, + default=2.0, + update=update_hole + ) + a0 = FloatProperty( + name="angle", + min=-2 * pi, + max=2 * pi, + default=0, + subtype='ANGLE', unit='ROTATION', + update=update_hole + ) + offset = FloatProperty( + name="offset", + min=0, + default=0, + update=update_hole + ) + + def find_in_selection(self, context): + raise NotImplementedError + + def draw(self, layout, context, index): + box = layout.box() + box.prop(self, "type", text=str(index + 1)) + box.prop(self, "length") + # box.prop(self, "offset") + box.prop(self, "a0") + + def update(self, context, update_parent=False): + props = self.find_in_selection(context) + if props is not None: + props.update(context, update_parent=update_parent) + + +def update_operation(self, context): + self.reverse(context, make_ccw=(self.operation == 'INTERSECTION')) + + +def update_path(self, context): + self.update_path(context) + + +def update(self, context): + self.update(context) + + +def update_manipulators(self, context): + self.update(context, manipulable_refresh=True) + + +class ArchipackCutter(): + n_parts = IntProperty( + name="parts", + min=1, + default=1, update=update_manipulators + ) + z = FloatProperty( + name="dumb z", + description="Dumb z for manipulator placeholder", + default=0.01, + options={'SKIP_SAVE'} + ) + user_defined_path = StringProperty( + name="user defined", + update=update_path + ) + user_defined_resolution = IntProperty( + name="resolution", + min=1, + max=128, + default=12, update=update_path + ) + operation = EnumProperty( + items=( + ('DIFFERENCE', 'Difference', 'Cut inside part', 0), + ('INTERSECTION', 'Intersection', 'Keep inside part', 1) + ), + default='DIFFERENCE', + update=update_operation + ) + auto_update = BoolProperty( + options={'SKIP_SAVE'}, + default=True, + update=update_manipulators + ) + # UI layout related + parts_expand = BoolProperty( + default=False + ) + closed = BoolProperty( + description="keep closed to be wall snap manipulator compatible", + options={'SKIP_SAVE'}, + default=True + ) + + def draw(self, layout, context): + box = layout.box() + row = box.row() + if self.parts_expand: + row.prop(self, 'parts_expand', icon="TRIA_DOWN", icon_only=True, text="Parts", emboss=False) + box.prop(self, 'n_parts') + for i, part in enumerate(self.parts): + if i < self.n_parts: + part.draw(layout, context, i) + else: + row.prop(self, 'parts_expand', icon="TRIA_RIGHT", icon_only=True, text="Parts", emboss=False) + + def update_parts(self): + # print("update_parts") + # remove rows + # NOTE: + # n_parts+1 + # as last one is end point of last segment or closing one + for i in range(len(self.parts), self.n_parts + 1, -1): + self.parts.remove(i - 1) + + # add rows + for i in range(len(self.parts), self.n_parts + 1): + self.parts.add() + + self.setup_manipulators() + + def update_parent(self, context): + raise NotImplementedError + + def setup_manipulators(self): + 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) + if n_manips < 5: + s = p.manipulators.add() + s.type_key = "SIZE" + s.prop1_name = "offset" + p.manipulators[2].prop1_name = str(i) + p.manipulators[3].prop1_name = str(i + 1) + + def get_generator(self): + g = CutterGenerator(self) + for i, part in enumerate(self.parts): + g.add_part(part) + g.set_offset() + g.close() + return g + + 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 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 ensure_direction(self): + # get segs ensure they are cw or ccw depending on operation + # whatever the user do with points + g = self.get_generator() + pts = [seg.p0.to_3d() for seg in g.segs] + if self.is_cw(pts) != (self.operation == 'INTERSECTION'): + return g + g.segs = [s.oposite for s in reversed(g.segs)] + return g + + def from_spline(self, context, 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) == (self.operation == 'INTERSECTION'): + pts = list(reversed(pts)) + + pt = wM.inverted() * pts[0] + + # pretranslate + o = self.find_in_selection(context, self.auto_update) + o.matrix_world = wM * Matrix([ + [1, 0, 0, pt.x], + [0, 1, 0, pt.y], + [0, 0, 1, pt.z], + [0, 0, 0, 1] + ]) + self.auto_update = False + self.from_points(pts) + self.auto_update = True + self.update_parent(context, o) + + def from_points(self, pts): + + self.n_parts = len(pts) - 2 + + 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 + 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 + + def reverse(self, context, make_ccw=False): + + o = self.find_in_selection(context, self.auto_update) + + g = self.get_generator() + + pts = [seg.p0.to_3d() for seg in g.segs] + + if self.is_cw(pts) != make_ccw: + return + + types = [p.type for p in self.parts] + + pts.append(pts[0]) + + pts = list(reversed(pts)) + self.auto_update = False + + self.from_points(pts) + + for i, type in enumerate(reversed(types)): + self.parts[i].type = type + self.auto_update = True + self.update_parent(context, o) + + def update_path(self, context): + user_def_path = context.scene.objects.get(self.user_defined_path) + if user_def_path is not None and user_def_path.type == 'CURVE': + self.from_spline(context, + user_def_path.matrix_world, + self.user_defined_resolution, + user_def_path.data.splines[0]) + + def make_surface(self, o, verts, edges): + bm = bmesh.new() + 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]])) + bm.edges.new((bm.verts[-1], bm.verts[0])) + bm.edges.ensure_lookup_table() + bm.to_mesh(o.data) + bm.free() + + def update(self, context, manipulable_refresh=False, update_parent=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 = [] + edges = [] + + g = self.get_generator() + g.locate_manipulators() + + # vertex index in order to build axis + g.get_verts(verts, edges) + + if len(verts) > 2: + self.make_surface(o, verts, edges) + + # enable manipulators rebuild + if manipulable_refresh: + self.manipulable_refresh = True + + # update parent on direct edit + if manipulable_refresh or update_parent: + self.update_parent(context, o) + + # restore context + self.restore_context(context) + + def manipulable_setup(self, context): + + self.manipulable_disable(context) + o = context.active_object + + n_parts = self.n_parts + 1 + + 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 + self.manip_stack.append(part.manipulators[1].setup(context, o, part)) + # index + self.manip_stack.append(part.manipulators[3].setup(context, o, self)) + # offset + # self.manip_stack.append(part.manipulators[4].setup(context, o, part)) + + # snap point + self.manip_stack.append(part.manipulators[2].setup(context, o, self)) diff --git a/archipack/archipack_door.py b/archipack/archipack_door.py index f29c44d1..ffc1e4c4 100644 --- a/archipack/archipack_door.py +++ b/archipack/archipack_door.py @@ -37,7 +37,6 @@ 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 @@ -549,7 +548,7 @@ class archipack_door_panel(ArchipackObject, PropertyGroup): 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] @@ -698,6 +697,9 @@ class ARCHIPACK_OT_door_panel(Operator): ), default='BOTH' ) + material = StringProperty( + default="" + ) def draw(self, context): layout = self.layout @@ -736,8 +738,11 @@ class ARCHIPACK_OT_door_panel(Operator): o.lock_scale[2] = True o.select = True context.scene.objects.active = o + m = o.archipack_material.add() + m.category = "door" + m.material = self.material d.update(context) - MaterialUtils.add_door_materials(o) + # MaterialUtils.add_door_materials(o) o.lock_rotation[0] = True o.lock_rotation[1] = True return o @@ -1049,8 +1054,14 @@ class archipack_door(ArchipackObject, Manipulable, PropertyGroup): 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) + bpy.ops.archipack.door_panel( + x=self.x, + z=self.z, + door_y=self.door_y, + n_panels=self.n_panels, + direction=self.direction, + material=o.archipack_material[0].material + ) child = context.active_object child.parent = o child.matrix_world = o.matrix_world.copy() @@ -1062,9 +1073,16 @@ class archipack_door(ArchipackObject, Manipulable, PropertyGroup): 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) + 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, + material=o.archipack_material[0].material + ) child = context.active_object + child.parent = o child.matrix_world = o.matrix_world.copy() location = self.x / 2 + BATTUE - SPACING @@ -1123,7 +1141,9 @@ class archipack_door(ArchipackObject, Manipulable, PropertyGroup): p.lock_scale[2] = True p.parent = linked p.matrix_world = linked.matrix_world.copy() - p.location = child.location.copy() + m = p.archipack_material.add() + m.category = 'door' + m.material = o.archipack_material[0].material else: p = l_childs[order[i]] @@ -1135,7 +1155,7 @@ class archipack_door(ArchipackObject, Manipulable, PropertyGroup): if handle is not None: if h is None: h = create_handle(context, p, handle.data) - MaterialUtils.add_handle_materials(h) + # MaterialUtils.add_handle_materials(h) h.location = handle.location.copy() elif h is not None: context.scene.objects.unlink(h) @@ -1223,7 +1243,8 @@ class archipack_door(ArchipackObject, Manipulable, PropertyGroup): panels_distrib=self.panels_distrib, panels_x=self.panels_x, panels_y=self.panels_y, - handle=handle + handle=handle, + material=o.archipack_material[0].material ) child = context.active_object # parenting at 0, 0, 0 before set object matrix_world @@ -1297,7 +1318,11 @@ class archipack_door(ArchipackObject, Manipulable, PropertyGroup): hole_obj['archipack_hole'] = True hole_obj.parent = o hole_obj.matrix_world = o.matrix_world.copy() - MaterialUtils.add_wall2_materials(hole_obj) + + hole_obj.data.materials.clear() + for mat in o.data.materials: + hole_obj.data.materials.append(mat) + hole = self.hole v = Vector((0, 0, 0)) offset = Vector((0, -0.001, 0)) @@ -1324,7 +1349,7 @@ class archipack_door(ArchipackObject, Manipulable, PropertyGroup): 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 @@ -1534,8 +1559,8 @@ class ARCHIPACK_OT_door(ArchipackCreateTool, Operator): context.scene.objects.link(o) o.select = True context.scene.objects.active = o - self.load_preset(d) self.add_material(o) + self.load_preset(d) o.select = True context.scene.objects.active = o return o @@ -1811,7 +1836,6 @@ class ARCHIPACK_OT_door_preset(ArchipackPreset, Operator): @property def blacklist(self): - # 'x', 'y', 'z', 'direction', return ['manipulators'] diff --git a/archipack/archipack_floor.py b/archipack/archipack_floor.py index 7f02c2dd..29957716 100644 --- a/archipack/archipack_floor.py +++ b/archipack/archipack_floor.py @@ -14,826 +14,1221 @@ # # 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. +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # # ##### END GPL LICENSE BLOCK ##### # # ---------------------------------------------------------- -# Base code inspired by JARCH Vis -# Original Author: Jacob Morris -# Author : Stephen Leger (s-leger) +# Author: Jacob Morris - Stephen Leger (s-leger) # ---------------------------------------------------------- import bpy from bpy.types import Operator, PropertyGroup, Mesh, Panel from bpy.props import ( - BoolProperty, EnumProperty, FloatProperty, - IntProperty, CollectionProperty + FloatProperty, CollectionProperty, StringProperty, + BoolProperty, IntProperty, EnumProperty ) -from random import uniform, randint -from math import tan, pi, sqrt -from mathutils import Vector +from mathutils import Vector, Matrix +from mathutils.geometry import interpolate_bezier +from random import uniform +from math import radians, cos, sin, pi, atan2, sqrt +import bmesh from .bmesh_utils import BmeshEdit as bmed -from .archipack_manipulator import Manipulable +from .archipack_2d import Line, Arc +from .archipack_manipulator import Manipulable, archipack_manipulator from .archipack_preset import ArchipackPreset, PresetMenuOperator from .archipack_object import ArchipackCreateTool, ArchipackObject +from .archipack_cutter import ( + CutAblePolygon, CutAbleGenerator, + ArchipackCutter, + ArchipackCutterPart + ) -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 - +# ------------------------------------------------------------------ +# Define property class to store object parameters and update mesh +# ------------------------------------------------------------------ -def tile_ls(ow, ol, tw, tl, s, z): - """ - pattern: - _____ - | |_| - |___| - x and y are axis of big one - """ +class Floor(): - verts = [] - faces = [] + def __init__(self): + # self.colour_inactive = (1, 1, 1, 1) + pass - # 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) + def set_offset(self, offset, last=None): + """ + Offset line and compute intersection point + between segments + """ + self.line = self.make_offset(offset, last) - # small, offset from big x,y - xo = 0.75 * tw - yo = 0.25 * tl + def straight_floor(self, a0, length): + s = self.straight(length).rotate(a0) + return StraightFloor(s.p, s.v) - # offset for pattern - rx = 2.5 * tw - ry = 0.5 * tl + def curved_floor(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 CurvedFloor(c, radius, a0, da) - # 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 +class StraightFloor(Floor, Line): - while y < ol_y: + def __init__(self, p, v): + Line.__init__(self, p, v) + Floor.__init__(self) - while x < ow_x: - p = len(verts) +class CurvedFloor(Floor, Arc): - # 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: + def __init__(self, c, radius, a0, da): + Arc.__init__(self, c, radius, a0, da) + Floor.__init__(self) - 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) +class FloorGenerator(CutAblePolygon, CutAbleGenerator): - if x1 > 0 and x0 < ow and y0 < ol: + def __init__(self, parts): + self.parts = parts + self.segs = [] + self.holes = [] + self.convex = True + self.xsize = 0 - 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)) + def add_part(self, part): - x += rx - - y += ry - x = x % rx - tw - if x < -tw: - x += rx - - return verts, faces + if len(self.segs) < 1: + s = None + else: + s = self.segs[-1] + # start a new floor + if s is None: + if part.type == 'S_SEG': + p = Vector((0, 0)) + v = part.length * Vector((cos(part.a0), sin(part.a0))) + s = StraightFloor(p, v) + elif part.type == 'C_SEG': + c = -part.radius * Vector((cos(part.a0), sin(part.a0))) + s = CurvedFloor(c, part.radius, part.a0, part.da) + else: + if part.type == 'S_SEG': + s = s.straight_floor(part.a0, part.length) + elif part.type == 'C_SEG': + s = s.curved_floor(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: + 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].line) + + 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__ == "StraightFloor": + # 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 s in self.segs: + if "Curved" in type(s).__name__: + for i in range(16): + # x, y = floor.line.lerp(i / 16) + verts.append(s.lerp(i / 16).to_3d()) + else: + # x, y = s.line.p0 + verts.append(s.p0.to_3d()) + """ + for i in range(33): + x, y = floor.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 limits(self): + x_size = [s.p0.x for s in self.segs] + y_size = [s.p0.y for s in self.segs] + self.xmin = min(x_size) + self.xmax = max(x_size) + self.xsize = self.xmax - self.xmin + self.ymin = min(y_size) + self.ymax = max(y_size) + self.ysize = self.ymax - self.ymin + + def cut(self, context, o): + """ + either external or holes cuts + """ + self.limits() + self.is_convex() + for b in o.children: + d = archipack_floor_cutter.datablock(b) + if d is not None: + g = d.ensure_direction() + g.change_coordsys(b.matrix_world, o.matrix_world) + self.slice(g) + + def floor(self, context, o, d): + + verts, faces, matids, uvs = [], [], [], [] + + if d.bevel: + bevel = d.bevel_amount + else: + bevel = 0 -def tile_hexagon(ow, ol, tw, s, z): - verts = [] - faces = [] - offset = False + if d.add_grout: + thickness = min(d.thickness - d.mortar_depth, d.thickness - 0.0001) + bottom = min(d.thickness - (d.mortar_depth + bevel), d.thickness - 0.0001) + else: + thickness = d.thickness + bottom = 0 - w = tw / 2 - y = 0.0 - h = w * tan(pi / 6) - r = sqrt((w * w) + (h * h)) + self.top = d.thickness - while y < ol + tw: - if not offset: - x = 0.0 - else: - x = w + (s / 2) + self.generate_pattern(d, verts, faces, matids, uvs) + bm = bmed.buildmesh( + context, o, verts, faces, matids=matids, uvs=uvs, + weld=False, clean=False, auto_smooth=True, temporary=True) - while x < ow + tw: - p = len(verts) + self.cut_holes(bm, self) + self.cut_boundary(bm, self) - 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)]) + bmesh.ops.dissolve_limit(bm, + angle_limit=0.01, + use_dissolve_boundaries=False, + verts=bm.verts, + edges=bm.edges, + delimit=1) - x += tw + s + bm.verts.ensure_lookup_table() - y += r + h + s - offset = not offset + # solidify and floor bottom + geom = bm.faces[:] + verts = bm.verts[:] + edges = bm.edges[:] + bmesh.ops.solidify(bm, geom=geom, thickness=0.0001) + for v in verts: + v.co.z = bottom - return verts, faces + # bevel + if d.bevel: + for v in bm.verts: + v.select = True + for v in verts: + v.select = False + for v in bm.edges: + v.select = True + for v in edges: + v.select = False + geom = [v for v in bm.verts if v.select] + geom.extend([v for v in bm.edges if v.select]) + bmesh.ops.bevel(bm, + geom=geom, + offset=d.bevel_amount, + offset_type=0, + segments=1, # d.bevel_res + profile=0.5, + vertex_only=False, + clamp_overlap=False, + material=-1) + + bm.to_mesh(o.data) + bm.free() + # Grout + if d.add_grout: + verts = [] + self.get_verts(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) + + self.cut_holes(bm, self) + self.cut_boundary(bm, self) + + bmesh.ops.dissolve_limit(bm, + angle_limit=0.01, + use_dissolve_boundaries=False, + verts=bm.verts, + edges=bm.edges, + delimit=1) + + bm.verts.ensure_lookup_table() + + geom = bm.faces[:] + bmesh.ops.solidify(bm, geom=geom, thickness=thickness) + bmed.bmesh_join(context, o, [bm], normal_update=True) + + bpy.ops.object.mode_set(mode='OBJECT') + + # --------------------------------------------------- + # Patterns + # --------------------------------------------------- + + def regular_tile(self, d, verts, faces, matids, uvs): + """ + ____ ____ ____ + | || || | Regular tile, rows can be offset, either manually or randomly + |____||____||____| + ____ ____ ____ + | || || | + |____||____||____| + """ + off = False + o = 1 / (100 / d.offset) if d.offset != 0 else 0 + y = self.ymin + + while y < self.ymax: + x = self.xmin + tl2 = d.tile_length + if y < self.ymax < y + d.tile_length: + tl2 = self.ymax - y + + while x < self.xmax: + tw2 = d.tile_width + + if x < self.xmax < x + d.tile_width: + tw2 = self.xmax - x + elif x == self.xmin and off and not d.random_offset: + tw2 = d.tile_width * o + elif x == self.xmin and d.random_offset: + v = d.tile_width * d.offset_variance * 0.0049 + tw2 = (d.tile_width / 2) + uniform(-v, v) + + self.add_plane(d, verts, faces, matids, uvs, x, y, tw2, tl2) + x += tw2 + d.spacing + + y += tl2 + d.spacing + off = not off + + def hopscotch(self, d, verts, faces, matids, uvs): + """ + ____ _ Large tile, plus small one on top right corner + | ||_| + |____| ____ _ But shifted up so next large one is right below previous small one + | ||_| + |____| + """ + sp = d.spacing + + # movement variables + row = 0 + + tw = d.tile_width + tl = d.tile_length + s_tw = (tw - sp) / 2 # small tile width + s_tl = (tl - sp) / 2 # small tile length + y = self.ymin - s_tl + + pre_y = y + while y < self.ymax + s_tl or (row == 2 and y - sp < self.ymax): + x = self.xmin + step_back = True + + if row == 1: # row start indented slightly + x = self.xmin + s_tw + sp + + while x < self.xmax: + if row == 0 or row == 1: + # adjust for if there is a need to cut off the bottom of the tile + if y < self.ymin - s_tl: + self.add_plane(d, verts, faces, matids, uvs, x, y, tw, tl + y - self.ymin) # large one + else: + self.add_plane(d, verts, faces, matids, uvs, x, y, tw, tl) # large one -def tile_lms(ow, ol, tw, s, z): - verts = [] - faces = [] - small = True + self.add_plane(d, verts, faces, matids, uvs, x + tw + sp, y + s_tl + sp, s_tw, s_tl) # small one - y = 0.0 - ref = (tw - s) / 2 + if step_back: + x += tw + sp + y -= s_tl + sp + else: + x += tw + s_tw + 2 * sp + y += s_tl + sp - 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 + step_back = not step_back 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 + if x == self.xmin: # half width for starting position + self.add_plane(d, verts, faces, matids, uvs, x, y, s_tw, tl) # large one + # small one on right + self.add_plane(d, verts, faces, matids, uvs, x + s_tw + sp, y + s_tl + sp, s_tw, s_tl) + # small one on bottom + self.add_plane(d, verts, faces, matids, uvs, x, y - sp - s_tl, s_tw, s_tl) + x += (2 * s_tw) + tw + (3 * sp) + else: + self.add_plane(d, verts, faces, matids, uvs, x, y, tw, tl) # large one + # small one on right + self.add_plane(d, verts, faces, matids, uvs, x + tw + sp, y + s_tl + sp, s_tw, s_tl) + x += (2 * tw) + (3 * sp) + s_tw + if row == 0 or row == 2: + y = pre_y + tl + sp + else: + y = pre_y + s_tl + sp + pre_y = y + + row = (row + 1) % 3 # keep wrapping rows + + def stepping_stone(self, d, verts, faces, matids, uvs): + """ + ____ __ ____ + | ||__|| | Row of large one, then two small ones stacked beside it + | | __ | | + |____||__||____| + __ __ __ __ + |__||__||__||__| Row of smalls + """ + sp = d.spacing + y = self.ymin + row = 0 + + tw = d.tile_width + tl = d.tile_length + s_tw = (tw - sp) / 2 + s_tl = (tl - sp) / 2 + + while y < self.ymax: + x = self.xmin + + while x < self.xmax: + if row == 0: # large one then two small ones stacked beside it + self.add_plane(d, verts, faces, matids, uvs, x, y, tw, tl) + self.add_plane(d, verts, faces, matids, uvs, x + tw + sp, y, s_tw, s_tl,) + self.add_plane(d, verts, faces, matids, uvs, x + tw + sp, y + s_tl + sp, s_tw, s_tl) + x += tw + s_tw + (2 * sp) + else: # row of small ones + self.add_plane(d, verts, faces, matids, uvs, x, y, s_tw, s_tl) + self.add_plane(d, verts, faces, matids, uvs, x + s_tw + sp, y, s_tw, s_tl) + x += tw + sp + + if row == 0: + y += tl + sp + else: + y += s_tl + sp + + row = (row + 1) % 2 + + def hexagon(self, d, verts, faces, matids, uvs): + """ + __ Hexagon tiles + / \ + \___/ + """ + sp = d.spacing + width = d.tile_width + dia = (width / 2) / cos(radians(30)) + # top of current, half way up next, vertical spacing component + vertical_spacing = dia * (1 + sin(radians(30))) + (sp * sin(radians(60))) # center of one row to next row + da = pi / 3 + base_points = [(sin(i * da), cos(i * da)) for i in range(6)] + + y = self.ymin + offset = False + while y - width / 2 < self.ymax: # place tile as long as bottom is still within bounds + if offset: + x = self.xmin + width / 2 + else: + x = self.xmin - sp / 2 -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 x - width / 2 < self.xmax: # place tile as long as left is still within bounds + f = len(verts) - while y < ol: + if d.vary_thickness and d.thickness_variance > 0: + v = d.thickness / 100 * d.thickness_variance + z = uniform(self.top, self.top + v) + else: + z = self.top + + for pt in base_points: + verts.append((dia * pt[0] + x, dia * pt[1] + y, z)) + + faces.append([f] + [i for i in range(f + 1, len(verts))]) + uvs.append(base_points) + self.add_matid(d, matids) + + x += width + sp + + y += vertical_spacing + offset = not offset + + def windmill(self, d, verts, faces, matids, uvs): + """ + __ ____ + | ||____| This also has a square one in the middle, totaling 5 tiles per pattern + |__| __ + ____ | | + |____||__| + """ + sp = d.spacing + + tw = d.tile_width + tl = d.tile_length + s_tw = (tw - sp) / 2 + s_tl = (tl - sp) / 2 + + y = self.ymin + while y < self.ymax: + x = self.xmin + + while x < self.xmax: + self.add_plane(d, verts, faces, matids, uvs, x, y, tw, s_tl) # bottom + self.add_plane(d, verts, faces, matids, uvs, x + tw + sp, y, s_tw, tl, rotate_uv=True) # right + self.add_plane(d, verts, faces, matids, uvs, x + s_tw + sp, y + tl + sp, tw, s_tl) # top + self.add_plane(d, verts, faces, matids, uvs, x, y + s_tl + sp, s_tw, tl, rotate_uv=True) # left + self.add_plane(d, verts, faces, matids, uvs, x + s_tw + sp, y + s_tl + sp, s_tw, s_tl) # center + + x += tw + s_tw + (2 * sp) + y += tl + s_tl + (2 * sp) + + def boards(self, d, verts, faces, matids, uvs): + """ + ||| Typical wood boards + ||| + """ + x = self.xmin + bw, bl = d.board_width, d.board_length + off = False + o = 1 / (100 / d.offset) if d.offset != 0 else 0 + + while x < self.xmax: + if d.vary_width: + v = bw * (d.width_variance / 100) * 0.99 + bw2 = bw + uniform(-v, v) + else: + bw2 = bw + + if bw2 + x > self.xmax: + bw2 = self.xmax - x + y = self.ymin + + counter = 1 + while y < self.ymax: + bl2 = bl + if d.vary_length: + v = bl * (d.length_variance / 100) * 0.99 + bl2 = bl + uniform(-v, v) + elif y == self.ymin and off and not d.random_offset: + bl2 = bl * o + elif y == self.ymin and d.random_offset: + v = bl * d.offset_variance * 0.0049 + bl2 = (bl / 2) + uniform(-v, v) + + if (counter >= d.max_boards and d.vary_length) or y + bl2 > self.ymax: + bl2 = self.ymax - y + + self.add_plane(d, verts, faces, matids, uvs, x, y, bw2, bl2, rotate_uv=True) + y += bl2 + d.length_spacing + counter += 1 + off = not off + x += bw2 + d.width_spacing + + def square_parquet(self, d, verts, faces, matids, uvs): + """ + ||--||-- Alternating groups oriented either horizontally, or forwards and backwards. + ||--||-- self.spacing is used because it is the same spacing for width and length + --||--|| Board width is calculated using number of boards and the length. + --||--|| + """ + x = self.xmin + start_orient_length = True + + # figure board width + bl = d.short_board_length + bw = (bl - (d.boards_in_group - 1) * d.spacing) / d.boards_in_group + while x < self.xmax: + y = self.ymin + orient_length = start_orient_length + while y < self.ymax: + + if orient_length: + start_x = x + + for i in range(d.boards_in_group): + if x < self.xmax and y < self.ymax: + self.add_plane(d, verts, faces, matids, uvs, x, y, bw, bl, rotate_uv=True) + x += bw + d.spacing + + x = start_x + y += bl + d.spacing - 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) + else: + for i in range(d.boards_in_group): + if x < self.xmax and y < self.ymax: + self.add_plane(d, verts, faces, matids, uvs, x, y, bl, bw) + y += bw + d.spacing - while x < ow: - p = len(verts) - x0 = max(0, x) - x1 = min(ow, x + tw) + orient_length = not orient_length - 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 + start_orient_length = not start_orient_length + x += bl + d.spacing - y += tl + s - off = not off + def herringbone(self, d, verts, faces, matids, uvs): + """ + Boards are at 45 degree angle, in chevron pattern, ends are angled + """ + width_dif = d.board_width / cos(radians(45)) + x_dif = d.short_board_length * cos(radians(45)) + y_dif = d.short_board_length * sin(radians(45)) + total_y_dif = width_dif + y_dif + sp_dif = d.spacing / cos(radians(45)) - return verts, faces + y = self.ymin - y_dif + while y < self.ymax: + x = self.xmin + while x < self.xmax: + # left side -def wood_parquet(ow, ol, bw, s, num_boards, z): - verts = [] - faces = [] - x = 0.0 + self.add_face(d, verts, faces, matids, uvs, + (x, y, 0), (x + x_dif, y + y_dif, 0), + (x + x_dif, y + total_y_dif, 0), (x, y + width_dif, 0)) - start_orient_length = True + x += x_dif + d.spacing - # figure board length - bl = (bw * num_boards) + (s * (num_boards - 1)) - while x < ow: + # right side + if x < self.xmax: + self.add_face(d, verts, faces, matids, uvs, + (x, y + y_dif, 0), (x + x_dif, y, 0), + (x + x_dif, y + width_dif, 0), (x, y + total_y_dif, 0)) + x += x_dif + d.spacing - y = 0.0 + y += width_dif + sp_dif # adjust spacing amount for 45 degree angle - orient_length = start_orient_length + def herringbone_parquet(self, d, verts, faces, matids, uvs): + """ + Boards are at 45 degree angle, in chevron pattern, ends are square, not angled + """ - while y < ol: + an_45 = 0.5 * sqrt(2) - if orient_length: - y0 = y - y1 = min(y + bl, ol) + x_dif = d.short_board_length * an_45 + y_dif = d.short_board_length * an_45 + y_dif_45 = d.board_width * an_45 + x_dif_45 = d.board_width * an_45 + total_y_dif = y_dif + y_dif_45 - for i in range(num_boards): + sp_dif = (d.spacing / an_45) / 2 # divide by two since it is used for both x and y + width_dif = d.board_width / an_45 - bx = x + i * (bw + s) + y = self.ymin - y_dif + while y - y_dif_45 < self.ymax: # continue as long as bottom left corner is still good + x = self.xmin - if bx < ow and y < ol: + while x - x_dif_45 < self.xmax: # continue as long as top left corner is still good + # left side - # make sure board should be placed - x0 = bx - x1 = min(bx + bw, ow) + self.add_face(d, verts, faces, matids, uvs, + (x, y, 0), + (x + x_dif, y + y_dif, 0), + (x + x_dif - x_dif_45, y + total_y_dif, 0), + (x - x_dif_45, y + y_dif_45, 0)) - 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)) + x += x_dif - x_dif_45 + sp_dif + y0 = y + y_dif - y_dif_45 - sp_dif - else: - x0 = x - x1 = min(x + bl, ow) + if x < self.xmax: + self.add_face(d, verts, faces, matids, uvs, + (x, y0, 0), + (x + x_dif, y0 - y_dif, 0), + (x + x_dif + x_dif_45, y0 - y_dif + y_dif_45, 0), + (x + x_dif_45, y0 + y_dif_45, 0)) - for i in range(num_boards): + x += x_dif + x_dif_45 + sp_dif - by = y + i * (bw + s) + else: # we didn't place the right board, so step ahead far enough the the while loop for x breaks + break - if x < ow and by < ol: - y0 = by - y1 = min(by + bw, ol) - p = len(verts) + y += width_dif + (2 * sp_dif) - verts.extend([(x0, y0, z), (x1, y0, z), (x1, y1, z), (x0, y1, z)]) - faces.append((p, p + 1, p + 2, p + 3)) + def add_matid(self, d, matids): + if d.vary_materials: + matid = uniform(1, d.matid) + else: + matid = d.matid + matids.append(matid) + + def add_plane(self, d, verts, faces, matids, uvs, x, y, w, l, rotate_uv=False): + """ + Adds vertices and faces for a place, clip to outer boundaries if clip is True + :param x: start x position + :param y: start y position + :param w: width (in x direction) + :param l: length (in y direction) + """ + + x1 = x + w + y1 = y + l + + if d.vary_thickness and d.thickness_variance > 0: + v = d.thickness / 100 * d.thickness_variance + z = uniform(self.top, self.top + v) + else: + z = self.top - y += bl + s + p = len(verts) + verts.extend([(x, y, z), (x1, y, z), (x1, y1, z), (x, y1, z)]) + faces.append([p + 3, p + 2, p + 1, p]) + if rotate_uv: + uvs.append([(0, 0), (0, 1), (1, 1), (1, 0)]) + else: + uvs.append([(0, 0), (1, 0), (1, 1), (0, 1)]) + self.add_matid(d, matids) + + def add_face(self, d, verts, faces, matids, uvs, p0, p1, p2, p3): + """ + Adds vertices and faces for a place, clip to outer boundaries if clip is True + :param x: start x position + :param y: start y position + :param w: width (in x direction) + :param l: length (in y direction) + """ + + if d.vary_thickness and d.thickness_variance > 0: + v = d.thickness / 100 * d.thickness_variance + z = uniform(self.top, self.top + v) + else: + z = self.top + + p = len(verts) + verts.extend([(v[0], v[1], z) for v in [p0, p1, p2, p3]]) + faces.append([p + 3, p + 2, p + 1, p]) + uvs.append([(0, 0), (1, 0), (1, 1), (0, 1)]) + self.add_matid(d, matids) + + def add_manipulator(self, name, pt1, pt2, pt3): + m = self.manipulators.add() + m.prop1_name = name + m.set_pts([pt1, pt2, pt3]) + + def generate_pattern(self, d, verts, faces, matids, uvs): + # clear data before refreshing it + + self.uv_factor = 1 / max(self.xmax, self.ymax) # automatically scale to keep within reasonable bounds + + if d.pattern == "boards": + self.boards(d, verts, faces, matids, uvs) + elif d.pattern == "square_parquet": + self.square_parquet(d, verts, faces, matids, uvs) + elif d.pattern == "herringbone": + self.herringbone(d, verts, faces, matids, uvs) + elif d.pattern == "herringbone_parquet": + self.herringbone_parquet(d, verts, faces, matids, uvs) + elif d.pattern == "regular_tile": + self.regular_tile(d, verts, faces, matids, uvs) + elif d.pattern == "hopscotch": + self.hopscotch(d, verts, faces, matids, uvs) + elif d.pattern == "stepping_stone": + self.stepping_stone(d, verts, faces, matids, uvs) + elif d.pattern == "hexagon": + self.hexagon(d, verts, faces, matids, uvs) + elif d.pattern == "windmill": + self.windmill(d, verts, faces, matids, uvs) - orient_length = not orient_length - start_orient_length = not start_orient_length +def update(self, context): + self.update(context) - x += bl + s - return verts, faces +def update_type(self, context): + d = self.find_in_selection(context) -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 d is not None and d.auto_update: - if is_width_vary: - v = bw * (width_vary / 100) * 0.499 - bw2 = uniform(bw / 2 - v, bw / 2 + v) - else: - bw2 = bw + d.auto_update = False + # find part index + idx = 0 + for i, part in enumerate(d.parts): + if part == self: + idx = i + break - 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) + 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_floor(part.a0, part.length) else: - y = -(row % 2) * bl * (offset / 100) + w = w0.curved_floor(part.a0, part.da, part.radius) + else: + g = FloorGenerator(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: - y = 0 + 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) - row += 1 - counter = 1 + if a0 > pi: + a0 -= 2 * pi + if a0 < -pi: + a0 += 2 * pi + part1.a0 = a0 - while y < ol: + d.auto_update = True - z = th - if is_r_h: - v = z * 0.5 * (r_h / 100) - z = uniform(z / 2 - v, z / 2 + v) +def update_manipulators(self, context): + self.update(context, manipulable_refresh=True) - 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) +def update_path(self, context): + self.update_path(context) - y0 = max(0, y) - y1 = min(y + bl2, ol) - if y1 > y0: - p = len(verts) +class archipack_floor_part(PropertyGroup): - verts.extend([(x, y0, z), (x1, y0, z), (x1, y1, z), (x, y1, z)]) - faces.append((p, p + 1, p + 2, p + 3)) + """ + A single manipulable polyline like segment + polyline like segment line or arc based + """ + 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 + ) + manipulators = CollectionProperty(type=archipack_manipulator) + + 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_floor.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_in_selection(context) + if props is not None: + props.update(context, manipulable_refresh) + + def draw(self, context, layout, index): + box = layout.box() + box.prop(self, "type", text=str(index + 1)) - y += bl2 + s_l + if self.type in ['C_SEG']: + box.prop(self, "radius") + box.prop(self, "da") + else: + box.prop(self, "length") + box.prop(self, "a0") - counter += 1 - x += bw2 + s_w +class archipack_floor(ArchipackObject, Manipulable, PropertyGroup): + n_parts = IntProperty( + name="parts", + min=1, + default=1, update=update_manipulators + ) + parts = CollectionProperty(type=archipack_floor_part) + user_defined_path = StringProperty( + name="user defined", + update=update_path + ) + user_defined_resolution = IntProperty( + name="resolution", + min=1, + max=128, + default=12, update=update_path + ) + closed = BoolProperty( + default=True, + name="Close", + options={'SKIP_SAVE'}, + update=update_manipulators + ) + # UI layout related + parts_expand = BoolProperty( + options={'SKIP_SAVE'}, + default=False + ) - return verts, faces + pattern = EnumProperty( + name='Floor Pattern', items=(("boards", "Boards", ""), ("square_parquet", "Square Parquet", ""), + ("herringbone_parquet", "Herringbone Parquet", ""), + ("herringbone", "Herringbone", ""), ("regular_tile", "Regular Tile", ""), + ("hopscotch", "Hopscotch", ""), ("stepping_stone", "Stepping Stone", ""), + ("hexagon", "Hexagon", ""), ("windmill", "Windmill", "")), + default="boards", update=update + ) + spacing = FloatProperty( + name='Spacing', + description='The amount of space between boards or tiles in both directions', + unit='LENGTH', subtype='DISTANCE', + min=0, + default=0.005, + precision=2, + update=update + ) + thickness = FloatProperty( + name='Thickness', + description='Thickness', + unit='LENGTH', subtype='DISTANCE', + min=0.0, + default=0.005, + precision=2, + update=update + ) + vary_thickness = BoolProperty( + name='Vary Thickness', + description='Vary board thickness?', + default=False, + update=update + ) + thickness_variance = FloatProperty( + name='Thickness Variance', + description='How much board thickness can vary by', + min=0, max=100, + default=25, + precision=2, + subtype='PERCENTAGE', + update=update + ) + board_width = FloatProperty( + name='Board Width', + description='The width of the boards', + unit='LENGTH', subtype='DISTANCE', + min=0.02, + default=0.2, + precision=2, + update=update + ) + vary_width = BoolProperty( + name='Vary Width', + description='Vary board width', + default=False, + update=update + ) + width_variance = FloatProperty( + name='Width Variance', + description='How much board width can vary by', + subtype='PERCENTAGE', + min=1, max=100, default=50, + precision=2, + update=update + ) + width_spacing = FloatProperty( + name='Width Spacing', + description='The amount of space between boards in the width direction', + unit='LENGTH', subtype='DISTANCE', + min=0, + default=0.002, + precision=2, + update=update + ) -def tile_grout(ow, ol, depth, th): - z = min(th - 0.001, max(0.001, th - depth)) - x = ow - y = ol + board_length = FloatProperty( + name='Board Length', + description='The length of the boards', + unit='LENGTH', subtype='DISTANCE', + precision=2, + min=0.02, + default=2, + update=update + ) + short_board_length = FloatProperty( + name='Board Length', + description='The length of the boards', + unit='LENGTH', subtype='DISTANCE', + precision=2, + min=0.02, + default=2, + update=update + ) + vary_length = BoolProperty( + name='Vary Length', + description='Vary board length', + default=False, + update=update + ) + length_variance = FloatProperty( + name='Length Variance', + description='How much board length can vary by', + subtype='PERCENTAGE', + min=1, max=100, default=50, + precision=2, update=update + ) + max_boards = IntProperty( + name='Max Boards', + description='Max number of boards in one row', + min=1, + default=20, + update=update + ) + length_spacing = FloatProperty( + name='Length Spacing', + description='The amount of space between boards in the length direction', + unit='LENGTH', subtype='DISTANCE', + min=0, + default=0.002, + precision=2, + update=update + ) - 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)] + # parquet specific + boards_in_group = IntProperty( + name='Boards in Group', + description='Number of boards in a group', + min=1, default=4, + update=update + ) - 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)] + # tile specific + tile_width = FloatProperty( + name='Tile Width', + description='Width of the tiles', + unit='LENGTH', subtype='DISTANCE', + min=0.002, + default=0.2, + precision=2, + update=update + ) + tile_length = FloatProperty( + name='Tile Length', + description='Length of the tiles', + unit='LENGTH', subtype='DISTANCE', + precision=2, + min=0.02, + default=0.3, + update=update + ) - return verts, faces + # grout + add_grout = BoolProperty( + name='Add Grout', + description='Add grout', + default=False, + update=update + ) + mortar_depth = FloatProperty( + name='Mortar Depth', + description='The depth of the mortar from the surface of the tile', + unit='LENGTH', subtype='DISTANCE', + precision=2, + step=0.005, + min=0, + default=0.001, + update=update + ) + # regular tile + random_offset = BoolProperty( + name='Random Offset', + description='Random amount of offset for each row of tiles', + update=update, default=False + ) + offset = FloatProperty( + name='Offset', + description='How much to offset each row of tiles', + min=0, max=100, default=0, + precision=2, + update=update + ) + offset_variance = FloatProperty( + name='Offset Variance', + description='How much to vary the offset each row of tiles', + min=0.001, max=100, default=50, + precision=2, + update=update + ) -def update(self, context): - self.update(context) + # UV stuff + random_uvs = BoolProperty( + name='Random UV\'s', update=update, default=True, description='Random UV positions for the faces' + ) + # bevel + bevel = BoolProperty( + name='Bevel', update=update, default=False, description='Bevel upper faces' + ) + bevel_amount = FloatProperty( + name='Bevel Amount', + description='Bevel amount', + unit='LENGTH', subtype='DISTANCE', + min=0.0001, default=0.001, + precision=2, step=0.05, + update=update + ) -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( + vary_materials = BoolProperty( name="Vary Material?", default=False, description="Vary Material indexes", update=update) - mat_vary = IntProperty( + matid = IntProperty( name="#variations", min=1, max=10, @@ -841,127 +1236,373 @@ class archipack_floor(ArchipackObject, Manipulable, PropertyGroup): description="Material index maxi", update=update) auto_update = BoolProperty( - options={'SKIP_SAVE'}, - default=True, - update=update + options={'SKIP_SAVE'}, + default=True, + update=update_manipulators + ) + z = FloatProperty( + name="dumb z", + description="Dumb z for manipulator placeholder", + default=0.01, + options={'SKIP_SAVE'} ) + def get_generator(self): + g = FloorGenerator(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 update_parts(self, o): + + for i in range(len(self.parts), self.n_parts, -1): + self.parts.remove(i - 1) + + # add rows + for i in range(len(self.parts), self.n_parts): + self.parts.add() + + self.setup_manipulators() + + g = self.get_generator() + + return g + + @staticmethod + def create_uv_seams(bm): + handled = set() + for edge in bm.edges: + if edge.verts[0].co.z == 0 and edge.verts[1].co.z == 0: # bottom + # make sure both vertices on the edge haven't been handled, this forces one edge to not be made a seam + # leaving the bottom face still attached + if not (edge.verts[0].index in handled and edge.verts[1].index in handled): + edge.seam = True + handled.add(edge.verts[0].index) + handled.add(edge.verts[1].index) + elif edge.verts[0].co.z != edge.verts[1].co.z: # not horizontal, so they are vertical seams + edge.seam = True + + 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, context, 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) + + pt = wM.inverted() * pts[0] + + # pretranslate + o = self.find_in_selection(context, self.auto_update) + o.matrix_world = wM * Matrix([ + [1, 0, 0, pt.x], + [0, 1, 0, pt.y], + [0, 0, 1, pt.z], + [0, 0, 0, 1] + ]) + self.from_points(pts) + + def from_points(self, pts): + + 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 = True + self.auto_update = True + + def update_path(self, context): + user_def_path = context.scene.objects.get(self.user_defined_path) + if user_def_path is not None and user_def_path.type == 'CURVE': + self.from_spline( + context, + user_def_path.matrix_world, + self.user_defined_resolution, + user_def_path.data.splines[0]) + + def add_manipulator(self, name, pt1, pt2, pt3): + m = self.manipulators.add() + m.prop1_name = name + m.set_pts([pt1, pt2, pt3]) + + def update_manipulators(self): + self.manipulators.clear() # clear every time, add new ones + self.add_manipulator("length", (0, 0, 0), (0, self.length, 0), (-0.4, 0, 0)) + self.add_manipulator("width", (0, 0, 0), (self.width, 0, 0), (0.4, 0, 0)) + + z = self.thickness + + if self.pattern == "boards": + self.add_manipulator("board_length", (0, 0, z), (0, self.board_length, z), (0.1, 0, z)) + self.add_manipulator("board_width", (0, 0, z), (self.board_width, 0, z), (-0.2, 0, z)) + elif self.pattern == "square_parquet": + self.add_manipulator("short_board_length", (0, 0, z), (0, self.short_board_length, z), (-0.2, 0, z)) + elif self.pattern in ("herringbone", "herringbone_parquet"): + dia = self.short_board_length * cos(radians(45)) + dia2 = self.board_width * cos(radians(45)) + self.add_manipulator("short_board_length", (0, 0, z), (dia, dia, z), (0, 0, z)) + self.add_manipulator("board_width", (dia, 0, z), (dia - dia2, dia2, z), (0, 0, z)) + else: + tl = self.tile_length + tw = self.tile_width + + if self.pattern in ("regular_tile", "hopscotch", "stepping_stone"): + self.add_manipulator("tile_width", (0, tl, z), (tw, tl, z), (0, 0, z)) + self.add_manipulator("tile_length", (0, 0, z), (0, tl, z), (0, 0, z)) + elif self.pattern == "hexagon": + self.add_manipulator("tile_width", (tw / 2 + self.spacing, 0, z), (tw * 1.5 + self.spacing, 0, z), + (0, 0, 0)) + elif self.pattern == "windmill": + self.add_manipulator("tile_width", (0, 0, z), (tw, 0, 0), (0, 0, z)) + self.add_manipulator("tile_length", (0, tl / 2 + self.spacing, z), (0, tl * 1.5 + self.spacing, z), + (0, 0, z)) + 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 + if len(self.manipulators) < 1: s = self.manipulators.add() - s.prop1_name = "over_length" - # s.prop2_name = "y" - s.type_key = 'SIZE' - - def update(self, context): + 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" + p.manipulators[0].type_key = 'ANGLE' + 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 update(self, context, manipulable_refresh=False): o = self.find_in_selection(context, self.auto_update) if o is None: return - self.setup_manipulators() + # clean up manipulators before any data model change + if manipulable_refresh: + self.manipulable_disable(context) - 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 + g = self.update_parts(o) - if self.is_grout: - th = min(self.grout_depth + bevel, self.thickness - 0.001) - bottom = th - else: - th = self.thickness - bottom = 0 + g.cut(context, o) + g.floor(context, o, self) - 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)]) + # 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 + + self.manipulable_setup(context) + self.manipulate_mode = True + + self._manipulable_invoke(context) + + return True + + +def update_hole(self, context): + self.update(context, update_parent=True) + + +def update_operation(self, context): + self.reverse(context, make_ccw=(self.operation == 'INTERSECTION')) + + +class archipack_floor_cutter_segment(ArchipackCutterPart, PropertyGroup): + manipulators = CollectionProperty(type=archipack_manipulator) + type = EnumProperty( + name="Type", + items=( + ('DEFAULT', 'Side', 'Side with rake', 0), + ('BOTTOM', 'Bottom', 'Bottom with gutter', 1), + ('LINK', 'Side link', 'Side witout decoration', 2), + ('AXIS', 'Top', 'Top part with hip and beam', 3) + # ('LINK_VALLEY', 'Side valley', 'Side with valley', 3), + # ('LINK_HIP', 'Side hip', 'Side with hip', 4) + ), + default='DEFAULT', + update=update_hole + ) + + def find_in_selection(self, context): + selected = [o for o in context.selected_objects] + for o in selected: + d = archipack_floor_cutter.datablock(o) + if d: + for part in d.parts: + if part == self: + return d + return None + + def draw(self, layout, context, index): + box = layout.box() + box.label(text="Part:" + str(index + 1)) + # box.prop(self, "type", text=str(index + 1)) + box.prop(self, "length") + box.prop(self, "a0") + + +class archipack_floor_cutter(ArchipackCutter, ArchipackObject, Manipulable, PropertyGroup): + parts = CollectionProperty(type=archipack_floor_cutter_segment) + + def update_points(self, context, o, pts, update_parent=False): + """ + Create boundary from roof + """ + self.auto_update = False + self.from_points(pts) + self.auto_update = True + if update_parent: + self.update_parent(context, o) + + def update_parent(self, context, o): + + d = archipack_floor.datablock(o.parent) + if d is not None: + o.parent.select = True + context.scene.objects.active = o.parent + d.update(context) + o.parent.select = False + context.scene.objects.active = o + + +# ------------------------------------------------------------------ +# Define panel class to show object parameters in ui panel (N) +# ------------------------------------------------------------------ + class ARCHIPACK_PT_floor(Panel): bl_idname = "ARCHIPACK_PT_floor" @@ -980,13 +1621,12 @@ class ARCHIPACK_PT_floor(Panel): if not archipack_floor.filter(o): return layout = self.layout - + scene = context.scene # retrieve datablock of your object props = archipack_floor.datablock(o) - - # Manipulate mode operator - layout.operator('archipack.floor_manipulate', icon='HAND') - + # manipulate + layout.operator("archipack.floor_manipulate", icon="HAND") + layout.separator() box = layout.box() row = box.row(align=True) @@ -1000,124 +1640,170 @@ class ARCHIPACK_PT_floor(Panel): text="", icon='ZOOMOUT').remove_active = True - layout.prop(props, "tile_types", icon="OBJECT_DATA") + box = layout.box() + box.operator('archipack.floor_cutter').parent = o.name - layout.separator() + box = layout.box() + box.label(text="From curve") + box.prop_search(props, "user_defined_path", scene, "objects", text="", icon='OUTLINER_OB_CURVE') + if props.user_defined_path != "": + box.prop(props, 'user_defined_resolution') - layout.prop(props, "over_width") - layout.prop(props, "over_length") + box = layout.box() + row = box.row() + if props.parts_expand: + row.prop(props, 'parts_expand', icon="TRIA_DOWN", icon_only=True, text="Parts", emboss=False) + box.prop(props, 'n_parts') + # box.prop(prop, 'closed') + for i, part in enumerate(props.parts): + part.draw(context, layout, i) + else: + row.prop(props, 'parts_expand', icon="TRIA_RIGHT", icon_only=True, text="Parts", emboss=False) layout.separator() + box = layout.box() + box.prop(props, 'pattern', text="") + # thickness + box.separator() + box.prop(props, 'thickness') + box.prop(props, 'vary_thickness', icon='RNDCURVE') + if props.vary_thickness: + box.prop(props, 'thickness_variance') + + box.separator() + if props.pattern == 'boards': + box.prop(props, 'board_length') + box.prop(props, 'vary_length', icon='RNDCURVE') + if props.vary_length: + box.prop(props, 'length_variance') + box.prop(props, 'max_boards') + box.separator() + + # width + box.prop(props, 'board_width') + # vary width + box.prop(props, 'vary_width', icon='RNDCURVE') + if props.vary_width: + box.prop(props, 'width_variance') + box.separator() + box.prop(props, 'length_spacing') + box.prop(props, 'width_spacing') + + elif props.pattern in {'square_parquet', 'herringbone_parquet', 'herringbone'}: + box.prop(props, 'short_board_length') + + if props.pattern != "square_parquet": + box.prop(props, "board_width") + box.prop(props, "spacing") + + if props.pattern == 'square_parquet': + box.prop(props, 'boards_in_group') + elif props.pattern in {'regular_tile', 'hopscotch', 'stepping_stone', 'hexagon', 'windmill'}: + # width and length and mortar + if props.pattern != "hexagon": + box.prop(props, "tile_length") + box.prop(props, "tile_width") + box.prop(props, "spacing") + + if props.pattern in {"regular_tile", "boards"}: + box.separator() + box.prop(props, "random_offset", icon="RNDCURVE") + if props.random_offset: + box.prop(props, "offset_variance") + else: + box.prop(props, "offset") - # width and lengths - layout.prop(props, "thickness") + # grout + box.separator() + box.prop(props, 'add_grout', icon='MESH_GRID') + if props.add_grout: + box.prop(props, 'mortar_depth') - type = int(props.tile_types) + # bevel + box.separator() + box.prop(props, 'bevel', icon='MOD_BEVEL') + if props.bevel: + box.prop(props, 'bevel_amount') - 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") + box.separator() + box.prop(props, "vary_materials", icon="MATERIAL") + if props.vary_materials: + box.prop(props, "matid") - # 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") +class ARCHIPACK_PT_floor_cutter(Panel): + bl_idname = "ARCHIPACK_PT_floor_cutter" + bl_label = "Floor Cutter" + bl_space_type = 'VIEW_3D' + bl_region_type = 'UI' + bl_category = 'ArchiPack' - layout.separator() - layout.prop(props, "is_mat_vary", icon="MATERIAL") - if props.is_mat_vary: - layout.prop(props, "mat_vary") + @classmethod + def poll(cls, context): + return archipack_floor_cutter.filter(context.active_object) + + def draw(self, context): + prop = archipack_floor_cutter.datablock(context.active_object) + if prop is None: + return + layout = self.layout + scene = context.scene + box = layout.box() + box.operator('archipack.floor_cutter_manipulate', icon='HAND') + box.prop(prop, 'operation', text="") + box = layout.box() + box.label(text="From curve") + box.prop_search(prop, "user_defined_path", scene, "objects", text="", icon='OUTLINER_OB_CURVE') + if prop.user_defined_path != "": + box.prop(prop, 'user_defined_resolution') + # box.prop(prop, 'x_offset') + # box.prop(prop, 'angle_limit') + """ + box.prop_search(prop, "boundary", scene, "objects", text="", icon='OUTLINER_OB_CURVE') + """ + prop.draw(layout, context) + + +# ------------------------------------------------------------------ +# Define operator class to create object +# ------------------------------------------------------------------ class ARCHIPACK_OT_floor(ArchipackCreateTool, Operator): bl_idname = "archipack.floor" bl_label = "Floor" - bl_description = "Create Floor" + bl_description = "Floor" bl_category = 'Archipack' bl_options = {'REGISTER', 'UNDO'} def create(self, context): - - # Create an empty mesh datablock + """ + expose only basic params in operator + use object property for other params + """ 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 + # make manipulators selectable + d.manipulable_selectable = True + angle_90 = pi / 2 + x, y, = 4, 4 + p = d.parts.add() + p.a0 = - angle_90 + p.length = y + p = d.parts.add() + p.a0 = angle_90 + p.length = x + p = d.parts.add() + p.a0 = angle_90 + p.length = y + p = d.parts.add() + p.a0 = angle_90 + p.length = x + d.n_parts = 4 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 @@ -1125,11 +1811,174 @@ class ARCHIPACK_OT_floor(ArchipackCreateTool, Operator): if context.mode == "OBJECT": bpy.ops.object.select_all(action="DESELECT") o = self.create(context) - o.location = bpy.context.scene.cursor_location + o.location = context.scene.cursor_location + # activate manipulators at creation time o.select = True context.scene.objects.active = o + bpy.ops.archipack.floor_manipulate() + return {'FINISHED'} + else: + self.report({'WARNING'}, "Option only valid in Object mode") + return {'CANCELLED'} + + +class ARCHIPACK_OT_floor_from_curve(ArchipackCreateTool, Operator): + bl_idname = "archipack.floor_from_curve" + bl_label = "Floor curve" + bl_description = "Create a floor 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' + # ----------------------------------------------------- + # 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.floor(auto_manipulate=self.auto_manipulate, filepath=self.filepath) + o = context.active_object + d = archipack_floor.datablock(o) + d.user_defined_path = curve.name + 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'} - # Start manipulate mode + +class ARCHIPACK_OT_floor_from_wall(ArchipackCreateTool, Operator): + bl_idname = "archipack.floor_from_wall" + bl_label = "->Floor" + bl_description = "Create a floor from a wall" + bl_category = 'Archipack' + bl_options = {'REGISTER', 'UNDO'} + + @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.floor(auto_manipulate=False, filepath=self.filepath) + o = context.scene.objects.active + d = archipack_floor.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 + o.matrix_world = wall.matrix_world.copy() + 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.floor_manipulate('INVOKE_DEFAULT') + return {'FINISHED'} + else: + self.report({'WARNING'}, "Archipack: Option only valid in Object mode") + return {'CANCELLED'} + + +class ARCHIPACK_OT_floor_cutter(ArchipackCreateTool, Operator): + bl_idname = "archipack.floor_cutter" + bl_label = "Floor Cutter" + bl_description = "Floor Cutter" + bl_category = 'Archipack' + bl_options = {'REGISTER', 'UNDO'} + + parent = StringProperty("") + + def create(self, context): + m = bpy.data.meshes.new("Floor Cutter") + o = bpy.data.objects.new("Floor Cutter", m) + d = m.archipack_floor_cutter.add() + parent = context.scene.objects.get(self.parent) + if parent is not None: + o.parent = parent + bbox = parent.bound_box + angle_90 = pi / 2 + x0, y0, z = bbox[0] + x1, y1, z = bbox[6] + x = 0.2 * (x1 - x0) + y = 0.2 * (y1 - y0) + o.matrix_world = parent.matrix_world * Matrix([ + [1, 0, 0, -3 * x], + [0, 1, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 1] + ]) + p = d.parts.add() + p.a0 = - angle_90 + p.length = y + p = d.parts.add() + p.a0 = angle_90 + p.length = x + p = d.parts.add() + p.a0 = angle_90 + p.length = y + d.n_parts = 3 + # d.close = True + pd = archipack_floor.datablock(parent) + pd.boundary = o.name + else: + o.location = context.scene.cursor_location + # make manipulators selectable + d.manipulable_selectable = True + context.scene.objects.link(o) + o.select = True + context.scene.objects.active = o + # self.add_material(o) + self.load_preset(d) + update_operation(d, context) + 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 self.manipulate() return {'FINISHED'} else: @@ -1137,8 +1986,13 @@ class ARCHIPACK_OT_floor(ArchipackCreateTool, Operator): return {'CANCELLED'} +# ------------------------------------------------------------------ +# Define operator class to manipulate object +# ------------------------------------------------------------------ + + class ARCHIPACK_OT_floor_preset_menu(PresetMenuOperator, Operator): - bl_description = "Show Floor Presets" + bl_description = "Show Floor presets" bl_idname = "archipack.floor_preset_menu" bl_label = "Floor preset" preset_subdir = "archipack_floor" @@ -1152,7 +2006,7 @@ class ARCHIPACK_OT_floor_preset(ArchipackPreset, Operator): @property def blacklist(self): - return ['manipulators', 'over_length', 'over_width'] + return ['manipulators', 'parts', 'n_parts', 'user_defined_path', 'user_defined_resolution'] class ARCHIPACK_OT_floor_manipulate(Operator): @@ -1171,7 +2025,31 @@ class ARCHIPACK_OT_floor_manipulate(Operator): return {'FINISHED'} +class ARCHIPACK_OT_floor_cutter_manipulate(Operator): + bl_idname = "archipack.floor_cutter_manipulate" + bl_label = "Manipulate" + bl_description = "Manipulate" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(self, context): + return archipack_floor_cutter.filter(context.active_object) + + def invoke(self, context, event): + d = archipack_floor_cutter.datablock(context.active_object) + d.manipulable_invoke(context) + return {'FINISHED'} + + def register(): + bpy.utils.register_class(archipack_floor_cutter_segment) + bpy.utils.register_class(archipack_floor_cutter) + Mesh.archipack_floor_cutter = CollectionProperty(type=archipack_floor_cutter) + bpy.utils.register_class(ARCHIPACK_OT_floor_cutter) + bpy.utils.register_class(ARCHIPACK_PT_floor_cutter) + bpy.utils.register_class(ARCHIPACK_OT_floor_cutter_manipulate) + + bpy.utils.register_class(archipack_floor_part) bpy.utils.register_class(archipack_floor) Mesh.archipack_floor = CollectionProperty(type=archipack_floor) bpy.utils.register_class(ARCHIPACK_PT_floor) @@ -1179,9 +2057,19 @@ def register(): 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) + bpy.utils.register_class(ARCHIPACK_OT_floor_from_curve) + bpy.utils.register_class(ARCHIPACK_OT_floor_from_wall) def unregister(): + bpy.utils.unregister_class(archipack_floor_cutter_segment) + bpy.utils.unregister_class(archipack_floor_cutter) + del Mesh.archipack_floor_cutter + bpy.utils.unregister_class(ARCHIPACK_OT_floor_cutter) + bpy.utils.unregister_class(ARCHIPACK_PT_floor_cutter) + bpy.utils.unregister_class(ARCHIPACK_OT_floor_cutter_manipulate) + + bpy.utils.unregister_class(archipack_floor_part) bpy.utils.unregister_class(archipack_floor) del Mesh.archipack_floor bpy.utils.unregister_class(ARCHIPACK_PT_floor) @@ -1189,3 +2077,5 @@ def unregister(): 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) + bpy.utils.unregister_class(ARCHIPACK_OT_floor_from_curve) + bpy.utils.unregister_class(ARCHIPACK_OT_floor_from_wall) diff --git a/archipack/archipack_gl.py b/archipack/archipack_gl.py index fc1f8c03..c50c61a2 100644 --- a/archipack/archipack_gl.py +++ b/archipack/archipack_gl.py @@ -54,116 +54,6 @@ class DefaultColorScheme: 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 @@ -205,11 +95,14 @@ class Gl(): return [round(co_2d.x * render_size[0]), round(co_2d.y * render_size[1])] def _end(self): + + # print("_end") bgl.glEnd() bgl.glPopAttrib() bgl.glLineWidth(1) bgl.glDisable(bgl.GL_BLEND) bgl.glColor4f(0.0, 0.0, 0.0, 1.0) + # print("_end %s" % (type(self).__name__)) class GlText(Gl): @@ -347,6 +240,8 @@ class GlText(Gl): self._text = self.add_units(context) def draw(self, context, render=False): + + # print("draw_text %s %s" % (self.text, type(self).__name__)) self.render = render x, y = self.position_2d_from_coord(context, self.pts[0], render) # dirty fast assignment @@ -381,6 +276,8 @@ class GlBaseLine(Gl): """ render flag when rendering """ + + # print("draw_line %s" % (type(self).__name__)) bgl.glPushAttrib(bgl.GL_ENABLE_BIT) if self.style == bgl.GL_LINE_STIPPLE: bgl.glLineStipple(1, 0x9999) @@ -671,6 +568,8 @@ class GlPolygon(Gl): """ render flag when rendering """ + + # print("draw_polygon") self.render = render bgl.glPushAttrib(bgl.GL_ENABLE_BIT) bgl.glEnable(bgl.GL_BLEND) @@ -693,6 +592,7 @@ class GlRect(GlPolygon): 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) @@ -1147,7 +1047,7 @@ class GlCursorFence(): """ Cursor crossing Fence """ - def __init__(self, width=1, colour=(1.0, 1.0, 1.0, 0.5), style=2852): + def __init__(self, width=1, colour=(1.0, 1.0, 1.0, 0.5), style=bgl.GL_LINE_STIPPLE): self.line_x = GlLine(d=2) self.line_x.style = style self.line_x.width = width @@ -1184,7 +1084,7 @@ class GlCursorArea(): width=1, bordercolour=(1.0, 1.0, 1.0, 0.5), areacolour=(0.5, 0.5, 0.5, 0.08), - style=2852): + style=bgl.GL_LINE_STIPPLE): self.border = GlPolyline(bordercolour, d=2) self.border.style = style @@ -1224,5 +1124,6 @@ class GlCursorArea(): def draw(self, context, render=False): if self.on: + # print("GlCursorArea.draw()") self.area.draw(context) self.border.draw(context) diff --git a/archipack/archipack_handle.py b/archipack/archipack_handle.py index 852fe2b6..fcdb570e 100644 --- a/archipack/archipack_handle.py +++ b/archipack/archipack_handle.py @@ -29,6 +29,7 @@ import bpy def create_handle(context, parent, mesh): + old = context.active_object handle = bpy.data.objects.new("Handle", mesh) handle['archipack_handle'] = True context.scene.objects.link(handle) @@ -37,6 +38,11 @@ def create_handle(context, parent, mesh): modif.levels = 1 handle.parent = parent handle.matrix_world = parent.matrix_world.copy() + context.scene.objects.active = handle + m = handle.archipack_material.add() + m.category = 'handle' + m.material = 'DEFAULT' + context.scene.objects.active = old return handle diff --git a/archipack/archipack_manipulator.py b/archipack/archipack_manipulator.py index c3e0fc24..c96af62a 100644 --- a/archipack/archipack_manipulator.py +++ b/archipack/archipack_manipulator.py @@ -776,6 +776,7 @@ class WallSnapManipulator(Manipulator): part.a0 = w.straight(1, 0).angle # move object when point 0 self.o.location += sp.delta + self.o.matrix_world.translation += sp.delta if "C_" in part.type: part.radius = w.r @@ -1636,6 +1637,11 @@ class DumbAngleManipulator(AngleManipulator): self.line_0.v = -self.line_0.cross.normalized() self.line_1.v = right self.line_1.v = self.line_1.cross.normalized() + + # prevent ValueError in angle_signed + if self.line_0.length == 0 or self.line_1.length == 0: + return + 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 diff --git a/archipack/archipack_material.py b/archipack/archipack_material.py new file mode 100644 index 00000000..7cb44180 --- /dev/null +++ b/archipack/archipack_material.py @@ -0,0 +1,575 @@ +# -*- coding:utf-8 -*- + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110- 1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +# + +# ---------------------------------------------------------- +# Author: Stephen Leger (s-leger) +# +# ---------------------------------------------------------- +# noinspection PyUnresolvedReferences +import bpy +import os +# noinspection PyUnresolvedReferences +from bpy.types import ( + Panel, PropertyGroup, + Object, Operator + ) +from bpy.props import ( + EnumProperty, CollectionProperty, + StringProperty + ) + +setman = None +libman = None + + +class MatLib(): + """ + A material library .blend file + Store material name + Apply material to objects + """ + def __init__(self, matlib_path, name): + self.name = name + self.path = os.path.join(matlib_path, name) + self.materials = [] + + def cleanup(self): + self.materials.clear() + + def load_list(self, sort=False): + """ + list material names + """ + # print("MatLib.load_list(%s)" % (self.name)) + + self.materials.clear() + + with bpy.data.libraries.load(self.path) as (data_from, data_to): + for mat in data_from.materials: + self.materials.append(mat) + if sort: + self.materials = list(sorted(self.materials)) + + def has(self, name): + return name in self.materials + + def load_mat(self, name, link): + """ + Load a material from library + """ + # print("MatLib.load_mat(%s) linked:%s" % (name, link)) + with bpy.data.libraries.load(self.path, link, False) as (data_from, data_to): + data_to.materials = [name] + + def get_mat(self, name, link): + """ + apply a material by name to active_object + into slot index + lazy load material list on demand + """ + + # Lazy load material names + if len(self.materials) < 1: + self.load_list() + + # material belongs to this libraray + if self.has(name): + + # load material + self.load_mat(name, link) + + return bpy.data.materials.get(name) + + return None + + +class MatlibsManager(): + """ + Manage multiple library + Lazy load + """ + def __init__(self): + self.matlibs = [] + + def cleanup(self): + for lib in self.matlibs: + lib.cleanup() + self.matlibs.clear() + + def get_prefs(self, context): + global __name__ + prefs = None + try: + # retrieve addon name from imports + addon_name = __name__.split('.')[0] + prefs = context.user_preferences.addons[addon_name].preferences + except: + pass + return prefs + + @property + def loaded_path(self): + """ + Loaded matlibs filenames + """ + return [lib.path for lib in self.matlibs] + + def from_data(self, name): + return bpy.data.materials.get(name) + + def add_to_list(self, path): + """ + Add material library to list + only store name of lib + reloading here dosent make sense + """ + loaded_path = self.loaded_path + + if os.path.exists(path): + self.matlibs.extend( + [ + MatLib(path, f) for f in os.listdir(path) + if f.endswith(".blend") and os.path.join(path, f) not in loaded_path + ] + ) + + def load_list(self, context): + """ + list available library path + """ + # default library + dir_path = os.path.dirname(os.path.realpath(__file__)) + mat_path = os.path.join(dir_path, "materials") + self.add_to_list(mat_path) + + # user def library path from addon prefs + try: + prefs = self.get_prefs(context) + self.add_to_list(prefs.matlib_path) + except: + print("unable to load %s" % mat_path) + pass + + def apply(self, context, slot_index, name, link=False): + + o = context.active_object + o.select = True + + # material with same name exist in scene + mat = self.from_data(name) + + # mat not in scene: try to load from lib + if mat is None: + # print("mat %s not found in scene, loading" % (name)) + # Lazy build matlibs list + if len(self.matlibs) < 1: + self.load_list(context) + + for lib in self.matlibs: + mat = lib.get_mat(name, link) + if mat is not None: + break + + # nothing found, build a default mat + if mat is None: + mat = bpy.data.materials.new(name) + + if slot_index < len(o.material_slots): + o.material_slots[slot_index].material = None + o.material_slots[slot_index].material = mat + o.active_material_index = slot_index + + if not link: + # break link + bpy.ops.object.make_local(type="SELECT_OBDATA_MATERIAL") + + +class MaterialSetManager(): + """ + Manage material sets for objects + Store material names for each set + Lazy load at enumerate time + """ + def __init__(self): + """ + Store sets for each object type + """ + self.objects = {} + + def get_filename(self, object_type): + + target_path = os.path.join("presets", "archipack_materials") + target_path = bpy.utils.user_resource('SCRIPTS', + target_path, + create=True) + return os.path.join(target_path, object_type) + '.txt' + + def cleanup(self): + self.objects.clear() + + def register_set(self, object_type, set_name, materials_names): + + if object_type not in self.objects.keys(): + self.objects[object_type] = {} + + self.objects[object_type][set_name.upper()] = materials_names + + def load(self, object_type): + + filename = self.get_filename(object_type) + + # preset not found in user prefs, load from archipack's default + if not os.path.exists(filename): + rel_filepath = \ + os.path.sep + "presets" + os.path.sep + \ + "archipack_materials" + os.path.sep + object_type + '.txt' + + filename = os.path.dirname(os.path.realpath(__file__)) + rel_filepath + + # print("load filename %s" % filename) + + material_sets = {} + + # create file object, and set open mode + if os.path.exists(filename): + + f = open(filename, 'r') + lines = f.readlines() + + for line in lines: + s_key, mat_name = line.split("##|##") + if str(s_key) not in material_sets.keys(): + material_sets[s_key] = [] + material_sets[s_key].append(mat_name.strip()) + + f.close() + + for s_key in material_sets.keys(): + self.register_set(object_type, s_key, material_sets[s_key]) + + def save(self, object_type): + # always save in user prefs + filename = self.get_filename(object_type) + # print("filename:%s" % filename) + o_dict = self.objects[object_type] + lines = [] + for s_key in o_dict.keys(): + for mat in o_dict[s_key]: + lines.append("{}##|##{}\n".format(s_key, mat)) + f = open(filename, 'w') + f.writelines(lines) + f.close() + + def add(self, context, set_name): + o = context.active_object + if "archipack_material" in o: + object_type = o.archipack_material[0].category + materials_names = [slot.name for slot in o.material_slots if slot.name != ''] + # print("%s " % materials_names) + self.register_set(object_type, set_name, materials_names) + self.save(object_type) + + def remove(self, context): + o = context.active_object + if "archipack_material" in o: + d = o.archipack_material[0] + object_type = d.category + set_name = d.material + if set_name in self.objects[object_type].keys(): + self.objects[object_type].pop(set_name) + self.save(object_type) + + def get_materials(self, object_type, set_name): + if object_type not in self.objects.keys(): + self.load(object_type) + if object_type not in self.objects.keys(): + return None + if set_name not in self.objects[object_type].keys(): + print("set {} not found".format(set_name)) + return None + return self.objects[object_type][set_name] + + def make_enum(self, object_type): + + if object_type not in self.objects.keys(): + self.load(object_type) + + if object_type not in self.objects.keys(): + self.objects[object_type] = {} + + s_keys = self.objects[object_type].keys() + + if len(s_keys) < 1: + return [('DEFAULT', 'Default', '', 0)] + + return [(s.upper(), s.capitalize(), '', i) for i, s in enumerate(s_keys)] + + +def material_enum(self, context): + global setman + if setman is None: + setman = MaterialSetManager() + return setman.make_enum(self.category) + + +def update(self, context): + self.update(context) + + +class archipack_material(PropertyGroup): + + category = StringProperty( + name="Category", + description="Archipack object name", + default="" + ) + material = EnumProperty( + name="Material", + description="Material type", + items=material_enum, + update=update + ) + + def apply_material(self, context, slot_index, name): + global libman + + if libman is None: + libman = MatlibsManager() + + libman.apply(context, slot_index, name, link=False) + + def update(self, context): + global setman + + if setman is None: + setman = MaterialSetManager() + + o = context.active_object + sel = [ + c for c in o.children + if 'archipack_material' in c and c.archipack_material[0].category == self.category] + + # handle wall's holes + if o.data and "archipack_wall2" in o.data: + if o.parent is not None: + for child in o.parent.children: + if ('archipack_hybridhole' in child or + 'archipack_robusthole' in child or + 'archipack_hole' in child): + sel.append(child) + + sel.append(o) + + mats = setman.get_materials(self.category, self.material) + + if mats is None: + return False + + for ob in sel: + context.scene.objects.active = ob + for slot_index, mat_name in enumerate(mats): + if slot_index >= len(ob.material_slots): + bpy.ops.object.material_slot_add() + self.apply_material(context, slot_index, mat_name) + + context.scene.objects.active = o + + return True + + +class ARCHIPACK_PT_material(Panel): + bl_idname = "ARCHIPACK_PT_material" + bl_label = "Archipack Material" + bl_space_type = 'VIEW_3D' + bl_region_type = 'UI' + # bl_category = 'ArchiPack' + + @classmethod + def poll(cls, context): + return context.active_object is not None and 'archipack_material' in context.active_object + + def draw(self, context): + layout = self.layout + props = context.active_object.archipack_material[0] + row = layout.row(align=True) + row.prop(props, 'material', text="") + row.operator('archipack.material_add', icon="ZOOMIN", text="") + row.operator('archipack.material_remove', icon="ZOOMOUT", text="") + + +class ARCHIPACK_OT_material(Operator): + bl_idname = "archipack.material" + bl_label = "Material" + bl_description = "Add archipack material" + bl_options = {'REGISTER', 'UNDO'} + + category = StringProperty( + name="Category", + description="Archipack object name", + default="" + ) + material = StringProperty( + name="Material", + description="Material type", + default="" + ) + + @classmethod + def poll(cls, context): + return context.active_object is not None + + def execute(self, context): + + o = context.active_object + + if 'archipack_material' in o: + m = o.archipack_material[0] + else: + m = o.archipack_material.add() + + m.category = self.category + try: + m.material = self.material + res = m.update(context) + except: + res = False + pass + + if res: + return {'FINISHED'} + else: + self.report({'WARNING'}, 'Material {} for {} not found'.format(self.material, self.category)) + return {'CANCELLED'} + + +class ARCHIPACK_OT_material_add(Operator): + bl_idname = "archipack.material_add" + bl_label = "Material" + bl_description = "Add a set of archipack material" + bl_options = {'REGISTER', 'UNDO'} + + material = StringProperty( + name="Material", + description="Material type", + default="" + ) + + @classmethod + def poll(cls, context): + return context.active_object is not None + + def invoke(self, context, event): + return context.window_manager.invoke_props_dialog(self) + + def execute(self, context): + + global setman + + if setman is None: + setman = MaterialSetManager() + + setman.add(context, self.material) + + return {'FINISHED'} + + +class ARCHIPACK_OT_material_remove(Operator): + bl_idname = "archipack.material_remove" + bl_label = "Material" + bl_description = "Remove a set of archipack material" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + return context.active_object is not None + + def execute(self, context): + + global setman + + if setman is None: + setman = MaterialSetManager() + + setman.remove(context) + + return {'FINISHED'} + + +class ARCHIPACK_OT_material_library(Operator): + bl_idname = "archipack.material_library" + bl_label = "Material Library" + bl_description = "Add all archipack materials on a single object" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + return context.active_object is not None + + def execute(self, context): + + global setman + + if setman is None: + setman = MaterialSetManager() + + o = context.active_object + + if 'archipack_material' in o: + m = o.archipack_material[0] + else: + m = o.archipack_material.add() + o.data.materials.clear() + + for category in setman.objects.keys(): + prefix = category.capitalize() + "_" + for part in setman.objects[category]["DEFAULT"]: + name = prefix + part + mat = m.get_material(name) + o.data.materials.append(mat) + + return {'FINISHED'} + + +def register(): + bpy.utils.register_class(archipack_material) + Object.archipack_material = CollectionProperty(type=archipack_material) + bpy.utils.register_class(ARCHIPACK_OT_material) + bpy.utils.register_class(ARCHIPACK_OT_material_add) + bpy.utils.register_class(ARCHIPACK_OT_material_remove) + bpy.utils.register_class(ARCHIPACK_OT_material_library) + bpy.utils.register_class(ARCHIPACK_PT_material) + + +def unregister(): + global libman + global setman + if libman is not None: + libman.cleanup() + if setman is not None: + setman.cleanup() + bpy.utils.unregister_class(ARCHIPACK_PT_material) + bpy.utils.unregister_class(ARCHIPACK_OT_material) + bpy.utils.unregister_class(ARCHIPACK_OT_material_add) + bpy.utils.unregister_class(ARCHIPACK_OT_material_remove) + bpy.utils.unregister_class(ARCHIPACK_OT_material_library) + bpy.utils.unregister_class(archipack_material) + del Object.archipack_material diff --git a/archipack/archipack_object.py b/archipack/archipack_object.py index 18ae43e5..b99fc33b 100644 --- a/archipack/archipack_object.py +++ b/archipack/archipack_object.py @@ -36,7 +36,7 @@ from bpy_extras.view3d_utils import ( region_2d_to_origin_3d, region_2d_to_vector_3d ) -from .materialutils import MaterialUtils +# from .materialutils import MaterialUtils class ArchipackObject(): @@ -117,9 +117,9 @@ class ArchipackObject(): o.select = True except: pass - - self.previously_active.select = True - context.scene.objects.active = self.previously_active + if self.previously_active is not None: + self.previously_active.select = True + context.scene.objects.active = self.previously_active self.previously_selected = None self.previously_active = None @@ -156,19 +156,20 @@ class ArchipackCreateTool(): 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): + def add_material(self, o, material='DEFAULT', category=None): try: - getattr(MaterialUtils, "add_" + self.archipack_category + "_materials")(o) + if category is None: + category = self.archipack_category + if bpy.ops.archipack.material.poll(): + bpy.ops.archipack.material(category=category, material=material) except: - print("Archipack MaterialUtils.add_%s_materials not found" % (self.archipack_category)) + print("Archipack %s materials not found" % (self.archipack_category)) pass def manipulate(self): diff --git a/archipack/archipack_preset.py b/archipack/archipack_preset.py index c5fe9446..3092e494 100644 --- a/archipack/archipack_preset.py +++ b/archipack/archipack_preset.py @@ -501,11 +501,17 @@ class ArchipackPreset(AddPresetBase): if prop_id not in blacklist: if not (prop.is_hidden or prop.is_skip_save): ret.append("d.%s" % prop_id) + ret.sort() return ret @property def preset_defines(self): - return ["d = bpy.context.active_object.data." + self.preset_subdir + "[0]"] + o = bpy.context.active_object + m = o.archipack_material[0] + return [ + "d = bpy.context.active_object.data." + self.preset_subdir + "[0]", + "bpy.ops.archipack.material(category='" + m.category + "', material='" + m.material + "')" + ] def pre_cb(self, context): return diff --git a/archipack/archipack_progressbar.py b/archipack/archipack_progressbar.py new file mode 100644 index 00000000..16740c99 --- /dev/null +++ b/archipack/archipack_progressbar.py @@ -0,0 +1,87 @@ +# -*- coding:utf-8 -*- + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110- 1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +# + +# ---------------------------------------------------------- +# Author: Stephen Leger (s-leger) +# Inspired reportpanel.py by Michel Anders +# ---------------------------------------------------------- +import bpy +from bpy.props import FloatProperty, StringProperty +from bpy.types import Scene +from time import time + + +last_update = 0 +info_header_draw = None + + +def update(self, context): + global last_update + areas = context.window.screen.areas + for area in areas: + if area.type == 'INFO': + area.tag_redraw() + if time() - last_update > 0.1: + bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1) + last_update = time() + + +def register(): + Scene.archipack_progress = FloatProperty( + options={'SKIP_SAVE'}, + default=-1, + subtype='PERCENTAGE', + precision=1, + min=-1, + soft_min=0, + soft_max=100, + max=101, + update=update) + + Scene.archipack_progress_text = StringProperty( + options={'SKIP_SAVE'}, + default="Progress", + update=update) + + global info_header_draw + info_header_draw = bpy.types.INFO_HT_header.draw + + def info_draw(self, context): + global info_header_draw + info_header_draw(self, context) + if (context.scene.archipack_progress > -1 and + context.scene.archipack_progress < 101): + self.layout.separator() + text = context.scene.archipack_progress_text + self.layout.prop(context.scene, + "archipack_progress", + text=text, + slider=True) + + bpy.types.INFO_HT_header.draw = info_draw + + +def unregister(): + del Scene.archipack_progress + del Scene.archipack_progress_text + global info_header_draw + bpy.types.INFO_HT_header.draw = info_header_draw diff --git a/archipack/archipack_reference_point.py b/archipack/archipack_reference_point.py index d81a6839..5e8f1f59 100644 --- a/archipack/archipack_reference_point.py +++ b/archipack/archipack_reference_point.py @@ -29,7 +29,8 @@ from bpy.types import Operator, PropertyGroup, Object, Panel from bpy.props import ( FloatVectorProperty, CollectionProperty, - FloatProperty + FloatProperty, + EnumProperty ) from mathutils import Vector from .bmesh_utils import BmeshEdit as bmed @@ -55,6 +56,13 @@ class archipack_reference_point(PropertyGroup): default=1, min=0.01, update=update) + symbol_type = EnumProperty( + name="Symbol type", + default='WALL', + items=( + ('WALL', 'Wall', '', 0), + ('ROOF', 'Roof', '', 1)), + update=update) @classmethod def filter(cls, o): @@ -98,29 +106,61 @@ class archipack_reference_point(PropertyGroup): 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)] + + if self.symbol_type == 'WALL': + + 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)] + + elif self.symbol_type == 'ROOF': + + 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.05, 0.05, 0.673), (-0.05, 0.674, -0.05), (-0.05, -0.05, 0.673), (-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.05, 0.05, 0.673), (0.05, -0.05, 0.673), (0.05, 0.674, 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), + (0.674, -0.05, 0.05), (0.108, 0.0, 0.216), (0.09, 0.0, 0.261), (0.001, 0.107, 0.216), + (0.001, -0.107, 0.216), (-0.107, 0.0, 0.216), (0.0, -0.089, 0.261), (0.0, 0.089, 0.261), + (-0.089, 0.0, 0.261), (0.0, 0.042, 0.694), (-0.042, 0.0, 0.694), (0.0, -0.042, 0.694), + (0.042, 0.0, 0.694)]] + + edges = [ + (1, 0), (0, 9), (10, 1), (3, 1), (11, 3), (2, 3), (12, 2), (0, 2), + (17, 22), (17, 19), (18, 23), (13, 14), (14, 15), (15, 16), (16, 13), + (15, 28), (17, 20), (24, 21), (18, 24), (14, 26), (13, 25), (16, 27), + (45, 29), (46, 29), (47, 30), (48, 30), (23, 21), (23, 31), (24, 31), + (32, 34), (35, 33), (32, 35), (34, 33), (34, 36), (35, 36), (28, 37), + (6, 38), (26, 37), (26, 39), (25, 39), (5, 43), (5, 44), (25, 41), + (27, 41), (7, 44), (8, 42), (28, 40), (27, 40), (20, 45), (22, 46), + (22, 47), (20, 48), (18, 19), (18, 21), (18, 31), (17, 30), (17, 29), + (32, 19), (32, 33), (32, 36), (4, 6), (4, 7), (4, 8), (4, 5), (8, 38), + (6, 43), (7, 42), (9, 10), (10, 11), (11, 12), (12, 9)] bm = bmed._start(context, o) bm.clear() @@ -154,8 +194,9 @@ class ARCHIPACK_PT_reference_point(Panel): layout.operator('archipack.move_2d_reference_to_cursor') else: layout.operator('archipack.move_to_2d') - layout.prop(props, 'symbol_scale') + layout.separator() + layout.operator('archipack.apply_holes') class ARCHIPACK_OT_reference_point(Operator): @@ -170,6 +211,13 @@ class ARCHIPACK_OT_reference_point(Operator): name="position 3d", default=Vector((0, 0, 0)) ) + symbol_type = EnumProperty( + name="Symbol type", + default='WALL', + items=( + ('WALL', 'Wall', '', 0), + ('ROOF', 'Roof', '', 1)) + ) @classmethod def poll(cls, context): @@ -190,6 +238,7 @@ class ARCHIPACK_OT_reference_point(Operator): d = o.archipack_reference_point.add() d.location_2d = Vector((x, y, 0)) d.location_3d = self.location_3d + d.symbol_type = self.symbol_type o.select = True context.scene.objects.active = o d.update(context) @@ -230,6 +279,67 @@ class ARCHIPACK_OT_move_to_3d(Operator): return {'CANCELLED'} +class ARCHIPACK_OT_apply_holes(Operator): + bl_idname = "archipack.apply_holes" + bl_label = "Apply holes" + bl_description = "Apply and remove holes from scene" + bl_category = 'Archipack' + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + return archipack_reference_point.filter(context.active_object) + + def apply_boolean(self, context, o): + # mods = [m for m in o.modifiers if m.type == 'BOOLEAN'] + ctx = bpy.context.copy() + ctx['object'] = o + for mod in o.modifiers[:]: + ctx['modifier'] = mod + try: + bpy.ops.object.modifier_apply(ctx, apply_as='DATA', + modifier=ctx['modifier'].name) + except: + pass + + def execute(self, context): + if context.mode == "OBJECT": + o = context.active_object + to_remove = [] + + for c in o.children: + if 'archipack_hybridhole' in c: + self.apply_boolean(context, c) + to_remove.append(c) + + for c in o.children: + if c.data is not None and "archipack_wall2" in c.data: + self.apply_boolean(context, c) + + for c in o.children: + if c.data is not None and ( + "archipack_window" in c.data or + "archipack_door" in c.data): + for h in c.children: + if "archipack_hole" in h: + to_remove.append(h) + + bpy.ops.object.select_all(action="DESELECT") + for r in to_remove: + r.hide_select = False + r.select = True + context.scene.objects.active = r + bpy.ops.object.delete(use_global=False) + + 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_2d(Operator): bl_idname = "archipack.move_to_2d" bl_label = "Move to 2d" @@ -354,6 +464,7 @@ def register(): 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) + bpy.utils.register_class(ARCHIPACK_OT_apply_holes) def unregister(): @@ -366,3 +477,4 @@ def unregister(): 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) + bpy.utils.unregister_class(ARCHIPACK_OT_apply_holes) diff --git a/archipack/archipack_roof.py b/archipack/archipack_roof.py new file mode 100644 index 00000000..43a63228 --- /dev/null +++ b/archipack/archipack_roof.py @@ -0,0 +1,5376 @@ +# -*- coding:utf-8 -*- + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110- 1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +# + +# ---------------------------------------------------------- +# Author: Stephen Leger (s-leger) +# +# ---------------------------------------------------------- +# noinspection PyUnresolvedReferences +import bpy +import time +# noinspection PyUnresolvedReferences +from bpy.types import Operator, PropertyGroup, Mesh, Panel +from bpy.props import ( + FloatProperty, BoolProperty, IntProperty, + StringProperty, EnumProperty, + CollectionProperty + ) +from .bmesh_utils import BmeshEdit as bmed +from random import randint +import bmesh +from mathutils import Vector, Matrix +from math import sin, cos, pi, atan2, sqrt, tan +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 +from .archipack_cutter import ( + CutAblePolygon, CutAbleGenerator, + ArchipackCutter, + ArchipackCutterPart + ) + + +class Roof(): + + def __init__(self): + self.angle_0 = 0 + self.v0_idx = 0 + self.v1_idx = 0 + self.constraint_type = None + self.slope_left = 1 + self.slope_right = 1 + self.width_left = 1 + self.width_right = 1 + self.auto_left = 'AUTO' + self.auto_right = 'AUTO' + self.type = 'SIDE' + # force hip or valley + self.enforce_part = 'AUTO' + self.triangular_end = False + # seg is part of hole + self.is_hole = False + + def copy_params(self, s): + s.angle_0 = self.angle_0 + s.v0_idx = self.v0_idx + s.v1_idx = self.v1_idx + s.constraint_type = self.constraint_type + s.slope_left = self.slope_left + s.slope_right = self.slope_right + s.width_left = self.width_left + s.width_right = self.width_right + s.auto_left = self.auto_left + s.auto_right = self.auto_right + s.type = self.type + s.enforce_part = self.enforce_part + s.triangular_end = self.triangular_end + # segment is part of hole / slice + s.is_hole = self.is_hole + + @property + def copy(self): + s = StraightRoof(self.p.copy(), self.v.copy()) + self.copy_params(s) + return s + + def straight(self, length, t=1): + s = self.copy + s.p = self.lerp(t) + s.v = self.v.normalized() * length + return s + + def set_offset(self, offset, last=None): + """ + Offset line and compute intersection point + between segments + """ + self.line = self.make_offset(offset, last) + + def offset(self, offset): + o = self.copy + o.p += offset * self.cross_z.normalized() + return o + + @property + def oposite(self): + o = self.copy + o.p += o.v + o.v = -o.v + return o + + @property + def t_diff(self): + return self.t_end - self.t_start + + def straight_roof(self, a0, length): + s = self.straight(length).rotate(a0) + r = StraightRoof(s.p, s.v) + r.angle_0 = a0 + return r + + def curved_roof(self, a0, da, radius): + n = self.normal(1).rotate(a0).scale(radius) + if da < 0: + n.v = -n.v + c = n.p - n.v + r = CurvedRoof(c, radius, n.angle, da) + r.angle_0 = a0 + return r + + +class StraightRoof(Roof, Line): + def __str__(self): + return "p0:{} p1:{}".format(self.p0, self.p1) + + def __init__(self, p, v): + Line.__init__(self, p, v) + Roof.__init__(self) + + +class CurvedRoof(Roof, 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): + Arc.__init__(self, c, radius, a0, da) + Roof.__init__(self) + + +class RoofSegment(): + """ + Roof part with 2 polygons + and "axis" StraightRoof segment + """ + def __init__(self, seg, left, right): + self.seg = seg + self.left = left + self.right = right + self.a0 = 0 + self.reversed = False + + +class RoofAxisNode(): + """ + Connection between parts + for radial analysis + """ + def __init__(self): + # axis segments + self.segs = [] + self.root = None + self.center = 0 + # store count of horizontal segs + self.n_horizontal = 0 + # store count of slopes segs + self.n_slope = 0 + + @property + def count(self): + return len(self.segs) + + @property + def last(self): + """ + last segments in this node + """ + return self.segs[-1] + + def left(self, index): + if index + 1 >= self.count: + return self.segs[0] + return self.segs[index + 1] + + def right(self, index): + return self.segs[index - 1] + + def add(self, a0, reversed, seg, left, right): + + if seg.constraint_type == 'HORIZONTAL': + self.n_horizontal += 1 + elif seg.constraint_type == 'SLOPE': + self.n_slope += 1 + + s = RoofSegment(seg, left, right) + s.a0 = a0 + s.reversed = reversed + if reversed: + self.root = s + self.segs.append(s) + + def update_center(self): + for i, s in enumerate(self.segs): + if s is self.root: + self.center = i + return + + # sort tree segments by angle + def partition(self, array, begin, end): + pivot = begin + for i in range(begin + 1, end + 1): + if array[i].a0 < array[begin].a0: + pivot += 1 + array[i], array[pivot] = array[pivot], array[i] + array[pivot], array[begin] = array[begin], array[pivot] + return pivot + + def sort(self): + 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) + + end = len(self.segs) - 1 + _quicksort(self.segs, 0, end) + + # index of root in segs array + self.update_center() + + +class RoofPolygon(CutAblePolygon): + """ + ccw roof pitch boundary + closed by explicit segment + handle triangular shape with zero axis length + + mov <_________________ + | /\ + | | rot + | | left last <> next + \/_____axis_______>| + + node <_____axis________ next + | /\ + | | rot + | | right last <> next + mov \/________________>| + side angle + """ + def __init__(self, axis, side, fake_axis=None): + """ + Create a default rectangle + axis from node to next + slope float -z for 1 in side direction + side in ['LEFT', 'RIGHT'] in axis direction + + NOTE: + when axis length is null (eg: triangular shape) + use "fake_axis" with a 1 length to handle + distance from segment + """ + if side == 'LEFT': + # slope + self.slope = axis.slope_left + # width + self.width = axis.width_left + # constraint width + self.auto_mode = axis.auto_left + else: + # slope + self.slope = axis.slope_right + # width + self.width = axis.width_right + # constraint width + self.auto_mode = axis.auto_right + + self.side = side + # backward deps + self.backward = False + # pointers to neighboors along axis + self.last = None + self.next = None + self.other_side = None + + # axis segment + if side == 'RIGHT': + self.axis = axis.oposite + else: + self.axis = axis + + self.fake_axis = None + + # _axis is either a fake one or real one + # to prevent further check + if fake_axis is None: + self._axis = self.axis + self.fake_axis = self.axis + self.next_cross = axis + self.last_cross = axis + else: + if side == 'RIGHT': + self.fake_axis = fake_axis.oposite + else: + self.fake_axis = fake_axis + self._axis = self.fake_axis + + # unit vector perpendicular to axis + # looking at outside part + v = self.fake_axis.sized_normal(0, -1) + self.cross = v + self.next_cross = v + self.last_cross = v + + self.convex = True + # segments from axis end in ccw order + # closed by explicit segment + self.segs = [] + # holes + self.holes = [] + + # Triangular ends + self.node_tri = False + self.next_tri = False + self.is_tri = False + + # sizes + self.tmin = 0 + self.tmax = 1 + self.dt = 1 + self.ysize = 0 + self.xsize = 0 + self.vx = Vector() + self.vy = Vector() + self.vz = Vector() + + def move_node(self, p): + """ + Move slope point in node side + """ + if self.side == 'LEFT': + self.segs[-1].p0 = p + self.segs[2].p1 = p + else: + self.segs[2].p0 = p + self.segs[1].p1 = p + + def move_next(self, p): + """ + Move slope point in next side + """ + if self.side == 'LEFT': + self.segs[2].p0 = p + self.segs[1].p1 = p + else: + self.segs[-1].p0 = p + self.segs[2].p1 = p + + def node_link(self, da): + angle_90 = round(pi / 2, 4) + if self.side == 'LEFT': + idx = -1 + else: + idx = 1 + da = abs(round(da, 4)) + type = "LINK" + if da < angle_90: + type += "_VALLEY" + elif da > angle_90: + type += "_HIP" + self.segs[idx].type = type + + def next_link(self, da): + angle_90 = round(pi / 2, 4) + if self.side == 'LEFT': + idx = 1 + else: + idx = -1 + da = abs(round(da, 4)) + type = "LINK" + if da < angle_90: + type += "_VALLEY" + elif da > angle_90: + type += "_HIP" + self.segs[idx].type = type + + def bind(self, last, ccw=False): + """ + always in axis real direction + """ + # backward dependancy relative to axis + if last.backward: + self.backward = self.side == last.side + + if self.side == last.side: + last.next_cross = self.cross + else: + last.last_cross = self.cross + + self.last_cross = last.cross + + # axis of last / next segments + if self.backward: + self.next = last + last.last = self + else: + self.last = last + last.next = self + + # width auto + if self.auto_mode == 'AUTO': + self.width = last.width + self.slope = last.slope + elif self.auto_mode == 'WIDTH' and self.width != 0: + self.slope = last.slope * last.width / self.width + elif self.auto_mode == 'SLOPE' and self.slope != 0: + self.width = last.width * last.slope / self.slope + + self.make_segments() + last.make_segments() + + res, p, t = self.segs[2].intersect(last.segs[2]) + + if res: + # dont move anything when no intersection found + # aka when delta angle == 0 + self.move_node(p) + if self.side != last.side: + last.move_node(p) + else: + last.move_next(p) + + # Free mode + # move border + # and find intersections + # with sides + if self.auto_mode == 'ALL': + s0 = self._axis.offset(-self.width) + res, p0, t = self.segs[1].intersect(s0) + if res: + self.segs[2].p0 = p0 + self.segs[1].p1 = p0 + res, p1, t = self.segs[-1].intersect(s0) + if res: + self.segs[2].p1 = p1 + self.segs[-1].p0 = p1 + + # /\ + # | angle + # |____> + # + # v1 node -> next + if self.side == 'LEFT': + v1 = self._axis.v + else: + v1 = -self._axis.v + + if last.side == self.side: + # contigous, v0 node <- next + + # half angle between segments + if self.side == 'LEFT': + v0 = -last._axis.v + else: + v0 = last._axis.v + da = v0.angle_signed(v1) + if ccw: + if da < 0: + da = 2 * pi + da + elif da > 0: + da = da - 2 * pi + last.next_link(0.5 * da) + + else: + # alternate v0 node -> next + # half angle between segments + if last.side == 'LEFT': + v0 = last._axis.v + else: + v0 = -last._axis.v + da = v0.angle_signed(v1) + # angle always ccw + if ccw: + if da < 0: + da = 2 * pi + da + elif da > 0: + da = da - 2 * pi + last.node_link(0.5 * da) + + self.node_link(-0.5 * da) + + def next_seg(self, index): + idx = self.get_index(index + 1) + return self.segs[idx] + + def last_seg(self, index): + return self.segs[index - 1] + + def make_segments(self): + if len(self.segs) < 1: + s0 = self._axis + w = self.width + s1 = s0.straight(w, 1).rotate(pi / 2) + s1.type = 'SIDE' + s3 = s0.straight(w, 0).rotate(pi / 2).oposite + s3.type = 'SIDE' + s2 = StraightRoof(s1.p1, s3.p0 - s1.p1) + s2.type = 'BOTTOM' + self.segs = [s0, s1, s2, s3] + + def move_side(self, pt): + """ + offset side to point + """ + s2 = self.segs[2] + d0, t = self.distance(s2.p0) + d1, t = self.distance(pt) + # adjust width and slope according + self.width = d1 + self.slope = self.slope * d0 / d1 + self.segs[2] = s2.offset(d1 - d0) + + def propagate_backward(self, pt): + """ + Propagate slope, keep 2d angle of slope + Move first point and border + keep border parallel + adjust slope + and next shape + """ + # distance of p + # offset side to point + self.move_side(pt) + + # move verts on node side + self.move_next(pt) + + if self.side == 'LEFT': + # move verts on next side + res, p, t = self.segs[-1].intersect(self.segs[2]) + else: + # move verts on next side + res, p, t = self.segs[1].intersect(self.segs[2]) + + if res: + self.move_node(p) + + if self.next is not None and self.next.auto_mode in {'AUTO'}: + self.next.propagate_backward(p) + + def propagate_forward(self, pt): + """ + Propagate slope, keep 2d angle of slope + Move first point and border + keep border parallel + adjust slope + and next shape + """ + # offset side to point + self.move_side(pt) + + # move verts on node side + self.move_node(pt) + if self.side == 'LEFT': + # move verts on next side + res, p, t = self.segs[1].intersect(self.segs[2]) + else: + # move verts on next side + res, p, t = self.segs[-1].intersect(self.segs[2]) + + if res: + self.move_next(p) + if self.next is not None and self.next.auto_mode in {'AUTO'}: + self.next.propagate_forward(p) + + def rotate_next_slope(self, a0): + """ + Rotate next slope part + """ + if self.side == 'LEFT': + s0 = self.segs[1].rotate(a0) + s1 = self.segs[2] + res, p, t = s1.intersect(s0) + else: + s0 = self.segs[2] + s1 = self.segs[-1] + res, p, t = s1.oposite.rotate(-a0).intersect(s0) + + if res: + s1.p0 = p + s0.p1 = p + + if self.next is not None: + if self.next.auto_mode == 'ALL': + return + if self.next.backward: + self.next.propagate_backward(p) + else: + self.next.propagate_forward(p) + + def rotate_node_slope(self, a0): + """ + Rotate node slope part + """ + if self.side == 'LEFT': + s0 = self.segs[2] + s1 = self.segs[-1] + res, p, t = s1.oposite.rotate(-a0).intersect(s0) + else: + s0 = self.segs[1].rotate(a0) + s1 = self.segs[2] + res, p, t = s1.intersect(s0) + + if res: + s1.p0 = p + s0.p1 = p + + if self.next is not None: + if self.next.auto_mode == 'ALL': + return + if self.next.backward: + self.next.propagate_backward(p) + else: + self.next.propagate_forward(p) + + def distance(self, pt): + """ + distance from axis + always use fake_axis here to + allow axis being cut and + still work + """ + res, d, t = self.fake_axis.point_sur_segment(pt) + return d, t + + def altitude(self, pt): + d, t = self.distance(pt) + return -d * self.slope + + def uv(self, pt): + d, t = self.distance(pt) + return ((t - self.tmin) * self.xsize, d) + + def intersect(self, seg): + """ + compute intersections of a segment with boundaries + segment must start on axis + return segments inside + """ + it = [] + for s in self.segs: + res, p, t, u = seg.intersect_ext(s) + if res: + it.append((t, p)) + return it + + def merge(self, other): + + raise NotImplementedError + + def draw(self, context, z, verts, edges): + f = len(verts) + # + # 0_______1 + # |_______| + # 3 2 + verts.extend([(s.p0.x, s.p0.y, z + self.altitude(s.p0)) for s in self.segs]) + n_segs = len(self.segs) - 1 + edges.extend([[f + i, f + i + 1] for i in range(n_segs)]) + edges.append([f + n_segs, f]) + """ + f = len(verts) + verts.extend([(s.p1.x, s.p1.y, z + self.altitude(s.p1)) for s in self.segs]) + n_segs = len(self.segs) - 1 + edges.extend([[f + i, f + i + 1] for i in range(n_segs)]) + edges.append([f + n_segs, f]) + """ + # holes + for hole in self.holes: + f = len(verts) + # + # 0_______1 + # |_______| + # 3 2 + verts.extend([(s.p0.x, s.p0.y, z + self.altitude(s.p0)) for s in hole.segs]) + n_segs = len(hole.segs) - 1 + edges.extend([[f + i, f + i + 1] for i in range(n_segs)]) + edges.append([f + n_segs, f]) + + # axis + """ + f = len(verts) + verts.extend([self.axis.p0.to_3d(), self.axis.p1.to_3d()]) + edges.append([f, f + 1]) + + # cross + f = len(verts) + verts.extend([self.axis.lerp(0.5).to_3d(), (self.axis.lerp(0.5) + self.cross.v).to_3d()]) + edges.append([f, f + 1]) + """ + + # relationships arrows + if self.next or self.last: + w = 0.2 + s0 = self._axis.offset(-0.5 * self.ysize) + p0 = s0.lerp(0.4).to_3d() + p0.z = z + p1 = s0.lerp(0.6).to_3d() + p1.z = z + if self.side == 'RIGHT': + p0, p1 = p1, p0 + if self.backward: + p0, p1 = p1, p0 + s1 = s0.sized_normal(0.5, w) + s2 = s0.sized_normal(0.5, -w) + f = len(verts) + p2 = s1.p1.to_3d() + p2.z = z + p3 = s2.p1.to_3d() + p3.z = z + verts.extend([p1, p0, p2, p3]) + edges.extend([[f + 1, f], [f + 2, f], [f + 3, f]]) + + def as_string(self): + """ + Print strips relationships + """ + if self.backward: + dir = "/\\" + print("%s next" % (dir)) + else: + dir = "\\/" + print("%s node" % (dir)) + print("%s %s" % (dir, self.side)) + if self.backward: + print("%s node" % (dir)) + else: + print("%s next" % (dir)) + if self.next: + print("_________") + self.next.as_string() + else: + print("#########") + + def limits(self): + dist = [] + param_t = [] + for s in self.segs: + res, d, t = self.fake_axis.point_sur_segment(s.p0) + param_t.append(t) + dist.append(d) + + if len(param_t) > 0: + self.tmin = min(param_t) + self.tmax = max(param_t) + else: + self.tmin = 0 + self.tmax = 1 + + self.dt = self.tmax - self.tmin + + if len(dist) > 0: + self.ysize = max(dist) + else: + self.ysize = 0 + + self.xsize = self.fake_axis.length * self.dt + # vectors components of part matrix + # where x is is axis direction + # y down + # z up + vx = -self.fake_axis.v.normalized().to_3d() + vy = Vector((-vx.y, vx.x, self.slope)).normalized() + self.vx = vx + self.vy = vy + self.vz = vx.cross(vy) + + +""" +import bmesh +m = C.object.data +[(round(v.co.x, 3), round(v.co.y, 3), round(v.co.z, 3)) for v in m.vertices] +[tuple(p.vertices) for p in m.polygons] + +uvs = [] +bpy.ops.object.mode_set(mode='EDIT') +bm = bmesh.from_edit_mesh(m) +[tuple(i.index for i in edge.verts) for edge in bm.edges] + +layer = bm.loops.layers.uv.verify() +for i, face in enumerate(bm.faces): + uv = [] + for j, loop in enumerate(face.loops): + co = loop[layer].uv + uv.append((round(co.x, 2), round(co.y, 2))) + uvs.append(uv) +uvs +""" + + +class RoofGenerator(CutAbleGenerator): + + def __init__(self, d, origin=Vector((0, 0, 0))): + self.parts = d.parts + self.segs = [] + self.nodes = [] + self.pans = [] + self.length = 0 + self.origin = origin.to_2d() + self.z = origin.z + self.width_right = d.width_right + self.width_left = d.width_left + self.slope_left = d.slope_left + self.slope_right = d.slope_right + self.user_defined_tile = None + self.user_defined_uvs = None + self.user_defined_mat = None + self.is_t_child = d.t_parent != "" + + def add_part(self, part): + + if len(self.segs) < 1 or part.bound_idx < 1: + s = None + else: + s = self.segs[part.bound_idx - 1] + + a0 = part.a0 + + if part.constraint_type == 'SLOPE' and a0 == 0: + a0 = 90 + + # start a new roof + if s is None: + v = part.length * Vector((cos(a0), sin(a0))) + s = StraightRoof(self.origin, v) + else: + s = s.straight_roof(a0, part.length) + + # parent segment (root) index is v0_idx - 1 + s.v0_idx = min(len(self.segs), part.bound_idx) + + s.constraint_type = part.constraint_type + + if part.constraint_type == 'SLOPE': + s.enforce_part = part.enforce_part + else: + s.enforce_part = 'AUTO' + + s.angle_0 = a0 + s.take_precedence = part.take_precedence + s.auto_right = part.auto_right + s.auto_left = part.auto_left + s.width_left = part.width_left + s.width_right = part.width_right + s.slope_left = part.slope_left + s.slope_right = part.slope_right + s.type = 'AXIS' + s.triangular_end = part.triangular_end + self.segs.append(s) + + def locate_manipulators(self): + """ + + """ + for i, f in enumerate(self.segs): + + manipulators = self.parts[i].manipulators + p0 = f.p0.to_3d() + p0.z = self.z + p1 = f.p1.to_3d() + p1.z = self.z + # angle from last to current segment + if i > 0: + + manipulators[0].type_key = 'ANGLE' + v0 = self.segs[f.v0_idx - 1].straight(-1, 1).v.to_3d() + v1 = f.straight(1, 0).v.to_3d() + manipulators[0].set_pts([p0, v0, v1]) + + # segment length + manipulators[1].type_key = 'SIZE' + manipulators[1].prop1_name = "length" + manipulators[1].set_pts([p0, p1, (1.0, 0, 0)]) + + # dumb segment id + manipulators[2].set_pts([p0, p1, (1, 0, 0)]) + + p0 = f.lerp(0.5).to_3d() + p0.z = self.z + # size left + p1 = f.sized_normal(0.5, -self.parts[i].width_left).p1.to_3d() + p1.z = self.z + manipulators[3].set_pts([p0, p1, (1, 0, 0)]) + + # size right + p1 = f.sized_normal(0.5, self.parts[i].width_right).p1.to_3d() + p1.z = self.z + manipulators[4].set_pts([p0, p1, (-1, 0, 0)]) + + # slope left + n0 = f.sized_normal(0.5, -1) + p0 = n0.p1.to_3d() + p0.z = self.z + p1 = p0.copy() + p1.z = self.z - self.parts[i].slope_left + manipulators[5].set_pts([p0, p1, (-1, 0, 0)], normal=n0.v.to_3d()) + + # slope right + n0 = f.sized_normal(0.5, 1) + p0 = n0.p1.to_3d() + p0.z = self.z + p1 = p0.copy() + p1.z = self.z - self.parts[i].slope_right + manipulators[6].set_pts([p0, p1, (1, 0, 0)], normal=n0.v.to_3d()) + + def seg_partition(self, array, begin, end): + """ + sort tree segments by angle + """ + pivot = begin + for i in range(begin + 1, end + 1): + if array[i].a0 < array[begin].a0: + pivot += 1 + array[i], array[pivot] = array[pivot], array[i] + array[pivot], array[begin] = array[begin], array[pivot] + return pivot + + def sort_seg(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.seg_partition(array, begin, end) + _quicksort(array, begin, pivot - 1) + _quicksort(array, pivot + 1, end) + return _quicksort(array, begin, end) + + def make_roof(self, context): + """ + Init data structure for possibly multi branched nodes + nodes : radial relationships + pans : quad strip linear relationships + """ + + pans = [] + + # node are connected segments + # node + # (segment idx) + # (angle from root part > 0 right) + # (reversed) a seg connected by p1 + # "root" of node + nodes = [RoofAxisNode() for s in range(len(self.segs) + 1)] + + # Init width on seg 0 + s0 = self.segs[0] + if self.parts[0].auto_left in {'AUTO', 'SLOPE'}: + s0.width_left = self.width_left + if self.parts[0].auto_right in {'AUTO', 'SLOPE'}: + s0.width_right = self.width_right + if self.parts[0].auto_left in {'AUTO', 'WIDTH'}: + s0.slope_left = self.slope_left + if self.parts[0].auto_left in {'AUTO', 'WIDTH'}: + s0.slope_right = self.slope_right + + # make nodes with HORIZONTAL constraints + for idx, s in enumerate(self.segs): + s.v1_idx = idx + 1 + if s.constraint_type == 'HORIZONTAL': + left = RoofPolygon(s, 'LEFT') + right = RoofPolygon(s, 'RIGHT') + left.other_side = right + right.other_side = left + rs = RoofSegment(s, left, right) + pans.append(rs) + nodes[s.v0_idx].add(s.angle_0, False, s, left, right) + nodes[s.v1_idx].add(-pi, True, s, left, right) + + # set first node root + # so regular sort does work + nodes[0].root = nodes[0].segs[0] + self.nodes = nodes + # Propagate slope and width + # on node basis along axis + # bi-direction Radial around node + # from left and right to center + # contigous -> same + # T: and (x % 2 == 1) + # First one take precedence over others + # others inherit from side + # + # l / rb l = left + # 3 r = right + # l _1_ / b = backward + # r \ + # 2 + # r\ l + # + # X: rigth one r left one l (x % 2 == 0) + # inherits from side + # + # l 3 lb l = left + # l__1_|_2_l r = right + # r | r b = backward -> propagate in reverse axis direction + # r 4 rb + # + # for idx, node in enumerate(nodes): + # print("idx:%s node:%s" % (idx, node.root)) + + for idx, node in enumerate(nodes): + + node.sort() + + nb_segs = node.count + + if node.root is None: + continue + + left = node.root.left + right = node.root.right + + # basic one single node + if nb_segs < 2: + left.make_segments() + right.make_segments() + continue + + # get "root" slope and width + l_bind = left + r_bind = right + + # simple case: 2 contigous segments + if nb_segs == 2: + s = node.last + s.right.bind(r_bind, ccw=False) + s.left.bind(l_bind, ccw=True) + continue + + # More than 2 segments, uneven distribution + if nb_segs % 2 == 1: + # find wich child does take precedence + # first one on rootline (arbitrary) + center = (nb_segs - 1) / 2 + else: + # even distribution + center = nb_segs / 2 + + # user defined precedence if any + for i, s in enumerate(node.segs): + if s.seg.take_precedence: + center = i + break + + # bind right side to center + for i, s in enumerate(node.segs): + # skip axis + if i > 0: + if i < center: + # right contigous with last + s.right.bind(r_bind, ccw=False) + + # next bind to left + r_bind = s.left + + # left backward, not bound + # so setup width and slope + if s.left.auto_mode in {'AUTO', 'WIDTH'}: + s.left.slope = right.slope + if s.left.auto_mode in {'AUTO', 'SLOPE'}: + s.left.width = right.width + s.left.backward = True + else: + # right bound to last + s.right.bind(r_bind, ccw=False) + break + + # bind left side to center + for i, s in enumerate(reversed(node.segs)): + # skip axis + if i < nb_segs - center - 1: + # left contigous with last + s.left.bind(l_bind, ccw=True) + # next bind to right + l_bind = s.right + # right backward, not bound + # so setup width and slope + if s.right.auto_mode in {'AUTO', 'WIDTH'}: + s.right.slope = left.slope + if s.right.auto_mode in {'AUTO', 'SLOPE'}: + s.right.width = left.width + s.right.backward = True + else: + # right bound to last + s.left.bind(l_bind, ccw=True) + break + + # slope constraints allowed between segments + # multiple (up to 2) on start and end + # single between others + # + # 2 slope 2 slope 2 slope + # | | | + # |______section_1___|___section_2_____| + # | | | + # | | | + # multiple single multiple + + # add slopes constraints to nodes + for i, s in enumerate(self.segs): + if s.constraint_type == 'SLOPE': + nodes[s.v0_idx].add(s.angle_0, False, s, None, None) + + # sort nodes, remove duplicate slopes between + # horizontal, keeping only first one + for idx, node in enumerate(nodes): + to_remove = [] + node.sort() + # remove dup between all + # but start / end nodes + if node.n_horizontal > 1: + last = None + for i, s in enumerate(node.segs): + if s.seg.constraint_type == last: + if s.seg.constraint_type == 'SLOPE': + to_remove.append(i) + last = s.seg.constraint_type + for i in reversed(to_remove): + node.segs.pop(i) + node.update_center() + + for idx, node in enumerate(nodes): + + # a node may contain many slopes + # 2 * (part starting from node - 1) + # + # s0 + # root 0 |_______ + # | + # s1 + # + # s1 + # root _______| + # | + # s0 + # + # s3 3 s2 + # l \l|r/ l + # root ___\|/___ 2 + # r /|\ r + # /r|l\ + # s0 1 s1 + # + # s2 s1=slope + # |r / + # | / l + # |/____s + # + # root to first child -> equal side + # any other childs -> oposite sides + + if node.n_horizontal == 1: + # slopes at start or end of segment + # segment slope is not affected + if node.n_slope > 0: + # node has user def slope + s = node.root + s0 = node.left(node.center) + a0 = s0.seg.delta_angle(s.seg) + if node.root.reversed: + # slope at end of segment + # first one is right or left + if a0 < 0: + # right side + res, p, t = s0.seg.intersect(s.right.segs[2]) + s.right.segs[-1].p0 = p + s.right.segs[2].p1 = p + else: + # left side + res, p, t = s0.seg.intersect(s.left.segs[2]) + s.left.segs[1].p1 = p + s.left.segs[2].p0 = p + if node.n_slope > 1: + # last one must be left + s1 = node.right(node.center) + a1 = s1.seg.delta_angle(s.seg) + # both slopes on same side: + # skip this one + if a0 > 0 and a1 < 0: + # right side + res, p, t = s1.seg.intersect(s.right.segs[2]) + s.right.segs[-1].p0 = p + s.right.segs[2].p1 = p + if a0 < 0 and a1 > 0: + # left side + res, p, t = s1.seg.intersect(s.left.segs[2]) + s.left.segs[1].p1 = p + s.left.segs[2].p0 = p + + else: + # slope at start of segment + if a0 < 0: + # right side + res, p, t = s0.seg.intersect(s.right.segs[2]) + s.right.segs[1].p1 = p + s.right.segs[2].p0 = p + else: + # left side + res, p, t = s0.seg.intersect(s.left.segs[2]) + s.left.segs[-1].p0 = p + s.left.segs[2].p1 = p + if node.n_slope > 1: + # last one must be right + s1 = node.right(node.center) + a1 = s1.seg.delta_angle(s.seg) + # both slopes on same side: + # skip this one + if a0 > 0 and a1 < 0: + # right side + res, p, t = s1.seg.intersect(s.right.segs[2]) + s.right.segs[1].p1 = p + s.right.segs[2].p0 = p + if a0 < 0 and a1 > 0: + # left side + res, p, t = s1.seg.intersect(s.left.segs[2]) + s.left.segs[-1].p0 = p + s.left.segs[2].p1 = p + + else: + # slopes between segments + # does change next segment slope + for i, s0 in enumerate(node.segs): + s1 = node.left(i) + s2 = node.left(i + 1) + + if s1.seg.constraint_type == 'SLOPE': + + # 3 cases: + # s0 is root contigous -> sides are same + # s2 is root contigous -> sides are same + # back to back -> sides are not same + + if s0.reversed: + # contigous right / right + # 2 cases + # right is backward + # right is forward + if s2.right.backward: + # s0 depends on s2 + main = s2.right + v = main.segs[1].v + else: + # s2 depends on s0 + main = s0.right + v = -main.segs[-1].v + res, p, t = s1.seg.intersect(main.segs[2]) + if res: + # slope vector + dp = p - s1.seg.p0 + a0 = dp.angle_signed(v) + if s2.right.backward: + main.rotate_node_slope(a0) + else: + main.rotate_next_slope(-a0) + elif s2.reversed: + # contigous left / left + # 2 cases + # left is backward + # left is forward + if s0.left.backward: + # s0 depends on s2 + main = s0.left + v = -main.segs[-1].v + else: + # s2 depends on s0 + main = s2.left + v = main.segs[1].v + res, p, t = s1.seg.intersect(main.segs[2]) + if res: + # slope vector + dp = p - s1.seg.p0 + a0 = dp.angle_signed(v) + if s0.left.backward: + main.rotate_node_slope(-a0) + else: + main.rotate_next_slope(a0) + else: + # back left / right + # 2 cases + # left is backward + # left is forward + if s0.left.backward: + # s2 depends on s0 + main = s0.left + v = -main.segs[-1].v + else: + # s0 depends on s2 + main = s2.right + v = main.segs[1].v + + res, p, t = s1.seg.intersect(main.segs[2]) + if res: + # slope vector + dp = p - s1.seg.p0 + a0 = dp.angle_signed(v) + if s0.left.backward: + main.rotate_node_slope(-a0) + else: + main.rotate_node_slope(a0) + + self.pans = [] + + # triangular ends + for node in self.nodes: + if node.n_horizontal == 1 and node.root.seg.triangular_end: + if node.root.reversed: + # Next side (segment end) + left = node.root.left + right = node.root.right + left.next_tri = True + right.next_tri = True + + s0 = left.segs[1] + s1 = left.segs[2] + s2 = right.segs[-1] + s3 = right.segs[2] + p0 = s1.lerp(-left.width / s1.length) + p1 = s0.p0 + p2 = s3.lerp(1 + right.width / s3.length) + + # compute slope from points + p3 = p0.to_3d() + p3.z = -left.width * left.slope + p4 = p1.to_3d() + p5 = p2.to_3d() + p5.z = -right.width * right.slope + n = (p3 - p4).normalized().cross((p5 - p4).normalized()) + v = n.cross(Vector((0, 0, 1))) + dz = n.cross(v) + + # compute axis + s = StraightRoof(p1, v) + res, d0, t = s.point_sur_segment(p0) + res, d1, t = s.point_sur_segment(p2) + p = RoofPolygon(s, 'RIGHT') + p.make_segments() + p.slope = -dz.z / dz.to_2d().length + p.is_tri = True + + p.cross = StraightRoof(p1, (p2 - p0)).sized_normal(0, -1) + p.next_cross = left.cross + p.last_cross = right.cross + right.next_cross = p.cross + left.next_cross = p.cross + + # remove axis seg of tri + p.segs[-1].p0 = p0 + p.segs[-1].p1 = p1 + p.segs[2].p0 = p2 + p.segs[2].p1 = p0 + p.segs[1].p1 = p2 + p.segs[1].p0 = p1 + p.segs[1].type = 'LINK_HIP' + p.segs[-1].type = 'LINK_HIP' + p.segs.pop(0) + # adjust left and side borders + s0.p1 = p0 + s1.p0 = p0 + s2.p0 = p2 + s3.p1 = p2 + s0.type = 'LINK_HIP' + s2.type = 'LINK_HIP' + self.pans.append(p) + + elif not self.is_t_child: + # no triangular part with t_child + # on "node" parent roof side + left = node.root.left + right = node.root.right + left.node_tri = True + right.node_tri = True + s0 = right.segs[1] + s1 = right.segs[2] + s2 = left.segs[-1] + s3 = left.segs[2] + p0 = s1.lerp(-right.width / s1.length) + p1 = s0.p0 + p2 = s3.lerp(1 + left.width / s3.length) + + # compute axis and slope from points + p3 = p0.to_3d() + p3.z = -right.width * right.slope + p4 = p1.to_3d() + p5 = p2.to_3d() + p5.z = -left.width * left.slope + n = (p3 - p4).normalized().cross((p5 - p4).normalized()) + v = n.cross(Vector((0, 0, 1))) + dz = n.cross(v) + + s = StraightRoof(p1, v) + p = RoofPolygon(s, 'RIGHT') + p.make_segments() + p.slope = -dz.z / dz.to_2d().length + p.is_tri = True + + p.cross = StraightRoof(p1, (p2 - p0)).sized_normal(0, -1) + p.next_cross = right.cross + p.last_cross = left.cross + right.last_cross = p.cross + left.last_cross = p.cross + + # remove axis seg of tri + p.segs[-1].p0 = p0 + p.segs[-1].p1 = p1 + p.segs[2].p0 = p2 + p.segs[2].p1 = p0 + p.segs[1].p1 = p2 + p.segs[1].p0 = p1 + p.segs[1].type = 'LINK_HIP' + p.segs[-1].type = 'LINK_HIP' + p.segs.pop(0) + # adjust left and side borders + s0.p1 = p0 + s1.p0 = p0 + s2.p0 = p2 + s3.p1 = p2 + s0.type = 'LINK_HIP' + s2.type = 'LINK_HIP' + self.pans.append(p) + + # make flat array + for pan in pans: + self.pans.extend([pan.left, pan.right]) + + # merge contigous with 0 angle diff + to_remove = [] + for i, pan in enumerate(self.pans): + if pan.backward: + next = pan.last + if next is not None: + # same side only can merge + if next.side == pan.side: + if round(next._axis.delta_angle(pan._axis), 4) == 0: + to_remove.append(i) + next.next = pan.next + next.last_cross = pan.last_cross + next.node_tri = pan.node_tri + + next.slope = pan.slope + if pan.side == 'RIGHT': + if next.backward: + next._axis.p1 = pan._axis.p1 + next.segs[1] = pan.segs[1] + next.segs[2].p0 = pan.segs[2].p0 + else: + next._axis.p0 = pan._axis.p0 + next.segs[-1] = pan.segs[-1] + next.segs[2].p1 = pan.segs[2].p1 + else: + if next.backward: + next._axis.p0 = pan._axis.p0 + next.segs[-1] = pan.segs[-1] + next.segs[2].p1 = pan.segs[2].p1 + else: + next._axis.p1 = pan._axis.p1 + next.segs[1] = pan.segs[1] + next.segs[2].p0 = pan.segs[2].p0 + else: + next = pan.next + if next is not None: + # same side only can merge + if next.side == pan.side: + if round(next._axis.delta_angle(pan._axis), 4) == 0: + to_remove.append(i) + next.last = pan.last + next.last_cross = pan.last_cross + next.node_tri = pan.node_tri + + next.slope = pan.slope + if pan.side == 'LEFT': + if next.backward: + next._axis.p1 = pan._axis.p1 + next.segs[1] = pan.segs[1] + next.segs[2].p0 = pan.segs[2].p0 + else: + next._axis.p0 = pan._axis.p0 + next.segs[-1] = pan.segs[-1] + next.segs[2].p1 = pan.segs[2].p1 + else: + if next.backward: + next._axis.p0 = pan._axis.p0 + next.segs[-1] = pan.segs[-1] + next.segs[2].p1 = pan.segs[2].p1 + else: + next._axis.p1 = pan._axis.p1 + next.segs[1] = pan.segs[1] + next.segs[2].p0 = pan.segs[2].p0 + + for i in reversed(to_remove): + self.pans.pop(i) + + # compute limits + for pan in self.pans: + pan.limits() + + """ + for pan in self.pans: + if pan.last is None: + pan.as_string() + """ + return + + def lambris(self, context, o, d): + + idmat = 0 + lambris_height = 0.02 + alt = self.z - lambris_height + for pan in self.pans: + + verts = [] + faces = [] + matids = [] + uvs = [] + + f = len(verts) + verts.extend([(s.p0.x, s.p0.y, alt + pan.altitude(s.p0)) for s in pan.segs]) + uvs.append([pan.uv(s.p0) for s in pan.segs]) + n_segs = len(pan.segs) + face = [f + i for i in range(n_segs)] + faces.append(face) + matids.append(idmat) + + bm = bmed.buildmesh( + context, o, verts, faces, matids=matids, uvs=uvs, + weld=False, clean=False, auto_smooth=True, temporary=True) + + self.cut_holes(bm, pan) + + bmesh.ops.dissolve_limit(bm, + angle_limit=0.01, + use_dissolve_boundaries=False, + verts=bm.verts, + edges=bm.edges, + delimit=1) + + geom = bm.faces[:] + verts = bm.verts[:] + bmesh.ops.solidify(bm, geom=geom, thickness=0.0001) + bmesh.ops.translate(bm, vec=Vector((0, 0, lambris_height)), space=o.matrix_world, verts=verts) + + # merge with object + bmed.bmesh_join(context, o, [bm], normal_update=True) + + bpy.ops.object.mode_set(mode='OBJECT') + + def couverture(self, context, o, d): + + idmat = 7 + rand = 3 + + sx, sy, sz = d.tile_size_x, d.tile_size_y, d.tile_size_z + + """ + /* Bevel offset_type slot values */ + enum { + BEVEL_AMT_OFFSET, + BEVEL_AMT_WIDTH, + BEVEL_AMT_DEPTH, + BEVEL_AMT_PERCENT + }; + """ + offset_type = 3 + + if d.tile_offset > 0: + offset = - d.tile_offset / 100 + else: + offset = 0 + + if d.tile_model == 'BRAAS2': + t_pts = [Vector(p) for p in [ + (0.06, -1.0, 1.0), (0.19, -1.0, 0.5), (0.31, -1.0, 0.5), (0.44, -1.0, 1.0), + (0.56, -1.0, 1.0), (0.69, -1.0, 0.5), (0.81, -1.0, 0.5), (0.94, -1.0, 1.0), + (0.06, 0.0, 0.5), (0.19, 0.0, 0.0), (0.31, 0.0, 0.0), (0.44, 0.0, 0.5), + (0.56, 0.0, 0.5), (0.69, 0.0, 0.0), (0.81, 0.0, 0.0), (0.94, 0.0, 0.5), + (-0.0, -1.0, 1.0), (-0.0, 0.0, 0.5), (1.0, -1.0, 1.0), (1.0, 0.0, 0.5)]] + t_faces = [ + (16, 0, 8, 17), (0, 1, 9, 8), (1, 2, 10, 9), (2, 3, 11, 10), + (3, 4, 12, 11), (4, 5, 13, 12), (5, 6, 14, 13), (6, 7, 15, 14), (7, 18, 19, 15)] + elif d.tile_model == 'BRAAS1': + t_pts = [Vector(p) for p in [ + (0.1, -1.0, 1.0), (0.2, -1.0, 0.5), (0.6, -1.0, 0.5), (0.7, -1.0, 1.0), + (0.1, 0.0, 0.5), (0.2, 0.0, 0.0), (0.6, 0.0, 0.0), (0.7, 0.0, 0.5), + (-0.0, -1.0, 1.0), (-0.0, 0.0, 0.5), (1.0, -1.0, 1.0), (1.0, 0.0, 0.5)]] + t_faces = [(8, 0, 4, 9), (0, 1, 5, 4), (1, 2, 6, 5), (2, 3, 7, 6), (3, 10, 11, 7)] + elif d.tile_model == 'ETERNIT': + t_pts = [Vector(p) for p in [ + (0.11, -1.0, 1.0), (0.9, -1.0, 1.0), (0.0, -0.79, 0.79), + (1.0, -0.79, 0.79), (0.0, 2.0, -2.0), (1.0, 2.0, -2.0)]] + t_faces = [(0, 1, 3, 5, 4, 2)] + elif d.tile_model == 'ONDULEE': + t_pts = [Vector(p) for p in [ + (0.0, -1.0, 0.1), (0.05, -1.0, 1.0), (0.1, -1.0, 0.1), + (0.15, -1.0, 1.0), (0.2, -1.0, 0.1), (0.25, -1.0, 1.0), + (0.3, -1.0, 0.1), (0.35, -1.0, 1.0), (0.4, -1.0, 0.1), + (0.45, -1.0, 1.0), (0.5, -1.0, 0.1), (0.55, -1.0, 1.0), + (0.6, -1.0, 0.1), (0.65, -1.0, 1.0), (0.7, -1.0, 0.1), + (0.75, -1.0, 1.0), (0.8, -1.0, 0.1), (0.85, -1.0, 1.0), + (0.9, -1.0, 0.1), (0.95, -1.0, 1.0), (1.0, -1.0, 0.1), + (0.0, 0.0, 0.0), (0.05, 0.0, 0.9), (0.1, 0.0, 0.0), + (0.15, 0.0, 0.9), (0.2, 0.0, 0.0), (0.25, 0.0, 0.9), + (0.3, 0.0, 0.0), (0.35, 0.0, 0.9), (0.4, 0.0, 0.0), + (0.45, 0.0, 0.9), (0.5, 0.0, 0.0), (0.55, 0.0, 0.9), + (0.6, 0.0, 0.0), (0.65, 0.0, 0.9), (0.7, 0.0, 0.0), + (0.75, 0.0, 0.9), (0.8, 0.0, 0.0), (0.85, 0.0, 0.9), + (0.9, 0.0, 0.0), (0.95, 0.0, 0.9), (1.0, 0.0, 0.0)]] + t_faces = [ + (0, 1, 22, 21), (1, 2, 23, 22), (2, 3, 24, 23), + (3, 4, 25, 24), (4, 5, 26, 25), (5, 6, 27, 26), + (6, 7, 28, 27), (7, 8, 29, 28), (8, 9, 30, 29), + (9, 10, 31, 30), (10, 11, 32, 31), (11, 12, 33, 32), + (12, 13, 34, 33), (13, 14, 35, 34), (14, 15, 36, 35), + (15, 16, 37, 36), (16, 17, 38, 37), (17, 18, 39, 38), + (18, 19, 40, 39), (19, 20, 41, 40)] + elif d.tile_model == 'METAL': + t_pts = [Vector(p) for p in [ + (0.0, -1.0, 0.0), (0.99, -1.0, 0.0), (1.0, -1.0, 0.0), + (0.0, 0.0, 0.0), (0.99, 0.0, 0.0), (1.0, 0.0, 0.0), + (0.99, -1.0, 1.0), (1.0, -1.0, 1.0), (1.0, 0.0, 1.0), (0.99, 0.0, 1.0)]] + t_faces = [(0, 1, 4, 3), (7, 2, 5, 8), (1, 6, 9, 4), (6, 7, 8, 9)] + elif d.tile_model == 'LAUZE': + t_pts = [Vector(p) for p in [ + (0.75, -0.8, 0.8), (0.5, -1.0, 1.0), (0.25, -0.8, 0.8), + (0.0, -0.5, 0.5), (1.0, -0.5, 0.5), (0.0, 0.5, -0.5), (1.0, 0.5, -0.5)]] + t_faces = [(1, 0, 4, 6, 5, 3, 2)] + elif d.tile_model == 'PLACEHOLDER': + t_pts = [Vector(p) for p in [(0.0, -1.0, 1.0), (1.0, -1.0, 1.0), (0.0, 0.0, 0.0), (1.0, 0.0, 0.0)]] + t_faces = [(0, 1, 3, 2)] + elif d.tile_model == 'ROMAN': + t_pts = [Vector(p) for p in [ + (0.18, 0.0, 0.3), (0.24, 0.0, 0.58), (0.76, 0.0, 0.58), + (0.82, 0.0, 0.3), (0.05, -1.0, 0.5), (0.14, -1.0, 0.8), + (0.86, -1.0, 0.8), (0.95, -1.0, 0.5), (0.45, 0.0, 0.5), + (0.36, 0.0, 0.2), (-0.36, 0.0, 0.2), (-0.45, -0.0, 0.5), + (0.32, -1.0, 0.7), (0.26, -1.0, 0.42), (-0.26, -1.0, 0.42), + (-0.32, -1.0, 0.7), (0.5, 0.0, 0.74), (0.5, -1.0, 1.0), + (-0.0, -1.0, 0.26), (-0.0, 0.0, 0.0)] + ] + t_faces = [ + (0, 4, 5, 1), (16, 17, 6, 2), (2, 6, 7, 3), + (13, 12, 8, 9), (18, 13, 9, 19), (15, 14, 10, 11), + (14, 18, 19, 10), (1, 5, 17, 16) + ] + elif d.tile_model == 'ROUND': + t_pts = [Vector(p) for p in [ + (0.0, -0.5, 0.5), (1.0, -0.5, 0.5), (0.0, 0.0, 0.0), + (1.0, 0.0, 0.0), (0.93, -0.71, 0.71), (0.78, -0.88, 0.88), + (0.39, -0.97, 0.97), (0.61, -0.97, 0.97), (0.07, -0.71, 0.71), + (0.22, -0.88, 0.88)] + ] + t_faces = [(6, 7, 5, 4, 1, 3, 2, 0, 8, 9)] + else: + return + + n_faces = len(t_faces) + t_uvs = [[(t_pts[i].x, t_pts[i].y) for i in f] for f in t_faces] + + dx, dy = d.tile_space_x, d.tile_space_y + + ttl = len(self.pans) + step = 100 / ttl + + context.scene.archipack_progress_text = "Build tiles:" + + for i, pan in enumerate(self.pans): + + seg = pan.fake_axis + # compute base matrix top left of face + vx = pan.vx + vy = pan.vy + vz = pan.vz + + x0, y0 = seg.lerp(pan.tmax) + z0 = self.z + d.tile_altitude + ysize_2d = (d.tile_border + pan.ysize) + space_x = pan.xsize + 2 * d.tile_side + space_y = ysize_2d * sqrt(1 + pan.slope * pan.slope) + n_x = 1 + int(space_x / dx) + n_y = 1 + int(space_y / dy) + + if d.tile_fit_x: + dx = space_x / n_x + + if d.tile_fit_y: + dy = space_y / n_y + + if d.tile_alternate: + n_y += 1 + + tM = Matrix([ + [vx.x, vy.x, vz.x, x0], + [vx.y, vy.y, vz.y, y0], + [vx.z, vy.z, vz.z, z0], + [0, 0, 0, 1] + ]) + + verts = [] + faces = [] + matids = [] + uvs = [] + + # steps for this pan + substep = step / n_y + # print("step:%s sub:%s" % (step, substep)) + + for k in range(n_y): + + progress = step * i + substep * k + # print("progress %s" % (progress)) + context.scene.archipack_progress = progress + + y = k * dy + + x0 = offset * dx - d.tile_side + nx = n_x + + if d.tile_alternate and k % 2 == 1: + x0 -= 0.5 * dx + nx += 1 + + if d.tile_offset > 0: + nx += 1 + + for j in range(nx): + x = x0 + j * dx + lM = tM * Matrix([ + [sx, 0, 0, x], + [0, sy, 0, -y], + [0, 0, sz, 0], + [0, 0, 0, 1] + ]) + + v = len(verts) + + verts.extend([lM * p for p in t_pts]) + faces.extend([tuple(i + v for i in f) for f in t_faces]) + id = randint(idmat, idmat + rand) + t_mats = [id for i in range(n_faces)] + matids.extend(t_mats) + uvs.extend(t_uvs) + + # build temp bmesh and bissect + bm = bmed.buildmesh( + context, o, verts, faces, matids=matids, uvs=uvs, + weld=False, clean=False, auto_smooth=True, temporary=True) + + # clean outer on convex parts + # pan.convex = False + remove = pan.convex + + for s in pan.segs: + # seg without length lead to invalid normal + if s.length > 0: + if s.type == 'AXIS': + self.bissect(bm, s.p1.to_3d(), s.cross_z.to_3d(), clear_outer=remove) + elif s.type == 'BOTTOM': + s0 = s.offset(d.tile_border) + dz = pan.altitude(s0.p0) + vx = s0.v.to_3d() + vx.z = pan.altitude(s0.p1) - dz + vy = vz.cross(vx.normalized()) + x, y = s0.p0 + z = z0 + dz + self.bissect(bm, Vector((x, y, z)), -vy, clear_outer=remove) + elif s.type == 'SIDE': + p0 = s.p0 + s.cross_z.normalized() * d.tile_side + self.bissect(bm, p0.to_3d(), s.cross_z.to_3d(), clear_outer=remove) + elif s.type == 'LINK_VALLEY': + p0 = s.p0 - s.cross_z.normalized() * d.tile_couloir + self.bissect(bm, p0.to_3d(), s.cross_z.to_3d(), clear_outer=remove) + elif s.type in {'LINK_HIP', 'LINK'}: + self.bissect(bm, s.p0.to_3d(), s.cross_z.to_3d(), clear_outer=remove) + + # when not convex, select and remove outer parts + if not pan.convex: + """ + /* del "context" slot values, used for operator too */ + enum { + DEL_VERTS = 1, + DEL_EDGES, + DEL_ONLYFACES, + DEL_EDGESFACES, + DEL_FACES, + /* A version of 'DEL_FACES' that keeps edges on face boundaries, + * allowing the surrounding edge-loop to be kept from removed face regions. */ + DEL_FACES_KEEP_BOUNDARY, + DEL_ONLYTAGGED + }; + """ + # Build boundary including borders and bottom offsets + new_s = None + segs = [] + for s in pan.segs: + if s.length > 0: + if s.type == 'LINK_VALLEY': + offset = -d.tile_couloir + elif s.type == 'BOTTOM': + offset = d.tile_border + elif s.type == 'SIDE': + offset = d.tile_side + else: + offset = 0 + new_s = s.make_offset(offset, new_s) + segs.append(new_s) + + if len(segs) > 0: + # last / first intersection + res, p, t = segs[0].intersect(segs[-1]) + if res: + segs[0].p0 = p + segs[-1].p1 = p + f_geom = [f for f in bm.faces if not pan.inside(f.calc_center_median().to_2d(), segs)] + if len(f_geom) > 0: + bmesh.ops.delete(bm, geom=f_geom, context=5) + + self.cut_holes(bm, pan) + + bmesh.ops.dissolve_limit(bm, + angle_limit=0.01, + use_dissolve_boundaries=False, + verts=bm.verts[:], + edges=bm.edges[:], + delimit=1) + + if d.tile_bevel: + geom = bm.verts[:] + geom.extend(bm.edges[:]) + bmesh.ops.bevel(bm, + geom=geom, + offset=d.tile_bevel_amt, + offset_type=offset_type, + segments=d.tile_bevel_segs, + profile=0.5, + vertex_only=False, + clamp_overlap=True, + material=-1) + + if d.tile_solidify: + geom = bm.faces[:] + verts = bm.verts[:] + bmesh.ops.solidify(bm, geom=geom, thickness=0.0001) + bmesh.ops.translate(bm, vec=vz * d.tile_height, space=o.matrix_world, verts=verts) + + # merge with object + bmed.bmesh_join(context, o, [bm], normal_update=True) + bpy.ops.object.mode_set(mode='OBJECT') + + context.scene.archipack_progress = -1 + + def _rake(self, s, i, boundary, pan, + width, height, altitude, offset, idmat, + verts, faces, edges, matids, uvs): + + f = len(verts) + + s0 = s.offset(offset - width) + s1 = s.offset(offset) + + p0 = s0.p0 + p1 = s1.p0 + p2 = s0.p1 + p3 = s1.p1 + + s2 = boundary.last_seg(i) + s3 = boundary.next_seg(i) + + if s2.type == 'SIDE': + # intersect last seg offset + s4 = s2.offset(offset - width) + s5 = s2.offset(offset) + res, p, t = s4.intersect(s0) + if res: + p0 = p + res, p, t = s5.intersect(s1) + if res: + p1 = p + + elif s2.type == 'AXIS' or 'LINK' in s2.type: + # intersect axis or link seg + res, p, t = s2.intersect(s0) + if res: + p0 = p + res, p, t = s2.intersect(s1) + if res: + p1 = p + + if s3.type == 'SIDE': + # intersect next seg offset + s4 = s3.offset(offset - width) + s5 = s3.offset(offset) + res, p, t = s4.intersect(s0) + if res: + p2 = p + res, p, t = s5.intersect(s1) + if res: + p3 = p + + elif s3.type == 'AXIS' or 'LINK' in s3.type: + # intersect axis or link seg + res, p, t = s3.intersect(s0) + if res: + p2 = p + res, p, t = s3.intersect(s1) + if res: + p3 = p + + x0, y0 = p0 + x1, y1 = p1 + x2, y2 = p3 + x3, y3 = p2 + + z0 = self.z + altitude + pan.altitude(p0) + z1 = self.z + altitude + pan.altitude(p1) + z2 = self.z + altitude + pan.altitude(p3) + z3 = self.z + altitude + pan.altitude(p2) + + verts.extend([ + (x0, y0, z0), + (x1, y1, z1), + (x2, y2, z2), + (x3, y3, z3), + ]) + z0 -= height + z1 -= height + z2 -= height + z3 -= height + verts.extend([ + (x0, y0, z0), + (x1, y1, z1), + (x2, y2, z2), + (x3, y3, z3), + ]) + + faces.extend([ + # top + (f, f + 1, f + 2, f + 3), + # sides + (f, f + 4, f + 5, f + 1), + (f + 1, f + 5, f + 6, f + 2), + (f + 2, f + 6, f + 7, f + 3), + (f + 3, f + 7, f + 4, f), + # bottom + (f + 4, f + 7, f + 6, f + 5) + ]) + edges.append([f, f + 3]) + edges.append([f + 1, f + 2]) + edges.append([f + 4, f + 7]) + edges.append([f + 5, f + 6]) + + matids.extend([idmat, idmat, idmat, idmat, idmat, idmat]) + uvs.extend([ + [(0, 0), (0, 1), (1, 1), (1, 0)], + [(0, 0), (0, 1), (1, 1), (1, 0)], + [(0, 0), (0, 1), (1, 1), (1, 0)], + [(0, 0), (0, 1), (1, 1), (1, 0)], + [(0, 0), (0, 1), (1, 1), (1, 0)], + [(0, 0), (0, 1), (1, 1), (1, 0)] + ]) + + def rake(self, d, verts, faces, edges, matids, uvs): + + ##################### + # Vire-vents + ##################### + + idmat = 1 + for pan in self.pans: + + for hole in pan.holes: + for i, s in enumerate(hole.segs): + if s.type == 'SIDE': + self._rake(s, + i, + hole, pan, + d.rake_width, + d.rake_height, + d.rake_altitude, + d.rake_offset, + idmat, + verts, + faces, + edges, + matids, + uvs) + + for i, s in enumerate(pan.segs): + if s.type == 'SIDE': + self._rake(s, + i, + pan, pan, + d.rake_width, + d.rake_height, + d.rake_altitude, + d.rake_offset, + idmat, + verts, + faces, + edges, + matids, + uvs) + + def _facia(self, s, i, boundary, pan, tri_0, tri_1, + width, height, altitude, offset, idmat, + verts, faces, edges, matids, uvs): + + f = len(verts) + s0 = s.offset(offset) + s1 = s.offset(offset + width) + + s2 = boundary.last_seg(i) + s3 = boundary.next_seg(i) + s4 = s2 + s5 = s3 + + p0 = s0.p0 + p1 = s1.p0 + p2 = s0.p1 + p3 = s1.p1 + + # find last neighboor depending on type + if s2.type == 'AXIS' or 'LINK' in s2.type: + # apply only on boundarys + if not s.is_hole: + # use last axis + if pan.side == 'LEFT': + s6 = pan.next_cross + else: + s6 = pan.last_cross + if tri_0: + s2 = s.copy + else: + s2 = s2.oposite + s2.v = (s.sized_normal(0, 1).v + s6.v).normalized() + s4 = s2 + + elif s2.type == 'SIDE': + s2 = s.copy + s2.type = 'SIDE' + s2.v = s.sized_normal(0, 1).v + s4 = s2 + else: + s2 = s2.offset(offset) + s4 = s2.offset(offset + width) + + # find next neighboor depending on type + if s3.type == 'AXIS' or 'LINK' in s3.type: + if not s.is_hole: + # use last axis + if pan.side == 'LEFT': + s6 = pan.last_cross + else: + s6 = pan.next_cross + if tri_1: + s3 = s.oposite + else: + s3 = s3.copy + s3.v = (s.sized_normal(0, 1).v + s6.v).normalized() + s5 = s3 + elif s3.type == 'SIDE': + # when next is side, use perpendicular + s3 = s.oposite + s3.type = 'SIDE' + s3.v = s.sized_normal(0, 1).v + s5 = s3 + else: + s3 = s3.offset(offset) + s5 = s3.offset(offset + width) + + # units vectors and scale + # is unit normal on sides + # print("s.p:%s, s.v:%s s1.p::%s s1.v::%s" % (s.p, s.v, s1.p, s1.v)) + res, p, t = s0.intersect(s2) + if res: + p0 = p + res, p, t = s0.intersect(s3) + if res: + p1 = p + res, p, t = s1.intersect(s4) + if res: + p2 = p + res, p, t = s1.intersect(s5) + if res: + p3 = p + + x0, y0 = p0 + x1, y1 = p2 + x2, y2 = p3 + x3, y3 = p1 + + z0 = self.z + altitude + pan.altitude(p0) + z1 = self.z + altitude + pan.altitude(p2) + z2 = self.z + altitude + pan.altitude(p3) + z3 = self.z + altitude + pan.altitude(p1) + + verts.extend([ + (x0, y0, z0), + (x1, y1, z1), + (x2, y2, z2), + (x3, y3, z3), + ]) + + z0 -= height + z1 -= height + z2 -= height + z3 -= height + verts.extend([ + (x0, y0, z0), + (x1, y1, z1), + (x2, y2, z2), + (x3, y3, z3), + ]) + + faces.extend([ + # top + (f, f + 1, f + 2, f + 3), + # sides + (f, f + 4, f + 5, f + 1), + (f + 1, f + 5, f + 6, f + 2), + (f + 2, f + 6, f + 7, f + 3), + (f + 3, f + 7, f + 4, f), + # bottom + (f + 4, f + 7, f + 6, f + 5) + ]) + edges.append([f, f + 3]) + edges.append([f + 1, f + 2]) + edges.append([f + 4, f + 7]) + edges.append([f + 5, f + 6]) + matids.extend([idmat, idmat, idmat, idmat, idmat, idmat]) + uvs.extend([ + [(0, 0), (0, 1), (1, 1), (1, 0)], + [(0, 0), (0, 1), (1, 1), (1, 0)], + [(0, 0), (0, 1), (1, 1), (1, 0)], + [(0, 0), (0, 1), (1, 1), (1, 0)], + [(0, 0), (0, 1), (1, 1), (1, 0)], + [(0, 0), (0, 1), (1, 1), (1, 0)] + ]) + + def facia(self, d, verts, faces, edges, matids, uvs): + + ##################### + # Larmiers + ##################### + + idmat = 2 + for pan in self.pans: + + for hole in pan.holes: + for i, s in enumerate(hole.segs): + if s.type == 'BOTTOM': + self._facia(s, + i, + hole, pan, + False, False, + d.facia_width, + d.facia_height, + d.facia_altitude, + d.facia_offset, + idmat, + verts, + faces, + edges, + matids, + uvs) + + for i, s in enumerate(pan.segs): + if s.type == 'BOTTOM': + + tri_0 = pan.node_tri + tri_1 = pan.next_tri + + # triangular ends apply on boundary only + # unless cut, boundary is parallel to axis + # except for triangular ends + if pan.side == 'LEFT': + tri_0, tri_1 = tri_1, tri_0 + + self._facia(s, + i, + pan, pan, + tri_0, tri_1, + d.facia_width, + d.facia_height, + d.facia_altitude, + d.facia_offset, + idmat, + verts, + faces, + edges, + matids, + uvs) + + continue + + f = len(verts) + s0 = s.offset(d.facia_width) + + s1 = pan.last_seg(i) + s2 = pan.next_seg(i) + + # triangular ends apply on boundary only + # unless cut, boundary is parallel to axis + # except for triangular ends + + tri_0 = (pan.node_tri and not s.is_hole) or pan.is_tri + tri_1 = (pan.next_tri and not s.is_hole) or pan.is_tri + + if pan.side == 'LEFT': + tri_0, tri_1 = tri_1, tri_0 + + # tiangular use bottom segment direction + # find last neighboor depending on type + if s1.type == 'AXIS' or 'LINK' in s1.type: + # apply only on boundarys + if not s.is_hole: + # use last axis + if pan.side == 'LEFT': + s3 = pan.next_cross + else: + s3 = pan.last_cross + if tri_0: + s1 = s.copy + else: + s1 = s1.oposite + s1.v = (s.sized_normal(0, 1).v + s3.v).normalized() + elif s1.type == 'SIDE': + s1 = s.copy + s1.type = 'SIDE' + s1.v = s.sized_normal(0, 1).v + else: + s1 = s1.offset(d.facia_width) + + # find next neighboor depending on type + if s2.type == 'AXIS' or 'LINK' in s2.type: + if not s.is_hole: + # use last axis + if pan.side == 'LEFT': + s3 = pan.last_cross + else: + s3 = pan.next_cross + if tri_1: + s2 = s.oposite + else: + s2 = s2.copy + s2.v = (s.sized_normal(0, 1).v + s3.v).normalized() + elif s2.type == 'SIDE': + s2 = s.oposite + s2.type = 'SIDE' + s2.v = s.sized_normal(0, 1).v + else: + + s2 = s2.offset(d.facia_width) + + # units vectors and scale + # is unit normal on sides + # print("s.p:%s, s.v:%s s1.p::%s s1.v::%s" % (s.p, s.v, s1.p, s1.v)) + res, p0, t = s0.intersect(s1) + res, p1, t = s0.intersect(s2) + + x0, y0 = s.p0 + x1, y1 = p0 + x2, y2 = p1 + x3, y3 = s.p1 + z0 = self.z + d.facia_altitude + pan.altitude(s.p0) + z1 = self.z + d.facia_altitude + pan.altitude(s.p1) + verts.extend([ + (x0, y0, z0), + (x1, y1, z0), + (x2, y2, z1), + (x3, y3, z1), + ]) + z0 -= d.facia_height + z1 -= d.facia_height + verts.extend([ + (x0, y0, z0), + (x1, y1, z0), + (x2, y2, z1), + (x3, y3, z1), + ]) + + faces.extend([ + # top + (f, f + 1, f + 2, f + 3), + # sides + (f, f + 4, f + 5, f + 1), + (f + 1, f + 5, f + 6, f + 2), + (f + 2, f + 6, f + 7, f + 3), + (f + 3, f + 7, f + 4, f), + # bottom + (f + 4, f + 7, f + 6, f + 5) + ]) + edges.append([f, f + 3]) + edges.append([f + 1, f + 2]) + edges.append([f + 4, f + 7]) + edges.append([f + 5, f + 6]) + matids.extend([idmat, idmat, idmat, idmat, idmat, idmat]) + uvs.extend([ + [(0, 0), (0, 1), (1, 1), (1, 0)], + [(0, 0), (0, 1), (1, 1), (1, 0)], + [(0, 0), (0, 1), (1, 1), (1, 0)], + [(0, 0), (0, 1), (1, 1), (1, 0)], + [(0, 0), (0, 1), (1, 1), (1, 0)], + [(0, 0), (0, 1), (1, 1), (1, 0)] + ]) + + def gutter(self, d, verts, faces, edges, matids, uvs): + + ##################### + # Chenaux + ##################### + + idmat = 5 + + # caps at start and end + if d.gutter_segs % 2 == 1: + n_faces = int((d.gutter_segs - 1) / 2) + else: + n_faces = int((d.gutter_segs / 2) - 1) + + df = 2 * d.gutter_segs + 1 + + for pan in self.pans: + for i, s in enumerate(pan.segs): + + if s.type == 'BOTTOM': + f = len(verts) + + s0 = s.offset(d.gutter_dist + d.gutter_width) + + s1 = pan.last_seg(i) + s2 = pan.next_seg(i) + + p0 = s0.p0 + p1 = s0.p1 + + tri_0 = pan.node_tri or pan.is_tri + tri_1 = pan.next_tri or pan.is_tri + + if pan.side == 'LEFT': + tri_0, tri_1 = tri_1, tri_0 + + f = len(verts) + + # tiangular use segment direction + # find last neighboor depending on type + if s1.type == 'AXIS' or 'LINK' in s1.type: + # apply only on boundarys + if not s.is_hole: + # use last axis + if pan.side == 'LEFT': + s3 = pan.next_cross + else: + s3 = pan.last_cross + if tri_0: + s1 = s.copy + else: + s1 = s1.oposite + s1.v = (s.sized_normal(0, 1).v + s3.v).normalized() + elif s1.type == 'SIDE': + s1 = s.copy + s1.type = 'SIDE' + s1.v = s.sized_normal(0, 1).v + else: + s1 = s1.offset(d.gutter_dist + d.gutter_width) + + # find next neighboor depending on type + if s2.type == 'AXIS' or 'LINK' in s2.type: + if not s.is_hole: + # use last axis + if pan.side == 'LEFT': + s3 = pan.last_cross + else: + s3 = pan.next_cross + if tri_1: + s2 = s.oposite + else: + s2 = s2.copy + s2.v = (s.sized_normal(0, 1).v + s3.v).normalized() + elif s2.type == 'SIDE': + s2 = s.oposite + s2.type = 'SIDE' + s2.v = s.sized_normal(0, 1).v + else: + s2 = s2.offset(d.gutter_dist + d.gutter_width) + + # units vectors and scale + # is unit normal on sides + # print("s.p:%s, s.v:%s s1.p::%s s1.v::%s" % (s.p, s.v, s1.p, s1.v)) + res, p, t = s0.intersect(s1) + if res: + p0 = p + res, p, t = s0.intersect(s2) + if res: + p1 = p + """ + f = len(verts) + verts.extend([s1.p0.to_3d(), s1.p1.to_3d()]) + edges.append([f, f + 1]) + + f = len(verts) + verts.extend([s2.p0.to_3d(), s2.p1.to_3d()]) + edges.append([f, f + 1]) + continue + """ + + v0 = p0 - s.p0 + v1 = p1 - s.p1 + + scale_0 = v0.length / (d.gutter_dist + d.gutter_width) + scale_1 = v1.length / (d.gutter_dist + d.gutter_width) + + s3 = Line(s.p0, v0.normalized()) + s4 = Line(s.p1, v1.normalized()) + + zt = self.z + d.facia_altitude + pan.altitude(s3.p0) + z0 = self.z + d.gutter_alt + pan.altitude(s3.p0) + z1 = z0 - 0.5 * d.gutter_width + z2 = z1 - 0.5 * d.gutter_width + z3 = z1 - 0.5 * d.gutter_boudin + dz0 = z2 - z1 + dz1 = z3 - z1 + + tt = scale_0 * d.facia_width + t0 = scale_0 * d.gutter_dist + t1 = t0 + scale_0 * (0.5 * d.gutter_width) + t2 = t1 + scale_0 * (0.5 * d.gutter_width) + t3 = t2 + scale_0 * (0.5 * d.gutter_boudin) + + # bord tablette + xt, yt = s3.lerp(tt) + + # bord + x0, y0 = s3.lerp(t0) + # axe chenaux + x1, y1 = s3.lerp(t1) + # bord boudin interieur + x2, y2 = s3.lerp(t2) + # axe boudin + x3, y3 = s3.lerp(t3) + + dx = x0 - x1 + dy = y0 - y1 + + verts.append((xt, yt, zt)) + # chenaux + da = pi / d.gutter_segs + for i in range(d.gutter_segs): + sa = sin(i * da) + ca = cos(i * da) + verts.append((x1 + dx * ca, y1 + dy * ca, z1 + dz0 * sa)) + + dx = x2 - x3 + dy = y2 - y3 + + # boudin + da = -pi / (0.75 * d.gutter_segs) + for i in range(d.gutter_segs): + sa = sin(i * da) + ca = cos(i * da) + verts.append((x3 + dx * ca, y3 + dy * ca, z1 + dz1 * sa)) + + zt = self.z + d.facia_altitude + pan.altitude(s4.p0) + z0 = self.z + d.gutter_alt + pan.altitude(s4.p0) + z1 = z0 - 0.5 * d.gutter_width + z2 = z1 - 0.5 * d.gutter_width + z3 = z1 - 0.5 * d.gutter_boudin + dz0 = z2 - z1 + dz1 = z3 - z1 + tt = scale_1 * d.facia_width + t0 = scale_1 * d.gutter_dist + t1 = t0 + scale_1 * (0.5 * d.gutter_width) + t2 = t1 + scale_1 * (0.5 * d.gutter_width) + t3 = t2 + scale_1 * (0.5 * d.gutter_boudin) + + # bord tablette + xt, yt = s4.lerp(tt) + + # bord + x0, y0 = s4.lerp(t0) + # axe chenaux + x1, y1 = s4.lerp(t1) + # bord boudin interieur + x2, y2 = s4.lerp(t2) + # axe boudin + x3, y3 = s4.lerp(t3) + + dx = x0 - x1 + dy = y0 - y1 + + # tablette + verts.append((xt, yt, zt)) + faces.append((f + df, f, f + 1, f + df + 1)) + uvs.append([(0, 0), (1, 0), (1, 1), (0, 1)]) + matids.append(idmat) + + # chenaux + da = pi / d.gutter_segs + for i in range(d.gutter_segs): + sa = sin(i * da) + ca = cos(i * da) + verts.append((x1 + dx * ca, y1 + dy * ca, z1 + dz0 * sa)) + + dx = x2 - x3 + dy = y2 - y3 + + # boudin + da = -pi / (0.75 * d.gutter_segs) + for i in range(d.gutter_segs): + sa = sin(i * da) + ca = cos(i * da) + verts.append((x3 + dx * ca, y3 + dy * ca, z1 + dz1 * sa)) + + df = 2 * d.gutter_segs + 1 + + for i in range(1, 2 * d.gutter_segs): + j = i + f + faces.append((j, j + df, j + df + 1, j + 1)) + uvs.append([(0, 0), (1, 0), (1, 1), (0, 1)]) + matids.append(idmat) + + """ + segs = 6 + + n_faces = segs / 2 - 1 + + 0 6 + 1 5 + 2 4 + 3 + """ + # close start + if s1.type == 'SIDE': + + if d.gutter_segs % 2 == 0: + faces.append((f + n_faces + 3, f + n_faces + 1, f + n_faces + 2)) + uvs.append([(0, 0), (1, 0), (0.5, -0.5)]) + matids.append(idmat) + + for i in range(n_faces): + + j = i + f + 1 + k = f + d.gutter_segs - i + faces.append((j + 1, k, k + 1, j)) + uvs.append([(0, 0), (1, 0), (1, 1), (0, 1)]) + matids.append(idmat) + + # close end + if s2.type == 'SIDE': + + f += 2 * d.gutter_segs + 1 + + if d.gutter_segs % 2 == 0: + faces.append((f + n_faces + 1, f + n_faces + 3, f + n_faces + 2)) + uvs.append([(0, 0), (1, 0), (0.5, -0.5)]) + matids.append(idmat) + + for i in range(n_faces): + + j = i + f + 1 + k = f + d.gutter_segs - i + faces.append((j, k + 1, k, j + 1)) + uvs.append([(0, 0), (1, 0), (1, 1), (0, 1)]) + matids.append(idmat) + + def beam_primary(self, d, verts, faces, edges, matids, uvs): + + idmat = 3 + + for pan in self.pans: + for i, s in enumerate(pan.segs): + + if s.type == 'AXIS': + + #################### + # Poutre Faitiere + #################### + + """ + 1___________________2 left + 0|___________________|3 axis + |___________________| right + 5 4 + """ + f = len(verts) + + s2 = s.offset(-0.5 * d.beam_width) + + # offset from roof border + s0 = pan.last_seg(i) + s1 = pan.next_seg(i) + t0 = 0 + t1 = 1 + + s0_tri = pan.next_tri + s1_tri = pan.node_tri + + if pan.side == 'LEFT': + s0_tri, s1_tri = s1_tri, s0_tri + + if s0.type == 'SIDE': + s0 = s0.offset(d.beam_offset) + t0 = -d.beam_offset / s.length + + if s0_tri: + p0 = s2.p0 + t0 = 0 + else: + res, p0, t = s2.intersect(s0) + if not res: + continue + + if s1.type == 'SIDE': + s1 = s1.offset(d.beam_offset) + t1 = 1 + d.beam_offset / s.length + + if s1_tri: + t1 = 1 + p1 = s2.p1 + else: + res, p1, t = s2.intersect(s1) + if not res: + continue + + x0, y0 = p0 + x1, y1 = s.lerp(t0) + x2, y2 = p1 + x3, y3 = s.lerp(t1) + z0 = self.z + d.beam_alt + z1 = z0 - d.beam_height + z2 = self.z + d.beam_alt + z3 = z2 - d.beam_height + verts.extend([ + (x0, y0, z0), + (x1, y1, z0), + (x2, y2, z2), + (x3, y3, z2), + (x0, y0, z1), + (x1, y1, z1), + (x2, y2, z3), + (x3, y3, z3), + ]) + if s0_tri or s0.type == 'SIDE': + faces.append((f + 4, f + 5, f + 1, f)) + uvs.append([(0, 0), (1, 0), (1, 1), (0, 1)]) + matids.append(idmat) + if s1_tri or s1.type == 'SIDE': + faces.append((f + 2, f + 3, f + 7, f + 6)) + uvs.append([(0, 0), (1, 0), (1, 1), (0, 1)]) + matids.append(idmat) + + faces.extend([ + # internal side + # (f + 1, f + 5, f + 7, f + 3), + # external side + (f + 2, f + 6, f + 4, f), + # top + (f, f + 1, f + 3, f + 2), + # bottom + (f + 5, f + 4, f + 6, f + 7) + ]) + matids.extend([ + idmat, idmat, idmat + ]) + uvs.extend([ + [(0, 0), (0, 1), (1, 1), (1, 0)], + [(0, 0), (0, 1), (1, 1), (1, 0)], + [(0, 0), (0, 1), (1, 1), (1, 0)] + ]) + + def rafter(self, context, o, d): + + idmat = 4 + + # Rafters / Chevrons + start = max(0.001 + 0.5 * d.rafter_width, d.rafter_start) + + holes_offset = -d.rafter_width + + # build temp bmesh and bissect + for pan in self.pans: + tmin, tmax, ysize = pan.tmin, pan.tmax, pan.ysize + + # print("tmin:%s tmax:%s ysize:%s" % (tmin, tmax, ysize)) + + f = 0 + + verts = [] + faces = [] + matids = [] + uvs = [] + alt = d.rafter_alt + seg = pan.fake_axis + + t0 = tmin + (start - 0.5 * d.rafter_width) / seg.length + t1 = tmin + (start + 0.5 * d.rafter_width) / seg.length + + tx = start / seg.length + dt = d.rafter_spacing / seg.length + + n_items = max(1, round((tmax - tmin) / dt, 0)) + + dt = ((tmax - tmin) - 2 * tx) / n_items + + for j in range(int(n_items) + 1): + n0 = seg.sized_normal(t1 + j * dt, - ysize) + n1 = seg.sized_normal(t0 + j * dt, - ysize) + f = len(verts) + + z0 = self.z + alt + pan.altitude(n0.p0) + x0, y0 = n0.p0 + z1 = self.z + alt + pan.altitude(n0.p1) + x1, y1 = n0.p1 + z2 = self.z + alt + pan.altitude(n1.p0) + x2, y2 = n1.p0 + z3 = self.z + alt + pan.altitude(n1.p1) + x3, y3 = n1.p1 + + verts.extend([ + (x0, y0, z0), + (x1, y1, z1), + (x2, y2, z2), + (x3, y3, z3) + ]) + + faces.append((f + 1, f, f + 2, f + 3)) + matids.append(idmat) + uvs.append([(0, 0), (1, 0), (1, 1), (0, 1)]) + + bm = bmed.buildmesh( + context, o, verts, faces, matids=matids, uvs=uvs, + weld=False, clean=False, auto_smooth=True, temporary=True) + + self.cut_boundary(bm, pan) + self.cut_holes(bm, pan, offset={'DEFAULT': holes_offset}) + + bmesh.ops.dissolve_limit(bm, + angle_limit=0.01, + use_dissolve_boundaries=False, + verts=bm.verts, + edges=bm.edges, + delimit=1) + + geom = bm.faces[:] + verts = bm.verts[:] + bmesh.ops.solidify(bm, geom=geom, thickness=0.0001) + bmesh.ops.translate(bm, vec=Vector((0, 0, -d.rafter_height)), space=o.matrix_world, verts=verts) + # uvs for sides + uvs = [(0, 0), (1, 0), (1, 1), (0, 1)] + layer = bm.loops.layers.uv.verify() + for i, face in enumerate(bm.faces): + if len(face.loops) == 4: + for j, loop in enumerate(face.loops): + loop[layer].uv = uvs[j] + + # merge with object + bmed.bmesh_join(context, o, [bm], normal_update=True) + + bpy.ops.object.mode_set(mode='OBJECT') + + def hips(self, d, verts, faces, edges, matids, uvs): + + idmat_valley = 5 + idmat = 6 + idmat_poutre = 4 + + sx, sy, sz = d.hip_size_x, d.hip_size_y, d.hip_size_z + + if d.hip_model == 'ROUND': + + # round hips + t_pts = [Vector((sx * x, sy * y, sz * z)) for x, y, z in [ + (-0.5, 0.34, 0.08), (-0.5, 0.32, 0.19), (0.5, -0.4, -0.5), + (0.5, 0.4, -0.5), (-0.5, 0.26, 0.28), (-0.5, 0.16, 0.34), + (-0.5, 0.05, 0.37), (-0.5, -0.05, 0.37), (-0.5, -0.16, 0.34), + (-0.5, -0.26, 0.28), (-0.5, -0.32, 0.19), (-0.5, -0.34, 0.08), + (-0.5, -0.25, -0.5), (-0.5, 0.25, -0.5), (0.5, -0.08, 0.5), + (0.5, -0.5, 0.08), (0.5, -0.24, 0.47), (0.5, -0.38, 0.38), + (0.5, -0.47, 0.24), (0.5, 0.5, 0.08), (0.5, 0.08, 0.5), + (0.5, 0.47, 0.24), (0.5, 0.38, 0.38), (0.5, 0.24, 0.47) + ]] + t_faces = [ + (23, 22, 4, 5), (3, 19, 21, 22, 23, 20, 14, 16, 17, 18, 15, 2), (14, 20, 6, 7), + (18, 17, 9, 10), (15, 18, 10, 11), (21, 19, 0, 1), (17, 16, 8, 9), + (13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 1, 0), (19, 3, 13, 0), (20, 23, 5, 6), (22, 21, 1, 4), + (3, 2, 12, 13), (2, 15, 11, 12), (16, 14, 7, 8) + ] + t_uvs = [ + [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)], + [(0.5, 1.0), (0.75, 0.93), (0.93, 0.75), + (1.0, 0.5), (0.93, 0.25), (0.75, 0.07), + (0.5, 0.0), (0.25, 0.07), (0.07, 0.25), + (0.0, 0.5), (0.07, 0.75), (0.25, 0.93)], + [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)], + [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)], + [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)], + [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)], + [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)], + [(0.5, 1.0), (0.75, 0.93), (0.93, 0.75), + (1.0, 0.5), (0.93, 0.25), (0.75, 0.07), + (0.5, 0.0), (0.25, 0.07), (0.07, 0.25), + (0.0, 0.5), (0.07, 0.75), (0.25, 0.93)], + [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)], + [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)], + [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)], + [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)], + [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)], + [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)] + ] + # affect vertex with slope + t_left = [] + t_right = [] + + elif d.hip_model == 'ETERNIT': + + # square hips "eternit like" + t_pts = [Vector((sx * x, sy * y, sz * z)) for x, y, z in [ + (0.5, 0.5, 0.0), (-0.5, 0.5, -0.5), (0.5, -0.5, 0.0), + (-0.5, -0.5, -0.5), (0.5, 0.0, 0.0), (-0.5, -0.0, -0.5), + (0.5, 0.0, 0.5), (0.5, -0.5, 0.5), (-0.5, -0.5, 0.0), + (-0.5, -0.0, 0.0), (0.5, 0.5, 0.5), (-0.5, 0.5, 0.0)] + ] + t_faces = [ + (4, 2, 3, 5), (0, 4, 5, 1), (6, 9, 8, 7), + (10, 11, 9, 6), (0, 10, 6, 4), (5, 9, 11, 1), + (2, 7, 8, 3), (1, 11, 10, 0), (4, 6, 7, 2), (3, 8, 9, 5) + ] + t_uvs = [ + [(0.0, 0.5), (0.0, 1.0), (1.0, 1.0), (1.0, 0.5)], [(0.0, 0.0), (0.0, 0.5), (1.0, 0.5), (1.0, 0.0)], + [(0.0, 0.5), (1.0, 0.5), (1.0, 1.0), (0.0, 1.0)], [(0.0, 0.0), (1.0, 0.0), (1.0, 0.5), (0.0, 0.5)], + [(0.0, 0.5), (0.0, 1.0), (0.5, 1.0), (0.5, 0.5)], [(0.5, 0.5), (0.5, 1.0), (0.0, 1.0), (0.0, 0.5)], + [(0.0, 0.5), (0.0, 1.0), (1.0, 1.0), (1.0, 0.5)], [(0.0, 0.5), (0.0, 1.0), (-1.0, 1.0), (-1.0, 0.5)], + [(0.5, 0.5), (0.5, 1.0), (1.0, 1.0), (1.0, 0.5)], [(0.0, 0.5), (0.0, 1.0), (-0.5, 1.0), (-0.5, 0.5)] + ] + t_left = [2, 3, 7, 8] + t_right = [0, 1, 10, 11] + + elif d.hip_model == 'FLAT': + # square hips "eternit like" + t_pts = [Vector((sx * x, sy * y, sz * z)) for x, y, z in [ + (-0.5, -0.4, 0.0), (-0.5, -0.4, 0.5), (-0.5, 0.4, 0.0), + (-0.5, 0.4, 0.5), (0.5, -0.5, 0.5), (0.5, -0.5, 1.0), + (0.5, 0.5, 0.5), (0.5, 0.5, 1.0), (-0.5, 0.33, 0.0), + (-0.5, -0.33, 0.0), (0.5, -0.33, 0.5), (0.5, 0.33, 0.5), + (-0.5, 0.33, -0.5), (-0.5, -0.33, -0.5), (0.5, -0.33, -0.5), + (0.5, 0.33, -0.5)] + ] + t_faces = [ + (0, 1, 3, 2, 8, 9), (2, 3, 7, 6), (6, 7, 5, 4, 10, 11), + (4, 5, 1, 0), (9, 10, 4, 0), (7, 3, 1, 5), + (2, 6, 11, 8), (9, 8, 12, 13), (12, 15, 14, 13), + (8, 11, 15, 12), (10, 9, 13, 14), (11, 10, 14, 15)] + t_uvs = [ + [(0.5, 1.0), (0.93, 0.75), (0.93, 0.25), (0.5, 0.0), (0.07, 0.25), (0.07, 0.75)], + [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)], + [(0.5, 1.0), (0.93, 0.75), (0.93, 0.25), (0.5, 0.0), (0.07, 0.25), (0.07, 0.75)], + [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)], + [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)], + [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)], + [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)], + [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)], + [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)], + [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)], + [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)], + [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)] + ] + t_left = [] + t_right = [] + + t_idmats = [idmat for f in t_faces] + + for pan in self.pans: + for i, s in enumerate(pan.segs): + if ('LINK' in s.type and + d.beam_sec_enable): + ############## + # beam inside + ############## + f = len(verts) + + s0 = s.offset(-0.5 * d.beam_sec_width) + + s2 = pan.last_seg(i) + s3 = pan.next_seg(i) + + res, p0, t0 = s0.intersect(s2) + res, p1, t1 = s0.intersect(s3) + + p0 = s.lerp(t0) + p1 = s.lerp(t1) + + x0, y0 = s0.lerp(t0) + x1, y1 = s.p0 + + z0 = self.z + d.beam_sec_alt + pan.altitude(p0) + z1 = z0 - d.beam_sec_height + z2 = self.z + d.beam_sec_alt + pan.altitude(s.p0) + z3 = z2 - d.beam_sec_height + + verts.extend([ + (x0, y0, z0), + (x0, y0, z1), + (x1, y1, z2), + (x1, y1, z3) + ]) + + x2, y2 = s0.lerp(t1) + x3, y3 = s.p1 + + z0 = self.z + d.beam_sec_alt + pan.altitude(p1) + z1 = z0 - d.beam_sec_height + z2 = self.z + d.beam_sec_alt + pan.altitude(s.p1) + z3 = z2 - d.beam_sec_height + + verts.extend([ + (x2, y2, z0), + (x2, y2, z1), + (x3, y3, z2), + (x3, y3, z3) + ]) + + faces.extend([ + (f, f + 4, f + 5, f + 1), + (f + 1, f + 5, f + 7, f + 3), + (f + 2, f + 3, f + 7, f + 6), + (f + 2, f + 6, f + 4, f), + (f, f + 1, f + 3, f + 2), + (f + 5, f + 4, f + 6, f + 7) + ]) + matids.extend([ + idmat_poutre, idmat_poutre, idmat_poutre, + idmat_poutre, idmat_poutre, idmat_poutre + ]) + uvs.extend([ + [(0, 0), (1, 0), (1, 1), (0, 1)], + [(0, 0), (1, 0), (1, 1), (0, 1)], + [(0, 0), (1, 0), (1, 1), (0, 1)], + [(0, 0), (1, 0), (1, 1), (0, 1)], + [(0, 0), (1, 0), (1, 1), (0, 1)], + [(0, 0), (1, 0), (1, 1), (0, 1)] + ]) + + if s.type == 'LINK_HIP': + + # TODO: + # Slice borders properly + + if d.hip_enable: + + s0 = pan.last_seg(i) + s1 = pan.next_seg(i) + s2 = s + p0 = s0.p1 + p1 = s1.p0 + z0 = pan.altitude(p0) + z1 = pan.altitude(p1) + + # s0 is top seg + if z1 > z0: + p0, p1 = p1, p0 + z0, z1 = z1, z0 + s2 = s2.oposite + dz = pan.altitude(s2.sized_normal(0, 1).p1) - z0 + + if dz < 0: + s1 = s1.offset(d.tile_border) + # vx from p0 to p1 + x, y = p1 - p0 + v = Vector((x, y, z1 - z0)) + vx = v.normalized() + vy = vx.cross(Vector((0, 0, 1))) + vz = vy.cross(vx) + + x0, y0 = p0 + d.hip_alt * vz.to_2d() + z2 = z0 + self.z + d.hip_alt * vz.z + tM = Matrix([ + [vx.x, vy.x, vz.x, x0], + [vx.y, vy.y, vz.y, y0], + [vx.z, vy.z, vz.z, z2], + [0, 0, 0, 1] + ]) + space_x = v.length - d.tile_border + n_x = 1 + int(space_x / d.hip_space_x) + dx = space_x / n_x + x0 = 0.5 * dx + + t_verts = [p for p in t_pts] + + # apply slope + + for i in t_left: + t_verts[i] = t_verts[i].copy() + t_verts[i].z -= dz * t_verts[i].y + for i in t_right: + t_verts[i] = t_verts[i].copy() + t_verts[i].z += dz * t_verts[i].y + + for k in range(n_x): + lM = tM * Matrix([ + [1, 0, 0, x0 + k * dx], + [0, -1, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 1] + ]) + f = len(verts) + + verts.extend([lM * p for p in t_verts]) + faces.extend([tuple(i + f for i in p) for p in t_faces]) + matids.extend(t_idmats) + uvs.extend(t_uvs) + + elif s.type == 'LINK_VALLEY': + if d.valley_enable: + f = len(verts) + s0 = s.offset(-2 * d.tile_couloir) + s1 = pan.last_seg(i) + s2 = pan.next_seg(i) + res, p0, t = s0.intersect(s1) + res, p1, t = s0.intersect(s2) + alt = self.z + d.valley_altitude + x0, y0 = s1.p1 + x1, y1 = p0 + x2, y2 = p1 + x3, y3 = s2.p0 + z0 = alt + pan.altitude(s1.p1) + z1 = alt + pan.altitude(p0) + z2 = alt + pan.altitude(p1) + z3 = alt + pan.altitude(s2.p0) + + verts.extend([ + (x0, y0, z0), + (x1, y1, z1), + (x2, y2, z2), + (x3, y3, z3), + ]) + faces.extend([ + (f, f + 3, f + 2, f + 1) + ]) + matids.extend([ + idmat_valley + ]) + uvs.extend([ + [(0, 0), (1, 0), (1, 1), (0, 1)] + ]) + + elif s.type == 'AXIS' and d.hip_enable and pan.side == 'LEFT': + + tmin = 0 + tmax = 1 + s0 = pan.last_seg(i) + if s0.type == 'SIDE': + tmin = 0 - d.tile_side / s.length + s1 = pan.next_seg(i) + + if s1.type == 'SIDE': + tmax = 1 + d.tile_side / s.length + + # print("tmin:%s tmax:%s" % (tmin, tmax)) + #################### + # Faitiere + #################### + + f = len(verts) + s_len = (tmax - tmin) * s.length + n_obj = 1 + int(s_len / d.hip_space_x) + dx = s_len / n_obj + x0 = 0.5 * dx + v = s.v.normalized() + p0 = s.lerp(tmin) + tM = Matrix([ + [v.x, v.y, 0, p0.x], + [v.y, -v.x, 0, p0.y], + [0, 0, 1, self.z + d.hip_alt], + [0, 0, 0, 1] + ]) + t_verts = [p.copy() for p in t_pts] + + # apply slope + for i in t_left: + t_verts[i].z += t_verts[i].y * (pan.other_side.slope - d.tile_size_z / d.tile_size_y) + for i in t_right: + t_verts[i].z -= t_verts[i].y * (pan.slope - d.tile_size_z / d.tile_size_y) + + for k in range(n_obj): + lM = tM * Matrix([ + [1, 0, 0, x0 + k * dx], + [0, -1, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 1] + ]) + v = len(verts) + verts.extend([lM * p for p in t_verts]) + faces.extend([tuple(i + v for i in f) for f in t_faces]) + matids.extend(t_idmats) + uvs.extend(t_uvs) + + def make_hole(self, context, hole_obj, o, d, update_parent=False): + """ + Hole for t child on parent + create / update a RoofCutter on parent + assume context object is child roof + with parent set + """ + # print("Make hole :%s hole_obj:%s" % (o.name, hole_obj)) + if o.parent is None: + return + # root is a RoofSegment + root = self.nodes[0].root + r_pan = root.right + l_pan = root.left + + # merge : + # 5 ____________ 4 + # / | + # / left | + # /_____axis_____| 3 <- kill axis and this one + # 0\ | + # \ right | + # 1 \____________| 2 + # + # degenerate case: + # + # /| + # / | + # \ | + # \| + # + + segs = [] + last = len(r_pan.segs) - 1 + for i, seg in enumerate(r_pan.segs): + # r_pan start parent roof side + if i == last: + to_merge = seg.copy + elif seg.type != 'AXIS': + segs.append(seg.copy) + + for i, seg in enumerate(l_pan.segs): + # l_pan end parent roof side + if i == 1: + # 0 is axis + to_merge.p1 = seg.p1 + segs.append(to_merge) + elif seg.type != 'AXIS': + segs.append(seg.copy) + + # if there is side offset: + # create an arrow + # + # 4 s4 + # /| + # / |___s1_______ + # / p3 | p2 s3 + # 0\ p0___s0_______| p1 + # \ | + # 1 \| + s0 = root.left._axis.offset( + max(0.001, + min( + root.right.ysize - 0.001, + root.right.ysize - d.hole_offset_right + ) + )) + s1 = root.left._axis.offset( + -max(0.001, + min( + root.left.ysize - 0.001, + root.left.ysize - d.hole_offset_left + ) + )) + + s3 = segs[2].offset( + -min(root.left.xsize - 0.001, d.hole_offset_front) + ) + s4 = segs[0].copy + p1 = s4.p1 + s4.p1 = segs[-1].p0 + s4.p0 = p1 + res, p0, t = s4.intersect(s0) + res, p1, t = s0.intersect(s3) + res, p2, t = s1.intersect(s3) + res, p3, t = s4.intersect(s1) + pts = [] + # pts in cw order for 'DIFFERENCE' mode + pts.extend([segs[-1].p1, segs[-1].p0]) + if (segs[-1].p0 - p3).length > 0.001: + pts.append(p3) + pts.extend([p2, p1]) + if (segs[0].p1 - p0).length > 0.001: + pts.append(p0) + pts.extend([segs[0].p1, segs[0].p0]) + + pts = [p.to_3d() for p in pts] + + if hole_obj is None: + context.scene.objects.active = o.parent + bpy.ops.archipack.roof_cutter(parent=d.t_parent) + hole_obj = context.active_object + else: + context.scene.objects.active = hole_obj + + hole_obj.select = True + if d.parts[0].a0 < 0: + y = -d.t_dist_y + else: + y = d.t_dist_y + + hole_obj.matrix_world = o.matrix_world * Matrix([ + [1, 0, 0, 0], + [0, 1, 0, y], + [0, 0, 1, 0], + [0, 0, 0, 1] + ]) + + hd = archipack_roof_cutter.datablock(hole_obj) + hd.boundary = o.name + hd.update_points(context, hole_obj, pts, update_parent=update_parent) + hole_obj.select = False + + context.scene.objects.active = o + + def change_coordsys(self, fromTM, toTM): + """ + move shape fromTM into toTM coordsys + """ + dp = (toTM.inverted() * fromTM.translation).to_2d() + da = toTM.row[1].to_2d().angle_signed(fromTM.row[1].to_2d()) + ca = cos(da) + sa = sin(da) + rM = Matrix([ + [ca, -sa], + [sa, ca] + ]) + for s in self.segs: + tp = (rM * s.p0) - s.p0 + dp + s.rotate(da) + s.translate(tp) + + def t_partition(self, array, begin, end): + pivot = begin + for i in range(begin + 1, end + 1): + # wall idx + if array[i][0] < array[begin][0]: + pivot += 1 + array[i], array[pivot] = array[pivot], array[i] + array[pivot], array[begin] = array[begin], array[pivot] + return pivot + + def sort_t(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.t_partition(array, begin, end) + _quicksort(array, begin, pivot - 1) + _quicksort(array, pivot + 1, end) + return _quicksort(array, begin, end) + + def make_wall_fit(self, context, o, wall, inside): + wd = wall.data.archipack_wall2[0] + wg = wd.get_generator() + z0 = self.z - wd.z + + # wg in roof coordsys + wg.change_coordsys(wall.matrix_world, o.matrix_world) + + if inside: + # fit inside + offset = -0.5 * (1 - wd.x_offset) * wd.width + else: + # fit outside + offset = 0 + + wg.set_offset(offset) + + wall_t = [[] for w in wg.segs] + + for pan in self.pans: + # walls segment + for widx, wseg in enumerate(wg.segs): + + ls = wseg.line.length + + for seg in pan.segs: + # intersect with a roof segment + # any linked or axis intersection here + # will be dup as they are between 2 roof parts + res, p, t, v = wseg.line.intersect_ext(seg) + if res: + z = z0 + pan.altitude(p) + wall_t[widx].append((t, z, t * ls)) + + # lie under roof + if type(wseg).__name__ == "CurvedWall": + for step in range(12): + t = step / 12 + p = wseg.line.lerp(t) + if pan.inside(p): + z = z0 + pan.altitude(p) + wall_t[widx].append((t, z, t * ls)) + else: + if pan.inside(wseg.line.p0): + z = z0 + pan.altitude(wseg.line.p0) + wall_t[widx].append((0, z, 0)) + + old = context.active_object + old_sel = wall.select + wall.select = True + context.scene.objects.active = wall + + wd.auto_update = False + # setup splits count and first split to 0 + for widx, seg in enumerate(wall_t): + self.sort_t(seg) + # print("seg: %s" % seg) + for s in seg: + t, z, d = s + wd.parts[widx].n_splits = len(seg) + 1 + wd.parts[widx].z[0] = 0 + wd.parts[widx].t[0] = 0 + break + + # add splits, skip dups + for widx, seg in enumerate(wall_t): + t0 = 0 + last_d = -1 + id = 1 + for s in seg: + t, z, d = s + if t == 0: + # add at end of last segment + if widx > 0: + lid = wd.parts[widx - 1].n_splits - 1 + wd.parts[widx - 1].z[lid] = z + wd.parts[widx - 1].t[lid] = 1 + else: + wd.parts[widx].z[0] = z + wd.parts[widx].t[0] = t + id = 1 + else: + if d - last_d < 0.001: + wd.parts[widx].n_splits -= 1 + continue + wd.parts[widx].z[id] = z + wd.parts[widx].t[id] = t - t0 + t0 = t + id += 1 + last_d = d + + if wd.closed: + last = wd.parts[wd.n_parts].n_splits - 1 + wd.parts[wd.n_parts].z[last] = wd.parts[0].z[0] + wd.parts[wd.n_parts].t[last] = 1.0 + + wd.auto_update = True + """ + for s in self.segs: + s.as_curve(context) + for s in wg.segs: + s.as_curve(context) + """ + wall.select = old_sel + context.scene.objects.active = old + + def boundary(self, context, o): + """ + either external or holes cuts + """ + for b in o.children: + d = archipack_roof_cutter.datablock(b) + if d is not None: + g = d.ensure_direction() + g.change_coordsys(b.matrix_world, o.matrix_world) + for pan in self.pans: + pan.slice(g) + pan.limits() + + def draft(self, context, verts, edges): + for pan in self.pans: + pan.draw(context, self.z, verts, edges) + + for s in self.segs: + if s.constraint_type == 'SLOPE': + f = len(verts) + p0 = s.p0.to_3d() + p0.z = self.z + p1 = s.p1.to_3d() + p1.z = self.z + verts.extend([p0, p1]) + edges.append([f, f + 1]) + + +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_parent(self, context): + + # update part a0 + o = context.active_object + p, d = self.find_parent(context) + + if d is not None: + + o.parent = p + + # trigger object update + # hole creation and parent's update + + self.parts[0].a0 = pi / 2 + + elif self.t_parent != "": + self.t_parent = "" + + +def update_cutter(self, context): + self.update(context, update_hole=True) + + +def update_childs(self, context): + self.update(context, update_childs=True, update_hole=True) + + +def update_components(self, context): + self.update(context, update_parent=False, update_hole=False) + + +class ArchipackSegment(): + length = FloatProperty( + name="length", + min=0.01, + max=1000.0, + default=2.0, + update=update + ) + a0 = FloatProperty( + name="angle", + min=-2 * pi, + max=2 * pi, + default=0, + subtype='ANGLE', unit='ROTATION', + update=update_cutter + ) + manipulators = CollectionProperty(type=archipack_manipulator) + + +class ArchipackLines(): + n_parts = IntProperty( + name="parts", + min=1, + default=1, update=update_manipulators + ) + # UI layout related + parts_expand = BoolProperty( + default=False + ) + + def draw(self, layout, context): + box = layout.box() + row = box.row() + if self.parts_expand: + row.prop(self, 'parts_expand', icon="TRIA_DOWN", icon_only=True, text="Parts", emboss=False) + box.prop(self, 'n_parts') + for i, part in enumerate(self.parts): + part.draw(layout, context, i) + else: + row.prop(self, 'parts_expand', icon="TRIA_RIGHT", icon_only=True, text="Parts", emboss=False) + + def update_parts(self): + # print("update_parts") + # remove rows + # NOTE: + # n_parts+1 + # as last one is end point of last segment or closing one + for i in range(len(self.parts), self.n_parts + 1, -1): + self.parts.remove(i - 1) + + # add rows + for i in range(len(self.parts), self.n_parts + 1): + self.parts.add() + + self.setup_manipulators() + + def setup_parts_manipulators(self): + 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) + if n_manips < 5: + s = p.manipulators.add() + s.type_key = "SIZE" + s.prop1_name = "offset" + p.manipulators[2].prop1_name = str(i) + p.manipulators[3].prop1_name = str(i + 1) + + +class archipack_roof_segment(ArchipackSegment, PropertyGroup): + + bound_idx = IntProperty( + default=0, + min=0, + update=update_manipulators + ) + width_left = FloatProperty( + name="L Width", + min=0.01, + default=3.0, + update=update_cutter + ) + width_right = FloatProperty( + name="R Width", + min=0.01, + default=3.0, + update=update_cutter + ) + slope_left = FloatProperty( + name="L slope", + min=0.0, + default=0.3, + update=update_cutter + ) + slope_right = FloatProperty( + name="R slope", + min=0.0, + default=0.3, + update=update_cutter + ) + auto_left = EnumProperty( + description="Left mode", + name="Left", + items=( + ('AUTO', 'Auto', '', 0), + ('WIDTH', 'Width', '', 1), + ('SLOPE', 'Slope', '', 2), + ('ALL', 'All', '', 3), + ), + default="AUTO", + update=update_manipulators + ) + auto_right = EnumProperty( + description="Right mode", + name="Right", + items=( + ('AUTO', 'Auto', '', 0), + ('WIDTH', 'Width', '', 1), + ('SLOPE', 'Slope', '', 2), + ('ALL', 'All', '', 3), + ), + default="AUTO", + update=update_manipulators + ) + triangular_end = BoolProperty( + name="Tri end", + default=False, + update=update + ) + take_precedence = BoolProperty( + name="Take precedence", + description="On T segment take width precedence", + default=False, + update=update + ) + + constraint_type = EnumProperty( + items=( + ('HORIZONTAL', 'Horizontal', '', 0), + ('SLOPE', 'Slope', '', 1) + ), + default='HORIZONTAL', + update=update_manipulators + ) + + enforce_part = EnumProperty( + name="Enforce part", + items=( + ('AUTO', 'Auto', '', 0), + ('VALLEY', 'Valley', '', 1), + ('HIP', 'Hip', '', 2) + ), + default='AUTO', + 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: + d = archipack_roof.datablock(o) + if d: + for part in d.parts: + if part == self: + return d + return None + + def draw(self, layout, context, index): + box = layout.box() + if index > 0: + box.prop(self, "constraint_type", text=str(index + 1)) + if self.constraint_type == 'SLOPE': + box.prop(self, "enforce_part", text="") + else: + box.label("Part 1:") + box.prop(self, "length") + box.prop(self, "a0") + + if index > 0: + box.prop(self, 'bound_idx') + if self.constraint_type == 'HORIZONTAL': + box.prop(self, "triangular_end") + row = box.row(align=True) + row.prop(self, "auto_left", text="") + row.prop(self, "auto_right", text="") + if self.auto_left in {'ALL', 'WIDTH'}: + box.prop(self, "width_left") + if self.auto_left in {'ALL', 'SLOPE'}: + box.prop(self, "slope_left") + if self.auto_right in {'ALL', 'WIDTH'}: + box.prop(self, "width_right") + if self.auto_right in {'ALL', 'SLOPE'}: + box.prop(self, "slope_right") + + def update(self, context, manipulable_refresh=False, update_hole=False): + props = self.find_in_selection(context) + if props is not None: + props.update(context, + manipulable_refresh, + update_parent=True, + update_hole=True, + update_childs=True) + + +class archipack_roof(ArchipackLines, ArchipackObject, Manipulable, PropertyGroup): + parts = CollectionProperty(type=archipack_roof_segment) + z = FloatProperty( + name="z", + default=3, precision=2, step=1, + unit='LENGTH', subtype='DISTANCE', + update=update_childs + ) + slope_left = FloatProperty( + name="L slope", + default=0.5, precision=2, step=1, + # unit='LENGTH', subtype='DISTANCE', + update=update_childs + ) + slope_right = FloatProperty( + name="R slope", + default=0.5, precision=2, step=1, + # unit='LENGTH', subtype='DISTANCE', + update=update_childs + ) + width_left = FloatProperty( + name="L width", + default=3, precision=2, step=1, + unit='LENGTH', subtype='DISTANCE', + update=update_cutter + ) + width_right = FloatProperty( + name="R width", + default=3, precision=2, step=1, + unit='LENGTH', subtype='DISTANCE', + update=update_cutter + ) + draft = BoolProperty( + options={'SKIP_SAVE'}, + name="Draft mode", + default=False, + update=update_manipulators + ) + auto_update = BoolProperty( + options={'SKIP_SAVE'}, + default=True, + update=update_manipulators + ) + quick_edit = BoolProperty( + options={'SKIP_SAVE'}, + name="Quick Edit", + default=True + ) + force_update = BoolProperty( + options={'SKIP_SAVE'}, + name="Throttle", + default=True + ) + + tile_enable = BoolProperty( + name="Enable", + default=True, + update=update_components + ) + tile_solidify = BoolProperty( + name="Solidify", + default=True, + update=update_components + ) + tile_height = FloatProperty( + name="Height", + description="Amount for solidify", + min=0, + default=0.02, + unit='LENGTH', subtype='DISTANCE', + update=update_components + ) + tile_bevel = BoolProperty( + name="Bevel", + default=False, + update=update_components + ) + tile_bevel_amt = FloatProperty( + name="Amount", + description="Amount for bevel", + min=0, + default=0.02, + unit='LENGTH', subtype='DISTANCE', + update=update_components + ) + tile_bevel_segs = IntProperty( + name="Segs", + description="Bevel Segs", + min=1, + default=2, + update=update_components + ) + tile_alternate = BoolProperty( + name="Alternate", + default=False, + update=update_components + ) + tile_offset = FloatProperty( + name="Offset", + description="Offset from start", + min=0, + max=100, + subtype="PERCENTAGE", + update=update_components + ) + tile_altitude = FloatProperty( + name="Altitude", + description="Altitude from roof", + default=0.1, + unit='LENGTH', subtype='DISTANCE', + update=update_components + ) + tile_size_x = FloatProperty( + name="x", + description="Size of tiles on x axis", + min=0.01, + default=0.2, + unit='LENGTH', subtype='DISTANCE', + update=update_components + ) + tile_size_y = FloatProperty( + name="y", + description="Size of tiles on y axis", + min=0.01, + default=0.3, + unit='LENGTH', subtype='DISTANCE', + update=update_components + ) + tile_size_z = FloatProperty( + name="z", + description="Size of tiles on z axis", + min=0.0, + default=0.02, + unit='LENGTH', subtype='DISTANCE', + update=update_components + ) + tile_space_x = FloatProperty( + name="x", + description="Space between tiles on x axis", + min=0.01, + default=0.2, + unit='LENGTH', subtype='DISTANCE', + update=update_components + ) + tile_space_y = FloatProperty( + name="y", + description="Space between tiles on y axis", + min=0.01, + default=0.3, + unit='LENGTH', subtype='DISTANCE', + update=update_components + ) + tile_fit_x = BoolProperty( + name="Fit x", + description="Fit roof on x axis", + default=True, + update=update_components + ) + tile_fit_y = BoolProperty( + name="Fit y", + description="Fit roof on y axis", + default=True, + update=update_components + ) + tile_expand = BoolProperty( + options={'SKIP_SAVE'}, + name="Tiles", + description="Expand tiles panel", + default=False + ) + tile_model = EnumProperty( + name="model", + items=( + ('BRAAS1', 'Braas 1', '', 0), + ('BRAAS2', 'Braas 2', '', 1), + ('ETERNIT', 'Eternit', '', 2), + ('LAUZE', 'Lauze', '', 3), + ('ROMAN', 'Roman', '', 4), + ('ROUND', 'Round', '', 5), + ('PLACEHOLDER', 'Square', '', 6), + ('ONDULEE', 'Ondule', '', 7), + ('METAL', 'Metal', '', 8), + # ('USER', 'User defined', '', 7) + ), + default="BRAAS2", + update=update_components + ) + tile_side = FloatProperty( + name="Side", + description="Space on side", + default=0, + unit='LENGTH', subtype='DISTANCE', + update=update_components + ) + tile_couloir = FloatProperty( + name="Valley", + description="Space between tiles on valley", + min=0, + default=0.05, + unit='LENGTH', subtype='DISTANCE', + update=update_components + ) + tile_border = FloatProperty( + name="Bottom", + description="Tiles offset from bottom", + default=0, + unit='LENGTH', subtype='DISTANCE', + update=update_components + ) + + gutter_expand = BoolProperty( + options={'SKIP_SAVE'}, + name="Gutter", + description="Expand gutter panel", + default=False + ) + gutter_enable = BoolProperty( + name="Enable", + default=True, + update=update_components + ) + gutter_alt = FloatProperty( + name="Altitude", + description="altitude", + default=0, + unit='LENGTH', subtype='DISTANCE', + update=update_components + ) + gutter_width = FloatProperty( + name="Width", + description="Width", + min=0.01, + default=0.15, + unit='LENGTH', subtype='DISTANCE', + update=update_components + ) + gutter_dist = FloatProperty( + name="Spacing", + description="Spacing", + min=0, + default=0.05, + unit='LENGTH', subtype='DISTANCE', + update=update_components + ) + gutter_boudin = FloatProperty( + name="Small width", + description="Small width", + min=0, + default=0.015, + unit='LENGTH', subtype='DISTANCE', + update=update_components + ) + gutter_segs = IntProperty( + default=6, + min=1, + name="Segs", + update=update_components + ) + + beam_expand = BoolProperty( + options={'SKIP_SAVE'}, + name="Beam", + description="Expand beam panel", + default=False + ) + beam_enable = BoolProperty( + name="Primary", + default=True, + update=update_components + ) + beam_width = FloatProperty( + name="Width", + description="Width", + min=0.01, + default=0.2, + unit='LENGTH', subtype='DISTANCE', + update=update_components + ) + beam_height = FloatProperty( + name="Height", + description="Height", + min=0.01, + default=0.35, + unit='LENGTH', subtype='DISTANCE', + update=update_components + ) + beam_offset = FloatProperty( + name="Offset", + description="Distance from roof border", + default=0.02, + unit='LENGTH', subtype='DISTANCE', + update=update_components + ) + beam_alt = FloatProperty( + name="Altitude", + description="Altitude from roof", + default=-0.15, + unit='LENGTH', subtype='DISTANCE', + update=update_components + ) + beam_sec_enable = BoolProperty( + name="Hip rafter", + default=True, + update=update_components + ) + beam_sec_width = FloatProperty( + name="Width", + description="Width", + min=0.01, + default=0.15, + unit='LENGTH', subtype='DISTANCE', + update=update_components + ) + beam_sec_height = FloatProperty( + name="Height", + description="Height", + min=0.01, + default=0.2, + unit='LENGTH', subtype='DISTANCE', + update=update_components + ) + beam_sec_alt = FloatProperty( + name="Altitude", + description="Distance from roof", + default=-0.1, + unit='LENGTH', subtype='DISTANCE', + update=update_components + ) + rafter_enable = BoolProperty( + name="Rafter", + default=True, + update=update_components + ) + rafter_width = FloatProperty( + name="Width", + description="Width", + min=0.01, + default=0.1, + unit='LENGTH', subtype='DISTANCE', + update=update_components + ) + rafter_height = FloatProperty( + name="Height", + description="Height", + min=0.01, + default=0.2, + unit='LENGTH', subtype='DISTANCE', + update=update_components + ) + rafter_spacing = FloatProperty( + name="Spacing", + description="Spacing", + min=0.1, + default=0.7, + unit='LENGTH', subtype='DISTANCE', + update=update_components + ) + rafter_start = FloatProperty( + name="Offset", + description="Spacing from roof border", + min=0, + default=0.1, + unit='LENGTH', subtype='DISTANCE', + update=update_components + ) + rafter_alt = FloatProperty( + name="Altitude", + description="Altitude from roof", + max=-0.0001, + default=-0.001, + unit='LENGTH', subtype='DISTANCE', + update=update_components + ) + + hip_enable = BoolProperty( + name="Enable", + default=True, + update=update_components + ) + hip_expand = BoolProperty( + options={'SKIP_SAVE'}, + name="Hips", + description="Expand hips panel", + default=False + ) + hip_alt = FloatProperty( + name="Altitude", + description="Hip altitude from roof", + default=0.1, + unit='LENGTH', subtype='DISTANCE', + update=update_components + ) + hip_space_x = FloatProperty( + name="Spacing", + description="Space between hips", + min=0.01, + default=0.4, + unit='LENGTH', subtype='DISTANCE', + update=update_components + ) + hip_size_x = FloatProperty( + name="l", + description="Length of hip", + min=0.01, + default=0.4, + unit='LENGTH', subtype='DISTANCE', + update=update_components + ) + hip_size_y = FloatProperty( + name="w", + description="Width of hip", + min=0.01, + default=0.15, + unit='LENGTH', subtype='DISTANCE', + update=update_components + ) + hip_size_z = FloatProperty( + name="h", + description="Height of hip", + min=0.0, + default=0.15, + unit='LENGTH', subtype='DISTANCE', + update=update_components + ) + hip_model = EnumProperty( + name="model", + items=( + ('ROUND', 'Round', '', 0), + ('ETERNIT', 'Eternit', '', 1), + ('FLAT', 'Flat', '', 2) + ), + default="ROUND", + update=update_components + ) + valley_altitude = FloatProperty( + name="Altitude", + description="Valley altitude from roof", + default=0.1, + unit='LENGTH', subtype='DISTANCE', + update=update_components + ) + valley_enable = BoolProperty( + name="Valley", + default=True, + update=update_components + ) + + facia_enable = BoolProperty( + name="Enable", + description="Enable Facia", + default=True, + update=update_components + ) + facia_expand = BoolProperty( + options={'SKIP_SAVE'}, + name="Facia", + description="Expand facia panel", + default=False + ) + facia_height = FloatProperty( + name="Height", + description="Height", + min=0.01, + default=0.3, + unit='LENGTH', subtype='DISTANCE', + update=update_components + ) + facia_width = FloatProperty( + name="Width", + description="Width", + min=0.01, + default=0.02, + unit='LENGTH', subtype='DISTANCE', + update=update_components + ) + facia_offset = FloatProperty( + name="Offset", + description="Offset from roof border", + default=0, + unit='LENGTH', subtype='DISTANCE', + update=update_components + ) + facia_altitude = FloatProperty( + name="Altitude", + description="Facia altitude from roof", + default=0.1, + unit='LENGTH', subtype='DISTANCE', + update=update_components + ) + + rake_enable = BoolProperty( + name="Enable", + description="Enable Rake", + default=True, + update=update_components + ) + rake_expand = BoolProperty( + options={'SKIP_SAVE'}, + name="Rake", + description="Expand rake panel", + default=False + ) + rake_height = FloatProperty( + name="Height", + description="Height", + min=0.01, + default=0.3, + unit='LENGTH', subtype='DISTANCE', + update=update_components + ) + rake_width = FloatProperty( + name="Width", + description="Width", + min=0.01, + default=0.02, + unit='LENGTH', subtype='DISTANCE', + update=update_components + ) + rake_offset = FloatProperty( + name="Offset", + description="Offset from roof border", + default=0.001, + unit='LENGTH', subtype='DISTANCE', + update=update_components + ) + rake_altitude = FloatProperty( + name="Altitude", + description="Facia altitude from roof", + default=0.1, + unit='LENGTH', subtype='DISTANCE', + update=update_components + ) + + t_parent = StringProperty( + name="Parent", + default="", + update=update_parent + ) + t_part = IntProperty( + name="Part", + description="Parent part index", + default=0, + min=0, + update=update_cutter + ) + t_dist_x = FloatProperty( + name="Dist x", + description="Location on axis ", + default=0, + update=update_cutter + ) + t_dist_y = FloatProperty( + name="Dist y", + description="Lateral distance from axis", + min=0.0001, + default=0.0001, + update=update_cutter + ) + + hole_offset_left = FloatProperty( + name="Left", + description="Left distance from border", + min=0, + default=0, + update=update_cutter + ) + hole_offset_right = FloatProperty( + name="Right", + description="Right distance from border", + min=0, + default=0, + update=update_cutter + ) + hole_offset_front = FloatProperty( + name="Front", + description="Front distance from border", + default=0, + update=update_cutter + ) + + def make_wall_fit(self, context, o, wall, inside=False): + origin = Vector((0, 0, self.z)) + g = self.get_generator(origin) + g.make_roof(context) + g.make_wall_fit(context, o, wall, inside) + + def update_parts(self): + # NOTE: + # n_parts+1 + # as last one is end point of last segment or closing one + for i in range(len(self.parts), self.n_parts, -1): + self.parts.remove(i - 1) + + # add rows + for i in range(len(self.parts), self.n_parts): + bound_idx = len(self.parts) + self.parts.add() + self.parts[-1].bound_idx = bound_idx + + self.setup_manipulators() + + def setup_manipulators(self): + if len(self.manipulators) < 1: + s = self.manipulators.add() + s.type_key = "SIZE" + s.prop1_name = "z" + s.normal = (0, 1, 0) + if len(self.manipulators) < 2: + s = self.manipulators.add() + s.type_key = "SIZE" + s.prop1_name = "width_left" + if len(self.manipulators) < 3: + s = self.manipulators.add() + s.type_key = "SIZE" + s.prop1_name = "width_right" + + 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 = 'DUMB_STRING' + s.prop1_name = str(i + 1) + p.manipulators[2].prop1_name = str(i + 1) + if n_manips < 4: + s = p.manipulators.add() + s.type_key = 'SIZE' + s.prop1_name = "width_left" + if n_manips < 5: + s = p.manipulators.add() + s.type_key = 'SIZE' + s.prop1_name = "width_right" + if n_manips < 6: + s = p.manipulators.add() + s.type_key = 'SIZE' + s.prop1_name = "slope_left" + if n_manips < 7: + s = p.manipulators.add() + s.type_key = 'SIZE' + s.prop1_name = "slope_right" + + def get_generator(self, origin=Vector((0, 0, 0))): + g = RoofGenerator(self, origin) + + # TODO: sort part by bound idx so deps always find parent + + for i, part in enumerate(self.parts): + # skip part if bound_idx > parent + # so deps always see parent + if part.bound_idx <= i: + g.add_part(part) + g.locate_manipulators() + return g + + def make_surface(self, o, verts, edges): + bm = bmesh.new() + 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]])) + bm.edges.ensure_lookup_table() + # bmesh.ops.contextual_create(bm, geom=bm.edges) + bm.to_mesh(o.data) + bm.free() + + def find_parent(self, context): + o = context.scene.objects.get(self.t_parent) + return o, archipack_roof.datablock(o) + + def intersection_angle(self, t_slope, t_width, p_slope, angle): + # 2d intersection angle between two roofs parts + dy = abs(t_slope * t_width / p_slope) + ca = cos(angle) + ta = tan(angle) + if ta == 0: + w0 = 0 + else: + w0 = dy * ta + if ca == 0: + w1 = 0 + else: + w1 = t_width / ca + dx = w1 - w0 + return atan2(dy, dx) + + def relocate_child(self, context, o, g, child): + + d = archipack_roof.datablock(child) + + if d is not None and d.t_part - 1 < len(g.segs): + # print("relocate_child(%s)" % (child.name)) + + seg = g.segs[d.t_part] + # adjust T part matrix_world from parent + # T part origin located on parent axis + # with y in parent direction + t = (d.t_dist_x / seg.length) + x, y, z = seg.lerp(t).to_3d() + dy = -seg.v.normalized() + child.matrix_world = o.matrix_world * Matrix([ + [dy.x, -dy.y, 0, x], + [dy.y, dy.x, 0, y], + [0, 0, 1, z], + [0, 0, 0, 1] + ]) + + def relocate_childs(self, context, o, g): + for child in o.children: + d = archipack_roof.datablock(child) + if d is not None and d.t_parent == o.name: + self.relocate_child(context, o, g, child) + + def update_childs(self, context, o, g): + for child in o.children: + d = archipack_roof.datablock(child) + if d is not None and d.t_parent == o.name: + # print("upate_childs(%s)" % (child.name)) + child.select = True + context.scene.objects.active = child + # regenerate hole + d.update(context, update_hole=True, update_parent=False) + child.select = False + o.select = True + context.scene.objects.active = o + + def update(self, + context, + manipulable_refresh=False, + update_childs=False, + update_parent=True, + update_hole=False, + force_update=False): + """ + update_hole: on t_child must update parent + update_childs: force childs update + force_update: skip throttle + """ + # print("update") + 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, edges, faces, matids, uvs = [], [], [], [], [] + + y = 0 + z = self.z + p, d = self.find_parent(context) + g = None + + # t childs: use parent to relocate + # setup slopes into generator + if d is not None: + pg = d.get_generator() + pg.make_roof(context) + + if self.t_part - 1 < len(pg.segs): + + seg = pg.nodes[self.t_part].root + + d.relocate_child(context, p, pg, o) + + a0 = self.parts[0].a0 + a_axis = a0 - pi / 2 + a_offset = 0 + s_left = self.slope_left + w_left = -self.width_left + s_right = self.slope_right + w_right = self.width_right + if a0 > 0: + # a_axis est mesure depuis la perpendiculaire à l'axe + slope = seg.right.slope + y = self.t_dist_y + else: + a_offset = pi + slope = seg.left.slope + y = -self.t_dist_y + s_left, s_right = s_right, s_left + w_left, w_right = -w_right, -w_left + + if slope == 0: + slope = 0.0001 + + # print("slope: %s" % (slope)) + + z = d.z - self.t_dist_y * slope + + # a_right from axis cross z + + b_right = self.intersection_angle( + s_left, + w_left, + slope, + a_axis) + + a_right = b_right + a_offset + + b_left = self.intersection_angle( + s_right, + w_right, + slope, + a_axis) + + a_left = b_left + a_offset + + g = self.get_generator(origin=Vector((0, y, z))) + + # override by user defined slope if any + make_right = True + make_left = True + for s in g.segs: + if (s.constraint_type == 'SLOPE' and + s.v0_idx == 0): + da = g.segs[0].v.angle_signed(s.v) + if da > 0: + make_left = False + else: + make_right = False + + if make_left: + # Add 'SLOPE' constraints for segment 0 + v = Vector((cos(a_left), sin(a_left))) + s = StraightRoof(g.origin, v) + s.v0_idx = 0 + s.constraint_type = 'SLOPE' + # s.enforce_part = 'VALLEY' + s.angle_0 = a_left + s.take_precedence = False + g.segs.append(s) + + if make_right: + v = Vector((cos(a_right), sin(a_right))) + s = StraightRoof(g.origin, v) + s.v0_idx = 0 + s.constraint_type = 'SLOPE' + # s.enforce_part = 'VALLEY' + s.angle_0 = a_right + s.take_precedence = False + g.segs.append(s) + + if g is None: + g = self.get_generator(origin=Vector((0, y, z))) + + # setup per segment manipulators + if len(g.segs) > 0: + f = g.segs[0] + # z + n = f.straight(-1, 0).v.to_3d() + self.manipulators[0].set_pts([(0, 0, 0), (0, 0, self.z), (1, 0, 0)], normal=n) + # left width + n = f.sized_normal(0, -self.width_left) + self.manipulators[1].set_pts([n.p0.to_3d(), n.p1.to_3d(), (-1, 0, 0)]) + # right width + n = f.sized_normal(0, self.width_right) + self.manipulators[2].set_pts([n.p0.to_3d(), n.p1.to_3d(), (1, 0, 0)]) + + g.make_roof(context) + + # update childs here so parent may use + # new holes when parent shape does change + if update_childs: + self.update_childs(context, o, g) + + # on t_child + if d is not None and update_hole: + hole_obj = self.find_hole(context, o) + g.make_hole(context, hole_obj, o, self, update_parent) + # print("make_hole") + + # add cutters + g.boundary(context, o) + + if self.draft: + + g.draft(context, verts, edges) + g.gutter(self, verts, faces, edges, matids, uvs) + self.make_surface(o, verts, edges) + + else: + + if self.rake_enable: + g.rake(self, verts, faces, edges, matids, uvs) + + if self.facia_enable: + g.facia(self, verts, faces, edges, matids, uvs) + + if self.beam_enable: + g.beam_primary(self, verts, faces, edges, matids, uvs) + + g.hips(self, verts, faces, edges, matids, uvs) + + if self.gutter_enable: + g.gutter(self, verts, faces, edges, matids, uvs) + + bmed.buildmesh( + + context, o, verts, faces, matids=matids, uvs=uvs, + weld=False, clean=False, auto_smooth=True, temporary=False) + + # bpy.ops.object.mode_set(mode='EDIT') + g.lambris(context, o, self) + # print("lambris") + + if self.rafter_enable: + # bpy.ops.object.mode_set(mode='EDIT') + g.rafter(context, o, self) + # print("rafter") + + if self.quick_edit and not force_update: + if self.tile_enable: + bpy.ops.archipack.roof_throttle_update(name=o.name) + else: + # throttle here + if self.tile_enable: + g.couverture(context, o, self) + # print("couverture") + + # enable manipulators rebuild + if manipulable_refresh: + self.manipulable_refresh = True + # print("rafter") + # restore context + self.restore_context(context) + # print("restore context") + + def find_hole(self, context, o): + p, d = self.find_parent(context) + if d is not None: + for child in p.children: + cd = archipack_roof_cutter.datablock(child) + if cd is not None and cd.boundary == o.name: + return child + return None + + 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 > 0: + # start angle + self.manip_stack.append(part.manipulators[0].setup(context, o, part)) + + if part.constraint_type == 'HORIZONTAL': + # length / radius + angle + self.manip_stack.append(part.manipulators[1].setup(context, o, part)) + + # index + self.manip_stack.append(part.manipulators[2].setup(context, o, self)) + + # size left + if part.auto_left in {'WIDTH', 'ALL'}: + self.manip_stack.append(part.manipulators[3].setup(context, o, part)) + # size right + if part.auto_right in {'WIDTH', 'ALL'}: + self.manip_stack.append(part.manipulators[4].setup(context, o, part)) + # slope left + if part.auto_left in {'SLOPE', 'ALL'}: + self.manip_stack.append(part.manipulators[5].setup(context, o, part)) + # slope right + if part.auto_right in {'SLOPE', 'ALL'}: + self.manip_stack.append(part.manipulators[6].setup(context, o, part)) + + for m in self.manipulators: + self.manip_stack.append(m.setup(context, o, self)) + + def draw(self, layout, context): + box = layout.box() + row = box.row() + if self.parts_expand: + row.prop(self, 'parts_expand', icon="TRIA_DOWN", icon_only=True, text="Parts", emboss=False) + box.prop(self, 'n_parts') + # box.prop(self, 'closed') + for i, part in enumerate(self.parts): + part.draw(layout, context, i) + else: + row.prop(self, 'parts_expand', icon="TRIA_RIGHT", icon_only=True, text="Parts", emboss=False) + + +def update_hole(self, context): + # update parent's roof only when manipulated + self.update(context, update_parent=True) + + +def update_operation(self, context): + self.reverse(context, make_ccw=(self.operation == 'INTERSECTION')) + + +class archipack_roof_cutter_segment(ArchipackCutterPart, PropertyGroup): + manipulators = CollectionProperty(type=archipack_manipulator) + type = EnumProperty( + name="Type", + items=( + ('SIDE', 'Side', 'Side with rake', 0), + ('BOTTOM', 'Bottom', 'Bottom with gutter', 1), + ('LINK', 'Side link', 'Side witout decoration', 2), + ('AXIS', 'Top', 'Top part with hip and beam', 3) + # ('LINK_VALLEY', 'Side valley', 'Side with valley', 3), + # ('LINK_HIP', 'Side hip', 'Side with hip', 4) + ), + default='SIDE', + update=update_hole + ) + + def find_in_selection(self, context): + selected = [o for o in context.selected_objects] + for o in selected: + d = archipack_roof_cutter.datablock(o) + if d: + for part in d.parts: + if part == self: + return d + return None + + +class archipack_roof_cutter(ArchipackCutter, ArchipackObject, Manipulable, PropertyGroup): + # boundary + parts = CollectionProperty(type=archipack_roof_cutter_segment) + boundary = StringProperty( + default="", + name="Boundary", + description="Boundary of t child to cut parent" + ) + + def update_points(self, context, o, pts, update_parent=False): + """ + Create boundary from roof + """ + self.auto_update = False + self.from_points(pts) + self.auto_update = True + if update_parent: + self.update_parent(context, o) + + def update_parent(self, context, o): + + d = archipack_roof.datablock(o.parent) + if d is not None: + o.parent.select = True + context.scene.objects.active = o.parent + d.update(context, update_childs=False, update_hole=False) + o.parent.select = False + context.scene.objects.active = o + + +class ARCHIPACK_PT_roof_cutter(Panel): + bl_idname = "ARCHIPACK_PT_roof_cutter" + bl_label = "Roof Cutter" + bl_space_type = 'VIEW_3D' + bl_region_type = 'UI' + bl_category = 'ArchiPack' + + @classmethod + def poll(cls, context): + return archipack_roof_cutter.filter(context.active_object) + + def draw(self, context): + prop = archipack_roof_cutter.datablock(context.active_object) + if prop is None: + return + layout = self.layout + scene = context.scene + box = layout.box() + box.operator('archipack.roof_cutter_manipulate', icon='HAND') + box.prop(prop, 'operation', text="") + box = layout.box() + box.label(text="From curve") + box.prop_search(prop, "user_defined_path", scene, "objects", text="", icon='OUTLINER_OB_CURVE') + if prop.user_defined_path != "": + box.prop(prop, 'user_defined_resolution') + # box.prop(prop, 'x_offset') + # box.prop(prop, 'angle_limit') + """ + box.prop_search(prop, "boundary", scene, "objects", text="", icon='OUTLINER_OB_CURVE') + """ + prop.draw(layout, context) + + +class ARCHIPACK_PT_roof(Panel): + bl_idname = "ARCHIPACK_PT_roof" + bl_label = "Roof" + bl_space_type = 'VIEW_3D' + bl_region_type = 'UI' + bl_category = 'ArchiPack' + + @classmethod + def poll(cls, context): + return archipack_roof.filter(context.active_object) + + def draw(self, context): + o = context.active_object + prop = archipack_roof.datablock(o) + if prop is None: + return + scene = context.scene + layout = self.layout + row = layout.row(align=True) + row.operator('archipack.roof_manipulate', icon='HAND') + + box = layout.box() + row = box.row(align=True) + row.operator("archipack.roof_preset_menu", text=bpy.types.ARCHIPACK_OT_roof_preset_menu.bl_label) + row.operator("archipack.roof_preset", text="", icon='ZOOMIN') + row.operator("archipack.roof_preset", text="", icon='ZOOMOUT').remove_active = True + box = layout.box() + box.prop_search(prop, "t_parent", scene, "objects", text="Parent", icon='OBJECT_DATA') + layout.operator('archipack.roof_cutter').parent = o.name + p, d = prop.find_parent(context) + if d is not None: + box.prop(prop, 't_part') + box.prop(prop, 't_dist_x') + box.prop(prop, 't_dist_y') + box.label(text="Hole") + box.prop(prop, 'hole_offset_front') + box.prop(prop, 'hole_offset_left') + box.prop(prop, 'hole_offset_right') + box = layout.box() + box.prop(prop, 'quick_edit', icon="MOD_MULTIRES") + box.prop(prop, 'draft') + if d is None: + box.prop(prop, 'z') + box.prop(prop, 'slope_left') + box.prop(prop, 'slope_right') + box.prop(prop, 'width_left') + box.prop(prop, 'width_right') + # parts + prop.draw(layout, context) + # tiles + box = layout.box() + row = box.row(align=True) + if prop.tile_expand: + row.prop(prop, 'tile_expand', icon="TRIA_DOWN", text="Tiles", icon_only=True, emboss=False) + else: + row.prop(prop, 'tile_expand', icon="TRIA_RIGHT", text="Tiles", icon_only=True, emboss=False) + row.prop(prop, 'tile_enable') + if prop.tile_expand: + box.prop(prop, 'tile_model', text="") + + box.prop(prop, 'tile_solidify', icon='MOD_SOLIDIFY') + if prop.tile_solidify: + box.prop(prop, 'tile_height') + box.separator() + box.prop(prop, 'tile_bevel', icon='MOD_BEVEL') + if prop.tile_bevel: + box.prop(prop, 'tile_bevel_amt') + box.prop(prop, 'tile_bevel_segs') + box.separator() + box.label(text="Tile size") + row = box.row(align=True) + row.prop(prop, 'tile_size_x') + row.prop(prop, 'tile_size_y') + row.prop(prop, 'tile_size_z') + box.prop(prop, 'tile_altitude') + + box.separator() + box.label(text="Distribution") + box.prop(prop, 'tile_alternate', icon='NLA') + row = box.row(align=True) + row.prop(prop, 'tile_fit_x', icon='ALIGN') + row.prop(prop, 'tile_fit_y', icon='ALIGN') + box.prop(prop, 'tile_offset') + + box.label(text="Spacing") + row = box.row(align=True) + row.prop(prop, 'tile_space_x') + row.prop(prop, 'tile_space_y') + + box.separator() # hip + box.label(text="Borders") + box.prop(prop, 'tile_side') + box.prop(prop, 'tile_couloir') + box.prop(prop, 'tile_border') + + box = layout.box() + row = box.row(align=True) + if prop.hip_expand: + row.prop(prop, 'hip_expand', icon="TRIA_DOWN", text="Hip", icon_only=True, emboss=False) + else: + row.prop(prop, 'hip_expand', icon="TRIA_RIGHT", text="Hip", icon_only=True, emboss=False) + row.prop(prop, 'hip_enable') + if prop.hip_expand: + box.prop(prop, 'hip_model', text="") + + box.label(text="Hip size") + row = box.row(align=True) + row.prop(prop, 'hip_size_x') + row.prop(prop, 'hip_size_y') + row.prop(prop, 'hip_size_z') + box.prop(prop, 'hip_alt') + box.prop(prop, 'hip_space_x') + box.separator() + box.prop(prop, 'valley_enable') + box.prop(prop, 'valley_altitude') + + box = layout.box() + row = box.row(align=True) + + if prop.beam_expand: + row.prop(prop, 'beam_expand', icon="TRIA_DOWN", text="Beam", icon_only=True, emboss=False) + else: + row.prop(prop, 'beam_expand', icon="TRIA_RIGHT", text="Beam", icon_only=True, emboss=False) + if prop.beam_expand: + box.prop(prop, 'beam_enable') + if prop.beam_enable: + box.prop(prop, 'beam_width') + box.prop(prop, 'beam_height') + box.prop(prop, 'beam_offset') + box.prop(prop, 'beam_alt') + box.separator() + box.prop(prop, 'beam_sec_enable') + if prop.beam_sec_enable: + box.prop(prop, 'beam_sec_width') + box.prop(prop, 'beam_sec_height') + box.prop(prop, 'beam_sec_alt') + box.separator() + box.prop(prop, 'rafter_enable') + if prop.rafter_enable: + box.prop(prop, 'rafter_height') + box.prop(prop, 'rafter_width') + box.prop(prop, 'rafter_spacing') + box.prop(prop, 'rafter_start') + box.prop(prop, 'rafter_alt') + + box = layout.box() + row = box.row(align=True) + if prop.gutter_expand: + row.prop(prop, 'gutter_expand', icon="TRIA_DOWN", text="Gutter", icon_only=True, emboss=False) + else: + row.prop(prop, 'gutter_expand', icon="TRIA_RIGHT", text="Gutter", icon_only=True, emboss=False) + row.prop(prop, 'gutter_enable') + if prop.gutter_expand: + box.prop(prop, 'gutter_alt') + box.prop(prop, 'gutter_width') + box.prop(prop, 'gutter_dist') + box.prop(prop, 'gutter_boudin') + box.prop(prop, 'gutter_segs') + + box = layout.box() + row = box.row(align=True) + if prop.facia_expand: + row.prop(prop, 'facia_expand', icon="TRIA_DOWN", text="Facia", icon_only=True, emboss=False) + else: + row.prop(prop, 'facia_expand', icon="TRIA_RIGHT", text="Facia", icon_only=True, emboss=False) + row.prop(prop, 'facia_enable') + if prop.facia_expand: + box.prop(prop, 'facia_altitude') + box.prop(prop, 'facia_width') + box.prop(prop, 'facia_height') + box.prop(prop, 'facia_offset') + + box = layout.box() + row = box.row(align=True) + if prop.rake_expand: + row.prop(prop, 'rake_expand', icon="TRIA_DOWN", text="Rake", icon_only=True, emboss=False) + else: + row.prop(prop, 'rake_expand', icon="TRIA_RIGHT", text="Rake", icon_only=True, emboss=False) + row.prop(prop, 'rake_enable') + if prop.rake_expand: + box.prop(prop, 'rake_altitude') + box.prop(prop, 'rake_width') + box.prop(prop, 'rake_height') + box.prop(prop, 'rake_offset') + + """ + box = layout.box() + row.prop_search(prop, "user_defined_path", scene, "objects", text="", icon='OUTLINER_OB_CURVE') + box.prop(prop, 'user_defined_resolution') + box.prop(prop, 'angle_limit') + """ + + +# ------------------------------------------------------------------ +# Define operator class to create object +# ------------------------------------------------------------------ + + +class ARCHIPACK_OT_roof(ArchipackCreateTool, Operator): + bl_idname = "archipack.roof" + bl_label = "Roof" + bl_description = "Roof" + bl_category = 'Archipack' + bl_options = {'REGISTER', 'UNDO'} + + def create(self, context): + m = bpy.data.meshes.new("Roof") + o = bpy.data.objects.new("Roof", m) + d = m.archipack_roof.add() + # make manipulators selectable + d.manipulable_selectable = True + context.scene.objects.link(o) + o.select = True + context.scene.objects.active = o + self.add_material(o) + self.load_preset(d) + 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'} + + +class ARCHIPACK_OT_roof_cutter(ArchipackCreateTool, Operator): + bl_idname = "archipack.roof_cutter" + bl_label = "Roof Cutter" + bl_description = "Roof Cutter" + bl_category = 'Archipack' + bl_options = {'REGISTER', 'UNDO'} + + parent = StringProperty("") + + def create(self, context): + m = bpy.data.meshes.new("Roof Cutter") + o = bpy.data.objects.new("Roof Cutter", m) + d = m.archipack_roof_cutter.add() + parent = context.scene.objects.get(self.parent) + if parent is not None: + o.parent = parent + bbox = parent.bound_box + angle_90 = pi / 2 + x0, y0, z = bbox[0] + x1, y1, z = bbox[6] + x = 0.2 * (x1 - x0) + y = 0.2 * (y1 - y0) + o.matrix_world = parent.matrix_world * Matrix([ + [1, 0, 0, -3 * x], + [0, 1, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 1] + ]) + p = d.parts.add() + p.a0 = - angle_90 + p.length = y + p = d.parts.add() + p.a0 = angle_90 + p.length = x + p = d.parts.add() + p.a0 = angle_90 + p.length = y + d.n_parts = 3 + # d.close = True + pd = archipack_roof.datablock(parent) + pd.boundary = o.name + else: + o.location = context.scene.cursor_location + # make manipulators selectable + d.manipulable_selectable = True + context.scene.objects.link(o) + o.select = True + context.scene.objects.active = o + self.add_material(o) + self.load_preset(d) + update_operation(d, context) + 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 + 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_roof_from_curve(Operator): + bl_idname = "archipack.roof_from_curve" + bl_label = "Roof curve" + bl_description = "Create a roof 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 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 + m = bpy.data.meshes.new("Roof") + o = bpy.data.objects.new("Roof", m) + d = m.archipack_roof.add() + # make manipulators selectable + d.manipulable_selectable = True + d.user_defined_path = curve.name + context.scene.objects.link(o) + o.select = True + context.scene.objects.active = o + d.update_path(context) + + spline = curve.data.splines[0] + 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] + ]) + o.select = True + context.scene.objects.active = o + return o + + # ----------------------------------------------------- + # Execute + # ----------------------------------------------------- + def execute(self, context): + if context.mode == "OBJECT": + bpy.ops.object.select_all(action="DESELECT") + self.create(context) + if self.auto_manipulate: + bpy.ops.archipack.roof_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_roof_manipulate(Operator): + bl_idname = "archipack.roof_manipulate" + bl_label = "Manipulate" + bl_description = "Manipulate" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(self, context): + return archipack_roof.filter(context.active_object) + + def invoke(self, context, event): + d = archipack_roof.datablock(context.active_object) + d.manipulable_invoke(context) + return {'FINISHED'} + + +class ARCHIPACK_OT_roof_cutter_manipulate(Operator): + bl_idname = "archipack.roof_cutter_manipulate" + bl_label = "Manipulate" + bl_description = "Manipulate" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(self, context): + return archipack_roof_cutter.filter(context.active_object) + + def invoke(self, context, event): + d = archipack_roof_cutter.datablock(context.active_object) + d.manipulable_invoke(context) + return {'FINISHED'} + + +# Update throttle +class ArchipackThrottleHandler(): + """ + One modal runs for each object at time + when call for 2nd one + update timer so first one wait more + and kill 2nd one + """ + def __init__(self, context, delay): + self._timer = None + self.start = 0 + self.update_state = False + self.delay = delay + + def start_timer(self, context): + self.start = time.time() + self._timer = context.window_manager.event_timer_add(self.delay, context.window) + + def stop_timer(self, context): + if self._timer is not None: + context.window_manager.event_timer_remove(self._timer) + self._timer = None + + def execute(self, context): + """ + refresh timer on execute + return + True if modal should run + False on complete + """ + if self._timer is None: + self.update_state = False + self.start_timer(context) + return True + + # allready a timer running + self.stop_timer(context) + + # prevent race conditions when allready in update mode + if self.is_updating: + return False + + self.start_timer(context) + return False + + def modal(self, context, event): + if event.type == 'TIMER' and not self.is_updating: + if time.time() - self.start > self.delay: + self.update_state = True + self.stop_timer(context) + return True + return False + + @property + def is_updating(self): + return self.update_state + + +throttle_handlers = {} +throttle_delay = 1 + + +class ARCHIPACK_OT_roof_throttle_update(Operator): + bl_idname = "archipack.roof_throttle_update" + bl_label = "Update childs with a delay" + + name = StringProperty() + + def kill_handler(self, context, name): + if name in throttle_handlers.keys(): + throttle_handlers[name].stop_timer(context) + del throttle_handlers[self.name] + + def get_handler(self, context, delay): + global throttle_handlers + if self.name not in throttle_handlers.keys(): + throttle_handlers[self.name] = ArchipackThrottleHandler(context, delay) + return throttle_handlers[self.name] + + def modal(self, context, event): + global throttle_handlers + if self.name in throttle_handlers.keys(): + if throttle_handlers[self.name].modal(context, event): + act = context.active_object + o = context.scene.objects.get(self.name) + # print("delay update of %s" % (self.name)) + if o is not None: + selected = o.select + o.select = True + context.scene.objects.active = o + d = o.data.archipack_roof[0] + d.update(context, + force_update=True, + update_parent=False) + # skip_parent_update=self.skip_parent_update) + o.select = selected + context.scene.objects.active = act + del throttle_handlers[self.name] + return {'FINISHED'} + else: + return {'PASS_THROUGH'} + else: + return {'FINISHED'} + + def execute(self, context): + global throttle_delay + handler = self.get_handler(context, throttle_delay) + if handler.execute(context): + context.window_manager.modal_handler_add(self) + return {'RUNNING_MODAL'} + return {'FINISHED'} + + +# ------------------------------------------------------------------ +# Define operator class to load / save presets +# ------------------------------------------------------------------ + + +class ARCHIPACK_OT_roof_preset_menu(PresetMenuOperator, Operator): + bl_description = "Show Roof presets" + bl_idname = "archipack.roof_preset_menu" + bl_label = "Roof Styles" + preset_subdir = "archipack_roof" + + +class ARCHIPACK_OT_roof_preset(ArchipackPreset, Operator): + """Add a Roof Styles""" + bl_idname = "archipack.roof_preset" + bl_label = "Add Roof Style" + preset_menu = "ARCHIPACK_OT_roof_preset_menu" + + @property + def blacklist(self): + return ['n_parts', 'parts', 'manipulators', 'user_defined_path'] + + +def register(): + # bpy.utils.register_class(archipack_roof_material) + bpy.utils.register_class(archipack_roof_cutter_segment) + bpy.utils.register_class(archipack_roof_cutter) + bpy.utils.register_class(ARCHIPACK_PT_roof_cutter) + bpy.utils.register_class(ARCHIPACK_OT_roof_cutter) + bpy.utils.register_class(ARCHIPACK_OT_roof_cutter_manipulate) + Mesh.archipack_roof_cutter = CollectionProperty(type=archipack_roof_cutter) + bpy.utils.register_class(archipack_roof_segment) + bpy.utils.register_class(archipack_roof) + Mesh.archipack_roof = CollectionProperty(type=archipack_roof) + bpy.utils.register_class(ARCHIPACK_OT_roof_preset_menu) + bpy.utils.register_class(ARCHIPACK_PT_roof) + bpy.utils.register_class(ARCHIPACK_OT_roof) + bpy.utils.register_class(ARCHIPACK_OT_roof_preset) + bpy.utils.register_class(ARCHIPACK_OT_roof_manipulate) + bpy.utils.register_class(ARCHIPACK_OT_roof_from_curve) + bpy.utils.register_class(ARCHIPACK_OT_roof_throttle_update) + + +def unregister(): + # bpy.utils.unregister_class(archipack_roof_material) + bpy.utils.unregister_class(archipack_roof_cutter_segment) + bpy.utils.unregister_class(archipack_roof_cutter) + bpy.utils.unregister_class(ARCHIPACK_PT_roof_cutter) + bpy.utils.unregister_class(ARCHIPACK_OT_roof_cutter) + bpy.utils.unregister_class(ARCHIPACK_OT_roof_cutter_manipulate) + del Mesh.archipack_roof_cutter + bpy.utils.unregister_class(archipack_roof_segment) + bpy.utils.unregister_class(archipack_roof) + del Mesh.archipack_roof + bpy.utils.unregister_class(ARCHIPACK_OT_roof_preset_menu) + bpy.utils.unregister_class(ARCHIPACK_PT_roof) + bpy.utils.unregister_class(ARCHIPACK_OT_roof) + bpy.utils.unregister_class(ARCHIPACK_OT_roof_preset) + bpy.utils.unregister_class(ARCHIPACK_OT_roof_manipulate) + bpy.utils.unregister_class(ARCHIPACK_OT_roof_from_curve) + bpy.utils.unregister_class(ARCHIPACK_OT_roof_throttle_update) diff --git a/archipack/archipack_slab.py b/archipack/archipack_slab.py index d29c1678..0326a9f0 100644 --- a/archipack/archipack_slab.py +++ b/archipack/archipack_slab.py @@ -40,6 +40,11 @@ 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 +from .archipack_cutter import ( + CutAblePolygon, CutAbleGenerator, + ArchipackCutter, + ArchipackCutterPart + ) class Slab(): @@ -82,11 +87,14 @@ class CurvedSlab(Slab, Arc): Slab.__init__(self) -class SlabGenerator(): +class SlabGenerator(CutAblePolygon, CutAbleGenerator): def __init__(self, parts): self.parts = parts self.segs = [] + self.holes = [] + self.convex = True + self.xsize = 0 def add_part(self, part): @@ -118,13 +126,6 @@ class SlabGenerator(): 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: @@ -146,9 +147,8 @@ class SlabGenerator(): w.v = dp if len(self.segs) > 1: - w.line = w.make_offset(self.parts[-1].offset, self.segs[-2]) + w.line = w.make_offset(self.parts[-1].offset, self.segs[-2].line) - 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 @@ -188,14 +188,14 @@ class SlabGenerator(): 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 s in self.segs: + if "Curved" in type(s).__name__: for i in range(16): - x, y = slab.line.lerp(i / 16) - verts.append((x, y, 0)) + # x, y = slab.line.lerp(i / 16) + verts.append(s.lerp(i / 16).to_3d()) else: - x, y = slab.line.p0 - verts.append((x, y, 0)) + # x, y = s.line.p0 + verts.append(s.p0.to_3d()) """ for i in range(33): x, y = slab.line.lerp(i / 32) @@ -238,6 +238,59 @@ class SlabGenerator(): for seg in self.segs: seg.draw(context, render=False) + def limits(self): + x_size = [s.p0.x for s in self.segs] + self.xsize = max(x_size) - min(x_size) + + def cut(self, context, o): + """ + either external or holes cuts + """ + self.limits() + for b in o.children: + d = archipack_slab_cutter.datablock(b) + if d is not None: + g = d.ensure_direction() + g.change_coordsys(b.matrix_world, o.matrix_world) + self.slice(g) + + def slab(self, context, o, d): + + verts = [] + self.get_verts(verts) + if len(verts) > 2: + + 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) + + self.cut_holes(bm, self) + + bmesh.ops.dissolve_limit(bm, + angle_limit=0.01, + use_dissolve_boundaries=False, + verts=bm.verts, + edges=bm.edges, + delimit=1) + + bm.to_mesh(o.data) + bm.free() + # geom = bm.faces[:] + # verts = bm.verts[:] + # bmesh.ops.solidify(bm, geom=geom, thickness=d.z) + + # merge with object + # bmed.bmesh_join(context, o, [bm], normal_update=True) + + bpy.ops.object.mode_set(mode='OBJECT') + def update(self, context): self.update(context) @@ -491,8 +544,9 @@ class archipack_slab(ArchipackObject, Manipulable, PropertyGroup): ) parts = CollectionProperty(type=archipack_slab_part) closed = BoolProperty( - default=False, + default=True, name="Close", + options={'SKIP_SAVE'}, update=update_manipulators ) # UI layout related @@ -939,6 +993,7 @@ class archipack_slab(ArchipackObject, Manipulable, PropertyGroup): s = p.manipulators.add() s.type_key = "ANGLE" s.prop1_name = "a0" + p.manipulators[0].type_key = 'ANGLE' if n_manips < 2: s = p.manipulators.add() s.type_key = "SIZE" @@ -1080,11 +1135,15 @@ class archipack_slab(ArchipackObject, Manipulable, PropertyGroup): g = self.update_parts(o, update_childs) - verts = [] + # relocate before cutting segs + self.relocate_childs(context, o, g) - g.get_verts(verts) - if len(verts) > 2: - self.make_surface(o, verts) + o.select = True + context.scene.objects.active = o + + g.cut(context, o) + + g.slab(context, o, self) modif = o.modifiers.get('Slab') if modif is None: @@ -1106,8 +1165,6 @@ class archipack_slab(ArchipackObject, Manipulable, PropertyGroup): (-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 @@ -1168,6 +1225,70 @@ class archipack_slab(ArchipackObject, Manipulable, PropertyGroup): return True +def update_hole(self, context): + # update parent's roof only when manipulated + self.update(context, update_parent=True) + + +def update_operation(self, context): + self.reverse(context, make_ccw=(self.operation == 'INTERSECTION')) + + +class archipack_slab_cutter_segment(ArchipackCutterPart, PropertyGroup): + manipulators = CollectionProperty(type=archipack_manipulator) + type = EnumProperty( + name="Type", + items=( + ('DEFAULT', 'Side', 'Side with rake', 0), + ('BOTTOM', 'Bottom', 'Bottom with gutter', 1), + ('LINK', 'Side link', 'Side witout decoration', 2), + ('AXIS', 'Top', 'Top part with hip and beam', 3) + # ('LINK_VALLEY', 'Side valley', 'Side with valley', 3), + # ('LINK_HIP', 'Side hip', 'Side with hip', 4) + ), + default='DEFAULT', + update=update_hole + ) + + def find_in_selection(self, context): + selected = [o for o in context.selected_objects] + for o in selected: + d = archipack_slab_cutter.datablock(o) + if d: + for part in d.parts: + if part == self: + return d + return None + + def draw(self, layout, context, index): + box = layout.box() + box.label(text="Part:" + str(index + 1)) + # box.prop(self, "type", text=str(index + 1)) + box.prop(self, "length") + box.prop(self, "a0") + + +class archipack_slab_cutter(ArchipackCutter, ArchipackObject, Manipulable, PropertyGroup): + parts = CollectionProperty(type=archipack_slab_cutter_segment) + + def update_points(self, context, o, pts, update_parent=False): + self.auto_update = False + self.from_points(pts) + self.auto_update = True + if update_parent: + self.update_parent(context, o) + + def update_parent(self, context, o): + + d = archipack_slab.datablock(o.parent) + if d is not None: + o.parent.select = True + context.scene.objects.active = o.parent + d.update(context) + o.parent.select = False + context.scene.objects.active = o + + class ARCHIPACK_PT_slab(Panel): """Archipack Slab""" bl_idname = "ARCHIPACK_PT_slab" @@ -1182,13 +1303,14 @@ class ARCHIPACK_PT_slab(Panel): return archipack_slab.filter(context.active_object) def draw(self, context): - prop = archipack_slab.datablock(context.active_object) + o = context.active_object + prop = archipack_slab.datablock(o) 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') + layout.operator('archipack.slab_manipulate', icon='HAND') + box = layout.box() + box.operator('archipack.slab_cutter').parent = o.name box = layout.box() box.prop(prop, 'z') box = layout.box() @@ -1205,6 +1327,39 @@ class ARCHIPACK_PT_slab(Panel): row.prop(prop, 'parts_expand', icon="TRIA_RIGHT", icon_only=True, text="Parts", emboss=False) +class ARCHIPACK_PT_slab_cutter(Panel): + bl_idname = "ARCHIPACK_PT_slab_cutter" + bl_label = "Slab Cutter" + bl_space_type = 'VIEW_3D' + bl_region_type = 'UI' + bl_category = 'ArchiPack' + + @classmethod + def poll(cls, context): + return archipack_slab_cutter.filter(context.active_object) + + def draw(self, context): + prop = archipack_slab_cutter.datablock(context.active_object) + if prop is None: + return + layout = self.layout + scene = context.scene + box = layout.box() + box.operator('archipack.slab_cutter_manipulate', icon='HAND') + box.prop(prop, 'operation', text="") + box = layout.box() + box.label(text="From curve") + box.prop_search(prop, "user_defined_path", scene, "objects", text="", icon='OUTLINER_OB_CURVE') + if prop.user_defined_path != "": + box.prop(prop, 'user_defined_resolution') + # box.prop(prop, 'x_offset') + # box.prop(prop, 'angle_limit') + """ + box.prop_search(prop, "boundary", scene, "objects", text="", icon='OUTLINER_OB_CURVE') + """ + prop.draw(layout, context) + + # ------------------------------------------------------------------ # Define operator class to create object # ------------------------------------------------------------------ @@ -1450,6 +1605,75 @@ class ARCHIPACK_OT_slab_from_wall(Operator): return {'CANCELLED'} +class ARCHIPACK_OT_slab_cutter(ArchipackCreateTool, Operator): + bl_idname = "archipack.slab_cutter" + bl_label = "Slab Cutter" + bl_description = "Slab Cutter" + bl_category = 'Archipack' + bl_options = {'REGISTER', 'UNDO'} + + parent = StringProperty("") + + def create(self, context): + m = bpy.data.meshes.new("Slab Cutter") + o = bpy.data.objects.new("Slab Cutter", m) + d = m.archipack_slab_cutter.add() + parent = context.scene.objects.get(self.parent) + if parent is not None: + o.parent = parent + bbox = parent.bound_box + angle_90 = pi / 2 + x0, y0, z = bbox[0] + x1, y1, z = bbox[6] + x = 0.2 * (x1 - x0) + y = 0.2 * (y1 - y0) + o.matrix_world = parent.matrix_world * Matrix([ + [1, 0, 0, -3 * x], + [0, 1, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 1] + ]) + p = d.parts.add() + p.a0 = - angle_90 + p.length = y + p = d.parts.add() + p.a0 = angle_90 + p.length = x + p = d.parts.add() + p.a0 = angle_90 + p.length = y + d.n_parts = 3 + # d.close = True + pd = archipack_slab.datablock(parent) + pd.boundary = o.name + else: + o.location = context.scene.cursor_location + # make manipulators selectable + d.manipulable_selectable = True + context.scene.objects.link(o) + o.select = True + context.scene.objects.active = o + # self.add_material(o) + self.load_preset(d) + update_operation(d, context) + 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 + self.manipulate() + return {'FINISHED'} + else: + self.report({'WARNING'}, "Archipack: Option only valid in Object mode") + return {'CANCELLED'} + + # ------------------------------------------------------------------ # Define operator class to manipulate object # ------------------------------------------------------------------ @@ -1471,7 +1695,30 @@ class ARCHIPACK_OT_slab_manipulate(Operator): return {'FINISHED'} +class ARCHIPACK_OT_slab_cutter_manipulate(Operator): + bl_idname = "archipack.slab_cutter_manipulate" + bl_label = "Manipulate" + bl_description = "Manipulate" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(self, context): + return archipack_slab_cutter.filter(context.active_object) + + def invoke(self, context, event): + d = archipack_slab_cutter.datablock(context.active_object) + d.manipulable_invoke(context) + return {'FINISHED'} + + def register(): + bpy.utils.register_class(archipack_slab_cutter_segment) + bpy.utils.register_class(archipack_slab_cutter) + Mesh.archipack_slab_cutter = CollectionProperty(type=archipack_slab_cutter) + bpy.utils.register_class(ARCHIPACK_OT_slab_cutter) + bpy.utils.register_class(ARCHIPACK_PT_slab_cutter) + bpy.utils.register_class(ARCHIPACK_OT_slab_cutter_manipulate) + bpy.utils.register_class(archipack_slab_material) bpy.utils.register_class(archipack_slab_child) bpy.utils.register_class(archipack_slab_part) @@ -1503,3 +1750,9 @@ def unregister(): 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) + del Mesh.archipack_slab_cutter + bpy.utils.unregister_class(archipack_slab_cutter_segment) + bpy.utils.unregister_class(archipack_slab_cutter) + bpy.utils.unregister_class(ARCHIPACK_OT_slab_cutter) + bpy.utils.unregister_class(ARCHIPACK_PT_slab_cutter) + bpy.utils.unregister_class(ARCHIPACK_OT_slab_cutter_manipulate) diff --git a/archipack/archipack_snap.py b/archipack/archipack_snap.py index 936a07d8..eb3898d3 100644 --- a/archipack/archipack_snap.py +++ b/archipack/archipack_snap.py @@ -273,10 +273,10 @@ class ARCHIPACK_OT_snap(ArchipackSnapBase, Operator): # 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)) + 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'}: + if event.type in ('ESC', 'RIGHTMOUSE'): SnapStore.callback(context, event, 'CANCEL', self) else: SnapStore.placeloc = SnapStore.helper.location @@ -290,7 +290,6 @@ class ARCHIPACK_OT_snap(ArchipackSnapBase, Operator): # 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, diff --git a/archipack/archipack_stair.py b/archipack/archipack_stair.py index 9cdd1449..172e1c38 100644 --- a/archipack/archipack_stair.py +++ b/archipack/archipack_stair.py @@ -2820,11 +2820,6 @@ class ARCHIPACK_OT_stair_preset(ArchipackPreset, Operator): def blacklist(self): return ['manipulators'] - """ - 'presets', 'n_parts', 'parts', 'width', 'height', 'radius', - 'total_angle', 'da', - """ - def register(): bpy.utils.register_class(archipack_stair_material) diff --git a/archipack/archipack_wall2.py b/archipack/archipack_wall2.py index 7c42456f..06dabb0e 100644 --- a/archipack/archipack_wall2.py +++ b/archipack/archipack_wall2.py @@ -25,7 +25,10 @@ # # ---------------------------------------------------------- import bpy -# import time +import bmesh + +import time + from bpy.types import Operator, PropertyGroup, Mesh, Panel from bpy.props import ( FloatProperty, BoolProperty, IntProperty, StringProperty, @@ -56,6 +59,13 @@ class Wall(): self.flip = flip self.z_step = len(z) + def set_offset(self, offset, last=None): + """ + Offset line and compute intersection point + between segments + """ + self.line = self.make_offset(offset, last) + def get_z(self, t): t0 = self.t[0] z0 = self.z[0] @@ -88,6 +98,11 @@ class Wall(): self.p3d(verts, t) self.make_faces(i, f, faces) + def make_hole(self, i, verts, z0): + t = self.t_step[i] + x, y = self.line.lerp(t) + verts.append((x, y, z0)) + 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) @@ -130,6 +145,21 @@ class WallGenerator(): self.faces_type = 'NONE' self.closed = False + def set_offset(self, offset): + last = None + for i, seg in enumerate(self.segs): + seg.set_offset(offset, last) + last = seg.line + + if self.closed: + w = self.segs[-1] + if len(self.segs) > 1: + w.line = w.make_offset(offset, self.segs[-2].line) + + p1 = self.segs[0].line.p1 + self.segs[0].line = self.segs[0].make_offset(offset, w.line) + self.segs[0].line.p1 = p1 + def add_part(self, part, wall_z, flip): # TODO: @@ -205,18 +235,7 @@ class WallGenerator(): 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 + def locate_manipulators(self, side): for i, wall in enumerate(self.segs): @@ -260,6 +279,21 @@ class WallGenerator(): z = Vector((0, 0, 0.75 * wall.wall_z)) manipulators[3].set_pts([p0 + z, p1 + z, (1, 0, 0)]) + 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): + wall.param_t(step_angle) if i < nb_segs: for j in range(wall.n_step + 1): @@ -271,6 +305,8 @@ class WallGenerator(): # print("%s" % (wall.n_step)) # wall.make_wall(j, verts, faces) + self.locate_manipulators(side) + def rotate(self, idx_from, a): """ apply rotation to all following segs @@ -298,6 +334,23 @@ class WallGenerator(): for i in range(idx_from + 1, len(self.segs)): self.segs[i].translate(dp) + def change_coordsys(self, fromTM, toTM): + """ + move shape fromTM into toTM coordsys + """ + dp = (toTM.inverted() * fromTM.translation).to_2d() + da = toTM.row[1].to_2d().angle_signed(fromTM.row[1].to_2d()) + ca = cos(da) + sa = sin(da) + rM = Matrix([ + [ca, -sa], + [sa, ca] + ]) + for s in self.segs: + tp = (rM * s.p0) - s.p0 + dp + s.rotate(da) + s.translate(tp) + def draw(self, context): for seg in self.segs: seg.draw(context, render=False) @@ -308,6 +361,41 @@ class WallGenerator(): x, y = wall.lerp(i / 32) verts.append((x, y, 0)) + def make_surface(self, o, verts, height): + 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) + geom = bm.faces[:] + bmesh.ops.solidify(bm, geom=geom, thickness=height) + bm.to_mesh(o.data) + bm.free() + + def make_hole(self, context, hole_obj, d): + + offset = -0.5 * (1 - d.x_offset) * d.width + + z0 = 0.1 + self.set_offset(offset) + + nb_segs = len(self.segs) - 1 + if d.closed: + nb_segs += 1 + + verts = [] + for i, wall in enumerate(self.segs): + wall.param_t(d.step_angle) + if i < nb_segs: + for j in range(wall.n_step + 1): + wall.make_hole(j, verts, -z0) + + self.make_surface(hole_obj, verts, d.z + z0) + def update(self, context): self.update(context) @@ -566,7 +654,6 @@ class archipack_wall2_part(PropertyGroup): ) z = FloatVectorProperty( name="height", - min=0, default=[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, @@ -1408,6 +1495,9 @@ class archipack_wall2(ArchipackObject, Manipulable, PropertyGroup): self.update_childs(context, o, g) def manipulable_move_t_part(self, context, o=None, manipulator=None): + """ + Callback for t_parts childs + """ type_name = type(manipulator).__name__ # print("manipulable_manipulate %s" % (type_name)) if type_name in [ @@ -1498,12 +1588,32 @@ class archipack_wall2(ArchipackObject, Manipulable, PropertyGroup): return True - -# Update throttle (smell hack here) + def find_roof(self, context, o, g): + tM = o.matrix_world + up = Vector((0, 0, 1)) + for seg in g.segs: + p = tM * seg.p0.to_3d() + p.z = 0.01 + # prevent self intersect + o.hide = True + res, pos, normal, face_index, r, matrix_world = context.scene.ray_cast( + p, + up) + o.hide = False + # print("res:%s" % res) + if res and r.data is not None and "archipack_roof" in r.data: + return r, r.data.archipack_roof[0] + + return None, None + + +# Update throttle (hack) # 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 +# Use to update floor boolean on edit update_timer = None update_timer_updating = False +throttle_delay = 0.5 +throttle_start = 0 class ARCHIPACK_OT_wall2_throttle_update(Operator): @@ -1515,34 +1625,37 @@ class ARCHIPACK_OT_wall2_throttle_update(Operator): 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) + # cant rely on TIMER event as another timer may run + if time.time() - throttle_start > throttle_delay: + update_timer_updating = True + o = context.scene.objects.get(self.name) + if o is not None: + m = o.modifiers.get("AutoBoolean") + if m is not None: + o.hide = False + # o.draw_type = 'TEXTURED' + # m.show_viewport = True + + return self.cancel(context) return {'PASS_THROUGH'} def execute(self, context): global update_timer global update_timer_updating + global throttle_delay + global throttle_start if update_timer is not None: + context.window_manager.event_timer_remove(update_timer) 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) + throttle_start = time.time() + update_timer = context.window_manager.event_timer_add(throttle_delay, context.window) return {'CANCELLED'} + throttle_start = time.time() update_timer_updating = False context.window_manager.modal_handler_add(self) - update_timer = context.window_manager.event_timer_add(0.1, context.window) + update_timer = context.window_manager.event_timer_add(throttle_delay, context.window) return {'RUNNING_MODAL'} def cancel(self, context): @@ -1579,8 +1692,10 @@ class ARCHIPACK_PT_wall2(Panel): 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') + layout.operator("archipack.wall2_reverse", icon='FILE_REFRESH') + row = layout.row(align=True) + row.operator("archipack.wall2_fit_roof") + # row.operator("archipack.wall2_fit_roof", text="Inside").inside = True n_parts = prop.n_parts if prop.closed: n_parts += 1 @@ -1757,6 +1872,29 @@ class ARCHIPACK_OT_wall2_from_slab(Operator): return {'CANCELLED'} +class ARCHIPACK_OT_wall2_fit_roof(Operator): + bl_idname = "archipack.wall2_fit_roof" + bl_label = "Fit roof" + bl_description = "Fit roof" + bl_category = 'Archipack' + bl_options = {'REGISTER', 'UNDO'} + + inside = BoolProperty(default=False) + + @classmethod + def poll(self, context): + return archipack_wall2.filter(context.active_object) + + def execute(self, context): + o = context.active_object + d = archipack_wall2.datablock(o) + g = d.get_generator() + r, rd = d.find_roof(context, o, g) + if rd is not None: + d.setup_childs(o, g) + rd.make_wall_fit(context, r, o, self.inside) + return {'FINISHED'} + # ------------------------------------------------------------------ # Define operator class to draw a wall # ------------------------------------------------------------------ @@ -1786,8 +1924,6 @@ class ARCHIPACK_OT_wall2_draw(ArchpackDrawTool, Operator): parent = None takemat = None - max_style_draw_tool = False - @classmethod def poll(cls, context): return True @@ -1928,22 +2064,17 @@ class ARCHIPACK_OT_wall2_draw(ArchpackDrawTool, Operator): # wait for takeloc being visible when button is over horizon rv3d = context.region_data viewinv = rv3d.view_matrix.inverted() + if (takeloc * viewinv).z < 0 or not rv3d.is_perspective: # 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, @@ -1972,6 +2103,7 @@ class ARCHIPACK_OT_wall2_draw(ArchpackDrawTool, Operator): evt_value = 'RELEASE' if event.value == evt_value: + if self.flag_next: self.flag_next = False o = self.o @@ -2028,9 +2160,9 @@ class ARCHIPACK_OT_wall2_draw(ArchpackDrawTool, Operator): else: self.o.select = True context.scene.objects.active = self.o - d = archipack_wall2.datablock(self.o) # remove last segment with blender mode + d = archipack_wall2.datablock(self.o) if not self.max_style_draw_tool: if not d.closed and d.n_parts > 1: d.n_parts -= 1 @@ -2201,6 +2333,7 @@ def register(): 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) + bpy.utils.register_class(ARCHIPACK_OT_wall2_fit_roof) def unregister(): @@ -2218,3 +2351,4 @@ def unregister(): 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) + bpy.utils.unregister_class(ARCHIPACK_OT_wall2_fit_roof) diff --git a/archipack/archipack_window.py b/archipack/archipack_window.py index 6768fe2b..a0400f54 100644 --- a/archipack/archipack_window.py +++ b/archipack/archipack_window.py @@ -32,11 +32,10 @@ from bpy.props import ( FloatProperty, IntProperty, BoolProperty, BoolVectorProperty, CollectionProperty, FloatVectorProperty, EnumProperty, StringProperty ) -from mathutils import Vector +from mathutils import Vector, Matrix 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 @@ -54,6 +53,10 @@ def update_childs(self, context): self.update(context, childs_only=True) +def update_portal(self, context): + self.update_portal(context) + + def set_cols(self, value): if self.n_cols != value: self.auto_update = False @@ -274,6 +277,10 @@ class archipack_window_panel(ArchipackObject, PropertyGroup): name="Fixed", default=False ) + enable_glass = BoolProperty( + name="Enable glass", + default=True + ) @property def window(self): @@ -304,14 +311,22 @@ class archipack_window_panel(ArchipackObject, PropertyGroup): y3 = y1 - chanfer y4 = chanfer + y0 y2 = (y0 + y2) / 2 + + side_cap_front = -1 + side_cap_back = -1 + + if self.enable_glass: + side_cap_front = 6 + side_cap_back = 7 + 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 + side_cap_front=side_cap_front, + side_cap_back=side_cap_back # cap index ) else: # profil avec chanfrein et joint et support pour verre @@ -330,14 +345,22 @@ class archipack_window_panel(ArchipackObject, PropertyGroup): else: # rail window interior materials = [0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0] + + side_cap_front = -1 + side_cap_back = -1 + + if self.enable_glass: + side_cap_front = 8 + side_cap_back = 9 + 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 + side_cap_front=side_cap_front, + side_cap_back=side_cap_back # cap index ) @property @@ -370,7 +393,7 @@ class archipack_window_panel(ArchipackObject, PropertyGroup): if handle is None: m = bpy.data.meshes.new("Handle") handle = create_handle(context, o, m) - MaterialUtils.add_handle_materials(handle) + # MaterialUtils.add_handle_materials(handle) if self.handle_model == 1: verts, faces = window_handle_vertical_01(1) else: @@ -391,7 +414,6 @@ class archipack_window_panel(ArchipackObject, PropertyGroup): 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: @@ -623,7 +645,13 @@ class archipack_window(ArchipackObject, Manipulable, PropertyGroup): ), default='FLAT', update=update, ) + enable_glass = BoolProperty( + name="Enable glass", + default=True, + update=update + ) warning = BoolProperty( + options={'SKIP_SAVE'}, name="warning", default=False ) @@ -669,6 +697,12 @@ class archipack_window(ArchipackObject, Manipulable, PropertyGroup): default=True, update=update ) + portal = BoolProperty( + default=False, + name="Portal", + description="Generate a portal", + update=update + ) @property def shape(self): @@ -957,6 +991,44 @@ class archipack_window(ArchipackObject, Manipulable, PropertyGroup): self.angle_y, 0, 0, self.frame_x, path_type='HORIZONTAL') return uvs + def find_portal(self, o): + for child in o.children: + if child.type == 'LAMP': + return child + return None + + def update_portal(self, context, o): + + lamp = self.find_portal(o) + if self.portal: + if lamp is None: + bpy.ops.object.lamp_add(type='AREA') + lamp = context.active_object + lamp.name = "Portal" + lamp.parent = o + + d = lamp.data + d.cycles.is_portal = True + d.shape = 'RECTANGLE' + d.size = self.x + d.size_y = self.z + + tM = Matrix([ + [1, 0, 0, 0], + [0, 0, -1, -0.5 * self.y], + [0, 1, 0, 0.5 * self.z + self.altitude], + [0, 0, 0, 1] + ]) + lamp.matrix_world = o.matrix_world * tM + + elif lamp is not None: + d = lamp.data + context.scene.objects.unlink(lamp) + bpy.data.objects.remove(lamp) + bpy.data.lamps.remove(d) + + context.scene.objects.active = o + def setup_manipulators(self): if len(self.manipulators) == 4: return @@ -1037,10 +1109,16 @@ class archipack_window(ArchipackObject, Manipulable, PropertyGroup): return handle return None + def _synch_portal(self, context, o, linked, childs): + # update portal + dl = archipack_window.datablock(linked) + dl.update_portal(context, linked) + 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] @@ -1077,7 +1155,9 @@ class archipack_window(ArchipackObject, Manipulable, PropertyGroup): p.lock_scale[2] = True p.parent = linked p.matrix_world = linked.matrix_world.copy() - + m = p.archipack_material.add() + m.category = 'window' + m.material = o.archipack_material[0].material else: p = l_childs[order[i]] @@ -1087,7 +1167,6 @@ class archipack_window(ArchipackObject, Manipulable, PropertyGroup): 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) @@ -1095,12 +1174,17 @@ class archipack_window(ArchipackObject, Manipulable, PropertyGroup): p.location = child.location.copy() + # restore context + context.scene.objects.active = o + 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) + for mat in hole.data.materials: + l_hole.data.materials.append(mat) l_hole.parent = linked l_hole.matrix_world = linked.matrix_world.copy() l_hole.location = hole.location.copy() @@ -1119,6 +1203,7 @@ class archipack_window(ArchipackObject, Manipulable, PropertyGroup): bpy.ops.object.select_linked(type='OBDATA') for linked in context.selected_objects: if linked != o: + self._synch_portal(context, o, linked, childs) self._synch_childs(context, o, linked, childs) if hole is not None: self._synch_hole(context, linked, hole) @@ -1200,12 +1285,15 @@ class archipack_window(ArchipackObject, Manipulable, PropertyGroup): frame_x=self.frame_x, frame_y=self.frame_y, angle_y=self.angle_y, + enable_glass=self.enable_glass, + material=o.archipack_material[0].material ) 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 @@ -1227,6 +1315,7 @@ class archipack_window(ArchipackObject, Manipulable, PropertyGroup): props.frame_x = self.frame_x props.frame_y = self.frame_y props.angle_y = self.angle_y + props.enable_glass = self.enable_glass props.update(context) # location y + frame width. frame depends on choosen profile (fixed or not) # update linked childs location too @@ -1321,6 +1410,7 @@ class archipack_window(ArchipackObject, Manipulable, PropertyGroup): if childs_only is False: bmed.buildmesh(context, o, self.verts, self.faces, self.matids, self.uvs) + self.update_portal(context, o) self.update_childs(context, o) # update hole @@ -1356,7 +1446,14 @@ class archipack_window(ArchipackObject, Manipulable, PropertyGroup): hole_obj['archipack_hole'] = True hole_obj.parent = o hole_obj.matrix_world = o.matrix_world.copy() - MaterialUtils.add_wall2_materials(hole_obj) + + """ + hole_obj.data.materials.clear() + + for mat in o.data.materials: + hole_obj.data.materials.append(mat) + # MaterialUtils.add_wall2_materials(hole_obj) + """ hole = self.hole center, origin, size, radius = self.get_radius() @@ -1413,7 +1510,7 @@ class archipack_window(ArchipackObject, Manipulable, PropertyGroup): 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) + # MaterialUtils.add_wall2_materials(o) o.select = True context.scene.objects.active = o return o @@ -1487,6 +1584,8 @@ class ARCHIPACK_PT_window(Panel): row.prop(prop, "display_detail", icon="TRIA_RIGHT", icon_only=True, text="Components", emboss=False) if prop.display_detail: + box = layout.box() + box.prop(prop, 'enable_glass') box = layout.box() box.label("Frame") box.prop(prop, 'frame_x') @@ -1559,6 +1658,8 @@ class ARCHIPACK_PT_window(Panel): box.prop(prop, 'hole_inside_mat') box.prop(prop, 'hole_outside_mat') + layout.prop(prop, 'portal', icon="LAMP_AREA") + class ARCHIPACK_PT_window_panel(Panel): bl_idname = "ARCHIPACK_PT_window_panel" @@ -1638,8 +1739,8 @@ class ARCHIPACK_OT_window(ArchipackCreateTool, Operator): context.scene.objects.link(o) o.select = True context.scene.objects.active = o - self.load_preset(d) self.add_material(o) + self.load_preset(d) # select frame o.select = True context.scene.objects.active = o @@ -1650,7 +1751,12 @@ class ARCHIPACK_OT_window(ArchipackCreateTool, Operator): if archipack_window.filter(o): bpy.ops.archipack.disable_manipulate() for child in o.children: - if 'archipack_hole' in child: + if child.type == 'LAMP': + d = child.data + context.scene.objects.unlink(child) + bpy.data.objects.remove(child) + bpy.data.lamps.remove(d) + elif '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: @@ -1672,6 +1778,7 @@ class ARCHIPACK_OT_window(ArchipackCreateTool, Operator): 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 @@ -1877,6 +1984,25 @@ class ARCHIPACK_OT_window_draw(ArchpackDrawTool, Operator): return {'CANCELLED'} +class ARCHIPACK_OT_window_portals(Operator): + bl_idname = "archipack.window_portals" + bl_label = "Portals" + bl_description = "Create portal for each window" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(self, context): + return True + + def invoke(self, context, event): + for o in context.scene.objects: + d = archipack_window.datablock(o) + if d is not None: + d.update_portal(context) + + return {'FINISHED'} + + # ------------------------------------------------------------------ # Define operator class to create object # ------------------------------------------------------------------ @@ -1974,6 +2100,14 @@ class ARCHIPACK_OT_window_panel(Operator): name="Fixed", default=False ) + material = StringProperty( + name="material", + default="" + ) + enable_glass = BoolProperty( + name="Enable glass", + default=True + ) def draw(self, context): layout = self.layout @@ -1999,9 +2133,13 @@ class ARCHIPACK_OT_window_panel(Operator): d.handle = self.handle d.handle_model = self.handle_model d.handle_altitude = self.handle_altitude + d.enable_glass = self.enable_glass context.scene.objects.link(o) o.select = True context.scene.objects.active = o + m = o.archipack_material.add() + m.category = "window" + m.material = self.material o.lock_location[0] = True o.lock_location[1] = True o.lock_location[2] = True @@ -2010,7 +2148,6 @@ class ARCHIPACK_OT_window_panel(Operator): o.lock_scale[1] = True o.lock_scale[2] = True d.update(context) - MaterialUtils.add_window_materials(o) return o def execute(self, context): @@ -2081,6 +2218,7 @@ def register(): bpy.utils.register_class(ARCHIPACK_OT_window_preset) bpy.utils.register_class(ARCHIPACK_OT_window_draw) bpy.utils.register_class(ARCHIPACK_OT_window_manipulate) + bpy.utils.register_class(ARCHIPACK_OT_window_portals) def unregister(): @@ -2097,3 +2235,4 @@ def unregister(): bpy.utils.unregister_class(ARCHIPACK_OT_window_preset) bpy.utils.unregister_class(ARCHIPACK_OT_window_draw) bpy.utils.unregister_class(ARCHIPACK_OT_window_manipulate) + bpy.utils.unregister_class(ARCHIPACK_OT_window_portals) diff --git a/archipack/bmesh_utils.py b/archipack/bmesh_utils.py index b49f4683..3f402d1d 100644 --- a/archipack/bmesh_utils.py +++ b/archipack/bmesh_utils.py @@ -42,6 +42,56 @@ class BmeshEdit(): bm.faces.ensure_lookup_table() return bm + @staticmethod + def bmesh_join(context, o, list_of_bmeshes, normal_update=False): + """ + takes as input a list of bm references and outputs a single merged bmesh + allows an additional 'normal_update=True' to force _normal_ calculations. + """ + bm = BmeshEdit._start(context, o) + + add_vert = bm.verts.new + add_face = bm.faces.new + add_edge = bm.edges.new + + for bm_to_add in list_of_bmeshes: + offset = len(bm.verts) + + for v in bm_to_add.verts: + add_vert(v.co) + + bm.verts.index_update() + bm.verts.ensure_lookup_table() + + if bm_to_add.faces: + layer = bm_to_add.loops.layers.uv.verify() + dest = bm.loops.layers.uv.verify() + for face in bm_to_add.faces: + f = add_face(tuple(bm.verts[i.index + offset] for i in face.verts)) + f.material_index = face.material_index + for j, loop in enumerate(face.loops): + f.loops[j][dest].uv = loop[layer].uv + bm.faces.index_update() + + if bm_to_add.edges: + for edge in bm_to_add.edges: + edge_seq = tuple(bm.verts[i.index + offset] for i in edge.verts) + try: + add_edge(edge_seq) + except ValueError: + # edge exists! + pass + bm.edges.index_update() + + # cleanup + for old_bm in list_of_bmeshes: + old_bm.free() + + if normal_update: + bm.normal_update() + + BmeshEdit._end(bm, o) + @staticmethod def _end(bm, o): """ @@ -76,19 +126,35 @@ class BmeshEdit(): 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() + def buildmesh(context, o, verts, faces, + matids=None, uvs=None, weld=False, + clean=False, auto_smooth=True, temporary=False): + + if temporary: + bm = bmesh.new() + else: + bm = BmeshEdit._start(context, o) + bm.clear() + for v in verts: bm.verts.new(v) + bm.verts.index_update() bm.verts.ensure_lookup_table() + for f in faces: bm.faces.new([bm.verts[i] for i in f]) + bm.faces.index_update() bm.faces.ensure_lookup_table() + if matids is not None: BmeshEdit._matids(bm, matids) + if uvs is not None: BmeshEdit._uvs(bm, uvs) + + if temporary: + return bm + if weld: bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.001) BmeshEdit._end(bm, o) diff --git a/archipack/icons/roof.png b/archipack/icons/roof.png new file mode 100644 index 00000000..7af48808 Binary files /dev/null and b/archipack/icons/roof.png differ diff --git a/archipack/materialutils.py b/archipack/materialutils.py deleted file mode 100644 index 92497924..00000000 --- a/archipack/materialutils.py +++ /dev/null @@ -1,169 +0,0 @@ -# -*- coding:utf-8 -*- - -# ##### BEGIN GPL LICENSE BLOCK ##### -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software Foundation, -# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110- 1301, USA. -# -# ##### END GPL LICENSE BLOCK ##### - -# - -# ---------------------------------------------------------- -# Author: Stephen Leger (s-leger) -# -# ---------------------------------------------------------- -import bpy - - -class MaterialUtils(): - - @staticmethod - def build_default_mat(name, color=(1.0, 1.0, 1.0)): - midx = bpy.data.materials.find(name) - if midx < 0: - mat = bpy.data.materials.new(name) - mat.diffuse_color = color - else: - mat = bpy.data.materials[midx] - return mat - - @staticmethod - def add_wall2_materials(obj): - int_mat = MaterialUtils.build_default_mat('inside', (0.5, 1.0, 1.0)) - out_mat = MaterialUtils.build_default_mat('outside', (0.5, 1.0, 0.5)) - oth_mat = MaterialUtils.build_default_mat('cuts', (1.0, 0.2, 0.2)) - alt1_mat = MaterialUtils.build_default_mat('wall_alternative1', (1.0, 0.2, 0.2)) - alt2_mat = MaterialUtils.build_default_mat('wall_alternative2', (1.0, 0.2, 0.2)) - alt3_mat = MaterialUtils.build_default_mat('wall_alternative3', (1.0, 0.2, 0.2)) - alt4_mat = MaterialUtils.build_default_mat('wall_alternative4', (1.0, 0.2, 0.2)) - alt5_mat = MaterialUtils.build_default_mat('wall_alternative5', (1.0, 0.2, 0.2)) - obj.data.materials.append(out_mat) - obj.data.materials.append(int_mat) - obj.data.materials.append(oth_mat) - obj.data.materials.append(alt1_mat) - obj.data.materials.append(alt2_mat) - obj.data.materials.append(alt3_mat) - obj.data.materials.append(alt4_mat) - obj.data.materials.append(alt5_mat) - - @staticmethod - def add_wall_materials(obj): - int_mat = MaterialUtils.build_default_mat('inside', (0.5, 1.0, 1.0)) - out_mat = MaterialUtils.build_default_mat('outside', (0.5, 1.0, 0.5)) - oth_mat = MaterialUtils.build_default_mat('cuts', (1.0, 0.2, 0.2)) - obj.data.materials.append(out_mat) - obj.data.materials.append(int_mat) - obj.data.materials.append(oth_mat) - - @staticmethod - def add_slab_materials(obj): - out_mat = MaterialUtils.build_default_mat('Slab_bottom', (0.5, 1.0, 1.0)) - int_mat = MaterialUtils.build_default_mat('Slab_top', (1.0, 0.2, 0.2)) - oth_mat = MaterialUtils.build_default_mat('Slab_side', (0.5, 1.0, 0.5)) - obj.data.materials.append(out_mat) - obj.data.materials.append(int_mat) - obj.data.materials.append(oth_mat) - - @staticmethod - def add_stair_materials(obj): - cei_mat = MaterialUtils.build_default_mat('Stair_ceiling', (0.5, 1.0, 1.0)) - whi_mat = MaterialUtils.build_default_mat('Stair_white', (1.0, 1.0, 1.0)) - con_mat = MaterialUtils.build_default_mat('Stair_concrete', (0.5, 0.5, 0.5)) - wood_mat = MaterialUtils.build_default_mat('Stair_wood', (0.28, 0.2, 0.1)) - metal_mat = MaterialUtils.build_default_mat('Stair_metal', (0.4, 0.4, 0.4)) - glass_mat = MaterialUtils.build_default_mat('Stair_glass', (0.2, 0.2, 0.2)) - glass_mat.use_transparency = True - glass_mat.alpha = 0.5 - glass_mat.game_settings.alpha_blend = 'ADD' - obj.data.materials.append(cei_mat) - obj.data.materials.append(whi_mat) - obj.data.materials.append(con_mat) - obj.data.materials.append(wood_mat) - obj.data.materials.append(metal_mat) - obj.data.materials.append(glass_mat) - - @staticmethod - def add_fence_materials(obj): - wood_mat = MaterialUtils.build_default_mat('Fence_wood', (0.28, 0.2, 0.1)) - metal_mat = MaterialUtils.build_default_mat('Fence_metal', (0.4, 0.4, 0.4)) - glass_mat = MaterialUtils.build_default_mat('Fence_glass', (0.2, 0.2, 0.2)) - glass_mat.use_transparency = True - glass_mat.alpha = 0.5 - glass_mat.game_settings.alpha_blend = 'ADD' - obj.data.materials.append(wood_mat) - obj.data.materials.append(metal_mat) - obj.data.materials.append(glass_mat) - - @staticmethod - def add_floor_materials(obj): - con_mat = MaterialUtils.build_default_mat('Floor_grout', (0.5, 0.5, 0.5)) - alt1_mat = MaterialUtils.build_default_mat('Floor_alt1', (0.5, 1.0, 1.0)) - alt2_mat = MaterialUtils.build_default_mat('Floor_alt2', (1.0, 1.0, 1.0)) - alt3_mat = MaterialUtils.build_default_mat('Floor_alt3', (0.28, 0.2, 0.1)) - alt4_mat = MaterialUtils.build_default_mat('Floor_alt4', (0.5, 1.0, 1.0)) - alt5_mat = MaterialUtils.build_default_mat('Floor_alt5', (1.0, 1.0, 0.5)) - alt6_mat = MaterialUtils.build_default_mat('Floor_alt6', (0.28, 0.5, 0.1)) - alt7_mat = MaterialUtils.build_default_mat('Floor_alt7', (0.5, 1.0, 0.5)) - alt8_mat = MaterialUtils.build_default_mat('Floor_alt8', (1.0, 0.2, 1.0)) - alt9_mat = MaterialUtils.build_default_mat('Floor_alt9', (0.28, 0.2, 0.5)) - alt10_mat = MaterialUtils.build_default_mat('Floor_alt10', (0.5, 0.2, 0.1)) - obj.data.materials.append(con_mat) - obj.data.materials.append(alt1_mat) - obj.data.materials.append(alt2_mat) - obj.data.materials.append(alt3_mat) - obj.data.materials.append(alt4_mat) - obj.data.materials.append(alt5_mat) - obj.data.materials.append(alt6_mat) - obj.data.materials.append(alt7_mat) - obj.data.materials.append(alt8_mat) - obj.data.materials.append(alt9_mat) - obj.data.materials.append(alt10_mat) - - @staticmethod - def add_handle_materials(obj): - metal_mat = MaterialUtils.build_default_mat('metal', (0.4, 0.4, 0.4)) - obj.data.materials.append(metal_mat) - - @staticmethod - def add_door_materials(obj): - int_mat = MaterialUtils.build_default_mat('door_inside', (0.7, 0.2, 0.2)) - out_mat = MaterialUtils.build_default_mat('door_outside', (0.7, 0.2, 0.7)) - glass_mat = MaterialUtils.build_default_mat('glass', (0.2, 0.2, 0.2)) - metal_mat = MaterialUtils.build_default_mat('metal', (0.4, 0.4, 0.4)) - glass_mat.use_transparency = True - glass_mat.alpha = 0.5 - glass_mat.game_settings.alpha_blend = 'ADD' - obj.data.materials.append(out_mat) - obj.data.materials.append(int_mat) - obj.data.materials.append(glass_mat) - obj.data.materials.append(metal_mat) - - @staticmethod - def add_window_materials(obj): - int_mat = MaterialUtils.build_default_mat('window_inside', (0.7, 0.2, 0.2)) - out_mat = MaterialUtils.build_default_mat('window_outside', (0.7, 0.2, 0.7)) - glass_mat = MaterialUtils.build_default_mat('glass', (0.2, 0.2, 0.2)) - metal_mat = MaterialUtils.build_default_mat('metal', (0.4, 0.4, 0.4)) - tablet_mat = MaterialUtils.build_default_mat('tablet', (0.2, 0.2, 0.2)) - blind_mat = MaterialUtils.build_default_mat('blind', (0.2, 0.0, 0.0)) - glass_mat.use_transparency = True - glass_mat.alpha = 0.5 - glass_mat.game_settings.alpha_blend = 'ADD' - obj.data.materials.append(out_mat) - obj.data.materials.append(int_mat) - obj.data.materials.append(glass_mat) - obj.data.materials.append(metal_mat) - obj.data.materials.append(tablet_mat) - obj.data.materials.append(blind_mat) diff --git a/archipack/presets/archipack_floor/boards_200x20.png b/archipack/presets/archipack_floor/boards_200x20.png new file mode 100644 index 00000000..86727dae Binary files /dev/null and b/archipack/presets/archipack_floor/boards_200x20.png differ diff --git a/archipack/presets/archipack_floor/boards_200x20.py b/archipack/presets/archipack_floor/boards_200x20.py new file mode 100644 index 00000000..0ec933a5 --- /dev/null +++ b/archipack/presets/archipack_floor/boards_200x20.py @@ -0,0 +1,30 @@ +import bpy +d = bpy.context.active_object.data.archipack_floor[0] +d.add_grout = False +d.bevel = False +d.bevel_amount = 0.001 +d.board_length = 2.0 +d.board_width = 0.2 +d.boards_in_group = 5 +d.length_spacing = 0.002 +d.length_variance = 50 +d.matid = 7 +d.max_boards = 20 +d.mortar_depth = 0.001 +d.offset = 50.0 +d.offset_variance = 50 +d.pattern = 'boards' +d.random_offset = True +d.random_uvs = True +d.short_board_length = 0.15 +d.spacing = 0.0 +d.thickness = 0.02 +d.thickness_variance = 25.0 +d.tile_length = 0.3 +d.tile_width = 0.2 +d.vary_length = False +d.vary_materials = True +d.vary_thickness = False +d.vary_width = False +d.width_spacing = 0.002 +d.width_variance = 50.0 diff --git a/archipack/presets/archipack_floor/herringbone_50x10.png b/archipack/presets/archipack_floor/herringbone_50x10.png index b6e7fe56..f5d5b0ef 100644 Binary files a/archipack/presets/archipack_floor/herringbone_50x10.png and b/archipack/presets/archipack_floor/herringbone_50x10.png differ diff --git a/archipack/presets/archipack_floor/herringbone_50x10.py b/archipack/presets/archipack_floor/herringbone_50x10.py index a1f196ef..33acfa23 100644 --- a/archipack/presets/archipack_floor/herringbone_50x10.py +++ b/archipack/presets/archipack_floor/herringbone_50x10.py @@ -1,34 +1,30 @@ 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.add_grout = False +d.bevel = False +d.bevel_amount = 0.001 +d.board_length = 2.0 +d.board_width = 0.1 +d.boards_in_group = 4 +d.length_spacing = 0.002 +d.length_variance = 50.0 +d.matid = 7 +d.max_boards = 20 +d.mortar_depth = 0.001 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 +d.offset_variance = 50.0 +d.pattern = 'herringbone' +d.random_offset = False +d.random_uvs = True +d.short_board_length = 0.5 +d.spacing = 0.0 +d.thickness = 0.02 +d.thickness_variance = 25.0 +d.tile_length = 0.3 +d.tile_width = 0.2 +d.vary_length = False +d.vary_materials = True +d.vary_thickness = False +d.vary_width = False +d.width_spacing = 0.002 +d.width_variance = 50.0 diff --git a/archipack/presets/archipack_floor/herringbone_p_50x10.png b/archipack/presets/archipack_floor/herringbone_p_50x10.png index 1a2b2370..75c9238a 100644 Binary files a/archipack/presets/archipack_floor/herringbone_p_50x10.png and b/archipack/presets/archipack_floor/herringbone_p_50x10.png differ diff --git a/archipack/presets/archipack_floor/herringbone_p_50x10.py b/archipack/presets/archipack_floor/herringbone_p_50x10.py index 088a22e4..03a68ec9 100644 --- a/archipack/presets/archipack_floor/herringbone_p_50x10.py +++ b/archipack/presets/archipack_floor/herringbone_p_50x10.py @@ -1,34 +1,30 @@ 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.add_grout = False +d.bevel = False +d.bevel_amount = 0.001 +d.board_length = 2.0 +d.board_width = 0.1 +d.boards_in_group = 4 +d.length_spacing = 0.002 +d.length_variance = 50.0 +d.matid = 7 +d.max_boards = 20 +d.mortar_depth = 0.001 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 +d.offset_variance = 50.0 +d.pattern = 'herringbone_parquet' +d.random_offset = False +d.random_uvs = True +d.short_board_length = 0.5 +d.spacing = 0.0 +d.thickness = 0.02 +d.thickness_variance = 25.0 +d.tile_length = 0.3 +d.tile_width = 0.2 +d.vary_length = False +d.vary_materials = True +d.vary_thickness = False +d.vary_width = False +d.width_spacing = 0.002 +d.width_variance = 50.0 diff --git a/archipack/presets/archipack_floor/hexagon_10.png b/archipack/presets/archipack_floor/hexagon_10.png new file mode 100644 index 00000000..f0e39743 Binary files /dev/null and b/archipack/presets/archipack_floor/hexagon_10.png differ diff --git a/archipack/presets/archipack_floor/hexagon_10.py b/archipack/presets/archipack_floor/hexagon_10.py new file mode 100644 index 00000000..d8db8bef --- /dev/null +++ b/archipack/presets/archipack_floor/hexagon_10.py @@ -0,0 +1,30 @@ +import bpy +d = bpy.context.active_object.data.archipack_floor[0] +d.add_grout = True +d.bevel = True +d.bevel_amount = 0.0015 +d.board_length = 2.0 +d.board_width = 0.2 +d.boards_in_group = 5 +d.length_spacing = 0.002 +d.length_variance = 50 +d.matid = 7 +d.max_boards = 20 +d.mortar_depth = 0.0015 +d.offset = 0.0 +d.offset_variance = 50 +d.pattern = 'hexagon' +d.random_offset = False +d.random_uvs = True +d.short_board_length = 0.15 +d.spacing = 0.005 +d.thickness = 0.1 +d.thickness_variance = 25.0 +d.tile_length = 0.3 +d.tile_width = 0.1 +d.vary_length = False +d.vary_materials = False +d.vary_thickness = False +d.vary_width = False +d.width_spacing = 0.002 +d.width_variance = 50.0 diff --git a/archipack/presets/archipack_floor/hopscotch_30x30.png b/archipack/presets/archipack_floor/hopscotch_30x30.png new file mode 100644 index 00000000..f64c890b Binary files /dev/null and b/archipack/presets/archipack_floor/hopscotch_30x30.png differ diff --git a/archipack/presets/archipack_floor/hopscotch_30x30.py b/archipack/presets/archipack_floor/hopscotch_30x30.py new file mode 100644 index 00000000..189c9b48 --- /dev/null +++ b/archipack/presets/archipack_floor/hopscotch_30x30.py @@ -0,0 +1,30 @@ +import bpy +d = bpy.context.active_object.data.archipack_floor[0] +d.add_grout = True +d.bevel = True +d.bevel_amount = 0.0015 +d.board_length = 2.0 +d.board_width = 0.2 +d.boards_in_group = 5 +d.length_spacing = 0.002 +d.length_variance = 50 +d.matid = 7 +d.max_boards = 20 +d.mortar_depth = 0.0015 +d.offset = 0.0 +d.offset_variance = 50 +d.pattern = 'hopscotch' +d.random_offset = False +d.random_uvs = True +d.short_board_length = 0.15 +d.spacing = 0.005 +d.thickness = 0.1 +d.thickness_variance = 25.0 +d.tile_length = 0.3 +d.tile_width = 0.3 +d.vary_length = False +d.vary_materials = False +d.vary_thickness = False +d.vary_width = False +d.width_spacing = 0.002 +d.width_variance = 50.0 diff --git a/archipack/presets/archipack_floor/parquet_15x3.png b/archipack/presets/archipack_floor/parquet_15x3.png index 2b35d58b..22e10310 100644 Binary files a/archipack/presets/archipack_floor/parquet_15x3.png and b/archipack/presets/archipack_floor/parquet_15x3.png differ diff --git a/archipack/presets/archipack_floor/parquet_15x3.py b/archipack/presets/archipack_floor/parquet_15x3.py index 5711c93a..2e62961a 100644 --- a/archipack/presets/archipack_floor/parquet_15x3.py +++ b/archipack/presets/archipack_floor/parquet_15x3.py @@ -1,34 +1,30 @@ 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.add_grout = False +d.bevel = False +d.bevel_amount = 0.001 +d.board_length = 2.0 +d.board_width = 0.1 +d.boards_in_group = 5 +d.length_spacing = 0.002 +d.length_variance = 50.0 +d.matid = 7 +d.max_boards = 20 +d.mortar_depth = 0.001 d.offset = 50.0 -d.b_length_s = 2.0 +d.offset_variance = 50.0 +d.pattern = 'square_parquet' +d.random_offset = False +d.random_uvs = True +d.short_board_length = 0.15 +d.spacing = 0.0 +d.thickness = 0.02 +d.thickness_variance = 25.0 +d.tile_length = 0.3 +d.tile_width = 0.2 +d.vary_length = False +d.vary_materials = True +d.vary_thickness = False +d.vary_width = False +d.width_spacing = 0.002 +d.width_variance = 50.0 diff --git a/archipack/presets/archipack_floor/planks_200x20.png b/archipack/presets/archipack_floor/planks_200x20.png deleted file mode 100644 index 94a49c57..00000000 Binary files a/archipack/presets/archipack_floor/planks_200x20.png and /dev/null differ diff --git a/archipack/presets/archipack_floor/planks_200x20.py b/archipack/presets/archipack_floor/planks_200x20.py deleted file mode 100644 index bbea2e66..00000000 --- a/archipack/presets/archipack_floor/planks_200x20.py +++ /dev/null @@ -1,34 +0,0 @@ -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/stepping_stone_30x30.png b/archipack/presets/archipack_floor/stepping_stone_30x30.png new file mode 100644 index 00000000..862e5c1d Binary files /dev/null and b/archipack/presets/archipack_floor/stepping_stone_30x30.png differ diff --git a/archipack/presets/archipack_floor/stepping_stone_30x30.py b/archipack/presets/archipack_floor/stepping_stone_30x30.py new file mode 100644 index 00000000..db85715d --- /dev/null +++ b/archipack/presets/archipack_floor/stepping_stone_30x30.py @@ -0,0 +1,30 @@ +import bpy +d = bpy.context.active_object.data.archipack_floor[0] +d.add_grout = True +d.bevel = True +d.bevel_amount = 0.0015 +d.board_length = 2.0 +d.board_width = 0.2 +d.boards_in_group = 5 +d.length_spacing = 0.002 +d.length_variance = 50 +d.matid = 7 +d.max_boards = 20 +d.mortar_depth = 0.0015 +d.offset = 0.0 +d.offset_variance = 50 +d.pattern = 'stepping_stone' +d.random_offset = False +d.random_uvs = True +d.short_board_length = 0.15 +d.spacing = 0.005 +d.thickness = 0.1 +d.thickness_variance = 25.0 +d.tile_length = 0.3 +d.tile_width = 0.3 +d.vary_length = False +d.vary_materials = False +d.vary_thickness = False +d.vary_width = False +d.width_spacing = 0.002 +d.width_variance = 50.0 diff --git a/archipack/presets/archipack_floor/tile_30x60.png b/archipack/presets/archipack_floor/tile_30x60.png new file mode 100644 index 00000000..a1921b22 Binary files /dev/null and b/archipack/presets/archipack_floor/tile_30x60.png differ diff --git a/archipack/presets/archipack_floor/tile_30x60.py b/archipack/presets/archipack_floor/tile_30x60.py new file mode 100644 index 00000000..af92cd68 --- /dev/null +++ b/archipack/presets/archipack_floor/tile_30x60.py @@ -0,0 +1,30 @@ +import bpy +d = bpy.context.active_object.data.archipack_floor[0] +d.add_grout = True +d.bevel = True +d.bevel_amount = 0.0015 +d.board_length = 2.0 +d.board_width = 0.2 +d.boards_in_group = 5 +d.length_spacing = 0.002 +d.length_variance = 50 +d.matid = 7 +d.max_boards = 20 +d.mortar_depth = 0.0015 +d.offset = 0.0 +d.offset_variance = 50 +d.pattern = 'regular_tile' +d.random_offset = False +d.random_uvs = True +d.short_board_length = 0.15 +d.spacing = 0.005 +d.thickness = 0.1 +d.thickness_variance = 25.0 +d.tile_length = 0.3 +d.tile_width = 0.6 +d.vary_length = False +d.vary_materials = False +d.vary_thickness = False +d.vary_width = False +d.width_spacing = 0.002 +d.width_variance = 50.0 diff --git a/archipack/presets/archipack_floor/tiles_15x15.png b/archipack/presets/archipack_floor/tiles_15x15.png deleted file mode 100644 index 2a3d8633..00000000 Binary files a/archipack/presets/archipack_floor/tiles_15x15.png and /dev/null differ diff --git a/archipack/presets/archipack_floor/tiles_15x15.py b/archipack/presets/archipack_floor/tiles_15x15.py deleted file mode 100644 index d3d244f9..00000000 --- a/archipack/presets/archipack_floor/tiles_15x15.py +++ /dev/null @@ -1,34 +0,0 @@ -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 deleted file mode 100644 index 16cdf0f1..00000000 Binary files a/archipack/presets/archipack_floor/tiles_60x30.png and /dev/null differ diff --git a/archipack/presets/archipack_floor/tiles_60x30.py b/archipack/presets/archipack_floor/tiles_60x30.py deleted file mode 100644 index f8b66129..00000000 --- a/archipack/presets/archipack_floor/tiles_60x30.py +++ /dev/null @@ -1,34 +0,0 @@ -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 deleted file mode 100644 index 4d4c8ecf..00000000 Binary files a/archipack/presets/archipack_floor/tiles_hex_10x10.png and /dev/null differ diff --git a/archipack/presets/archipack_floor/tiles_hex_10x10.py b/archipack/presets/archipack_floor/tiles_hex_10x10.py deleted file mode 100644 index 01086dc8..00000000 --- a/archipack/presets/archipack_floor/tiles_hex_10x10.py +++ /dev/null @@ -1,34 +0,0 @@ -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 deleted file mode 100644 index 07c6e266..00000000 Binary files a/archipack/presets/archipack_floor/tiles_l+ms_30x30_15x15.png and /dev/null 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 deleted file mode 100644 index 3ee45a2d..00000000 --- a/archipack/presets/archipack_floor/tiles_l+ms_30x30_15x15.py +++ /dev/null @@ -1,34 +0,0 @@ -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 deleted file mode 100644 index 33d28657..00000000 Binary files a/archipack/presets/archipack_floor/tiles_l+s_30x30_15x15.png and /dev/null 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 deleted file mode 100644 index 8f4253fe..00000000 --- a/archipack/presets/archipack_floor/tiles_l+s_30x30_15x15.py +++ /dev/null @@ -1,34 +0,0 @@ -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_floor/windmill_30x30.png b/archipack/presets/archipack_floor/windmill_30x30.png new file mode 100644 index 00000000..4fe5cb93 Binary files /dev/null and b/archipack/presets/archipack_floor/windmill_30x30.png differ diff --git a/archipack/presets/archipack_floor/windmill_30x30.py b/archipack/presets/archipack_floor/windmill_30x30.py new file mode 100644 index 00000000..8a690a1a --- /dev/null +++ b/archipack/presets/archipack_floor/windmill_30x30.py @@ -0,0 +1,30 @@ +import bpy +d = bpy.context.active_object.data.archipack_floor[0] +d.add_grout = True +d.bevel = True +d.bevel_amount = 0.0015 +d.board_length = 2.0 +d.board_width = 0.2 +d.boards_in_group = 5 +d.length_spacing = 0.002 +d.length_variance = 50 +d.matid = 7 +d.max_boards = 20 +d.mortar_depth = 0.0015 +d.offset = 50.0 +d.offset_variance = 50 +d.pattern = 'windmill' +d.random_offset = True +d.random_uvs = True +d.short_board_length = 0.15 +d.spacing = 0.005 +d.thickness = 0.1 +d.thickness_variance = 25.0 +d.tile_length = 0.3 +d.tile_width = 0.3 +d.vary_length = False +d.vary_materials = False +d.vary_thickness = False +d.vary_width = False +d.width_spacing = 0.002 +d.width_variance = 50.0 diff --git a/archipack/presets/archipack_materials/door.txt b/archipack/presets/archipack_materials/door.txt new file mode 100644 index 00000000..18951498 --- /dev/null +++ b/archipack/presets/archipack_materials/door.txt @@ -0,0 +1,4 @@ +DEFAULT##|##Door_inside +DEFAULT##|##Door_outside +DEFAULT##|##Door_glass +DEFAULT##|##Door_metal diff --git a/archipack/presets/archipack_materials/fence.txt b/archipack/presets/archipack_materials/fence.txt new file mode 100644 index 00000000..00827582 --- /dev/null +++ b/archipack/presets/archipack_materials/fence.txt @@ -0,0 +1,4 @@ +DEFAULT##|##Fence_wood +DEFAULT##|##Fence_metal +DEFAULT##|##Fence_glass +DEFAULT##|##Fence_concrete diff --git a/archipack/presets/archipack_materials/floor.txt b/archipack/presets/archipack_materials/floor.txt new file mode 100644 index 00000000..e04180a6 --- /dev/null +++ b/archipack/presets/archipack_materials/floor.txt @@ -0,0 +1,11 @@ +DEFAULT##|##Floor_grout +DEFAULT##|##Floor_alt1 +DEFAULT##|##Floor_alt2 +DEFAULT##|##Floor_alt3 +DEFAULT##|##Floor_alt4 +DEFAULT##|##Floor_alt5 +DEFAULT##|##Floor_alt6 +DEFAULT##|##Floor_alt7 +DEFAULT##|##Floor_alt8 +DEFAULT##|##Floor_alt9 +DEFAULT##|##Floor_alt10 diff --git a/archipack/presets/archipack_materials/handle.txt b/archipack/presets/archipack_materials/handle.txt new file mode 100644 index 00000000..458cb1c2 --- /dev/null +++ b/archipack/presets/archipack_materials/handle.txt @@ -0,0 +1,2 @@ +DEFAULT##|##Handle_inside +DEFAULT##|##Handle_outside diff --git a/archipack/presets/archipack_materials/roof.txt b/archipack/presets/archipack_materials/roof.txt new file mode 100644 index 00000000..84e6394e --- /dev/null +++ b/archipack/presets/archipack_materials/roof.txt @@ -0,0 +1,12 @@ +DEFAULT##|##Roof_sheeting +DEFAULT##|##Roof_rakes +DEFAULT##|##Roof_eaves +DEFAULT##|##Roof_ridge +DEFAULT##|##Roof_rafter +DEFAULT##|##Roof_valley +DEFAULT##|##Roof_hip_tiles +DEFAULT##|##Roof_tiles +DEFAULT##|##Roof_tiles2 +DEFAULT##|##Roof_tiles3 +DEFAULT##|##Roof_tiles4 +DEFAULT##|##Roof_tiles5 diff --git a/archipack/presets/archipack_materials/slab.txt b/archipack/presets/archipack_materials/slab.txt new file mode 100644 index 00000000..8d3490fe --- /dev/null +++ b/archipack/presets/archipack_materials/slab.txt @@ -0,0 +1,3 @@ +DEFAULT##|##Slab_bottom +DEFAULT##|##Slab_top +DEFAULT##|##Slab_side diff --git a/archipack/presets/archipack_materials/stair.txt b/archipack/presets/archipack_materials/stair.txt new file mode 100644 index 00000000..44966d35 --- /dev/null +++ b/archipack/presets/archipack_materials/stair.txt @@ -0,0 +1,6 @@ +DEFAULT##|##Stair_ceiling +DEFAULT##|##Stair_white +DEFAULT##|##Stair_concrete +DEFAULT##|##Stair_wood +DEFAULT##|##Stair_metal +DEFAULT##|##Stair_glass diff --git a/archipack/presets/archipack_materials/truss.txt b/archipack/presets/archipack_materials/truss.txt new file mode 100644 index 00000000..00718d4b --- /dev/null +++ b/archipack/presets/archipack_materials/truss.txt @@ -0,0 +1 @@ +DEFAULT##|##Truss_truss diff --git a/archipack/presets/archipack_materials/wall2.txt b/archipack/presets/archipack_materials/wall2.txt new file mode 100644 index 00000000..789c285d --- /dev/null +++ b/archipack/presets/archipack_materials/wall2.txt @@ -0,0 +1,8 @@ +DEFAULT##|##Wall2_inside +DEFAULT##|##Wall2_outside +DEFAULT##|##Wall2_cuts +DEFAULT##|##Wall2_alt1 +DEFAULT##|##Wall2_alt2 +DEFAULT##|##Wall2_alt3 +DEFAULT##|##Wall2_alt4 +DEFAULT##|##Wall2_alt5 diff --git a/archipack/presets/archipack_materials/window.txt b/archipack/presets/archipack_materials/window.txt new file mode 100644 index 00000000..8f5f8575 --- /dev/null +++ b/archipack/presets/archipack_materials/window.txt @@ -0,0 +1,6 @@ +DEFAULT##|##Window_inside +DEFAULT##|##Window_outside +DEFAULT##|##Window_glass +DEFAULT##|##Window_metal +DEFAULT##|##Window_stone +DEFAULT##|##Window_blind diff --git a/archipack/presets/archipack_roof/braas_1.png b/archipack/presets/archipack_roof/braas_1.png new file mode 100644 index 00000000..98d831f3 Binary files /dev/null and b/archipack/presets/archipack_roof/braas_1.png differ diff --git a/archipack/presets/archipack_roof/braas_1.py b/archipack/presets/archipack_roof/braas_1.py new file mode 100644 index 00000000..5ba9e6c6 --- /dev/null +++ b/archipack/presets/archipack_roof/braas_1.py @@ -0,0 +1,30 @@ +import bpy +d = bpy.context.active_object.data.archipack_roof[0] +d.tile_model = 'BRAAS1' +d.tile_size_z = 0.05 +d.tile_border = 0.0 +d.tile_space_x = 0.205 +d.tile_couloir = 0.05 +d.hip_size_x = 0.42 +d.tile_altitude = 0.1 +d.tile_fit_y = True +d.tile_side = 0.0 +d.hip_space_x = 0.4 +d.tile_size_x = 0.2 +d.tile_size_y = 0.32 +d.tile_offset = 0.0 +d.tile_bevel_amt = 25.0 +d.hip_size_z = 0.18 +d.tile_solidify = True +d.tile_height = 0.02 +d.tile_bevel = True +d.hip_model = 'ROUND' +d.tile_space_y = 0.3 +d.hip_enable = True +d.hip_size_y = 0.18 +d.tile_enable = True +d.tile_alternate = False +d.hip_alt = 0.13 +d.tile_bevel_segs = 2 +d.tile_fit_x = False +d.valley_enable = True \ No newline at end of file diff --git a/archipack/presets/archipack_roof/braas_2.png b/archipack/presets/archipack_roof/braas_2.png new file mode 100644 index 00000000..8db07470 Binary files /dev/null and b/archipack/presets/archipack_roof/braas_2.png differ diff --git a/archipack/presets/archipack_roof/braas_2.py b/archipack/presets/archipack_roof/braas_2.py new file mode 100644 index 00000000..bd573c8e --- /dev/null +++ b/archipack/presets/archipack_roof/braas_2.py @@ -0,0 +1,30 @@ +import bpy +d = bpy.context.active_object.data.archipack_roof[0] +d.tile_model = 'BRAAS2' +d.tile_size_z = 0.05 +d.tile_border = 0.0 +d.tile_space_x = 0.205 +d.tile_couloir = 0.05 +d.hip_size_x = 0.42 +d.tile_altitude = 0.1 +d.tile_fit_y = True +d.tile_side = 0.0 +d.hip_space_x = 0.4 +d.tile_size_x = 0.2 +d.tile_size_y = 0.32 +d.tile_offset = 0.0 +d.tile_bevel_amt = 25.0 +d.hip_size_z = 0.18 +d.tile_solidify = True +d.tile_height = 0.02 +d.tile_bevel = True +d.hip_model = 'ROUND' +d.tile_space_y = 0.3 +d.hip_enable = True +d.hip_size_y = 0.18 +d.tile_enable = True +d.tile_alternate = False +d.hip_alt = 0.13 +d.tile_bevel_segs = 2 +d.tile_fit_x = False +d.valley_enable = True \ No newline at end of file diff --git a/archipack/presets/archipack_roof/eternit.png b/archipack/presets/archipack_roof/eternit.png new file mode 100644 index 00000000..874901f5 Binary files /dev/null and b/archipack/presets/archipack_roof/eternit.png differ diff --git a/archipack/presets/archipack_roof/eternit.py b/archipack/presets/archipack_roof/eternit.py new file mode 100644 index 00000000..033cbf11 --- /dev/null +++ b/archipack/presets/archipack_roof/eternit.py @@ -0,0 +1,30 @@ +import bpy +d = bpy.context.active_object.data.archipack_roof[0] +d.tile_model = 'ETERNIT' +d.tile_size_z = 0.01 +d.tile_border = 0.0 +d.tile_space_x = 0.41 +d.tile_couloir = 0.05 +d.hip_size_x = 0.4 +d.tile_altitude = 0.1 +d.tile_fit_y = False +d.tile_side = 0.0 +d.hip_space_x = 0.4 +d.tile_size_x = 0.4 +d.tile_size_y = 0.2 +d.tile_offset = 0.0 +d.tile_bevel_amt = 50.0 +d.hip_size_z = 0.01 +d.tile_solidify = True +d.tile_height = 0.004 +d.tile_bevel = False +d.hip_model = 'ETERNIT' +d.tile_space_y = 0.2 +d.hip_enable = True +d.hip_size_y = 0.3 +d.tile_enable = True +d.tile_alternate = True +d.hip_alt = 0.12 +d.tile_bevel_segs = 3 +d.tile_fit_x = False +d.valley_enable = True \ No newline at end of file diff --git a/archipack/presets/archipack_roof/lauze.png b/archipack/presets/archipack_roof/lauze.png new file mode 100644 index 00000000..925f46bf Binary files /dev/null and b/archipack/presets/archipack_roof/lauze.png differ diff --git a/archipack/presets/archipack_roof/lauze.py b/archipack/presets/archipack_roof/lauze.py new file mode 100644 index 00000000..987d55bc --- /dev/null +++ b/archipack/presets/archipack_roof/lauze.py @@ -0,0 +1,30 @@ +import bpy +d = bpy.context.active_object.data.archipack_roof[0] +d.tile_model = 'LAUZE' +d.tile_size_z = 0.04 +d.tile_border = 0.0 +d.tile_space_x = 0.61 +d.tile_couloir = 0.05 +d.hip_size_x = 0.42 +d.tile_altitude = 0.1 +d.tile_fit_y = False +d.tile_side = 0.0 +d.hip_space_x = 0.4 +d.tile_size_x = 0.6 +d.tile_size_y = 0.6 +d.tile_offset = 0.0 +d.tile_bevel_amt = 50.0 +d.hip_size_z = 0.06 +d.tile_solidify = True +d.tile_height = 0.02 +d.tile_bevel = False +d.hip_model = 'FLAT' +d.tile_space_y = 0.3 +d.hip_enable = True +d.hip_size_y = 0.15 +d.tile_enable = True +d.tile_alternate = True +d.hip_alt = 0.13 +d.tile_bevel_segs = 3 +d.tile_fit_x = False +d.valley_enable = True \ No newline at end of file diff --git a/archipack/presets/archipack_roof/metal.png b/archipack/presets/archipack_roof/metal.png new file mode 100644 index 00000000..dbcc7fee Binary files /dev/null and b/archipack/presets/archipack_roof/metal.png differ diff --git a/archipack/presets/archipack_roof/metal.py b/archipack/presets/archipack_roof/metal.py new file mode 100644 index 00000000..33d35f66 --- /dev/null +++ b/archipack/presets/archipack_roof/metal.py @@ -0,0 +1,30 @@ +import bpy +d = bpy.context.active_object.data.archipack_roof[0] +d.tile_side = 0.0 +d.hip_alt = 0.07 +d.tile_fit_y = False +d.tile_space_y = 2.2 +d.tile_size_z = 0.05 +d.hip_size_z = 0.18 +d.tile_space_x = 1.0 +d.hip_size_x = 0.4 +d.hip_space_x = 0.4 +d.tile_enable = True +d.tile_size_x = 1.0 +d.tile_border = 0.0 +d.tile_bevel = False +d.tile_bevel_amt = 25.0 +d.tile_solidify = False +d.tile_model = 'METAL' +d.hip_size_y = 0.18 +d.tile_height = 0.02 +d.tile_alternate = False +d.tile_couloir = 0.0 +d.valley_enable = False +d.tile_size_y = 2.5 +d.tile_altitude = 0.1 +d.tile_fit_x = False +d.hip_model = 'ROUND' +d.hip_enable = False +d.tile_bevel_segs = 3 +d.tile_offset = 0.0 diff --git a/archipack/presets/archipack_roof/ondule.png b/archipack/presets/archipack_roof/ondule.png new file mode 100644 index 00000000..d3d99ac4 Binary files /dev/null and b/archipack/presets/archipack_roof/ondule.png differ diff --git a/archipack/presets/archipack_roof/ondule.py b/archipack/presets/archipack_roof/ondule.py new file mode 100644 index 00000000..68be8fa4 --- /dev/null +++ b/archipack/presets/archipack_roof/ondule.py @@ -0,0 +1,29 @@ +import bpy +d = bpy.context.active_object.data.archipack_roof[0] +d.tile_side = 0.0 +d.hip_alt = 0.07 +d.tile_fit_y = False +d.tile_space_y = 2.2 +d.tile_size_z = 0.05 +d.hip_size_z = 0.18 +d.tile_space_x = 1.0 +d.hip_size_x = 0.4 +d.hip_space_x = 0.4 +d.tile_enable = True +d.tile_size_x = 1.0 +d.tile_border = 0.0 +d.tile_bevel = True +d.tile_bevel_amt = 25.0 +d.tile_solidify = False +d.tile_model = 'ONDULEE' +d.tile_height = 0.02 +d.tile_alternate = False +d.tile_couloir = 0 +d.valley_enable = False +d.tile_size_y = 2.5 +d.tile_altitude = 0.1 +d.tile_fit_x = False +d.hip_model = 'ROUND' +d.hip_enable = True +d.tile_bevel_segs = 3 +d.tile_offset = 0.0 diff --git a/archipack/presets/archipack_roof/roman.png b/archipack/presets/archipack_roof/roman.png new file mode 100644 index 00000000..20615e27 Binary files /dev/null and b/archipack/presets/archipack_roof/roman.png differ diff --git a/archipack/presets/archipack_roof/roman.py b/archipack/presets/archipack_roof/roman.py new file mode 100644 index 00000000..6f3849dd --- /dev/null +++ b/archipack/presets/archipack_roof/roman.py @@ -0,0 +1,29 @@ +import bpy +d = bpy.context.active_object.data.archipack_roof[0] +d.tile_model = 'ROMAN' +d.tile_size_z = 0.16 +d.tile_border = 0.0 +d.tile_space_x = 0.2 +d.tile_couloir = 0.05 +d.hip_size_x = 0.42 +d.tile_altitude = 0.07 +d.tile_fit_y = True +d.tile_side = 0.0 +d.hip_space_x = 0.4 +d.tile_size_x = 0.2 +d.tile_size_y = 0.3 +d.tile_offset = 0.0 +d.tile_bevel_amt = 50.0 +d.hip_size_z = 0.18 +d.tile_solidify = True +d.tile_height = 0.02 +d.tile_bevel = True +d.hip_model = 'ROUND' +d.tile_space_y = 0.28 +d.hip_enable = True +d.hip_size_y = 0.18 +d.tile_enable = True +d.tile_alternate = False +d.hip_alt = 0.16 +d.tile_bevel_segs = 3 +d.tile_fit_x = False diff --git a/archipack/presets/archipack_roof/round.png b/archipack/presets/archipack_roof/round.png new file mode 100644 index 00000000..9bd57982 Binary files /dev/null and b/archipack/presets/archipack_roof/round.png differ diff --git a/archipack/presets/archipack_roof/round.py b/archipack/presets/archipack_roof/round.py new file mode 100644 index 00000000..8da9a049 --- /dev/null +++ b/archipack/presets/archipack_roof/round.py @@ -0,0 +1,30 @@ +import bpy +d = bpy.context.active_object.data.archipack_roof[0] +d.tile_model = 'ROUND' +d.tile_size_z = 0.02 +d.tile_border = 0.0 +d.tile_space_x = 0.105 +d.tile_couloir = 0.05 +d.hip_size_x = 0.42 +d.tile_altitude = 0.1 +d.tile_fit_y = False +d.tile_side = 0.0 +d.hip_space_x = 0.4 +d.tile_size_x = 0.1 +d.tile_size_y = 0.15 +d.tile_offset = 0.0 +d.tile_bevel_amt = 50.0 +d.hip_size_z = 0.15 +d.tile_solidify = True +d.tile_height = 0.02 +d.tile_bevel = False +d.hip_model = 'ROUND' +d.tile_space_y = 0.07 +d.hip_enable = True +d.hip_size_y = 0.15 +d.tile_enable = True +d.tile_alternate = True +d.hip_alt = 0.1 +d.tile_bevel_segs = 3 +d.tile_fit_x = False +d.valley_enable = True \ No newline at end of file diff --git a/archipack/presets/archipack_roof/square.png b/archipack/presets/archipack_roof/square.png new file mode 100644 index 00000000..97b8d966 Binary files /dev/null and b/archipack/presets/archipack_roof/square.png differ diff --git a/archipack/presets/archipack_roof/square.py b/archipack/presets/archipack_roof/square.py new file mode 100644 index 00000000..a26a15a8 --- /dev/null +++ b/archipack/presets/archipack_roof/square.py @@ -0,0 +1,30 @@ +import bpy +d = bpy.context.active_object.data.archipack_roof[0] +d.tile_model = 'PLACEHOLDER' +d.tile_size_z = 0.01 +d.tile_border = 0.0 +d.tile_space_x = 0.401 +d.tile_couloir = 0.05 +d.hip_size_x = 0.4 +d.tile_altitude = 0.1 +d.tile_fit_y = False +d.tile_side = 0.0 +d.hip_space_x = 0.4 +d.tile_size_x = 0.4 +d.tile_size_y = 0.4 +d.tile_offset = 0.0 +d.tile_bevel_amt = 50.0 +d.hip_size_z = 0.01 +d.tile_solidify = True +d.tile_height = 0.004 +d.tile_bevel = False +d.hip_model = 'ETERNIT' +d.tile_space_y = 0.2 +d.hip_enable = True +d.hip_size_y = 0.3 +d.tile_enable = True +d.tile_alternate = True +d.hip_alt = 0.12 +d.tile_bevel_segs = 3 +d.tile_fit_x = False +d.valley_enable = True \ No newline at end of file diff --git a/archipack/presets/archipack_stair/u_wood_over_concrete.py b/archipack/presets/archipack_stair/u_wood_over_concrete.py index b523dcde..ba16dde5 100644 --- a/archipack/presets/archipack_stair/u_wood_over_concrete.py +++ b/archipack/presets/archipack_stair/u_wood_over_concrete.py @@ -1,6 +1,6 @@ import bpy d = bpy.context.active_object.data.archipack_stair[0] - +d.auto_update = False d.steps_type = 'CLOSED' d.handrail_slice_right = True d.total_angle = 6.2831854820251465 -- cgit v1.2.3