diff options
Diffstat (limited to 'archipack/archipack_polylib.py')
-rw-r--r-- | archipack/archipack_polylib.py | 2274 |
1 files changed, 2274 insertions, 0 deletions
diff --git a/archipack/archipack_polylib.py b/archipack/archipack_polylib.py new file mode 100644 index 00000000..886029ba --- /dev/null +++ b/archipack/archipack_polylib.py @@ -0,0 +1,2274 @@ +# -*- coding:utf-8 -*- + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110- 1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +# <pep8 compliant> + +# ---------------------------------------------------------- +# Author: Stephen Leger (s-leger) +# +# ---------------------------------------------------------- + +bl_info = { + 'name': 'PolyLib', + 'description': 'Polygons detection from unordered splines', + 'author': 's-leger', + 'license': 'GPL', + 'deps': 'shapely', + 'version': (1, 1), + 'blender': (2, 7, 8), + 'location': 'View3D > Tools > Polygons', + 'warning': '', + 'wiki_url': 'https://github.com/s-leger/blenderPolygons/wiki', + 'tracker_url': 'https://github.com/s-leger/blenderPolygons/issues', + 'link': 'https://github.com/s-leger/blenderPolygons', + 'support': 'COMMUNITY', + 'category': '3D View' + } + +import sys +import time +import bpy +import bgl +import numpy as np +from math import cos, sin, pi, atan2 +import bmesh + +# let shapely import raise ImportError when missing +import shapely.ops +import shapely.prepared +from shapely.geometry import Point as ShapelyPoint +from shapely.geometry import Polygon as ShapelyPolygon + +try: + import shapely.speedups + if shapely.speedups.available: + shapely.speedups.enable() +except: + pass + +from .bitarray import BitArray +from .pyqtree import _QuadTree +from mathutils import Vector, Matrix +from mathutils.geometry import intersect_line_plane, interpolate_bezier +from bpy_extras import view3d_utils +from bpy.types import Operator, PropertyGroup +from bpy.props import StringProperty, FloatProperty, PointerProperty, EnumProperty, IntProperty, BoolProperty +from bpy.app.handlers import persistent +from .materialutils import MaterialUtils +from .archipack_gl import ( + FeedbackPanel, + GlCursorFence, + GlCursorArea, + GlLine, + GlPolyline +) + +# module globals vars dict +vars_dict = { + # spacial tree for segments and points + 'seg_tree': None, + 'point_tree': None, + # keep track of shapely geometry selection sets + 'select_polygons': None, + 'select_lines': None, + 'select_points': None + } + + +# module constants +# precision 1e-4 = 0.1mm +EPSILON = 1.0e-4 +# Qtree params +MAX_ITEMS = 10 +MAX_DEPTH = 20 + + +class CoordSys(object): + """ + reference coordsys + world : matrix from local to world + invert: matrix from world to local + width, height: bonding region size + """ + def __init__(self, objs): + x = [] + y = [] + if len(objs) > 0: + if hasattr(objs[0], 'bound_box'): + for obj in objs: + pos = obj.location + x.append(obj.bound_box[0][0] + pos.x) + x.append(obj.bound_box[6][0] + pos.x) + y.append(obj.bound_box[0][1] + pos.y) + y.append(obj.bound_box[6][1] + pos.y) + elif hasattr(objs[0], 'bounds'): + for geom in objs: + x0, y0, x1, y1 = geom.bounds + x.append(x0) + x.append(x1) + y.append(y0) + y.append(y1) + else: + raise Exception("CoordSys require at least one object with bounds or bound_box property to initialize") + else: + raise Exception("CoordSys require at least one object to initialize bounds") + x0 = min(x) + y0 = min(y) + x1 = max(x) + y1 = max(y) + width, height = x1 - x0, y1 - y0 + midx, midy = x0 + width / 2.0, y0 + height / 2.0 + # reference coordsys bounding box center + self.world = Matrix([ + [1, 0, 0, midx], + [0, 1, 0, midy], + [0, 0, 1, 0], + [0, 0, 0, 1], + ]) + self.invert = self.world.inverted() + self.width = width + self.height = height + + +class Prolongement(): + """ intersection of two segments outside segment (projection) + c0 = extremite sur le segment courant + c1 = intersection point on oposite segment + id = oposite segment id + t = param t on oposite segment + d = distance from ends to segment + insert = do we need to insert the point on other segment + use id, c1 and t to insert segment slices + """ + def __init__(self, c0, c1, id, t, d): + self.length = c0.distance(c1) + self.c0 = c0 + self.c1 = c1 + self.id = id + self.t = t + self.d = d + + +class Point(): + + def __init__(self, co, precision=EPSILON): + self.users = 0 + self.co = tuple(co) + x, y, z = co + self.shapeIds = [] + self.bounds = (x - precision, y - precision, x + precision, y + precision) + + @property + def geom(self): + return ShapelyPoint(self.co) + + def vect(self, point): + """ vector from this point to another """ + return np.subtract(point.co, self.co) + + def distance(self, point): + """ euclidian distance between points """ + return np.linalg.norm(self.vect(point)) + + def add_user(self): + self.users += 1 + + +class Segment(): + + def __init__(self, c0, c1, extend=EPSILON): + + self.c0 = c0 + self.c1 = c1 + self._splits = [] + + self.available = True + # ensure uniqueness when merge + + self.opposite = False + # this seg has an opposite + + self.original = False + # source of opposite + + x0, y0, z0 = c0.co + x1, y1, z1 = c1.co + self.bounds = (min(x0, x1) - extend, min(y0, y1) - extend, max(x0, x1) + extend, max(y0, y1) + extend) + + @property + def splits(self): + return sorted(self._splits) + + @property + def vect(self): + """ vector c0-c1""" + return np.subtract(self.c1.co, self.c0.co) + + @property + def vect_2d(self): + v = self.vect + v[2] = 0 + return v + + def lerp(self, t): + return np.add(self.c0.co, np.multiply(t, self.vect)) + + def _point_sur_segment(self, point): + """ _point_sur_segment + point: Point + t: param t de l'intersection sur le segment courant + d: distance laterale perpendiculaire + """ + vect = self.vect + dp = point.vect(self.c0) + dl = np.linalg.norm(vect) + d = np.linalg.norm(np.cross(vect, dp)) / dl + t = -np.divide(np.dot(dp, vect), np.multiply(dl, dl)) + if d < EPSILON: + if t > 0 and t < 1: + self._append_splits((t, point)) + + def is_end(self, point): + return point == self.c0 or point == self.c1 + + def min_intersect_dist(self, t, point): + """ distance intersection extremite la plus proche + t: param t de l'intersection sur le segment courant + point: Point d'intersection + return d: distance + """ + if t > 0.5: + return self.c1.distance(point) + else: + return self.c0.distance(point) + + def intersect(self, segment): + """ point_sur_segment return + p: point d'intersection + u: param t de l'intersection sur le segment courant + v: param t de l'intersection sur le segment segment + """ + v2d = self.vect_2d + c2 = np.cross(segment.vect_2d, (0, 0, 1)) + d = np.dot(v2d, c2) + if d == 0: + # segments paralleles + segment._point_sur_segment(self.c0) + segment._point_sur_segment(self.c1) + self._point_sur_segment(segment.c0) + self._point_sur_segment(segment.c1) + return False, 0, 0, 0 + c1 = np.cross(v2d, (0, 0, 1)) + v3 = self.c0.vect(segment.c0) + v3[2] = 0.0 + u = np.dot(c2, v3) / d + v = np.dot(c1, v3) / d + co = self.lerp(u) + return True, co, u, v + + def _append_splits(self, split): + """ + append a unique split point + """ + if split not in self._splits: + self._splits.append(split) + + def slice(self, d, t, point): + if d > EPSILON: + if t > 0.5: + if point != self.c1: + self._append_splits((t, point)) + else: + if point != self.c0: + self._append_splits((t, point)) + + def add_user(self): + self.c0.add_user() + self.c1.add_user() + + def consume(self): + self.available = False + + +class Shape(): + """ + Ensure uniqueness and fix precision issues by design + implicit closed with last point + require point_tree and seg_tree + """ + + def __init__(self, points=[]): + """ + @vertex: list of coords + """ + self.available = True + # Ensure uniqueness of shape when merging + + self._segs = [] + # Shape segments + + self.shapeId = [] + # Id of shape in shapes to keep a track of shape parts when merging + + self._create_segments(points) + + def _create_segments(self, points): + global vars_dict + if vars_dict['seg_tree'] is None: + raise RuntimeError('Shape._create_segments() require spacial index ') + # skip null segments with unique points test + self._segs = list(vars_dict['seg_tree'].newSegment(points[v], points[v + 1]) + for v in range(len(points) - 1) if points[v] != points[v + 1]) + + @property + def coords(self): + coords = list(seg.c0.co for seg in self._segs) + coords.append(self.c1.co) + return coords + + @property + def points(self): + points = list(seg.c0 for seg in self._segs) + points.append(self.c1) + return points + + @property + def c0(self): + if not self.valid: + raise RuntimeError('Shape does not contains any segments') + return self._segs[0].c0 + + @property + def c1(self): + if not self.valid: + raise RuntimeError('Shape does not contains any segments') + return self._segs[-1].c1 + + @property + def nbsegs(self): + return len(self._segs) + + @property + def valid(self): + return self.nbsegs > 0 + + @property + def closed(self): + return self.valid and bool(self.c0 == self.c1) + + def merge(self, shape): + """ merge this shape with specified shape + shapes must share at least one vertex + """ + if not self.valid or not shape.valid: + raise RuntimeError('Trying to merge invalid shape') + if self.c1 == shape.c1 or self.c0 == shape.c0: + shape._reverse() + if self.c1 == shape.c0: + self._segs += shape._segs + elif shape.c1 == self.c0: + self._segs = shape._segs + self._segs + else: + # should never happen + raise RuntimeError("Shape merge failed {} {} {} {}".format( + id(self), id(shape), self.shapeId, shape.shapeId)) + + def _reverse(self): + """ + reverse vertex order + """ + points = self.points[::-1] + self._create_segments(points) + + def slice(self, shapes): + """ + slice shape into smaller parts at intersections + """ + if not self.valid: + raise RuntimeError('Cant slice invalid shape') + points = [] + for seg in self._segs: + if seg.available and not seg.original: + seg.consume() + points.append(seg.c0) + if seg.c1.users > 2: + points.append(seg.c1) + shape = Shape(points) + shapes.append(shape) + points = [] + if len(points) > 0: + points.append(self.c1) + shape = Shape(points) + shapes.append(shape) + + def add_points(self): + """ + add points from intersection data + """ + points = [] + if self.nbsegs > 0: + for seg in self._segs: + points.append(seg.c0) + for split in seg.splits: + points.append(split[1]) + points.append(self.c1) + self._create_segments(points) + + def set_users(self): + """ + add users on segments and points + """ + for seg in self._segs: + seg.add_user() + + def consume(self): + self.available = False + + +class Qtree(_QuadTree): + """ + The top spatial index to be created by the user. Once created it can be + populated with geographically placed members that can later be tested for + intersection with a user inputted geographic bounding box. + """ + def __init__(self, coordsys, extend=EPSILON, max_items=MAX_ITEMS, max_depth=MAX_DEPTH): + """ + objs may be blender objects or shapely geoms + extend: how much seek arround + """ + self._extend = extend + self._geoms = [] + + # store input coordsys + self.coordsys = coordsys + + super(Qtree, self).__init__(0, 0, coordsys.width, coordsys.height, max_items, max_depth) + + @property + def ngeoms(self): + return len(self._geoms) + + def build(self, geoms): + """ + Build a spacial index from shapely geoms + """ + t = time.time() + self._geoms = geoms + for i, geom in enumerate(geoms): + self._insert(i, geom.bounds) + print("Qtree.build() :%.2f seconds" % (time.time() - t)) + + def insert(self, id, geom): + self._geoms.append(geom) + self._insert(id, geom.bounds) + + def newPoint(self, co): + point = Point(co, self._extend) + count, found = self.intersects(point) + for id in found: + return self._geoms[id] + self.insert(self.ngeoms, point) + return point + + def newSegment(self, c0, c1): + """ + allow "opposite" segments, + those segments are not found by intersects + and not stored in self.geoms + """ + new_seg = Segment(c0, c1, self._extend) + count, found = self.intersects(new_seg) + for id in found: + old_seg = self._geoms[id] + if (old_seg.c0 == c0 and old_seg.c1 == c1): + return old_seg + if (old_seg.c0 == c1 and old_seg.c1 == c0): + if not old_seg.opposite: + old_seg.opposite = new_seg + new_seg.original = old_seg + return old_seg.opposite + self.insert(self.ngeoms, new_seg) + return new_seg + + def intersects(self, geom): + selection = list(self._intersect(geom.bounds)) + count = len(selection) + return count, sorted(selection) + + +class Io(): + + @staticmethod + def ensure_iterable(obj): + try: + iter(obj) + except TypeError: + obj = [obj] + return obj + + # Conversion methods + @staticmethod + def _to_geom(shape): + if not shape.valid: + raise RuntimeError('Cant convert invalid shape to Shapely LineString') + return shapely.geometry.LineString(shape.coords) + + @staticmethod + def shapes_to_geoms(shapes): + return [Io._to_geom(shape) for shape in shapes] + + @staticmethod + def _to_shape(geometry, shapes): + global vars_dict + if vars_dict['point_tree'] is None: + raise RuntimeError("geoms to shapes require a global point_tree spacial index") + if hasattr(geometry, 'exterior'): + Io._to_shape(geometry.exterior, shapes) + for geom in geometry.interiors: + Io._to_shape(geom, shapes) + elif hasattr(geometry, 'geoms'): + # Multi and Collections + for geom in geometry.geoms: + Io._to_shape(geom, shapes) + else: + points = list(vars_dict['point_tree'].newPoint(p) for p in list(geometry.coords)) + shape = Shape(points) + shapes.append(shape) + + @staticmethod + def geoms_to_shapes(geoms, shapes=[]): + for geom in geoms: + Io._to_shape(geom, shapes) + return shapes + + # Input methods + @staticmethod + def _interpolate_bezier(pts, wM, p0, p1, resolution): + # straight segment, worth testing here + # since this can lower points count by a resolution factor + # use normalized to handle non linear t + if resolution == 0: + pts.append(wM * p0.co.to_3d()) + else: + v = (p1.co - p0.co).normalized() + d1 = (p0.handle_right - p0.co).normalized() + d2 = (p1.co - p1.handle_left).normalized() + if d1 == v and d2 == v: + pts.append(wM * p0.co.to_3d()) + else: + seg = interpolate_bezier(wM * p0.co, + wM * p0.handle_right, + wM * p1.handle_left, + wM * p1.co, + resolution) + for i in range(resolution - 1): + pts.append(seg[i].to_3d()) + + @staticmethod + def _coords_from_spline(wM, resolution, spline): + pts = [] + if spline.type == 'POLY': + pts = [wM * p.co.to_3d() for p in spline.points] + if spline.use_cyclic_u: + pts.append(pts[0]) + elif spline.type == 'BEZIER': + points = spline.bezier_points + for i in range(1, len(points)): + p0 = points[i - 1] + p1 = points[i] + Io._interpolate_bezier(pts, wM, p0, p1, resolution) + pts.append(wM * points[-1].co) + if spline.use_cyclic_u: + p0 = points[-1] + p1 = points[0] + Io._interpolate_bezier(pts, wM, p0, p1, resolution) + pts.append(pts[0]) + return pts + + @staticmethod + def _add_geom_from_curve(curve, invert_world, resolution, geoms): + wM = invert_world * curve.matrix_world + for spline in curve.data.splines: + pts = Io._coords_from_spline(wM, resolution, spline) + geom = shapely.geometry.LineString(pts) + geoms.append(geom) + + @staticmethod + def curves_to_geoms(curves, resolution, geoms=[]): + """ + @curves : blender curves collection + Return coordsys for outputs + """ + curves = Io.ensure_iterable(curves) + coordsys = CoordSys(curves) + t = time.time() + for curve in curves: + Io._add_geom_from_curve(curve, coordsys.invert, resolution, geoms) + print("Io.curves_as_line() :%.2f seconds" % (time.time() - t)) + return coordsys + + @staticmethod + def _add_shape_from_curve(curve, invert_world, resolution, shapes): + global vars_dict + wM = invert_world * curve.matrix_world + for spline in curve.data.splines: + pts = Io._coords_from_spline(wM, resolution, spline) + pts = [vars_dict['point_tree'].newPoint(pt) for pt in pts] + shape = Shape(points=pts) + shapes.append(shape) + + @staticmethod + def curves_to_shapes(curves, coordsys, resolution, shapes=[]): + """ + @curves : blender curves collection + Return simple shapes + """ + curves = Io.ensure_iterable(curves) + t = time.time() + for curve in curves: + Io._add_shape_from_curve(curve, coordsys.invert, resolution, shapes) + print("Io.curves_to_shapes() :%.2f seconds" % (time.time() - t)) + + # Output methods + @staticmethod + def _poly_to_wall(scene, matrix_world, poly, height, name): + global vars_dict + curve = bpy.data.curves.new(name, type='CURVE') + curve.dimensions = "2D" + curve.fill_mode = 'BOTH' + curve.extrude = height + n_ext = len(poly.exterior.coords) + n_int = len(poly.interiors) + Io._add_spline(curve, poly.exterior) + for geom in poly.interiors: + Io._add_spline(curve, geom) + curve_obj = bpy.data.objects.new(name, curve) + curve_obj.matrix_world = matrix_world + scene.objects.link(curve_obj) + curve_obj.select = True + scene.objects.active = curve_obj + return n_ext, n_int, curve_obj + + @staticmethod + def wall_uv(me, bm): + + for face in bm.faces: + face.select = face.material_index > 0 + + bmesh.update_edit_mesh(me, True) + bpy.ops.uv.cube_project(scale_to_bounds=False, correct_aspect=True) + + for face in bm.faces: + face.select = face.material_index < 1 + + bmesh.update_edit_mesh(me, True) + bpy.ops.uv.smart_project(use_aspect=True, stretch_to_bounds=False) + + @staticmethod + def to_wall(scene, coordsys, geoms, height, name, walls=[]): + """ + use curve extrude as it does respect vertices number and is not removing doubles + so it is easy to set material index + cap faces are tri, sides faces are quads + """ + bpy.ops.object.select_all(action='DESELECT') + geoms = Io.ensure_iterable(geoms) + for poly in geoms: + if hasattr(poly, 'exterior'): + half_height = height / 2.0 + n_ext, n_int, obj = Io._poly_to_wall(scene, coordsys.world, poly, half_height, name) + bpy.ops.object.convert(target="MESH") + bpy.ops.object.mode_set(mode='EDIT') + me = obj.data + bm = bmesh.from_edit_mesh(me) + bm.verts.ensure_lookup_table() + bm.faces.ensure_lookup_table() + for v in bm.verts: + v.co.z += half_height + nfaces = 0 + for i, f in enumerate(bm.faces): + bm.faces[i].material_index = 2 + if len(f.verts) > 3: + nfaces = i + break + # walls without holes are inside + mat_index = 0 if n_int > 0 else 1 + for i in range(nfaces, nfaces + n_ext - 1): + bm.faces[i].material_index = mat_index + for i in range(nfaces + n_ext - 1, len(bm.faces)): + bm.faces[i].material_index = 1 + bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.003) + bmesh.update_edit_mesh(me, True) + Io.wall_uv(me, bm) + bpy.ops.mesh.dissolve_limited(angle_limit=0.00349066, delimit={'NORMAL'}) + bpy.ops.mesh.dissolve_degenerate() + bpy.ops.object.mode_set(mode='OBJECT') + bpy.ops.object.shade_flat() + MaterialUtils.add_wall_materials(obj) + walls.append(obj) + return walls + + @staticmethod + def _add_spline(curve, geometry): + coords = list(geometry.coords) + spline = curve.splines.new('POLY') + spline.use_endpoint_u = False + spline.use_cyclic_u = coords[0] == coords[-1] + spline.points.add(len(coords) - 1) + for i, coord in enumerate(coords): + x, y, z = Vector(coord).to_3d() + spline.points[i].co = (x, y, z, 1) + + @staticmethod + def _as_spline(curve, geometry): + """ + add a spline into a blender curve + @curve : blender curve + """ + if hasattr(geometry, 'exterior'): + # Polygon + Io._add_spline(curve, geometry.exterior) + for geom in geometry.interiors: + Io._add_spline(curve, geom) + elif hasattr(geometry, 'geoms'): + # Multi and Collections + for geom in geometry.geoms: + Io._as_spline(curve, geom) + else: + # LinearRing, LineString and Shape + Io._add_spline(curve, geometry) + + @staticmethod + def to_curve(scene, coordsys, geoms, name, dimensions='3D'): + global vars_dict + t = time.time() + geoms = Io.ensure_iterable(geoms) + curve = bpy.data.curves.new(name, type='CURVE') + curve.dimensions = dimensions + for geom in geoms: + Io._as_spline(curve, geom) + curve_obj = bpy.data.objects.new(name, curve) + curve_obj.matrix_world = coordsys.world + scene.objects.link(curve_obj) + curve_obj.select = True + print("Io.to_curves() :%.2f seconds" % (time.time() - t)) + return curve_obj + + @staticmethod + def to_curves(scene, coordsys, geoms, name, dimensions='3D'): + geoms = Io.ensure_iterable(geoms) + return [Io.to_curve(scene, coordsys, geom, name, dimensions) for geom in geoms] + + +class ShapelyOps(): + + @staticmethod + def min_bounding_rect(geom): + """ min_bounding_rect + minimum area oriented bounding rect + """ + # Compute edges (x2-x1,y2-y1) + if geom.convex_hull.geom_type == 'Polygon': + hull_points_2d = [list(coord[0:2]) for coord in list(geom.convex_hull.exterior.coords)] + else: + hull_points_2d = [list(coord[0:2]) for coord in list(geom.convex_hull.coords)] + edges = np.zeros((len(hull_points_2d) - 1, 2)) + # empty 2 column array + for i in range(len(edges)): + edge_x = hull_points_2d[i + 1][0] - hull_points_2d[i][0] + edge_y = hull_points_2d[i + 1][1] - hull_points_2d[i][1] + edges[i] = [edge_x, edge_y] + # Calculate edge angles atan2(y/x) + edge_angles = np.zeros((len(edges))) # empty 1 column array + for i in range(len(edge_angles)): + edge_angles[i] = atan2(edges[i, 1], edges[i, 0]) + # Check for angles in 1st quadrant + for i in range(len(edge_angles)): + edge_angles[i] = abs(edge_angles[i] % (pi / 2)) # want strictly positive answers + # Remove duplicate angles + edge_angles = np.unique(edge_angles) + # Test each angle to find bounding box with smallest area + min_bbox = (0, sys.maxsize, 0, 0, 0, 0, 0, 0) # rot_angle, area, width, height, min_x, max_x, min_y, max_y + # print "Testing", len(edge_angles), "possible rotations for bounding box... \n" + for i in range(len(edge_angles)): + # Create rotation matrix to shift points to baseline + # R = [ cos(theta) , cos(theta-PI/2) + # cos(theta+PI/2) , cos(theta) ] + R = np.array([[cos(edge_angles[i]), cos(edge_angles[i] - (pi / 2))], + [cos(edge_angles[i] + (pi / 2)), cos(edge_angles[i])]]) + # Apply this rotation to convex hull points + rot_points = np.dot(R, np.transpose(hull_points_2d)) # 2x2 * 2xn + # Find min/max x,y points + min_x = np.nanmin(rot_points[0], axis=0) + max_x = np.nanmax(rot_points[0], axis=0) + min_y = np.nanmin(rot_points[1], axis=0) + max_y = np.nanmax(rot_points[1], axis=0) + # Calculate height/width/area of this bounding rectangle + width = max_x - min_x + height = max_y - min_y + area = width * height + # Store the smallest rect found first + if (area < min_bbox[1]): + min_bbox = (edge_angles[i], area, width, height, min_x, max_x, min_y, max_y) + # Re-create rotation matrix for smallest rect + angle = min_bbox[0] + R = np.array([[cos(angle), cos(angle - (pi / 2))], [cos(angle + (pi / 2)), cos(angle)]]) + # min/max x,y points are against baseline + min_x = min_bbox[4] + max_x = min_bbox[5] + min_y = min_bbox[6] + max_y = min_bbox[7] + # Calculate center point and project onto rotated frame + center_x = (min_x + max_x) / 2 + center_y = (min_y + max_y) / 2 + center_point = np.dot([center_x, center_y], R) + if min_bbox[2] > min_bbox[3]: + a = -cos(angle) + b = sin(angle) + w = min_bbox[2] / 2 + h = min_bbox[3] / 2 + else: + a = -cos(angle + (pi / 2)) + b = sin(angle + (pi / 2)) + w = min_bbox[3] / 2 + h = min_bbox[2] / 2 + tM = Matrix([[a, b, 0, center_point[0]], [-b, a, 0, center_point[1]], [0, 0, 1, 0], [0, 0, 0, 1]]) + l_pts = [Vector((-w, -h, 0)), Vector((-w, h, 0)), Vector((w, h, 0)), Vector((w, -h, 0))] + w_pts = [tM * pt for pt in l_pts] + return tM, 2 * w, 2 * h, l_pts, w_pts + + @staticmethod + def detect_polygons(geoms): + """ detect_polygons + """ + print("Ops.detect_polygons()") + t = time.time() + result, dangles, cuts, invalids = shapely.ops.polygonize_full(geoms) + print("Ops.detect_polygons() :%.2f seconds" % (time.time() - t)) + return result, dangles, cuts, invalids + + @staticmethod + def optimize(geoms, tolerance=0.001, preserve_topology=True): + """ optimize + """ + t = time.time() + geoms = Io.ensure_iterable(geoms) + optimized = [geom.simplify(tolerance, preserve_topology) for geom in geoms] + print("Ops.optimize() :%.2f seconds" % (time.time() - t)) + return optimized + + @staticmethod + def union(geoms): + """ union (shapely based) + cascaded union - may require snap before use to fix precision issues + use union2 for best performances + """ + t = time.time() + geoms = Io.ensure_iterable(geoms) + collection = shapely.geometry.GeometryCollection(geoms) + union = shapely.ops.cascaded_union(collection) + print("Ops.union() :%.2f seconds" % (time.time() - t)) + return union + + +class ShapeOps(): + + @staticmethod + def union(shapes, extend=0.001): + """ union2 (Shape based) + cascaded union + require point_tree and seg_tree + """ + split = ShapeOps.split(shapes, extend=extend) + union = ShapeOps.merge(split) + return union + + @staticmethod + def _intersection_point(d, t, point, seg): + if d > EPSILON: + return point + elif t > 0.5: + return seg.c1 + else: + return seg.c0 + + @staticmethod + def split(shapes, extend=0.01): + """ _split + detect intersections between segments and slice shapes according + is able to project segment ends on closest segment + require point_tree and seg_tree + """ + global vars_dict + t = time.time() + new_shapes = [] + segs = vars_dict['seg_tree']._geoms + nbsegs = len(segs) + it_start = [None for x in range(nbsegs)] + it_end = [None for x in range(nbsegs)] + for s, seg in enumerate(segs): + count, idx = vars_dict['seg_tree'].intersects(seg) + for id in idx: + if id > s: + intersect, co, u, v = seg.intersect(segs[id]) + if intersect: + point = vars_dict['point_tree'].newPoint(co) + du = seg.min_intersect_dist(u, point) + dv = segs[id].min_intersect_dist(v, point) + # point intersection sur segment id + pt = ShapeOps._intersection_point(dv, v, point, segs[id]) + # print("s:%s id:%s u:%7f v:%7f du:%7f dv:%7f" % (s, id, u, v, du, dv)) + if u <= 0: + # prolonge segment s c0 + if du < extend and not seg.is_end(pt): + it = Prolongement(seg.c0, pt, id, v, du) + last = it_start[s] + if last is None or last.length > it.length: + it_start[s] = it + elif u < 1: + # intersection sur segment s + seg.slice(du, u, pt) + else: + # prolonge segment s c1 + if du < extend and not seg.is_end(pt): + it = Prolongement(seg.c1, pt, id, v, du) + last = it_end[s] + if last is None or last.length > it.length: + it_end[s] = it + pt = ShapeOps._intersection_point(du, u, point, seg) + if v <= 0: + # prolonge segment id c0 + if dv < extend and not segs[id].is_end(pt): + it = Prolongement(segs[id].c0, pt, s, u, dv) + last = it_start[id] + if last is None or last.length > it.length: + it_start[id] = it + elif v < 1: + # intersection sur segment s + segs[id].slice(dv, v, pt) + else: + # prolonge segment s c1 + if dv < extend and not segs[id].is_end(pt): + it = Prolongement(segs[id].c1, pt, s, u, dv) + last = it_end[id] + if last is None or last.length > it.length: + it_end[id] = it + for it in it_start: + if it is not None: + # print("it_start[%s] id:%s t:%4f d:%4f" % (s, it.id, it.t, it.d) ) + if it.t > 0 and it.t < 1: + segs[it.id]._append_splits((it.t, it.c1)) + if it.d > EPSILON: + shape = Shape([it.c0, it.c1]) + shapes.append(shape) + for it in it_end: + if it is not None: + # print("it_end[%s] id:%s t:%4f d:%4f" % (s, it.id, it.t, it.d) ) + if it.t > 0 and it.t < 1: + segs[it.id]._append_splits((it.t, it.c1)) + if it.d > EPSILON: + shape = Shape([it.c0, it.c1]) + shapes.append(shape) + print("Ops.split() intersect :%.2f seconds" % (time.time() - t)) + t = time.time() + for shape in shapes: + shape.add_points() + for shape in shapes: + shape.set_users() + for shape in shapes: + if shape.valid: + shape.slice(new_shapes) + print("Ops.split() slice :%.2f seconds" % (time.time() - t)) + return new_shapes + + @staticmethod + def merge(shapes): + """ merge + merge shapes ends + reverse use seg_tree + does not need tree as all: + - set shape ids to end vertices + - traverse shapes looking for points with 2 shape ids + - merge different shapes according + """ + t = time.time() + merged = [] + for i, shape in enumerate(shapes): + shape.available = True + shape.shapeId = [i] + shape.c0.shapeIds = [] + shape.c1.shapeIds = [] + for i, shape in enumerate(shapes): + shape.c0.shapeIds.append(i) + shape.c1.shapeIds.append(i) + for i, shape in enumerate(shapes): + shapeIds = shape.c1.shapeIds + if len(shapeIds) == 2: + if shapeIds[0] in shape.shapeId: + s = shapeIds[1] + else: + s = shapeIds[0] + if shape != shapes[s]: + shape.merge(shapes[s]) + shape.shapeId += shapes[s].shapeId + for j in shape.shapeId: + shapes[j] = shape + shapeIds = shape.c0.shapeIds + if len(shapeIds) == 2: + if shapeIds[0] in shape.shapeId: + s = shapeIds[1] + else: + s = shapeIds[0] + if shape != shapes[s]: + shape.merge(shapes[s]) + shape.shapeId += shapes[s].shapeId + for j in shape.shapeId: + shapes[j] = shape + for shape in shapes: + if shape.available: + shape.consume() + merged.append(shape) + print("Ops.merge() :%.2f seconds" % (time.time() - t)) + return merged + + +class Selectable(object): + + """ selectable shapely geoms """ + def __init__(self, geoms, coordsys): + # selection sets (bitArray) + self.selections = [] + # selected objects on screen representation + self.curves = [] + # Rtree to speedup region selections + self.tree = Qtree(coordsys) + self.tree.build(geoms) + # BitArray ids of selected geoms + self.ba = BitArray(self.ngeoms) + # Material to represent selection on screen + self.mat = self.build_display_mat("Selected", + color=bpy.context.user_preferences.themes[0].view_3d.object_selected) + self.cursor_fence = GlCursorFence() + self.cursor_fence.enable() + self.cursor_area = GlCursorArea() + self.feedback = FeedbackPanel() + self.action = None + self.store_index = 0 + + @property + def coordsys(self): + return self.tree.coordsys + + @property + def geoms(self): + return self.tree._geoms + + @property + def ngeoms(self): + return self.tree.ngeoms + + @property + def nsets(self): + return len(self.selections) + + def build_display_mat(self, name, color=(0.2, 0.2, 0)): + mat = MaterialUtils.build_default_mat(name, color) + mat.use_object_color = True + mat.emit = 0.2 + mat.alpha = 0.2 + mat.game_settings.alpha_blend = 'ADD' + return mat + + def _unselect(self, selection): + t = time.time() + for i in selection: + self.ba.clear(i) + print("Selectable._unselect() :%.2f seconds" % (time.time() - t)) + + def _select(self, selection): + t = time.time() + for i in selection: + self.ba.set(i) + print("Selectable._select() :%.2f seconds" % (time.time() - t)) + + def _position_3d_from_coord(self, context, coord): + """return point in local input coordsys + """ + region = context.region + rv3d = context.region_data + view_vector_mouse = view3d_utils.region_2d_to_vector_3d(region, rv3d, coord) + ray_origin_mouse = view3d_utils.region_2d_to_origin_3d(region, rv3d, coord) + loc = intersect_line_plane(ray_origin_mouse, ray_origin_mouse + view_vector_mouse, + Vector((0, 0, 0)), Vector((0, 0, 1)), False) + x, y, z = self.coordsys.invert * loc + return Vector((x, y, z)) + + def _position_2d_from_coord(self, context, coord): + """ coord given in local input coordsys + """ + region = context.region + rv3d = context.region_data + loc = view3d_utils.location_3d_to_region_2d(region, rv3d, self.coordsys.world * coord) + x, y = loc + return Vector((x, y)) + + def _contains(self, context, coord, event): + t = time.time() + point = self._position_3d_from_coord(context, coord) + selection = [] + pt = ShapelyPoint(point) + prepared_pt = shapely.prepared.prep(pt) + count, gids = self.tree.intersects(pt) + selection = [i for i in gids if prepared_pt.intersects(self.geoms[i])] + print("Selectable._contains() :%.2f seconds" % (time.time() - t)) + if event.shift: + self._unselect(selection) + else: + self._select(selection) + self._draw(context) + + def _intersects(self, context, coord, event): + t = time.time() + c0 = self._position_3d_from_coord(context, coord) + c1 = self._position_3d_from_coord(context, (coord[0], event.mouse_region_y)) + c2 = self._position_3d_from_coord(context, (event.mouse_region_x, event.mouse_region_y)) + c3 = self._position_3d_from_coord(context, (event.mouse_region_x, coord[1])) + poly = ShapelyPolygon([c0, c1, c2, c3]) + prepared_poly = shapely.prepared.prep(poly) + count, gids = self.tree.intersects(poly) + if event.ctrl: + selection = [i for i in gids if prepared_poly.contains(self.geoms[i])] + else: + selection = [i for i in gids if prepared_poly.intersects(self.geoms[i])] + print("Selectable._intersects() :%.2f seconds" % (time.time() - t)) + if event.shift: + self._unselect(selection) + else: + self._select(selection) + self._draw(context) + + def _hide(self, context): + t = time.time() + if len(self.curves) > 0: + try: + for curve in self.curves: + data = curve.data + context.scene.objects.unlink(curve) + bpy.data.objects.remove(curve, do_unlink=True) + if data is None: + return + name = data.name + if bpy.data.curves.find(name) > - 1: + bpy.data.curves.remove(data, do_unlink=True) + except: + pass + self.curves = [] + print("Selectable._hide() :%.2f seconds" % (time.time() - t)) + + def _draw(self, context): + print("Selectable._draw() %s" % (self.coordsys.world)) + t = time.time() + self._hide(context) + selection = [self.geoms[i] for i in self.ba.list] + if len(selection) > 1000: + self.curves = [Io.to_curve(context.scene, self.coordsys, selection, 'selection', '3D')] + else: + self.curves = Io.to_curves(context.scene, self.coordsys, selection, 'selection', '2D') + for curve in self.curves: + curve.color = (1, 1, 0, 1) + if len(curve.data.materials) < 1: + curve.data.materials.append(self.mat) + curve.active_material = self.mat + curve.select = True + print("Selectable._draw() :%.2f seconds" % (time.time() - t)) + + def store(self): + self.selections.append(self.ba.copy) + self.store_index = self.nsets + + def recall(self): + if self.nsets > 0: + if self.store_index < 1: + self.store_index = self.nsets + self.store_index -= 1 + self.ba = self.selections[self.store_index].copy + + def select(self, context, coord, event): + if abs(event.mouse_region_x - coord[0]) > 2 and abs(event.mouse_region_y - coord[1]) > 2: + self._intersects(context, coord, event) + else: + self._contains(context, (event.mouse_region_x, event.mouse_region_y), event) + + def init(self, pick_tool, context, action): + raise NotImplementedError("Selectable must implement init(self, pick_tool, context, action)") + + def keyboard(self, context, event): + """ keyboard events modal handler """ + raise NotImplementedError("Selectable must implement keyboard(self, context, event)") + + def complete(self, context): + raise NotImplementedError("Selectable must implement complete(self, context)") + + def modal(self, context, event): + """ modal handler """ + raise NotImplementedError("Selectable must implement modal(self, context, event)") + + def draw_callback(self, _self, context): + """ a gl draw callback """ + raise NotImplementedError("Selectable must implement draw_callback(self, _self, context)") + + +class SelectPoints(Selectable): + + def __init__(self, shapes, coordsys): + geoms = [] + for shape in shapes: + if shape.valid: + for point in shape.points: + point.users = 1 + for shape in shapes: + if shape.valid: + for point in shape.points: + if point.users > 0: + point.users = 0 + geoms.append(point.geom) + super(SelectPoints, self).__init__(geoms, coordsys) + + def _draw(self, context): + """ override draw method """ + print("SelectPoints._draw()") + t = time.time() + self._hide(context) + selection = list(self.geoms[i] for i in self.ba.list) + geom = ShapelyOps.union(selection) + self.curves = [Io.to_curve(context.scene, self.coordsys, geom.convex_hull, 'selection', '3D')] + for curve in self.curves: + curve.color = (1, 1, 0, 1) + curve.select = True + print("SelectPoints._draw() :%.2f seconds" % (time.time() - t)) + + def init(self, pick_tool, context, action): + # Post selection actions + self.selectMode = True + self.object_location = None + self.startPoint = (0, 0) + self.endPoint = (0, 0) + self.drag = False + self.feedback.instructions(context, "Select Points", "Click & Drag to select points in area", [ + ('SHIFT', 'deselect'), + ('CTRL', 'contains'), + ('A', 'All'), + ('I', 'Inverse'), + ('F', 'Create line around selection'), + # ('W', 'Create window using selection'), + # ('D', 'Create door using selection'), + ('ALT+F', 'Create best fit rectangle'), + ('R', 'Retrieve selection'), + ('S', 'Store selection'), + ('ESC or RIGHTMOUSE', 'exit when done') + ]) + self.feedback.enable() + args = (self, context) + self._handle = bpy.types.SpaceView3D.draw_handler_add(self.draw_callback, args, 'WINDOW', 'POST_PIXEL') + self.action = action + self._draw(context) + print("SelectPoints.init()") + + def complete(self, context): + self.feedback.disable() + self._hide(context) + + def keyboard(self, context, event): + if event.type in {'A'}: + if len(self.ba.list) > 0: + self.ba.none() + else: + self.ba.all() + elif event.type in {'I'}: + self.ba.reverse() + elif event.type in {'S'}: + self.store() + elif event.type in {'R'}: + self.recall() + elif event.type in {'F'}: + sel = [self.geoms[i] for i in self.ba.list] + if len(sel) > 0: + scene = context.scene + geom = ShapelyOps.union(sel) + if event.alt: + tM, w, h, l_pts, w_pts = ShapelyOps.min_bounding_rect(geom) + x0 = -w / 2.0 + y0 = -h / 2.0 + x1 = w / 2.0 + y1 = h / 2.0 + poly = shapely.geometry.LineString([(x0, y0, 0), (x1, y0, 0), (x1, y1, 0), + (x0, y1, 0), (x0, y0, 0)]) + result = Io.to_curve(scene, self.coordsys, poly, 'points') + result.matrix_world = self.coordsys.world * tM + scene.objects.active = result + else: + result = Io.to_curve(scene, self.coordsys, geom.convex_hull, 'points') + scene.objects.active = result + self.ba.none() + self.complete(context) + elif event.type in {'W'}: + sel = [self.geoms[i] for i in self.ba.list] + if len(sel) > 0: + scene = context.scene + geom = ShapelyOps.union(sel) + if event.alt: + tM, w, h, l_pts, w_pts = ShapelyOps.min_bounding_rect(geom) + x0 = -w / 2.0 + y0 = -h / 2.0 + x1 = w / 2.0 + y1 = h / 2.0 + poly = shapely.geometry.LineString([(x0, y0, 0), (x1, y0, 0), (x1, y1, 0), + (x0, y1, 0), (x0, y0, 0)]) + result = Io.to_curve(scene, self.coordsys, poly, 'points') + result.matrix_world = self.coordsys.world * tM + scene.objects.active = result + else: + result = Io.to_curve(scene, self.coordsys, geom.convex_hull, 'points') + scene.objects.active = result + self.ba.none() + self.complete(context) + elif event.type in {'D'}: + sel = [self.geoms[i] for i in self.ba.list] + if len(sel) > 0: + scene = context.scene + geom = ShapelyOps.union(sel) + if event.alt: + tM, w, h, l_pts, w_pts = ShapelyOps.min_bounding_rect(geom) + x0 = -w / 2.0 + y0 = -h / 2.0 + x1 = w / 2.0 + y1 = h / 2.0 + poly = shapely.geometry.LineString([(x0, y0, 0), (x1, y0, 0), (x1, y1, 0), + (x0, y1, 0), (x0, y0, 0)]) + result = Io.to_curve(scene, self.coordsys, poly, 'points') + result.matrix_world = self.coordsys.world * tM + scene.objects.active = result + else: + result = Io.to_curve(scene, self.coordsys, geom.convex_hull, 'points') + scene.objects.active = result + self.ba.none() + self.complete(context) + self._draw(context) + + def modal(self, context, event): + if event.type in {'I', 'A', 'S', 'R', 'F'} and event.value == 'PRESS': + self.keyboard(context, event) + elif event.type in {'RIGHTMOUSE', 'ESC'}: + self.complete(context) + bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW') + return {'FINISHED'} + elif event.type == 'LEFTMOUSE' and event.value == 'PRESS': + self.drag = True + self.cursor_area.enable() + self.cursor_fence.disable() + self.startPoint = (event.mouse_region_x, event.mouse_region_y) + self.endPoint = (event.mouse_region_x, event.mouse_region_y) + elif event.type == 'LEFTMOUSE' and event.value == 'RELEASE': + self.drag = False + self.cursor_area.disable() + self.cursor_fence.enable() + self.endPoint = (event.mouse_region_x, event.mouse_region_y) + self.select(context, self.startPoint, event) + elif event.type == 'MOUSEMOVE': + self.endPoint = (event.mouse_region_x, event.mouse_region_y) + return {'RUNNING_MODAL'} + + def draw_callback(self, _self, context): + self.feedback.draw(context) + self.cursor_area.set_location(context, self.startPoint, self.endPoint) + self.cursor_fence.set_location(context, self.endPoint) + self.cursor_area.draw(context) + self.cursor_fence.draw(context) + + +class SelectLines(Selectable): + + def __init__(self, geoms, coordsys): + super(SelectLines, self).__init__(geoms, coordsys) + + def _draw(self, context): + """ override draw method """ + print("SelectLines._draw()") + t = time.time() + self._hide(context) + selection = list(self.geoms[i] for i in self.ba.list) + self.curves = [Io.to_curve(context.scene, self.coordsys, selection, 'selection', '3D')] + for curve in self.curves: + curve.color = (1, 1, 0, 1) + curve.select = True + print("SelectLines._draw() :%.2f seconds" % (time.time() - t)) + + def init(self, pick_tool, context, action): + # Post selection actions + self.selectMode = True + self.object_location = None + self.startPoint = (0, 0) + self.endPoint = (0, 0) + self.drag = False + self.feedback.instructions(context, "Select Lines", "Click & Drag to select lines in area", [ + ('SHIFT', 'deselect'), + ('CTRL', 'contains'), + ('A', 'All'), + ('I', 'Inverse'), + # ('F', 'Create lines from selection'), + ('R', 'Retrieve selection'), + ('S', 'Store selection'), + ('ESC or RIGHTMOUSE', 'exit when done') + ]) + self.feedback.enable() + args = (self, context) + self._handle = bpy.types.SpaceView3D.draw_handler_add( + self.draw_callback, args, 'WINDOW', 'POST_PIXEL') + self.action = action + self._draw(context) + print("SelectLines.init()") + + def complete(self, context): + print("SelectLines.complete()") + t = time.time() + self._hide(context) + scene = context.scene + selection = list(self.geoms[i] for i in self.ba.list) + if len(selection) > 0: + if self.action == 'select': + result = Io.to_curve(scene, self.coordsys, selection, 'selection') + scene.objects.active = result + elif self.action == 'union': + shapes = Io.geoms_to_shapes(selection) + merged = ShapeOps.merge(shapes) + union = Io.shapes_to_geoms(merged) + # union = self.ops.union(selection) + resopt = ShapelyOps.optimize(union) + result = Io.to_curve(scene, self.coordsys, resopt, 'union') + scene.objects.active = result + self.feedback.disable() + print("SelectLines.complete() :%.2f seconds" % (time.time() - t)) + + def keyboard(self, context, event): + if event.type in {'A'}: + if len(self.ba.list) > 0: + self.ba.none() + else: + self.ba.all() + elif event.type in {'I'}: + self.ba.reverse() + elif event.type in {'S'}: + self.store() + elif event.type in {'R'}: + self.recall() + self._draw(context) + + def modal(self, context, event): + if event.type in {'I', 'A', 'S', 'R'} and event.value == 'PRESS': + self.keyboard(context, event) + elif event.type in {'RIGHTMOUSE', 'ESC'}: + self.complete(context) + bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW') + return {'FINISHED'} + elif event.type == 'LEFTMOUSE' and event.value == 'PRESS': + self.drag = True + self.cursor_area.enable() + self.cursor_fence.disable() + self.startPoint = (event.mouse_region_x, event.mouse_region_y) + self.endPoint = (event.mouse_region_x, event.mouse_region_y) + elif event.type == 'LEFTMOUSE' and event.value == 'RELEASE': + self.drag = False + self.cursor_area.disable() + self.cursor_fence.enable() + self.endPoint = (event.mouse_region_x, event.mouse_region_y) + self.select(context, self.startPoint, event) + elif event.type == 'MOUSEMOVE': + self.endPoint = (event.mouse_region_x, event.mouse_region_y) + return {'RUNNING_MODAL'} + + def draw_callback(self, _self, context): + self.feedback.draw(context) + self.cursor_area.set_location(context, self.startPoint, self.endPoint) + self.cursor_fence.set_location(context, self.endPoint) + self.cursor_area.draw(context) + self.cursor_fence.draw(context) + + +class SelectPolygons(Selectable): + + def __init__(self, geoms, coordsys): + super(SelectPolygons, self).__init__(geoms, coordsys) + + """ + pick_tools actions + """ + def init(self, pick_tool, context, action): + # Post selection actions + self.need_rotation = False + self.direction = 0 + self.object_location = None + self.selectMode = True + self.startPoint = (0, 0) + self.endPoint = (0, 0) + if action in ['select', 'union', 'rectangle']: + self.feedback.instructions(context, "Select Polygons", "Click & Drag to select polygons in area", [ + ('SHIFT', 'deselect'), + ('CTRL', 'contains'), + ('A', 'All'), + ('I', 'Inverse'), + ('B', 'Bigger than current'), + # ('F', 'Create from selection'), + ('R', 'Retrieve selection'), + ('S', 'Store selection'), + ('ESC or RIGHTMOUSE', 'exit when done') + ]) + elif action == 'wall': + self.feedback.instructions(context, "Select Polygons", "Click & Drag to select polygons in area", [ + ('SHIFT', 'deselect'), + ('CTRL', 'contains'), + ('A', 'All'), + ('I', 'Inverse'), + ('B', 'Bigger than current'), + ('R', 'Retrieve selection'), + ('S', 'Store selection'), + ('ESC or RIGHTMOUSE', 'exit and build wall when done') + ]) + elif action == 'window': + self.feedback.instructions(context, "Select Polygons", "Click & Drag to select polygons in area", [ + ('SHIFT', 'deselect'), + ('CTRL', 'contains'), + ('A', 'All'), + ('I', 'Inverse'), + ('B', 'Bigger than current'), + ('F', 'Create a window from selection'), + ('ESC or RIGHTMOUSE', 'exit tool when done') + ]) + elif action == 'door': + self.feedback.instructions(context, "Select Polygons", "Click & Drag to select polygons in area", [ + ('SHIFT', 'deselect'), + ('CTRL', 'contains'), + ('A', 'All'), + ('I', 'Inverse'), + ('B', 'Bigger than current'), + ('F', 'Create a door from selection'), + ('ESC or RIGHTMOUSE', 'exit tool when done') + ]) + self.gl_arc = GlPolyline((1.0, 1.0, 1.0, 0.5), d=3) + self.gl_arc.width = 1 + self.gl_arc.style = bgl.GL_LINE_STIPPLE + self.gl_line = GlLine(d=3) + self.gl_line.colour_inactive = (1.0, 1.0, 1.0, 0.5) + self.gl_line.width = 2 + self.gl_line.style = bgl.GL_LINE_STIPPLE + self.gl_side = GlLine(d=2) + self.gl_side.colour_inactive = (1.0, 1.0, 1.0, 0.5) + self.gl_side.width = 2 + self.gl_side.style = bgl.GL_LINE_STIPPLE + self.feedback.enable() + self.drag = False + args = (self, context) + self._handle = bpy.types.SpaceView3D.draw_handler_add( + self.draw_callback, args, 'WINDOW', 'POST_PIXEL') + self.action = action + self._draw(context) + print("SelectPolygons.init()") + + def complete(self, context): + print("SelectPolygons.complete()") + t = time.time() + scene = context.scene + self._hide(context) + selection = list(self.geoms[i] for i in self.ba.list) + if len(selection) > 0: + if self.action == 'select': + result = Io.to_curve(scene, self.coordsys, selection, 'selection') + scene.objects.active = result + elif self.action == 'union': + union = ShapelyOps.union(selection) + resopt = ShapelyOps.optimize(union) + result = Io.to_curve(scene, self.coordsys, resopt, 'union') + scene.objects.active = result + elif self.action == 'wall': + union = ShapelyOps.union(selection) + union = ShapelyOps.optimize(union) + res = [] + z = context.window_manager.archipack_polylib.solidify_thickness + Io.to_wall(scene, self.coordsys, union, z, 'wall', res) + if len(res) > 0: + scene.objects.active = res[0] + if len(res) > 1: + bpy.ops.object.join() + bpy.ops.archipack.wall(z=z) + elif self.action == 'rectangle': + # currently only output a best fitted rectangle + # over selection + if self.object_location is not None: + tM, w, h, l_pts, w_pts = self.object_location + poly = shapely.geometry.LineString(l_pts) + result = Io.to_curve(scene, self.coordsys, poly, 'rectangle') + result.matrix_world = self.coordsys.world * tM + scene.objects.active = result + self.ba.none() + elif self.action == 'window': + if self.object_location is not None: + + tM, w, h, l_pts, w_pts = self.object_location + + if self.need_rotation: + rM = Matrix([ + [-1, 0, 0, 0], + [0, -1, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 1], + ]) + else: + rM = Matrix() + + if w > 1.8: + z = 2.2 + altitude = 0.0 + else: + z = 1.2 + altitude = 1.0 + + bpy.ops.archipack.window(x=w, y=h, z=z, altitude=altitude, auto_manipulate=False) + result = context.object + result.matrix_world = self.coordsys.world * tM * rM + result.data.archipack_window[0].hole_margin = 0.02 + self.ba.none() + elif self.action == 'door': + if self.object_location is not None: + + tM, w, h, l_pts, w_pts = self.object_location + + if self.need_rotation: + rM = Matrix([ + [-1, 0, 0, 0], + [0, -1, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 1], + ]) + else: + rM = Matrix() + + if w < 1.5: + n_panels = 1 + else: + n_panels = 2 + + bpy.ops.archipack.door(x=w, y=h, z=2.0, n_panels=n_panels, + direction=self.direction, auto_manipulate=False) + result = context.object + result.matrix_world = self.coordsys.world * tM * rM + result.data.archipack_door[0].hole_margin = 0.02 + self.ba.none() + + if self.action not in ['window', 'door']: + self.feedback.disable() + + print("SelectPolygons.complete() :%.2f seconds" % (time.time() - t)) + + def keyboard(self, context, event): + if event.type in {'A'}: + if len(self.ba.list) > 0: + self.ba.none() + else: + self.ba.all() + elif event.type in {'I'}: + self.ba.reverse() + elif event.type in {'S'}: + self.store() + elif event.type in {'R'}: + self.recall() + elif event.type in {'B'}: + areas = [self.geoms[i].area for i in self.ba.list] + area = max(areas) + self.ba.none() + for i, geom in enumerate(self.geoms): + if geom.area > area: + self.ba.set(i) + elif event.type in {'F'}: + if self.action == 'rectangle': + self.complete(context) + else: + sel = [self.geoms[i] for i in self.ba.list] + if len(sel) > 0: + if self.action == 'window': + self.feedback.instructions(context, + "Select Polygons", "Click & Drag to select polygons in area", [ + ('CLICK & DRAG', 'Set window orientation'), + ('RELEASE', 'Create window'), + ('F', 'Return to select mode'), + ('ESC or RIGHTMOUSE', 'exit tool when done') + ]) + elif self.action == 'door': + self.feedback.instructions(context, + "Select Polygons", "Click & Drag to select polygons in area", [ + ('CLICK & DRAG', 'Set door orientation'), + ('RELEASE', 'Create door'), + ('F', 'Return to select mode'), + ('ESC or RIGHTMOUSE', 'exit tool when done') + ]) + self.selectMode = not self.selectMode + geom = ShapelyOps.union(sel) + tM, w, h, l_pts, w_pts = ShapelyOps.min_bounding_rect(geom) + self.object_location = (tM, w, h, l_pts, w_pts) + self.startPoint = self._position_2d_from_coord(context, tM.translation) + self._draw(context) + + def modal(self, context, event): + if event.type in {'I', 'A', 'S', 'R', 'F', 'B'} and event.value == 'PRESS': + + self.keyboard(context, event) + elif event.type in {'RIGHTMOUSE', 'ESC'}: + if self.action == 'object': + self._hide(context) + else: + self.complete(context) + bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW') + return {'FINISHED'} + elif event.type == 'LEFTMOUSE' and event.value == 'PRESS': + self.drag = True + self.cursor_area.enable() + self.cursor_fence.disable() + if self.selectMode: + self.startPoint = (event.mouse_region_x, event.mouse_region_y) + self.endPoint = (event.mouse_region_x, event.mouse_region_y) + elif event.type == 'LEFTMOUSE' and event.value == 'RELEASE': + self.drag = False + self.cursor_area.disable() + self.cursor_fence.enable() + self.endPoint = (event.mouse_region_x, event.mouse_region_y) + if self.selectMode: + self.select(context, self.startPoint, event) + else: + self.complete(context) + if self.action == 'window': + self.feedback.instructions(context, "Select Polygons", "Click & Drag to select polygons in area", [ + ('SHIFT', 'deselect'), + ('CTRL', 'contains'), + ('A', 'All'), + ('I', 'Inverse'), + ('B', 'Bigger than current'), + ('F', 'Create a window from selection'), + ('ESC or RIGHTMOUSE', 'exit tool when done') + ]) + elif self.action == 'door': + self.feedback.instructions(context, "Select Polygons", "Click & Drag to select polygons in area", [ + ('SHIFT', 'deselect'), + ('CTRL', 'contains'), + ('A', 'All'), + ('I', 'Inverse'), + ('B', 'Bigger than current'), + ('F', 'Create a door from selection'), + ('ESC or RIGHTMOUSE', 'exit tool when done') + ]) + self.selectMode = True + if event.type == 'MOUSEMOVE': + self.endPoint = (event.mouse_region_x, event.mouse_region_y) + return {'RUNNING_MODAL'} + + def _draw_2d_arc(self, context, c, p0, p1): + """ + draw projection of 3d arc in 2d space + """ + d0 = np.subtract(c, p0) + d1 = np.subtract(p1, c) + a0 = atan2(d0[1], d0[0]) + a1 = atan2(d1[1], d1[0]) + da = a1 - a0 + if da < pi: + da += 2 * pi + if da > pi: + da -= 2 * pi + da = da / 12 + r = np.linalg.norm(d1) + pts = [] + for i in range(13): + a = a0 + da * i + p3d = c + Vector((cos(a) * r, sin(a) * r, 0)) + pts.append(self.coordsys.world * p3d) + + self.gl_arc.set_pos(pts) + self.gl_arc.draw(context) + self.gl_line.p = self.coordsys.world * c + self.gl_line.v = pts[0] - self.gl_line.p + self.gl_line.draw(context) + + def draw_callback(self, _self, context): + """ + draw on screen feedback using gl. + """ + self.feedback.draw(context) + + if self.selectMode: + self.cursor_area.set_location(context, self.startPoint, self.endPoint) + self.cursor_fence.set_location(context, self.endPoint) + self.cursor_area.draw(context) + self.cursor_fence.draw(context) + else: + if self.drag: + x0, y0 = self.startPoint + x1, y1 = self.endPoint + # draw 2d line marker + # self.gl.Line(x0, y0, x1, y1, self.gl.line_colour) + + # 2d line + self.gl_side.p = Vector(self.startPoint) + self.gl_side.v = Vector(self.endPoint) - Vector(self.startPoint) + self.gl_side.draw(context) + + tM, w, h, l_pts, w_pts = self.object_location + pt = self._position_3d_from_coord(context, self.endPoint) + pt = tM.inverted() * Vector(pt) + self.need_rotation = pt.y < 0 + if self.action == 'door': + # symbole porte + if pt.x > 0: + if pt.y > 0: + self.direction = 1 + i_s, i_c, i_e = 3, 2, 1 + else: + self.direction = 0 + i_s, i_c, i_e = 2, 3, 0 + else: + if pt.y > 0: + self.direction = 0 + i_s, i_c, i_e = 0, 1, 2 + else: + self.direction = 1 + i_s, i_c, i_e = 1, 0, 3 + self._draw_2d_arc(context, w_pts[i_c], w_pts[i_s], w_pts[i_e]) + elif self.action == 'window': + # symbole fenetre + if pt.y > 0: + i_s0, i_c0 = 0, 1 + i_s1, i_c1 = 3, 2 + else: + i_s0, i_c0 = 1, 0 + i_s1, i_c1 = 2, 3 + pc = w_pts[i_c0] + 0.5 * (w_pts[i_c1] - w_pts[i_c0]) + self._draw_2d_arc(context, w_pts[i_c0], w_pts[i_s0], pc) + self._draw_2d_arc(context, w_pts[i_c1], w_pts[i_s1], pc) + + +class ARCHIPACK_OP_PolyLib_Pick2DPoints(Operator): + bl_idname = "archipack.polylib_pick_2d_points" + bl_label = "Pick lines" + bl_description = "Pick lines" + bl_options = {'REGISTER', 'UNDO'} + pass_keys = ['NUMPAD_0', 'NUMPAD_1', 'NUMPAD_3', 'NUMPAD_4', + 'NUMPAD_5', 'NUMPAD_6', 'NUMPAD_7', 'NUMPAD_8', + 'NUMPAD_9', 'MIDDLEMOUSE', 'WHEELUPMOUSE', 'WHEELDOWNMOUSE'] + action = StringProperty(name="action", default="select") + + @classmethod + def poll(self, context): + global vars_dict + return vars_dict['select_points'] is not None + + def modal(self, context, event): + global vars_dict + context.area.tag_redraw() + if event.type in self.pass_keys: + return {'PASS_THROUGH'} + return vars_dict['select_points'].modal(context, event) + + def invoke(self, context, event): + global vars_dict + if vars_dict['select_points'] is None: + self.report({'WARNING'}, "Use detect before") + return {'CANCELLED'} + elif context.space_data.type == 'VIEW_3D': + vars_dict['select_points'].init(self, context, self.action) + context.window_manager.modal_handler_add(self) + return {'RUNNING_MODAL'} + else: + self.report({'WARNING'}, "Active space must be a View3d") + return {'CANCELLED'} + + +class ARCHIPACK_OP_PolyLib_Pick2DLines(Operator): + bl_idname = "archipack.polylib_pick_2d_lines" + bl_label = "Pick lines" + bl_description = "Pick lines" + bl_options = {'REGISTER', 'UNDO'} + pass_keys = ['NUMPAD_0', 'NUMPAD_1', 'NUMPAD_3', 'NUMPAD_4', + 'NUMPAD_5', 'NUMPAD_6', 'NUMPAD_7', 'NUMPAD_8', + 'NUMPAD_9', 'MIDDLEMOUSE', 'WHEELUPMOUSE', 'WHEELDOWNMOUSE'] + action = StringProperty(name="action", default="select") + + @classmethod + def poll(self, context): + global vars_dict + return vars_dict['select_lines'] is not None + + def modal(self, context, event): + global vars_dict + context.area.tag_redraw() + if event.type in self.pass_keys: + return {'PASS_THROUGH'} + return vars_dict['select_lines'].modal(context, event) + + def invoke(self, context, event): + global vars_dict + if vars_dict['select_lines'] is None: + self.report({'WARNING'}, "Use detect before") + return {'CANCELLED'} + elif context.space_data.type == 'VIEW_3D': + vars_dict['select_lines'].init(self, context, self.action) + context.window_manager.modal_handler_add(self) + return {'RUNNING_MODAL'} + else: + self.report({'WARNING'}, "Active space must be a View3d") + return {'CANCELLED'} + + +class ARCHIPACK_OP_PolyLib_Pick2DPolygons(Operator): + bl_idname = "archipack.polylib_pick_2d_polygons" + bl_label = "Pick 2d" + bl_description = "Pick polygons" + bl_options = {'REGISTER', 'UNDO'} + pass_keys = ['NUMPAD_0', 'NUMPAD_1', 'NUMPAD_3', 'NUMPAD_4', + 'NUMPAD_5', 'NUMPAD_6', 'NUMPAD_7', 'NUMPAD_8', + 'NUMPAD_9', 'MIDDLEMOUSE', 'WHEELUPMOUSE', 'WHEELDOWNMOUSE'] + action = StringProperty(name="action", default="select") + + @classmethod + def poll(self, context): + global vars_dict + return vars_dict['select_polygons'] is not None + + def modal(self, context, event): + global vars_dict + context.area.tag_redraw() + if event.type in self.pass_keys: + return {'PASS_THROUGH'} + return vars_dict['select_polygons'].modal(context, event) + + def invoke(self, context, event): + global vars_dict + if vars_dict['select_polygons'] is None: + self.report({'WARNING'}, "Use detect before") + return {'CANCELLED'} + elif context.space_data.type == 'VIEW_3D': + vars_dict['select_polygons'].init(self, context, self.action) + context.window_manager.modal_handler_add(self) + return {'RUNNING_MODAL'} + else: + self.report({'WARNING'}, "Active space must be a View3d") + return {'CANCELLED'} + + +class ARCHIPACK_OP_PolyLib_Detect(Operator): + bl_idname = "archipack.polylib_detect" + bl_label = "Detect Polygons" + bl_description = "Detect polygons from unordered splines" + bl_options = {'REGISTER', 'UNDO'} + extend = FloatProperty(name="extend", default=0.01, subtype='DISTANCE', unit='LENGTH', min=0) + + @classmethod + def poll(self, context): + return len(context.selected_objects) > 0 and context.object is not None and context.object.type == 'CURVE' + + def execute(self, context): + global vars_dict + print("Detect") + t = time.time() + objs = [obj for obj in context.selected_objects if obj.type == 'CURVE'] + + if len(objs) < 1: + self.report({'WARNING'}, "Select a curve object before") + return {'CANCELLED'} + + for obj in objs: + obj.select = False + + coordsys = CoordSys(objs) + + vars_dict['point_tree'] = Qtree(coordsys, extend=0.5 * EPSILON) + vars_dict['seg_tree'] = Qtree(coordsys, extend=self.extend) + + # Shape based union + shapes = [] + Io.curves_to_shapes(objs, coordsys, context.window_manager.archipack_polylib.resolution, shapes) + union = ShapeOps.union(shapes, self.extend) + + # output select points + vars_dict['select_points'] = SelectPoints(shapes, coordsys) + + geoms = Io.shapes_to_geoms(union) + + # output select_lines + vars_dict['select_lines'] = SelectLines(geoms, coordsys) + + # Shapely based union + # vars_dict['select_polygons'].io.curves_as_shapely(objs, lines) + # geoms = vars_dict['select_polygons'].ops.union(lines, self.extend) + + result, dangles, cuts, invalids = ShapelyOps.detect_polygons(geoms) + vars_dict['select_polygons'] = SelectPolygons(result, coordsys) + + if len(invalids) > 0: + errs = Io.to_curve(context.scene, coordsys, invalids, "invalid_polygons") + err_mat = vars_dict['select_polygons'].build_display_mat("Invalid_polygon", (1, 0, 0)) + # curve.data.bevel_depth = 0.02 + errs.color = (1, 0, 0, 1) + if len(errs.data.materials) < 1: + errs.data.materials.append(err_mat) + errs.active_material = err_mat + errs.select = True + self.report({'WARNING'}, str(len(invalids)) + " invalid polygons detected") + print("Detect :%.2f seconds polygons:%s invalids:%s" % (time.time() - t, len(result), len(invalids))) + return {'FINISHED'} + + +class ARCHIPACK_OP_PolyLib_Offset(Operator): + bl_idname = "archipack.polylib_offset" + bl_label = "Offset" + bl_description = "Offset lines" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(self, context): + return len(context.selected_objects) > 0 and context.object is not None and context.object.type == 'CURVE' + + def execute(self, context): + wm = context.window_manager.archipack_polylib + objs = list(obj for obj in context.selected_objects if obj.type == 'CURVE') + if len(objs) < 1: + self.report({'WARNING'}, "Select a curve object before") + return {'CANCELLED'} + for obj in objs: + obj.select = False + lines = [] + coordsys = Io.curves_to_geoms(objs, wm.resolution, lines) + offset = [] + for line in lines: + res = line.parallel_offset(wm.offset_distance, wm.offset_side, resolution=wm.offset_resolution, + join_style=int(wm.offset_join_style), mitre_limit=wm.offset_mitre_limit) + offset.append(res) + Io.to_curve(context.scene, coordsys, offset, 'offset') + return {'FINISHED'} + + +class ARCHIPACK_OP_PolyLib_Simplify(Operator): + bl_idname = "archipack.polylib_simplify" + bl_label = "Simplify" + bl_description = "Simplify lines" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(self, context): + return (len(context.selected_objects) > 0 and + context.object is not None and + context.object.type == 'CURVE') + + def execute(self, context): + global vars_dict + wm = context.window_manager.archipack_polylib + objs = [obj for obj in context.selected_objects if obj.type == 'CURVE'] + if len(objs) < 1: + self.report({'WARNING'}, "Select a curve object before") + return {'CANCELLED'} + for obj in objs: + obj.select = False + simple = [] + lines = [] + coordsys = Io.curves_to_geoms(objs, wm.resolution, lines) + for line in lines: + res = line.simplify(wm.simplify_tolerance, preserve_topology=wm.simplify_preserve_topology) + simple.append(res) + Io.to_curve(context.scene, coordsys, simple, 'simplify') + return {'FINISHED'} + + +class ARCHIPACK_OP_PolyLib_OutputPolygons(Operator): + bl_idname = "archipack.polylib_output_polygons" + bl_label = "Output Polygons" + bl_description = "Output all polygons" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(self, context): + global vars_dict + return vars_dict['select_polygons'] is not None + + def execute(self, context): + global vars_dict + result = Io.to_curve(context.scene, vars_dict['select_polygons'].coordsys, + vars_dict['select_polygons'].geoms, 'polygons') + context.scene.objects.active = result + return {'FINISHED'} + + +class ARCHIPACK_OP_PolyLib_OutputLines(Operator): + bl_idname = "archipack.polylib_output_lines" + bl_label = "Output lines" + bl_description = "Output all lines" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(self, context): + global vars_dict + return vars_dict['select_lines'] is not None + + def execute(self, context): + global vars_dict + result = Io.to_curve(context.scene, vars_dict['select_lines'].coordsys, + vars_dict['select_lines'].geoms, 'lines') + context.scene.objects.active = result + return {'FINISHED'} + + +class ARCHIPACK_OP_PolyLib_Solidify(Operator): + bl_idname = "archipack.polylib_solidify" + bl_label = "Extrude" + bl_description = "Extrude all polygons" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(self, context): + return (len(context.selected_objects) > 0 and + context.object is not None and + context.object.type == 'CURVE') + + def execute(self, context): + wm = context.window_manager.archipack_polylib + objs = [obj for obj in context.selected_objects if obj.type == 'CURVE'] + if len(objs) < 1: + self.report({'WARNING'}, "Select a curve object before") + return {'CANCELLED'} + for obj in objs: + obj.data.dimensions = '2D' + mod = obj.modifiers.new("Solidify", 'SOLIDIFY') + mod.thickness = wm.solidify_thickness + mod.offset = 1.00 + mod.use_even_offset = True + mod.use_quality_normals = True + return {'FINISHED'} + + +class archipack_polylib(PropertyGroup): + bl_idname = 'archipack.polylib_parameters' + extend = FloatProperty( + name="Extend", + description="Extend to closest intersecting segment", + default=0.01, + subtype='DISTANCE', unit='LENGTH', min=0 + ) + offset_distance = FloatProperty( + name="Distance", + default=0.05, + subtype='DISTANCE', unit='LENGTH', min=0 + ) + offset_side = EnumProperty( + name="Side", default='left', + items=[('left', 'Left', 'Left'), + ('right', 'Right', 'Right')] + ) + offset_resolution = IntProperty( + name="Resolution", default=16 + ) + offset_join_style = EnumProperty( + name="Style", default='2', + items=[('1', 'Round', 'Round'), + ('2', 'Mitre', 'Mitre'), + ('3', 'Bevel', 'Bevel')] + ) + offset_mitre_limit = FloatProperty( + name="Mitre limit", + default=10.0, + subtype='DISTANCE', + unit='LENGTH', min=0 + ) + simplify_tolerance = FloatProperty( + name="Tolerance", + default=0.01, + subtype='DISTANCE', unit='LENGTH', min=0 + ) + simplify_preserve_topology = BoolProperty( + name="Preserve topology", + description="Preserve topology (fast without, but may introduce self crossing)", + default=True + ) + solidify_thickness = FloatProperty( + name="Thickness", + default=2.7, + subtype='DISTANCE', unit='LENGTH', min=0 + ) + resolution = IntProperty( + name="Bezier resolution", min=0, default=12 + ) + + +@persistent +def load_handler(dummy): + global vars_dict + vars_dict['select_polygons'] = None + vars_dict['select_lines'] = None + vars_dict['seg_tree'] = None + vars_dict['point_tree'] = None + + +def register(): + global vars_dict + vars_dict = { + # spacial tree for segments and points + 'seg_tree': None, + 'point_tree': None, + # keep track of shapely geometry selection sets + 'select_polygons': None, + 'select_lines': None, + 'select_points': None + } + bpy.utils.register_class(ARCHIPACK_OP_PolyLib_Pick2DPolygons) + bpy.utils.register_class(ARCHIPACK_OP_PolyLib_Pick2DLines) + bpy.utils.register_class(ARCHIPACK_OP_PolyLib_Pick2DPoints) + bpy.utils.register_class(ARCHIPACK_OP_PolyLib_OutputPolygons) + bpy.utils.register_class(ARCHIPACK_OP_PolyLib_OutputLines) + bpy.utils.register_class(ARCHIPACK_OP_PolyLib_Offset) + bpy.utils.register_class(ARCHIPACK_OP_PolyLib_Simplify) + bpy.utils.register_class(ARCHIPACK_OP_PolyLib_Detect) + bpy.utils.register_class(ARCHIPACK_OP_PolyLib_Solidify) + bpy.utils.register_class(archipack_polylib) + bpy.types.WindowManager.archipack_polylib = PointerProperty(type=archipack_polylib) + bpy.app.handlers.load_post.append(load_handler) + + +def unregister(): + global vars_dict + del vars_dict + bpy.utils.unregister_class(ARCHIPACK_OP_PolyLib_Pick2DPolygons) + bpy.utils.unregister_class(ARCHIPACK_OP_PolyLib_Pick2DLines) + bpy.utils.unregister_class(ARCHIPACK_OP_PolyLib_Pick2DPoints) + bpy.utils.unregister_class(ARCHIPACK_OP_PolyLib_Detect) + bpy.utils.unregister_class(ARCHIPACK_OP_PolyLib_OutputPolygons) + bpy.utils.unregister_class(ARCHIPACK_OP_PolyLib_OutputLines) + bpy.utils.unregister_class(ARCHIPACK_OP_PolyLib_Offset) + bpy.utils.unregister_class(ARCHIPACK_OP_PolyLib_Simplify) + bpy.utils.unregister_class(ARCHIPACK_OP_PolyLib_Solidify) + bpy.utils.unregister_class(archipack_polylib) + bpy.app.handlers.load_post.remove(load_handler) + del bpy.types.WindowManager.archipack_polylib + + +if __name__ == "__main__": + register() |