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

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 

17 

18 

19import math 

20from typing import Any 

21import logging 

22 

23from OpenGL.GL import * # type:ignore 

24from OpenGL.GLU import * # type:ignore 

25from OpenGL.GLUT import * # type:ignore 

26 

27from tatlin.lib.ui.basescene import BaseScene 

28 

29from .model import Model 

30from .views import View2D, View3D 

31from .util import html_color 

32 

33 

34class Scene(BaseScene): 

35 """ 

36 A scene is responsible for displaying a model and accompanying objects (actors). 

37 

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 """ 

42 

43 PAN_SPEED = 25 

44 ROTATE_SPEED = 25 

45 

46 def __init__(self, parent): 

47 super(Scene, self).__init__(parent) 

48 

49 self.model: Any = None 

50 self.actors = [] 

51 self.cursor_x = 0 

52 self.cursor_y = 0 

53 

54 self.view_ortho = View2D() 

55 self.view_perspective = View3D() 

56 self.current_view = self.view_perspective 

57 

58 def add_model(self, model): 

59 self.model = model 

60 self.actors.append(self.model) 

61 

62 def add_supporting_actor(self, actor): 

63 self.actors.append(actor) 

64 

65 def clear(self): 

66 self.actors = [] 

67 

68 # ------------------------------------------------------------------------ 

69 # DRAWING 

70 # ------------------------------------------------------------------------ 

71 

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) 

76 

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) 

83 

84 self.init_actors() 

85 

86 self.initialized = True 

87 

88 def init_actors(self): 

89 for actor in self.actors: 

90 if not actor.initialized: 

91 actor.init() 

92 

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 

96 

97 # discard back-facing polygons 

98 glCullFace(GL_BACK) 

99 

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) 

103 

104 glutInit() 

105 

106 self.view_ortho.begin(w, h) 

107 self.draw_axes() 

108 self.view_ortho.end() 

109 

110 self.current_view.begin(w, h) 

111 self.current_view.display_transform() 

112 

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)) 

129 

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) 

137 

138 #glVertex(plane_size/2, plane_size/2, eye_height) 

139 #glVertex(plane_size/2, -plane_size/2, eye_height) 

140 

141 #glVertex(plane_size/2, -plane_size/2, eye_height) 

142 #glVertex(-plane_size/2, -plane_size/2, eye_height) 

143 

144 #glVertex(-plane_size/2, -plane_size/2, eye_height) 

145 #glVertex(-plane_size/2, plane_size/2, eye_height) 

146 #glEnd() 

147 """ 

148 

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 ) 

155 

156 self.current_view.end() 

157 

158 def reshape(self, w, h): 

159 glViewport(0, 0, w, h) 

160 

161 def draw_axes(self, length=50.0): 

162 glPushMatrix() 

163 self.current_view.ui_transform(length) 

164 

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"] 

172 

173 glBegin(GL_LINES) 

174 

175 for axis, color in zip(axes, colors): 

176 glColor(*color) 

177 glVertex(0.0, 0.0, 0.0) 

178 glVertex(*axis) 

179 

180 glEnd() 

181 

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 

188 

189 glPopMatrix() 

190 

191 # ------------------------------------------------------------------------ 

192 # VIEWING MANIPULATIONS 

193 # ------------------------------------------------------------------------ 

194 

195 def button_press(self, x, y): 

196 self.cursor_x = x 

197 self.cursor_y = y 

198 

199 def button_motion(self, x, y, left, middle, right): 

200 delta_x = x - self.cursor_x 

201 delta_y = y - self.cursor_y 

202 

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 ) 

216 

217 self.cursor_x = x 

218 self.cursor_y = y 

219 

220 self.invalidate() 

221 

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 

229 

230 self.current_view.zoom(0, delta_y) 

231 self.invalidate() 

232 

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() 

239 

240 @property 

241 def mode_2d(self): 

242 return isinstance(self.current_view, View2D) 

243 

244 @mode_2d.setter 

245 def mode_2d(self, value): 

246 self.current_view = self.view_ortho if value else self.view_perspective 

247 

248 @property 

249 def mode_ortho(self): 

250 return ( 

251 self.current_view.supports_ortho and self.current_view.ortho # type:ignore 

252 ) 

253 

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 

258 

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() 

264 

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] 

275 

276 # ------------------------------------------------------------------------ 

277 # MODEL MANIPULATION 

278 # ------------------------------------------------------------------------ 

279 

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 

285 

286 def scale_model(self, factor): 

287 logging.info("--- scaling model by factor of:", factor) 

288 self.model.scale(factor) 

289 self.model.init() 

290 

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() 

303 

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) 

310 

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() 

315 

316 def show_arrows(self, show): 

317 self.model.arrows_enabled = show 

318 self.model.init() 

319 

320 @property 

321 def model_modified(self): 

322 """ 

323 Return true when an important model property has been modified. 

324 

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