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

git.blender.org/blender-addons.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'archipack/archipack_polylib.py')
-rw-r--r--archipack/archipack_polylib.py2274
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()