1#!/usr/bin/env python 2# -*- coding: utf-8 -*- 3# 4# C++ version Copyright (c) 2006-2007 Erin Catto http://www.box2d.org 5# Python version by Ken Lauer / sirkne at gmail dot com 6# 7# This software is provided 'as-is', without any express or implied 8# warranty. In no event will the authors be held liable for any damages 9# arising from the use of this software. 10# Permission is granted to anyone to use this software for any purpose, 11# including commercial applications, and to alter it and redistribute it 12# freely, subject to the following restrictions: 13# 1. The origin of this software must not be misrepresented; you must not 14# claim that you wrote the original software. If you use this software 15# in a product, an acknowledgment in the product documentation would be 16# appreciated but is not required. 17# 2. Altered source versions must be plainly marked as such, and must not be 18# misrepresented as being the original software. 19# 3. This notice may not be removed or altered from any source distribution. 20 21from .framework import (Framework, Keys, main) 22from random import random 23from math import sqrt, sin, cos 24 25from Box2D import (b2BodyDef, b2CircleShape, b2Color, b2EdgeShape, 26 b2FixtureDef, b2PolygonShape, b2RayCastCallback, b2Vec2, 27 b2_dynamicBody, b2_pi) 28 29 30class RayCastClosestCallback(b2RayCastCallback): 31 """This callback finds the closest hit""" 32 33 def __repr__(self): 34 return 'Closest hit' 35 36 def __init__(self, **kwargs): 37 b2RayCastCallback.__init__(self, **kwargs) 38 self.fixture = None 39 self.hit = False 40 41 def ReportFixture(self, fixture, point, normal, fraction): 42 ''' 43 Called for each fixture found in the query. You control how the ray 44 proceeds by returning a float that indicates the fractional length of 45 the ray. By returning 0, you set the ray length to zero. By returning 46 the current fraction, you proceed to find the closest point. By 47 returning 1, you continue with the original ray clipping. By returning 48 -1, you will filter out the current fixture (the ray will not hit it). 49 ''' 50 self.hit = True 51 self.fixture = fixture 52 self.point = b2Vec2(point) 53 self.normal = b2Vec2(normal) 54 # NOTE: You will get this error: 55 # "TypeError: Swig director type mismatch in output value of 56 # type 'float32'" 57 # without returning a value 58 return fraction 59 60 61class RayCastAnyCallback(b2RayCastCallback): 62 """This callback finds any hit""" 63 64 def __repr__(self): 65 return 'Any hit' 66 67 def __init__(self, **kwargs): 68 b2RayCastCallback.__init__(self, **kwargs) 69 self.fixture = None 70 self.hit = False 71 72 def ReportFixture(self, fixture, point, normal, fraction): 73 self.hit = True 74 self.fixture = fixture 75 self.point = b2Vec2(point) 76 self.normal = b2Vec2(normal) 77 return 0.0 78 79 80class RayCastMultipleCallback(b2RayCastCallback): 81 """This raycast collects multiple hits.""" 82 83 def __repr__(self): 84 return 'Multiple hits' 85 86 def __init__(self, **kwargs): 87 b2RayCastCallback.__init__(self, **kwargs) 88 self.fixture = None 89 self.hit = False 90 self.points = [] 91 self.normals = [] 92 93 def ReportFixture(self, fixture, point, normal, fraction): 94 self.hit = True 95 self.fixture = fixture 96 self.points.append(b2Vec2(point)) 97 self.normals.append(b2Vec2(normal)) 98 return 1.0 99 100 101class Raycast (Framework): 102 name = "Raycast" 103 description = "Press 1-5 to drop stuff, d to delete, m to switch callback modes" 104 p1_color = b2Color(0.4, 0.9, 0.4) 105 s1_color = b2Color(0.8, 0.8, 0.8) 106 s2_color = b2Color(0.9, 0.9, 0.4) 107 108 def __init__(self): 109 super(Raycast, self).__init__() 110 111 self.world.gravity = (0, 0) 112 # The ground 113 ground = self.world.CreateBody( 114 shapes=b2EdgeShape(vertices=[(-40, 0), (40, 0)]) 115 ) 116 117 # The various shapes 118 w = 1.0 119 b = w / (2.0 + sqrt(2.0)) 120 s = sqrt(2.0) * b 121 122 self.shapes = [ 123 b2PolygonShape(vertices=[(-0.5, 0), (0.5, 0), (0, 1.5)]), 124 b2PolygonShape(vertices=[(-0.1, 0), (0.1, 0), (0, 1.5)]), 125 b2PolygonShape( 126 vertices=[(0.5 * s, 0), (0.5 * w, b), (0.5 * w, b + s), 127 (0.5 * s, w), (-0.5 * s, w), (-0.5 * w, b + s), 128 (-0.5 * w, b), (-0.5 * s, 0.0)] 129 ), 130 b2PolygonShape(box=(0.5, 0.5)), 131 b2CircleShape(radius=0.5), 132 ] 133 self.angle = 0 134 135 self.callbacks = [RayCastClosestCallback, 136 RayCastAnyCallback, RayCastMultipleCallback] 137 self.callback_class = self.callbacks[0] 138 139 def CreateShape(self, shapeindex): 140 try: 141 shape = self.shapes[shapeindex] 142 except IndexError: 143 return 144 145 pos = (10.0 * (2.0 * random() - 1.0), 10.0 * (2.0 * random())) 146 defn = b2BodyDef( 147 type=b2_dynamicBody, 148 fixtures=b2FixtureDef(shape=shape, friction=0.3), 149 position=pos, 150 angle=(b2_pi * (2.0 * random() - 1.0)), 151 ) 152 153 if isinstance(shape, b2CircleShape): 154 defn.angularDamping = 0.02 155 156 self.world.CreateBody(defn) 157 158 def DestroyBody(self): 159 for body in self.world.bodies: 160 if not self.world.locked: 161 self.world.DestroyBody(body) 162 break 163 164 def Keyboard(self, key): 165 if key in (Keys.K_1, Keys.K_2, Keys.K_3, Keys.K_4, Keys.K_5): 166 self.CreateShape(key - Keys.K_1) 167 elif key == Keys.K_d: 168 self.DestroyBody() 169 elif key == Keys.K_m: 170 idx = ((self.callbacks.index(self.callback_class) + 1) % 171 len(self.callbacks)) 172 self.callback_class = self.callbacks[idx] 173 174 def Step(self, settings): 175 super(Raycast, self).Step(settings) 176 177 def draw_hit(cb_point, cb_normal): 178 cb_point = self.renderer.to_screen(cb_point) 179 head = b2Vec2(cb_point) + 0.5 * cb_normal 180 181 cb_normal = self.renderer.to_screen(cb_normal) 182 self.renderer.DrawPoint(cb_point, 5.0, self.p1_color) 183 self.renderer.DrawSegment(point1, cb_point, self.s1_color) 184 self.renderer.DrawSegment(cb_point, head, self.s2_color) 185 186 # Set up the raycast line 187 length = 11 188 point1 = b2Vec2(0, 10) 189 d = (length * cos(self.angle), length * sin(self.angle)) 190 point2 = point1 + d 191 192 callback = self.callback_class() 193 194 self.world.RayCast(callback, point1, point2) 195 196 # The callback has been called by this point, and if a fixture was hit it will have been 197 # set to callback.fixture. 198 point1 = self.renderer.to_screen(point1) 199 point2 = self.renderer.to_screen(point2) 200 201 if callback.hit: 202 if hasattr(callback, 'points'): 203 for point, normal in zip(callback.points, callback.normals): 204 draw_hit(point, normal) 205 else: 206 draw_hit(callback.point, callback.normal) 207 else: 208 self.renderer.DrawSegment(point1, point2, self.s1_color) 209 210 self.Print("Callback: %s" % callback) 211 if callback.hit: 212 self.Print("Hit") 213 214 if not settings.pause or settings.singleStep: 215 self.angle += 0.25 * b2_pi / 180 216 217if __name__ == "__main__": 218 main(Raycast) 219