Files
flatcam-wsl/appGUI/PlotCanvas3d.py
2023-04-08 12:43:34 +03:00

526 lines
18 KiB
Python

# ##########################################################
# FlatCAM: 2D Post-processing for Manufacturing #
# Author: Dennis Hayrullin (c) #
# Date: 2016 #
# MIT Licence #
# ##########################################################
from PyQt6 import QtCore
from PyQt6.QtGui import QPalette
# from PyQt6.QtCore import QSettings
import time
import vispy.scene as scene
from vispy.scene.cameras.base_camera import BaseCamera
from vispy.scene.cameras.perspective import PerspectiveCamera
from vispy.util import keys
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_EVO")
if settings.contains("theme"):
theme = settings.value('theme', type=str)
else:
theme = 'default'
if settings.contains("dark_canvas"):
dark_canvas = settings.value('dark_canvas', type=bool)
else:
dark_canvas = False
if settings.contains("axis_font_size"):
a_fsize = settings.value('axis_font_size', type=int)
else:
a_fsize = 8
if (theme == 'default' or theme == 'light') and not dark_canvas:
theme_color = Color('#FFFFFF')
tick_color = Color('#000000')
back_color = str(QPalette().color(QPalette.ColorRole.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 == 'light':
# 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.options["global_cursor_color_enabled"]:
c_color = Color(self.fcapp.options["global_cursor_color"]).rgba
else:
c_color = self.line_color
# font size
qsettings = QtCore.QSettings("Open Source", "FlatCAM_EVO")
if qsettings.contains("hud_font_size"):
fsize = qsettings.value('hud_font_size', type=int)
else:
fsize = 8
# units
# units = self.fcapp.app_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.options["global_gridx"]
pan_delta_y = self.fcapp.options["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.options["global_cursor_width"],
# size=self.fcapp.options["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):
pass
# 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 viewbox_mouse_event(self, event):
"""
The viewbox received a mouse event; update transform
accordingly.
Parameters
----------
event : instance of Event
The event.
"""
if event.handled or not self.interactive:
return
PerspectiveCamera.viewbox_mouse_event(self, event)
if event.type == 'mouse_release':
self._event_value = None # Reset
elif event.type == 'mouse_press':
event.handled = True
elif event.type == 'mouse_move':
if event.press_event is None:
return
modifiers = event.mouse_event.modifiers
p1 = event.mouse_event.press_event.pos
p2 = event.mouse_event.pos
d = p2 - p1
if 1 in event.buttons and not modifiers:
# Rotate
self._update_rotation(event)
elif 1 in event.buttons and keys.SHIFT in modifiers:
# Zoom
if self._event_value is None:
self._event_value = (self._scale_factor, self._distance)
zoomy = (1 + self.zoom_factor) ** d[1]
self.scale_factor = self._event_value[0] * zoomy
# Modify distance if its given
if self._distance is not None:
self._distance = self._event_value[1] * zoomy
self.view_changed()
elif 2 in event.buttons and not modifiers:
# Translate
norm = np.mean(self._viewbox.size)
if self._event_value is None or len(self._event_value) == 2:
self._event_value = self.center
dist = (p1 - p2) / norm * self._scale_factor
dist[1] *= -1
# Black magic part 1: turn 2D into 3D translations
dx, dy, dz = self._dist_to_trans(dist)
# Black magic part 2: take up-vector and flipping into account
ff = self._flip_factors
up, forward, right = self._get_dim_vectors()
dx, dy, dz = right * dx + forward * dy + up * dz
dx, dy, dz = ff[0] * dx, ff[1] * dy, dz * ff[2]
c = self._event_value
self.center = c[0] + dx, c[1] + dy, c[2] + dz
elif 2 in event.buttons and keys.SHIFT in modifiers:
# Change fov
if self._event_value is None:
self._event_value = self._fov
fov = self._event_value - d[1] / 5.0
self.fov = min(180.0, max(0.0, fov))
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)