Newer
Older
####################################################################################################
#
# Patro - A Python library to make patterns for fashion design
# Copyright (C) 2017 Fabrice Salvaire
#
# 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 3 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, see <http://www.gnu.org/licenses/>.
#
####################################################################################################
####################################################################################################
__all__ = [
'GraphicScene',
# sphinx
'GraphicSceneScope',
]
####################################################################################################
import logging
from Patro.GeometryEngine import (
Bezier,
Conic,
Line,
Path,
Polygon,
Polyline,
Rectangle,
Segment,
Spline,
Triangle,
)
from Patro.GeometryEngine.Transformation import AffineTransformation2D
from Patro.GeometryEngine.Vector import Vector2D
from . import GraphicItem
####################################################################################################
_module_logger = logging.getLogger(__name__)
####################################################################################################
class GraphicSceneScope:
__ITEM_CTOR__ = {
'circle': GraphicItem.CircleItem,
'cubic_bezier': GraphicItem.CubicBezierItem,
'ellipse': GraphicItem.EllipseItem,
'image': GraphicItem.ImageItem,
# 'path': GraphicItem.PathItem,
# 'polygon': GraphicItem.PolygonItem,
'rectangle': GraphicItem.RectangleItem,
'segment': GraphicItem.SegmentItem,
Fabrice Salvaire
committed
'polyline': GraphicItem.PolylineItem,
'text': GraphicItem.TextItem,
}
##############################################
def __init__(self, transformation=None):
if transformation is None:
transformation = AffineTransformation2D.Identity()
self._transformation = transformation
self._items = {} # id(item) -> item, e.g. for rtree query
self._user_data_map = {}
self._rtree = rtree.index.Index()
# item_id -> bounding_box, used to delete item in rtree (cf. rtree api)
self._item_bounding_box_cache = {}
##############################################
@property
def transformation(self):
return self._transformation
##############################################
def __iter__(self):
return iter(self._items.values())
##############################################
def z_value_iter(self):
# Fixme: cache ???
# Group by z_value and keep inserting order
z_map = {}
for item in self._items.values():
if item.visible:
items = z_map.setdefault(item.z_value, [])
items.append(item)
for z_value in sorted(z_map.keys()):
for item in z_map[z_value]:
yield item
##############################################
@property
def selected_items(self):
# Fixme: cache ?
return [item for item in self._items.values() if item.selected]
##############################################
def unselect_items(self):
for item in self.selected_items:
item.selected = False
##############################################
def add_coordinate(self, name, position):
item = CoordinateItem(name, position)
self._coordinates[name] = item
return item
##############################################
def remove_coordinate(self, name):
del self._coordinates[name]
##############################################
def coordinate(self, name):
return self._coordinates[name]
##############################################
def cast_position(self, position):
"""Cast coordinate and apply scope transformation, *position* can be a coordinate name string of a
:class:`Patro.GeometryEngine.Vector.Vector2D`.
Fabrice Salvaire
committed
"""
# Fixme: cache ?
if isinstance(position, str):
vector = self._coordinates[position].position
elif isinstance(position, Vector2D):
vector = position
return self._transformation * vector
##############################################
item = cls(self, *args, **kwargs)
# print(item, item.user_data, hash(item))
# if item in self._items:
# print('Warning duplicate', item.user_data)
item_id = id(item) # Fixme: hash ???
self._items[item_id] = item
user_data = item.user_data
if user_data is not None:
user_data_id = id(user_data) # Fixme: hash ???
items = self._user_data_map.setdefault(user_data_id, [])
items.append(item)
##############################################
self.update_rtree(item, insert=False)
items = self.item_for_user_data(item.user_data)
if items:
items.remove(item)
del self._items[item]
##############################################
def item_for_user_data(self, user_data):
user_data_id = id(user_data)
return self._user_data_map.get(user_data_id, None)
##############################################
def update_rtree(self):
for item in self._items.values():
if item.dirty:
self.update_rtree_item(item)
##############################################
def update_rtree_item(self, item, insert=True):
item_id = id(item)
old_bounding_box = self._item_bounding_box_cache.pop(item_id, None)
if old_bounding_box is not None:
self._rtree.delete(item_id, old_bounding_box)
if insert:
# try:
bounding_box = item.bounding_box.bounding_box # Fixme: name
self._rtree.insert(item_id, bounding_box)
self._item_bounding_box_cache[item_id] = bounding_box
# except AttributeError:
# print('bounding_box not implemented for', item)
# pass # Fixme:
##############################################
def item_in_bounding_box(self, bounding_box):
# Fixme: Interval2D ok ?
item_ids = self._rtree.intersection(bounding_box)
if item_ids:
return [self._items[item_id] for item_id in item_ids]
else:
return None
##############################################
def item_at(self, position, radius):
x, y = list(position)
bounding_box = (
x - radius, y - radius,
x + radius, y + radius,
)
items = []
for item in self.item_in_bounding_box(bounding_box):
try: # Fixme
distance = item.distance_to_point(position)
# print('distance_to_point {:6.2f} {}'.format(distance, item))
if distance <= radius:
items.append((distance, item))
except NotImplementedError:
pass
return sorted(items, key=lambda pair: pair[0])
##############################################
# Fixme: !!!
# def add_scope(self, *args, **kwargs):
# return self.add_item(GraphicSceneScope, self, *args, **kwargs)
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
def add_geometry(self, item, path_style):
"""Add a geometry primitive"""
ctor = None
points = None
args = []
args_tail = [path_style]
kwargs = dict(user_data=item)
# Bezier
if isinstance(item, Bezier.QuadraticBezier2D):
# ctor = self._scene.quadratic_bezier
# raise NotImplementedError
ctor = self._scene.cubic_bezier
points = list(item.to_cubic().points)
elif isinstance(item, Bezier.CubicBezierItem):
ctor = self._scene.cubic_bezier
# Conic
elif isinstance(item, Conic.Circle2D):
ctor = self._scene.circle
args = [item.radius]
if item.domain:
kwargs['start_angle'] = item.domain.start
kwargs['stop_angle'] = item.domain.stop
elif isinstance(item, Conic.Ellipse2D):
ctor = self._scene.ellipse
args = [item.x_radius, item.y_radius, item.angle]
# Line
elif isinstance(item, Line.Line2D):
# Fixme: extent ???
raise NotImplementedError
# Path
elif isinstance(item, Path.Path2D):
raise NotImplementedError
# Polygon
elif isinstance(item, Path.Polygon2D):
# Fixme: to path
raise NotImplementedError
# Polyline
elif isinstance(item, Polyline.Polyline2D):
ctor = self._scene.polyline
# fixme: to path
# Rectangle
elif isinstance(item, Rectangle.Rectangle2D):
ctor = self._scene.rectangle
# Fixme: to path
# Segment
if isinstance(item, Segment.Segment2D):
ctor = self._scene.segment
# Spline
elif isinstance(item, Spline.BSpline2D):
return self._add_spline(item, path_style)
# Triangle
if isinstance(item, Triangle.Triangle2D):
# Fixme: to path
raise NotImplementedError
# Not implemented
else:
raise NotImplementedError
if ctor is not None:
if points is None:
points = list(item.points)
return ctor(*points, *args, *args_tail, **kwargs)
##############################################
def add_spline(self, item, path_style):
return [
self._scene.cubic_bezier(*bezier.points, path_style, user_data=item)
for bezier in item.to_bezier()
]
##############################################
Fabrice Salvaire
committed
def bezier_path(self, points, degree, *args, **kwargs):
"""Add a Bézier curve with the given control points and degree"""
Fabrice Salvaire
committed
if degree == 1:
method = self.segment
elif degree == 2:
Fabrice Salvaire
committed
method = self.quadratic_bezier
Fabrice Salvaire
committed
elif degree == 3:
method = self.cubic_bezier
else:
raise ValueError('Unsupported degree for Bezier curve: {}'.format(degree))
# Fixme: generic code
number_of_points = len(points)
n = number_of_points -1
if n % degree:
raise ValueError('Wrong number of points for Bezier {} curve: {}'.format(degree, number_of_points))
items = []
for i in range(number_of_points // degree):
j = degree * i
k = j + degree
item = method(*points[j:k+1], *args, **kwargs)
items.append(item)
return items
##############################################
# Register a method in GraphicSceneScope class for each type of graphic item
def wrapper(self, *args, **kwargs):
return wrapper
for name, cls in GraphicSceneScope.__ITEM_CTOR__.items():
setattr(GraphicSceneScope, name, _make_add_item_wrapper(cls))
####################################################################################################
class GraphicScene(GraphicSceneScope):