* guide line position

* use direction from line to shape

* optimize intersection detection

* fix flapack elf

* handle weird guide lines better

* update starting point for self crossing (multiple) fills

* ripple: fixes and non circular join style

* avoid jumps in ripple stitch

* fallback only necessary if shape does not intersect grating

* make valid may return a polygon

* add profiling

* Stitch.__init__ didn't work right and was super slow

* shrink or grow to multipolygon

Co-authored-by: Lex Neva
pull/1718/head
Kaalleen 2022-06-30 19:22:33 +02:00 zatwierdzone przez GitHub
rodzic 725281f075
commit 8d5ef5b663
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
11 zmienionych plików z 236 dodań i 98 usunięć

Wyświetl plik

@ -75,7 +75,7 @@ elif [ "$BUILD" = "linux" ]; then
# error:
#
# ELF load command address/offset not properly aligned
find dist/inkstitch -type f | grep -E '.so($|\.)' | grep -v _fblas | xargs strip
find dist/inkstitch -type f | grep -E '\.so($|\.)' | grep -v _fblas | grep -v _flapack | xargs strip
else
LD_LIBRARY_PATH="${site_packages}/wx" python -m PyInstaller $pyinstaller_args --strip inkstitch.py;
fi

Wyświetl plik

@ -2,7 +2,8 @@
#
# Copyright (c) 2010 Authors
# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details.
import cProfile
import pstats
import logging
import os
import sys
@ -50,6 +51,11 @@ my_args, remaining_args = parser.parse_known_args()
if os.path.exists(os.path.join(os.path.dirname(os.path.realpath(__file__)), "DEBUG")):
debug.enable()
profiler = None
if os.path.exists(os.path.join(os.path.dirname(os.path.realpath(__file__)), "PROFILE")):
profiler = cProfile.Profile()
profiler.enable()
extension_name = my_args.extension
# example: foo_bar_baz -> FooBarBaz
@ -58,8 +64,19 @@ extension_class_name = extension_name.title().replace("_", "")
extension_class = getattr(extensions, extension_class_name)
extension = extension_class()
if hasattr(sys, 'gettrace') and sys.gettrace():
if (hasattr(sys, 'gettrace') and sys.gettrace()) or profiler is not None:
extension.run(args=remaining_args)
if profiler:
path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "profile_stats")
profiler.disable()
profiler.dump_stats(path + ".prof")
with open(path, 'w') as stats_file:
stats = pstats.Stats(profiler, stream=stats_file)
stats.sort_stats(pstats.SortKey.CUMULATIVE)
stats.print_stats()
print(f"profiling stats written to {path} and {path}.prof", file=sys.stderr)
else:
save_stderr()
exception = None

Wyświetl plik

@ -266,6 +266,10 @@ class FillStitch(EmbroideryElement):
valid_shape = make_valid(shape)
logger.setLevel(level)
if isinstance(valid_shape, shgeo.Polygon):
return shgeo.MultiPolygon([valid_shape])
polygons = []
for polygon in valid_shape.geoms:
if isinstance(polygon, shgeo.Polygon):
@ -499,15 +503,18 @@ class FillStitch(EmbroideryElement):
return self.get_boolean_param('underlay_underpath', True)
def shrink_or_grow_shape(self, shape, amount, validate=False):
new_shape = shape
if amount:
shape = shape.buffer(amount)
new_shape = shape.buffer(amount)
# changing the size can empty the shape
# in this case we want to use the original shape rather than returning an error
if shape.is_empty and not validate:
return shape
if not isinstance(shape, shgeo.MultiPolygon):
shape = shgeo.MultiPolygon([shape])
return shape
if (new_shape.is_empty and not validate):
new_shape = shape
if not isinstance(new_shape, shgeo.MultiPolygon):
new_shape = shgeo.MultiPolygon([new_shape])
return new_shape
def underlay_shape(self, shape):
return self.shrink_or_grow_shape(shape, -self.fill_underlay_inset)
@ -532,26 +539,31 @@ class FillStitch(EmbroideryElement):
else:
return None
def to_stitch_groups(self, last_patch):
def to_stitch_groups(self, last_patch): # noqa: C901
# backwards compatibility: legacy_fill used to be inkstitch:auto_fill == False
if not self.auto_fill or self.fill_method == 3:
return self.do_legacy_fill()
else:
stitch_groups = []
start = self.get_starting_point(last_patch)
end = self.get_ending_point()
for shape in self.shape.geoms:
start = self.get_starting_point(last_patch)
try:
if self.fill_underlay:
underlay_stitch_groups, start = self.do_underlay(shape, start)
stitch_groups.extend(underlay_stitch_groups)
if self.fill_method == 0:
stitch_groups.extend(self.do_auto_fill(shape, last_patch, start, end))
if self.fill_method == 1:
stitch_groups.extend(self.do_contour_fill(self.fill_shape(shape), last_patch, start))
elif self.fill_method == 2:
stitch_groups.extend(self.do_guided_fill(shape, last_patch, start, end))
underlay_shapes = self.underlay_shape(shape)
for underlay_shape in underlay_shapes.geoms:
underlay_stitch_groups, start = self.do_underlay(underlay_shape, start)
stitch_groups.extend(underlay_stitch_groups)
fill_shapes = self.fill_shape(shape)
for fill_shape in fill_shapes.geoms:
if self.fill_method == 0:
stitch_groups.extend(self.do_auto_fill(fill_shape, last_patch, start, end))
if self.fill_method == 1:
stitch_groups.extend(self.do_contour_fill(fill_shape, last_patch, start))
elif self.fill_method == 2:
stitch_groups.extend(self.do_guided_fill(fill_shape, last_patch, start, end))
except Exception:
self.fatal_fill_error()
last_patch = stitch_groups[-1]
@ -576,7 +588,7 @@ class FillStitch(EmbroideryElement):
color=self.color,
tags=("auto_fill", "auto_fill_underlay"),
stitches=auto_fill(
self.underlay_shape(shape),
shape,
self.fill_underlay_angle[i],
self.fill_underlay_row_spacing,
self.fill_underlay_row_spacing,
@ -597,7 +609,7 @@ class FillStitch(EmbroideryElement):
color=self.color,
tags=("auto_fill", "auto_fill_top"),
stitches=auto_fill(
self.fill_shape(shape),
shape,
self.angle,
self.row_spacing,
self.end_row_spacing,
@ -663,7 +675,7 @@ class FillStitch(EmbroideryElement):
color=self.color,
tags=("guided_fill", "auto_fill_top"),
stitches=guided_fill(
self.fill_shape(shape),
shape,
guide_line.geoms[0],
self.angle,
self.row_spacing,

Wyświetl plik

@ -150,7 +150,7 @@ class Stroke(EmbroideryElement):
return max(self.get_int_param("line_count", 10), 1)
def get_line_count(self):
if self.is_closed:
if self.is_closed or self.join_style == 1:
return self.line_count + 1
return self.line_count
@ -246,7 +246,7 @@ class Stroke(EmbroideryElement):
type='dropdown',
default=0,
# 0: xy, 1: x, 2: y, 3: none
options=[_("X Y"), _("X"), _("Y"), _("None")],
options=["X Y", "X", "Y", _("None")],
select_items=[('stroke_method', 1)],
sort_index=12)
def scale_axis(self):
@ -286,6 +286,19 @@ class Stroke(EmbroideryElement):
def rotate_ripples(self):
return self.get_boolean_param("rotate_ripples", True)
@property
@param('join_style',
_('Join style'),
tooltip=_('Join style for non circular ripples.'),
type='dropdown',
default=0,
options=(_("flat"), _("point")),
select_items=[('stroke_method', 1)],
sort_index=16)
@cache
def join_style(self):
return self.get_int_param('join_style', 0)
@property
@cache
def is_closed(self):

Wyświetl plik

@ -4,7 +4,6 @@
# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details.
from ..utils.geometry import Point
from copy import deepcopy
class Stitch(Point):
@ -12,10 +11,14 @@ class Stitch(Point):
def __init__(self, x, y=None, color=None, jump=False, stop=False, trim=False, color_change=False,
tie_modus=0, force_lock_stitches=False, no_ties=False, tags=None):
base_stitch = None
if isinstance(x, Stitch):
# Allow creating a Stitch from another Stitch. Attributes passed as
# arguments will override any existing attributes.
vars(self).update(deepcopy(vars(x)))
base_stitch = x
self.x = base_stitch.x
self.y = base_stitch.y
elif isinstance(x, Point):
# Allow creating a Stitch from a Point
point = x
@ -24,17 +27,19 @@ class Stitch(Point):
else:
Point.__init__(self, x, y)
self.color = color
self.jump = jump
self.trim = trim
self.stop = stop
self.color_change = color_change
self.force_lock_stitches = force_lock_stitches
self.tie_modus = tie_modus
self.no_ties = no_ties
self.tags = set()
self._set('color', color, base_stitch)
self._set('jump', jump, base_stitch)
self._set('trim', trim, base_stitch)
self._set('stop', stop, base_stitch)
self._set('color_change', color_change, base_stitch)
self._set('force_lock_stitches', force_lock_stitches, base_stitch)
self._set('tie_modus', tie_modus, base_stitch)
self._set('no_ties', no_ties, base_stitch)
self.tags = set()
self.add_tags(tags or [])
if base_stitch is not None:
self.add_tags(base_stitch.tags)
def __repr__(self):
return "Stitch(%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)" % (self.x,
@ -48,6 +53,14 @@ class Stitch(Point):
"NO TIES" if self.no_ties else " ",
"COLOR CHANGE" if self.color_change else " ")
def _set(self, attribute, value, base_stitch):
# Set an attribute. If the caller passed a Stitch object, use its value, unless
# they overrode it with arguments.
if base_stitch is not None:
setattr(self, attribute, getattr(base_stitch, attribute))
if value or base_stitch is None:
setattr(self, attribute, value)
def add_tags(self, tags):
for tag in tags:
self.add_tag(tag)

Wyświetl plik

@ -59,14 +59,14 @@ def auto_fill(shape,
starting_point,
ending_point=None,
underpath=True):
try:
rows = intersect_region_with_grating(shape, angle, row_spacing, end_row_spacing)
segments = [segment for row in rows for segment in row]
fill_stitch_graph = build_fill_stitch_graph(shape, segments, starting_point, ending_point)
except ValueError:
# Small shapes will cause the graph to fail - min() arg is an empty sequence through insert node
rows = intersect_region_with_grating(shape, angle, row_spacing, end_row_spacing)
if not rows:
# Small shapes may not intersect with the grating at all.
return fallback(shape, running_stitch_length, running_stitch_tolerance)
segments = [segment for row in rows for segment in row]
fill_stitch_graph = build_fill_stitch_graph(shape, segments, starting_point, ending_point)
if not graph_is_valid(fill_stitch_graph, shape, max_stitch_length):
return fallback(shape, running_stitch_length, running_stitch_tolerance)

Wyświetl plik

@ -132,7 +132,7 @@ def intersect_region_with_grating(shape, angle, row_spacing, end_row_spacing=Non
start -= (start + normal * center) % row_spacing
current_row_y = start
rows = []
while current_row_y < end:
p0 = center + normal * current_row_y + direction * half_length
p1 = center + normal * current_row_y - direction * half_length
@ -157,13 +157,15 @@ def intersect_region_with_grating(shape, angle, row_spacing, end_row_spacing=Non
runs.reverse()
runs = [tuple(reversed(run)) for run in runs]
yield runs
rows.append(runs)
if end_row_spacing:
current_row_y += row_spacing + (end_row_spacing - row_spacing) * ((current_row_y - start) / height)
else:
current_row_y += row_spacing
return rows
def section_to_stitches(group_of_segments, angle, row_spacing, max_stitch_length, staggers, skip_last):
stitches = []

Wyświetl plik

@ -1,15 +1,20 @@
from math import atan2, copysign
from random import random
import numpy as np
import shapely.prepared
from shapely import geometry as shgeo
from shapely.affinity import translate
from shapely.ops import linemerge, unary_union
from shapely.ops import linemerge, nearest_points, unary_union
from .auto_fill import (build_fill_stitch_graph,
build_travel_graph, collapse_sequential_outline_edges, fallback,
find_stitch_path, graph_is_valid, travel)
from ..debug import debug
from ..i18n import _
from ..stitch_plan import Stitch
from ..utils.geometry import Point as InkstitchPoint, ensure_geometry_collection, ensure_multi_line_string, reverse_line_string
from ..utils.geometry import Point as InkstitchPoint
from ..utils.geometry import (ensure_geometry_collection,
ensure_multi_line_string, reverse_line_string)
from .auto_fill import (auto_fill, build_fill_stitch_graph, build_travel_graph,
collapse_sequential_outline_edges, find_stitch_path,
graph_is_valid, travel)
def guided_fill(shape,
@ -27,10 +32,15 @@ def guided_fill(shape,
strategy
):
segments = intersect_region_with_grating_guideline(shape, guideline, row_spacing, num_staggers, max_stitch_length, strategy)
if not segments:
return fallback(shape, guideline, row_spacing, max_stitch_length, running_stitch_length, running_stitch_tolerance,
num_staggers, skip_last, starting_point, ending_point, underpath)
fill_stitch_graph = build_fill_stitch_graph(shape, segments, starting_point, ending_point)
if not graph_is_valid(fill_stitch_graph, shape, max_stitch_length):
return fallback(shape, running_stitch_length, running_stitch_tolerance)
return fallback(shape, guideline, row_spacing, max_stitch_length, running_stitch_length, running_stitch_tolerance,
num_staggers, skip_last, starting_point, ending_point, underpath)
travel_graph = build_travel_graph(fill_stitch_graph, shape, angle, underpath)
path = find_stitch_path(fill_stitch_graph, travel_graph, starting_point, ending_point)
@ -39,6 +49,15 @@ def guided_fill(shape,
return result
def fallback(shape, guideline, row_spacing, max_stitch_length, running_stitch_length, running_stitch_tolerance,
num_staggers, skip_last, starting_point, ending_point, underpath):
# fall back to normal auto-fill with an angle that matches the guideline (sorta)
guide_start, guide_end = [guideline.coords[0], guideline.coords[-1]]
angle = atan2(guide_end[1] - guide_start[1], guide_end[0] - guide_start[0]) * -1
return auto_fill(shape, angle, row_spacing, None, max_stitch_length, running_stitch_length, running_stitch_tolerance,
num_staggers, skip_last, starting_point, ending_point, underpath)
def path_to_stitches(path, travel_graph, fill_stitch_graph, stitch_length, running_stitch_length, running_stitch_tolerance, skip_last):
path = collapse_sequential_outline_edges(path)
@ -75,22 +94,23 @@ def path_to_stitches(path, travel_graph, fill_stitch_graph, stitch_length, runni
def extend_line(line, shape):
(minx, miny, maxx, maxy) = shape.bounds
line = line.simplify(0.01, False)
upper_left = InkstitchPoint(minx, miny)
lower_right = InkstitchPoint(maxx, maxy)
length = (upper_left - lower_right).length()
point1 = InkstitchPoint(*line.coords[0])
point2 = InkstitchPoint(*line.coords[1])
new_starting_point = point1 - (point2 - point1).unit() * length
start_point = InkstitchPoint.from_tuple(line.coords[0])
end_point = InkstitchPoint.from_tuple(line.coords[-1])
direction = (end_point - start_point).unit()
point3 = InkstitchPoint(*line.coords[-2])
point4 = InkstitchPoint(*line.coords[-1])
new_ending_point = point4 + (point4 - point3).unit() * length
new_start_point = start_point - direction * length
new_end_point = end_point + direction * length
return shgeo.LineString([new_starting_point.as_tuple()] +
line.coords[1:-1] + [new_ending_point.as_tuple()])
# without this, we seem especially likely to run into this libgeos bug:
# https://github.com/shapely/shapely/issues/820
new_start_point += InkstitchPoint(random() * 0.01, random() * 0.01)
new_end_point += InkstitchPoint(random() * 0.01, random() * 0.01)
return shgeo.LineString((new_start_point, *line.coords, new_end_point))
def repair_multiple_parallel_offset_curves(multi_line):
@ -114,8 +134,8 @@ def repair_non_simple_line(line):
repaired = unary_union(linemerge(line_segments))
counter += 1
if repaired.geom_type != 'LineString':
raise ValueError(
_("Guide line (or offset copy) is self crossing!"))
# They gave us a line with complicated self-intersections. Use a fallback.
return shgeo.LineString((line.coords[0], line.coords[-1]))
else:
return repaired
@ -158,7 +178,12 @@ def prepare_guide_line(line, shape):
if line.geom_type != 'LineString' or not line.is_simple:
line = repair_non_simple_line(line)
# extend the line towards the ends to increase probability that all offsetted curves cross the shape
if line.is_ring:
# If they pass us a ring, break it to avoid dividing by zero when
# calculating a unit vector from start to end.
line = shgeo.LineString(line.coords[:-2])
# extend the end points away from each other
line = extend_line(line, shape)
return line
@ -176,24 +201,41 @@ def clean_offset_line(offset_line):
return offset_line
def _get_start_row(line, shape, row_spacing, line_direction):
if line.intersects(shape):
return 0
point1, point2 = nearest_points(line, shape.centroid)
distance = point1.distance(point2)
row = int(distance / row_spacing)
# This flips the sign of the starting row if the shape is on the other side
# of the guide line
shape_direction = InkstitchPoint.from_shapely_point(point2) - InkstitchPoint.from_shapely_point(point1)
return copysign(row, shape_direction * line_direction)
def intersect_region_with_grating_guideline(shape, line, row_spacing, num_staggers, max_stitch_length, strategy):
debug.log_line_string(shape.exterior, "guided fill shape")
if strategy == 0:
translate_direction = InkstitchPoint(*line.coords[-1]) - InkstitchPoint(*line.coords[0])
translate_direction = translate_direction.unit().rotate_left()
line = prepare_guide_line(line, shape)
row = 0
debug.log_line_string(shape.exterior, "guided fill shape")
translate_direction = InkstitchPoint(*line.coords[-1]) - InkstitchPoint(*line.coords[0])
translate_direction = translate_direction.unit().rotate_left()
shape_envelope = shapely.prepared.prep(shape.convex_hull)
start_row = _get_start_row(line, shape, row_spacing, translate_direction)
row = start_row
direction = 1
offset_line = None
rows = []
while True:
if strategy == 0:
translate_amount = translate_direction * row * direction * row_spacing
translate_amount = translate_direction * row * row_spacing
offset_line = translate(line, xoff=translate_amount.x, yoff=translate_amount.y)
elif strategy == 1:
offset_line = line.parallel_offset(row * row_spacing * direction, 'left', join_style=shgeo.JOIN_STYLE.bevel)
offset_line = line.parallel_offset(row * row_spacing, 'left', join_style=shgeo.JOIN_STYLE.round)
offset_line = clean_offset_line(offset_line)
@ -201,18 +243,20 @@ def intersect_region_with_grating_guideline(shape, line, row_spacing, num_stagge
# negative parallel offsets are reversed, so we need to compensate
offset_line = reverse_line_string(offset_line)
debug.log_line_string(offset_line, f"offset {row * direction}")
debug.log_line_string(offset_line, f"offset {row}")
stitched_line = apply_stitches(offset_line, max_stitch_length, num_staggers, row_spacing, row * direction)
intersection = shape.intersection(stitched_line)
if intersection.is_empty:
if shape_envelope.intersects(stitched_line):
for segment in take_only_line_strings(intersection).geoms:
rows.append(segment.coords[:])
row += direction
else:
if direction == 1:
direction = -1
row = 1
row = start_row - 1
else:
break
else:
for segment in take_only_line_strings(intersection).geoms:
yield segment.coords[:]
row += 1
return rows

Wyświetl plik

@ -63,6 +63,8 @@ def _get_helper_lines(stroke):
if stroke.is_closed:
return False, _get_circular_ripple_helper_lines(stroke, outline)
elif stroke.join_style == 1:
return True, _get_point_style_linear_helper_lines(stroke, outline)
else:
return True, _get_linear_ripple_helper_lines(stroke, outline)
@ -74,7 +76,7 @@ def _get_satin_ripple_helper_lines(stroke):
# use satin column points for satin like build ripple stitches
rail_points = SatinColumn(stroke.node).plot_points_on_rails(length, 0)
steps = _get_steps(stroke.line_count, exponent=stroke.exponent, flip=stroke.flip_exponent)
steps = _get_steps(stroke.get_line_count(), exponent=stroke.exponent, flip=stroke.flip_exponent)
helper_lines = []
for point0, point1 in zip(*rail_points):
@ -83,24 +85,39 @@ def _get_satin_ripple_helper_lines(stroke):
for step in steps:
helper_lines[-1].append(InkstitchPoint.from_shapely_point(helper_line.interpolate(step, normalized=True)))
if stroke.join_style == 1:
helper_lines = _converge_helper_line_points(helper_lines, True)
return helper_lines
def _get_circular_ripple_helper_lines(stroke, outline):
helper_lines = _get_linear_ripple_helper_lines(stroke, outline)
# Now we want to adjust the helper lines to make a spiral.
def _converge_helper_line_points(helper_lines, point_edge=False):
num_lines = len(helper_lines)
steps = _get_steps(num_lines)
for i, line in enumerate(helper_lines):
points = []
for j in range(len(line) - 1):
points.append(line[j] * (1 - steps[i]) + line[j + 1] * steps[i])
if point_edge and j % 2 == 1:
k = num_lines - 1 - i
points.append(line[j] * (1 - steps[k]) + line[j + 1] * steps[k])
else:
points.append(line[j] * (1 - steps[i]) + line[j + 1] * steps[i])
helper_lines[i] = points
return helper_lines
def _get_circular_ripple_helper_lines(stroke, outline):
helper_lines = _get_linear_ripple_helper_lines(stroke, outline)
# Now we want to adjust the helper lines to make a spiral.
return _converge_helper_line_points(helper_lines)
def _get_point_style_linear_helper_lines(stroke, outline):
helper_lines = _get_linear_ripple_helper_lines(stroke, outline)
return _converge_helper_line_points(helper_lines, True)
def _get_linear_ripple_helper_lines(stroke, outline):
guide_line = stroke.get_guide_line()
max_dist = stroke.grid_size or stroke.running_stitch_length
@ -124,10 +141,25 @@ def _target_point_helper_lines(stroke, outline):
return helper_lines
def _adjust_helper_lines_for_grid(stroke, helper_lines):
num_lines = stroke.line_count - stroke.skip_end
if stroke.reverse:
helper_lines = [helper_line[::-1] for helper_line in helper_lines]
num_lines = stroke.skip_start
if (num_lines % 2 != 0 and not stroke.is_closed) or (stroke.is_closed and not stroke.reverse):
helper_lines.reverse()
return helper_lines
def _do_grid(stroke, helper_lines):
helper_lines = _adjust_helper_lines_for_grid(stroke, helper_lines)
start = stroke.get_skip_start()
skip_end = stroke.get_skip_end()
if stroke.reverse:
start, skip_end = skip_end, start
for i, helper in enumerate(helper_lines):
start = stroke.get_skip_start()
end = len(helper) - stroke.get_skip_end()
end = len(helper) - skip_end
points = helper[start:end]
if i % 2 == 0:
points.reverse()
@ -146,7 +178,7 @@ def _get_guided_helper_lines(stroke, outline, max_distance):
def _generate_guided_helper_lines(stroke, outline, max_distance, guide_line):
# helper lines are generated by making copies of the outline alog the guide line
# helper lines are generated by making copies of the outline along the guide line
line_point_dict = defaultdict(list)
outline = LineString(running_stitch(line_string_to_point_list(outline), max_distance, stroke.running_stitch_tolerance))

Wyświetl plik

@ -64,17 +64,7 @@ inkstitch_attribs = [
'join_style',
'avoid_self_crossing',
'clockwise',
'line_count',
'skip_start',
'skip_end',
'grid_size',
'reverse',
'exponent',
'flip_exponent',
'scale_axis',
'scale_start',
'scale_end',
'rotate_ripples',
'expand_mm',
'fill_underlay',
'fill_underlay_angle',
@ -97,6 +87,17 @@ inkstitch_attribs = [
'repeats',
'running_stitch_length_mm',
'running_stitch_tolerance_mm',
# ripples
'line_count',
'exponent',
'flip_exponent',
'skip_start',
'skip_end',
'scale_axis',
'scale_start',
'scale_end',
'rotate_ripples',
'grid_size',
# satin column
'satin_column',
'short_stitch_distance_mm',

Wyświetl plik

@ -129,6 +129,10 @@ class Point:
def from_shapely_point(cls, point):
return cls(point.x, point.y)
@classmethod
def from_tuple(cls, point):
return cls(point[0], point[1])
def __json__(self):
return vars(self)