- added a visual cue in Menu -> Edit about the entries to enter the Editor and to Save & Exit Editor. When one is enabled the other is disabled.
- grouped all the UI files in flatcamGUI folder
This commit is contained in:
5727
flatcamGUI/FlatCAMGUI.py
Normal file
5727
flatcamGUI/FlatCAMGUI.py
Normal file
File diff suppressed because it is too large
Load Diff
1420
flatcamGUI/GUIElements.py
Normal file
1420
flatcamGUI/GUIElements.py
Normal file
File diff suppressed because it is too large
Load Diff
1676
flatcamGUI/ObjectUI.py
Normal file
1676
flatcamGUI/ObjectUI.py
Normal file
File diff suppressed because it is too large
Load Diff
236
flatcamGUI/PlotCanvas.py
Normal file
236
flatcamGUI/PlotCanvas.py
Normal file
@@ -0,0 +1,236 @@
|
||||
############################################################
|
||||
# FlatCAM: 2D Post-processing for Manufacturing #
|
||||
# http://caram.cl/software/flatcam #
|
||||
# Author: Juan Pablo Caram (c) #
|
||||
# Date: 2/5/2014 #
|
||||
# MIT Licence #
|
||||
############################################################
|
||||
|
||||
from PyQt5 import QtCore
|
||||
|
||||
import logging
|
||||
from flatcamGUI.VisPyCanvas import VisPyCanvas, time
|
||||
from flatcamGUI.VisPyVisuals import ShapeGroup, ShapeCollection, TextCollection, TextGroup, Cursor
|
||||
from vispy.scene.visuals import InfiniteLine, Line
|
||||
import numpy as np
|
||||
from vispy.geometry import Rect
|
||||
|
||||
log = logging.getLogger('base')
|
||||
|
||||
|
||||
class PlotCanvas(QtCore.QObject):
|
||||
"""
|
||||
Class handling the plotting area in the application.
|
||||
"""
|
||||
|
||||
def __init__(self, container, app):
|
||||
"""
|
||||
The constructor configures the VisPy figure that
|
||||
will contain all plots, creates the base axes and connects
|
||||
events to the plotting area.
|
||||
|
||||
:param container: The parent container in which to draw plots.
|
||||
:rtype: PlotCanvas
|
||||
"""
|
||||
|
||||
super(PlotCanvas, self).__init__()
|
||||
|
||||
self.app = app
|
||||
|
||||
# Parent container
|
||||
self.container = container
|
||||
|
||||
# workspace lines; I didn't use the rectangle because I didn't want to add another VisPy Node,
|
||||
# which might decrease performance
|
||||
self.b_line, self.r_line, self.t_line, self.l_line = None, None, None, None
|
||||
|
||||
# Attach to parent
|
||||
self.vispy_canvas = VisPyCanvas()
|
||||
|
||||
self.vispy_canvas.create_native()
|
||||
self.vispy_canvas.native.setParent(self.app.ui)
|
||||
self.container.addWidget(self.vispy_canvas.native)
|
||||
|
||||
### AXIS ###
|
||||
self.v_line = InfiniteLine(pos=0, color=(0.70, 0.3, 0.3, 1.0), vertical=True,
|
||||
parent=self.vispy_canvas.view.scene)
|
||||
|
||||
self.h_line = InfiniteLine(pos=0, color=(0.70, 0.3, 0.3, 1.0), vertical=False,
|
||||
parent=self.vispy_canvas.view.scene)
|
||||
|
||||
# draw a rectangle made out of 4 lines on the canvas to serve as a hint for the work area
|
||||
# all CNC have a limited workspace
|
||||
|
||||
self.draw_workspace()
|
||||
|
||||
# if self.app.defaults['global_workspace'] is True:
|
||||
# if self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().upper() == 'MM':
|
||||
# self.wkspace_t = Line(pos=)
|
||||
|
||||
self.shape_collections = []
|
||||
|
||||
self.shape_collection = self.new_shape_collection()
|
||||
self.app.pool_recreated.connect(self.on_pool_recreated)
|
||||
self.text_collection = self.new_text_collection()
|
||||
|
||||
# TODO: Should be setting to show/hide CNC job annotations (global or per object)
|
||||
self.text_collection.enabled = False
|
||||
|
||||
# draw a rectangle made out of 4 lines on the canvas to serve as a hint for the work area
|
||||
# all CNC have a limited workspace
|
||||
def draw_workspace(self):
|
||||
a = np.empty((0, 0))
|
||||
|
||||
a4p_in = np.array([(0, 0), (8.3, 0), (8.3, 11.7), (0, 11.7)])
|
||||
a4l_in = np.array([(0, 0), (11.7, 0), (11.7, 8.3), (0, 8.3)])
|
||||
a3p_in = np.array([(0, 0), (11.7, 0), (11.7, 16.5), (0, 16.5)])
|
||||
a3l_in = np.array([(0, 0), (16.5, 0), (16.5, 11.7), (0, 11.7)])
|
||||
|
||||
a4p_mm = np.array([(0, 0), (210, 0), (210, 297), (0, 297)])
|
||||
a4l_mm = np.array([(0, 0), (297, 0), (297,210), (0, 210)])
|
||||
a3p_mm = np.array([(0, 0), (297, 0), (297, 420), (0, 420)])
|
||||
a3l_mm = np.array([(0, 0), (420, 0), (420, 297), (0, 297)])
|
||||
|
||||
if self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().upper() == 'MM':
|
||||
if self.app.defaults['global_workspaceT'] == 'A4P':
|
||||
a = a4p_mm
|
||||
elif self.app.defaults['global_workspaceT'] == 'A4L':
|
||||
a = a4l_mm
|
||||
elif self.app.defaults['global_workspaceT'] == 'A3P':
|
||||
a = a3p_mm
|
||||
elif self.app.defaults['global_workspaceT'] == 'A3L':
|
||||
a = a3l_mm
|
||||
else:
|
||||
if self.app.defaults['global_workspaceT'] == 'A4P':
|
||||
a = a4p_in
|
||||
elif self.app.defaults['global_workspaceT'] == 'A4L':
|
||||
a = a4l_in
|
||||
elif self.app.defaults['global_workspaceT'] == 'A3P':
|
||||
a = a3p_in
|
||||
elif self.app.defaults['global_workspaceT'] == 'A3L':
|
||||
a = a3l_in
|
||||
|
||||
self.delete_workspace()
|
||||
|
||||
self.b_line = Line(pos=a[0:2], color=(0.70, 0.3, 0.3, 1.0),
|
||||
antialias= True, method='agg', parent=self.vispy_canvas.view.scene)
|
||||
self.r_line = Line(pos=a[1:3], color=(0.70, 0.3, 0.3, 1.0),
|
||||
antialias= True, method='agg', parent=self.vispy_canvas.view.scene)
|
||||
|
||||
self.t_line = Line(pos=a[2:4], color=(0.70, 0.3, 0.3, 1.0),
|
||||
antialias= True, method='agg', parent=self.vispy_canvas.view.scene)
|
||||
self.l_line = Line(pos=np.array((a[0], a[3])), color=(0.70, 0.3, 0.3, 1.0),
|
||||
antialias= True, method='agg', parent=self.vispy_canvas.view.scene)
|
||||
|
||||
if self.app.defaults['global_workspace'] is False:
|
||||
self.delete_workspace()
|
||||
|
||||
# delete the workspace lines from the plot by removing the parent
|
||||
def delete_workspace(self):
|
||||
try:
|
||||
self.b_line.parent = None
|
||||
self.r_line.parent = None
|
||||
self.t_line.parent = None
|
||||
self.l_line.parent = None
|
||||
except:
|
||||
pass
|
||||
|
||||
# redraw the workspace lines on the plot by readding them to the parent view.scene
|
||||
def restore_workspace(self):
|
||||
try:
|
||||
self.b_line.parent = self.vispy_canvas.view.scene
|
||||
self.r_line.parent = self.vispy_canvas.view.scene
|
||||
self.t_line.parent = self.vispy_canvas.view.scene
|
||||
self.l_line.parent = self.vispy_canvas.view.scene
|
||||
except:
|
||||
pass
|
||||
|
||||
def vis_connect(self, event_name, callback):
|
||||
return getattr(self.vispy_canvas.events, event_name).connect(callback)
|
||||
|
||||
def vis_disconnect(self, event_name, callback):
|
||||
getattr(self.vispy_canvas.events, event_name).disconnect(callback)
|
||||
|
||||
def zoom(self, factor, center=None):
|
||||
"""
|
||||
Zooms the plot by factor around a given
|
||||
center point. Takes care of re-drawing.
|
||||
|
||||
:param factor: Number by which to scale the plot.
|
||||
:type factor: float
|
||||
:param center: Coordinates [x, y] of the point around which to scale the plot.
|
||||
:type center: list
|
||||
:return: None
|
||||
"""
|
||||
self.vispy_canvas.view.camera.zoom(factor, center)
|
||||
|
||||
def new_shape_group(self, shape_collection=None):
|
||||
if shape_collection:
|
||||
return ShapeGroup(shape_collection)
|
||||
return ShapeGroup(self.shape_collection)
|
||||
|
||||
def new_shape_collection(self, **kwargs):
|
||||
# sc = ShapeCollection(parent=self.vispy_canvas.view.scene, pool=self.app.pool, **kwargs)
|
||||
# self.shape_collections.append(sc)
|
||||
# return sc
|
||||
return ShapeCollection(parent=self.vispy_canvas.view.scene, pool=self.app.pool, **kwargs)
|
||||
|
||||
def new_cursor(self):
|
||||
c = Cursor(pos=np.empty((0, 2)), parent=self.vispy_canvas.view.scene)
|
||||
c.antialias = 0
|
||||
return c
|
||||
|
||||
def new_text_group(self):
|
||||
return TextGroup(self.text_collection)
|
||||
|
||||
def new_text_collection(self, **kwargs):
|
||||
return TextCollection(parent=self.vispy_canvas.view.scene, **kwargs)
|
||||
|
||||
def fit_view(self, rect=None):
|
||||
|
||||
# Lock updates in other threads
|
||||
self.shape_collection.lock_updates()
|
||||
|
||||
if not rect:
|
||||
rect = Rect(-1, -1, 20, 20)
|
||||
try:
|
||||
rect.left, rect.right = self.shape_collection.bounds(axis=0)
|
||||
rect.bottom, rect.top = self.shape_collection.bounds(axis=1)
|
||||
except TypeError:
|
||||
pass
|
||||
|
||||
# adjust the view camera to be slightly bigger than the bounds so the shape colleaction can be seen clearly
|
||||
# otherwise the shape collection boundary will have no border
|
||||
rect.left *= 0.96
|
||||
rect.bottom *= 0.96
|
||||
rect.right *= 1.01
|
||||
rect.top *= 1.01
|
||||
|
||||
self.vispy_canvas.view.camera.rect = rect
|
||||
|
||||
self.shape_collection.unlock_updates()
|
||||
|
||||
def fit_center(self, loc, rect=None):
|
||||
|
||||
# Lock updates in other threads
|
||||
self.shape_collection.lock_updates()
|
||||
|
||||
if not rect:
|
||||
try:
|
||||
rect = Rect(loc[0]-20, loc[1]-20, 40, 40)
|
||||
except TypeError:
|
||||
pass
|
||||
|
||||
self.vispy_canvas.view.camera.rect = rect
|
||||
|
||||
self.shape_collection.unlock_updates()
|
||||
|
||||
def clear(self):
|
||||
pass
|
||||
|
||||
def redraw(self):
|
||||
self.shape_collection.redraw([])
|
||||
self.text_collection.redraw()
|
||||
|
||||
def on_pool_recreated(self, pool):
|
||||
self.shape_collection.pool = pool
|
||||
175
flatcamGUI/VisPyCanvas.py
Normal file
175
flatcamGUI/VisPyCanvas.py
Normal file
@@ -0,0 +1,175 @@
|
||||
############################################################
|
||||
# FlatCAM: 2D Post-processing for Manufacturing #
|
||||
# http://flatcam.org #
|
||||
# File Author: Dennis Hayrullin #
|
||||
# Date: 2/5/2016 #
|
||||
# MIT Licence #
|
||||
############################################################
|
||||
|
||||
import numpy as np
|
||||
from PyQt5.QtGui import QPalette
|
||||
import vispy.scene as scene
|
||||
from vispy.scene.cameras.base_camera import BaseCamera
|
||||
from vispy.color import Color
|
||||
import time
|
||||
|
||||
white = Color("#ffffff" )
|
||||
black = Color("#000000")
|
||||
|
||||
|
||||
class VisPyCanvas(scene.SceneCanvas):
|
||||
|
||||
def __init__(self, config=None):
|
||||
scene.SceneCanvas.__init__(self, keys=None, config=config)
|
||||
|
||||
self.unfreeze()
|
||||
|
||||
back_color = str(QPalette().color(QPalette.Window).name())
|
||||
|
||||
self.central_widget.bgcolor = back_color
|
||||
self.central_widget.border_color = back_color
|
||||
|
||||
self.grid_widget = self.central_widget.add_grid(margin=10)
|
||||
self.grid_widget.spacing = 0
|
||||
|
||||
top_padding = self.grid_widget.add_widget(row=0, col=0, col_span=2)
|
||||
top_padding.height_max = 0
|
||||
|
||||
self.yaxis = scene.AxisWidget(orientation='left', axis_color='black', text_color='black', font_size=8)
|
||||
self.yaxis.width_max = 55
|
||||
self.grid_widget.add_widget(self.yaxis, row=1, col=0)
|
||||
|
||||
self.xaxis = scene.AxisWidget(orientation='bottom', axis_color='black', text_color='black', font_size=8)
|
||||
self.xaxis.height_max = 25
|
||||
self.grid_widget.add_widget(self.xaxis, row=2, col=1)
|
||||
|
||||
right_padding = self.grid_widget.add_widget(row=0, col=2, row_span=2)
|
||||
# right_padding.width_max = 24
|
||||
right_padding.width_max = 0
|
||||
|
||||
view = self.grid_widget.add_view(row=1, col=1, border_color='black', bgcolor='white')
|
||||
view.camera = Camera(aspect=1, rect=(-25,-25,150,150))
|
||||
|
||||
# Following function was removed from 'prepare_draw()' of 'Grid' class by patch,
|
||||
# it is necessary to call manually
|
||||
self.grid_widget._update_child_widget_dim()
|
||||
|
||||
self.xaxis.link_view(view)
|
||||
self.yaxis.link_view(view)
|
||||
|
||||
grid1 = scene.GridLines(parent=view.scene, color='dimgray')
|
||||
grid1.set_gl_state(depth_test=False)
|
||||
|
||||
self.view = view
|
||||
self.grid = grid1
|
||||
|
||||
self.freeze()
|
||||
|
||||
# self.measure_fps()
|
||||
|
||||
def translate_coords(self, pos):
|
||||
tr = self.grid.get_transform('canvas', 'visual')
|
||||
return tr.map(pos)
|
||||
|
||||
def translate_coords_2(self, pos):
|
||||
tr = self.grid.get_transform('visual', 'document')
|
||||
return tr.map(pos)
|
||||
|
||||
|
||||
class Camera(scene.PanZoomCamera):
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(Camera, self).__init__(**kwargs)
|
||||
|
||||
self.minimum_scene_size = 0.01
|
||||
self.maximum_scene_size = 10000
|
||||
|
||||
self.last_event = None
|
||||
self.last_time = 0
|
||||
|
||||
# Default mouse button for panning is RMB
|
||||
self.pan_button_setting = "2"
|
||||
|
||||
def zoom(self, factor, center=None):
|
||||
center = center if (center is not None) else self.center
|
||||
super(Camera, self).zoom(factor, center)
|
||||
|
||||
def viewbox_mouse_event(self, event):
|
||||
"""
|
||||
The SubScene received a mouse event; update transform
|
||||
accordingly.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
event : instance of Event
|
||||
The event.
|
||||
"""
|
||||
if event.handled or not self.interactive:
|
||||
return
|
||||
|
||||
# Limit mouse move events
|
||||
last_event = event.last_event
|
||||
t = time.time()
|
||||
if t - self.last_time > 0.015:
|
||||
self.last_time = t
|
||||
if self.last_event:
|
||||
last_event = self.last_event
|
||||
self.last_event = None
|
||||
else:
|
||||
if not self.last_event:
|
||||
self.last_event = last_event
|
||||
event.handled = True
|
||||
return
|
||||
|
||||
# Scrolling
|
||||
BaseCamera.viewbox_mouse_event(self, event)
|
||||
|
||||
if event.type == 'mouse_wheel':
|
||||
center = self._scene_transform.imap(event.pos)
|
||||
scale = (1 + self.zoom_factor) ** (-event.delta[1] * 30)
|
||||
self.limited_zoom(scale, center)
|
||||
event.handled = True
|
||||
|
||||
elif event.type == 'mouse_move':
|
||||
if event.press_event is None:
|
||||
return
|
||||
|
||||
modifiers = event.mouse_event.modifiers
|
||||
|
||||
# self.pan_button_setting is actually self.FlatCAM.APP.defaults['global_pan_button']
|
||||
if event.button == int(self.pan_button_setting) and not modifiers:
|
||||
# Translate
|
||||
p1 = np.array(last_event.pos)[:2]
|
||||
p2 = np.array(event.pos)[:2]
|
||||
p1s = self._transform.imap(p1)
|
||||
p2s = self._transform.imap(p2)
|
||||
self.pan(p1s-p2s)
|
||||
event.handled = True
|
||||
elif event.button in [2, 3] and 'Shift' in modifiers:
|
||||
# Zoom
|
||||
p1c = np.array(last_event.pos)[:2]
|
||||
p2c = np.array(event.pos)[:2]
|
||||
scale = ((1 + self.zoom_factor) **
|
||||
((p1c-p2c) * np.array([1, -1])))
|
||||
center = self._transform.imap(event.press_event.pos[:2])
|
||||
self.limited_zoom(scale, center)
|
||||
event.handled = True
|
||||
else:
|
||||
event.handled = False
|
||||
elif event.type == 'mouse_press':
|
||||
# accept the event if it is button 1 or 2.
|
||||
# This is required in order to receive future events
|
||||
event.handled = event.button in [1, 2, 3]
|
||||
else:
|
||||
event.handled = False
|
||||
|
||||
def limited_zoom(self, scale, center):
|
||||
|
||||
try:
|
||||
zoom_in = scale[1] < 1
|
||||
except IndexError:
|
||||
zoom_in = scale < 1
|
||||
|
||||
if (not zoom_in and self.rect.width < self.maximum_scene_size) \
|
||||
or (zoom_in and self.rect.width > self.minimum_scene_size):
|
||||
self.zoom(scale, center)
|
||||
134
flatcamGUI/VisPyPatches.py
Normal file
134
flatcamGUI/VisPyPatches.py
Normal file
@@ -0,0 +1,134 @@
|
||||
############################################################
|
||||
# FlatCAM: 2D Post-processing for Manufacturing #
|
||||
# http://flatcam.org #
|
||||
# File Author: Dennis Hayrullin #
|
||||
# Date: 2/5/2016 #
|
||||
# MIT Licence #
|
||||
############################################################
|
||||
|
||||
from vispy.visuals import markers, LineVisual, InfiniteLineVisual
|
||||
from vispy.visuals.axis import Ticker, _get_ticks_talbot
|
||||
from vispy.scene.widgets import Grid
|
||||
import numpy as np
|
||||
|
||||
|
||||
def apply_patches():
|
||||
# Patch MarkersVisual to have crossed lines marker
|
||||
cross_lines = """
|
||||
float cross(vec2 pointcoord, float size)
|
||||
{
|
||||
//vbar
|
||||
float r1 = abs(pointcoord.x - 0.5)*size;
|
||||
float r2 = abs(pointcoord.y - 0.5)*size - $v_size/2;
|
||||
float vbar = max(r1,r2);
|
||||
//hbar
|
||||
float r3 = abs(pointcoord.y - 0.5)*size;
|
||||
float r4 = abs(pointcoord.x - 0.5)*size - $v_size/2;
|
||||
float hbar = max(r3,r4);
|
||||
return min(vbar, hbar);
|
||||
}
|
||||
"""
|
||||
|
||||
markers._marker_dict['++'] = cross_lines
|
||||
markers.marker_types = tuple(sorted(list(markers._marker_dict.copy().keys())))
|
||||
|
||||
# # Add clear_data method to LineVisual to have possibility of clearing data
|
||||
# def clear_data(self):
|
||||
# self._bounds = None
|
||||
# self._pos = None
|
||||
# self._changed['pos'] = True
|
||||
# self.update()
|
||||
#
|
||||
# LineVisual.clear_data = clear_data
|
||||
|
||||
# Patch VisPy Grid to prevent updating layout on PaintGL, which cause low fps
|
||||
def _prepare_draw(self, view):
|
||||
pass
|
||||
|
||||
def _update_clipper(self):
|
||||
super(Grid, self)._update_clipper()
|
||||
try:
|
||||
self._update_child_widget_dim()
|
||||
except Exception as e:
|
||||
print(e)
|
||||
|
||||
Grid._prepare_draw = _prepare_draw
|
||||
Grid._update_clipper = _update_clipper
|
||||
|
||||
# Patch InfiniteLine visual to 1px width
|
||||
def _prepare_draw(self, view=None):
|
||||
"""This method is called immediately before each draw.
|
||||
The *view* argument indicates which view is about to be drawn.
|
||||
"""
|
||||
GL = None
|
||||
from vispy.app._default_app import default_app
|
||||
|
||||
if default_app is not None and \
|
||||
default_app.backend_name != 'ipynb_webgl':
|
||||
try:
|
||||
import OpenGL.GL as GL
|
||||
except Exception: # can be other than ImportError sometimes
|
||||
pass
|
||||
|
||||
if GL:
|
||||
GL.glDisable(GL.GL_LINE_SMOOTH)
|
||||
GL.glLineWidth(1.0)
|
||||
|
||||
if self._changed['pos']:
|
||||
self.pos_buf.set_data(self._pos)
|
||||
self._changed['pos'] = False
|
||||
|
||||
if self._changed['color']:
|
||||
self._program.vert['color'] = self._color
|
||||
self._changed['color'] = False
|
||||
|
||||
InfiniteLineVisual._prepare_draw = _prepare_draw
|
||||
|
||||
# Patch AxisVisual to have less axis labels
|
||||
def _get_tick_frac_labels(self):
|
||||
"""Get the major ticks, minor ticks, and major labels"""
|
||||
minor_num = 4 # number of minor ticks per major division
|
||||
if (self.axis.scale_type == 'linear'):
|
||||
domain = self.axis.domain
|
||||
if domain[1] < domain[0]:
|
||||
flip = True
|
||||
domain = domain[::-1]
|
||||
else:
|
||||
flip = False
|
||||
offset = domain[0]
|
||||
scale = domain[1] - domain[0]
|
||||
|
||||
transforms = self.axis.transforms
|
||||
length = self.axis.pos[1] - self.axis.pos[0] # in logical coords
|
||||
n_inches = np.sqrt(np.sum(length ** 2)) / transforms.dpi
|
||||
|
||||
# major = np.linspace(domain[0], domain[1], num=11)
|
||||
# major = MaxNLocator(10).tick_values(*domain)
|
||||
major = _get_ticks_talbot(domain[0], domain[1], n_inches, 1)
|
||||
|
||||
labels = ['%g' % x for x in major]
|
||||
majstep = major[1] - major[0]
|
||||
minor = []
|
||||
minstep = majstep / (minor_num + 1)
|
||||
minstart = 0 if self.axis._stop_at_major[0] else -1
|
||||
minstop = -1 if self.axis._stop_at_major[1] else 0
|
||||
for i in range(minstart, len(major) + minstop):
|
||||
maj = major[0] + i * majstep
|
||||
minor.extend(np.linspace(maj + minstep,
|
||||
maj + majstep - minstep,
|
||||
minor_num))
|
||||
major_frac = (major - offset) / scale
|
||||
minor_frac = (np.array(minor) - offset) / scale
|
||||
major_frac = major_frac[::-1] if flip else major_frac
|
||||
use_mask = (major_frac > -0.0001) & (major_frac < 1.0001)
|
||||
major_frac = major_frac[use_mask]
|
||||
labels = [l for li, l in enumerate(labels) if use_mask[li]]
|
||||
minor_frac = minor_frac[(minor_frac > -0.0001) &
|
||||
(minor_frac < 1.0001)]
|
||||
elif self.axis.scale_type == 'logarithmic':
|
||||
return NotImplementedError
|
||||
elif self.axis.scale_type == 'power':
|
||||
return NotImplementedError
|
||||
return major_frac, minor_frac, labels
|
||||
|
||||
Ticker._get_tick_frac_labels = _get_tick_frac_labels
|
||||
98
flatcamGUI/VisPyTesselators.py
Normal file
98
flatcamGUI/VisPyTesselators.py
Normal file
@@ -0,0 +1,98 @@
|
||||
############################################################
|
||||
# FlatCAM: 2D Post-processing for Manufacturing #
|
||||
# http://flatcam.org #
|
||||
# File Author: Dennis Hayrullin #
|
||||
# Date: 2/5/2016 #
|
||||
# MIT Licence #
|
||||
############################################################
|
||||
|
||||
from OpenGL import GLU
|
||||
|
||||
|
||||
class GLUTess:
|
||||
def __init__(self):
|
||||
"""
|
||||
OpenGL GLU triangulation class
|
||||
"""
|
||||
self.tris = []
|
||||
self.pts = []
|
||||
self.vertex_index = 0
|
||||
|
||||
def _on_begin_primitive(self, type):
|
||||
pass
|
||||
|
||||
def _on_new_vertex(self, vertex):
|
||||
self.tris.append(vertex)
|
||||
|
||||
# Force GLU to return separate triangles (GLU_TRIANGLES)
|
||||
def _on_edge_flag(self, flag):
|
||||
pass
|
||||
|
||||
def _on_combine(self, coords, data, weight):
|
||||
return (coords[0], coords[1], coords[2])
|
||||
|
||||
def _on_error(self, errno):
|
||||
print("GLUTess error:", errno)
|
||||
|
||||
def _on_end_primitive(self):
|
||||
pass
|
||||
|
||||
def triangulate(self, polygon):
|
||||
"""
|
||||
Triangulates polygon
|
||||
:param polygon: shapely.geometry.polygon
|
||||
Polygon to tessellate
|
||||
:return: list, list
|
||||
Array of triangle vertex indices [t0i0, t0i1, t0i2, t1i0, t1i1, ... ]
|
||||
Array of polygon points [(x0, y0), (x1, y1), ... ]
|
||||
"""
|
||||
# Create tessellation object
|
||||
tess = GLU.gluNewTess()
|
||||
|
||||
# Setup callbacks
|
||||
GLU.gluTessCallback(tess, GLU.GLU_TESS_BEGIN, self._on_begin_primitive)
|
||||
GLU.gluTessCallback(tess, GLU.GLU_TESS_VERTEX, self._on_new_vertex)
|
||||
GLU.gluTessCallback(tess, GLU.GLU_TESS_EDGE_FLAG, self._on_edge_flag)
|
||||
GLU.gluTessCallback(tess, GLU.GLU_TESS_COMBINE, self._on_combine)
|
||||
GLU.gluTessCallback(tess, GLU.GLU_TESS_ERROR, self._on_error)
|
||||
GLU.gluTessCallback(tess, GLU.GLU_TESS_END, self._on_end_primitive)
|
||||
|
||||
# Reset data
|
||||
del self.tris[:]
|
||||
del self.pts[:]
|
||||
self.vertex_index = 0
|
||||
|
||||
# Define polygon
|
||||
GLU.gluTessBeginPolygon(tess, None)
|
||||
|
||||
def define_contour(contour):
|
||||
vertices = list(contour.coords) # Get vertices coordinates
|
||||
if vertices[0] == vertices[-1]: # Open ring
|
||||
vertices = vertices[:-1]
|
||||
|
||||
self.pts += vertices
|
||||
|
||||
GLU.gluTessBeginContour(tess) # Start contour
|
||||
|
||||
# Set vertices
|
||||
for vertex in vertices:
|
||||
point = (vertex[0], vertex[1], 0)
|
||||
GLU.gluTessVertex(tess, point, self.vertex_index)
|
||||
self.vertex_index += 1
|
||||
|
||||
GLU.gluTessEndContour(tess) # End contour
|
||||
|
||||
# Polygon exterior
|
||||
define_contour(polygon.exterior)
|
||||
|
||||
# Interiors
|
||||
for interior in polygon.interiors:
|
||||
define_contour(interior)
|
||||
|
||||
# Start tessellation
|
||||
GLU.gluTessEndPolygon(tess)
|
||||
|
||||
# Free resources
|
||||
GLU.gluDeleteTess(tess)
|
||||
|
||||
return self.tris, self.pts
|
||||
604
flatcamGUI/VisPyVisuals.py
Normal file
604
flatcamGUI/VisPyVisuals.py
Normal file
@@ -0,0 +1,604 @@
|
||||
############################################################
|
||||
# FlatCAM: 2D Post-processing for Manufacturing #
|
||||
# http://flatcam.org #
|
||||
# File Author: Dennis Hayrullin #
|
||||
# Date: 2/5/2016 #
|
||||
# MIT Licence #
|
||||
############################################################
|
||||
|
||||
from vispy.visuals import CompoundVisual, LineVisual, MeshVisual, TextVisual, MarkersVisual
|
||||
from vispy.scene.visuals import VisualNode, generate_docstring, visuals
|
||||
from vispy.gloo import set_state
|
||||
from vispy.color import Color
|
||||
from shapely.geometry import Polygon, LineString, LinearRing
|
||||
import threading
|
||||
import numpy as np
|
||||
from flatcamGUI.VisPyTesselators import GLUTess
|
||||
|
||||
|
||||
class FlatCAMLineVisual(LineVisual):
|
||||
def __init__(self, pos=None, color=(0.5, 0.5, 0.5, 1), width=1, connect='strip',
|
||||
method='gl', antialias=False):
|
||||
LineVisual.__init__(self, pos=None, color=(0.5, 0.5, 0.5, 1), width=1, connect='strip',
|
||||
method='gl', antialias=True)
|
||||
|
||||
def clear_data(self):
|
||||
self._bounds = None
|
||||
self._pos = None
|
||||
self._changed['pos'] = True
|
||||
self.update()
|
||||
|
||||
|
||||
def _update_shape_buffers(data, triangulation='glu'):
|
||||
"""
|
||||
Translates Shapely geometry to internal buffers for speedup redraws
|
||||
:param data: dict
|
||||
Input shape data
|
||||
:param triangulation: str
|
||||
Triangulation engine
|
||||
"""
|
||||
mesh_vertices = [] # Vertices for mesh
|
||||
mesh_tris = [] # Faces for mesh
|
||||
mesh_colors = [] # Face colors
|
||||
line_pts = [] # Vertices for line
|
||||
line_colors = [] # Line color
|
||||
|
||||
geo, color, face_color, tolerance = data['geometry'], data['color'], data['face_color'], data['tolerance']
|
||||
|
||||
if geo is not None and not geo.is_empty:
|
||||
simple = geo.simplify(tolerance) if tolerance else geo # Simplified shape
|
||||
pts = [] # Shape line points
|
||||
tri_pts = [] # Mesh vertices
|
||||
tri_tris = [] # Mesh faces
|
||||
|
||||
if type(geo) == LineString:
|
||||
# Prepare lines
|
||||
pts = _linestring_to_segments(list(simple.coords))
|
||||
|
||||
elif type(geo) == LinearRing:
|
||||
# Prepare lines
|
||||
pts = _linearring_to_segments(list(simple.coords))
|
||||
|
||||
elif type(geo) == Polygon:
|
||||
# Prepare polygon faces
|
||||
if face_color is not None:
|
||||
if triangulation == 'glu':
|
||||
gt = GLUTess()
|
||||
tri_tris, tri_pts = gt.triangulate(simple)
|
||||
else:
|
||||
print("Triangulation type '%s' isn't implemented. Drawing only edges." % triangulation)
|
||||
|
||||
# Prepare polygon edges
|
||||
if color is not None:
|
||||
pts = _linearring_to_segments(list(simple.exterior.coords))
|
||||
for ints in simple.interiors:
|
||||
pts += _linearring_to_segments(list(ints.coords))
|
||||
|
||||
# Appending data for mesh
|
||||
if len(tri_pts) > 0 and len(tri_tris) > 0:
|
||||
mesh_tris += tri_tris
|
||||
mesh_vertices += tri_pts
|
||||
mesh_colors += [Color(face_color).rgba] * (len(tri_tris) // 3)
|
||||
|
||||
# Appending data for line
|
||||
if len(pts) > 0:
|
||||
line_pts += pts
|
||||
line_colors += [Color(color).rgba] * len(pts)
|
||||
|
||||
# Store buffers
|
||||
data['line_pts'] = line_pts
|
||||
data['line_colors'] = line_colors
|
||||
data['mesh_vertices'] = mesh_vertices
|
||||
data['mesh_tris'] = mesh_tris
|
||||
data['mesh_colors'] = mesh_colors
|
||||
|
||||
# Clear shapely geometry
|
||||
del data['geometry']
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def _linearring_to_segments(arr):
|
||||
# Close linear ring
|
||||
"""
|
||||
Translates linear ring to line segments
|
||||
:param arr: numpy.array
|
||||
Array of linear ring vertices
|
||||
:return: numpy.array
|
||||
Line segments
|
||||
"""
|
||||
if arr[0] != arr[-1]:
|
||||
arr.append(arr[0])
|
||||
|
||||
return _linestring_to_segments(arr)
|
||||
|
||||
|
||||
def _linestring_to_segments(arr):
|
||||
"""
|
||||
Translates line strip to segments
|
||||
:param arr: numpy.array
|
||||
Array of line strip vertices
|
||||
:return: numpy.array
|
||||
Line segments
|
||||
"""
|
||||
return [arr[i // 2] for i in range(0, len(arr) * 2)][1:-1]
|
||||
|
||||
|
||||
class ShapeGroup(object):
|
||||
def __init__(self, collection):
|
||||
"""
|
||||
Represents group of shapes in collection
|
||||
:param collection: ShapeCollection
|
||||
Collection to work with
|
||||
"""
|
||||
self._collection = collection
|
||||
self._indexes = []
|
||||
self._visible = True
|
||||
self._color = None
|
||||
|
||||
def add(self, **kwargs):
|
||||
"""
|
||||
Adds shape to collection and store index in group
|
||||
:param kwargs: keyword arguments
|
||||
Arguments for ShapeCollection.add function
|
||||
"""
|
||||
self._indexes.append(self._collection.add(**kwargs))
|
||||
|
||||
def clear(self, update=False):
|
||||
"""
|
||||
Removes group shapes from collection, clear indexes
|
||||
:param update: bool
|
||||
Set True to redraw collection
|
||||
"""
|
||||
for i in self._indexes:
|
||||
self._collection.remove(i, False)
|
||||
|
||||
del self._indexes[:]
|
||||
|
||||
if update:
|
||||
self._collection.redraw([]) # Skip waiting results
|
||||
|
||||
def redraw(self):
|
||||
"""
|
||||
Redraws shape collection
|
||||
"""
|
||||
self._collection.redraw(self._indexes)
|
||||
|
||||
@property
|
||||
def visible(self):
|
||||
"""
|
||||
Visibility of group
|
||||
:return: bool
|
||||
"""
|
||||
return self._visible
|
||||
|
||||
@visible.setter
|
||||
def visible(self, value):
|
||||
"""
|
||||
Visibility of group
|
||||
:param value: bool
|
||||
"""
|
||||
self._visible = value
|
||||
for i in self._indexes:
|
||||
self._collection.data[i]['visible'] = value
|
||||
|
||||
self._collection.redraw([])
|
||||
|
||||
|
||||
class ShapeCollectionVisual(CompoundVisual):
|
||||
|
||||
def __init__(self, line_width=1, triangulation='gpc', layers=3, pool=None, **kwargs):
|
||||
"""
|
||||
Represents collection of shapes to draw on VisPy scene
|
||||
:param line_width: float
|
||||
Width of lines/edges
|
||||
:param triangulation: str
|
||||
Triangulation method used for polygons translation
|
||||
'vispy' - VisPy lib triangulation
|
||||
'gpc' - Polygon2 lib
|
||||
:param layers: int
|
||||
Layers count
|
||||
Each layer adds 2 visuals on VisPy scene. Be careful: more layers cause less fps
|
||||
:param kwargs:
|
||||
"""
|
||||
self.data = {}
|
||||
self.last_key = -1
|
||||
|
||||
# Thread locks
|
||||
self.key_lock = threading.Lock()
|
||||
self.results_lock = threading.Lock()
|
||||
self.update_lock = threading.Lock()
|
||||
|
||||
# Process pool
|
||||
self.pool = pool
|
||||
self.results = {}
|
||||
|
||||
self._meshes = [MeshVisual() for _ in range(0, layers)]
|
||||
# self._lines = [LineVisual(antialias=True) for _ in range(0, layers)]
|
||||
self._lines = [FlatCAMLineVisual(antialias=True) for _ in range(0, layers)]
|
||||
|
||||
self._line_width = line_width
|
||||
self._triangulation = triangulation
|
||||
|
||||
visuals_ = [self._lines[i // 2] if i % 2 else self._meshes[i // 2] for i in range(0, layers * 2)]
|
||||
|
||||
CompoundVisual.__init__(self, visuals_, **kwargs)
|
||||
|
||||
for m in self._meshes:
|
||||
pass
|
||||
m.set_gl_state(polygon_offset_fill=True, polygon_offset=(1, 1), cull_face=False)
|
||||
|
||||
for l in self._lines:
|
||||
pass
|
||||
l.set_gl_state(blend=True)
|
||||
|
||||
self.freeze()
|
||||
|
||||
def add(self, shape=None, color=None, face_color=None, alpha=None, visible=True,
|
||||
update=False, layer=1, tolerance=0.01):
|
||||
"""
|
||||
Adds shape to collection
|
||||
:return:
|
||||
:param shape: shapely.geometry
|
||||
Shapely geometry object
|
||||
:param color: str, tuple
|
||||
Line/edge color
|
||||
:param face_color: str, tuple
|
||||
Polygon face color
|
||||
:param visible: bool
|
||||
Shape visibility
|
||||
:param update: bool
|
||||
Set True to redraw collection
|
||||
:param layer: int
|
||||
Layer number. 0 - lowest.
|
||||
:param tolerance: float
|
||||
Geometry simplifying tolerance
|
||||
:return: int
|
||||
Index of shape
|
||||
"""
|
||||
# Get new key
|
||||
self.key_lock.acquire(True)
|
||||
self.last_key += 1
|
||||
key = self.last_key
|
||||
self.key_lock.release()
|
||||
|
||||
# Prepare data for translation
|
||||
self.data[key] = {'geometry': shape, 'color': color, 'alpha': alpha, 'face_color': face_color,
|
||||
'visible': visible, 'layer': layer, 'tolerance': tolerance}
|
||||
|
||||
# Add data to process pool if pool exists
|
||||
try:
|
||||
self.results[key] = self.pool.map_async(_update_shape_buffers, [self.data[key]])
|
||||
except:
|
||||
self.data[key] = _update_shape_buffers(self.data[key])
|
||||
|
||||
if update:
|
||||
self.redraw() # redraw() waits for pool process end
|
||||
|
||||
return key
|
||||
|
||||
def remove(self, key, update=False):
|
||||
"""
|
||||
Removes shape from collection
|
||||
:param key: int
|
||||
Shape index to remove
|
||||
:param update:
|
||||
Set True to redraw collection
|
||||
"""
|
||||
# Remove process result
|
||||
self.results_lock.acquire(True)
|
||||
if key in list(self.results.copy().keys()):
|
||||
del self.results[key]
|
||||
self.results_lock.release()
|
||||
|
||||
# Remove data
|
||||
del self.data[key]
|
||||
|
||||
if update:
|
||||
self.__update()
|
||||
|
||||
def clear(self, update=False):
|
||||
"""
|
||||
Removes all shapes from collection
|
||||
:param update: bool
|
||||
Set True to redraw collection
|
||||
"""
|
||||
self.data.clear()
|
||||
if update:
|
||||
self.__update()
|
||||
|
||||
def __update(self):
|
||||
"""
|
||||
Merges internal buffers, sets data to visuals, redraws collection on scene
|
||||
"""
|
||||
mesh_vertices = [[] for _ in range(0, len(self._meshes))] # Vertices for mesh
|
||||
mesh_tris = [[] for _ in range(0, len(self._meshes))] # Faces for mesh
|
||||
mesh_colors = [[] for _ in range(0, len(self._meshes))] # Face colors
|
||||
line_pts = [[] for _ in range(0, len(self._lines))] # Vertices for line
|
||||
line_colors = [[] for _ in range(0, len(self._lines))] # Line color
|
||||
|
||||
# Lock sub-visuals updates
|
||||
self.update_lock.acquire(True)
|
||||
|
||||
# Merge shapes buffers
|
||||
for data in list(self.data.values()):
|
||||
if data['visible'] and 'line_pts' in data:
|
||||
try:
|
||||
line_pts[data['layer']] += data['line_pts']
|
||||
line_colors[data['layer']] += data['line_colors']
|
||||
mesh_tris[data['layer']] += [x + len(mesh_vertices[data['layer']])
|
||||
for x in data['mesh_tris']]
|
||||
|
||||
mesh_vertices[data['layer']] += data['mesh_vertices']
|
||||
mesh_colors[data['layer']] += data['mesh_colors']
|
||||
except Exception as e:
|
||||
print("Data error", e)
|
||||
|
||||
# Updating meshes
|
||||
for i, mesh in enumerate(self._meshes):
|
||||
if len(mesh_vertices[i]) > 0:
|
||||
set_state(polygon_offset_fill=False)
|
||||
mesh.set_data(np.asarray(mesh_vertices[i]), np.asarray(mesh_tris[i], dtype=np.uint32)
|
||||
.reshape((-1, 3)), face_colors=np.asarray(mesh_colors[i]))
|
||||
else:
|
||||
mesh.set_data()
|
||||
|
||||
mesh._bounds_changed()
|
||||
|
||||
# Updating lines
|
||||
for i, line in enumerate(self._lines):
|
||||
if len(line_pts[i]) > 0:
|
||||
line.set_data(np.asarray(line_pts[i]), np.asarray(line_colors[i]), self._line_width, 'segments')
|
||||
else:
|
||||
line.clear_data()
|
||||
|
||||
line._bounds_changed()
|
||||
|
||||
self._bounds_changed()
|
||||
|
||||
self.update_lock.release()
|
||||
|
||||
def redraw(self, indexes=None):
|
||||
"""
|
||||
Redraws collection
|
||||
:param indexes: list
|
||||
Shape indexes to get from process pool
|
||||
"""
|
||||
# Only one thread can update data
|
||||
self.results_lock.acquire(True)
|
||||
|
||||
for i in list(self.data.copy().keys()) if not indexes else indexes:
|
||||
if i in list(self.results.copy().keys()):
|
||||
try:
|
||||
self.results[i].wait() # Wait for process results
|
||||
if i in self.data:
|
||||
self.data[i] = self.results[i].get()[0] # Store translated data
|
||||
del self.results[i]
|
||||
except Exception as e:
|
||||
print(e, indexes)
|
||||
|
||||
self.results_lock.release()
|
||||
|
||||
self.__update()
|
||||
|
||||
def lock_updates(self):
|
||||
self.update_lock.acquire(True)
|
||||
|
||||
def unlock_updates(self):
|
||||
self.update_lock.release()
|
||||
|
||||
|
||||
class TextGroup(object):
|
||||
def __init__(self, collection):
|
||||
self._collection = collection
|
||||
self._index = None
|
||||
self._visible = None
|
||||
|
||||
def set(self, **kwargs):
|
||||
"""
|
||||
Adds text to collection and store index
|
||||
:param kwargs: keyword arguments
|
||||
Arguments for TextCollection.add function
|
||||
"""
|
||||
self._index = self._collection.add(**kwargs)
|
||||
|
||||
def clear(self, update=False):
|
||||
"""
|
||||
Removes text from collection, clear index
|
||||
:param update: bool
|
||||
Set True to redraw collection
|
||||
"""
|
||||
|
||||
if self._index is not None:
|
||||
self._collection.remove(self._index, False)
|
||||
self._index = None
|
||||
|
||||
if update:
|
||||
self._collection.redraw()
|
||||
|
||||
def redraw(self):
|
||||
"""
|
||||
Redraws text collection
|
||||
"""
|
||||
self._collection.redraw()
|
||||
|
||||
@property
|
||||
def visible(self):
|
||||
"""
|
||||
Visibility of group
|
||||
:return: bool
|
||||
"""
|
||||
return self._visible
|
||||
|
||||
@visible.setter
|
||||
def visible(self, value):
|
||||
"""
|
||||
Visibility of group
|
||||
:param value: bool
|
||||
"""
|
||||
self._visible = value
|
||||
self._collection.data[self._index]['visible'] = value
|
||||
|
||||
self._collection.redraw()
|
||||
|
||||
|
||||
class TextCollectionVisual(TextVisual):
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""
|
||||
Represents collection of shapes to draw on VisPy scene
|
||||
:param kwargs: keyword arguments
|
||||
Arguments to pass for TextVisual
|
||||
"""
|
||||
self.data = {}
|
||||
self.last_key = -1
|
||||
self.lock = threading.Lock()
|
||||
|
||||
super(TextCollectionVisual, self).__init__(**kwargs)
|
||||
|
||||
self.freeze()
|
||||
|
||||
def add(self, text, pos, visible=True, update=True):
|
||||
"""
|
||||
Adds array of text to collection
|
||||
:param text: list
|
||||
Array of strings ['str1', 'str2', ... ]
|
||||
:param pos: list
|
||||
Array of string positions [(0, 0), (10, 10), ... ]
|
||||
:param update: bool
|
||||
Set True to redraw collection
|
||||
:return: int
|
||||
Index of array
|
||||
"""
|
||||
# Get new key
|
||||
self.lock.acquire(True)
|
||||
self.last_key += 1
|
||||
key = self.last_key
|
||||
self.lock.release()
|
||||
|
||||
# Prepare data for translation
|
||||
self.data[key] = {'text': text, 'pos': pos, 'visible': visible}
|
||||
|
||||
if update:
|
||||
self.redraw()
|
||||
|
||||
return key
|
||||
|
||||
def remove(self, key, update=False):
|
||||
"""
|
||||
Removes shape from collection
|
||||
:param key: int
|
||||
Shape index to remove
|
||||
:param update:
|
||||
Set True to redraw collection
|
||||
"""
|
||||
del self.data[key]
|
||||
|
||||
if update:
|
||||
self.__update()
|
||||
|
||||
def clear(self, update=False):
|
||||
"""
|
||||
Removes all shapes from colleciton
|
||||
:param update: bool
|
||||
Set True to redraw collection
|
||||
"""
|
||||
self.data.clear()
|
||||
if update:
|
||||
self.__update()
|
||||
|
||||
def __update(self):
|
||||
"""
|
||||
Merges internal buffers, sets data to visuals, redraws collection on scene
|
||||
"""
|
||||
labels = []
|
||||
pos = []
|
||||
|
||||
# Merge buffers
|
||||
for data in list(self.data.values()):
|
||||
if data['visible']:
|
||||
try:
|
||||
labels += data['text']
|
||||
pos += data['pos']
|
||||
except Exception as e:
|
||||
print("Data error", e)
|
||||
|
||||
# Updating text
|
||||
if len(labels) > 0:
|
||||
self.text = labels
|
||||
self.pos = pos
|
||||
else:
|
||||
self.text = None
|
||||
self.pos = (0, 0)
|
||||
|
||||
self._bounds_changed()
|
||||
|
||||
def redraw(self):
|
||||
"""
|
||||
Redraws collection
|
||||
"""
|
||||
self.__update()
|
||||
|
||||
|
||||
# Add 'enabled' property to visual nodes
|
||||
def create_fast_node(subclass):
|
||||
# Create a new subclass of Node.
|
||||
|
||||
# Decide on new class name
|
||||
clsname = subclass.__name__
|
||||
if not (clsname.endswith('Visual') and
|
||||
issubclass(subclass, visuals.BaseVisual)):
|
||||
raise RuntimeError('Class "%s" must end with Visual, and must '
|
||||
'subclass BaseVisual' % clsname)
|
||||
clsname = clsname[:-6]
|
||||
|
||||
# Generate new docstring based on visual docstring
|
||||
try:
|
||||
doc = generate_docstring(subclass, clsname)
|
||||
except Exception:
|
||||
# If parsing fails, just return the original Visual docstring
|
||||
doc = subclass.__doc__
|
||||
|
||||
# New __init__ method
|
||||
def __init__(self, *args, **kwargs):
|
||||
parent = kwargs.pop('parent', None)
|
||||
name = kwargs.pop('name', None)
|
||||
self.name = name # to allow __str__ before Node.__init__
|
||||
self._visual_superclass = subclass
|
||||
|
||||
# parent: property,
|
||||
# _parent: attribute of Node class
|
||||
# __parent: attribute of fast_node class
|
||||
self.__parent = parent
|
||||
self._enabled = False
|
||||
|
||||
subclass.__init__(self, *args, **kwargs)
|
||||
self.unfreeze()
|
||||
VisualNode.__init__(self, parent=parent, name=name)
|
||||
self.freeze()
|
||||
|
||||
# Create new class
|
||||
cls = type(clsname, (VisualNode, subclass),
|
||||
{'__init__': __init__, '__doc__': doc})
|
||||
|
||||
# 'Enabled' property clears/restores 'parent' property of Node class
|
||||
# Scene will be painted quicker than when using 'visible' property
|
||||
def get_enabled(self):
|
||||
return self._enabled
|
||||
|
||||
def set_enabled(self, enabled):
|
||||
if enabled:
|
||||
self.parent = self.__parent # Restore parent
|
||||
else:
|
||||
if self.parent: # Store parent
|
||||
self.__parent = self.parent
|
||||
self.parent = None
|
||||
|
||||
cls.enabled = property(get_enabled, set_enabled)
|
||||
|
||||
return cls
|
||||
|
||||
|
||||
ShapeCollection = create_fast_node(ShapeCollectionVisual)
|
||||
TextCollection = create_fast_node(TextCollectionVisual)
|
||||
Cursor = create_fast_node(MarkersVisual)
|
||||
0
flatcamGUI/__init__.py
Normal file
0
flatcamGUI/__init__.py
Normal file
Reference in New Issue
Block a user