OpenGLContext 3.0.0a1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (417) hide show
  1. OpenGLContext/__init__.py +151 -0
  2. OpenGLContext/arrays.py +2 -0
  3. OpenGLContext/atlas.py +300 -0
  4. OpenGLContext/audio/__init__.py +32 -0
  5. OpenGLContext/audio/scene.py +130 -0
  6. OpenGLContext/audio/settings.py +82 -0
  7. OpenGLContext/bin/__init__.py +0 -0
  8. OpenGLContext/bin/choosecontext.py +224 -0
  9. OpenGLContext/bin/choosefonts.py +209 -0
  10. OpenGLContext/bin/gltest.py +121 -0
  11. OpenGLContext/bin/gltf_demo.py +492 -0
  12. OpenGLContext/bin/gltf_regression.py +645 -0
  13. OpenGLContext/bin/gltf_view.py +30 -0
  14. OpenGLContext/bin/keyboardevents.py +93 -0
  15. OpenGLContext/bin/lorentz.py +109 -0
  16. OpenGLContext/bin/physics_cook.py +81 -0
  17. OpenGLContext/bin/profile_view.py +38 -0
  18. OpenGLContext/bin/terrain_view.py +581 -0
  19. OpenGLContext/bin/tiles_view.py +39 -0
  20. OpenGLContext/bin/ui_demo.py +228 -0
  21. OpenGLContext/bin/view.py +341 -0
  22. OpenGLContext/bin/vrml_view.py +42 -0
  23. OpenGLContext/capture.py +125 -0
  24. OpenGLContext/context.py +1253 -0
  25. OpenGLContext/contextconfig.py +177 -0
  26. OpenGLContext/contextdefinition.py +275 -0
  27. OpenGLContext/debug/__init__.py +6 -0
  28. OpenGLContext/debug/bufferimage.py +80 -0
  29. OpenGLContext/debug/leaks.py +13 -0
  30. OpenGLContext/debug/logcontext.py +9 -0
  31. OpenGLContext/debug/logs.py +15 -0
  32. OpenGLContext/debug/state.py +329 -0
  33. OpenGLContext/displaylist.py +69 -0
  34. OpenGLContext/doinchildmatrix.py +34 -0
  35. OpenGLContext/drawcube.py +58 -0
  36. OpenGLContext/events/__init__.py +10 -0
  37. OpenGLContext/events/default_config.py +29 -0
  38. OpenGLContext/events/event.py +65 -0
  39. OpenGLContext/events/eventhandlermixin.py +214 -0
  40. OpenGLContext/events/eventmanager.py +269 -0
  41. OpenGLContext/events/fxevents.py +90 -0
  42. OpenGLContext/events/glfwevents.py +424 -0
  43. OpenGLContext/events/glutevents.py +184 -0
  44. OpenGLContext/events/inputstate.py +129 -0
  45. OpenGLContext/events/internaltime.py +161 -0
  46. OpenGLContext/events/keyboardevents.py +153 -0
  47. OpenGLContext/events/mouseevents.py +473 -0
  48. OpenGLContext/events/pygameevents.py +162 -0
  49. OpenGLContext/events/systemtime.py +12 -0
  50. OpenGLContext/events/timeeventgeneratormanager.py +40 -0
  51. OpenGLContext/events/timeevents.py +143 -0
  52. OpenGLContext/events/timer.py +170 -0
  53. OpenGLContext/events/tkevents.py +172 -0
  54. OpenGLContext/events/wxevents.py +185 -0
  55. OpenGLContext/extensionmanager.py +158 -0
  56. OpenGLContext/framecounter.py +74 -0
  57. OpenGLContext/frustum.py +135 -0
  58. OpenGLContext/glfwcontext.py +395 -0
  59. OpenGLContext/glfwinteractivecontext.py +34 -0
  60. OpenGLContext/glfwvrmlcontext.py +11 -0
  61. OpenGLContext/glutcontext.py +224 -0
  62. OpenGLContext/glutinteractivecontext.py +24 -0
  63. OpenGLContext/gluttestingcontext.py +11 -0
  64. OpenGLContext/glutvrmlcontext.py +25 -0
  65. OpenGLContext/glutvrmltestingcontext.py +48 -0
  66. OpenGLContext/hud.py +404 -0
  67. OpenGLContext/interactivecontext.py +29 -0
  68. OpenGLContext/loaders/__init__.py +1 -0
  69. OpenGLContext/loaders/base.py +84 -0
  70. OpenGLContext/loaders/cc0.py +137 -0
  71. OpenGLContext/loaders/gltf/__init__.py +71 -0
  72. OpenGLContext/loaders/gltf/accessors.py +255 -0
  73. OpenGLContext/loaders/gltf/animation.py +611 -0
  74. OpenGLContext/loaders/gltf/draco.py +166 -0
  75. OpenGLContext/loaders/gltf/environment_sky.py +416 -0
  76. OpenGLContext/loaders/gltf/loader.py +80 -0
  77. OpenGLContext/loaders/gltf/materials.py +298 -0
  78. OpenGLContext/loaders/gltf/meshes.py +292 -0
  79. OpenGLContext/loaders/gltf/samples.py +162 -0
  80. OpenGLContext/loaders/gltf/scene.py +673 -0
  81. OpenGLContext/loaders/gltf/specular_glossiness.py +154 -0
  82. OpenGLContext/loaders/gltf/textures.py +155 -0
  83. OpenGLContext/loaders/gltf/transforms.py +261 -0
  84. OpenGLContext/loaders/gltf_demos.py +511 -0
  85. OpenGLContext/loaders/hdr.py +228 -0
  86. OpenGLContext/loaders/hdri.py +83 -0
  87. OpenGLContext/loaders/loader.py +255 -0
  88. OpenGLContext/loaders/obj.py +299 -0
  89. OpenGLContext/loaders/resolver.py +536 -0
  90. OpenGLContext/loaders/tiles3d/__init__.py +9 -0
  91. OpenGLContext/loaders/tiles3d/boundingvolume.py +120 -0
  92. OpenGLContext/loaders/tiles3d/dem.py +71 -0
  93. OpenGLContext/loaders/tiles3d/fetch.py +102 -0
  94. OpenGLContext/loaders/tiles3d/foliage.py +786 -0
  95. OpenGLContext/loaders/tiles3d/frustum.py +70 -0
  96. OpenGLContext/loaders/tiles3d/geomorph.py +57 -0
  97. OpenGLContext/loaders/tiles3d/gltf_uploader.py +246 -0
  98. OpenGLContext/loaders/tiles3d/loadmanager.py +121 -0
  99. OpenGLContext/loaders/tiles3d/physics_colliders.py +78 -0
  100. OpenGLContext/loaders/tiles3d/procedural.py +355 -0
  101. OpenGLContext/loaders/tiles3d/residency.py +102 -0
  102. OpenGLContext/loaders/tiles3d/runtime.py +168 -0
  103. OpenGLContext/loaders/tiles3d/sample.py +212 -0
  104. OpenGLContext/loaders/tiles3d/scatter.py +77 -0
  105. OpenGLContext/loaders/tiles3d/screenspaceerror.py +31 -0
  106. OpenGLContext/loaders/tiles3d/tileset.py +390 -0
  107. OpenGLContext/loaders/tiles3d/traversal.py +108 -0
  108. OpenGLContext/loaders/tiles3d/vegetation.py +361 -0
  109. OpenGLContext/loaders/vrml97.py +133 -0
  110. OpenGLContext/looptrace.py +307 -0
  111. OpenGLContext/move/__init__.py +0 -0
  112. OpenGLContext/move/bindingstore.py +232 -0
  113. OpenGLContext/move/direct.py +156 -0
  114. OpenGLContext/move/dragwatcher.py +42 -0
  115. OpenGLContext/move/examinemanager.py +100 -0
  116. OpenGLContext/move/followcam.py +121 -0
  117. OpenGLContext/move/modes.py +522 -0
  118. OpenGLContext/move/movementmanager.py +74 -0
  119. OpenGLContext/move/navigation.py +137 -0
  120. OpenGLContext/move/physicsplatform.py +203 -0
  121. OpenGLContext/move/physicswalk.py +469 -0
  122. OpenGLContext/move/smooth.py +68 -0
  123. OpenGLContext/move/terrainwalk.py +451 -0
  124. OpenGLContext/move/trackball.py +92 -0
  125. OpenGLContext/move/viewplatform.py +283 -0
  126. OpenGLContext/move/viewplatformmixin.py +335 -0
  127. OpenGLContext/nav/__init__.py +13 -0
  128. OpenGLContext/nav/navmesh.py +476 -0
  129. OpenGLContext/passes/__init__.py +0 -0
  130. OpenGLContext/passes/_flat.py +1317 -0
  131. OpenGLContext/passes/asyncpick.py +216 -0
  132. OpenGLContext/passes/bloom.py +257 -0
  133. OpenGLContext/passes/flatcompat.py +246 -0
  134. OpenGLContext/passes/flatcore.py +198 -0
  135. OpenGLContext/passes/flateffects.py +372 -0
  136. OpenGLContext/passes/ibl.py +711 -0
  137. OpenGLContext/passes/instancing.py +775 -0
  138. OpenGLContext/passes/pbrpass.py +829 -0
  139. OpenGLContext/passes/renderpass.py +79 -0
  140. OpenGLContext/passes/renderstats.py +55 -0
  141. OpenGLContext/passes/rendervisitor.py +60 -0
  142. OpenGLContext/passes/selection.py +450 -0
  143. OpenGLContext/passes/selectionbuffers.py +442 -0
  144. OpenGLContext/passes/shaderpass.py +1018 -0
  145. OpenGLContext/passes/shaderpass_shadow.py +197 -0
  146. OpenGLContext/passes/shadersource.py +121 -0
  147. OpenGLContext/passes/shadowcaps.py +182 -0
  148. OpenGLContext/passes/shadowmap.py +330 -0
  149. OpenGLContext/passes/shadowmath.py +290 -0
  150. OpenGLContext/passes/shadowmixin.py +906 -0
  151. OpenGLContext/passes/shadowpool.py +186 -0
  152. OpenGLContext/passes/transmission.py +124 -0
  153. OpenGLContext/physics/__init__.py +16 -0
  154. OpenGLContext/physics/debugdraw.py +370 -0
  155. OpenGLContext/physics/demo.py +157 -0
  156. OpenGLContext/physics/gltf_world.py +185 -0
  157. OpenGLContext/physics/manager.py +84 -0
  158. OpenGLContext/physics/threaded.py +93 -0
  159. OpenGLContext/plugins.py +61 -0
  160. OpenGLContext/pygamecontext.py +178 -0
  161. OpenGLContext/pygameinteractivecontext.py +36 -0
  162. OpenGLContext/pygametestingcontext.py +10 -0
  163. OpenGLContext/pygamevrmlcontext.py +13 -0
  164. OpenGLContext/quaternion.py +190 -0
  165. OpenGLContext/renderoptions.py +280 -0
  166. OpenGLContext/resources/__init__.py +36 -0
  167. OpenGLContext/resources/available_contexts.txt +4 -0
  168. OpenGLContext/resources/available_contexts_txt.py +9 -0
  169. OpenGLContext/resources/context_icon_png.py +46 -0
  170. OpenGLContext/resources/context_icon_small_png.py +21 -0
  171. OpenGLContext/resources/disk_icon_hi_png.py +14 -0
  172. OpenGLContext/resources/disk_icon_png.py +15 -0
  173. OpenGLContext/resources/environment/pimbackground_BK.jpg +0 -0
  174. OpenGLContext/resources/environment/pimbackground_DN.jpg +0 -0
  175. OpenGLContext/resources/environment/pimbackground_FR.jpg +0 -0
  176. OpenGLContext/resources/environment/pimbackground_LF.jpg +0 -0
  177. OpenGLContext/resources/environment/pimbackground_RT.jpg +0 -0
  178. OpenGLContext/resources/environment/pimbackground_UP.jpg +0 -0
  179. OpenGLContext/resources/environment/studio_BK.jpg +0 -0
  180. OpenGLContext/resources/environment/studio_DN.jpg +0 -0
  181. OpenGLContext/resources/environment/studio_FR.jpg +0 -0
  182. OpenGLContext/resources/environment/studio_LF.jpg +0 -0
  183. OpenGLContext/resources/environment/studio_RT.jpg +0 -0
  184. OpenGLContext/resources/environment/studio_UP.jpg +0 -0
  185. OpenGLContext/resources/environment/studiobright_BK.jpg +0 -0
  186. OpenGLContext/resources/environment/studiobright_DN.jpg +0 -0
  187. OpenGLContext/resources/environment/studiobright_FR.jpg +0 -0
  188. OpenGLContext/resources/environment/studiobright_LF.jpg +0 -0
  189. OpenGLContext/resources/environment/studiobright_RT.jpg +0 -0
  190. OpenGLContext/resources/environment/studiobright_UP.jpg +0 -0
  191. OpenGLContext/resources/green_arrow_right_png.py +47 -0
  192. OpenGLContext/resources/legacy_lighting.vert.txt +37 -0
  193. OpenGLContext/resources/legacy_lighting_vert_txt.py +18 -0
  194. OpenGLContext/resources/lights.vert.txt +107 -0
  195. OpenGLContext/resources/lights_vert_txt.py +23 -0
  196. OpenGLContext/resources/phongprecalc_vert.py +30 -0
  197. OpenGLContext/resources/phongweights_frag.py +47 -0
  198. OpenGLContext/resources/pygame_icon_png.py +47 -0
  199. OpenGLContext/resources/red_arrow_right_png.py +50 -0
  200. OpenGLContext/resources/simpleshader.frag.txt +3 -0
  201. OpenGLContext/resources/simpleshader.vert.txt +3 -0
  202. OpenGLContext/resources/simpleshader_frag_txt.py +9 -0
  203. OpenGLContext/resources/simpleshader_vert_txt.py +10 -0
  204. OpenGLContext/scenegraph/__init__.py +19 -0
  205. OpenGLContext/scenegraph/appearance.py +107 -0
  206. OpenGLContext/scenegraph/arraygeometry.py +238 -0
  207. OpenGLContext/scenegraph/audio.py +632 -0
  208. OpenGLContext/scenegraph/background.py +108 -0
  209. OpenGLContext/scenegraph/basenodes.py +50 -0
  210. OpenGLContext/scenegraph/billboard.py +14 -0
  211. OpenGLContext/scenegraph/boundingvolume.py +494 -0
  212. OpenGLContext/scenegraph/box.py +180 -0
  213. OpenGLContext/scenegraph/collision.py +14 -0
  214. OpenGLContext/scenegraph/coordinate.py +20 -0
  215. OpenGLContext/scenegraph/coordinatebounded.py +18 -0
  216. OpenGLContext/scenegraph/cubebackground.py +207 -0
  217. OpenGLContext/scenegraph/extrusions.py +275 -0
  218. OpenGLContext/scenegraph/fog.py +123 -0
  219. OpenGLContext/scenegraph/gear.py +323 -0
  220. OpenGLContext/scenegraph/group.py +20 -0
  221. OpenGLContext/scenegraph/grouping.py +140 -0
  222. OpenGLContext/scenegraph/hdrbackground.py +384 -0
  223. OpenGLContext/scenegraph/ifscompiler.py +633 -0
  224. OpenGLContext/scenegraph/imagetexture.py +365 -0
  225. OpenGLContext/scenegraph/indexedfaceset.py +238 -0
  226. OpenGLContext/scenegraph/indexedlineset.py +418 -0
  227. OpenGLContext/scenegraph/indexedpolygons.py +422 -0
  228. OpenGLContext/scenegraph/inline.py +79 -0
  229. OpenGLContext/scenegraph/instancedgl.py +187 -0
  230. OpenGLContext/scenegraph/interpolators.py +122 -0
  231. OpenGLContext/scenegraph/light.py +213 -0
  232. OpenGLContext/scenegraph/lod.py +22 -0
  233. OpenGLContext/scenegraph/material.py +96 -0
  234. OpenGLContext/scenegraph/material_fields.py +50 -0
  235. OpenGLContext/scenegraph/mouseover.py +43 -0
  236. OpenGLContext/scenegraph/nodepath.py +52 -0
  237. OpenGLContext/scenegraph/nurbs.py +637 -0
  238. OpenGLContext/scenegraph/nurbssampling.py +101 -0
  239. OpenGLContext/scenegraph/nurbstess.py +316 -0
  240. OpenGLContext/scenegraph/nurbstrim.py +54 -0
  241. OpenGLContext/scenegraph/particles.py +786 -0
  242. OpenGLContext/scenegraph/pbrmaterial.py +229 -0
  243. OpenGLContext/scenegraph/pbrmesh.py +558 -0
  244. OpenGLContext/scenegraph/physicsbody.py +72 -0
  245. OpenGLContext/scenegraph/pointset.py +257 -0
  246. OpenGLContext/scenegraph/polygon.py +166 -0
  247. OpenGLContext/scenegraph/polygonsort.py +119 -0
  248. OpenGLContext/scenegraph/polygontessellator.py +213 -0
  249. OpenGLContext/scenegraph/quadrics.py +494 -0
  250. OpenGLContext/scenegraph/scenegraph.py +21 -0
  251. OpenGLContext/scenegraph/shadergeometry.py +403 -0
  252. OpenGLContext/scenegraph/shaders.py +812 -0
  253. OpenGLContext/scenegraph/shape.py +243 -0
  254. OpenGLContext/scenegraph/simplebackground.py +34 -0
  255. OpenGLContext/scenegraph/spherebackground.py +462 -0
  256. OpenGLContext/scenegraph/switch.py +75 -0
  257. OpenGLContext/scenegraph/teapot.py +439 -0
  258. OpenGLContext/scenegraph/teapot_nurbs.py +291 -0
  259. OpenGLContext/scenegraph/teapot_nurbs_data.py +384 -0
  260. OpenGLContext/scenegraph/terrain/__init__.py +12 -0
  261. OpenGLContext/scenegraph/terrain/heightfield.py +172 -0
  262. OpenGLContext/scenegraph/terrain/splat.py +224 -0
  263. OpenGLContext/scenegraph/tessellationlod.py +97 -0
  264. OpenGLContext/scenegraph/text/__init__.py +1 -0
  265. OpenGLContext/scenegraph/text/_toolsfont.py +200 -0
  266. OpenGLContext/scenegraph/text/font.py +452 -0
  267. OpenGLContext/scenegraph/text/fontprovider.py +193 -0
  268. OpenGLContext/scenegraph/text/fonts/__init__.py +75 -0
  269. OpenGLContext/scenegraph/text/fonts/font_atlas_10.py +133 -0
  270. OpenGLContext/scenegraph/text/fonts/font_atlas_12.py +158 -0
  271. OpenGLContext/scenegraph/text/fonts/font_atlas_14.py +176 -0
  272. OpenGLContext/scenegraph/text/fonts/font_atlas_16.py +204 -0
  273. OpenGLContext/scenegraph/text/fonts/font_atlas_18.py +222 -0
  274. OpenGLContext/scenegraph/text/fonts/font_atlas_20.py +244 -0
  275. OpenGLContext/scenegraph/text/fonts/font_atlas_24.py +294 -0
  276. OpenGLContext/scenegraph/text/fonts/font_atlas_28.py +339 -0
  277. OpenGLContext/scenegraph/text/fonts/font_atlas_32.py +390 -0
  278. OpenGLContext/scenegraph/text/fontstyle3d.py +62 -0
  279. OpenGLContext/scenegraph/text/glutfont.py +186 -0
  280. OpenGLContext/scenegraph/text/pygamefont.py +213 -0
  281. OpenGLContext/scenegraph/text/shaderfont.py +340 -0
  282. OpenGLContext/scenegraph/text/shadertext.py +433 -0
  283. OpenGLContext/scenegraph/text/text.py +75 -0
  284. OpenGLContext/scenegraph/text/toolsfont.py +819 -0
  285. OpenGLContext/scenegraph/text/ttfregistry.py +135 -0
  286. OpenGLContext/scenegraph/text/wglfont.py +228 -0
  287. OpenGLContext/scenegraph/text/wglfontprovider.py +37 -0
  288. OpenGLContext/scenegraph/text/wxfont.py +314 -0
  289. OpenGLContext/scenegraph/texturetransform.py +66 -0
  290. OpenGLContext/scenegraph/tilesterrain.py +95 -0
  291. OpenGLContext/scenegraph/timesensor.py +51 -0
  292. OpenGLContext/scenegraph/transform.py +180 -0
  293. OpenGLContext/scenegraph/vegetation/__init__.py +20 -0
  294. OpenGLContext/scenegraph/vegetation/base.py +96 -0
  295. OpenGLContext/scenegraph/vegetation/billboards.py +145 -0
  296. OpenGLContext/scenegraph/vegetation/clumps.py +260 -0
  297. OpenGLContext/scenegraph/vegetation/grid.py +112 -0
  298. OpenGLContext/scenegraph/vegetation/nearmesh.py +185 -0
  299. OpenGLContext/scenegraph/vertex.py +54 -0
  300. OpenGLContext/scenegraph/viewpoint.py +42 -0
  301. OpenGLContext/scenegraph/winding.py +49 -0
  302. OpenGLContext/shaders/_brdf_inc.glsl +124 -0
  303. OpenGLContext/shaders/_common_inc.glsl +24 -0
  304. OpenGLContext/shaders/_cubemap_inc.glsl +26 -0
  305. OpenGLContext/shaders/_lights_inc.glsl +32 -0
  306. OpenGLContext/shaders/_objectid_inc.glsl +14 -0
  307. OpenGLContext/shaders/_shadow_inc.glsl +254 -0
  308. OpenGLContext/shaders/_vrml97_lighting_inc.glsl +78 -0
  309. OpenGLContext/shaders/hdr_background.frag +32 -0
  310. OpenGLContext/shaders/hdr_background.vert +15 -0
  311. OpenGLContext/shaders/ibl_brdf.frag +78 -0
  312. OpenGLContext/shaders/ibl_env.frag +53 -0
  313. OpenGLContext/shaders/ibl_equirect.frag +19 -0
  314. OpenGLContext/shaders/ibl_fullscreen.vert +11 -0
  315. OpenGLContext/shaders/ibl_irradiance.frag +40 -0
  316. OpenGLContext/shaders/ibl_prefilter.frag +56 -0
  317. OpenGLContext/shaders/particle.frag +42 -0
  318. OpenGLContext/shaders/particle.vert +52 -0
  319. OpenGLContext/shaders/pbr.frag +906 -0
  320. OpenGLContext/shaders/pbr.vert +70 -0
  321. OpenGLContext/shaders/shadow_depth.frag +4 -0
  322. OpenGLContext/shaders/shadow_depth.vert +18 -0
  323. OpenGLContext/shaders/terrain_splat.frag +80 -0
  324. OpenGLContext/shaders/terrain_splat.vert +16 -0
  325. OpenGLContext/shaders/veg_billboard.frag +35 -0
  326. OpenGLContext/shaders/veg_billboard.vert +17 -0
  327. OpenGLContext/shaders/veg_clump.frag +27 -0
  328. OpenGLContext/shaders/veg_mesh.frag +26 -0
  329. OpenGLContext/shaders/veg_mesh.vert +17 -0
  330. OpenGLContext/shaders/vrml97_background.frag +17 -0
  331. OpenGLContext/shaders/vrml97_background.vert +24 -0
  332. OpenGLContext/shaders/vrml97_lighting.frag +111 -0
  333. OpenGLContext/shaders/vrml97_lighting.vert +50 -0
  334. OpenGLContext/shaders/vrml97_line.frag +23 -0
  335. OpenGLContext/shaders/vrml97_line.vert +24 -0
  336. OpenGLContext/shaders/vrml97_point.frag +36 -0
  337. OpenGLContext/shaders/vrml97_point.vert +21 -0
  338. OpenGLContext/shaders/vrml97_unlit.frag +59 -0
  339. OpenGLContext/shaders/vrml97_unlit.vert +18 -0
  340. OpenGLContext/shaders/vrml97_vertex_color.frag +85 -0
  341. OpenGLContext/shaders/vrml97_vertex_color.vert +34 -0
  342. OpenGLContext/stalltrace.py +637 -0
  343. OpenGLContext/testing/__init__.py +67 -0
  344. OpenGLContext/testing/display.py +30 -0
  345. OpenGLContext/testing/event_injector.py +564 -0
  346. OpenGLContext/testing/framebuffer_comparison.py +842 -0
  347. OpenGLContext/testing/gl_env.py +39 -0
  348. OpenGLContext/testing/paths.py +23 -0
  349. OpenGLContext/testing/process_exit.py +30 -0
  350. OpenGLContext/testing/report_generator.py +594 -0
  351. OpenGLContext/testing/subprocess_runner.py +411 -0
  352. OpenGLContext/testingcontext.py +36 -0
  353. OpenGLContext/tests/test_atlas.py +62 -0
  354. OpenGLContext/tests/test_configs.py +27 -0
  355. OpenGLContext/tests/test_polygonsort.py +45 -0
  356. OpenGLContext/tests/test_utilities.py +84 -0
  357. OpenGLContext/tests/test_viewplatform.py +39 -0
  358. OpenGLContext/texture.py +449 -0
  359. OpenGLContext/texturecache.py +37 -0
  360. OpenGLContext/triangleutilities.py +89 -0
  361. OpenGLContext/ui/__init__.py +39 -0
  362. OpenGLContext/ui/bindings.py +243 -0
  363. OpenGLContext/ui/console.py +339 -0
  364. OpenGLContext/ui/debugoverlay.py +519 -0
  365. OpenGLContext/ui/dialogs.py +124 -0
  366. OpenGLContext/ui/draw.py +617 -0
  367. OpenGLContext/ui/gallery.py +425 -0
  368. OpenGLContext/ui/generate.py +195 -0
  369. OpenGLContext/ui/geometry.py +88 -0
  370. OpenGLContext/ui/hudwidgets.py +1053 -0
  371. OpenGLContext/ui/layout.py +263 -0
  372. OpenGLContext/ui/metrics.py +218 -0
  373. OpenGLContext/ui/overlay.py +412 -0
  374. OpenGLContext/ui/panel.py +459 -0
  375. OpenGLContext/ui/pictures.py +261 -0
  376. OpenGLContext/ui/screen.py +177 -0
  377. OpenGLContext/ui/scroll.py +297 -0
  378. OpenGLContext/ui/session.py +329 -0
  379. OpenGLContext/ui/settings.py +273 -0
  380. OpenGLContext/ui/skin.py +330 -0
  381. OpenGLContext/ui/widgets.py +1199 -0
  382. OpenGLContext/userpaths.py +48 -0
  383. OpenGLContext/utilities.py +94 -0
  384. OpenGLContext/vectorutilities.py +101 -0
  385. OpenGLContext/viewer/__init__.py +60 -0
  386. OpenGLContext/viewer/adapters/__init__.py +150 -0
  387. OpenGLContext/viewer/adapters/base.py +157 -0
  388. OpenGLContext/viewer/adapters/gltf.py +31 -0
  389. OpenGLContext/viewer/adapters/obj.py +20 -0
  390. OpenGLContext/viewer/adapters/scenegraph.py +71 -0
  391. OpenGLContext/viewer/adapters/tiles.py +296 -0
  392. OpenGLContext/viewer/adapters/vrml.py +25 -0
  393. OpenGLContext/viewer/asyncscene.py +117 -0
  394. OpenGLContext/viewer/caption.py +126 -0
  395. OpenGLContext/viewer/capture.py +77 -0
  396. OpenGLContext/viewer/debug.py +78 -0
  397. OpenGLContext/viewer/environment.py +131 -0
  398. OpenGLContext/viewer/framing.py +111 -0
  399. OpenGLContext/viewer/library.py +235 -0
  400. OpenGLContext/viewer/menu.py +218 -0
  401. OpenGLContext/viewer/options.py +150 -0
  402. OpenGLContext/viewer/overlay.py +63 -0
  403. OpenGLContext/viewer/sceneviewer.py +1056 -0
  404. OpenGLContext/viewer/screens.py +172 -0
  405. OpenGLContext/viewer/source.py +56 -0
  406. OpenGLContext/visitor.py +68 -0
  407. OpenGLContext/vrmlcontext.py +72 -0
  408. OpenGLContext/wxcontext.py +424 -0
  409. OpenGLContext/wxinteractivecontext.py +25 -0
  410. OpenGLContext/wxtestingcontext.py +9 -0
  411. OpenGLContext/wxvrmlcontext.py +13 -0
  412. openglcontext-3.0.0a1.dist-info/METADATA +650 -0
  413. openglcontext-3.0.0a1.dist-info/RECORD +417 -0
  414. openglcontext-3.0.0a1.dist-info/WHEEL +5 -0
  415. openglcontext-3.0.0a1.dist-info/entry_points.txt +11 -0
  416. openglcontext-3.0.0a1.dist-info/licenses/license.txt +89 -0
  417. openglcontext-3.0.0a1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,645 @@
1
+ #! /usr/bin/env python
2
+ """Render every glTF demo and diff it against a verified baseline (``oglc-gltf-regression``).
3
+
4
+ The conformance work was verified by hand: capture a demo, eyeball it against the
5
+ Khronos reference, move on. Nothing stored a baseline, so nothing caught a
6
+ *regression* when the next shader or loader change landed. This command closes
7
+ that loop.
8
+
9
+ For every scene in the shared roster (`OpenGLContext.loaders.gltf_demos`) and each
10
+ of its viewpoints it:
11
+
12
+ 1. renders the model through the ``oglc-gltf`` viewer in its own hidden GL context
13
+ (one process per view -- the robust way to render a batch),
14
+ 2. compares the new render against **our reference** -- the verified baseline in
15
+ the separate ``reference-images`` sub-repo -- and flags a regression when they
16
+ diverge beyond tolerance,
17
+ 3. fetches (and disk-caches) the **upstream Khronos reference** screenshot as a
18
+ tertiary sanity check, and
19
+ 4. writes an HTML report with *our-reference | new | diff | upstream* side by
20
+ side, click-to-zoom, and an expand-all control. The report links the on-disk
21
+ renders by path (it is not self-contained), keeping it small.
22
+
23
+ oglc-gltf-regression --bless # establish/refresh baselines (after review)
24
+ oglc-gltf-regression # exit non-zero if anything regressed
25
+ oglc-gltf-regression --only MetalRoughSpheres --only Duck
26
+
27
+ Downloaded ``.glb`` files and reference screenshots are cached under the shared
28
+ glTF cache, so reruns only re-render.
29
+ """
30
+ import argparse
31
+ import hashlib
32
+ import json
33
+ import os
34
+ import subprocess
35
+ import sys
36
+ import urllib.parse
37
+ import urllib.request
38
+ from typing import Any, cast
39
+
40
+ from OpenGLContext import renderoptions
41
+ from OpenGLContext.loaders import gltf
42
+ from OpenGLContext.loaders import resolver
43
+ from OpenGLContext.loaders import gltf_demos
44
+
45
+ HERE = os.path.dirname(os.path.abspath(__file__))
46
+ REPO = os.path.dirname(os.path.dirname(HERE)) # this package's parent dir
47
+
48
+
49
+ def _source_root() -> str | None:
50
+ """Locate the openglcontext source checkout (dir with OpenGLContext/ + tests/).
51
+
52
+ Searched from the current directory upward, then this file's location. When
53
+ the package is pip-installed into a virtualenv, ``__file__`` points at
54
+ ``site-packages`` -- deriving output paths from it drops the report inside the
55
+ venv. The working tree is what we want, so prefer it.
56
+ """
57
+ def markers(d: str) -> bool:
58
+ return (os.path.isdir(os.path.join(d, 'tests')) and
59
+ os.path.isfile(os.path.join(d, 'OpenGLContext', '__init__.py')))
60
+
61
+ for start in (os.getcwd(), REPO):
62
+ d = start
63
+ while True:
64
+ for cand in (d, os.path.join(d, 'openglcontext')):
65
+ if markers(cand):
66
+ return cand
67
+ parent = os.path.dirname(d)
68
+ if parent == d:
69
+ break
70
+ d = parent
71
+ return None
72
+
73
+
74
+ def default_report_path() -> str:
75
+ """Default report location: a subdirectory of the source tree's tests/."""
76
+ root = _source_root() or REPO
77
+ return os.path.join(root, 'tests', 'gltf_regression', 'gltf_regression_report.html')
78
+
79
+ # Render/capture defaults. Six frames + a short settle is the combination the
80
+ # conformance-capture harness proved reliable for back-to-back hidden captures.
81
+ DEFAULT_SIZE = (900, 640)
82
+ # Canonical capture params -- both the baseline bless and the conformance test
83
+ # render with these, so a re-render matches its baseline (a mismatch in frame count
84
+ # / settle delay shifts settle-sensitive scenes -- Parthenon, heavy/animated models
85
+ # -- past tolerance). Keep bless and test on the SAME values.
86
+ DEFAULT_FRAMES = 8
87
+ DEFAULT_DELAY = 0.5
88
+ # Regression tolerance: percent of pixels allowed to differ by more than
89
+ # DIFF_THRESHOLD per channel (the project's documented visual-regression default).
90
+ DEFAULT_TOLERANCE = 2.0
91
+ DIFF_THRESHOLD = 8
92
+
93
+
94
+ def default_baseline_root() -> str:
95
+ """Where verified 'our reference' baselines live.
96
+
97
+ ``OPENGLCONTEXT_GLTF_BASELINE`` wins; otherwise the sibling ``reference-images``
98
+ sub-repo's ``gltf_baseline`` folder (kept out of the main code repo).
99
+ """
100
+ env = os.environ.get('OPENGLCONTEXT_GLTF_BASELINE', '').strip()
101
+ if env:
102
+ return env
103
+ parent = os.path.dirname(REPO)
104
+ return os.path.join(parent, 'reference-images', 'gltf_baseline')
105
+
106
+
107
+ def _env_prefix(name: str = 'pimbackground_') -> str | None:
108
+ """Path prefix of a bundled environment cubemap face set, or None.
109
+
110
+ ``pimbackground_`` is the default outdoor set; ``studio_`` is the neutral
111
+ studio set the Khronos material references use (bright grey backdrop +
112
+ softboxes) for metals/glass/anisotropy.
113
+ """
114
+ import OpenGLContext
115
+ prefix = os.path.join(os.path.dirname(OpenGLContext.__file__),
116
+ 'resources', 'environment', name)
117
+ return prefix if os.path.exists(prefix + 'UP.jpg') else None
118
+
119
+
120
+ def _env_prefix_for(spec: gltf_demos.SceneSpec) -> str | None:
121
+ """The env-cubemap prefix a scene should reflect, honouring ``spec.environment``."""
122
+ env = getattr(spec, 'environment', None)
123
+ if env == 'studio':
124
+ return _env_prefix('studio_') or _env_prefix()
125
+ if env == 'studiobright': # near-white studio for showcase metals/glass
126
+ return _env_prefix('studiobright_') or _env_prefix('studio_') or _env_prefix()
127
+ return _env_prefix()
128
+
129
+
130
+ # The procedural studio cube is smooth and low-detail, so even a mirror metal
131
+ # reflects it softly -- unlike the Khronos references, which are shot against a
132
+ # real HDR studio. Map the studio keywords to a CC0 Poly Haven HDR panorama the
133
+ # viewer loads as both the IBL probe and the skybox, so metals get sharp, detailed
134
+ # reflections. Non-keyword ``environment`` values are treated as an explicit HDR
135
+ # (a Poly Haven catalogue name or an .hdr URL/path) and passed straight through.
136
+ _STUDIO_HDR = 'brown_photostudio_02'
137
+ _PROCEDURAL_ENVS = (None, 'studio', 'studiobright')
138
+
139
+
140
+ def _environment_for(spec: gltf_demos.SceneSpec) -> tuple[Any, str]:
141
+ """Return ``(--environment value, --ibl-intensity)`` for a cube-background scene.
142
+
143
+ ``'studio'``/``'studiobright'`` resolve to the HDR studio panorama; ``None``
144
+ keeps the bundled procedural outdoor cube; any other value is an explicit HDR
145
+ source (catalogue name or URL) reflected and drawn as the skybox.
146
+ """
147
+ env = getattr(spec, 'environment', None)
148
+ if env in ('studio', 'studiobright'):
149
+ return _STUDIO_HDR, '1.0'
150
+ if env == 'procedural_studio':
151
+ # The bundled neutral-grey procedural studio (dim, even). For a *subtle*
152
+ # effect whose colour a bright HDR would tonemap to white -- dielectric
153
+ # thin-film iridescence -- against which the pale tint actually reads, as
154
+ # in the Khronos neutral-grey reference.
155
+ return (_env_prefix('studio_') or _env_prefix()), '0.7'
156
+ if env is None:
157
+ return _env_prefix_for(spec), '0.9'
158
+ return env, '1.0'
159
+
160
+
161
+ # --------------------------------------------------------------------------- #
162
+ # Model resolution (cached): local path the viewer can load.
163
+ # --------------------------------------------------------------------------- #
164
+ _MIRROR_DIR = os.path.join(resolver._default_cache_dir(), 'gltf_mirror')
165
+
166
+
167
+ def _cached_url_path(url: str, cache_dir: str) -> str:
168
+ """Fetch ``url`` into the sha1-keyed cache and return its local path."""
169
+ resolver._fetch_url(url, cache_dir)
170
+ key = hashlib.sha1(url.encode('utf-8')).hexdigest() + os.path.splitext(url)[1]
171
+ return os.path.join(cache_dir, key)
172
+
173
+
174
+ def _dl(url: str, dst: str) -> None:
175
+ """Download ``url`` to ``dst`` (once), preserving the on-disk layout so a
176
+ ``.gltf``'s relative buffer/image URIs resolve when the viewer loads it."""
177
+ if os.path.exists(dst) and os.path.getsize(dst) > 0:
178
+ return
179
+ os.makedirs(os.path.dirname(dst), exist_ok=True)
180
+ with urllib.request.urlopen(resolver.safe_url(url), timeout=60) as r, \
181
+ open(dst, 'wb') as f:
182
+ f.write(r.read())
183
+
184
+
185
+ def resolve_model(spec: gltf_demos.SceneSpec, parthenon: str | None = None) -> str | None:
186
+ """Return a local, loadable path for a scene's model (or None if unavailable).
187
+
188
+ Khronos samples resolve to the cached ``.glb``; a sample published only as
189
+ ``.gltf`` (EnvironmentTest, IridescenceMetallicSpheres, TextureTransformTest)
190
+ is mirrored -- the ``.gltf`` plus its external buffers/images -- into the
191
+ cache so the viewer can load it. A local ``source`` (Parthenon) is used
192
+ as-is, and a scene named by URL is fetched into the cache.
193
+ """
194
+ source, is_local = gltf_demos.resolve_source(spec, parthenon)
195
+ if is_local:
196
+ return source if source and os.path.exists(source) else None
197
+ cache_dir = resolver._default_cache_dir()
198
+ if resolver.is_url(source):
199
+ # One published file, so a failed fetch is the end of it: the .gltf
200
+ # mirror below only makes sense for the catalogue's directory layout.
201
+ try:
202
+ return _cached_url_path(cast(str, source), cache_dir)
203
+ except Exception as err:
204
+ print(' resolve FAILED %s: %s' % (spec.name, err))
205
+ return None
206
+
207
+ name = cast(str, source) # a non-local scene resolves to a sample name
208
+ glb_url = gltf.sample_model_url(name)
209
+ try:
210
+ return _cached_url_path(glb_url, cache_dir)
211
+ except Exception:
212
+ pass
213
+ # Fall back to the external .gltf: mirror it plus its referenced URIs.
214
+ base = '%s/%s' % (gltf.SAMPLE_MODELS_BASE, name)
215
+ gdir = os.path.join(_MIRROR_DIR, name)
216
+ local_gltf = os.path.join(gdir, '%s.gltf' % name)
217
+ try:
218
+ _dl('%s/glTF/%s.gltf' % (base, name), local_gltf)
219
+ doc = json.load(open(local_gltf))
220
+ for coll in ('buffers', 'images'):
221
+ for item in doc.get(coll, []):
222
+ uri = item.get('uri')
223
+ if not uri or uri.startswith('data:'):
224
+ continue
225
+ rel = urllib.parse.unquote(uri)
226
+ _dl('%s/glTF/%s' % (base, uri), os.path.join(gdir, rel))
227
+ return local_gltf
228
+ except Exception as err:
229
+ print(' resolve FAILED %s: %s' % (name, err))
230
+ return None
231
+
232
+
233
+ # --------------------------------------------------------------------------- #
234
+ # Render one view through the viewer CLI (its own hidden GL context).
235
+ # --------------------------------------------------------------------------- #
236
+ def render_view(spec: gltf_demos.SceneSpec, camera: int | None, model: str, out: str,
237
+ size: tuple[int, int], frames: int, delay: float,
238
+ env_prefix: str | None) -> tuple[bool, dict[str, Any]]:
239
+ """Render one scene/camera to ``out``. Returns ``(ok, stats)`` where ``stats``
240
+ holds the viewer-reported ``load_seconds``/``fps`` for the run (empty on failure)."""
241
+ w, h = size
242
+ common = ['--no-physics', '--no-shadows', '--capture', out,
243
+ '--frames', str(frames), '--capture-delay', repr(delay),
244
+ '--size', '%dx%d' % (w, h), '--background', spec.capture_background]
245
+ if camera is None:
246
+ args = [model, '--no-cameras', '--no-rotate',
247
+ '--yaw', repr(spec.yaw), '--elevation', repr(spec.elevation),
248
+ '--tilt', repr(spec.tilt), '--margin', repr(spec.margin)] + common
249
+ if spec.eye is not None and spec.look_at is not None:
250
+ # ``=`` form so a negative coordinate isn't parsed as another option.
251
+ args += ['--eye=%s' % ','.join(repr(v) for v in spec.eye),
252
+ '--look-at=%s' % ','.join(repr(v) for v in spec.look_at)]
253
+ else:
254
+ # Baked camera: adopt the authored pose, don't re-frame or spin.
255
+ args = [model, '--camera', str(camera), '--no-rotate'] + common
256
+ if spec.capture_background == 'cube':
257
+ env_arg, ibl = _environment_for(spec)
258
+ if env_arg:
259
+ args += ['--environment', env_arg, '--ibl-intensity', ibl]
260
+ # Pin animated models to a fixed time so the captured pose is reproducible
261
+ # (otherwise the frame lands at whatever animation time the settle reached).
262
+ if spec.anim_time is not None:
263
+ args += ['--anim-time', repr(float(spec.anim_time))]
264
+
265
+ # Started from a *clean* rendering environment rather than from whatever
266
+ # this process happens to be carrying: a baseline comparison whose result
267
+ # depends on which variables were set before it is not a baseline. See
268
+ # `renderoptions.clean_environment`.
269
+ env = renderoptions.clean_environment(
270
+ OPENGLCONTEXT_HIDDEN='1', OPENGLCONTEXT_NO_VSYNC='1',
271
+ OPENGLCONTEXT_PROFILE='core', OPENGLCONTEXT_BACKEND='glfw',
272
+ OPENGLCONTEXT_SHADOW_CASCADES='3',
273
+ OPENGLCONTEXT_BLOOM='1' if getattr(spec, 'bloom', False) else '0',
274
+ OPENGLCONTEXT_DISABLE_FPS_DISPLAY='1')
275
+ try:
276
+ proc = subprocess.run(
277
+ [sys.executable, '-m', 'OpenGLContext.bin.view'] + args,
278
+ timeout=120, capture_output=True, text=True,
279
+ cwd=_source_root() or REPO, env=env)
280
+ except subprocess.TimeoutExpired:
281
+ print(' render TIMEOUT')
282
+ return False, {}
283
+ stats = _parse_capture_stats(proc.stdout)
284
+ return os.path.exists(out), stats
285
+
286
+
287
+ def _parse_capture_stats(stdout: str | None) -> dict[str, Any]:
288
+ """Pull ``load_seconds``/``fps`` from the viewer's ``CAPTURE_STATS`` line."""
289
+ stats: dict[str, Any] = {}
290
+ for line in (stdout or '').splitlines():
291
+ if line.startswith('CAPTURE_STATS'):
292
+ for tok in line.split()[1:]:
293
+ key, _, val = tok.partition('=')
294
+ try:
295
+ stats[key] = float(val) if val not in ('', 'None') else None
296
+ except ValueError:
297
+ stats[key] = val or None
298
+ return stats
299
+
300
+
301
+ # --------------------------------------------------------------------------- #
302
+ # Compare new render vs baseline.
303
+ # --------------------------------------------------------------------------- #
304
+ def compare(baseline_path: str, new_path: str, diff_out: str,
305
+ threshold: int) -> tuple[Any, dict[str, Any]]:
306
+ """Compare two PNGs. Returns (ComparisonResult, stats_dict) or (None, None)."""
307
+ import numpy as np
308
+ from PIL import Image
309
+ from OpenGLContext.testing.framebuffer_comparison import ComparisonResult
310
+
311
+ base = np.asarray(Image.open(baseline_path).convert('RGB'))
312
+ new = np.asarray(Image.open(new_path).convert('RGB'))
313
+ result = ComparisonResult(base, new, threshold)
314
+ if result.diff_image is not None:
315
+ Image.fromarray(result.diff_image, 'RGB').save(diff_out)
316
+ stats = {
317
+ 'shapes_match': 'Yes' if result.shapes_match else 'No',
318
+ 'max_diff': result.max_diff,
319
+ 'mean_diff': result.mean_diff,
320
+ 'percent_different': result.percent_different,
321
+ 'pixels_different': result.pixels_different,
322
+ 'total_pixels': result.total_pixels,
323
+ }
324
+ return result, stats
325
+
326
+
327
+ def is_regression(result: Any, tolerance: float) -> bool:
328
+ """A regression is a shape change or more than ``tolerance`` percent of pixels
329
+ differing beyond the per-channel threshold."""
330
+ if not result.shapes_match:
331
+ return True
332
+ return result.percent_different > tolerance
333
+
334
+
335
+ # --------------------------------------------------------------------------- #
336
+ # Driver.
337
+ # --------------------------------------------------------------------------- #
338
+ def select_scenes(only: list[str] | None) -> list[gltf_demos.SceneSpec]:
339
+ """The scenes to run: all, or the subset named in ``only`` (case-sensitive)."""
340
+ if not only:
341
+ return list(gltf_demos.iter_scenes())
342
+ wanted = set(only)
343
+ return [s for s in gltf_demos.iter_scenes() if s.name in wanted]
344
+
345
+
346
+ def _gl_renderer() -> dict[str, str]:
347
+ """The GPU's GL_RENDERER/GL_VERSION, queried in a throwaway hidden context."""
348
+ code = ("import glfw;glfw.init();glfw.window_hint(glfw.VISIBLE,glfw.FALSE);"
349
+ "w=glfw.create_window(8,8,'r',None,None);glfw.make_context_current(w);"
350
+ "from OpenGL.GL import glGetString,GL_RENDERER,GL_VERSION;"
351
+ "print((glGetString(GL_RENDERER) or b'').decode());"
352
+ "print((glGetString(GL_VERSION) or b'').decode())")
353
+ try:
354
+ out = subprocess.run(
355
+ [sys.executable, '-c', code], capture_output=True, text=True, timeout=30,
356
+ env=dict(os.environ, OPENGLCONTEXT_HIDDEN='1', OPENGLCONTEXT_BACKEND='glfw'),
357
+ ).stdout.splitlines()
358
+ return {'gl_renderer': (out[0].strip() if out else '') or 'unknown',
359
+ 'gl_version': out[1].strip() if len(out) > 1 else ''}
360
+ except Exception:
361
+ return {'gl_renderer': 'unknown', 'gl_version': ''}
362
+
363
+
364
+ def _provenance() -> dict[str, str]:
365
+ """Git hash (+``-dirty``), timestamp and GPU identity, stamped into every
366
+ render's metadata so a baseline records exactly how (and on what) it was made."""
367
+ import datetime
368
+ root = _source_root() or REPO
369
+ git = 'unknown'
370
+ try:
371
+ rev = subprocess.run(['git', 'rev-parse', '--short', 'HEAD'], cwd=root,
372
+ capture_output=True, text=True)
373
+ dirty = subprocess.run(['git', 'status', '--porcelain'], cwd=root,
374
+ capture_output=True, text=True)
375
+ if rev.returncode == 0 and rev.stdout.strip():
376
+ git = rev.stdout.strip() + ('-dirty' if dirty.stdout.strip() else '')
377
+ except Exception:
378
+ pass
379
+ prov = {'git': git,
380
+ 'rendered_at': datetime.datetime.now().isoformat(timespec='seconds')}
381
+ prov.update(_gl_renderer())
382
+ return prov
383
+
384
+
385
+ def _view_metadata(spec: gltf_demos.SceneSpec, camera: int | None, url: str | None,
386
+ args: argparse.Namespace, provenance: dict[str, str],
387
+ stats: dict[str, Any] | None = None) -> dict[str, Any]:
388
+ """The per-view render metadata recorded to a sidecar JSON and shown in the
389
+ report: what was rendered, how it was framed/lit, and the run provenance.
390
+
391
+ ``url`` is the ORIGINAL source the viewer loaded -- the remote http(s) URL for a
392
+ Khronos sample (loaded as an untrusted resource through the resolver), not a
393
+ local cache path -- so the record reflects the real load context. ``stats``
394
+ carries the viewer-reported load time / fps for this render."""
395
+ env_arg = ibl = None
396
+ if spec.capture_background == 'cube':
397
+ env_arg, ibl = _environment_for(spec)
398
+ stats = stats or {}
399
+ return {
400
+ 'scene': spec.name,
401
+ 'slug': spec.slug(camera),
402
+ 'camera': camera,
403
+ 'url': url,
404
+ 'load_context': 'url' if isinstance(url, str) and url.startswith(('http://', 'https://')) else 'local',
405
+ 'load_seconds': stats.get('load_seconds'),
406
+ 'fps': stats.get('fps'),
407
+ 'background': spec.capture_background,
408
+ 'environment': env_arg,
409
+ 'ibl_intensity': ibl,
410
+ 'framing': ({'eye': list(spec.eye), 'look_at': list(spec.look_at)}
411
+ if spec.eye is not None and spec.look_at is not None
412
+ else {'yaw': spec.yaw, 'elevation': spec.elevation,
413
+ 'tilt': spec.tilt, 'margin': spec.margin}),
414
+ 'anim_time': spec.anim_time,
415
+ 'frames': args.frames,
416
+ 'delay': args.delay,
417
+ 'size': list(args.size),
418
+ 'git': provenance['git'],
419
+ 'rendered_at': provenance['rendered_at'],
420
+ 'gl_renderer': provenance['gl_renderer'],
421
+ 'gl_version': provenance['gl_version'],
422
+ }
423
+
424
+
425
+ def default_out_dir() -> str:
426
+ """Persistent directory of the latest per-view renders/metadata/diffs. Kept
427
+ across runs so re-rendering one scene (``--only``) leaves the others intact;
428
+ the report is assembled from whatever it holds."""
429
+ return os.path.join(os.path.dirname(default_report_path()), 'renders')
430
+
431
+
432
+ def resolve_model_url(spec: gltf_demos.SceneSpec,
433
+ parthenon: str | None = None) -> tuple[str | None, bool]:
434
+ """The source the viewer should load, and whether it is a URL.
435
+
436
+ A Khronos sample resolves to its **http(s) URL** so the viewer loads it through
437
+ the security-hardened resolver -- an untrusted remote resource, exactly as an
438
+ end user would -- rather than as a trusted local file. The ``.glb`` variant is
439
+ used when it exists (probed via the shared cache), else the multi-file ``.gltf``
440
+ URL, whose external buffers/images the resolver fetches same-origin. A scene
441
+ that names its own URL is handed over as it stands, and the local Parthenon
442
+ build stays a path. Returns ``(source, is_url)`` or ``(None, False)``.
443
+ """
444
+ source, is_local = gltf_demos.resolve_source(spec, parthenon)
445
+ if is_local:
446
+ return (source if source and os.path.exists(source) else None), False
447
+ if resolver.is_url(source):
448
+ return source, True
449
+ name = cast(str, source) # a non-local scene resolves to a sample name
450
+ glb_url = gltf.sample_model_url(name)
451
+ try:
452
+ _cached_url_path(glb_url, resolver._default_cache_dir()) # confirm it exists
453
+ return glb_url, True
454
+ except Exception:
455
+ return '%s/%s/glTF/%s.gltf' % (gltf.SAMPLE_MODELS_BASE, name, name), True
456
+
457
+
458
+ def render_scenes(scenes: list[gltf_demos.SceneSpec], out_dir: str, baseline_root: str,
459
+ parthenon: str | None, args: argparse.Namespace,
460
+ provenance: dict[str, str]) -> None:
461
+ """Render each selected view to ``out_dir`` (``<slug>.png`` + ``<slug>.json``),
462
+ blessing to ``baseline_root`` when asked. Overwrites only the rendered views, so
463
+ prior renders of other scenes survive for the report."""
464
+ import shutil
465
+ bless_names = None if args.bless in (None, []) else set(args.bless)
466
+ bless_all = args.bless == []
467
+ os.makedirs(out_dir, exist_ok=True)
468
+ if args.bless is not None:
469
+ os.makedirs(baseline_root, exist_ok=True)
470
+ for spec in scenes:
471
+ url, _is_url = resolve_model_url(spec, parthenon)
472
+ for camera in spec.camera_ids():
473
+ slug = spec.slug(camera)
474
+ new_path = os.path.join(out_dir, slug + '.png')
475
+ if url is None:
476
+ print(' ERROR %s (model unavailable)' % slug)
477
+ continue
478
+ ok, stats = render_view(spec, camera, url, new_path, args.size,
479
+ args.frames, args.delay, None)
480
+ if not ok:
481
+ print(' FAIL %s (render failed)' % slug)
482
+ continue
483
+ metadata = _view_metadata(spec, camera, url, args, provenance, stats)
484
+ do_bless = bless_all or (bless_names and spec.name in bless_names)
485
+ try:
486
+ with open(os.path.join(out_dir, slug + '.json'), 'w') as fh:
487
+ json.dump(metadata, fh, indent=2)
488
+ if do_bless:
489
+ shutil.copyfile(new_path, os.path.join(baseline_root, slug + '.png'))
490
+ with open(os.path.join(baseline_root, slug + '.json'), 'w') as fh:
491
+ json.dump(metadata, fh, indent=2)
492
+ except OSError:
493
+ pass
494
+ print(' %-5s %s' % ('BLESS' if do_bless else 'OK', slug))
495
+
496
+
497
+ def build_report(out_dir: str, baseline_root: str, report_path: str, tolerance: float,
498
+ diff_threshold: int) -> int:
499
+ """Assemble the HTML report from whatever renders are present in ``out_dir``
500
+ (each ``<slug>.json`` + ``<slug>.png``), diffed against the baseline. Decoupled
501
+ from rendering, so re-rendering one scene then rebuilding keeps every other row.
502
+ Returns the regression count."""
503
+ import shutil
504
+ from OpenGLContext.testing.report_generator import TestReportGenerator
505
+ gen = TestReportGenerator(title='glTF Demo Regression -- our reference vs new')
506
+ report_dir = os.path.dirname(os.path.abspath(report_path))
507
+
508
+ def stage(src: str | None, name: str) -> str | None:
509
+ """Return an image for the report to link, guaranteeing it lives under the
510
+ report directory. The result and diff already do; the baseline and Khronos
511
+ upstream live in other trees, so copy those in under ``name`` -- then the
512
+ report (which links images relative to itself) copies elsewhere as one
513
+ self-contained bundle. Missing sources return None."""
514
+ if not (src and os.path.exists(src)):
515
+ return None
516
+ if os.path.abspath(src).startswith(report_dir + os.sep):
517
+ return src
518
+ dst = os.path.join(out_dir, name)
519
+ try:
520
+ shutil.copyfile(src, dst)
521
+ return dst
522
+ except OSError:
523
+ return src
524
+
525
+ regressions = 0
526
+ slugs = sorted(f[:-5] for f in os.listdir(out_dir) if f.endswith('.json'))
527
+ for slug in slugs:
528
+ try:
529
+ meta = json.load(open(os.path.join(out_dir, slug + '.json')))
530
+ except Exception:
531
+ meta = {}
532
+ scene = meta.get('scene', slug.split('__')[0])
533
+ new_path = os.path.join(out_dir, slug + '.png')
534
+ baseline_path = os.path.join(baseline_root, slug + '.png')
535
+ diff_path = os.path.join(out_dir, slug + '_diff.png')
536
+ upstream_path = None
537
+ try:
538
+ upstream_path = gltf.cache_reference_screenshot(scene)
539
+ except Exception:
540
+ pass
541
+ status, stats, stderr = 'pass', None, ''
542
+ if not os.path.exists(new_path):
543
+ status, stderr = 'error', 'no render on disk'
544
+ elif not os.path.exists(baseline_path):
545
+ status, stderr = 'skip', 'no baseline -- bless after reviewing'
546
+ else:
547
+ result, stats = compare(baseline_path, new_path, diff_path, diff_threshold)
548
+ if is_regression(result, tolerance):
549
+ status, regressions = 'fail', regressions + 1
550
+ stderr = 'REGRESSION: %s' % result
551
+ else:
552
+ stderr = str(result)
553
+ gen.add_test({
554
+ 'test_name': '%s -- %s' % (slug, meta.get('description', '')),
555
+ 'status': status,
556
+ 'reference_image': stage(baseline_path, slug + '_baseline.png'),
557
+ 'result_image': new_path if os.path.exists(new_path) else None,
558
+ 'diff_image': diff_path if (stats and os.path.exists(diff_path)) else None,
559
+ 'upstream_image': stage(upstream_path, slug + '_upstream.png'),
560
+ 'comparison_stats': stats,
561
+ 'metadata': meta,
562
+ 'stderr': stderr,
563
+ 'duration': 0.0,
564
+ })
565
+ os.makedirs(os.path.dirname(os.path.abspath(report_path)), exist_ok=True)
566
+ # Link the renders rather than base64-embedding them: a run covers dozens of
567
+ # views x four images, and embedding balloons the single HTML to hundreds of
568
+ # MB. save() links each image relative to the report; with every image staged
569
+ # under out_dir, the report plus its render directory copy elsewhere as one
570
+ # self-contained bundle (as tests/report.html links its images).
571
+ gen.save(report_path, embed_images=False)
572
+ return regressions
573
+
574
+
575
+ def run(args: argparse.Namespace) -> int:
576
+ baseline_root = args.baseline_root or default_baseline_root()
577
+ out_dir = args.out_dir or default_out_dir()
578
+ os.makedirs(out_dir, exist_ok=True)
579
+ parthenon = args.parthenon or gltf_demos.find_parthenon()
580
+ report = args.report or default_report_path()
581
+
582
+ if not getattr(args, 'report_only', False):
583
+ provenance = _provenance()
584
+ print('Baselines: %s' % baseline_root)
585
+ print('Renders: %s' % out_dir)
586
+ print('Provenance: git %s, %s, %s' % (
587
+ provenance['git'], provenance['rendered_at'], provenance['gl_renderer']))
588
+ render_scenes(select_scenes(args.only), out_dir, baseline_root,
589
+ parthenon, args, provenance)
590
+
591
+ regressions = build_report(out_dir, baseline_root, report,
592
+ args.tolerance, args.diff_threshold)
593
+ print('\nReport: %s' % report)
594
+ if regressions:
595
+ print('%d regression(s) detected.' % regressions)
596
+ return 1 if regressions else 0
597
+
598
+
599
+ def _parse_size(text: str) -> tuple[int, int]:
600
+ w, _, h = text.lower().partition('x')
601
+ return (int(w), int(h))
602
+
603
+
604
+ def build_parser(prog: str = 'oglc-gltf-regression') -> argparse.ArgumentParser:
605
+ p = argparse.ArgumentParser(prog=prog, description=__doc__,
606
+ formatter_class=argparse.RawDescriptionHelpFormatter)
607
+ p.add_argument('--bless', nargs='*', metavar='NAME', default=None,
608
+ help='promote the new renders to baselines (after review). With '
609
+ 'no NAME, blesses every scene run this pass; otherwise only '
610
+ 'the named scenes.')
611
+ p.add_argument('--only', action='append', metavar='NAME',
612
+ help='render only this scene (repeatable)')
613
+ p.add_argument('--baseline-root', metavar='DIR',
614
+ help='baseline directory (default: sibling reference-images/gltf_baseline '
615
+ 'or $OPENGLCONTEXT_GLTF_BASELINE)')
616
+ p.add_argument('--parthenon', metavar='GLB',
617
+ help='path to a local Parthenon .glb (default: auto-detect sibling)')
618
+ p.add_argument('--report', metavar='HTML', help='report output path')
619
+ p.add_argument('--out-dir', metavar='DIR',
620
+ help='persistent directory of per-view renders/metadata/diffs '
621
+ '(default: tests/gltf_regression/renders)')
622
+ p.add_argument('--report-only', action='store_true',
623
+ help='rebuild the report from the existing renders directory '
624
+ 'without rendering anything')
625
+ p.add_argument('--size', type=_parse_size, default=DEFAULT_SIZE, metavar='WxH',
626
+ help='render size (default %dx%d)' % DEFAULT_SIZE)
627
+ p.add_argument('--frames', type=int, default=DEFAULT_FRAMES,
628
+ help='frames to render before capture (default %d)' % DEFAULT_FRAMES)
629
+ p.add_argument('--delay', type=float, default=DEFAULT_DELAY,
630
+ help='settle seconds before capture (default %.2f)' % DEFAULT_DELAY)
631
+ p.add_argument('--tolerance', type=float, default=DEFAULT_TOLERANCE, metavar='PCT',
632
+ help='percent of pixels allowed to differ (default %.1f)' % DEFAULT_TOLERANCE)
633
+ p.add_argument('--diff-threshold', type=int, default=DIFF_THRESHOLD, metavar='N',
634
+ help='per-channel delta a pixel must exceed to count as different '
635
+ '(default %d)' % DIFF_THRESHOLD)
636
+ return p
637
+
638
+
639
+ def main(argv: list[str] | None = None) -> int:
640
+ args = build_parser().parse_args(argv)
641
+ return run(args)
642
+
643
+
644
+ if __name__ == '__main__': # pragma: no cover - CLI entry point
645
+ sys.exit(main())
@@ -0,0 +1,30 @@
1
+ #! /usr/bin/env python
2
+ """``oglc-gltf`` -- the former glTF-only viewer, now an alias for ``oglc-view``.
3
+
4
+ There is one viewer, and it works out the format from the source, so the command
5
+ that opens a ``.glb`` is the command that opens a ``.wrl``. Every option this
6
+ command took is still accepted, unchanged; see :mod:`OpenGLContext.bin.view`.
7
+
8
+ This name is kept for one release cycle so existing scripts and documentation
9
+ keep working, and prints where to go instead. It will then be removed.
10
+ """
11
+ import sys
12
+ from typing import Any, Optional
13
+
14
+ from OpenGLContext.bin.view import main as view_main
15
+
16
+ #: What to type now. Named here so the notice and the replacement cannot drift.
17
+ REPLACEMENT = 'oglc-view'
18
+
19
+
20
+ def main(argv: Optional[list] = None) -> Any:
21
+ """Run ``oglc-view``, having said that is what this now is."""
22
+ sys.stderr.write(
23
+ "oglc-gltf is deprecated and will be removed; use %s instead "
24
+ "(same options, and it opens every format).\n" % REPLACEMENT)
25
+ sys.stderr.flush()
26
+ return view_main(argv, prog='oglc-gltf')
27
+
28
+
29
+ if __name__ == "__main__": # pragma: no cover - CLI entry point
30
+ main()