Coverage for tatlin/lib/gl/scene.py: 100%
167 statements
« prev ^ index » next coverage.py v7.4.4, created at 2024-03-20 05:56 +0000
« prev ^ index » next coverage.py v7.4.4, created at 2024-03-20 05:56 +0000
1# -*- coding: utf-8 -*-
2# Copyright (C) 2011 Denis Kobozev
3#
4# This program is free software; you can redistribute it and/or modify
5# it under the terms of the GNU General Public License as published by
6# the Free Software Foundation; either version 2 of the License, or
7# (at your option) any later version.
8#
9# This program is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU General Public License for more details.
13#
14# You should have received a copy of the GNU General Public License
15# along with this program; if not, write to the Free Software Foundation,
16# Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
19import math
20from typing import Any
21import logging
23from OpenGL.GL import * # type:ignore
24from OpenGL.GLU import * # type:ignore
25from OpenGL.GLUT import * # type:ignore
27from tatlin.lib.ui.basescene import BaseScene
29from .model import Model
30from .views import View2D, View3D
31from .util import html_color
34class Scene(BaseScene):
35 """
36 A scene is responsible for displaying a model and accompanying objects (actors).
38 In addition to calling display functions on its actors, the scene is also
39 responsible for viewing transformations such as zooming, panning and
40 rotation, as well as being the interface for the actors.
41 """
43 PAN_SPEED = 25
44 ROTATE_SPEED = 25
46 def __init__(self, parent):
47 super(Scene, self).__init__(parent)
49 self.model: Any = None
50 self.actors = []
51 self.cursor_x = 0
52 self.cursor_y = 0
54 self.view_ortho = View2D()
55 self.view_perspective = View3D()
56 self.current_view = self.view_perspective
58 def add_model(self, model):
59 self.model = model
60 self.actors.append(self.model)
62 def add_supporting_actor(self, actor):
63 self.actors.append(actor)
65 def clear(self):
66 self.actors = []
68 # ------------------------------------------------------------------------
69 # DRAWING
70 # ------------------------------------------------------------------------
72 def init(self):
73 glClearColor(0.0, 0.0, 0.0, 0.0) # set clear color to black
74 glClearDepth(1.0) # set depth value to 1
75 glDepthFunc(GL_LEQUAL)
77 glEnable(GL_COLOR_MATERIAL)
78 glEnable(GL_DEPTH_TEST)
79 glEnable(GL_CULL_FACE)
80 # simulate translucency by blending colors
81 glEnable(GL_BLEND)
82 glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
84 self.init_actors()
86 self.initialized = True
88 def init_actors(self):
89 for actor in self.actors:
90 if not actor.initialized:
91 actor.init()
93 def display(self, w, h):
94 # clear the color and depth buffers from any leftover junk
95 glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) # type:ignore
97 # discard back-facing polygons
98 glCullFace(GL_BACK)
100 # fix normals after scaling to prevent problems with lighting
101 # see: http://www.opengl.org/resources/faq/technical/lights.htm#ligh0090
102 glEnable(GL_RESCALE_NORMAL)
104 glutInit()
106 self.view_ortho.begin(w, h)
107 self.draw_axes()
108 self.view_ortho.end()
110 self.current_view.begin(w, h)
111 self.current_view.display_transform()
113 if self.mode_ortho:
114 for actor in self.actors:
115 actor.display(
116 elevation=-self.current_view.elevation,
117 mode_ortho=self.mode_ortho,
118 mode_2d=self.mode_2d,
119 )
120 else:
121 # actors may use eye height to perform rendering optimizations; in
122 # the simplest terms, in the most convenient definitions, eye
123 # height in the perspective projection divides the screen into two
124 # horizontal halves - one seen from above, the other from below
125 y = self.current_view.y / self.current_view.zoom_factor
126 z = self.current_view.z
127 angle = -math.degrees(math.atan2(z, y)) - self.current_view.elevation
128 eye_height = math.sqrt(y**2 + z**2) * math.sin(math.radians(angle))
130 # draw line of sight plane
131 """
132 #plane_size = 200
133 #glBegin(GL_LINES)
134 #glColor(1.0, 0.0, 0.0)
135 #glVertex(-plane_size/2, plane_size/2, eye_height)
136 #glVertex(plane_size/2, plane_size/2, eye_height)
138 #glVertex(plane_size/2, plane_size/2, eye_height)
139 #glVertex(plane_size/2, -plane_size/2, eye_height)
141 #glVertex(plane_size/2, -plane_size/2, eye_height)
142 #glVertex(-plane_size/2, -plane_size/2, eye_height)
144 #glVertex(-plane_size/2, -plane_size/2, eye_height)
145 #glVertex(-plane_size/2, plane_size/2, eye_height)
146 #glEnd()
147 """
149 for actor in self.actors:
150 actor.display(
151 eye_height=eye_height,
152 mode_ortho=self.mode_ortho,
153 mode_2d=self.mode_2d,
154 )
156 self.current_view.end()
158 def reshape(self, w, h):
159 glViewport(0, 0, w, h)
161 def draw_axes(self, length=50.0):
162 glPushMatrix()
163 self.current_view.ui_transform(length)
165 axes = [
166 (-length, 0.0, 0.0),
167 (0.0, -length, 0.0),
168 (0.0, 0.0, length),
169 ]
170 colors = [(1.0, 0.0, 0.0), (0.0, 1.0, 0.0), html_color("008aff")]
171 labels = ["x", "y", "z"]
173 glBegin(GL_LINES)
175 for axis, color in zip(axes, colors):
176 glColor(*color)
177 glVertex(0.0, 0.0, 0.0)
178 glVertex(*axis)
180 glEnd()
182 # draw axis labels
183 for label, axis, color in zip(labels, axes, colors):
184 glColor(*color)
185 # add padding to labels
186 glRasterPos(axis[0] + 2, axis[1] + 2, axis[2] + 2)
187 glutBitmapCharacter(GLUT_BITMAP_8_BY_13, ord(label)) # type:ignore
189 glPopMatrix()
191 # ------------------------------------------------------------------------
192 # VIEWING MANIPULATIONS
193 # ------------------------------------------------------------------------
195 def button_press(self, x, y):
196 self.cursor_x = x
197 self.cursor_y = y
199 def button_motion(self, x, y, left, middle, right):
200 delta_x = x - self.cursor_x
201 delta_y = y - self.cursor_y
203 if left:
204 self.current_view.rotate(
205 delta_x * self.ROTATE_SPEED / 100, delta_y * self.ROTATE_SPEED / 100
206 )
207 elif middle:
208 if hasattr(self.current_view, "offset"):
209 self.current_view.offset( # type:ignore
210 delta_x * self.PAN_SPEED / 100, delta_y * self.PAN_SPEED / 100
211 )
212 elif right:
213 self.current_view.pan(
214 delta_x * self.PAN_SPEED / 100, delta_y * self.PAN_SPEED / 100
215 )
217 self.cursor_x = x
218 self.cursor_y = y
220 self.invalidate()
222 def wheel_scroll(self, direction):
223 delta_y = 30.0
224 if direction < 0:
225 direction = -1
226 else:
227 direction = 1
228 delta_y = direction * delta_y
230 self.current_view.zoom(0, delta_y)
231 self.invalidate()
233 def reset_view(self, both=False):
234 if both:
235 self.view_ortho.reset_state()
236 self.view_perspective.reset_state()
237 else:
238 self.current_view.reset_state()
240 @property
241 def mode_2d(self):
242 return isinstance(self.current_view, View2D)
244 @mode_2d.setter
245 def mode_2d(self, value):
246 self.current_view = self.view_ortho if value else self.view_perspective
248 @property
249 def mode_ortho(self):
250 return (
251 self.current_view.supports_ortho and self.current_view.ortho # type:ignore
252 )
254 @mode_ortho.setter
255 def mode_ortho(self, value):
256 if self.current_view.supports_ortho:
257 self.current_view.ortho = value # type:ignore
259 def rotate_view(self, azimuth, elevation):
260 if not self.mode_2d:
261 self.current_view.azimuth = azimuth
262 self.current_view.elevation = elevation
263 self.invalidate()
265 def view_model_center(self):
266 """
267 Display the model in the center of the scene without modifying the vertices.
268 """
269 bounding_box = self.model.bounding_box
270 lower_corner = bounding_box.lower_corner
271 upper_corner = bounding_box.upper_corner
272 self.model.offset_x = -(upper_corner[0] + lower_corner[0]) / 2
273 self.model.offset_y = -(upper_corner[1] + lower_corner[1]) / 2
274 self.model.offset_z = -lower_corner[2]
276 # ------------------------------------------------------------------------
277 # MODEL MANIPULATION
278 # ------------------------------------------------------------------------
280 def change_num_layers(self, number):
281 """
282 Change number of visible layers for Gcode model.
283 """
284 self.model.num_layers_to_draw = number
286 def scale_model(self, factor):
287 logging.info("--- scaling model by factor of:", factor)
288 self.model.scale(factor)
289 self.model.init()
291 def center_model(self):
292 """
293 Center the model on platform and raise its lowest point to z=0.
294 """
295 bounding_box = self.model.bounding_box
296 lower_corner = bounding_box.lower_corner
297 upper_corner = bounding_box.upper_corner
298 offset_x = -(upper_corner[0] + lower_corner[0]) / 2
299 offset_y = -(upper_corner[1] + lower_corner[1]) / 2
300 offset_z = -lower_corner[2]
301 self.model.translate(offset_x, offset_y, offset_z)
302 self.model.init()
304 def change_model_dimension(self, dimension, value):
305 current_value = getattr(self.model, dimension)
306 # since our scaling is absolute, we have to take current scaling factor
307 # into account
308 factor = (value / current_value) * self.model.scaling_factor
309 self.scale_model(factor)
311 def rotate_model(self, angle, axis_name):
312 axis = Model.letter_axis_map[axis_name]
313 self.model.rotate_abs(angle, axis)
314 self.model.init()
316 def show_arrows(self, show):
317 self.model.arrows_enabled = show
318 self.model.init()
320 @property
321 def model_modified(self):
322 """
323 Return true when an important model property has been modified.
325 Important properties exclude viewing transformations and can be
326 something like size, shape or color.
327 """
328 return self.model and self.model.modified