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