#!/usr/bin/env python
#-----------------------------------------------------------------------------
# Copyright 2012-2016 Claude Zervas
# email: claude@utlco.com
#-----------------------------------------------------------------------------
"""
An Inkscape extension to create quasicrystalline/Penrose tesselations.
====
"""
# Python 3 compatibility boilerplate
from __future__ import (absolute_import, division,
print_function, unicode_literals)
from future_builtins import *
import sys
import math
import random
import gettext
import logging
import geom.debug
from geom import quasi
from geom import planargraph
from geom import transform2d
from geom import polygon
from inkscape import inkext
__version__ = "0.2"
_ = gettext.gettext
logger = logging.getLogger(__name__)
[docs]class IdentityProjector(object):
"""Identity projection. No distortion."""
[docs] def project(self, p):
return p
[docs]class SphericalProjector(IdentityProjector):
"""Project a point on to a sphere."""
def __init__(self, center, radius, invert=False):
self.center = center
self.radius = radius
self.invert = invert
[docs] def project(self, p):
v = p - self.center
h = v.length()
if h > self.radius:
return p
scale = math.sin((h * (math.pi / 2)) / self.radius)
if not self.invert:
scale = (self.radius * scale) / h
return (v * scale) + self.center
[docs]class QuasiExtension(inkext.InkscapeExtension):
"""Inkscape plugin that creates quasi-crystal-like patterns.
Based on quasi.c by Eric Weeks.
"""
_styles = {
'infotext':
'font-size:.2;font-style:normal;font-weight:normal;'
'line-height:125%;letter-spacing:0px;word-spacing:0px;'
'fill:#000000;fill-opacity:1;stroke:none;font-family:Sans',
'dot':
'fill:%s;stroke-width:.2;stroke:#000000;',
'frame':
'fill:none;stroke-width:%s;stroke:#504030;',
'margins':
'fill:none;stroke-width:.1;stroke:#3030ff;',
'bbox':
'fill:none;stroke-width:.1;stroke:#30f090;',
'polygon':
'fill:none;stroke-opacity:1.0;stroke-linejoin:round;'
'stroke-width:$polygon_stroke_width;stroke:$polygon_stroke;',
'polygon_filled':
'fill:%s;stroke:%s;stroke-width:$polygon_fill_stroke_width;',
'polygon_circle':
'stroke-opacity:1.0;stroke-linejoin:round;'
'fill:none;stroke-width:.1;stroke:#903030;',
'polygon_ellipse':
'stroke-opacity:1.0;stroke-linejoin:round;'
'fill:none;stroke-width:.1;stroke:#306060;',
'polyseg':
'fill:none;stroke-opacity:0.8;'
'stroke-width:$polyseg_stroke_width;stroke:$polyseg_stroke;',
'polyseg_color':
'fill:none;stroke-opacity:0.8;'
'stroke-width:$polyseg_stroke_width;stroke:%s;',
'segment':
'fill:none;stroke-opacity:0.8;'
'stroke-width:$segment_stroke_width;stroke:$segment_stroke;',
'segbox':
'fill:%s;stroke:None',
'segchain':
'fill:%s;fill-opacity:0.5;stroke-opacity:0.75;stroke-linejoin:round;'
'stroke-width:$segchain_stroke_width;stroke:$segchain_stroke;',
}
_style_defaults = {
'infotext_size': '12pt',
'margin_stroke_width': '.1in',
'margin_stroke': '#000000',
'polygon_fill_stroke_width': '1px',
'polygon_stroke_width': '3pt',
'polygon_stroke': '#00ff00',
'polyseg_stroke_width': '.1in',
'polyseg_stroke': '#000000',
'segment_stroke_width': '3pt',
'segment_stroke': '#000000',
'segchain_stroke_width': '1pt',
'segchain_stroke': '#000000',
}
_FILL_LUT = {
'none': ('#808080',),
'cmy': ('#00FFFF', '#FF00FF', '#FFFF00'),
'rgb': ('#FF0000', '#00FF00', '#0000FF'),
'rainbow': ('#FF0000', '#FF7F00', '#FFFF00', '#00FF00',
'#0000FF', '#4B0082', '#8B00FF'),
# Gray 5% increments
'gray05': ('#f2f2f2', '#e6e6e6', '#d9d9d9', '#cccccc',
'#bfbfbf', '#b3b3b3', '#a6a6a6', '#999999',
'#8c8c8c', '#808080', '#737373', '#666666',
'#595959', '#4d4d4d', '#404040', '#333333',
'#262626', '#1a1a1a', '#0d0d0d', '#000000'),
# Gray 10% increments
'gray10': ('#e6e6e6', '#cccccc', '#b3b3b3', '#999999',
'#808080', '#666666', '#4d4d4d', '#333333',
'#1a1a1a', '#000000'),
'red05': ('#ffe5e5', '#ffcccc', '#ffb3b3', '#ff9999',
'#ff8080', '#ff6666', '#ff4d4d', '#ff3333',
'#ff1a1a', '#ff0000', '#e60000', '#cc0000',
'#b30000', '#990000', '#800000', '#660000',
'#4d0000', '#330000', '#1a0000', '#000000'),
'red10': ('#ffcccc', '#ff9999', '#ff6666', '#ff3333',
'#ff0000', '#cc0000', '#990000', '#660000',
'#330000', '#000000'),
'green10': ('#ccffcc', '#99ff99', '#66ff66', '#45ff45',
'#00ff00', '#00ee20', '#00dd00', '#00bb00',
'#009900', '#007700', '#006600',
'#003300', '#000000'),
'yellow05': ('#fffde5', '#fffacc', '#fff8b3', '#fff599',
'#fff380', '#fff166', '#ffee4d', '#ffec33',
'#ffe91a', '#ffe700', '#e6d000', '#ccb900',
'#b3a200', '#998b00', '#807300', '#665c00',
'#4d4500', '#332e00', '#1a1700', '#000000',),
'yellow10': ('#fffde5', '#fff8b3', '#fff380', '#ffee4d',
'#ffe91a', '#e6d000', '#b3a200', '#807300',
'#4d4500', '#4d4500', '#1a1700',),
'gray': [],
'red': [],
'yellow': [],
'green': [],
'blue': [],
}
POLYGON_SORT_NONE, \
POLYGON_SORT_INSIDE_OUT, \
POLYGON_SORT_OUTSIDE_IN = range(3)
# Scale multiplier. This should be about right to get the whole
# thing on a A4 size sheet of paper at 1.0 scale.
_SCALE_SCALE = .1
[docs] def run(self):
"""Main entry point for Inkscape plugins.
"""
random.seed()
geom.set_epsilon(self.options.epsilon)
geom.debug.set_svg_context(self.debug_svg)
doc_size = geom.P(self.svg.get_document_size())
self.doc_center = doc_size / 2
# Determine clipping rectangle which is bounded by the document
# or the margins, depending on user options. The margins can
# be outside the document bounds.
# (Y axis is inverted in Inkscape)
clip_rect = None # Default
bottom_left = geom.P(self.options.margin_left, self.options.margin_top)
top_right = doc_size - geom.P(self.options.margin_right,
self.options.margin_bottom)
self.margin_clip_rect = geom.box.Box(bottom_left, top_right)
doc_clip_rect = geom.box.Box(geom.P(0, 0), doc_size)
if self.options.clip_to_doc and self.options.clip_to_margins:
clip_rect = doc_clip_rect.intersection(self.margin_clip_rect)
elif self.options.clip_to_doc:
clip_rect = doc_clip_rect
elif self.options.clip_to_margins:
clip_rect = self.margin_clip_rect
# The clipping region can be a circle or a rectangle
if clip_rect is not None and self.options.clip_to_circle:
radius = min(clip_rect.width(), clip_rect.height()) / 2.0
self.clip_region = geom.ellipse.Ellipse(clip_rect.center(), radius)
else:
self.clip_region = clip_rect
if self.options.clip_offset_center:
clip_offset = clip_rect.center() - self.doc_center
self.options.offset_x += clip_offset.x
self.options.offset_y += clip_offset.y
# Optionally insert spherical point projection
if self.options.project_sphere:
projector = SphericalProjector(self.doc_center,
self.options.project_radius,
invert=self.options.project_invert)
else:
projector = IdentityProjector()
# Set up plotter transform for rotation, scale, and offset.
# Origin at document center.
scale = self.options.scale * self._SCALE_SCALE
offset = geom.P(self.doc_center) + geom.P(self.options.offset_x,
self.options.offset_y)
transform1 = transform2d.matrix_rotate(self.options.rotate)
transform2 = transform2d.matrix_scale_translate(scale, scale,
offset.x, offset.y)
plot_transform = transform2d.compose_transform(transform1, transform2)
plotter = _QuasiPlotter(self.clip_region, plot_transform, projector)
# Create color LUTs
for i in range(255):
ci = 255 - i
self._FILL_LUT['red'].append('#%02x0000' % ci)
self._FILL_LUT['yellow'].append('#%02x%02x00' % (ci, ci))
q = quasi.Quasi()
q.offset_salt_x = self.options.salt_x
q.offset_salt_y = self.options.salt_y
q.skinnyfat_ratio = self.options.skinnyfat_ratio
q.segment_ratio = self.options.segment_ratio
q.segtype_skinny = self.options.segtype_skinny
q.segtype_fat = self.options.segtype_fat
q.segment_split_cross = self.options.segment_split_cross
q.symmetry = self.options.symmetry
q.numlines = self.options.numlines
q.plotter = plotter
q.color_fill = self.options.polygon_fill
q.color_by_polytype = self.options.polygon_zfill
q.quasi()
# Re-center the quasi polygons to the clip region borders
if self.options.clip_recenter and self.clip_region is not None:
q.plotter.recenter()
polygon_segment_graph = planargraph.Graph()
for poly in q.plotter.polygons:
polygon_segment_graph.add_poly(poly)
polygon_segments = list(polygon_segment_graph.edges)
# Optionally sort the polygons to change drawing order.
if self.options.polygon_sort != self.POLYGON_SORT_NONE:
outer_hull = polygon_segment_graph.boundary_polygon()
hull_centroid = polygon.centroid(outer_hull)
# Find the distance of the farthest polygon
max_d = 0.0
for poly in q.plotter.polygons:
d = hull_centroid.distance(polygon.centroid(poly))
if d > max_d:
max_d = d
segment_size = q.plotter.polygons[0][0].distance(q.plotter.polygons[0][1])
# Secondary sort key is angular location
angle_key = lambda poly: hull_centroid.ccw_angle2(hull_centroid + geom.P(1, 0), polygon.centroid(poly))
q.plotter.polygons.sort(key=angle_key)
angle_key = lambda segment: hull_centroid.ccw_angle2(hull_centroid + geom.P(1, 0), segment.midpoint())
polygon_segments.sort(key=angle_key)
q.plotter.segments.sort(key=angle_key)
# Primary sort key is distance from centroid
dist_key = lambda poly: int(hull_centroid.distance(polygon.centroid(poly)) / segment_size)
dist_key2 = lambda segment: int(hull_centroid.distance(segment.midpoint()) / segment_size)
if self.options.polygon_sort == self.POLYGON_SORT_INSIDE_OUT:
q.plotter.polygons.sort(key=dist_key)
polygon_segments.sort(key=dist_key2)
q.plotter.segments.sort(key=dist_key2)
elif self.options.polygon_sort == self.POLYGON_SORT_OUTSIDE_IN:
q.plotter.polygons.sort(key=dist_key, reverse=True)
polygon_segments.sort(key=dist_key2, reverse=True)
q.plotter.segments.sort(key=dist_key2, reverse=True)
# angle_key = lambda poly: hull_centroid.ccw_angle2(hull_centroid + geom.P(1, 0), polygon.centroid(poly))
# q.plotter.polygons.sort(key=angle_key)
# angle_key = lambda segment: hull_centroid.ccw_angle2(hull_centroid + geom.P(1, 0), segment.midpoint())
# polygon_segments.sort(key=angle_key)
# Update styles with any command line option values
self._styles.update(self.svg.styles_from_templates(
self._styles, self._style_defaults, vars(self.options)))
# logger.debug('colors: %d' % len(plotter.color_count))
# for color in sorted(plotter.color_count.keys()):
# logger.debug('[%.5f]: %d' % (color, plotter.color_count[color]))
if self.options.create_info_layer:
self._draw_info_layer()
if self.options.margin_draw:
self._draw_margins(q.plotter.bbox())
if self.options.polygon_draw:
self._draw_polygons(q.plotter)
# self._draw_polygon_circles(q.plotter.polygons)
if self.options.polyseg_draw:
self._draw_polygon_segments(polygon_segments)
if self.options.polygon_mult > 0:
self._draw_inset_polygons(q.plotter.polygons,
self.options.polygon_mult_spacing,
self.options.polygon_mult)
if self.options.ellipse_draw:
self._draw_polygon_ellipses(q.plotter.polygons,
self.options.ellipse_inset)
if self.options.segtype_skinny == quasi.Quasi.SEG_NONE \
and self.options.segtype_fat == quasi.Quasi.SEG_NONE:
self.options.segment_draw = False
self.options.segpath_draw = False
if self.options.segment_draw:
self._draw_segments(q.plotter.segments)
self._draw_segboxes(q.plotter)
if self.options.segpath_draw:
self._draw_segment_chains(q.plotter.segments)
if self.options.frame_draw and (self.options.frame_width > 0 and
self.options.frame_height > 0):
self._draw_frame()
def _draw_info_layer(self):
"""Draw some info about this tesselation."""
layer = self.svg.create_layer('q_info')
info = ('Symmetry: %d' % self.options.symmetry,
'Scale: %.1f' % self.options.scale,
'Rotation: %.2f' % math.degrees(self.options.rotate),
'Offset X: %.2f' % self.options.offset_x,
'Offset Y: %.2f' % self.options.offset_y,
'Skinny-fat ratio: %.2f' % self.options.skinnyfat_ratio,
'Segment ratio: %.2f' % self.options.segment_ratio,
'Num lines: %d' % self.options.numlines,
'Salt X: %.5f' % self.options.salt_x,
'Salt Y: %.5f' % self.options.salt_y,
'Epsilon: %.5f' % self.options.epsilon,
'Margins: T%.2f, L%.2f, R%.2f, B%.2f' % (self.options.margin_top,
self.options.margin_left,
self.options.margin_right,
self.options.margin_bottom),
'Polygon sort: %d' % self.options.polygon_sort,
)
self.svg.create_text(info, self.svg.unit2uu('10px'),
self.svg.unit2uu('30px'),
line_height=self.svg.unit2uu('25px'),
style=self._styles['infotext'],
parent=layer)
def _draw_margins(self, bbox):
layer = self.svg.create_layer('q_margins')
if isinstance(self.clip_region, geom.ellipse.Ellipse):
self.svg.create_ellipse(self.clip_region.center,
self.clip_region.rx, self.clip_region.ry,
angle=0.0,
style=self._styles['margins'], parent=layer)
else:
margin_vertices = [self.clip_region.p1,
geom.P(self.clip_region.p1.x,
self.clip_region.p2.y),
self.clip_region.p2,
geom.P(self.clip_region.p2.x,
self.clip_region.p1.y),
]
self.svg.create_polygon(margin_vertices,
style=self._styles['margins'], parent=layer)
bbox_vertices = [bbox.p1, geom.P(bbox.p1.x, bbox.p2.y),
bbox.p2, geom.P(bbox.p2.x, bbox.p1.y)]
self.svg.create_polygon(bbox_vertices,
style=self._styles['bbox'], parent=layer)
def _draw_frame(self):
layer = self.svg.create_layer('q_frame')
offset = self.options.frame_thickness / 2
cx = self.options.frame_width / 2 + offset
cy = self.options.frame_height / 2 + offset
frame_vertices = [self.doc_center + geom.P(cx, cy),
self.doc_center + geom.P(cx, -cy),
self.doc_center + geom.P(-cx, -cy),
self.doc_center + geom.P(-cx, cy)]
style = self._styles['frame'] % self.options.frame_thickness
self.svg.create_polygon(frame_vertices,
style=style, parent=layer)
def _draw_polygons(self, plotter):
polygon_list = plotter.polygons
layer1_name = 'q_polygons_%d' % self.options.symmetry
layer1 = self.svg.create_layer(layer1_name, incr_suffix=True)
# if self.options.create_culledrhombus_layer:
# layer2_name = 'q_polygons_x_%d' % self.options.symmetry
# layer2 = self.svg.create_layer(layer2_name, incr_suffix=True)
# if self.options.polygon_fill and self.options.polygon_stroke == 'none':
if self.options.polygon_fill:
fill_lut = self._FILL_LUT[self.options.polygon_fill_lut]
fill_lut_offset = self.options.polygon_fill_lut_offset
fill_style_template = self._styles['polygon_filled']
fill_colors = sorted(plotter.color_count.keys())
else:
style = self._styles['polygon']
# logger.debug('fill: %s', str(self.options.polygon_fill))
# logger.debug('style: %s', style)
color_index = 0
for i, vertices in enumerate(polygon_list):
if self.options.polygon_fill:
if self.options.polygon_zfill:
color = plotter.polygon_colors[i]
color_index = fill_colors.index(color)
# color_index = int(len(fill_lut) * color / 2)
else:
color_index = (color_index + 1)
color_index = (color_index + fill_lut_offset) % len(fill_lut)
css_color = fill_lut[color_index]
style = fill_style_template % (css_color, css_color)
self.svg.create_polygon(vertices, style=style, parent=layer1)
# if self.options.create_culledrhombus_layer:
# d1 = vertices[0].distance(vertices[2])
# d2 = vertices[1].distance(vertices[3])
# if self.options.min_rhombus_width < min(d1, d2):
# style = self._styles['polygon'] % (fill_lut[color_index],)
# self.svg.create_polygon(vertices, style=style, parent=layer2)
def _draw_segboxes(self, plotter):
polygon_list = plotter.segpolys
fill_lut = self._FILL_LUT[self.options.polygon_fill_lut]
fill_lut_offset = self.options.polygon_fill_lut_offset
fill_style_template = self._styles['segbox']
fill_colors = sorted(plotter.segpoly_color_count.keys())
layer_name_prefix = 'q_segpolys_%d' % self.options.symmetry
if self.options.segbox_layers:
layers = []
for i in range(len(fill_colors)):
layer_name = layer_name_prefix + '_' + str(i)
layer = self.svg.create_layer(layer_name, incr_suffix=True)
layers.append(layer)
else:
layer1 = self.svg.create_layer(layer_name_prefix, incr_suffix=True)
color_index = 0
for i, vertices in enumerate(polygon_list):
if self.options.polygon_zfill:
color = plotter.segpoly_colors[i]
color_index = fill_colors.index(color)
else:
color_index = (color_index + 1)
css_color = fill_lut[(color_index + fill_lut_offset) % len(fill_lut)]
style = fill_style_template % (css_color,)
if self.options.segbox_layers:
self.svg.create_polygon(vertices, style=style,
parent=layers[color_index])
else:
self.svg.create_polygon(vertices, style=style, parent=layer1)
def _draw_inset_polygons(self, polygon_list, offset, nmax=1):
style = self._styles['polygon']
offset_total = offset
for n in range(nmax):
layer = self.svg.create_layer('q_insetpolygons_%d' % (n + 1,),
incr_suffix=True)
for vertices in polygon_list:
vertices = self._inset_polygon(vertices, offset_total)
if vertices is not None:
self.svg.create_polygon(vertices, style=style, parent=layer)
offset_total += offset
def _inset_polygon(self, vertices, offset):
"""Inset the polygon by the amount :offset:"""
L1 = geom.Line(vertices[0], vertices[1])
L2 = geom.Line(vertices[1], vertices[2])
L3 = geom.Line(vertices[2], vertices[3])
L4 = geom.Line(vertices[3], vertices[0])
d1 = L1.distance_to_point(L3.p1)
d2 = L2.distance_to_point(L1.p1)
if d1 >= offset * 2 and d2 >= offset * 2:
offset *= L1.which_side(L2.p2)
L1_o = L1.offset(offset)
L2_o = L2.offset(offset)
L3_o = L3.offset(offset)
L4_o = L4.offset(offset)
p1 = L4_o.intersection(L1_o)
p2 = L1_o.intersection(L2_o)
p3 = L2_o.intersection(L3_o)
p4 = L3_o.intersection(L4_o)
return (p1, p2, p3, p4)
return None
def _draw_polygon_circles(self, polygon_list):
layer = self.svg.create_layer('q_polygon_circles', incr_suffix=True)
style = self._styles['polygon_circle']
for poly in polygon_list:
center = geom.Line(poly[0], poly[2]).midpoint()
a = abs(poly[1].angle2(poly[0], poly[2]))
radius = math.sin(a) * poly[1].distance(poly[2]) / 2
# if a > math.pi:
# radius = math.sin(a - math.pi) * poly[0].distance(poly[1]) / 2
# else:
# radius = math.sin(a) * poly[1].distance(poly[2]) / 2
self.svg.create_circle(center, radius, style=style, parent=layer)
def _draw_polygon_ellipses(self, polygon_list, inset):
layer = self.svg.create_layer('q_polygon_ellipses', incr_suffix=True)
style = self._styles['polygon_ellipse']
for poly in polygon_list:
e = geom.ellipse.ellipse_in_parallelogram(poly)
self.svg.create_ellipse(e.center, e.rx - inset, e.ry - inset,
e.phi, style=style, parent=layer)
def _draw_polygon_segments(self, segment_list):
fill_lut = self._FILL_LUT[self.options.polyseg_lut]
if self.options.polyseg_layer_per_color:
layers = []
for i in range(len(fill_lut)):
layer = self.svg.create_layer('q_polyseg_%d' % i,
incr_suffix=True)
layers.append(layer)
else:
layer = self.svg.create_layer('q_polysegs', incr_suffix=True)
color_index = 0
for segment in segment_list:
if self.options.polyseg_scale != 1.0:
seglen = segment.length()
ext = (seglen * self.options.polyseg_scale) - seglen
segment = segment.extend(ext, from_midpoint=True)
if self.options.polyseg_clip_to_margins:
segment = self.margin_clip_rect.clip_line(segment)
style = self._styles['polyseg_color'] % fill_lut[color_index]
color_index = (color_index + 1) % len(fill_lut)
if self.options.polyseg_layer_per_color:
layer = layers[color_index]
self.svg.create_line(segment.p1, segment.p2, style=style,
parent=layer)
def _draw_segments(self, segment_list):
layer = self.svg.create_layer('q_segments', incr_suffix=True)
for segment in segment_list:
if self.options.segment_scale != 1.0:
seglen = segment.length()
ext = (seglen * self.options.segment_scale) - seglen
segment = segment.extend(ext, from_midpoint=True)
self.svg.create_line(segment.p1, segment.p2,
style=self._styles['segment'],
parent=layer)
def _draw_segment_chains(self, segment_list):
layer = self.svg.create_layer('q_segment_chains', incr_suffix=True)
chain_list = self._create_chains(segment_list)
# Sort segment paths so that the largest are at the bottom of the Z-order
key = lambda v: abs(polygon.area(v))
chain_list.sort(key=key, reverse=True)
for vertices in chain_list:
if self.options.segpath_fillclosed and polygon.is_closed(vertices):
style = self._styles['segchain'] % '#c0c0c0'
else:
style = self._styles['segchain'] % 'none'
if not self.options.segpath_closed or polygon.is_closed(vertices):
self.svg.create_polygon(vertices, close_polygon=False,
close_path=False, style=style,
parent=layer)
def _create_chains(self, segments):
chain_list = []
while segments:
chain = _SegmentChain()
n = 1
while n > 0:
unchained_segments = []
for segment in segments:
if not chain.connect_segment(segment):
unchained_segments.append(segment)
n = len(segments) - len(unchained_segments)
segments = unchained_segments
if chain:
chain_list.append(chain.polyline())
return chain_list
class _SegmentChain(list):
"""A simple polygonal chain as a series of connected line segments.
"""
def __init__(self, min_corner_angle=0.0):
self.min_corner_angle = min_corner_angle
@property
def startp(self):
return self[0].p1
@property
def endp(self):
return self[-1].p2
def connect_segment(self, segment):
"""Try to connect the segment to the chain.
:return: True if successful otherwise False.
"""
if len(self) == 0:
self.append(segment)
return True
if segment.p1 == self.endp:
return self._append_segment(segment)
elif segment.p2 == self.endp:
return self._append_segment(segment.reversed())
elif segment.p2 == self.startp:
return self._prepend_segment(segment)
elif segment.p1 == self.startp:
return self._prepend_segment(segment.reversed())
else:
return False
def polyline(self):
"""Return a list of vertices.
"""
vertices = [self[0].p1]
for segment in self:
vertices.append(segment.p2)
return vertices
def _prepend_segment(self, segment):
angle_ok = self._angle_is_ok(segment, self[0])
if angle_ok:
self.insert(0, segment)
return angle_ok
def _append_segment(self, segment):
angle_ok = self._angle_is_ok(self[-1], segment)
if angle_ok:
self.append(segment)
return angle_ok
def _angle_is_ok(self, seg1, seg2):
return (self.min_corner_angle <= 0.0 or
abs(seg1.p2.angle2(seg1.p1, seg2.p2)) > self.min_corner_angle)
class _QuasiPlotter(quasi.QuasiPlotter):
"""Accumulates the quasi geometry. Also transforms and clips.
"""
def __init__(self, clip_region, transform_matrix, projector, clip_all=True):
"""
:param clip_region: The clipping region
:param transform_matrix: All objects will be tranformed using
this transform matrix.
:param projector: All objects will be transformed using this projector.
:param clip_all: If true the entire polygon or segment will not be plotted
if any part of it is clipped.
"""
self.polygons = []
self.polygon_colors = []
self.segments = []
self.segpolys = []
self.segpoly_colors = []
self.segpoly_color_count = {}
self.clip_region = clip_region
self.transform_matrix = transform_matrix
self.clip_all = clip_all
if projector is None:
self.projector = IdentityProjector()
else:
self.projector = projector
self._xmin = sys.float_info.max
self._ymin = sys.float_info.max
self._xmax = sys.float_info.min
self._ymax = sys.float_info.min
# # Color index incr
# self.color_index = 0
# # Map of color to color_index
# self.color_map = {}
# Count of polygons in each color
self.color_count = {}
def plot_polygon(self, vertices, color):
"""
"""
# assert(0.0 <= color <= 1.0)
xvertices = self._transform(vertices)
if not xvertices:
return False
self._update_bbox(xvertices)
self.polygons.append(xvertices)
# if color in self.color_map:
# color_index = self.color_map[color]
# self.color_count[color_index] += 1
# else:
# self.color_map[color] = self.color_index
# self.color_count[self.color_index] = 1
# color_index = self.color_index
# self.color_index += 1
# assert(color > 0.0 and color < 1.0)
if color in self.color_count:
self.color_count[color] += 1
else:
self.color_count[color] = 1
self.polygon_colors.append(color)
return True
def plot_segment(self, p1, p2):
p1 = self.projector.project(geom.P(p1).transform(self.transform_matrix))
p2 = self.projector.project(geom.P(p2).transform(self.transform_matrix))
# if self.clip_region is None or (self.clip_region.point_inside(p1) and
# self.clip_region.point_inside(p1)):
self.segments.append(geom.Line(p1, p2))
def plot_segpoly(self, vertices, color):
"""
"""
xvertices = self._transform(vertices)
if not xvertices:
return False
self.segpolys.append(xvertices)
if color in self.color_count:
self.segpoly_color_count[color] += 1
else:
self.segpoly_color_count[color] = 1
self.segpoly_colors.append(color)
return True
def recenter(self):
"""Re-center the polygons and segments so the bounding box is centered
within the clipping region.
"""
if self.clip_region is None:
return
cx = (self._xmax - self._xmin) / 2
cy = (self._ymax - self._ymin) / 2
bbox_center = geom.P(self._xmin + cx, self._ymin + cy)
offset = self.clip_region.center - bbox_center
centered_polygons = []
centered_segments = []
for poly in self.polygons:
new_poly = []
for p in poly:
new_poly.append(p + offset)
centered_polygons.append(new_poly)
for segment in self.segments:
offset_segment = geom.Line(segment.p1 + offset, segment.p2 + offset)
centered_segments.append(offset_segment)
self.segments = centered_segments
self.polygons = centered_polygons
self._xmin += offset.x
self._xmax += offset.x
self._ymin += offset.y
self._ymax += offset.y
def bbox(self):
"""Bounding box.
"""
return geom.Box(geom.P(self._xmin, self._ymin),
geom.P(self._xmax, self._ymax))
def _transform(self, vertices):
"""Apply projection and transforms to a list of points.
Returns:
A list of transformed points.
"""
xvertices = []
clip_count = 0
for vertex in vertices:
p = transform2d.matrix_apply_to_point(self.transform_matrix, vertex)
p = self.projector.project(geom.P(p))
xvertices.append(p)
if self.clip_region and not self.clip_region.point_inside(p):
clip_count += 1
if (self.clip_all and clip_count > 0) or clip_count > 3:
return []
return xvertices
def _update_bbox(self, points):
"""Update the bounding box with the given vertex point."""
for p in points:
self._xmin = min(self._xmin, p.x)
self._ymin = min(self._ymin, p.y)
self._xmax = max(self._xmax, p.x)
self._ymax = max(self._ymax, p.y)
_OPTIONSPEC = (
inkext.ExtOption('--scale', '-s', type='docunits', default=5.0, help='Output scale.'),
inkext.ExtOption('--rotate', '-r', type='degrees', default=0.0, help='Rotation.'),
inkext.ExtOption('--symmetry', '-S', type='int', default=5, help='Degrees of symmetry.'),
inkext.ExtOption('--numlines', '-n', type='int', default=30, help='Number of lines.'),
inkext.ExtOption('--offset-x', type='docunits', default=0.0, help='X offset'),
inkext.ExtOption('--offset-y', type='docunits', default=0.0, help='Y offset'),
inkext.ExtOption('--salt-x', type='float', default=0.31416, help='X offset salt'),
inkext.ExtOption('--salt-y', type='float', default=0.64159, help='Y offset salt'),
inkext.ExtOption('--epsilon', type='docunits', default=0.00001, help='Epsilon'),
inkext.ExtOption('--segment-draw', type='inkbool', default=False, help='Draw segments.'),
inkext.ExtOption('--segtype-skinny', '-M', type='int', default=0, help='Midpoint type for skinny diamonds.'),
inkext.ExtOption('--segtype-fat', '-N', type='int', default=0, help='Midpoint type for fat diamonds.'),
inkext.ExtOption('--skinnyfat-ratio', type='float', default=0.5, help='Skinny/fat ratio'),
inkext.ExtOption('--segment-ratio', type='float', default=0.5, help='Segment ratio'),
inkext.ExtOption('--segment-scale', type='float', default=1.0, help='Segment scale.'),
inkext.ExtOption('--segment-split-cross', type='inkbool', default=False, help='Split crossed segments.'),
# inkext.ExtOption('--segment-stroke', default='#000000', help='Segment CSS stroke color.'),
# inkext.ExtOption('--segment-width', default='.1in', help='Segment CSS stroke width.'),
inkext.ExtOption('--segment-sort', type='int', default=0, help='Sort segments by.'),
inkext.ExtOption('--segbox-fill', type='inkbool', default=True, help='Fill segment boxes.'),
inkext.ExtOption('--segbox-layers', type='inkbool', default=True, help='Create layers for box types.'),
inkext.ExtOption('--segpath-draw', type='inkbool', default=False, help='Draw segment paths.'),
inkext.ExtOption('--segpath-closed', type='inkbool', default=False, help='Draw closed polygons only.'),
inkext.ExtOption('--segpath-fillclosed', type='inkbool', default=False, help='Fill closed polygons.'),
inkext.ExtOption('--segpath-min-segments', '-m', type='int', default=1, help='Min segments in path.'),
# inkext.ExtOption('--segpath-stroke', default='#000000', help='Segment CSS stroke color.'),
# inkext.ExtOption('--segpath-width', default='.1in', help='Segment CSS stroke width.'),
inkext.ExtOption('--polygon-draw', type='inkbool', default=True, help='Draw polygons.'),
inkext.ExtOption('--polygon-mult', type='int', default=0, help='Number of concentric polygons.'),
inkext.ExtOption('--min-rhombus-width', type='docunits', default=0.0, help='Minimum rhombus width.'),
inkext.ExtOption('--polygon-mult-spacing', type='docunits', default=0.0, help='Concentric polygon spacing.'),
inkext.ExtOption('--polygon-fill', '-f', type='inkbool', default=False, help='Fill polygons.'),
# inkext.ExtOption('--polygon-colorfill', type='inkbool', default=False, help='Use color fill.'),
inkext.ExtOption('--polygon-zfill', '-z', type='inkbool', default=True, help='Fill color according to polygon type.'),
inkext.ExtOption('--polygon-stroke', default='#f03030', help='Polygon CSS stroke color.'),
inkext.ExtOption('--polygon-fill-lut', default='gray10', help='Fill color LUT'),
inkext.ExtOption('--polygon-fill-lut-offset', type='int', default=0, help='LUT offset'),
# inkext.ExtOption('--polygon-stroke-width', default='.2pt', help='Polygon CSS stroke width.'),
inkext.ExtOption('--polygon-sort', type='int', default=0, help='Sort polygons by.'),
inkext.ExtOption('--ellipse-draw', type='inkbool', default=False, help='Draw ellipses.'),
inkext.ExtOption('--ellipse-cull', type='inkbool', default=False, help='Cull eccentric ellipses.'),
inkext.ExtOption('--ellipse-min-radius', type='docunits', default=1.0, help='Ellipse min radius.'),
inkext.ExtOption('--ellipse-inset', type='docunits', default=0, help='Ellipse inset.'),
inkext.ExtOption('--polyseg-draw', type='inkbool', default=True, help='Draw polygon segments.'),
inkext.ExtOption('--polyseg-scale', type='float', default=1.0, help='Polyseg scale.'),
inkext.ExtOption('--polyseg-stroke', default='#000000', help='Polyseg CSS stroke color.'),
inkext.ExtOption('--polyseg-stroke-width', default='.1in', help='Polyseg CSS stroke width.'),
inkext.ExtOption('--polyseg-lut', default='none', help='Color set.'),
inkext.ExtOption('--polyseg-layer-per-color', type='inkbool', default=False, help='Layer per color.'),
inkext.ExtOption('--polyseg-clip-to-margins', type='inkbool', default=True, help='Clip polyseg to margins.'),
inkext.ExtOption('--clip-to-doc', type='inkbool', default=True, help='Clip to document.'),
inkext.ExtOption('--clip-to-circle', type='inkbool', default=False, help='Circular clip region.'),
inkext.ExtOption('--clip-to-margins', '-C', type='inkbool', default=True, help='Clip to document margins.'),
inkext.ExtOption('--clip-offset-center', type='inkbool', default=False, help='Offset center to clip region.'),
inkext.ExtOption('--clip-recenter', type='inkbool', default=False, help='Re-center bounding box to clip region.'),
inkext.ExtOption('--margin-left', type='docunits', default=0.0, help='Left margin'),
inkext.ExtOption('--margin-right', type='docunits', default=0.0, help='Right margin'),
inkext.ExtOption('--margin-top', type='docunits', default=0.0, help='Top margin'),
inkext.ExtOption('--margin-bottom', type='docunits', default=0.0, help='Bottom margin'),
inkext.ExtOption('--margin-draw', type='inkbool', default=False, help='Draw margins.'),
inkext.ExtOption('--frame-draw', type='inkbool', default=False, help='Draw frame.'),
inkext.ExtOption('--frame-width', type='docunits', default=0.0, help='Frame width'),
inkext.ExtOption('--frame-height', type='docunits', default=0.0, help='Frame height'),
inkext.ExtOption('--frame-thickness', type='docunits', default=1.0, help='Frame thickness'),
inkext.ExtOption('--project-sphere', type='inkbool', default=False, help='Project on to sphere.'),
inkext.ExtOption('--project-invert', type='inkbool', default=False, help='Invert projection.'),
inkext.ExtOption('--project-radius-useclip', type='inkbool', default=False, help='Use clipping circle for radius.'),
inkext.ExtOption('--project-radius', type='docunits', default=0.0, help='Projection radius.'),
inkext.ExtOption('--blowup-scale', type='float', default=1.0, help='Blow up scale.'),
inkext.ExtOption('--create-info-layer', type='inkbool', default=False, help='Create info layer'),
inkext.ExtOption('--create-culledrhombus-layer', type='inkbool', default=False, help='Create culled rhombus layer'),
)
if __name__ == '__main__':
plugin = QuasiExtension()
plugin.main(_OPTIONSPEC)