def _observe_document(self, change): """ Read the document from stdin """ if change['type'] == 'update' and self.document == '-': #: Only load from stdin when explicitly changed to it (when doing #: open from the cli) otherwise when restoring state this hangs #: startup self.path = QtSvgDoc(sys.stdin, **self.document_kwargs) elif self.document and os.path.exists(self.document): self.path = QtSvgDoc(self.document, **self.document_kwargs)
def apply_filter(self, job, doc): """ Remove all subpaths from doc that are in this layer by reparsing the xml. """ # Copy it since we're modifying svg = copy.deepcopy(doc._e) # Remove all nodes with that stroke style for e in svg.xpath('//*[@style]'): style = get_node_style(e) if style.get(self.style_attr) == self.data: e.getparent().remove(e) return QtSvgDoc(svg, parent=True)
def apply_filter(self, job, doc): """ Remove all subpaths from doc that are in this layer by reparsing the xml. """ # Copy it since we're modifying svg = copy.deepcopy(doc._e) # Find the layer in the cloned doc for g in get_layers(svg): label = get_layer_label(g) if label == self.name: g.getparent().remove(g) break return QtSvgDoc(svg, parent=True)
def test_svg(path): """ Make sure the document can be parsed """ try: doc = QtSvgDoc(path) except NotImplementedError as e: pytest.skip(str(e))
class Job(Model): """ Create a plot depending on the properties set. Any property that is a traitlet will cause an update when the value is changed. """ #: Material this job will be run on material = Instance(Material, ()).tag(config=True) #: Path to svg document this job parses document = Unicode().tag(config=True) #: Nodes to restrict document_kwargs = Dict().tag(config=True) #: Meta info a the job info = Instance(JobInfo, ()).tag(config=True) # Job properties used for generating the plot size = ContainerList(Float(), default=[1, 1]) # TODO: hooookk scale = ContainerList(Float(), default=[1, 1]).tag(config=True) auto_scale = Bool(False).tag( config=True, help="automatically scale if it's too big for the area") lock_scale = Bool(True).tag( config=True, help="automatically scale if it's too big for the area") mirror = ContainerList(Bool(), default=[False, False]).tag(config=True) align_center = ContainerList(Bool(), default=[False, False]).tag(config=True) rotation = Float(0).tag(config=True) auto_rotate = Bool(False).tag( config=True, help="automatically rotate if it saves space") copies = Int(1).tag(config=True) auto_copies = Bool(False).tag(config=True, help="always use a full stack") copy_spacing = ContainerList(Float(), default=[10, 10]).tag(config=True) copy_weedline = Bool(False).tag(config=True) copy_weedline_padding = ContainerList(Float(), default=[10, 10, 10, 10]).tag(config=True) plot_weedline = Bool(False).tag(config=True) plot_weedline_padding = ContainerList(Float(), default=[10, 10, 10, 10]).tag(config=True) order = Enum(*sorted(ordering.REGISTRY.keys())).tag(config=True) def _default_order(self): return 'Normal' feed_to_end = Bool(False).tag(config=True) feed_after = Float(0).tag(config=True) stack_size = ContainerList(Int(), default=[0, 0]) path = Instance(QtGui.QPainterPath) # Original path model = Instance(QtGui.QPainterPath) # Copy using job properties _blocked = Bool(False) # block change events _desired_copies = Int(1) # required for auto copies def _observe_document(self, change): """ Read the document from stdin """ if change['type'] == 'update' and self.document == '-': #: Only load from stdin when explicitly changed to it (when doing #: open from the cli) otherwise when restoring state this hangs #: startup self.path = QtSvgDoc(sys.stdin, **self.document_kwargs) elif self.document and os.path.exists(self.document): self.path = QtSvgDoc(self.document, **self.document_kwargs) def _create_copy(self): """ Creates a copy of the original graphic applying the given transforms """ bbox = self.path.boundingRect() # Create the base copy t = QtGui.QTransform() t.scale( self.scale[0] * (self.mirror[0] and -1 or 1), self.scale[1] * (self.mirror[1] and -1 or 1), ) # Rotate about center if self.rotation != 0: c = bbox.center() t.translate(-c.x(), -c.y()) t.rotate(self.rotation) t.translate(c.x(), c.y()) # Apply transform path = self.path * t # Add weedline to copy if self.copy_weedline: self._add_weedline(path, self.copy_weedline_padding) # Apply ordering to path # this delegates to objects in the ordering module # TODO: Should this be done via plugins? OrderingHandler = ordering.REGISTRY.get(self.order) if OrderingHandler: path = OrderingHandler().order(self, path) # If it's too big we have to scale it w, h = path.boundingRect().width(), path.boundingRect().height() available_area = self.material.available_area #: This screws stuff up! if w > available_area.width() or h > available_area.height(): # If it's too big an auto scale is enabled, resize it to fit if not self.auto_scale: raise JobError("Image is too large to fit on the material") sx, sy = 1, 1 if w > available_area.width(): sx = available_area.width() / w if h > available_area.height(): sy = available_area.height() / h s = min(sx, sy) # Fit to the smaller of the two path = self.path * QtGui.QTransform.fromScale(s, s) # Move to bottom left p = path.boundingRect().bottomRight() path = path * QtGui.QTransform.fromTranslate(-p.x(), -p.y()) return path @contextmanager def events_suppressed(self): """ Block change events to prevent feedback loops """ self._blocked = True try: yield finally: self._blocked = False @observe('path', 'scale', 'auto_scale', 'lock_scale', 'mirror', 'align_center', 'rotation', 'auto_rotate', 'copies', 'order', 'copy_spacing', 'copy_weedline', 'copy_weedline_padding', 'plot_weedline', 'plot_weedline_padding', 'feed_to_end', 'feed_after', 'material', 'material.size', 'material.padding', 'auto_copies') def _job_changed(self, change): """ Recreate an instance of of the plot using the current settings """ if self._blocked: return if change['name'] == 'copies': self._desired_copies = self.copies #try: model = QtGui.QPainterPath() if not self.path: return path = self._create_copy() # Update size bbox = path.boundingRect() self.size = [bbox.width(), bbox.height()] # Create copies c = 0 points = self._copy_positions_iter(path) if self.auto_copies: self.stack_size = self._compute_stack_sizes(path) if self.stack_size[0]: copies_left = self.copies % self.stack_size[0] if copies_left: # not a full stack with self.events_suppressed(): self.copies = self._desired_copies self.add_stack() while c < self.copies: x, y = next(points) model.addPath(path * QtGui.QTransform.fromTranslate(x, -y)) c += 1 # Create weedline if self.plot_weedline: self._add_weedline(model, self.plot_weedline_padding) # Move to 0,0 bbox = model.boundingRect() p = bbox.bottomLeft() tx, ty = -p.x(), -p.y() # Center or set to padding tx += ((self.material.width() - bbox.width()) / 2.0 if self.align_center[0] else self.material.padding_left) ty += (-(self.material.height() - bbox.height()) / 2.0 if self.align_center[1] else -self.material.padding_bottom) t = QtGui.QTransform.fromTranslate(tx, ty) model = model * t end_point = (QtCore.QPointF( 0, -self.feed_after + model.boundingRect().top()) if self.feed_to_end else QtCore.QPointF(0, 0)) model.moveTo(end_point) # Set new model self.model = model #.simplified() # Set device model #self.device_model = self.device.driver.prepare_job(self) #except: # # Undo the change # if 'oldvalue' in change: # setattr(change['object'],change['name'],change['oldvalue']) # raise #if not self.check_bounds(self.boundingRect(),self.available_area): # raise JobError( # "Plot outside of plotting area, increase the area" # "or decrease the scale or decrease number of copies!") def _check_bounds(self, plot, area): """ Checks that the width and height of plot are less than the width and height of area """ return plot.width() > area.width() or plot.height() > area.height() def _copy_positions_iter(self, path, axis=0): """ Generator that creates positions of points """ other_axis = axis + 1 % 2 p = [0, 0] bbox = path.boundingRect() d = (bbox.width(), bbox.height()) pad = self.copy_spacing stack_size = self._compute_stack_sizes(path) while True: p[axis] = 0 yield p # Beginning of each row for i in range(stack_size[axis] - 1): p[axis] += d[axis] + pad[axis] yield p p[other_axis] += d[other_axis] + pad[other_axis] def _compute_stack_sizes(self, path): # Usable area material = self.material a = [material.width(), material.height()] a[0] -= material.padding[Padding.LEFT] + material.padding[ Padding.RIGHT] a[1] -= material.padding[Padding.TOP] + material.padding[ Padding.BOTTOM] # Clone includes weedline but not spacing bbox = path.boundingRect() size = [bbox.width(), bbox.height()] stack_size = [0, 0] p = [0, 0] for i in range(2): # Compute stack while (p[i] + size[i]) < a[i]: # while another one fits stack_size[i] += 1 p[i] += size[i] + self.copy_spacing[i] # Add only to end self.stack_size = stack_size return stack_size def _add_weedline(self, path, padding): """ Adds a weedline to the path by creating a box around the path with the given padding """ bbox = path.boundingRect() w, h = bbox.width(), bbox.height() tl = bbox.topLeft() x = tl.x() - padding[Padding.LEFT] y = tl.y() - padding[Padding.TOP] w += padding[Padding.LEFT] + padding[Padding.RIGHT] h += padding[Padding.TOP] + padding[Padding.BOTTOM] path.addRect(x, y, w, h) return path @property def state(self): pass @property def move_path(self): """ Returns the path the head moves when not cutting """ # Compute the negative path = QtGui.QPainterPath() for i in range(self.model.elementCount()): e = self.model.elementAt(i) if e.isMoveTo(): path.lineTo(e.x, e.y) else: path.moveTo(e.x, e.y) return path @property def cut_path(self): """ Returns path where it is cutting """ return self.model # def get_offset_path(self,device): # """ Returns path where it is cutting """ # path = QtGui.QPainterPath() # _p = QtCore.QPointF(0,0) # previous point # step = 0.1 # for subpath in QtSvgDoc.toSubpathList(self.model):#.toSubpathPolygons(): # e = subpath.elementAt(0) # path.moveTo(QtCore.QPointF(e.x,e.y)) # length = subpath.length() # distance = 0 # while distance<=length: # t = subpath.percentAtLength(distance) # p = subpath.pointAtPercent(t) # a = subpath.angleAtPercent(t)+90 # #path.moveTo(p)#QtCore.QPointF(x,y)) # # TOOD: Do i need numpy here??? # x = p.x()+np.multiply(self.device.blade_offset,np.sin(np.deg2rad(a))) # y = p.y()+np.multiply(self.device.blade_offset,np.cos(np.deg2rad(a))) # path.lineTo(QtCore.QPointF(x,y)) # distance+=step # #_p = p # update last # # return path def add_stack(self): """ Add a complete stack or fill the row """ copies_left = self.stack_size[0] - (self.copies % self.stack_size[0]) if copies_left == 0: # Add full stack self.copies = self.copies + self.stack_size[0] else: # Fill stack self.copies = self.copies + copies_left def remove_stack(self): """ Remove a complete stack or the rest of the row """ if self.copies <= self.stack_size[0]: self.copies = 1 return copies_left = self.copies % self.stack_size[0] if copies_left == 0: # Add full stack self.copies = self.copies - self.stack_size[0] else: # Fill stack self.copies = self.copies - copies_left def clone(self): """ Return a cloned instance of this object """ clone = Job(**self.members()) return clone
class Job(Model): """ Create a plot depending on the properties set. Any property that is a traitlet will cause an update when the value is changed. """ #: Material this job will be run on material = Instance(Material, ()).tag(config=True) #: Path to svg document this job parses document = Unicode().tag(config=True) #: Nodes to restrict document_kwargs = Dict().tag(config=True) #: Meta info a the job info = Instance(JobInfo, ()).tag(config=True) # Job properties used for generating the plot size = ContainerList(Float(), default=[1, 1]) scale = ContainerList(Float(), default=[1, 1]).tag(config=True) auto_scale = Bool(False).tag( config=True, help="automatically scale if it's too big for the area") lock_scale = Bool(True).tag( config=True, help="automatically scale if it's too big for the area") mirror = ContainerList(Bool(), default=[False, False]).tag(config=True) align_center = ContainerList(Bool(), default=[False, False]).tag(config=True) rotation = Float(0).tag(config=True) auto_rotate = Bool(False).tag( config=True, help="automatically rotate if it saves space") copies = Int(1).tag(config=True) auto_copies = Bool(False).tag( config=True, help="always use a full stack") copy_spacing = ContainerList(Float(), default=[10, 10]).tag(config=True) copy_weedline = Bool(False).tag(config=True) copy_weedline_padding = ContainerList( Float(), default=[10, 10, 10, 10]).tag(config=True) plot_weedline = Bool(False).tag(config=True) plot_weedline_padding = ContainerList( Float(), default=[10, 10, 10, 10]).tag(config=True) order = Enum(*sorted(ordering.REGISTRY.keys())).tag(config=True) def _default_order(self): return 'Normal' feed_to_end = Bool(False).tag(config=True) feed_after = Float(0).tag(config=True) stack_size = ContainerList(Int(), default=[0, 0]) path = Instance(QtGui.QPainterPath) # Original path model = Instance(QtGui.QPainterPath) # Copy using job properties _blocked = Bool(False) # block change events _desired_copies = Int(1) # required for auto copies def __setstate__(self, *args, **kwargs): """ Ensure that when restoring from disk the material and info are not set to None. Ideally these would be defined as Typed but the material may be made extendable at some point. """ super(Job, self).__setstate__(*args, **kwargs) if not self.info: self.info = JobInfo() if not self.material: self.material = Material() def _observe_document(self, change): """ Read the document from stdin """ if change['type'] == 'update' and self.document == '-': #: Only load from stdin when explicitly changed to it (when doing #: open from the cli) otherwise when restoring state this hangs #: startup self.path = QtSvgDoc(sys.stdin, **self.document_kwargs) elif self.document and os.path.exists(self.document): self.path = QtSvgDoc(self.document, **self.document_kwargs) def _create_copy(self): """ Creates a copy of the original graphic applying the given transforms """ bbox = self.path.boundingRect() # Create the base copy t = QtGui.QTransform() t.scale( self.scale[0] * (self.mirror[0] and -1 or 1), self.scale[1] * (self.mirror[1] and -1 or 1), ) # Rotate about center if self.rotation != 0: c = bbox.center() t.translate(-c.x(), -c.y()) t.rotate(self.rotation) t.translate(c.x(), c.y()) # Apply transform path = self.path * t # Add weedline to copy if self.copy_weedline: self._add_weedline(path, self.copy_weedline_padding) # Apply ordering to path # this delegates to objects in the ordering module # TODO: Should this be done via plugins? OrderingHandler = ordering.REGISTRY.get(self.order) if OrderingHandler: path = OrderingHandler().order(self, path) # If it's too big we have to scale it w, h = path.boundingRect().width(), path.boundingRect().height() available_area = self.material.available_area #: This screws stuff up! if w > available_area.width() or h > available_area.height(): # If it's too big an auto scale is enabled, resize it to fit if not self.auto_scale: raise JobError("Image is too large to fit on the material") sx, sy = 1, 1 if w > available_area.width(): sx = available_area.width() / w if h > available_area.height(): sy = available_area.height() / h s = min(sx, sy) # Fit to the smaller of the two path = self.path * QtGui.QTransform.fromScale(s, s) # Move to bottom left p = path.boundingRect().bottomRight() path = path * QtGui.QTransform.fromTranslate(-p.x(), -p.y()) return path @contextmanager def events_suppressed(self): """ Block change events to prevent feedback loops """ self._blocked = True try: yield finally: self._blocked = False @observe('path', 'scale', 'auto_scale', 'lock_scale', 'mirror', 'align_center', 'rotation', 'auto_rotate', 'copies', 'order', 'copy_spacing', 'copy_weedline', 'copy_weedline_padding', 'plot_weedline', 'plot_weedline_padding', 'feed_to_end', 'feed_after', 'material', 'material.size', 'material.padding', 'auto_copies') def _job_changed(self, change): """ Recreate an instance of of the plot using the current settings """ if self._blocked: return if change['name'] == 'copies': self._desired_copies = self.copies #try: model = QtGui.QPainterPath() if not self.path: return path = self._create_copy() # Update size bbox = path.boundingRect() self.size = [bbox.width(), bbox.height()] # Create copies c = 0 points = self._copy_positions_iter(path) if self.auto_copies: self.stack_size = self._compute_stack_sizes(path) if self.stack_size[0]: copies_left = self.copies % self.stack_size[0] if copies_left: # not a full stack with self.events_suppressed(): self.copies = self._desired_copies self.add_stack() while c < self.copies: x, y = next(points) model.addPath(path * QtGui.QTransform.fromTranslate(x, -y)) c += 1 # Create weedline if self.plot_weedline: self._add_weedline(model, self.plot_weedline_padding) # Move to 0,0 bbox = model.boundingRect() p = bbox.bottomLeft() tx, ty = -p.x(), -p.y() # Center or set to padding tx += ((self.material.width() -bbox.width())/2.0 if self.align_center[0] else self.material.padding_left) ty += (-(self.material.height()-bbox.height())/2.0 if self.align_center[1] else -self.material.padding_bottom) t = QtGui.QTransform.fromTranslate(tx, ty) model = model * t end_point = (QtCore.QPointF( 0, -self.feed_after + model.boundingRect().top()) if self.feed_to_end else QtCore.QPointF(0, 0)) model.moveTo(end_point) # Set new model self.model = model#.simplified() # Set device model #self.device_model = self.device.driver.prepare_job(self) #except: # # Undo the change # if 'oldvalue' in change: # setattr(change['object'],change['name'],change['oldvalue']) # raise #if not self.check_bounds(self.boundingRect(),self.available_area): # raise JobError( # "Plot outside of plotting area, increase the area" # "or decrease the scale or decrease number of copies!") def _check_bounds(self, plot, area): """ Checks that the width and height of plot are less than the width and height of area """ return plot.width() > area.width() or plot.height() > area.height() def _copy_positions_iter(self, path, axis=0): """ Generator that creates positions of points """ other_axis = axis +1 % 2 p = [0, 0] bbox = path.boundingRect() d = (bbox.width(), bbox.height()) pad = self.copy_spacing stack_size = self._compute_stack_sizes(path) while True: p[axis] = 0 yield p # Beginning of each row for i in range(stack_size[axis]-1): p[axis] += d[axis]+pad[axis] yield p p[other_axis] += d[other_axis]+pad[other_axis] def _compute_stack_sizes(self, path): # Usable area material = self.material a = [material.width(), material.height()] a[0] -= material.padding[Padding.LEFT] + material.padding[Padding.RIGHT] a[1] -= material.padding[Padding.TOP] + material.padding[Padding.BOTTOM] # Clone includes weedline but not spacing bbox = path.boundingRect() size = [bbox.width(), bbox.height()] stack_size = [0, 0] p = [0, 0] for i in range(2): # Compute stack while (p[i]+size[i]) < a[i]: # while another one fits stack_size[i] += 1 p[i] += size[i] + self.copy_spacing[i] # Add only to end self.stack_size = stack_size return stack_size def _add_weedline(self, path, padding): """ Adds a weedline to the path by creating a box around the path with the given padding """ bbox = path.boundingRect() w, h = bbox.width(), bbox.height() tl = bbox.topLeft() x = tl.x() - padding[Padding.LEFT] y = tl.y() - padding[Padding.TOP] w += padding[Padding.LEFT] + padding[Padding.RIGHT] h += padding[Padding.TOP] + padding[Padding.BOTTOM] path.addRect(x, y, w, h) return path @property def state(self): pass @property def move_path(self): """ Returns the path the head moves when not cutting """ # Compute the negative path = QtGui.QPainterPath() for i in range(self.model.elementCount()): e = self.model.elementAt(i) if e.isMoveTo(): path.lineTo(e.x, e.y) else: path.moveTo(e.x, e.y) return path @property def cut_path(self): """ Returns path where it is cutting """ return self.model # def get_offset_path(self,device): # """ Returns path where it is cutting """ # path = QtGui.QPainterPath() # _p = QtCore.QPointF(0,0) # previous point # step = 0.1 # for subpath in QtSvgDoc.toSubpathList(self.model):#.toSubpathPolygons(): # e = subpath.elementAt(0) # path.moveTo(QtCore.QPointF(e.x,e.y)) # length = subpath.length() # distance = 0 # while distance<=length: # t = subpath.percentAtLength(distance) # p = subpath.pointAtPercent(t) # a = subpath.angleAtPercent(t)+90 # #path.moveTo(p)#QtCore.QPointF(x,y)) # # TOOD: Do i need numpy here??? # x = p.x()+np.multiply(self.device.blade_offset,np.sin(np.deg2rad(a))) # y = p.y()+np.multiply(self.device.blade_offset,np.cos(np.deg2rad(a))) # path.lineTo(QtCore.QPointF(x,y)) # distance+=step # #_p = p # update last # # return path def add_stack(self): """ Add a complete stack or fill the row """ copies_left = self.stack_size[0]-(self.copies % self.stack_size[0]) if copies_left == 0: # Add full stack self.copies = self.copies + self.stack_size[0] else: # Fill stack self.copies = self.copies+copies_left def remove_stack(self): """ Remove a complete stack or the rest of the row """ if self.copies <= self.stack_size[0]: self.copies = 1 return copies_left = self.copies % self.stack_size[0] if copies_left == 0: # Add full stack self.copies = self.copies - self.stack_size[0] else: # Fill stack self.copies = self.copies - copies_left def clone(self): """ Return a cloned instance of this object """ state = self.__getstate__() state.update({ 'material': Material(**self.material.__getstate__()), 'info': JobInfo(**self.info.__getstate__()), }) return Job(**state)
def test_svg(path): f = os.path.join('tests', 'data', path) doc = QtSvgDoc(f)