Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 85 additions & 5 deletions inkcut/job/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
from atom.api import Atom, Str, Instance, Bool, Float
from enaml.colors import Color, ColorMember, SVG_COLORS
from inkcut.core.svg import QtSvgDoc, EtreeElement
from enaml.qt.QtGui import QPainterPath, QPolygonF
from enaml.qt.QtCore import QPointF
from inkcut.core.utils import (
log, find_subclasses, split_painter_path, join_painter_paths
)
Expand Down Expand Up @@ -50,7 +52,7 @@ def get_layer_label(g):
return g.attrib.get(attr)


class JobFilter(Atom):
class Filter(Atom):
#: A fixed type name for the UI to extract without using isinstance
type = ""

Expand Down Expand Up @@ -101,8 +103,79 @@ def apply_filter(self, job, doc):
"""
raise NotImplementedError()

# These filters receive an SVG document
# and produce a filtered version of that
# same SVG document after filtering/modifying
# based on the SVG document structure and
# attributes.
class SvgFilter(Filter):
pass

# These filters receive a general QPainterPath
# of the entire job after the shapes have been
# potentially copied, transformed, and otherwise modified
# by the structure of the job.
class JobFilter(Filter):
pass

# This filter clips the geometry to the
# bounding-box implied by the Job's material
# settings. This helps prevent us from accidentally
# sending the plotter head off the gantry trying to
# draw geometry that is invalid for the material/device.
class ClipFilter(JobFilter):
@classmethod
def get_filter_options(cls, job, doc):
# This filter is always 'enabled' but that
# just means it will always run.
# It may clip or not clip depending on whether
# the job's clip_to_plot_area is true. This is because
# the get_filter_options is only called when
# the document is loaded, but not when options
# are checked by the user.
return [cls(enabled=False)]

class LayerFilter(JobFilter):
def apply_filter(self, job, doc):
# If it's not enabled by the user, we do nothing.
if not job.clip_to_plot_area:
return doc

# We want to clip to the defined material
# boundaries. We could use job.material.path,
# but this doesn't account for the margins and we would
# like to make sure that the margins are not cut.
# The signs are a bit wierd here because
# plus is down instead of up in this context.
clip_x0 = job.material.padding_left
clip_y0 = -job.material.padding_bottom
clip_x1 = job.material.width() - job.material.padding_right
clip_y1 = -job.material.height() + job.material.padding_top

# Then we assemble this as a list of points
clip_points = [
QPointF(clip_x0, clip_y0),
QPointF(clip_x1, clip_y0),
QPointF(clip_x1, clip_y1),
QPointF(clip_x0, clip_y1),
QPointF(clip_x0, clip_y0)
]

# And finally turn this into a polygon
# and then a painter path
clip_polygon = QPolygonF(clip_points)
clip_path = QPainterPath()
clip_path.addPolygon(clip_polygon)

# Finally, we do the work of clipping
# so we can remove the un-needed pieces.
# It is important that the clip happen
# before the optimize path because
# removing path segments would change
# the optimizer's results
clipped = doc.intersected(clip_path)
return clipped

class LayerFilter(SvgFilter):
type = "layer"

layer = Instance(EtreeElement)
Expand Down Expand Up @@ -142,7 +215,7 @@ def apply_filter(self, job, doc):
return QtSvgDoc(svg, parent=True)


class FillColorFilter(JobFilter):
class FillColorFilter(SvgFilter):
type = "fill-color"
style_attr = 'fill'

Expand Down Expand Up @@ -188,5 +261,12 @@ class StrokeColorFilter(FillColorFilter):
style_attr = 'stroke'


#: Register all subclasses
REGISTRY = {c.type: c for c in find_subclasses(JobFilter)}
# SVG Filters apply to the input SVG document
# and can rely on the SVG/XML structure of the document
# for things like filtering layers, colors, and styles.
SVG_FILTERS = {c.type: c for c in find_subclasses(SvgFilter)}
# Job filters apply to the whole document after
# transformations and copies have been added.
# These rely only on the geometry/paths (QPainterPath)
# and do not have access to the SVG/XML structure.
JOB_FILTERS = {c.type: c for c in find_subclasses(JobFilter)}
45 changes: 32 additions & 13 deletions inkcut/job/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
Dict, Callable, observe
)
from contextlib import contextmanager
from enaml.qt.QtGui import QPainterPath, QTransform
from enaml.qt.QtGui import QPainterPath, QTransform, QPolygonF
from enaml.qt.QtCore import QPointF, QRectF
from enaml.colors import ColorMember
from inkcut.core.api import Model, AreaBase
Expand Down Expand Up @@ -163,6 +163,7 @@ class Job(Model):
mirror = ContainerList(Bool(), default=[False, False]).tag(config=True)
align_center = ContainerList(Bool(),
default=[False, False]).tag(config=True)
clip_to_plot_area = Bool().tag(config = True)

# Shifting of original file
auto_shift = Bool(True).tag(config=True, help="shift to start at origin")
Expand Down Expand Up @@ -196,7 +197,8 @@ def _default_order(self):
stack_size = ContainerList(Int(), default=[0, 0])

#: Filters to cut only certain items
filters = ContainerList(filters.JobFilter)
svg_filters = ContainerList(filters.SvgFilter)
job_filters = ContainerList(filters.JobFilter)

#: Original path parsed from the source document
doc = Instance(QtSvgDoc)
Expand Down Expand Up @@ -254,30 +256,44 @@ def _observe_document(self, change):
self.doc = self.path = QtSvgDoc(source, **self.document_kwargs)

# Recreate available filters when the document changes
self.filters = self._default_filters()
self.svg_filters = self._default_svg_filters()
self.job_filters = self._default_job_filters()

def _default_filters(self):
def get_filters_from_registry(self, registry):
results = []
if not self.path:
return results
for Filter in filters.REGISTRY.values():
for Filter in registry.values():
try:
results.extend(Filter.get_filter_options(self, self.path))
except Exception as e:
log.error("Failed loading filters for: %s" % Filter)
log.exception(e)
return results

def _default_optimized_path(self):
""" Filter parts of the documen based on the selected layers and colors
def _default_svg_filters(self):
return self.get_filters_from_registry(filters.SVG_FILTERS)

"""
doc = self.path
for f in self.filters:
def _default_job_filters(self):
return self.get_filters_from_registry(filters.JOB_FILTERS)

def apply_filters(self, filter_list, doc):
for f in filter_list:
# If the color/layer is NOT enabled, then remove that color/layer
if not f.enabled:
log.debug("Applying filter {}".format(f))
doc = f.apply_filter(self, doc)
return doc

def _default_optimized_path(self):
""" Filter parts of the documen based on the selected layers and colors

"""
doc = self.path
# SVG filters need to happen first,
# these rely on the SVG structure of
# the document.
doc = self.apply_filters(self.svg_filters, doc)

# Apply ordering to path
# this delegates to objects in the ordering module
Expand All @@ -287,7 +303,7 @@ def _default_optimized_path(self):

return doc

@observe('path', 'order', 'filters')
@observe('path', 'order', 'svg_filters', 'job_filters', 'clip_to_plot_area')
def _update_optimized_path(self, change):
""" Whenever the loaded file (and parsed SVG path) changes update
it based on the filters from the job.
Expand Down Expand Up @@ -355,7 +371,6 @@ def _create_copy(self):
# Move to bottom left
br = bbox.bottomRight()
path = QTransform.fromTranslate(-br.x(), -br.y()).map(path)

return path

@contextmanager
Expand All @@ -374,7 +389,7 @@ def events_suppressed(self):
'copy_spacing', 'copy_weedline', 'copy_weedline_padding',
'plot_weedline', 'plot_weedline_padding', 'feed_to_end',
'feed_after', 'material', 'material.size', 'material.padding',
'auto_copies', 'auto_shift')
'auto_copies', 'auto_shift', 'clip_to_plot_area')
def update_document(self, change=None):
""" Recreate an instance of of the plot using the current settings

Expand Down Expand Up @@ -476,6 +491,10 @@ def create(self, swap_xy=False, scale=None):
if self.feed_to_end else QPointF(0, 0))
model.moveTo(end_point)

# Apply the job filters to the final result
# after copies and transformations have been applied.
model = self.apply_filters(self.job_filters, model)

return model

def _check_bounds(self, plot, area):
Expand Down
7 changes: 6 additions & 1 deletion inkcut/job/view.enaml
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,7 @@ enamldef WeedlinesDockItem(DockItem):
enamldef LayersDockItem(DockItem):
attr plugin
attr job: Job << plugin.job
attr filters << job.filters if job else []
attr filters << job.svg_filters if job else []

title = QApplication.translate("job", "Layers")
name = 'layers-item'
Expand Down Expand Up @@ -505,6 +505,11 @@ enamldef MaterialDockItem(DockItem):
text = QApplication.translate("job", 'Align center vertically')
icon = load_icon('shape_align_middle')
checked := job.align_center[1]
CheckBox:
text = QApplication.translate("device", "Clip to plot area")
icon = load_icon('shape_square_width')
checked := job.clip_to_plot_area

Container:
padding = 0
constraints = [
Expand Down
60 changes: 60 additions & 0 deletions tests/data/clip/clip_test.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading