- added an experimental 3D area (archball camera)
This commit is contained in:
451
appGUI/PlotCanvas3d.py
Normal file
451
appGUI/PlotCanvas3d.py
Normal file
@@ -0,0 +1,451 @@
|
||||
# ##########################################################
|
||||
# FlatCAM: 2D Post-processing for Manufacturing #
|
||||
# Author: Dennis Hayrullin (c) #
|
||||
# Date: 2016 #
|
||||
# MIT Licence #
|
||||
# ##########################################################
|
||||
|
||||
from PyQt5 import QtCore
|
||||
from PyQt5.QtGui import QPalette
|
||||
# from PyQt5.QtCore import QSettings
|
||||
|
||||
import time
|
||||
|
||||
import vispy.scene as scene
|
||||
from vispy.scene.cameras.base_camera import BaseCamera
|
||||
from vispy.color import Color
|
||||
from appGUI.VisPyVisuals import ShapeGroup, ShapeCollection, TextCollection, TextGroup, Cursor
|
||||
from vispy.scene.visuals import InfiniteLine, Line, Rectangle, Text, XYZAxis
|
||||
|
||||
import gettext
|
||||
import appTranslation as fcTranslate
|
||||
import builtins
|
||||
|
||||
import numpy as np
|
||||
from vispy.geometry import Rect
|
||||
|
||||
fcTranslate.apply_language('strings')
|
||||
if '_' not in builtins.__dict__:
|
||||
_ = gettext.gettext
|
||||
|
||||
white = Color("#ffffff")
|
||||
black = Color("#000000")
|
||||
|
||||
|
||||
class PlotCanvas3d(QtCore.QObject, scene.SceneCanvas):
|
||||
"""
|
||||
Class handling the plotting area in the application.
|
||||
"""
|
||||
|
||||
def __init__(self, container, fcapp):
|
||||
"""
|
||||
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__()
|
||||
# QtCore.QObject.__init__(self)
|
||||
# VisPyCanvas.__init__(self)
|
||||
super().__init__()
|
||||
|
||||
# VisPyCanvas does not allow new attributes. Override.
|
||||
self.unfreeze()
|
||||
|
||||
self.fcapp = fcapp
|
||||
|
||||
# Parent container
|
||||
self.container = container
|
||||
|
||||
settings = QtCore.QSettings("Open Source", "FlatCAM")
|
||||
if settings.contains("theme"):
|
||||
theme = settings.value('theme', type=str)
|
||||
else:
|
||||
theme = 'white'
|
||||
|
||||
if settings.contains("axis_font_size"):
|
||||
a_fsize = settings.value('axis_font_size', type=int)
|
||||
else:
|
||||
a_fsize = 8
|
||||
|
||||
if theme == 'white':
|
||||
theme_color = Color('#FFFFFF')
|
||||
tick_color = Color('#000000')
|
||||
back_color = str(QPalette().color(QPalette.Window).name())
|
||||
else:
|
||||
theme_color = Color('#000000')
|
||||
tick_color = Color('gray')
|
||||
back_color = Color('#000000')
|
||||
|
||||
self.central_widget.bgcolor = back_color
|
||||
self.central_widget.border_color = back_color
|
||||
|
||||
self.grid_widget = self.central_widget.add_grid()
|
||||
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=tick_color, text_color=tick_color, font_size=a_fsize, axis_width=1
|
||||
# )
|
||||
# self.yaxis.width_max = 55
|
||||
# self.grid_widget.add_widget(self.yaxis, row=1, col=0)
|
||||
#
|
||||
# self.xaxis = scene.AxisWidget(
|
||||
# orientation='bottom', axis_color=tick_color, text_color=tick_color, font_size=a_fsize, axis_width=1,
|
||||
# anchors=['center', 'bottom']
|
||||
# )
|
||||
# self.xaxis.height_max = 30
|
||||
# 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
|
||||
|
||||
self.view = self.grid_widget.add_view(row=1, col=1, border_color=tick_color, bgcolor=theme_color)
|
||||
# self.view.camera = Camera_3D(aspect=1, rect=(-25, -25, 150, 150))
|
||||
self.view.camera = Camera_3D()
|
||||
|
||||
xax = scene.Axis(pos=[[0, 0], [0, 500]], tick_direction=(0, 1), domain=(0, 500),
|
||||
font_size=16, axis_color='k', tick_color='k', text_color='k',
|
||||
parent=self.view.scene)
|
||||
xax.transform = scene.STTransform(translate=(0, 0, -0.2))
|
||||
|
||||
yax = scene.Axis(pos=[[0, 0], [500, 0]], tick_direction=(1, 0), domain=(0, 500),
|
||||
font_size=16, axis_color='k', tick_color='k', text_color='k',
|
||||
parent=self.view.scene)
|
||||
yax.transform = scene.STTransform(translate=(0, 0, -0.2))
|
||||
|
||||
self.xyz_axis = XYZAxis(parent=self.view.scene)
|
||||
|
||||
# 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(self.view)
|
||||
# self.yaxis.link_view(self.view)
|
||||
|
||||
# if theme == 'white':
|
||||
# self.grid = scene.GridLines(parent=self.view.scene, color='dimgray')
|
||||
# else:
|
||||
# self.grid = scene.GridLines(parent=self.view.scene, color='#dededeff')
|
||||
#
|
||||
# self.grid.set_gl_state(depth_test=False)
|
||||
|
||||
# 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
|
||||
self.workspace_line = None
|
||||
|
||||
# <VisPyCanvas>
|
||||
self.create_native()
|
||||
self.native.setParent(self.fcapp.ui)
|
||||
|
||||
# <QtCore.QObject>
|
||||
self.container.addWidget(self.native)
|
||||
|
||||
self.line_parent = None
|
||||
if self.fcapp.defaults["global_cursor_color_enabled"]:
|
||||
c_color = Color(self.fcapp.defaults["global_cursor_color"]).rgba
|
||||
else:
|
||||
c_color = self.line_color
|
||||
|
||||
# font size
|
||||
qsettings = QtCore.QSettings("Open Source", "FlatCAM")
|
||||
if qsettings.contains("hud_font_size"):
|
||||
fsize = qsettings.value('hud_font_size', type=int)
|
||||
else:
|
||||
fsize = 8
|
||||
|
||||
# units
|
||||
# units = self.fcapp.defaults["units"].upper()
|
||||
|
||||
# coordinates and anchors
|
||||
height = fsize * 11 # 90. Constant 11 is something that works
|
||||
width = height * 2 # width is double the height = it is something that works
|
||||
# center_x = (width / 2) + 5
|
||||
# center_y = (height / 2) + 5
|
||||
|
||||
# enable Grid lines
|
||||
self.grid_lines_enabled = True
|
||||
|
||||
self.shape_collections = []
|
||||
|
||||
self.shape_collection = self.new_shape_collection()
|
||||
self.fcapp.pool_recreated.connect(self.on_pool_recreated)
|
||||
|
||||
self.text_collection = self.new_text_collection()
|
||||
self.text_collection.enabled = True
|
||||
|
||||
self.c = None
|
||||
|
||||
# Keep VisPy canvas happy by letting it be "frozen" again.
|
||||
self.freeze()
|
||||
self.fit_view()
|
||||
|
||||
# self.graph_event_connect('mouse_wheel', self.on_mouse_scroll)
|
||||
|
||||
def graph_event_connect(self, event_name, callback):
|
||||
return getattr(self.events, event_name).connect(callback)
|
||||
|
||||
def graph_event_disconnect(self, event_name, callback=None):
|
||||
if callback is None:
|
||||
getattr(self.events, event_name).disconnect()
|
||||
else:
|
||||
getattr(self.events, event_name).disconnect(callback)
|
||||
|
||||
def translate_coords(self, pos):
|
||||
"""
|
||||
Translate pixels to FlatCAM units.
|
||||
|
||||
"""
|
||||
tr = self.grid.get_transform('canvas', 'visual')
|
||||
return tr.map(pos)
|
||||
|
||||
def translate_coords_2(self, pos):
|
||||
"""
|
||||
Translate FlatCAM units to pixels.
|
||||
"""
|
||||
tr = self.grid.get_transform('visual', 'document')
|
||||
return tr.map(pos)
|
||||
|
||||
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.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.view.scene, pool=self.app.pool, **kwargs)
|
||||
# self.shape_collections.append(sc)
|
||||
# return sc
|
||||
return ShapeCollection(parent=self.view.scene, pool=self.fcapp.pool, **kwargs)
|
||||
|
||||
def new_cursor(self):
|
||||
"""
|
||||
Will create a mouse cursor pointer on canvas
|
||||
|
||||
:return: the mouse cursor object
|
||||
"""
|
||||
self.c = Cursor(pos=np.empty((0, 2)), parent=self.view.scene)
|
||||
self.c.antialias = 0
|
||||
return self.c
|
||||
|
||||
def on_mouse_scroll(self, event):
|
||||
# key modifiers
|
||||
modifiers = event.modifiers
|
||||
|
||||
pan_delta_x = self.fcapp.defaults["global_gridx"]
|
||||
pan_delta_y = self.fcapp.defaults["global_gridy"]
|
||||
curr_pos = event.pos
|
||||
|
||||
# Controlled pan by mouse wheel
|
||||
if 'Shift' in modifiers:
|
||||
p1 = np.array(curr_pos)[:2]
|
||||
|
||||
if event.delta[1] > 0:
|
||||
curr_pos[0] -= pan_delta_x
|
||||
else:
|
||||
curr_pos[0] += pan_delta_x
|
||||
p2 = np.array(curr_pos)[:2]
|
||||
self.view.camera.pan(p2 - p1)
|
||||
elif 'Control' in modifiers:
|
||||
p1 = np.array(curr_pos)[:2]
|
||||
|
||||
if event.delta[1] > 0:
|
||||
curr_pos[1] += pan_delta_y
|
||||
else:
|
||||
curr_pos[1] -= pan_delta_y
|
||||
p2 = np.array(curr_pos)[:2]
|
||||
self.view.camera.pan(p2 - p1)
|
||||
|
||||
# if self.fcapp.grid_status():
|
||||
# pos_canvas = self.translate_coords(curr_pos)
|
||||
# pos = self.fcapp.geo_editor.snap(pos_canvas[0], pos_canvas[1])
|
||||
#
|
||||
# # Update cursor
|
||||
# self.fcapp.app_cursor.set_data(np.asarray([(pos[0], pos[1])]),
|
||||
# symbol='++', edge_color=self.fcapp.cursor_color_3D,
|
||||
# edge_width=self.fcapp.defaults["global_cursor_width"],
|
||||
# size=self.fcapp.defaults["global_cursor_size"])
|
||||
|
||||
def new_text_group(self, collection=None):
|
||||
if collection:
|
||||
return TextGroup(collection)
|
||||
else:
|
||||
return TextGroup(self.text_collection)
|
||||
|
||||
def new_text_collection(self, **kwargs):
|
||||
return TextCollection(parent=self.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 collection can be seen clearly
|
||||
# otherwise the shape collection boundary will have no border
|
||||
dx = rect.right - rect.left
|
||||
dy = rect.top - rect.bottom
|
||||
x_factor = dx * 0.02
|
||||
y_factor = dy * 0.02
|
||||
|
||||
rect.left -= x_factor
|
||||
rect.bottom -= y_factor
|
||||
rect.right += x_factor
|
||||
rect.top += y_factor
|
||||
|
||||
self.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.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
|
||||
|
||||
|
||||
class Camera_3D(scene.ArcballCamera):
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(Camera_3D, 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_3D, 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
|
||||
#
|
||||
# # key modifiers
|
||||
# modifiers = event.mouse_event.modifiers
|
||||
#
|
||||
# # 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':
|
||||
# if not modifiers:
|
||||
# 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
|
||||
#
|
||||
# # ################ Panning ############################
|
||||
# # 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)
|
||||
Reference in New Issue
Block a user