File: shapecollisions.py

package info (click to toggle)
kivy 2.3.1-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 35,316 kB
  • sloc: python: 80,678; ansic: 5,326; javascript: 780; objc: 725; lisp: 195; sh: 173; makefile: 150
file content (397 lines) | stat: -rw-r--r-- 14,260 bytes parent folder | download | duplicates (3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
# This is a simple demo for advanced collisions and mesh creation from a set
# of points. Its purpose is only to give an idea on how to make complex stuff.

# Check garden.collider for better performance.

from math import cos, sin, pi, sqrt
from random import random, randint
from itertools import combinations

from kivy.app import App
from kivy.clock import Clock
from kivy.uix.label import Label
from kivy.uix.widget import Widget
from kivy.core.window import Window
from kivy.graphics import Color, Mesh, Point
from kivy.uix.floatlayout import FloatLayout
from kivy.properties import (
    ListProperty,
    StringProperty,
    ObjectProperty,
    NumericProperty
)


# Cloud polygon, 67 vertices + custom origin [150, 50]
cloud_poly = [
    150, 50,
    109.7597, 112.9600, 115.4326, 113.0853, 120.1966, 111.9883,
    126.0889, 111.9570, 135.0841, 111.9570, 138.5944, 112.5525,
    145.7403, 115.5301, 150.5357, 120.3256, 155.5313, 125.5938,
    160.8438, 130.5000, 165.7813, 132.5000, 171.8125, 132.3438,
    177.5000, 128.4688, 182.1531, 121.4990, 185.1438, 114.0406,
    185.9181, 108.5649, 186.2226, 102.5978, 187.8059, 100.2231,
    193.2257, 100.1622, 197.6712, 101.8671, 202.6647, 104.1809,
    207.1102, 105.8858, 214.2351, 105.0333, 219.3747, 102.8301,
    224.0413, 98.7589, 225.7798, 93.7272, 226.0000, 86.8750,
    222.9375, 81.0625, 218.3508, 76.0867, 209.8301, 70.8090,
    198.7806, 66.1360, 189.7651, 62.2327, 183.6082, 56.6252,
    183.2784, 50.5778, 190.9155, 42.7294, 196.8470, 36.1343,
    197.7339, 29.9272, 195.5720, 23.4430, 191.2500, 15.9803,
    184.0574, 9.5882, 175.8811, 3.9951, 165.7992, 3.4419,
    159.0369, 7.4370, 152.5205, 14.8125, 147.4795, 24.2162,
    142.4385, 29.0103, 137.0287, 30.9771, 127.1560, 27.4818,
    119.1371, 20.0388, 112.1820, 11.3690, 104.6541, 7.1976,
    97.2080, 6.2979, 88.9437, 9.8149, 80.3433, 17.3218,
    76.5924, 26.5452, 78.1678, 37.0432, 83.5068, 47.1104,
    92.8529, 58.3561, 106.3021, 69.2978, 108.9615, 73.9329,
    109.0375, 80.6955, 104.4713, 88.6708, 100.6283, 95.7483,
    100.1226, 101.5114, 102.8532, 107.2745, 105.6850, 110.9144,
    109.7597, 112.9600
]


class BaseShape(Widget):
    '''(internal) Base class for moving with touches or calls.'''

    # keep references for offset
    _old_pos = ListProperty([0, 0])
    _old_touch = ListProperty([0, 0])
    _new_touch = ListProperty([0, 0])

    # shape properties
    name = StringProperty('')
    poly = ListProperty([])
    shape = ObjectProperty()
    poly_len = NumericProperty(0)
    shape_len = NumericProperty(0)
    debug_collider = ObjectProperty()
    debug_collider_len = NumericProperty(0)

    def __init__(self, **kwargs):
        '''Create a shape with size [100, 100]
        and give it a label if it's named.
        '''
        super(BaseShape, self).__init__(**kwargs)
        self.size_hint = (None, None)
        self.add_widget(Label(text=self.name))

    def move_label(self, x, y, *args):
        '''Move label with shape name as the only child.'''
        self.children[0].pos = [x, y]

    def move_collider(self, offset_x, offset_y, *args):
        '''Move debug collider when the shape moves.'''
        points = self.debug_collider.points[:]

        for i in range(0, self.debug_collider_len, 2):
            points[i] += offset_x
            points[i + 1] += offset_y
        self.debug_collider.points = points

    def on_debug_collider(self, instance, value):
        '''Recalculate length of collider points' array.'''
        self.debug_collider_len = len(value.points)

    def on_poly(self, instance, value):
        '''Recalculate length of polygon points' array.'''
        self.poly_len = len(value)

    def on_shape(self, instance, value):
        '''Recalculate length of Mesh vertices' array.'''
        self.shape_len = len(value.vertices)

    def on_pos(self, instance, pos):
        '''Move polygon and its Mesh on each position change.
        This event is above all and changes positions of the other
        children-like components, so that a simple::

            shape.pos = (100, 200)

        would move everything, not just the widget itself.
        '''

        # position changed by touch
        offset_x = self._new_touch[0] - self._old_touch[0]
        offset_y = self._new_touch[1] - self._old_touch[1]

        # position changed by call (shape.pos = X)
        if not offset_x and not offset_y:
            offset_x = pos[0] - self._old_pos[0]
            offset_y = pos[1] - self._old_pos[1]
            self._old_pos = pos

        # move polygon points by offset
        for i in range(0, self.poly_len, 2):
            self.poly[i] += offset_x
            self.poly[i + 1] += offset_y

        # stick label to bounding box (widget)
        if self.name:
            self.move_label(*pos)

        # move debug collider if available
        if self.debug_collider is not None:
            self.move_collider(offset_x, offset_y)

        # return if no Mesh available
        if self.shape is None:
            return

        # move Mesh vertices by offset
        points = self.shape.vertices[:]
        for i in range(0, self.shape_len, 2):
            points[i] += offset_x
            points[i + 1] += offset_y
        self.shape.vertices = points

    def on_touch_move(self, touch, *args):
        '''Move shape with dragging.'''

        # grab single touch for shape
        if touch.grab_current is not self:
            return

        # get touches
        x, y = touch.pos
        new_pos = [x, y]
        self._new_touch = new_pos
        self._old_touch = [touch.px, touch.py]

        # get offsets, move & trigger on_pos event
        offset_x = self._new_touch[0] - self._old_touch[0]
        offset_y = self._new_touch[1] - self._old_touch[1]
        self.pos = [self.x + offset_x, self.y + offset_y]

    def shape_collide(self, x, y, *args):
        '''Point to polygon collision through a list of points.'''

        # ignore if no polygon area is set
        poly = self.poly
        if not poly:
            return False

        n = self.poly_len
        inside = False
        p1x = poly[0]
        p1y = poly[1]

        # compare point pairs via PIP algo, too long, read
        # https://en.wikipedia.org/wiki/Point_in_polygon
        for i in range(0, n + 2, 2):
            p2x = poly[i % n]
            p2y = poly[(i + 1) % n]

            if y > min(p1y, p2y) and y <= max(p1y, p2y) and x <= max(p1x, p2x):
                if p1y != p2y:
                    xinters = (y - p1y) * (p2x - p1x) / (p2y - p1y) + p1x
                if p1x == p2x or x <= xinters:
                    inside = not inside

            p1x, p1y = p2x, p2y
        return inside


class RegularShape(BaseShape):
    '''Starting from center and creating edges around for i.e.:
    regular triangles, squares, regular pentagons, up to "circle".
    '''

    def __init__(self, edges=3, color=None, **kwargs):
        super(RegularShape, self).__init__(**kwargs)
        if edges < 3:
            raise Exception('Not enough edges! (3+ only)')

        color = color or [random() for i in range(3)]
        rad_edge = (pi * 2) / float(edges)
        r_x = self.width / 2.0
        r_y = self.height / 2.0
        poly = []
        vertices = []
        for i in range(edges):
            # get points within a circle with radius of [r_x, r_y]
            x = cos(rad_edge * i) * r_x + self.center_x
            y = sin(rad_edge * i) * r_y + self.center_y
            poly.extend([x, y])

            # add UV layout zeros for Mesh, see Mesh docs
            vertices.extend([x, y, 0, 0])

        # draw Mesh shape from generated poly points
        with self.canvas:
            Color(rgba=(color[0], color[1], color[2], 0.6))
            self.shape = Mesh(
                pos=self.pos,
                vertices=vertices,
                indices=list(range(edges)),
                mode='triangle_fan'
            )
        self.poly = poly

    def on_touch_down(self, touch, *args):
        if self.shape_collide(*touch.pos):
            touch.grab(self)


class MeshShape(BaseShape):
    '''Starting from a custom origin and custom points, draw
    a convex Mesh shape with both touch and shape collisions.

    .. note::

        To get the points, use e.g. Pen tool from your favorite
        graphics editor and export it to a human readable format.
    '''

    def __init__(self, color=None, **kwargs):
        super(MeshShape, self).__init__(**kwargs)

        color = color or [random() for i in range(3)]
        min_x = 10000
        min_y = 10000
        max_x = 0
        max_y = 0

        # first point has to be the center of the convex shape's mass,
        # that's where the triangle fan starts from
        poly = [
            50, 50, 0, 0, 100, 0, 100, 100, 0, 100
        ] if not self.poly else self.poly

        # make the polygon smaller to fit 100x100 bounding box
        poly = [round(p / 1.5, 4) for p in poly]
        poly_len = len(poly)

        # create list of vertices & get edges of the polygon
        vertices = []
        vertices_len = 0
        for i in range(0, poly_len, 2):
            min_x = poly[i] if poly[i] < min_x else min_x
            min_y = poly[i + 1] if poly[i + 1] < min_y else min_y
            max_x = poly[i] if poly[i] > max_x else max_x
            max_y = poly[i + 1] if poly[i + 1] > max_y else max_y

            # add UV layout zeros for Mesh
            vertices_len += 4
            vertices.extend([poly[i], poly[i + 1], 0, 0])

        # get center of poly from edges
        poly_center_x, poly_center_y = [
            (max_x - min_x) / 2.0,
            (max_y - min_y) / 2.0
        ]

        # get distance from the widget's center and push the points to
        # the widget's origin, so that min_x and min_y for the poly would
        # result in 0 i.e.: points moved as close as possible to [0, 0]
        # -> No editor gives poly points moved to the origin directly
        dec_x = (self.center_x - poly_center_x) - min_x
        dec_y = (self.center_y - poly_center_y) - min_y

        # move polygon points to the bounding box (touch)
        for i in range(0, poly_len, 2):
            poly[i] += dec_x
            poly[i + 1] += dec_y

        # move mesh points to the bounding box (image)
        # has to contain the same points as polygon
        for i in range(0, vertices_len, 4):
            vertices[i] += dec_x
            vertices[i + 1] += dec_y

        # draw Mesh shape from generated poly points
        with self.canvas:
            Color(rgba=(color[0], color[1], color[2], 0.6))
            self.shape = Mesh(
                pos=self.pos,
                vertices=vertices,
                indices=list(range(int(poly_len / 2.0))),
                mode='triangle_fan'
            )
            # debug polygon points with Line to see the origin point
            # and intersections with the other points
            # Line(points=poly)
        self.poly = poly

    def on_touch_down(self, touch, *args):
        if self.shape_collide(*touch.pos):
            touch.grab(self)


class Collisions(App):
    def __init__(self, **kwargs):
        super(Collisions, self).__init__(**kwargs)
        # register an event for collision
        self.register_event_type('on_collision')

    def collision_circles(self, shapes=None, distance=100, debug=False, *args):
        '''Simple circle <-> circle collision between the shapes i.e. there's
        a simple line between the centers of the two shapes and the collision
        is only about measuring distance -> 1+ radii intersections.
        '''

        # get all combinations from all available shapes
        if not hasattr(self, 'combins'):
            self.combins = list(combinations(shapes, 2))

        for com in self.combins:
            x = (com[0].center_x - com[1].center_x) ** 2
            y = (com[0].center_y - com[1].center_y) ** 2
            if sqrt(x + y) <= distance:
                # dispatch a custom event if the objects collide
                self.dispatch('on_collision', (com[0], com[1]))

        # draw collider only if debugging
        if not debug:
            return

        # add circle collider only if the shape doesn't have one
        for shape in shapes:
            if shape.debug_collider is not None:
                continue

            d = distance / 2.0
            cx, cy = shape.center
            points = [(cx + d * cos(i), cy + d * sin(i)) for i in range(44)]
            points = [p for ps in points for p in ps]
            with shape.canvas:
                Color(rgba=(0, 1, 0, 1))
                shape.debug_collider = Point(points=points)

    def on_collision(self, pair, *args):
        '''Dispatched when objects collide, gives back colliding objects
        as a "pair" argument holding their instances.
        '''
        print('Collision {} x {}'.format(pair[0].name, pair[1].name))

    def build(self):
        # the environment for all 2D shapes
        scene = FloatLayout()

        # list of 2D shapes, starting with regular ones
        shapes = [
            RegularShape(
                name='Shape {}'.format(x), edges=x
            ) for x in range(3, 13)
        ]

        shapes.append(MeshShape(name='DefaultMesh'))
        shapes.append(MeshShape(name='Cloud', poly=cloud_poly))
        shapes.append(MeshShape(
            name='3QuarterCloud',
            poly=cloud_poly[:110]
        ))

        # move shapes to some random position
        for shape in shapes:
            shape.pos = [randint(50, i - 50) for i in Window.size]
            scene.add_widget(shape)

        # check for simple collisions between the shapes
        Clock.schedule_interval(
            lambda *t: self.collision_circles(shapes, debug=True), 0.1)
        return scene


if __name__ == '__main__':
    Collisions().run()