├── LICENCE.txt ├── README.md ├── arkit ├── art.scnassets │ └── dragon │ │ ├── dragon.scn │ │ ├── dragon_DIFFUSE.png │ │ ├── fire_PARTICLE.png │ │ └── main.scn ├── assets │ └── dragon │ │ ├── dragon.scn │ │ ├── dragon_DIFFUSE.png │ │ ├── fire_PARTICLE.png │ │ └── main.scn ├── main.py └── myDebugToolKit.py ├── rshell └── rshell.py ├── webvr ├── Gestures.py ├── main.py └── templates │ └── control.html └── webvr_embedded ├── main.py ├── web └── static │ ├── index.html │ ├── js │ ├── aframe-stereo-component.js │ ├── aframe-stereo-component.min.js │ ├── aframe.js │ ├── aframe.min.js │ └── debug_utils.js │ ├── stream │ └── MaryOculus.mp4 │ ├── video_debug.html │ └── vr_embedded.html └── wkwebview.py /LICENCE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pythonista 2 | Here you can find things about Pythonista (a complete development environment for writing Python™ scripts on your iPad or iPhone). 3 | 4 | ## webVR 5 | As it's currently difficult to watch web VR content in full screen on iOS devices, i created this tool. 6 | It has some hidden features ;o) 7 | 8 |  9 | 10 | * Some gestures allow you to adjust the presentation (vertical offset and scaling). 11 | 12 | * A long press allows you to deactivate momentarily the previous gestures and gives access to the web view content through touch events. 13 | 14 | * You can change the current content using a remote web browser connected to your iOS device. 15 | 16 | * The tool detects sketchfab content and activates automatically its VR view. A same mechanism exists for A-frame content but it's more experimental. 17 | 18 | ## ARKit 19 | 20 | My first attempt to use the ARKit framework within iOS 12.1 (and Pythonista 3.3)... 21 | **!! Caution, slippery floor !!** 22 | **Work in progress** 23 | 24 |  25 | 26 | 27 | ## RShell 28 | 29 | If you want to access to your stash opened on your iOS device from your desktop, you can use this script. 30 | You have to download it on your bin stash directory and call it from the stash prompt simply by using rshell with -l as argument. 31 | 32 | ``` 33 | stash> rshell -l 34 | ``` 35 | 36 | Then, on your desktop, install this script and call it with the ip address of your iOS device as argument and let the magic happen... 37 | ``` 38 | $> rshell 192.168.0.40 39 | ``` 40 | 41 | Personally, on my desktop, I prefer to call it from my PyCharm IDE because its console is smarter than the standard terminal to use the backspace key for example... 42 | 43 |  44 | -------------------------------------------------------------------------------- /arkit/art.scnassets/dragon/dragon.scn: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Brun0oO/Pythonista/e3ae57b7894dfc328f075f61fc51fbc8697cfaa3/arkit/art.scnassets/dragon/dragon.scn -------------------------------------------------------------------------------- /arkit/art.scnassets/dragon/dragon_DIFFUSE.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Brun0oO/Pythonista/e3ae57b7894dfc328f075f61fc51fbc8697cfaa3/arkit/art.scnassets/dragon/dragon_DIFFUSE.png -------------------------------------------------------------------------------- /arkit/art.scnassets/dragon/fire_PARTICLE.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Brun0oO/Pythonista/e3ae57b7894dfc328f075f61fc51fbc8697cfaa3/arkit/art.scnassets/dragon/fire_PARTICLE.png -------------------------------------------------------------------------------- /arkit/art.scnassets/dragon/main.scn: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Brun0oO/Pythonista/e3ae57b7894dfc328f075f61fc51fbc8697cfaa3/arkit/art.scnassets/dragon/main.scn -------------------------------------------------------------------------------- /arkit/assets/dragon/dragon.scn: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Brun0oO/Pythonista/e3ae57b7894dfc328f075f61fc51fbc8697cfaa3/arkit/assets/dragon/dragon.scn -------------------------------------------------------------------------------- /arkit/assets/dragon/dragon_DIFFUSE.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Brun0oO/Pythonista/e3ae57b7894dfc328f075f61fc51fbc8697cfaa3/arkit/assets/dragon/dragon_DIFFUSE.png -------------------------------------------------------------------------------- /arkit/assets/dragon/fire_PARTICLE.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Brun0oO/Pythonista/e3ae57b7894dfc328f075f61fc51fbc8697cfaa3/arkit/assets/dragon/fire_PARTICLE.png -------------------------------------------------------------------------------- /arkit/assets/dragon/main.scn: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Brun0oO/Pythonista/e3ae57b7894dfc328f075f61fc51fbc8697cfaa3/arkit/assets/dragon/main.scn -------------------------------------------------------------------------------- /arkit/main.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | import objc_util 3 | 4 | from objc_util import * 5 | import ui 6 | import os 7 | import sys 8 | 9 | #from myDebugToolKit import * 10 | import time 11 | from enum import IntFlag 12 | 13 | 14 | load_framework('SceneKit') 15 | load_framework('ARKit') 16 | 17 | # Some 'constants' used by ARkit 18 | # But i can't transfer them to the ARKit framework, why ????? 19 | 20 | class ARWorldAlignment(IntFlag): 21 | ARWorldAlignmentGravity = 0 22 | ARWorldAlignmentGravityAndHeading = 1 23 | ARWorldAlignmentCamera = 2 24 | 25 | class ARPlaneDetection(IntFlag): 26 | ARPlaneDetectionNone = 0 27 | ARPlaneDetectionHorizontal = 1 << 0 28 | ARPlaneDetectionVertical = 1 << 1 29 | 30 | # Work In Progress here, I'm deciphering the arkit constants... 31 | #class ARSCNDebugOption(IntFlag): 32 | # ARSCNDebugOptionNone = 0 33 | # ARSCNDebugOptionShowWorldOrigin = int("ffffffff80000000", 16) 34 | # ARSCNDebugOptionShowFeaturePoints = int("ffffffff40000000", 16) 35 | 36 | class ARSessionRunOptions(IntFlag): 37 | ARSessionRunOptionsNone = 0 38 | ARSessionRunOptionResetTracking = 1 << 0 39 | ARSessionRunOptionRemoveExistingAnchors = 1 << 1 40 | 41 | 42 | NSError = ObjCClass('NSError') 43 | SCNScene = ObjCClass('SCNScene') 44 | ARSCNView = ObjCClass('ARSCNView') 45 | ARWorldTrackingConfiguration = ObjCClass('ARWorldTrackingConfiguration') 46 | ARSession = ObjCClass('ARSession') 47 | UIViewController = ObjCClass('UIViewController') 48 | ARPlaneAnchor = ObjCClass('ARPlaneAnchor') 49 | 50 | 51 | 52 | # I should refactor te following line in a class but I need to learn more the create_objcc_class function 53 | sceneview = None 54 | 55 | # Here some set up functions used by the main class 56 | def createSampleScene(): 57 | # an empty scene 58 | scene = SCNScene.scene() 59 | return scene 60 | 61 | def setDebugOptions(arscn): 62 | # Work In Progress Here, I'm trying to decipher the arkit constants... 63 | #val = ARSCNDebugOption.ARSCNDebugOptionShowWorldOrigin | ARSCNDebugOption.ARSCNDebugOptionShowFeaturePoints 64 | val = int("fffffffffc000000", 16) # this value is a combination of ShowWorldOrigin and ShowFeaturePoints flags, but I can't isolate each flags.... 65 | print('Before calling setDebugOptions_(%s) : debugOptions=%s' %(hex(val), hex(arscn.debugOptions()))) 66 | arscn.setDebugOptions_(val) 67 | print('After calling setDebugOptions_(%s) : debugOptions=%s' % (hex(val),hex(arscn.debugOptions()))) 68 | 69 | 70 | def createARSceneView(x, y, w, h, debug=True): 71 | v = ARSCNView.alloc().initWithFrame_((CGRect(CGPoint(x, y), CGSize(w, h)))) 72 | v.setShowsStatistics_(debug) # I love statistics... 73 | return v 74 | 75 | # Some callback definitions used by create_objc_class 76 | def CustomViewController_touchesBegan_withEvent_(_self, _cmd, _touches, event): 77 | touches = ObjCInstance(_touches) 78 | for t in touches: 79 | loc = t.locationInView_(sceneview) 80 | sz = ui.get_screen_size() 81 | print(loc) 82 | 83 | @on_main_thread 84 | def runARSession(arsession): 85 | arconfiguration = ARWorldTrackingConfiguration.alloc().init() 86 | arconfiguration.setPlaneDetection_ (ARPlaneDetection.ARPlaneDetectionHorizontal) 87 | arconfiguration.setWorldAlignment_(ARWorldAlignment.ARWorldAlignmentGravity) # I do not use ARWorldAlignmentGravityAndHeading anymore because on my device, sometimes it fails to initialize the ar session because of an unitialized sensor (error 102). I think my magnetic phone casing plays tricks on me... 88 | 89 | arsession.runWithConfiguration_options_(arconfiguration, ARSessionRunOptions.ARSessionRunOptionResetTracking | ARSessionRunOptions.ARSessionRunOptionRemoveExistingAnchors ) 90 | 91 | time.sleep(0.5) # Let the system breathe ;o) Ok, that's the workarround I found to retrieve the ar session configuration (otherwise I got None).... 92 | print('configuration',arsession.configuration()) # Very usefull for the debuging (at least for me !) 93 | 94 | 95 | def CustomViewController_viewWillAppear_(_self, _cmd, animated): 96 | return 97 | 98 | def CustomViewController_viewWillDisappear_(_self, _cmd, animated): 99 | session = sceneview.session() 100 | session.pause() 101 | 102 | def MyARSCNViewDelegate_renderer_didAdd_for_(_self, _cmd, scenerenderer, node, anchor): 103 | if not isinstance(anchor, (ARPlaneAnchor)): 104 | return 105 | # to be implemented... 106 | 107 | 108 | def MyARSCNViewDelegate_session_didFailWithError_(_self,_cmd,_session,_error): 109 | print('error',_error,_cmd,_session) 110 | err_obj=ObjCInstance(_error) 111 | print(err_obj) # Again, very usefull for the debuging... 112 | 113 | # The main class... 114 | class MyARView(ui.View): 115 | def __init__(self): 116 | super().__init__(self) 117 | 118 | 119 | @on_main_thread 120 | def initialize(self): 121 | global sceneview 122 | self.flex = 'WH' 123 | 124 | screen = ui.get_screen_size() 125 | 126 | # set up the scene 127 | scene = createSampleScene() 128 | 129 | # set up the ar scene view delegate 130 | methods = [MyARSCNViewDelegate_renderer_didAdd_for_,MyARSCNViewDelegate_session_didFailWithError_] 131 | protocols = ['ARSCNViewDelegate'] 132 | MyARSCNViewDelegate = create_objc_class('MyARSCNViewDelegate', NSObject, methods=methods, protocols=protocols) 133 | delegate = MyARSCNViewDelegate.alloc().init() 134 | 135 | # set up the ar scene view 136 | sceneview = createARSceneView(0, 0, screen.width, screen.height) 137 | sceneview.scene = scene 138 | sceneview.setDelegate_(delegate) 139 | 140 | 141 | # set up the custom view controller 142 | methods = [CustomViewController_touchesBegan_withEvent_, CustomViewController_viewWillAppear_, CustomViewController_viewWillDisappear_] 143 | protocols = [] 144 | CustomViewController = create_objc_class('CustomViewController', UIViewController, methods=methods, protocols=protocols) 145 | cvc = CustomViewController.alloc().init() 146 | cvc.view = sceneview 147 | 148 | 149 | # internal kitchen 150 | self_objc = ObjCInstance(self) 151 | self_objc.nextResponder().addChildViewController_(cvc) 152 | self_objc.addSubview_(sceneview) 153 | cvc.didMoveToParentViewController_(self_objc) 154 | 155 | # here, we try... 156 | runARSession(sceneview.session()) # I call here this function because I'm trying to find the best place to run the ar session... 157 | 158 | setDebugOptions(sceneview) # I call here this function because I'm trying to find the best place to set the debuging options.... 159 | 160 | def will_close(self): 161 | session = sceneview.session() 162 | session.pause() 163 | 164 | 165 | 166 | if __name__ == '__main__': 167 | v = MyARView() 168 | v.present('full_screen', hide_title_bar=True, orientations=['portrait']) 169 | v.initialize() 170 | -------------------------------------------------------------------------------- /arkit/myDebugToolKit.py: -------------------------------------------------------------------------------- 1 | # My debug toolkit... 2 | # It allows me to copy some usefull informations to the clipboard. 3 | # Then, I use the free ios app ´CloudClip' to share these informations with my desktop. 4 | from objc_util import * 5 | from objc_util import class_copyMethodList, method_getName, sel_getName, free,ObjCInstanceMethodProxy, objc_getClass 6 | 7 | import clipboard 8 | import inspect 9 | from datetime import datetime 10 | import sound 11 | from inspect import signature 12 | 13 | def info(obj, obj_name, private=False): 14 | if obj_name in globals(): 15 | beep() 16 | callerframerecord = inspect.stack()[1] 17 | frame = callerframerecord[0] 18 | info = inspect.getframeinfo(frame) 19 | filename = info.filename 20 | filename = filename[filename.find('Documents'):] 21 | function = info.function 22 | lineno = info.lineno 23 | content = '' 24 | if inspect.ismodule(obj) or inspect.isclass(obj): 25 | methods = dir(obj) 26 | for method in methods: 27 | to_be_added = True 28 | if not private: 29 | to_be_added = not(method.startswith('__') and method.endswith('__')) 30 | if to_be_added: 31 | if content != '': 32 | content += '\n' 33 | content += '#\t'+ method 34 | content = '# List of its public method(s) :\n'+content 35 | elif inspect.ismethod(obj) or inspect.isfunction(obj): 36 | content = str(signature(obj)) 37 | elif isinstance(obj, (ObjCClass)) or isinstance(obj, (ObjCInstance)): 38 | methods = inspectObjc(obj) 39 | for method in methods: 40 | if content != '': 41 | content += '\n' 42 | content += '#\t'+method 43 | content = '# List of its method(s) :\n'+content 44 | timestamp = str(datetime.now()) 45 | text = "# timestamp = %s\n" % timestamp 46 | text += "# filename = %s\n" % filename 47 | text += "# function = %s\n" % function 48 | text += "# line number = %d\n" % lineno 49 | text += "# Inspected object = '%s'\n" % obj_name 50 | text += "# Type(object) = %s\n" % type(obj) 51 | text += content 52 | clipboard.set(text) 53 | 54 | def inspectObjc(obj): 55 | if isinstance(obj, (ObjCClass)): 56 | name = obj.class_name.decode('utf-8') 57 | elif isinstance(obj, (ObjCInstance)): 58 | name = obj._get_objc_classname().decode('utf-8') 59 | elif isinstance(obj, (str)): 60 | name = obj 61 | else: 62 | # not supported ! 63 | return [] 64 | 65 | py_methods = [] 66 | c = ObjCClass(name) 67 | num_methods = c_uint(0) 68 | method_list_ptr = class_copyMethodList(c.ptr, byref(num_methods)) 69 | for i in range(num_methods.value): 70 | selector = method_getName(method_list_ptr[i]) 71 | sel_name = sel_getName(selector) 72 | if not isinstance(sel_name, str): 73 | sel_name = sel_name.decode('ascii') 74 | py_method_name = sel_name.replace(':','_') 75 | if '.' not in py_method_name: 76 | py_methods.append(py_method_name) 77 | 78 | free(method_list_ptr) 79 | return py_methods 80 | 81 | def beep(): 82 | sound.play_effect('Ding_3', volume=1.0) 83 | -------------------------------------------------------------------------------- /rshell/rshell.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | # coding: utf-8 3 | 4 | # RSHELL is a remote interactive access to Stash (a bash like shell for Pythonista). 5 | # It's based on the Guido Wesdorp's work for its remote interactive shell (ripshell). 6 | 7 | from __future__ import print_function 8 | import socket 9 | import sys 10 | import traceback 11 | import thread 12 | import time 13 | 14 | 15 | 16 | class config: 17 | server_ip = '0.0.0.0' 18 | port = 10101 19 | 20 | __version__ = '1.0' 21 | 22 | # get the local ip address of the current device 23 | def get_local_ip_addr(): 24 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # make a socket object 25 | s.connect(('8.8.8.8', 80)) # connect to google ;o) 26 | ip = s.getsockname()[0] # get our IP address from the socket 27 | s.close() # close the socket 28 | return ip # and return the IP address 29 | 30 | 31 | class STDFilePointers: 32 | def __init__(self, conn): 33 | self.conn = conn 34 | 35 | def write(self, s): 36 | self.conn.send(s) 37 | 38 | def read(self, l): 39 | r = self.conn.recv(l) 40 | if r: 41 | return r 42 | return ' ' 43 | 44 | def readlines(self): 45 | data = [] 46 | while 1: 47 | c = self.read(1) 48 | if c == '\n': 49 | line = ''.join(data) 50 | # well..here a little hack in order to intercept a quit command from the client.... 51 | if line == 'quit': 52 | raise SystemExit('quit') 53 | line += '\n' 54 | return line 55 | data.append(c) 56 | 57 | # 58 | # The server 59 | # 60 | # Launch Pythonista on your iOS device 61 | # Execute launch_stash.py 62 | # Type rshell.py 63 | # Look at the printed ip address on the console ;o) 64 | # 65 | class RSHELLServer: 66 | banner = ( 'RShell Server v%s\n' 67 | 'Type "help", "version" for more information.\n\n' 68 | '**To stop the server: quit\n' 69 | % ( __version__) 70 | ) 71 | 72 | # open a socket and start waiting for connections 73 | def __init__(self, config): 74 | self.config = config 75 | self.sock = s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 76 | try: 77 | s.bind((config.server_ip, config.port)) 78 | s.listen(1) 79 | while 1: 80 | conn, addr = s.accept() 81 | print('Connection from', addr) 82 | self.handle(conn, addr) 83 | conn.close() 84 | print('Connection closed') 85 | finally: 86 | print('Closing') 87 | s.close() 88 | 89 | # handle a new connection 90 | def handle(self, conn, addr): 91 | backup_stdin = sys.stdin 92 | backup_stdout = sys.stdout 93 | backup_stderr = sys.stderr 94 | stdfps = STDFilePointers(conn) 95 | sys.stdin = stdfps 96 | sys.stdout = stdfps 97 | sys.stderr = stdfps 98 | try: 99 | try: 100 | command = conn.recv(1) 101 | # dispatch depending on command (first char sent should be '-' 102 | # for the interactive interpreter loop, 'x' for executing code) 103 | if command == '-': 104 | self.interpreterloop(conn, addr) 105 | else: 106 | print('Unexpected input, exiting...') 107 | except SystemExit as e: 108 | # raise a SystemExit with message 'quit' to stop the server 109 | # from a client 110 | if str(e) == 'quit': 111 | print('Stopping server') # this string will be intercepted by the client... 112 | raise # kill the server 113 | print('SystemExit') 114 | except: 115 | exc, e, tb = sys.exc_info() 116 | try: 117 | print('%s - %s' % (exc, e)) 118 | print('\n'.join(traceback.format_tb(tb))) 119 | except: 120 | pass 121 | del tb 122 | print('Exception:', exc, '-', e, file=sys.__stdout__) 123 | finally: 124 | sys.stdin = backup_stdin 125 | sys.stdout = backup_stdout #sys.__stdout__ 126 | sys.stderr = backup_stderr #sys.__stderr__ 127 | 128 | # interpreter loop 129 | def interpreterloop(self, conn, addr): 130 | _stash = globals()['_stash'] 131 | _, current_state = _stash.runtime.get_current_worker_and_state() 132 | print(self.banner) 133 | while (1): 134 | _stash(sys.stdin.readlines(), persistent_level=1) 135 | time.sleep(0.5) 136 | 137 | # 138 | # The Client 139 | # 140 | # On your desktop, execute rshell.py ###.###.###.### (with the ip address of your server) 141 | # Then, you can type stash commands on the console and wait for their completion on your iOS device. 142 | # Their output is automatically displayed on your console. 143 | # Your desktop and your iOS device need to be on the same local network. 144 | # 145 | class RSHELLClient: 146 | # connect to the server and start the session 147 | def __init__(self, server_ip, config): 148 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 149 | 150 | try: 151 | s.settimeout(10.0) # 10 second timeout for connect 152 | try: 153 | s.connect((server_ip, config.port)) 154 | except socket.error as msg: 155 | print("Couldn't connect with the socket-server: %s" % msg) 156 | sys.exit(1) 157 | s.settimeout(None) 158 | 159 | self.interpreterloop(s) 160 | except SystemExit as e: 161 | if str(e) == 'quit': 162 | print('Stopping client') 163 | raise # kill the client 164 | print('SystemExit') 165 | finally: 166 | s.close() 167 | 168 | def interpreterloop(self, sock): 169 | sock.send(b'-') # tell the server we want to get a prompt 170 | thread.start_new_thread(self.readloop, (sock,)) 171 | self.writeloop(sock) 172 | 173 | def readloop(self, sock): 174 | while 1: 175 | try: 176 | sock.send(sys.stdin.read(1)) 177 | except socket.error: 178 | return 179 | 180 | def writeloop(self, sock): 181 | while 1: 182 | c = sock.recv(1) 183 | if not c: 184 | break 185 | 186 | # try to decode ANSI color sequences 187 | # need to use a buffer and apply a string replace before display the string 188 | end_main_loop = False 189 | buffer = "" 190 | while c != '\n': 191 | buffer += c 192 | c = sock.recv(1) 193 | if not c: 194 | end_main_loop = True 195 | break 196 | if not end_main_loop: 197 | # here an other hack in order to intercept the end of the server 198 | if buffer == "Stopping server": # sent from the server 199 | raise SystemExit('quit') # kill the client 200 | buffer += '\n' 201 | 202 | 203 | buffer = buffer.replace('\xc2\x9b', '\033[') 204 | 205 | sys.stdout.write(buffer) 206 | sys.stdout.flush() 207 | 208 | if end_main_loop: 209 | break 210 | 211 | if __name__ == '__main__': 212 | from optparse import OptionParser 213 | usage = ('usage: %prog [-p] -l | remote_server\n' 214 | '\n' 215 | 'RSHELL allows you to access STASH running on a remote\n' 216 | 'iOS device over an unencrypted socket. Use RSHELL in listening mode\n' 217 | '(use -l) or to connect to a RSHELL server') 218 | parser = OptionParser(usage) 219 | parser.add_option('-l', '--listen', action='store_true', dest='listen', default=False, 220 | help='Listen for a remote connection' ) 221 | parser.add_option('-p', '--port', action='store', type='int', dest='port', default=config.port, 222 | help='port for rshell to use') 223 | (options, args) = parser.parse_args() 224 | if options.port <= 0 or options.port >= 65535: 225 | parser.error('Invalid port specified') 226 | if options.listen: 227 | if len(args) > 0: 228 | parser.error("-l cannot use be specified with a hostname") 229 | local_ip = get_local_ip_addr() 230 | print('RShell server listening on %s:%d' % (local_ip, options.port)) 231 | config.port = options.port 232 | # blocks until done 233 | RSHELLServer(config) 234 | 235 | elif len(args) == 1: 236 | config.server_ip = args[0] 237 | config.port = options.port 238 | # blocks until done 239 | RSHELLClient(config.server_ip, config) 240 | else: 241 | parser.error('Invalid arguments specified.') 242 | -------------------------------------------------------------------------------- /webvr/Gestures.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | # Gestures for Pythonista 4 | # Copied from https://github.com/mikaelho/pythonista-gestures 5 | # As its author said, this is a convenience class for enabling gestures in Pythonista ui applications, 6 | # including built-in views. Main intent here has been to make them Python friendly, 7 | # hiding all the Objective-C stuff. 8 | 9 | import ui 10 | from objc_util import * 11 | import uuid 12 | 13 | # https://developer.apple.com/library/prerelease/ios/documentation/UIKit/Reference/UIGestureRecognizer_Class/index.html#//apple_ref/occ/cl/UIGestureRecognizer 14 | 15 | class Gestures(): 16 | 17 | TAP = b'UITapGestureRecognizer' 18 | PINCH = b'UIPinchGestureRecognizer' 19 | ROTATION = b'UIRotationGestureRecognizer' 20 | SWIPE = b'UISwipeGestureRecognizer' 21 | PAN = b'UIPanGestureRecognizer' 22 | SCREEN_EDGE_PAN = b'UIScreenEdgePanGestureRecognizer' 23 | LONG_PRESS = b'UILongPressGestureRecognizer' 24 | 25 | POSSIBLE = 0 26 | BEGAN = 1 27 | RECOGNIZED = 1 28 | CHANGED = 2 29 | ENDED = 3 30 | CANCELLED = 4 31 | FAILED = 5 32 | 33 | RIGHT = 1 34 | LEFT = 2 35 | UP = 4 36 | DOWN = 8 37 | 38 | EDGE_NONE = 0 39 | EDGE_TOP = 1 40 | EDGE_LEFT = 2 41 | EDGE_BOTTOM = 4 42 | EDGE_RIGHT = 8 43 | EDGE_ALL = 15 44 | 45 | def __init__(self, retain_global_reference = True): 46 | self.buttons = {} 47 | self.views = {} 48 | self.recognizers = {} 49 | self.actions = {} 50 | if retain_global_reference: 51 | retain_global(self) 52 | 53 | # Friendly delegate functions 54 | 55 | def recognize_simultaneously_default(gr_name, other_gr_name): 56 | return False 57 | self.recognize_simultaneously = recognize_simultaneously_default 58 | 59 | def fail_default(gr_name, other_gr_name): 60 | return False 61 | self.fail = fail_default 62 | 63 | def fail_other_default(gr_name, other_gr_name): 64 | return False 65 | self.fail_other = fail_other_default 66 | 67 | # ObjC delegate functions 68 | 69 | def simplify(func, gr, other_gr): 70 | gr_o = ObjCInstance(gr) 71 | other_gr_o = ObjCInstance(other_gr) 72 | if (gr_o.view() != other_gr_o.view()): 73 | return False 74 | gr_name = gr_o._get_objc_classname() 75 | other_gr_name = other_gr_o._get_objc_classname() 76 | return func(gr_name, other_gr_name) 77 | 78 | # Recognize simultaneously 79 | 80 | def gestureRecognizer_shouldRecognizeSimultaneouslyWithGestureRecognizer_(_self, _sel, gr, other_gr): 81 | return self.objc_should_recognize_simultaneously(self.recognize_simultaneously, gr, other_gr) 82 | 83 | def objc_should_recognize_simultaneously_default(func, gr, other_gr): 84 | return simplify(func, gr, other_gr) 85 | 86 | self.objc_should_recognize_simultaneously = objc_should_recognize_simultaneously_default 87 | 88 | # Fail other 89 | 90 | def gestureRecognizer_shouldRequireFailureOfGestureRecognizer_(_self, _sel, gr, other_gr): 91 | return self.objc_should_require_failure(self.fail_other, gr, other_gr) 92 | 93 | def objc_should_require_failure_default(func, gr, other_gr): 94 | return simplify(func, gr, other_gr) 95 | 96 | self.objc_should_require_failure = objc_should_require_failure_default 97 | 98 | # Fail 99 | 100 | def gestureRecognizer_shouldBeRequiredToFailByGestureRecognizer_(_self, _sel, gr, other_gr): 101 | return self.objc_should_fail(self.fail, gr, other_gr) 102 | 103 | def objc_should_fail_default(func, gr, other_gr): 104 | return simplify(func, gr, other_gr) 105 | 106 | self.objc_should_fail = objc_should_fail_default 107 | 108 | # Delegate 109 | 110 | try: 111 | PythonGestureDelegate = ObjCClass('PythonGestureDelegate') 112 | except: 113 | PythonGestureDelegate = create_objc_class('PythonistaGestureDelegate', 114 | superclass=NSObject, 115 | methods=[ 116 | gestureRecognizer_shouldRecognizeSimultaneouslyWithGestureRecognizer_, 117 | gestureRecognizer_shouldRequireFailureOfGestureRecognizer_, 118 | gestureRecognizer_shouldBeRequiredToFailByGestureRecognizer_], 119 | classmethods=[], 120 | protocols=['UIGestureRecognizerDelegate'], 121 | debug=True) 122 | self._delegate = PythonGestureDelegate.new() 123 | 124 | @on_main_thread 125 | def add_tap(self, view, action, number_of_taps_required = None, number_of_touches_required = None): 126 | recog = self._get_recog('UITapGestureRecognizer', view, self._general_action, action) 127 | 128 | if number_of_taps_required: 129 | recog.numberOfTapsRequired = number_of_taps_required 130 | if number_of_touches_required: 131 | recog.numberOfTouchesRequired = number_of_touches_required 132 | 133 | return recog 134 | 135 | @on_main_thread 136 | def add_long_press(self, view, action, number_of_taps_required = None, number_of_touches_required = None, minimum_press_duration = None, allowable_movement = None): 137 | recog = self._get_recog('UILongPressGestureRecognizer', view, self._general_action, action) 138 | 139 | if number_of_taps_required: 140 | recog.numberOfTapsRequired = number_of_taps_required 141 | if number_of_touches_required: 142 | recog.numberOfTouchesRequired = number_of_touches_required 143 | if minimum_press_duration: 144 | recog.minimumPressDuration = minimum_press_duration 145 | if allowable_movement: 146 | recog.allowableMovement = allowable_movement 147 | 148 | return recog 149 | 150 | @on_main_thread 151 | def add_pan(self, view, action, minimum_number_of_touches = None, maximum_number_of_touches = None, set_translation = None): 152 | recog = self._get_recog('UIPanGestureRecognizer', view, self._pan_action, action) 153 | 154 | if minimum_number_of_touches: 155 | recog.minimumNumberOfTouches = minimum_number_of_touches 156 | if maximum_number_of_touches: 157 | recog.maximumNumberOfTouches = maximum_number_of_touches 158 | if set_translation: 159 | recog.set_translation_(CGPoint(set_translation.x, set_translation.y), ObjCInstance(view)) 160 | 161 | return recog 162 | 163 | @on_main_thread 164 | def add_screen_edge_pan(self, view, action, edges = None): 165 | recog = self._get_recog('UIScreenEdgePanGestureRecognizer', view, self._pan_action, action) 166 | 167 | if edges: 168 | recog.edges = edges 169 | 170 | return recog 171 | 172 | @on_main_thread 173 | def add_pinch(self, view, action): 174 | recog = self._get_recog('UIPinchGestureRecognizer', view, self._pinch_action, action) 175 | 176 | return recog 177 | 178 | @on_main_thread 179 | def add_rotation(self, view, action): 180 | recog = self._get_recog('UIRotationGestureRecognizer', view, self._rotation_action, action) 181 | 182 | return recog 183 | 184 | @on_main_thread 185 | def add_swipe(self, view, action, direction = None, number_of_touches_required = None): 186 | recog = self._get_recog('UISwipeGestureRecognizer', view, self._general_action, action) 187 | 188 | if direction: 189 | combined_dir = direction 190 | if isinstance(direction, list): 191 | combined_dir = 0 192 | for one_direction in direction: 193 | combined_dir |= one_direction 194 | recog.direction = combined_dir 195 | if number_of_touches_required: 196 | recog.numberOfTouchesRequired = number_of_touches_required 197 | 198 | return recog 199 | 200 | @on_main_thread 201 | def remove(self, view, recognizer): 202 | key = None 203 | for id in self.recognizers: 204 | if self.recognizers[id] == recognizer: 205 | key = id 206 | break 207 | if key: 208 | del self.buttons[key] 209 | del self.views[key] 210 | del self.recognizers[key] 211 | del self.actions[key] 212 | ObjCInstance(view).removeGestureRecognizer_(recognizer) 213 | 214 | @on_main_thread 215 | def enable(self, recognizer): 216 | ObjCInstance(recognizer).enabled = True 217 | 218 | @on_main_thread 219 | def disable(self, recognizer): 220 | ObjCInstance(recognizer).enabled = False 221 | 222 | @on_main_thread 223 | def remove_all_gestures(self, view): 224 | gestures = ObjCInstance(view).gestureRecognizers() 225 | for recog in gestures: 226 | self.remove(view, recog) 227 | 228 | def _get_recog(self, recog_name, view, internal_action, final_handler): 229 | button = ui.Button() 230 | key = str(uuid.uuid4()) 231 | button.name = key 232 | button.action = internal_action 233 | self.buttons[key] = button 234 | self.views[key] = view 235 | recognizer = ObjCClass(recog_name).alloc().initWithTarget_action_(button, sel('invokeAction:')).autorelease() 236 | self.recognizers[key] = recognizer 237 | self.actions[key] = final_handler 238 | ObjCInstance(view).addGestureRecognizer_(recognizer) 239 | recognizer.delegate = self._delegate 240 | return recognizer 241 | 242 | class Data(): 243 | def __init__(self): 244 | self.recognizer = self.view = self.location = self.state = self.number_of_touches = self.scale = self.rotation = self.velocity = None 245 | 246 | def _context(self, button): 247 | key = button.name 248 | (view, recog, action) = (self.views[key], self.recognizers[key], self.actions[key]) 249 | data = Gestures.Data() 250 | data.recognizer = recog 251 | data.view = view 252 | data.location = self._location(view, recog) 253 | data.state = recog.state() 254 | data.number_of_touches = recog.numberOfTouches() 255 | return (data, action) 256 | 257 | def _location(self, view, recog): 258 | loc = recog.locationInView_(ObjCInstance(view)) 259 | return ui.Point(loc.x, loc.y) 260 | 261 | def _general_action(self, sender): 262 | (data, action) = self._context(sender) 263 | action(data) 264 | 265 | def _pan_action(self, sender): 266 | (data, action) = self._context(sender) 267 | 268 | trans = data.recognizer.translationInView_(ObjCInstance(data.view)) 269 | vel = data.recognizer.velocityInView_(ObjCInstance(data.view)) 270 | data.translation = ui.Point(trans.x, trans.y) 271 | data.velocity = ui.Point(vel.x, vel.y) 272 | 273 | action(data) 274 | 275 | def _pinch_action(self, sender): 276 | (data, action) = self._context(sender) 277 | 278 | data.scale = data.recognizer.scale() 279 | data.velocity = data.recognizer.velocity() 280 | 281 | action(data) 282 | 283 | def _rotation_action(self, sender): 284 | (data, action) = self._context(sender) 285 | 286 | data.rotation = data.recognizer.rotation() 287 | data.velocity = data.recognizer.velocity() 288 | 289 | action(data) 290 | 291 | # TESTING AND DEMONSTRATION 292 | 293 | if __name__ == "__main__": 294 | 295 | class EventDisplay(ui.View): 296 | def __init__(self): 297 | self.tv = ui.TextView(flex='WH') 298 | self.add_subview(self.tv) 299 | self.tv.frame = (0, 0, self.width, self.height) 300 | 301 | g = Gestures() 302 | 303 | g.recognize_simultaneously = lambda gr, other_gr: gr == Gestures.PAN and other_gr == Gestures.PINCH 304 | 305 | g.fail_other = lambda gr, other_gr: other_gr == Gestures.PINCH 306 | 307 | g.add_tap(self, self.general_handler) 308 | 309 | g.add_long_press(self.tv, self.long_press_handler) 310 | 311 | pan = g.add_pan(self, self.pan_handler) 312 | 313 | #g.add_screen_edge_pan(self.tv, self.pan_handler, edges = Gestures.EDGE_LEFT) 314 | 315 | #g.add_swipe(self.tv, self.general_handler, direction = [Gestures.DOWN]) 316 | 317 | g.add_pinch(self, self.pinch_handler) 318 | 319 | #g.add_rotation(self.tv, self.rotation_handler) 320 | 321 | def t(self, msg): 322 | self.tv.text = self.tv.text + msg + '\n' 323 | 324 | def general_handler(self, data): 325 | self.t('General: ' + str(data.location) + ' - state: ' + str(data.state) + ' - touches: ' + str(data.number_of_touches)) 326 | 327 | def long_press_handler(self, data): 328 | if data.state == Gestures.ENDED: 329 | self.t('Long press: ' + str(data.location) + ' - state: ' + str(data.state) + ' - touches: ' + str(data.number_of_touches)) 330 | 331 | def pan_handler(self, data): 332 | self.t('Pan: ' + str(data.translation) + ' - state: ' + str(data.state)) 333 | 334 | def pinch_handler(self, data): 335 | self.t('Pinch: ' + str(data.scale) + ' state: ' + str(data.state)) 336 | 337 | def rotation_handler(self, data): 338 | self.t('Rotation: ' + str(data.rotation)) 339 | 340 | view = EventDisplay() 341 | view.present() 342 | 343 | -------------------------------------------------------------------------------- /webvr/main.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | # This tool allows to open webvr content in true fullscreen mode on iOS devices using Pythonista. 4 | # Two vr contents are available : 5 | # - the first one comes from sketchfab and displays a 3D room. 6 | # - the second one comes from https://github.com/ryanbetts/dayframe . 7 | # It uses the web framework 'AFrame' for building vr experiences. The "more one thing" is the emulation of a daydream controller. 8 | # (when you choose this demo, try to use an other phone with a web browser opened on https://dayframe-demo.herokuapp.com/remote 9 | # but as this is a public demo and as the author mentioned it : only one person at a time can control the remote. If you join, you will disconnect the previously connected remote) 10 | # Three secret features are also available : 11 | # - you can trigger an url page loading on your device using a remote web browser connected to this device 12 | # - you can adjust the vertical offset and the scale of the web rendering using gestures (find them !). All adjustments are stored so if an url is reloaded, there are applyed again automatically... 13 | # - if you need to interact with the web view, make a long press until you feel a vibration, then you have 10 seconds to manipulate it before the top view takes the control again. You will feel a new vibration, and the top view will catch again the touch events (vertical offset and scale gestures) 14 | 15 | import ui, console, time, motion 16 | import threading, queue 17 | from contextlib import closing 18 | 19 | from datetime import datetime 20 | import re, urllib.request, socket 21 | 22 | from flask import Flask, request, render_template 23 | 24 | from objc_util import * 25 | import ctypes 26 | c=ctypes.CDLL(None) 27 | 28 | def vibrate(): 29 | p = c.AudioServicesPlaySystemSound 30 | p.restype, p.argtypes = None, [ctypes.c_int32] 31 | vibrate_id=0x00000fff 32 | p(vibrate_id) 33 | 34 | import requests 35 | from threading import Timer 36 | import httplib2 37 | from urllib.parse import urlparse 38 | 39 | from Gestures import Gestures 40 | import math 41 | import json, os 42 | REGISTRY_PATH='./.data/registry.txt' 43 | 44 | theDemoURLs = ["https://sketchfab.com/models/311d052a9f034ba8bce55a1a8296b6f9/embed?autostart=1&cardboard=1","https://dayframe-demo.herokuapp.com/scene"] 45 | theHttpPort = 8080 46 | theThread = None 47 | theSharing = {} 48 | theLock = threading.RLock() 49 | theApp = Flask(__name__) 50 | 51 | 52 | @theApp.route("/", methods=['GET', 'POST']) 53 | def index(): 54 | if request.method == 'GET': 55 | return render_template('control.html') 56 | else: 57 | theLock.acquire() 58 | try: 59 | q = theSharing['queue'] 60 | obj = q.get() 61 | obj.next_url = request.form['command'] 62 | q.task_done() 63 | finally: 64 | theLock.release() 65 | return render_template('control.html') 66 | 67 | 68 | LAST_REQUEST_MS = 0 69 | @theApp.before_request 70 | def update_last_request_ms(): 71 | global LAST_REQUEST_MS 72 | LAST_REQUEST_MS = time.time() * 1000 73 | 74 | @theApp.route('/seriouslykill', methods=['POST']) 75 | def seriouslykill(): 76 | func = request.environ.get('werkzeug.server.shutdown') 77 | if func is None: 78 | raise RuntimeError('Not running with the Werkzeug Server') 79 | func() 80 | return "Shutting down..." 81 | 82 | @theApp.route('/kill', methods=['POST']) 83 | def kill(): 84 | last_ms = LAST_REQUEST_MS 85 | def shutdown(): 86 | if LAST_REQUEST_MS <= last_ms: # subsequent requests abort shutdown 87 | requests.post('http://localhost:%d/seriouslykill' % theHttpPort) 88 | else: 89 | pass 90 | 91 | Timer(1.0, shutdown).start() # wait 1 second 92 | return "Shutting down..." 93 | 94 | def get_local_ip_addr(): # Get the local ip address of the device 95 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # Make a socket object 96 | s.connect(('8.8.8.8', 80)) # Connect to google 97 | ip = s.getsockname()[0] # Get our IP address from the socket 98 | s.close() # Close the socket 99 | return ip # And return the IP address 100 | 101 | def check_if_url_is_valid(value): 102 | h = httplib2.Http() 103 | value = unshorten_url(value) 104 | resp = h.request(value, 'HEAD') 105 | return int(resp[0]['status']) < 400 106 | 107 | def unshorten_url(url): 108 | r = requests.head(url, allow_redirects=True) 109 | return r.url 110 | 111 | 112 | 113 | # thread worker 114 | class workerThread(threading.Thread): 115 | def __init__(self): 116 | threading.Thread.__init__(self) 117 | self.daemon = True 118 | 119 | def run(self): 120 | NSNetService = ObjCClass('NSNetService') # Bonjour publication 121 | service = NSNetService.alloc().initWithDomain_type_name_port_('', '_http._tcp', 'iOS webVR Viewer', theHttpPort) 122 | try: 123 | service.publish() 124 | theApp.run(host='0.0.0.0', port=theHttpPort) 125 | finally: 126 | service.stop() 127 | service.release() 128 | 129 | def stop(self): 130 | requests.post('http://localhost:%d/kill' % theHttpPort) 131 | 132 | 133 | 134 | # As it's important to hold the phone in landscape mode before creating the view, 135 | # a dedicated function has been created... 136 | def waitForLandscapeMode(): 137 | msg = 'Please, hold your phone in landscape mode' 138 | console.hud_alert(msg, duration = 3) 139 | motion.start_updates() 140 | try: 141 | count=0 142 | while True: 143 | x, y, z = motion.get_gravity() 144 | count+=1 145 | if count>2: 146 | if abs(x) > abs(y): 147 | break 148 | else: 149 | console.hud_alert(msg, duration = 2) 150 | time.sleep(0.5) 151 | finally: 152 | motion.stop_updates() 153 | time.sleep(1) 154 | 155 | # the main class 156 | class MyWebVRView(ui.View): 157 | def __init__(self, url): 158 | self.width, self.height = ui.get_window_size() 159 | self.background_color= 'black' 160 | # the webview 161 | self.wv = ui.WebView(frame=self.bounds) 162 | self.wv.background_color= 'black' 163 | self.finished = False 164 | self.current_url = None 165 | self.next_url = "" 166 | self.start_workerThread() 167 | self.add_subview(self.wv) 168 | # a top view for catching gesture events 169 | self.gv = ui.View(frame=self.bounds) 170 | self.gv.alpha = 0.05 171 | self.gv.background_color = 'white' 172 | self.add_subview(self.gv) 173 | self.gv.bring_to_front() 174 | # some variables for setting the layout 175 | self.ty=-27 176 | self.sx=1 177 | self.applyVerticalOffset() 178 | self.applyScale() 179 | # view adjustment per url are saved here 180 | self.registry={} 181 | self.readRegistry() 182 | # gesture setup 183 | g = Gestures() 184 | g.add_pan(self.gv, self.pan_handler,1,1) 185 | g.add_pinch(self.gv, self.pinch_handler) 186 | g.add_long_press(self.gv, self.long_press_handler) 187 | # launch the layout 188 | self.present("full_screen", hide_title_bar=True, orientations=['landscape']) 189 | # load the url 190 | self.loadURL(url) 191 | 192 | def get_pan_x_limits(self): 193 | range = self.width*0.1 194 | x_min= (self.width-range)*0.5 195 | x_max = (self.width+range)*0.5 196 | return x_min, x_max 197 | 198 | def pan_handler(self, data): 199 | # get the safe area for this gesture 200 | x_min, x_max = self.get_pan_x_limits() 201 | x = data.location.x 202 | if (x >= x_min) and (x <= x_max): 203 | self.ty += int(data.velocity.y*1.0/50) 204 | self.applyVerticalOffset() 205 | self.saveInfoToRegistry(self.current_url, self.ty, self.sx) 206 | 207 | 208 | def pinch_handler(self, data): 209 | self.sx += data.velocity/50 210 | self.applyScale() 211 | self.saveInfoToRegistry(self.current_url, self.ty, self.sx) 212 | 213 | 214 | def long_press_handler(self, data): 215 | # get the safe area for this gesture 216 | x_min, x_max = self.get_pan_x_limits() 217 | x = data.location.x 218 | if (x <= x_min) or (x >= x_max): 219 | if data.state==Gestures.BEGAN: 220 | # a little feedback... 221 | vibrate() 222 | # ...and we disable the top view using the alpha parameter of this view 223 | self.gv.alpha = 0 224 | ui.delay(self.restoreAlpha, 10) 225 | 226 | 227 | def restoreAlpha(self): 228 | # restore the alpha parameter of the top view. Notice, it's a small small value ;) 229 | self.gv.alpha=0.05 230 | vibrate() 231 | 232 | # small persistent storage mechanism 233 | def writeRegistry(self): 234 | with open(REGISTRY_PATH, 'w') as outfile: 235 | json.dump(self.registry, outfile, indent=2, sort_keys=True) 236 | 237 | 238 | def readRegistry(self): 239 | directory = os.path.dirname(REGISTRY_PATH) 240 | if not os.path.exists(directory): 241 | os.makedirs(directory) 242 | return 243 | if os.path.exists(REGISTRY_PATH): 244 | with open(REGISTRY_PATH) as json_file: 245 | self.registry = json.load(json_file) 246 | 247 | 248 | def readInfoFromRegistry(self, url): 249 | key = self.buildKeyFromURL(url) 250 | if key in self.registry: 251 | return self.registry[key] 252 | return (self.ty, self.sx) 253 | 254 | def saveInfoToRegistry(self, url, pos, scale): 255 | key = self.buildKeyFromURL(url) 256 | self.registry[key] = (pos,scale) 257 | self.writeRegistry() 258 | 259 | # create a key from an url 260 | def buildKeyFromURL(self, url): 261 | pos = url.find("://") 262 | tokens = url[pos+3:].split('/') 263 | return tokens[0] 264 | 265 | # a little function to set the web view layout 266 | def applyVerticalOffset(self): 267 | self.wv.y = self.ty 268 | 269 | def applyScale(self): 270 | self.wv.transform = ui.Transform().scale(self.sx, self.sx) 271 | 272 | # here we observe the exit 273 | def will_close(self): 274 | self.finished = True 275 | 276 | # some thread management used to 277 | # react to the remote change 278 | # of the current url 279 | def start_workerThread(self): 280 | global theThread 281 | global theSharing 282 | with theLock: 283 | theSharing['queue'] = queue.Queue(1) 284 | theThread = workerThread() 285 | theThread.start() 286 | 287 | 288 | def stop_workerThread(self): 289 | if theThread is None: 290 | return 291 | theThread.stop() 292 | while theThread and theThread.isAlive(): 293 | with theLock: 294 | q = theSharing['queue'] 295 | if q.empty(): 296 | q.put(self) 297 | time.sleep(1.0/60) 298 | 299 | # this method allows to maintain a communication with the worker thread using a queue 300 | def update(self): 301 | url = "" 302 | with theLock: 303 | q = theSharing['queue'] 304 | if q.empty(): 305 | q.put(self) 306 | url = self.next_url 307 | if url != "": 308 | self.loadURL(url) 309 | 310 | def run(self): 311 | while not self.finished: 312 | self.update() 313 | time.sleep(1.0/60) 314 | self.stop_workerThread() 315 | 316 | 317 | 318 | def loadURL(self, url): 319 | if url=="": # force reload/refresh the current url 320 | url=self.current_url 321 | self.current_url=None 322 | 323 | url = self.patch_SKETCHFAB_page(url) 324 | if check_if_url_is_valid(url): 325 | if self.current_url is None or (self.current_url != url): 326 | print("loading %s" % url) 327 | self.current_url = url 328 | self.wv.load_url(self.current_url) 329 | self.patch_AFRAME_page() 330 | self.ty, self.sx = self.readInfoFromRegistry(url) 331 | self.applyVerticalOffset() 332 | self.applyScale() 333 | 334 | # The following function returns the given url but in case of a sketchfab url, it adds the auto cardboard view parameter at the end of string... 335 | def patch_SKETCHFAB_page(self, url): 336 | result = url.lower() 337 | if result.startswith("https://sketchfab.com/models/"): 338 | if not result.endswith("/embed?autostart=1&cardboard=1"): 339 | result += "/embed?autostart=1&cardboard=1" 340 | return result 341 | 342 | 343 | 344 | # An other trick in case of a aframe url, it will inject a custom javascript code in order to force the enterVR trigger... 345 | # but sometimes, the following hack seems to be wrong... 346 | # The screen stays in desktop mode, you have to restart the demo or click on the cardboard icon. 347 | # Perhaps, my delay is too short or something goes wrong with the browser cache... 348 | 349 | def patch_AFRAME_page(self): 350 | js_code = """ 351 | function customEnterVR () { 352 | var scene = document.getElementById('scene'); 353 | if (scene) { 354 | if (scene.hasLoaded) { 355 | scene.enterVR(); 356 | } else { 357 | scene.addEventListener('loaded', scene.enterVR); 358 | } 359 | } 360 | } 361 | customEnterVR(); 362 | """ 363 | searchITEM = "scene" 364 | searchID = self.wv.evaluate_javascript('document.getElementById("%s").id' % searchITEM) 365 | searchCount = 0 366 | while not searchID == "%s" % searchITEM: 367 | time.sleep(1) # wait for 1 second before searching again 368 | searchID = self.wv.evaluate_javascript('document.getElementById("%s").id' % searchITEM) 369 | searchCount += 1 370 | if searchCount>2: # max two attempts... 371 | break 372 | if searchID == searchITEM: 373 | res=self.wv.eval_js(js_code) 374 | 375 | if __name__ == '__main__': 376 | # disable the ios screensaver 377 | console.set_idle_timer_disabled(True) 378 | 379 | # ask the user for the first url loading. 380 | # he has the choice between a sketchfab or an a-frame scene 381 | demoID = console.alert('Select a demo','(%s:%d)'% (get_local_ip_addr(), theHttpPort),'sketchfab','a-frame') 382 | url = theDemoURLs[demoID-1] 383 | 384 | # it's very important to hold the phone in landscape mode before the ui.view creation so ... 385 | waitForLandscapeMode() 386 | 387 | # fasten your seatbelts, start the engine and let's get doing!... 388 | MyWebVRView(url).run() 389 | 390 | # restore the ios screensaver 391 | console.set_idle_timer_disabled(False) 392 | -------------------------------------------------------------------------------- /webvr/templates/control.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 |