ttyengine 0.1.0__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.
- ttyengine/__init__.py +45 -0
- ttyengine/ansi.py +201 -0
- ttyengine/ascii_styler.py +473 -0
- ttyengine/components.py +4718 -0
- ttyengine/compositor.py +780 -0
- ttyengine/errors.py +181 -0
- ttyengine/figlet-LICENSE.txt +27 -0
- ttyengine/figlet.py +430 -0
- ttyengine/fonts.zip +0 -0
- ttyengine/image_reader.py +813 -0
- ttyengine/input_handler.py +814 -0
- ttyengine/launcher.py +822 -0
- ttyengine/region.py +180 -0
- ttyengine/scene.py +614 -0
- ttyengine/sound.py +1439 -0
- ttyengine/terminal.py +283 -0
- ttyengine-0.1.0.dist-info/METADATA +903 -0
- ttyengine-0.1.0.dist-info/RECORD +21 -0
- ttyengine-0.1.0.dist-info/WHEEL +5 -0
- ttyengine-0.1.0.dist-info/licenses/LICENSE +373 -0
- ttyengine-0.1.0.dist-info/top_level.txt +1 -0
ttyengine/components.py
ADDED
|
@@ -0,0 +1,4718 @@
|
|
|
1
|
+
#_owned - list of owned variables which will be inspected upon obj().create()
|
|
2
|
+
#_required - list of required components to be included upon obj().create()
|
|
3
|
+
#_default - bool of the component auto-inclusion upon obj().create()
|
|
4
|
+
#def on_discard() - function that will be called upon obj().discard()
|
|
5
|
+
|
|
6
|
+
#MODULES
|
|
7
|
+
import math, inspect, collections.abc, functools
|
|
8
|
+
from . import ansi
|
|
9
|
+
from .ascii_styler import alphaparse, alphamark, bgrecomp, fgrecomp
|
|
10
|
+
|
|
11
|
+
#GLOBAL
|
|
12
|
+
_NEIGHBOURS4 = ((-1, 0), (1, 0), (0, -1), (0, 1))
|
|
13
|
+
_NEIGHBOURS8 = _NEIGHBOURS4 + ((-1, -1), (1, -1), (-1, 1), (1, 1))
|
|
14
|
+
_NEIGHBOURS_HALF = ((1, 0), (0, 1), (1, 1), (-1, 1))
|
|
15
|
+
|
|
16
|
+
_EASINGS = {
|
|
17
|
+
'linear': lambda p: p,
|
|
18
|
+
'in': lambda p: p * p,
|
|
19
|
+
'out': lambda p: 1 - (1 - p) * (1 - p),
|
|
20
|
+
'smooth': lambda p: p * p * (3 - 2 * p),
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
def _easing(mode):
|
|
24
|
+
if callable(mode):
|
|
25
|
+
return mode
|
|
26
|
+
try:
|
|
27
|
+
return _EASINGS[mode]
|
|
28
|
+
except (KeyError, TypeError):
|
|
29
|
+
raise ValueError('ease must be a callable or one of: ' + ', '.join(_EASINGS))
|
|
30
|
+
|
|
31
|
+
@functools.lru_cache(maxsize=16)
|
|
32
|
+
def _offsets(radius, aspect):
|
|
33
|
+
span = int(radius / aspect) + 1
|
|
34
|
+
found = []
|
|
35
|
+
for dx in range(-span, span + 1):
|
|
36
|
+
for dy in range(-radius, radius + 1):
|
|
37
|
+
if dx or dy:
|
|
38
|
+
cost = math.hypot(dx * aspect, dy)
|
|
39
|
+
if cost <= radius:
|
|
40
|
+
found.append((cost - (0.01 if dy > 0 else 0), dx, dy))
|
|
41
|
+
found.sort()
|
|
42
|
+
return tuple((dx, dy) for _, dx, dy in found)
|
|
43
|
+
_SHAPES = ('circle', 'square', 'diamond')
|
|
44
|
+
|
|
45
|
+
@functools.lru_cache(maxsize=64)
|
|
46
|
+
def _footprint(radius, aspect, shape):
|
|
47
|
+
if shape not in _SHAPES:
|
|
48
|
+
raise ValueError('shape must be one of: ' + ', '.join(_SHAPES))
|
|
49
|
+
reach = max(1, radius) - 0.5
|
|
50
|
+
span_x = int(reach / aspect) + 1
|
|
51
|
+
span_y = int(reach) + 1
|
|
52
|
+
found = []
|
|
53
|
+
for dy in range(-span_y, span_y + 1):
|
|
54
|
+
for dx in range(-span_x, span_x + 1):
|
|
55
|
+
ux, uy = abs(dx) * aspect, abs(dy)
|
|
56
|
+
if shape == 'circle':
|
|
57
|
+
inside = math.hypot(ux, uy) < reach
|
|
58
|
+
elif shape == 'square':
|
|
59
|
+
inside = ux < reach and uy < reach
|
|
60
|
+
else:
|
|
61
|
+
inside = ux + uy < reach
|
|
62
|
+
if inside:
|
|
63
|
+
found.append((dx, dy))
|
|
64
|
+
return tuple(found)
|
|
65
|
+
|
|
66
|
+
_SHADOWS = ('physics', 'visual', 'hitbox')
|
|
67
|
+
|
|
68
|
+
def _check_shadow(value):
|
|
69
|
+
if value is not None and value not in _SHADOWS:
|
|
70
|
+
raise ValueError('shadow must be None or one of: ' + ', '.join(_SHADOWS))
|
|
71
|
+
return value
|
|
72
|
+
|
|
73
|
+
def _contact_point_cells(pixels, nx, ny, center):
|
|
74
|
+
n = len(pixels)
|
|
75
|
+
mx = sum(p[0] for p in pixels) / n
|
|
76
|
+
my = sum(p[1] for p in pixels) / n
|
|
77
|
+
if nx is None:
|
|
78
|
+
return (mx, my)
|
|
79
|
+
cx, cy = center
|
|
80
|
+
if ny:
|
|
81
|
+
xs = [p[0] for p in pixels]
|
|
82
|
+
return (max(min(xs) - 0.5, min(max(xs) + 0.5, cx)), my)
|
|
83
|
+
ys = [p[1] for p in pixels]
|
|
84
|
+
return (mx, max(min(ys) - 0.5, min(max(ys) + 0.5, cy)))
|
|
85
|
+
|
|
86
|
+
def _gradient_cells(pixels, center, sign, aspect):
|
|
87
|
+
cx, cy = center
|
|
88
|
+
sx = sy = 0.0
|
|
89
|
+
for x in range(math.ceil(cx - 2.5), math.floor(cx + 2.5) + 1):
|
|
90
|
+
for y in range(math.ceil(cy - 2.5), math.floor(cy + 2.5) + 1):
|
|
91
|
+
if (x, y) in pixels:
|
|
92
|
+
sx += (x - cx) * aspect
|
|
93
|
+
sy += y - cy
|
|
94
|
+
length = math.hypot(sx, sy)
|
|
95
|
+
if length < 1e-9:
|
|
96
|
+
return None
|
|
97
|
+
return sign * sx / length, sign * sy / length
|
|
98
|
+
|
|
99
|
+
def _contact_normal_cells(mine, theirs, point, nx, ny, inside, slope, aspect):
|
|
100
|
+
if not slope:
|
|
101
|
+
return nx, ny
|
|
102
|
+
shift = 0.5 if inside else -0.5
|
|
103
|
+
cx, cy = point
|
|
104
|
+
if abs(nx) > abs(ny):
|
|
105
|
+
cx += shift * (1 if nx > 0 else -1)
|
|
106
|
+
else:
|
|
107
|
+
cy += shift * (1 if ny > 0 else -1)
|
|
108
|
+
center = (round(cx * 2) / 2, round(cy * 2) / 2)
|
|
109
|
+
go = _gradient_cells(theirs, center, -1, aspect)
|
|
110
|
+
gs = _gradient_cells(mine, center, 1, aspect)
|
|
111
|
+
best = None
|
|
112
|
+
for g in (go, gs):
|
|
113
|
+
if g is None:
|
|
114
|
+
continue
|
|
115
|
+
d = g[0] * nx + g[1] * ny
|
|
116
|
+
if best is None or d > best[0]:
|
|
117
|
+
best = (d, g)
|
|
118
|
+
if best is None or best[0] < 0.25:
|
|
119
|
+
return nx, ny
|
|
120
|
+
return best[1]
|
|
121
|
+
|
|
122
|
+
def _mtv_cells(mine, other_pixels, aspect):
|
|
123
|
+
best = None
|
|
124
|
+
for dx, dy, cost in ((1, 0, aspect), (-1, 0, aspect), (0, 1, 1.0), (0, -1, 1.0)):
|
|
125
|
+
for k in range(1, 64):
|
|
126
|
+
if not ({(p[0] + dx * k, p[1] + dy * k) for p in mine} & other_pixels):
|
|
127
|
+
score = k * cost - (0.01 if dy == 1 else 0)
|
|
128
|
+
if best is None or score < best[0]:
|
|
129
|
+
best = (score, dx, dy, k)
|
|
130
|
+
break
|
|
131
|
+
return best
|
|
132
|
+
|
|
133
|
+
_SGR = ansi.SGR
|
|
134
|
+
_ALPHAS = functools.lru_cache(maxsize=4096)(alphaparse)
|
|
135
|
+
_HOLD = object()
|
|
136
|
+
|
|
137
|
+
def _unbake(baked, ref, alpha):
|
|
138
|
+
if alpha <= 0:
|
|
139
|
+
return baked
|
|
140
|
+
return tuple((baked[k] - ref[k] * (1 - alpha)) / alpha for k in range(3))
|
|
141
|
+
|
|
142
|
+
def _rebake(ref, source, alpha):
|
|
143
|
+
return tuple(max(0, min(255, int(round(ref[k] * (1 - alpha) + source[k] * alpha)))) for k in range(3))
|
|
144
|
+
_FGCODE = fgrecomp
|
|
145
|
+
_BGCODE = bgrecomp
|
|
146
|
+
|
|
147
|
+
def _argslen(cb):
|
|
148
|
+
params = inspect.signature(cb).parameters
|
|
149
|
+
l = len(params) - (1 if hasattr(cb, '__self__') else 0)
|
|
150
|
+
return l
|
|
151
|
+
|
|
152
|
+
def _closure_refs(fn, obj):
|
|
153
|
+
if not fn.__closure__:
|
|
154
|
+
return False
|
|
155
|
+
for cell in fn.__closure__:
|
|
156
|
+
try:
|
|
157
|
+
if cell.cell_contents is obj:
|
|
158
|
+
return True
|
|
159
|
+
except ValueError:
|
|
160
|
+
pass
|
|
161
|
+
return False
|
|
162
|
+
|
|
163
|
+
def _styleprop(name):
|
|
164
|
+
def fget(self):
|
|
165
|
+
return getattr(self, '_' + name)
|
|
166
|
+
def fset(self, value):
|
|
167
|
+
self._setstyle(name, value)
|
|
168
|
+
return property(fget, fset)
|
|
169
|
+
|
|
170
|
+
class _metainit(type):
|
|
171
|
+
def __new__(mcs, name, bases, attrs, **kwargs):
|
|
172
|
+
init_val = kwargs.pop('init', None)
|
|
173
|
+
cls = super().__new__(mcs, name, bases, attrs, **kwargs)
|
|
174
|
+
if init_val:
|
|
175
|
+
cls._target_init = init_val
|
|
176
|
+
return cls
|
|
177
|
+
def __call__(cls, *args, **kwargs):
|
|
178
|
+
inst = super().__call__(*args, **kwargs)
|
|
179
|
+
initfunc = getattr(cls, '_target_init', '_init')
|
|
180
|
+
instinit = getattr(inst, initfunc, None)
|
|
181
|
+
if instinit:
|
|
182
|
+
instinit()
|
|
183
|
+
return inst
|
|
184
|
+
|
|
185
|
+
class _change_callback(metaclass=_metainit, init='_chinit'):
|
|
186
|
+
def _chinit(self):
|
|
187
|
+
self._onchangecb = {}
|
|
188
|
+
self._changing = False
|
|
189
|
+
def cln():
|
|
190
|
+
discarded = self.obj
|
|
191
|
+
for other in self.obj.scene.objs:
|
|
192
|
+
com = getattr(other, self.__class__.__name__, None)
|
|
193
|
+
if other is discarded or not com or not hasattr(com, '_onchangecb'):
|
|
194
|
+
continue
|
|
195
|
+
com._onchangecb = {
|
|
196
|
+
cb:par for cb,par in com._onchangecb.items()
|
|
197
|
+
if not _closure_refs(cb, discarded)
|
|
198
|
+
}
|
|
199
|
+
if not hasattr(self, 'on_discard'):
|
|
200
|
+
self.on_discard = cln
|
|
201
|
+
else:
|
|
202
|
+
old_func = self.on_discard
|
|
203
|
+
def cln_():
|
|
204
|
+
try:
|
|
205
|
+
old_func(self)
|
|
206
|
+
except TypeError:
|
|
207
|
+
old_func()
|
|
208
|
+
cln()
|
|
209
|
+
self.on_discard = cln_
|
|
210
|
+
|
|
211
|
+
def onchange(self, cb, once=False):
|
|
212
|
+
if once:
|
|
213
|
+
match_attr = once if isinstance(once, str) else None
|
|
214
|
+
l = _argslen(cb)
|
|
215
|
+
fired = [False]
|
|
216
|
+
orig = cb
|
|
217
|
+
def wrapped(attr=None, old=None, internal=False):
|
|
218
|
+
if fired[0]: return
|
|
219
|
+
if match_attr is not None and attr != match_attr: return
|
|
220
|
+
fired[0] = True
|
|
221
|
+
if wrapped in self._onchangecb:
|
|
222
|
+
del self._onchangecb[wrapped]
|
|
223
|
+
if l > 2: orig(attr, old, internal)
|
|
224
|
+
elif l > 1: orig(attr, old)
|
|
225
|
+
elif l > 0: orig(attr)
|
|
226
|
+
else: orig()
|
|
227
|
+
cb = wrapped
|
|
228
|
+
self._onchangecb[cb] = 3
|
|
229
|
+
else:
|
|
230
|
+
self._onchangecb[cb] = _argslen(cb)
|
|
231
|
+
return cb
|
|
232
|
+
|
|
233
|
+
def offchange(self, cb):
|
|
234
|
+
self._onchangecb = {f:par for f,par in self._onchangecb.items() if f is not cb}
|
|
235
|
+
|
|
236
|
+
def _change(self, attr, old):
|
|
237
|
+
internal = self._changing
|
|
238
|
+
self._changing = True
|
|
239
|
+
try:
|
|
240
|
+
for cb, l in list(self._onchangecb.items()):
|
|
241
|
+
if l > 2:
|
|
242
|
+
cb(attr, old, internal)
|
|
243
|
+
elif l > 1:
|
|
244
|
+
cb(attr, old)
|
|
245
|
+
elif l > 0:
|
|
246
|
+
cb(attr)
|
|
247
|
+
else:
|
|
248
|
+
cb()
|
|
249
|
+
finally:
|
|
250
|
+
self._changing = internal
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
#COMPONENTS
|
|
254
|
+
class _lerpable:
|
|
255
|
+
_angles = frozenset()
|
|
256
|
+
_colors = frozenset()
|
|
257
|
+
|
|
258
|
+
def _lerpbase(self, key):
|
|
259
|
+
return None
|
|
260
|
+
|
|
261
|
+
def _lerpkeep(self, key, value):
|
|
262
|
+
return value
|
|
263
|
+
|
|
264
|
+
@staticmethod
|
|
265
|
+
def _lerpturn(current, target):
|
|
266
|
+
return (target - current + 540) % 360 - 180
|
|
267
|
+
|
|
268
|
+
def _lerpcolor(self, value):
|
|
269
|
+
if isinstance(value, list):
|
|
270
|
+
value = tuple(value)
|
|
271
|
+
return self.scene.styler.rgb(value)
|
|
272
|
+
|
|
273
|
+
def _lerpvalue(self, key, current, target, alpha):
|
|
274
|
+
if key in self._colors:
|
|
275
|
+
goal = self._lerpcolor(target)
|
|
276
|
+
if goal is None:
|
|
277
|
+
raise ValueError('unknown color: ' + repr(target))
|
|
278
|
+
start = self._lerpbase(key) if current is None else self._lerpcolor(current)
|
|
279
|
+
if start is None:
|
|
280
|
+
return target if alpha >= 1 else _HOLD
|
|
281
|
+
return tuple(int(round(a + (b - a) * alpha)) for a, b in zip(start, goal))
|
|
282
|
+
if current is None:
|
|
283
|
+
current = self._lerpbase(key)
|
|
284
|
+
if (isinstance(current, (tuple, list)) and isinstance(target, (tuple, list))
|
|
285
|
+
and len(current) == len(target)):
|
|
286
|
+
parts = [self._lerpvalue(key, a, b, alpha) for a, b in zip(current, target)]
|
|
287
|
+
if any(part is _HOLD for part in parts):
|
|
288
|
+
return target if alpha >= 1 else _HOLD
|
|
289
|
+
return type(current)(parts)
|
|
290
|
+
numeric = (int, float)
|
|
291
|
+
if (isinstance(current, numeric) and isinstance(target, numeric)
|
|
292
|
+
and not isinstance(current, bool) and not isinstance(target, bool)):
|
|
293
|
+
if key in self._angles:
|
|
294
|
+
return current + self._lerpturn(current, target) * alpha
|
|
295
|
+
return current + (target - current) * alpha
|
|
296
|
+
return target if alpha >= 1 else _HOLD
|
|
297
|
+
|
|
298
|
+
def lerp(self, weight=1, **kwargs):
|
|
299
|
+
for key, target in kwargs.items():
|
|
300
|
+
if not hasattr(self, key):
|
|
301
|
+
raise AttributeError('%s has no attribute %r' % (type(self).__name__, key))
|
|
302
|
+
if weight == 1:
|
|
303
|
+
value = target
|
|
304
|
+
else:
|
|
305
|
+
value = self._lerpvalue(key, getattr(self, key), target, weight)
|
|
306
|
+
if value is _HOLD:
|
|
307
|
+
continue
|
|
308
|
+
setattr(self, key, self._lerpkeep(key, value))
|
|
309
|
+
return self
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
class transform(_change_callback, _lerpable):
|
|
313
|
+
_angles = frozenset(('rotation',))
|
|
314
|
+
_colors = frozenset(('fg', 'bg'))
|
|
315
|
+
_owned = [
|
|
316
|
+
'x', 'y',
|
|
317
|
+
'vx', 'vy',
|
|
318
|
+
'text', 'rtext', 'htext',
|
|
319
|
+
'vtext', 'vrtext', 'vhtext',
|
|
320
|
+
'rotation',
|
|
321
|
+
'aspect',
|
|
322
|
+
'fg', 'bg', 'alpha', 'bgalpha',
|
|
323
|
+
'flip',
|
|
324
|
+
'shadow',
|
|
325
|
+
]
|
|
326
|
+
_default = True
|
|
327
|
+
_settings = ('x', 'y', 'text', 'rtext', 'htext', 'rotation', 'aspect', 'fg', 'bg', 'alpha', 'bgalpha',
|
|
328
|
+
'flip', 'shadow')
|
|
329
|
+
|
|
330
|
+
def __init__(self, obj, **kwargs):
|
|
331
|
+
self.obj = obj
|
|
332
|
+
self.scene = obj.scene
|
|
333
|
+
raw = kwargs.get('text', '')
|
|
334
|
+
ctext = obj.scene.recomp.sub('', raw) if raw else ''
|
|
335
|
+
self._x = kwargs.get('x', 0)
|
|
336
|
+
self._y = kwargs.get('y', 0)
|
|
337
|
+
self._vx = kwargs.get('vx', 0)
|
|
338
|
+
self._vy = kwargs.get('vy', 0)
|
|
339
|
+
self._fg = kwargs.get('fg', None)
|
|
340
|
+
self._bg = kwargs.get('bg', None)
|
|
341
|
+
self._alpha = kwargs.get('alpha', None)
|
|
342
|
+
self._bgalpha = kwargs.get('bgalpha', None)
|
|
343
|
+
self._opacity_cache = None
|
|
344
|
+
self._style_cache = {}
|
|
345
|
+
self._styled_for = None
|
|
346
|
+
self._fade = None
|
|
347
|
+
self._plain_layout = None
|
|
348
|
+
self.vtext = raw
|
|
349
|
+
self._vbase = raw
|
|
350
|
+
self._vrtext = raw
|
|
351
|
+
self.vhtext = raw
|
|
352
|
+
self._text = ctext
|
|
353
|
+
self._rtext = ctext
|
|
354
|
+
self._htext = ctext
|
|
355
|
+
self._rotation = kwargs.get('rotation', 0)
|
|
356
|
+
self._rot_cache_angle = None
|
|
357
|
+
self._aspect = kwargs.get('aspect', None)
|
|
358
|
+
self._flip = kwargs.get('flip', None)
|
|
359
|
+
self._shadow = _check_shadow(kwargs.get('shadow', None))
|
|
360
|
+
self._cells_cache = None
|
|
361
|
+
self._anim = None
|
|
362
|
+
self._onsplit = {}
|
|
363
|
+
self._xlen_cache = None
|
|
364
|
+
self._ylen_cache = None
|
|
365
|
+
self._parse_pixels_key = None
|
|
366
|
+
self._parse_pixels_val = None
|
|
367
|
+
self._centroid_cache = {}
|
|
368
|
+
self._bg_cache = {}
|
|
369
|
+
self._rotmap = None
|
|
370
|
+
self._rotcells_v = {}
|
|
371
|
+
self._rotsrc_v = {}
|
|
372
|
+
self._rotmaps_ok = True
|
|
373
|
+
self._rotangle = 0
|
|
374
|
+
self._rotsize = (0, 0)
|
|
375
|
+
self._rotorigin = (0, 0)
|
|
376
|
+
self._ptext = ctext
|
|
377
|
+
self._pvx = 0
|
|
378
|
+
self._pvy = 0
|
|
379
|
+
self._pkey = None
|
|
380
|
+
self._onchangecb = {}
|
|
381
|
+
self._changing = False
|
|
382
|
+
self._vrtext = self._restyle(raw)
|
|
383
|
+
self.rotate(self._rotation, init=True)
|
|
384
|
+
if 'htext' in kwargs:
|
|
385
|
+
self.htext = kwargs['htext']
|
|
386
|
+
elif 'rtext' in kwargs:
|
|
387
|
+
self.rtext = kwargs['rtext']
|
|
388
|
+
|
|
389
|
+
def __getitem__(self, key):
|
|
390
|
+
return getattr(self, key)
|
|
391
|
+
|
|
392
|
+
def __setitem__(self, key, value):
|
|
393
|
+
setattr(self, key, value)
|
|
394
|
+
|
|
395
|
+
def _invalidate(self):
|
|
396
|
+
self._xlen_cache = None
|
|
397
|
+
self._ylen_cache = None
|
|
398
|
+
self._parse_pixels_key = None
|
|
399
|
+
self._parse_pixels_val = None
|
|
400
|
+
self._centroid_cache = {}
|
|
401
|
+
self._rot_cache_angle = None
|
|
402
|
+
|
|
403
|
+
@property
|
|
404
|
+
def x(self):
|
|
405
|
+
return self._x
|
|
406
|
+
@x.setter
|
|
407
|
+
def x(self, value):
|
|
408
|
+
old = self._x
|
|
409
|
+
self._x = value
|
|
410
|
+
self._change('x', old)
|
|
411
|
+
|
|
412
|
+
@property
|
|
413
|
+
def y(self):
|
|
414
|
+
return self._y
|
|
415
|
+
@y.setter
|
|
416
|
+
def y(self, value):
|
|
417
|
+
old = self._y
|
|
418
|
+
self._y = value
|
|
419
|
+
self._change('y', old)
|
|
420
|
+
|
|
421
|
+
@property
|
|
422
|
+
def vx(self):
|
|
423
|
+
return self._vx
|
|
424
|
+
@vx.setter
|
|
425
|
+
def vx(self, value):
|
|
426
|
+
old = self._vx
|
|
427
|
+
self._vx = value
|
|
428
|
+
self._change('vx', old)
|
|
429
|
+
|
|
430
|
+
@property
|
|
431
|
+
def vy(self):
|
|
432
|
+
return self._vy
|
|
433
|
+
@vy.setter
|
|
434
|
+
def vy(self, value):
|
|
435
|
+
old = self._vy
|
|
436
|
+
self._vy = value
|
|
437
|
+
self._change('vy', old)
|
|
438
|
+
|
|
439
|
+
@property
|
|
440
|
+
def text(self):
|
|
441
|
+
return self._text
|
|
442
|
+
@text.setter
|
|
443
|
+
def text(self, value):
|
|
444
|
+
old = self._text
|
|
445
|
+
self._anim = None
|
|
446
|
+
ctext = self.scene.recomp.sub('', value)
|
|
447
|
+
self.vrtext = value
|
|
448
|
+
self.vhtext = value
|
|
449
|
+
self._htext = ctext
|
|
450
|
+
self._rtext = ctext
|
|
451
|
+
self._invalidate()
|
|
452
|
+
self.rotate(self._rotation)
|
|
453
|
+
self._change('text', old)
|
|
454
|
+
|
|
455
|
+
@property
|
|
456
|
+
def rtext(self):
|
|
457
|
+
return self._rtext
|
|
458
|
+
@rtext.setter
|
|
459
|
+
def rtext(self, value):
|
|
460
|
+
old = self._rtext
|
|
461
|
+
ctext = self.scene.recomp.sub('', value)
|
|
462
|
+
self.vrtext = value
|
|
463
|
+
self._rtext = ctext
|
|
464
|
+
self._invalidate()
|
|
465
|
+
self.rotate(self._rotation)
|
|
466
|
+
self._change('rtext', old)
|
|
467
|
+
|
|
468
|
+
@property
|
|
469
|
+
def htext(self):
|
|
470
|
+
return self._htext
|
|
471
|
+
@htext.setter
|
|
472
|
+
def htext(self, value):
|
|
473
|
+
old = self._htext
|
|
474
|
+
self._anim = None
|
|
475
|
+
ctext = self.scene.recomp.sub('', value)
|
|
476
|
+
self.vrtext = value
|
|
477
|
+
self.vhtext = value
|
|
478
|
+
self._htext = ctext
|
|
479
|
+
self._rtext = ctext
|
|
480
|
+
self._invalidate()
|
|
481
|
+
self.rotate(self._rotation)
|
|
482
|
+
self._change('htext', old)
|
|
483
|
+
|
|
484
|
+
@property
|
|
485
|
+
def ptext(self):
|
|
486
|
+
return self._ptext
|
|
487
|
+
|
|
488
|
+
@property
|
|
489
|
+
def pvx(self):
|
|
490
|
+
return self._pvx
|
|
491
|
+
|
|
492
|
+
@property
|
|
493
|
+
def pvy(self):
|
|
494
|
+
return self._pvy
|
|
495
|
+
|
|
496
|
+
@property
|
|
497
|
+
def rotation(self):
|
|
498
|
+
return self._rotation
|
|
499
|
+
@rotation.setter
|
|
500
|
+
def rotation(self, value):
|
|
501
|
+
old = self._rotation
|
|
502
|
+
oldt = self._text
|
|
503
|
+
self._rotation = value
|
|
504
|
+
self.rotate(value)
|
|
505
|
+
self._change('rotation', old)
|
|
506
|
+
self._change('text', oldt)
|
|
507
|
+
|
|
508
|
+
@property
|
|
509
|
+
def aspect(self):
|
|
510
|
+
return self.scene.aspect if self._aspect is None else self._aspect
|
|
511
|
+
@aspect.setter
|
|
512
|
+
def aspect(self, value):
|
|
513
|
+
old = self._aspect
|
|
514
|
+
oldt = self._text
|
|
515
|
+
self._aspect = value
|
|
516
|
+
self._invalidate()
|
|
517
|
+
self.rotate(self._rotation)
|
|
518
|
+
self._change('aspect', old)
|
|
519
|
+
self._change('text', oldt)
|
|
520
|
+
|
|
521
|
+
@property
|
|
522
|
+
def flip(self):
|
|
523
|
+
return self._flip
|
|
524
|
+
@flip.setter
|
|
525
|
+
def flip(self, value):
|
|
526
|
+
old = self._flip
|
|
527
|
+
oldt = self._text
|
|
528
|
+
self._flip = value
|
|
529
|
+
self._invalidate()
|
|
530
|
+
self.rotate(self._rotation)
|
|
531
|
+
self._change('flip', old)
|
|
532
|
+
self._change('text', oldt)
|
|
533
|
+
|
|
534
|
+
@property
|
|
535
|
+
def shadow(self):
|
|
536
|
+
return self._shadow
|
|
537
|
+
@shadow.setter
|
|
538
|
+
def shadow(self, value):
|
|
539
|
+
old = self._shadow
|
|
540
|
+
self._shadow = _check_shadow(value)
|
|
541
|
+
self._change('shadow', old)
|
|
542
|
+
|
|
543
|
+
@property
|
|
544
|
+
def vrtext(self):
|
|
545
|
+
return self._vrtext
|
|
546
|
+
@vrtext.setter
|
|
547
|
+
def vrtext(self, value):
|
|
548
|
+
self._vbase = value
|
|
549
|
+
self._vrtext = self._restyle(value)
|
|
550
|
+
|
|
551
|
+
fg = _styleprop('fg')
|
|
552
|
+
bg = _styleprop('bg')
|
|
553
|
+
alpha = _styleprop('alpha')
|
|
554
|
+
bgalpha = _styleprop('bgalpha')
|
|
555
|
+
|
|
556
|
+
def _setstyle(self, key, value):
|
|
557
|
+
self._fade = None
|
|
558
|
+
self._applystyle(**{key: value})
|
|
559
|
+
|
|
560
|
+
def _applystyle(self, **changes):
|
|
561
|
+
olds = {key: getattr(self, '_' + key) for key in changes}
|
|
562
|
+
for key, value in changes.items():
|
|
563
|
+
setattr(self, '_' + key, value)
|
|
564
|
+
self._style_cache = {}
|
|
565
|
+
self._vrtext = self._restyle(self._vbase)
|
|
566
|
+
self._vrotate()
|
|
567
|
+
for key, old in olds.items():
|
|
568
|
+
self._change(key, old)
|
|
569
|
+
|
|
570
|
+
def _stylecheck(self):
|
|
571
|
+
if self._styled_for is not None and self._styled_for != (self.scene.bgcolor, self.scene.fgcolor):
|
|
572
|
+
self._style_cache = {}
|
|
573
|
+
self._vrtext = self._restyle(self._vbase)
|
|
574
|
+
self._vrotate()
|
|
575
|
+
|
|
576
|
+
def _styled(self, style):
|
|
577
|
+
cached = self._style_cache.get(style)
|
|
578
|
+
if cached is None:
|
|
579
|
+
sample = self.scene.styler.style(style + 'x', color=self._fg, alpha=self._alpha,
|
|
580
|
+
bgcolor=self._bg, bgalpha=self._bgalpha, onbg=True, clean=True)
|
|
581
|
+
cached = sample[:sample.index('x')]
|
|
582
|
+
self._style_cache[style] = cached
|
|
583
|
+
return cached
|
|
584
|
+
|
|
585
|
+
def _restyle(self, base):
|
|
586
|
+
if self._fg is None and self._bg is None and self._alpha is None and self._bgalpha is None:
|
|
587
|
+
self._styled_for = None
|
|
588
|
+
return base
|
|
589
|
+
self._styled_for = (self.scene.bgcolor, self.scene.fgcolor)
|
|
590
|
+
pixels = self.parse_pixels(base)
|
|
591
|
+
last = {}
|
|
592
|
+
widths = {}
|
|
593
|
+
for (ci, ri), (char, _) in pixels.items():
|
|
594
|
+
widths[ri] = max(widths.get(ri, 0), ci + 1)
|
|
595
|
+
if char != ' ':
|
|
596
|
+
last[ri] = max(last.get(ri, -1), ci)
|
|
597
|
+
restyled = {}
|
|
598
|
+
for (ci, ri), (char, style) in pixels.items():
|
|
599
|
+
restyled[(ci, ri)] = (char, self._styled(style) if ci <= last.get(ri, -1) else style)
|
|
600
|
+
return '\n'.join(self.build_rows(restyled, 0, widths.get(ri, 0) - 1, ri, ri)
|
|
601
|
+
for ri in range(len(base.splitlines())))
|
|
602
|
+
|
|
603
|
+
def _has_background(self, style):
|
|
604
|
+
cached = self._bg_cache.get(style)
|
|
605
|
+
if cached is not None:
|
|
606
|
+
return cached
|
|
607
|
+
found = False
|
|
608
|
+
for match in _SGR.finditer(style):
|
|
609
|
+
params = [int(v) if v else 0 for v in match.group(1).split(';')]
|
|
610
|
+
i = 0
|
|
611
|
+
while i < len(params):
|
|
612
|
+
v = params[i]
|
|
613
|
+
if v == 48 or 40 <= v <= 47 or 100 <= v <= 107:
|
|
614
|
+
found = True
|
|
615
|
+
break
|
|
616
|
+
if v == 38:
|
|
617
|
+
i += 2 if i + 1 < len(params) and params[i + 1] == 5 else 4
|
|
618
|
+
i += 1
|
|
619
|
+
if found:
|
|
620
|
+
break
|
|
621
|
+
self._bg_cache[style] = found
|
|
622
|
+
return found
|
|
623
|
+
|
|
624
|
+
def _pixel_centroid(self, txt):
|
|
625
|
+
cached = self._centroid_cache.get(id(txt))
|
|
626
|
+
if cached is not None and cached[0] == txt:
|
|
627
|
+
return cached[1]
|
|
628
|
+
clean = self.scene.recomp.sub('', txt)
|
|
629
|
+
xs, ys = [], []
|
|
630
|
+
for ri, row in enumerate(clean.splitlines()):
|
|
631
|
+
for ci, char in enumerate(row):
|
|
632
|
+
if char not in (' ', '\n'):
|
|
633
|
+
xs.append(ci)
|
|
634
|
+
ys.append(ri)
|
|
635
|
+
result = (sum(xs) / len(xs), sum(ys) / len(ys)) if xs else (0.0, 0.0)
|
|
636
|
+
self._centroid_cache[id(txt)] = (txt, result)
|
|
637
|
+
return result
|
|
638
|
+
|
|
639
|
+
def parse_pixels(self, text):
|
|
640
|
+
if self._parse_pixels_key is not None and self._parse_pixels_key == text:
|
|
641
|
+
return self._parse_pixels_val
|
|
642
|
+
pixels = {}
|
|
643
|
+
current_style = ''
|
|
644
|
+
for ri, line in enumerate(text.splitlines()):
|
|
645
|
+
ci = 0
|
|
646
|
+
for part in self.scene.recomp.split(line):
|
|
647
|
+
if not part:
|
|
648
|
+
continue
|
|
649
|
+
if self.scene.recomp.match(part):
|
|
650
|
+
if part == ansi.RESET:
|
|
651
|
+
current_style = ''
|
|
652
|
+
else:
|
|
653
|
+
current_style += part
|
|
654
|
+
else:
|
|
655
|
+
for char in part:
|
|
656
|
+
pixels[(ci, ri)] = (char, current_style)
|
|
657
|
+
ci += 1
|
|
658
|
+
self._parse_pixels_key = text
|
|
659
|
+
self._parse_pixels_val = pixels
|
|
660
|
+
return pixels
|
|
661
|
+
|
|
662
|
+
def build_rows(self, pixels, min_x, max_x, min_y, max_y):
|
|
663
|
+
out_rows = []
|
|
664
|
+
for ri in range(min_y, max_y + 1):
|
|
665
|
+
row = ''
|
|
666
|
+
prev_style = ''
|
|
667
|
+
for ci in range(min_x, max_x + 1):
|
|
668
|
+
if (ci, ri) in pixels:
|
|
669
|
+
char, style = pixels[(ci, ri)]
|
|
670
|
+
if style != prev_style:
|
|
671
|
+
row += (ansi.RESET if prev_style else '') + style
|
|
672
|
+
prev_style = style
|
|
673
|
+
row += char
|
|
674
|
+
else:
|
|
675
|
+
if prev_style:
|
|
676
|
+
row += ansi.RESET
|
|
677
|
+
prev_style = ''
|
|
678
|
+
row += ' '
|
|
679
|
+
if prev_style:
|
|
680
|
+
row += ansi.RESET
|
|
681
|
+
out_rows.append(row)
|
|
682
|
+
return '\n'.join(out_rows)
|
|
683
|
+
|
|
684
|
+
def merge(self, *layers, dx=0, dy=0, center=True):
|
|
685
|
+
items = []
|
|
686
|
+
for layer in layers:
|
|
687
|
+
if isinstance(layer, str):
|
|
688
|
+
items.append((layer, 0, 0))
|
|
689
|
+
else:
|
|
690
|
+
text, ox, oy = (tuple(layer) + (0, 0))[:3]
|
|
691
|
+
items.append((text, ox, oy))
|
|
692
|
+
if len(items) == 1:
|
|
693
|
+
items.insert(0, (self._vrtext, 0, 0))
|
|
694
|
+
floor = math.floor
|
|
695
|
+
|
|
696
|
+
def middle(pixels):
|
|
697
|
+
cells = [p for p, (char, _) in pixels.items() if char != ' '] or list(pixels)
|
|
698
|
+
if not cells:
|
|
699
|
+
return None
|
|
700
|
+
xs = [p[0] for p in cells]
|
|
701
|
+
ys = [p[1] for p in cells]
|
|
702
|
+
return (min(xs) + max(xs)) / 2, (min(ys) + max(ys)) / 2
|
|
703
|
+
|
|
704
|
+
merged = dict(self.parse_pixels(items[0][0]))
|
|
705
|
+
anchor = middle(merged)
|
|
706
|
+
for text, ox, oy in items[1:]:
|
|
707
|
+
pixels = self.parse_pixels(text)
|
|
708
|
+
mid = middle(pixels)
|
|
709
|
+
sx = ox + dx
|
|
710
|
+
sy = -(oy + dy)
|
|
711
|
+
if center and anchor is not None and mid is not None:
|
|
712
|
+
sx += floor(anchor[0] - mid[0] + 0.5)
|
|
713
|
+
sy += floor(anchor[1] - mid[1] + 0.5)
|
|
714
|
+
for (ci, ri), (char, style) in pixels.items():
|
|
715
|
+
if char != ' ' or (style and self._has_background(style)):
|
|
716
|
+
merged[(ci + sx, ri + sy)] = (char, style)
|
|
717
|
+
if not merged:
|
|
718
|
+
return ''
|
|
719
|
+
xs = [p[0] for p in merged]
|
|
720
|
+
ys = [p[1] for p in merged]
|
|
721
|
+
return self.build_rows(merged, min(xs), max(xs), min(ys), max(ys))
|
|
722
|
+
|
|
723
|
+
def cells(self, mode=None):
|
|
724
|
+
mode = self._shadow if mode is None else _check_shadow(mode)
|
|
725
|
+
p = getattr(self.obj, 'physics', None)
|
|
726
|
+
if mode in (None, 'physics'):
|
|
727
|
+
if p is not None:
|
|
728
|
+
return p.getpixels()
|
|
729
|
+
text, ox, oy, full = self._ptext, self._pvx, self._pvy, False
|
|
730
|
+
elif mode == 'hitbox':
|
|
731
|
+
text, ox, oy, full = self._htext, 0, 0, True
|
|
732
|
+
else:
|
|
733
|
+
text, ox, oy, full = self._text, self.vx, self.vy, False
|
|
734
|
+
rx = round(self._x + ox)
|
|
735
|
+
ry = round(self._y + oy)
|
|
736
|
+
key = (full, rx, ry, text)
|
|
737
|
+
cached = self._cells_cache
|
|
738
|
+
if cached is not None and cached[0] == key:
|
|
739
|
+
return cached[1]
|
|
740
|
+
found = set()
|
|
741
|
+
for i, row in enumerate(text.splitlines()):
|
|
742
|
+
for j, char in enumerate(row):
|
|
743
|
+
if full or char != ' ':
|
|
744
|
+
found.add((rx + j, ry - i))
|
|
745
|
+
self._cells_cache = (key, found)
|
|
746
|
+
return found
|
|
747
|
+
|
|
748
|
+
def collides(self, objs, mode=None):
|
|
749
|
+
mine = self.cells(mode)
|
|
750
|
+
if not isinstance(objs, (list, tuple, set)):
|
|
751
|
+
objs = [objs]
|
|
752
|
+
found = {}
|
|
753
|
+
if not mine:
|
|
754
|
+
return found
|
|
755
|
+
for other in objs:
|
|
756
|
+
other = getattr(other, 'obj', other)
|
|
757
|
+
ot = getattr(other, 'transform', None)
|
|
758
|
+
if other is self.obj or ot is None:
|
|
759
|
+
continue
|
|
760
|
+
inter = mine & ot.cells(mode)
|
|
761
|
+
if inter:
|
|
762
|
+
found[other] = inter
|
|
763
|
+
return found
|
|
764
|
+
|
|
765
|
+
def normal(self, other, local=None):
|
|
766
|
+
me = self.obj if local is None else getattr(local, 'obj', local)
|
|
767
|
+
other = getattr(other, 'obj', other)
|
|
768
|
+
mine = me.transform.cells()
|
|
769
|
+
theirs = other.transform.cells()
|
|
770
|
+
aspect = self.scene.aspect
|
|
771
|
+
slope = all(getattr(getattr(o, 'physics', None), 'slope', True) for o in (me, other))
|
|
772
|
+
inside = bool(mine & theirs)
|
|
773
|
+
if inside:
|
|
774
|
+
best = _mtv_cells(mine, theirs, aspect)
|
|
775
|
+
if best is None:
|
|
776
|
+
return None
|
|
777
|
+
nx, ny = best[1], best[2]
|
|
778
|
+
contact = mine & theirs
|
|
779
|
+
else:
|
|
780
|
+
found = None
|
|
781
|
+
for dx, dy in ((1, 0), (-1, 0), (0, 1), (0, -1)):
|
|
782
|
+
touching = [q for q in mine if (q[0] + dx, q[1] + dy) in theirs]
|
|
783
|
+
if touching and (found is None or len(touching) > len(found[2])):
|
|
784
|
+
found = (-dx, -dy, touching)
|
|
785
|
+
if found is None:
|
|
786
|
+
return None
|
|
787
|
+
nx, ny, contact = found
|
|
788
|
+
n = len(mine)
|
|
789
|
+
center = (sum(q[0] for q in mine) / n, sum(q[1] for q in mine) / n)
|
|
790
|
+
point = _contact_point_cells(contact, nx, ny, center)
|
|
791
|
+
gx, gy = _contact_normal_cells(mine, theirs, point, nx, ny, inside, slope, aspect)
|
|
792
|
+
return math.degrees(math.atan2(gx, gy)) % 360
|
|
793
|
+
|
|
794
|
+
def tolocal(self, pixels):
|
|
795
|
+
rx = round(self._x + self.vx)
|
|
796
|
+
ry = round(self._y + self.vy)
|
|
797
|
+
local_pixels = set()
|
|
798
|
+
for gx, gy in pixels:
|
|
799
|
+
local_pixels.add((gx - rx, ry - gy))
|
|
800
|
+
return local_pixels
|
|
801
|
+
|
|
802
|
+
def toworld(self, pixels):
|
|
803
|
+
rx = round(self._x + self.vx)
|
|
804
|
+
ry = round(self._y + self.vy)
|
|
805
|
+
local_pixels = set()
|
|
806
|
+
for gx, gy in pixels:
|
|
807
|
+
local_pixels.add((gx + rx, ry + gy))
|
|
808
|
+
return local_pixels
|
|
809
|
+
|
|
810
|
+
def _rotator(self, angle_degrees):
|
|
811
|
+
angle = math.radians(angle_degrees)
|
|
812
|
+
cos_a = math.cos(angle)
|
|
813
|
+
sin_a = math.sin(angle)
|
|
814
|
+
if self._flip is None:
|
|
815
|
+
ma, mb, mc, md = cos_a, -sin_a, sin_a, cos_a
|
|
816
|
+
ja, jb, jc, jd = cos_a, sin_a, -sin_a, cos_a
|
|
817
|
+
else:
|
|
818
|
+
mirror = math.radians(2 * self._flip)
|
|
819
|
+
fa, fb, fc, fd = -math.cos(mirror), -math.sin(mirror), -math.sin(mirror), math.cos(mirror)
|
|
820
|
+
ma, mb = cos_a * fa - sin_a * fc, cos_a * fb - sin_a * fd
|
|
821
|
+
mc, md = sin_a * fa + cos_a * fc, sin_a * fb + cos_a * fd
|
|
822
|
+
ja, jb = fa * cos_a - fb * sin_a, fa * sin_a + fb * cos_a
|
|
823
|
+
jc, jd = fc * cos_a - fd * sin_a, fc * sin_a + fd * cos_a
|
|
824
|
+
ASPECT = self.aspect
|
|
825
|
+
floor = math.floor
|
|
826
|
+
ident = self._flip is None and angle_degrees % 360 == 0
|
|
827
|
+
|
|
828
|
+
def layout(source):
|
|
829
|
+
glyphs = [p for p, px in source.items() if px[0] != ' ']
|
|
830
|
+
if not glyphs:
|
|
831
|
+
return None
|
|
832
|
+
count = len(glyphs)
|
|
833
|
+
cx = sum(p[0] for p in glyphs) / count
|
|
834
|
+
cy = sum(p[1] for p in glyphs) / count
|
|
835
|
+
if ident:
|
|
836
|
+
xs = [p[0] for p in source]
|
|
837
|
+
ys = [p[1] for p in glyphs]
|
|
838
|
+
return glyphs, cx, cy, {p: p for p in glyphs}, {}, min(xs), max(xs), min(ys), max(ys)
|
|
839
|
+
|
|
840
|
+
spots = []
|
|
841
|
+
for ci, ri in glyphs:
|
|
842
|
+
u = (ci - cx) * ASPECT
|
|
843
|
+
v = ri - cy
|
|
844
|
+
fx = cx + (u * ma + v * mb) / ASPECT
|
|
845
|
+
fy = cy + (u * mc + v * md)
|
|
846
|
+
gx = floor(fx + 0.5 + 1e-7)
|
|
847
|
+
gy = floor(fy + 0.5 + 1e-7)
|
|
848
|
+
spots.append((((fx - gx) * ASPECT) ** 2 + (fy - gy) ** 2, ri, ci, fx, fy, gx, gy))
|
|
849
|
+
spots.sort()
|
|
850
|
+
|
|
851
|
+
placed = {}
|
|
852
|
+
loose = []
|
|
853
|
+
for spot in spots:
|
|
854
|
+
cell = (spot[5], spot[6])
|
|
855
|
+
if cell in placed:
|
|
856
|
+
loose.append(spot)
|
|
857
|
+
else:
|
|
858
|
+
placed[cell] = (spot[2], spot[1])
|
|
859
|
+
for _, ri, ci, fx, fy, gx, gy in loose:
|
|
860
|
+
reach_x = 2
|
|
861
|
+
reach_y = 1
|
|
862
|
+
best = None
|
|
863
|
+
while best is None:
|
|
864
|
+
for ny in range(gy - reach_y, gy + reach_y + 1):
|
|
865
|
+
for nx in range(gx - reach_x, gx + reach_x + 1):
|
|
866
|
+
if (nx, ny) in placed:
|
|
867
|
+
continue
|
|
868
|
+
rx = (nx - cx) * ASPECT
|
|
869
|
+
ry = ny - cy
|
|
870
|
+
hit = (floor((rx * ja + ry * jb) / ASPECT + cx + 0.5 + 1e-7),
|
|
871
|
+
floor(rx * jc + ry * jd + cy + 0.5 + 1e-7)) in source
|
|
872
|
+
cost = ((nx - fx) * ASPECT) ** 2 + (ny - fy) ** 2 - 0.02 * (nx - fx) * ASPECT + (0 if hit else 1)
|
|
873
|
+
key = (round(cost, 6), ny, -nx)
|
|
874
|
+
if best is None or key < best:
|
|
875
|
+
best = key
|
|
876
|
+
reach_x *= 2
|
|
877
|
+
reach_y *= 2
|
|
878
|
+
placed[(-best[2], best[1])] = (ci, ri)
|
|
879
|
+
|
|
880
|
+
where = {src: cell for cell, src in placed.items()}
|
|
881
|
+
extra = {}
|
|
882
|
+
for (ci, ri), a in where.items():
|
|
883
|
+
for dx, dy in _NEIGHBOURS_HALF:
|
|
884
|
+
b = where.get((ci + dx, ri + dy))
|
|
885
|
+
if b is None:
|
|
886
|
+
continue
|
|
887
|
+
ex = b[0] - a[0]
|
|
888
|
+
ey = b[1] - a[1]
|
|
889
|
+
if -1 <= ex <= 1 and -1 <= ey <= 1:
|
|
890
|
+
continue
|
|
891
|
+
steps = max(abs(ex), abs(ey))
|
|
892
|
+
for k in range(1, steps):
|
|
893
|
+
f = k / steps
|
|
894
|
+
cell = (floor(a[0] + ex * f + 0.5 + 1e-7), floor(a[1] + ey * f + 0.5 + 1e-7))
|
|
895
|
+
if cell not in placed and cell not in extra:
|
|
896
|
+
extra[cell] = (ci, ri) if f < 0.5 else (ci + dx, ri + dy)
|
|
897
|
+
|
|
898
|
+
around = {}
|
|
899
|
+
for group in (placed, extra):
|
|
900
|
+
for x, y in group:
|
|
901
|
+
for dx, dy in _NEIGHBOURS4:
|
|
902
|
+
c = (x + dx, y + dy)
|
|
903
|
+
around[c] = around.get(c, 0) + 1
|
|
904
|
+
for (nx, ny), n in around.items():
|
|
905
|
+
if n < 4 or (nx, ny) in placed or (nx, ny) in extra:
|
|
906
|
+
continue
|
|
907
|
+
rx = (nx - cx) * ASPECT
|
|
908
|
+
ry = ny - cy
|
|
909
|
+
ci = floor((rx * ja + ry * jb) / ASPECT + cx + 0.5 + 1e-7)
|
|
910
|
+
ri = floor(rx * jc + ry * jd + cy + 0.5 + 1e-7)
|
|
911
|
+
px = source.get((ci, ri))
|
|
912
|
+
if px is not None and px[0] != ' ':
|
|
913
|
+
extra[(nx, ny)] = (ci, ri)
|
|
914
|
+
|
|
915
|
+
min_x = min(c[0] for c in placed)
|
|
916
|
+
max_x = max(c[0] for c in placed)
|
|
917
|
+
min_y = min(c[1] for c in placed)
|
|
918
|
+
max_y = max(c[1] for c in placed)
|
|
919
|
+
for cell in extra:
|
|
920
|
+
if cell[0] < min_x:
|
|
921
|
+
min_x = cell[0]
|
|
922
|
+
if cell[0] > max_x:
|
|
923
|
+
max_x = cell[0]
|
|
924
|
+
if cell[1] < min_y:
|
|
925
|
+
min_y = cell[1]
|
|
926
|
+
if cell[1] > max_y:
|
|
927
|
+
max_y = cell[1]
|
|
928
|
+
for (ci, ri), px in source.items():
|
|
929
|
+
if px[0] != ' ':
|
|
930
|
+
continue
|
|
931
|
+
gx = floor(cx + (((ci - cx) * ASPECT) * ma + (ri - cy) * mb) / ASPECT + 0.5 + 1e-7)
|
|
932
|
+
if gx < min_x:
|
|
933
|
+
min_x = gx
|
|
934
|
+
if gx > max_x:
|
|
935
|
+
max_x = gx
|
|
936
|
+
return glyphs, cx, cy, placed, extra, min_x, max_x, min_y, max_y
|
|
937
|
+
|
|
938
|
+
def render(source, lay, record=False, size=(0, 0), maps=False):
|
|
939
|
+
glyphs, cx, cy, placed, extra, min_x, max_x, min_y, max_y = lay
|
|
940
|
+
new_pixels = {}
|
|
941
|
+
cells = {}
|
|
942
|
+
sources = {}
|
|
943
|
+
resample = not ident and len({px[1] for px in source.values() if px[0] != ' '}) > 1
|
|
944
|
+
for group in (placed, extra):
|
|
945
|
+
for cell, src in group.items():
|
|
946
|
+
chosen = source[src]
|
|
947
|
+
if resample:
|
|
948
|
+
rx = (cell[0] - cx) * ASPECT
|
|
949
|
+
ry = cell[1] - cy
|
|
950
|
+
near = source.get((floor((rx * ja + ry * jb) / ASPECT + cx + 0.5 + 1e-7),
|
|
951
|
+
floor(rx * jc + ry * jd + cy + 0.5 + 1e-7)))
|
|
952
|
+
if near is not None and near[0] == chosen[0]:
|
|
953
|
+
chosen = near
|
|
954
|
+
new_pixels[cell] = chosen
|
|
955
|
+
if maps:
|
|
956
|
+
cells[cell] = [src]
|
|
957
|
+
sources.setdefault(src, []).append(cell)
|
|
958
|
+
if any(px[0] == ' ' and px[1] and self._has_background(px[1]) for px in source.values()):
|
|
959
|
+
for ny in range(min_y, max_y + 1):
|
|
960
|
+
ry = ny - cy
|
|
961
|
+
for nx in range(min_x, max_x + 1):
|
|
962
|
+
if (nx, ny) in new_pixels:
|
|
963
|
+
continue
|
|
964
|
+
rx = (nx - cx) * ASPECT
|
|
965
|
+
ci = floor((rx * ja + ry * jb) / ASPECT + cx + 0.5 + 1e-7)
|
|
966
|
+
ri = floor(rx * jc + ry * jd + cy + 0.5 + 1e-7)
|
|
967
|
+
px = source.get((ci, ri))
|
|
968
|
+
if px is not None and px[0] == ' ' and px[1] and self._has_background(px[1]):
|
|
969
|
+
new_pixels[(nx, ny)] = px
|
|
970
|
+
if record:
|
|
971
|
+
self._rotmap = (cx, cy, min_x, min_y, ASPECT, ja, jb, jc, jd)
|
|
972
|
+
self._rotsize = size
|
|
973
|
+
self._rotorigin = (min_x, min_y)
|
|
974
|
+
if maps:
|
|
975
|
+
self._rotcells_v = cells
|
|
976
|
+
self._rotsrc_v = sources
|
|
977
|
+
return self.build_rows(new_pixels, min_x, max_x, min_y, max_y)
|
|
978
|
+
|
|
979
|
+
layout.ident = ident
|
|
980
|
+
return layout, render
|
|
981
|
+
|
|
982
|
+
def _vrender(self, layout, render, plain_layout):
|
|
983
|
+
styled_source = self.parse_pixels(self._vrtext)
|
|
984
|
+
if plain_layout is None:
|
|
985
|
+
styled_layout = layout(styled_source) if self._vrtext.splitlines() else None
|
|
986
|
+
elif [p for p, px in styled_source.items() if px[0] != ' '] == plain_layout[0]:
|
|
987
|
+
styled_layout = plain_layout
|
|
988
|
+
else:
|
|
989
|
+
styled_layout = layout(styled_source)
|
|
990
|
+
self.vtext = render(styled_source, styled_layout) if styled_layout is not None else ''
|
|
991
|
+
|
|
992
|
+
def _keeps_layout(self):
|
|
993
|
+
return (self._fade is not None or self._fg is not None or self._bg is not None
|
|
994
|
+
or self._alpha is not None or self._bgalpha is not None)
|
|
995
|
+
|
|
996
|
+
def _vrotate(self):
|
|
997
|
+
layout, render = self._rotator(self._rotation)
|
|
998
|
+
plain = self._plain_layout
|
|
999
|
+
if plain is None and not layout.ident and self._rtext.splitlines():
|
|
1000
|
+
plain = layout(self.parse_pixels(self._rtext))
|
|
1001
|
+
if self._keeps_layout():
|
|
1002
|
+
self._plain_layout = plain
|
|
1003
|
+
self._vrender(layout, render, plain)
|
|
1004
|
+
self._parse_pixels_key = None
|
|
1005
|
+
self._parse_pixels_val = None
|
|
1006
|
+
|
|
1007
|
+
def rotate(self, angle_degrees, init=False):
|
|
1008
|
+
if not init:
|
|
1009
|
+
rounded = round(angle_degrees * 20) / 20
|
|
1010
|
+
if self._rot_cache_angle == rounded:
|
|
1011
|
+
return
|
|
1012
|
+
self._rot_cache_angle = rounded
|
|
1013
|
+
|
|
1014
|
+
layout, render = self._rotator(angle_degrees)
|
|
1015
|
+
plain = self._rtext
|
|
1016
|
+
crows = plain.splitlines()
|
|
1017
|
+
plain_source = self.parse_pixels(plain)
|
|
1018
|
+
plain_layout = layout(plain_source) if crows else None
|
|
1019
|
+
self._plain_layout = plain_layout if not layout.ident and self._keeps_layout() else None
|
|
1020
|
+
self._rotangle = angle_degrees
|
|
1021
|
+
self._vrender(layout, render, plain_layout)
|
|
1022
|
+
if plain_layout is not None:
|
|
1023
|
+
self._text = render(plain_source, plain_layout, True, (max(len(r) for r in crows), len(crows)))
|
|
1024
|
+
self._rotcells_v = {}
|
|
1025
|
+
self._rotsrc_v = {}
|
|
1026
|
+
self._rotmaps_ok = False
|
|
1027
|
+
else:
|
|
1028
|
+
self._text = ''
|
|
1029
|
+
self._rotmap = None
|
|
1030
|
+
self._rotcells_v = {}
|
|
1031
|
+
self._rotsrc_v = {}
|
|
1032
|
+
self._rotmaps_ok = True
|
|
1033
|
+
self._rotsize = (0, 0)
|
|
1034
|
+
self._rotorigin = (0, 0)
|
|
1035
|
+
self._xlen_cache = None
|
|
1036
|
+
self._ylen_cache = None
|
|
1037
|
+
self._protate(angle_degrees, layout, render)
|
|
1038
|
+
self._parse_pixels_key = None
|
|
1039
|
+
self._parse_pixels_val = None
|
|
1040
|
+
|
|
1041
|
+
self.vx = self._rotorigin[0]
|
|
1042
|
+
self.vy = -self._rotorigin[1]
|
|
1043
|
+
|
|
1044
|
+
def _protate(self, angle_degrees, layout, render):
|
|
1045
|
+
if self._htext == self._rtext:
|
|
1046
|
+
self._ptext = self._text
|
|
1047
|
+
self._pvx = self._rotorigin[0]
|
|
1048
|
+
self._pvy = -self._rotorigin[1]
|
|
1049
|
+
self._pkey = None
|
|
1050
|
+
return
|
|
1051
|
+
key = (angle_degrees, self._htext, self._flip, self.aspect)
|
|
1052
|
+
if self._pkey == key:
|
|
1053
|
+
return
|
|
1054
|
+
self._pkey = key
|
|
1055
|
+
source = self.parse_pixels(self._htext)
|
|
1056
|
+
lay = layout(source) if self._htext.splitlines() else None
|
|
1057
|
+
if lay is None:
|
|
1058
|
+
self._ptext = ''
|
|
1059
|
+
self._pvx = 0
|
|
1060
|
+
self._pvy = 0
|
|
1061
|
+
else:
|
|
1062
|
+
self._ptext = render(source, lay)
|
|
1063
|
+
self._pvx = lay[5]
|
|
1064
|
+
self._pvy = -lay[7]
|
|
1065
|
+
|
|
1066
|
+
def _rotmaps(self):
|
|
1067
|
+
if self._rotmaps_ok:
|
|
1068
|
+
return
|
|
1069
|
+
self._rotmaps_ok = True
|
|
1070
|
+
plain = self._rtext
|
|
1071
|
+
if not plain.splitlines():
|
|
1072
|
+
return
|
|
1073
|
+
layout, render = self._rotator(self._rotangle)
|
|
1074
|
+
source = self.parse_pixels(plain)
|
|
1075
|
+
lay = layout(source)
|
|
1076
|
+
if lay is not None:
|
|
1077
|
+
render(source, lay, True, self._rotsize, True)
|
|
1078
|
+
|
|
1079
|
+
@property
|
|
1080
|
+
def _rotcells(self):
|
|
1081
|
+
self._rotmaps()
|
|
1082
|
+
return self._rotcells_v
|
|
1083
|
+
|
|
1084
|
+
@property
|
|
1085
|
+
def _rotsrc(self):
|
|
1086
|
+
self._rotmaps()
|
|
1087
|
+
return self._rotsrc_v
|
|
1088
|
+
|
|
1089
|
+
def _origin(self):
|
|
1090
|
+
return round(self._x + self.vx), round(self._y + self.vy)
|
|
1091
|
+
|
|
1092
|
+
def _cell_world(self, cell):
|
|
1093
|
+
m = self._rotmap
|
|
1094
|
+
ox, oy = self._origin()
|
|
1095
|
+
return ox + (cell[0] - m[2]), oy - (cell[1] - m[3])
|
|
1096
|
+
|
|
1097
|
+
def rtext_to_world(self, ci, ri):
|
|
1098
|
+
if self._rotmap is None:
|
|
1099
|
+
return None
|
|
1100
|
+
cells = self._rotsrc.get((ci, ri))
|
|
1101
|
+
if not cells:
|
|
1102
|
+
return None
|
|
1103
|
+
return self._cell_world(min(cells))
|
|
1104
|
+
|
|
1105
|
+
def worldcells(self):
|
|
1106
|
+
if self._rotmap is None:
|
|
1107
|
+
return set()
|
|
1108
|
+
return {self._cell_world(c) for c in self._rotcells}
|
|
1109
|
+
|
|
1110
|
+
def world_to_rtext_all(self, wx, wy):
|
|
1111
|
+
m = self._rotmap
|
|
1112
|
+
if m is None:
|
|
1113
|
+
return []
|
|
1114
|
+
ox, oy = self._origin()
|
|
1115
|
+
return self._rotcells.get((wx - ox + m[2], oy - wy + m[3]), [])
|
|
1116
|
+
|
|
1117
|
+
def world_to_rtext(self, wx, wy):
|
|
1118
|
+
found = self.world_to_rtext_all(wx, wy)
|
|
1119
|
+
return found[0] if found else None
|
|
1120
|
+
|
|
1121
|
+
def world_to_rtextf(self, wx, wy):
|
|
1122
|
+
m = self._rotmap
|
|
1123
|
+
if m is None:
|
|
1124
|
+
return None
|
|
1125
|
+
cx, cy, min_x, min_y, ASPECT, ja, jb, jc, jd = m
|
|
1126
|
+
ox, oy = self._origin()
|
|
1127
|
+
rx_n = (wx - ox + min_x - cx) * ASPECT
|
|
1128
|
+
ry_n = oy - wy + min_y - cy
|
|
1129
|
+
rel_x = (rx_n * ja + ry_n * jb) / ASPECT
|
|
1130
|
+
rel_y = rx_n * jc + ry_n * jd
|
|
1131
|
+
return rel_x + cx, rel_y + cy
|
|
1132
|
+
|
|
1133
|
+
def cutline(self, p0, p1):
|
|
1134
|
+
a = self.world_to_rtextf(*p0)
|
|
1135
|
+
b = self.world_to_rtextf(*p1)
|
|
1136
|
+
if a is None or b is None:
|
|
1137
|
+
return set()
|
|
1138
|
+
width, height = self._rotsize
|
|
1139
|
+
if (max(a[0], b[0]) < -1 or min(a[0], b[0]) > width
|
|
1140
|
+
or max(a[1], b[1]) < -1 or min(a[1], b[1]) > height):
|
|
1141
|
+
return set()
|
|
1142
|
+
steps = max(1, int(math.ceil(max(abs(b[0] - a[0]), abs(b[1] - a[1])) * 2)))
|
|
1143
|
+
pixels = self.parse_pixels(self.vrtext)
|
|
1144
|
+
walls = set()
|
|
1145
|
+
for i in range(steps + 1):
|
|
1146
|
+
u = i / steps
|
|
1147
|
+
c = (round(a[0] + (b[0] - a[0]) * u), round(a[1] + (b[1] - a[1]) * u))
|
|
1148
|
+
if c in pixels and pixels[c][0] != ' ':
|
|
1149
|
+
walls.add(c)
|
|
1150
|
+
return walls
|
|
1151
|
+
|
|
1152
|
+
def _clear(self, cells):
|
|
1153
|
+
styled = self.parse_pixels(self._vbase)
|
|
1154
|
+
hit = [c for c in cells if c in styled and styled[c][0] != ' ']
|
|
1155
|
+
if not hit:
|
|
1156
|
+
return False
|
|
1157
|
+
styled = dict(styled)
|
|
1158
|
+
for c in hit:
|
|
1159
|
+
styled[c] = (' ', '')
|
|
1160
|
+
ref = next((p for p, (char, _) in styled.items() if char != ' ' and self._rotsrc.get(p)), None)
|
|
1161
|
+
before = self.rtext_to_world(*ref) if ref is not None else None
|
|
1162
|
+
xs = [p[0] for p in styled]
|
|
1163
|
+
ys = [p[1] for p in styled]
|
|
1164
|
+
phys = getattr(self.obj, 'physics', None)
|
|
1165
|
+
old_mass = phys.mass if phys is not None else None
|
|
1166
|
+
rows = self.build_rows(styled, min(xs), max(xs), min(ys), max(ys))
|
|
1167
|
+
self._htext = self.scene.recomp.sub('', rows)
|
|
1168
|
+
self.vhtext = rows
|
|
1169
|
+
self.rtext = rows
|
|
1170
|
+
if old_mass and phys.mass != old_mass:
|
|
1171
|
+
k = phys.mass / old_mass
|
|
1172
|
+
phys.xvel *= k
|
|
1173
|
+
phys.yvel *= k
|
|
1174
|
+
phys.avel *= k
|
|
1175
|
+
after = self.rtext_to_world(ref[0] - min(xs), ref[1] - min(ys)) if ref is not None else None
|
|
1176
|
+
if before is not None and after is not None:
|
|
1177
|
+
if before[0] != after[0]:
|
|
1178
|
+
self.x = self._x + before[0] - after[0]
|
|
1179
|
+
if before[1] != after[1]:
|
|
1180
|
+
self.y = self._y + before[1] - after[1]
|
|
1181
|
+
return True
|
|
1182
|
+
|
|
1183
|
+
def cut(self, wx, wy, r=1, shape='circle'):
|
|
1184
|
+
return self.cutpoints([(wx, wy)], r, shape)
|
|
1185
|
+
|
|
1186
|
+
def cutpoints(self, points, r=1, shape='circle'):
|
|
1187
|
+
offsets = _footprint(r, self.aspect, shape)
|
|
1188
|
+
cells = []
|
|
1189
|
+
for wx, wy in points:
|
|
1190
|
+
for dx, dy in offsets:
|
|
1191
|
+
cells.extend(self.world_to_rtext_all(wx + dx, wy + dy))
|
|
1192
|
+
return self._clear(cells)
|
|
1193
|
+
|
|
1194
|
+
def _notify_whole(self, callback):
|
|
1195
|
+
if callable(callback):
|
|
1196
|
+
l = _argslen(callback)
|
|
1197
|
+
if l > 1: callback([self.obj], self.obj)
|
|
1198
|
+
elif l > 0: callback([self.obj])
|
|
1199
|
+
else: callback()
|
|
1200
|
+
|
|
1201
|
+
def _fragments(self, coords, diagonal=True, walls=()):
|
|
1202
|
+
walls = set(walls)
|
|
1203
|
+
neighbours = _NEIGHBOURS8 if diagonal else _NEIGHBOURS4
|
|
1204
|
+
remaining = set(coords)
|
|
1205
|
+
components = []
|
|
1206
|
+
while remaining:
|
|
1207
|
+
group = set()
|
|
1208
|
+
stack = [next(iter(remaining))]
|
|
1209
|
+
while stack:
|
|
1210
|
+
pos = stack.pop()
|
|
1211
|
+
if pos not in remaining:
|
|
1212
|
+
continue
|
|
1213
|
+
remaining.remove(pos)
|
|
1214
|
+
group.add(pos)
|
|
1215
|
+
cx, cy = pos
|
|
1216
|
+
for dx, dy in neighbours:
|
|
1217
|
+
nxt = (cx + dx, cy + dy)
|
|
1218
|
+
if nxt not in remaining:
|
|
1219
|
+
continue
|
|
1220
|
+
if dx and dy and (cx + dx, cy) in walls and (cx, cy + dy) in walls:
|
|
1221
|
+
continue
|
|
1222
|
+
stack.append(nxt)
|
|
1223
|
+
components.append(group)
|
|
1224
|
+
return components
|
|
1225
|
+
|
|
1226
|
+
def _spawn_from_components(self, components, pixels, callback=None):
|
|
1227
|
+
rotation = self._rotation
|
|
1228
|
+
ox, oy = self._x, self._y
|
|
1229
|
+
parent = getattr(self.obj, 'physics', None)
|
|
1230
|
+
motion = None
|
|
1231
|
+
if parent is not None:
|
|
1232
|
+
A = self.scene.aspect
|
|
1233
|
+
pcx, pcy, _ = parent.body()
|
|
1234
|
+
motion = (parent.xvel / parent.mass * A, parent.yvel / parent.mass, parent._omega(),
|
|
1235
|
+
parent.avel / parent.mass, pcx, pcy)
|
|
1236
|
+
|
|
1237
|
+
fading = dict(self._fade) if self._fade is not None else None
|
|
1238
|
+
spawns = []
|
|
1239
|
+
anchors = []
|
|
1240
|
+
for group in components:
|
|
1241
|
+
xs = [p[0] for p in group]
|
|
1242
|
+
ys = [p[1] for p in group]
|
|
1243
|
+
min_x, max_x = min(xs), max(xs) + 1
|
|
1244
|
+
min_y, max_y = min(ys), max(ys) + 1
|
|
1245
|
+
target = {self._cell_world(c) for p in group for c in self._rotsrc.get(p, ())}
|
|
1246
|
+
|
|
1247
|
+
kw = self.obj.kwargs()
|
|
1248
|
+
for key in ('mass', 'rtext', 'htext', 'vtext', 'vrtext', 'vhtext', 'vx', 'vy', 'xvel', 'yvel', 'avel', 'joints'):
|
|
1249
|
+
kw.pop(key, None)
|
|
1250
|
+
if parent is not None:
|
|
1251
|
+
kw.update(dynamic=parent.dynamic, bounciness=parent.bounciness, friction=parent.friction)
|
|
1252
|
+
if self._aspect is not None:
|
|
1253
|
+
kw['aspect'] = self._aspect
|
|
1254
|
+
for key in ('fg', 'bg', 'alpha', 'bgalpha'):
|
|
1255
|
+
value = getattr(self, key)
|
|
1256
|
+
if value is not None:
|
|
1257
|
+
kw[key] = value
|
|
1258
|
+
spawns.append({
|
|
1259
|
+
**kw,
|
|
1260
|
+
'text': self.build_rows({p: pixels[p] for p in group}, min_x, max_x - 1, min_y, max_y - 1),
|
|
1261
|
+
'x': ox + min_x,
|
|
1262
|
+
'y': oy - min_y,
|
|
1263
|
+
'rotation': rotation,
|
|
1264
|
+
'flip': self._flip,
|
|
1265
|
+
})
|
|
1266
|
+
anchors.append(target)
|
|
1267
|
+
|
|
1268
|
+
obj_ref = self.obj
|
|
1269
|
+
def do_split(spawns=spawns, anchors=anchors, obj_ref=obj_ref, cb=callback, motion=motion, fading=fading):
|
|
1270
|
+
parts = []
|
|
1271
|
+
for kw, target in zip(spawns, anchors):
|
|
1272
|
+
part = obj_ref.scene.obj().create(**kw)
|
|
1273
|
+
ft = part.transform
|
|
1274
|
+
if fading is not None:
|
|
1275
|
+
ft._fade = dict(fading)
|
|
1276
|
+
placed = ft.worldcells()
|
|
1277
|
+
if target and placed:
|
|
1278
|
+
dx = round(sum(w[0] for w in target) / len(target) - sum(w[0] for w in placed) / len(placed))
|
|
1279
|
+
dy = round(sum(w[1] for w in target) / len(target) - sum(w[1] for w in placed) / len(placed))
|
|
1280
|
+
best = min(
|
|
1281
|
+
((sx, sy) for sx in range(dx - 1, dx + 2) for sy in range(dy - 1, dy + 2)),
|
|
1282
|
+
key=lambda s: len({(wx + s[0], wy + s[1]) for wx, wy in placed} ^ target)
|
|
1283
|
+
)
|
|
1284
|
+
ft.x = ft.x + best[0]
|
|
1285
|
+
ft.y = ft.y + best[1]
|
|
1286
|
+
fp = getattr(part, 'physics', None)
|
|
1287
|
+
if fp is not None and motion is not None:
|
|
1288
|
+
vx, vy, omega, spin, pcx, pcy = motion
|
|
1289
|
+
fcx, fcy, _ = fp.body()
|
|
1290
|
+
A = obj_ref.scene.aspect
|
|
1291
|
+
rx, ry = (fcx - pcx) * A, fcy - pcy
|
|
1292
|
+
fp.xvel = (vx - omega * ry) / A * fp.mass
|
|
1293
|
+
fp.yvel = (vy + omega * rx) * fp.mass
|
|
1294
|
+
fp.avel = spin * fp.mass
|
|
1295
|
+
parts.append(part)
|
|
1296
|
+
if getattr(obj_ref, 'joint', None) is not None:
|
|
1297
|
+
joint.transfer(obj_ref, parts)
|
|
1298
|
+
if callable(cb):
|
|
1299
|
+
l = _argslen(cb)
|
|
1300
|
+
if l > 1: cb(parts, obj_ref)
|
|
1301
|
+
elif l > 0: cb(parts)
|
|
1302
|
+
else: cb()
|
|
1303
|
+
for calb, cl in list(self._onsplit.items()):
|
|
1304
|
+
if cl > 1: calb(parts, obj_ref)
|
|
1305
|
+
elif cl > 0: calb(parts)
|
|
1306
|
+
else: calb()
|
|
1307
|
+
obj_ref.discard()
|
|
1308
|
+
do_split()
|
|
1309
|
+
return do_split
|
|
1310
|
+
|
|
1311
|
+
def split(self, callback=None, diagonal=True):
|
|
1312
|
+
if getattr(self.obj, 'transform', None) is not self:
|
|
1313
|
+
return
|
|
1314
|
+
pixels = self.parse_pixels(self._vbase)
|
|
1315
|
+
coords = {pos for pos, (char, _) in pixels.items() if char != ' '}
|
|
1316
|
+
if not coords:
|
|
1317
|
+
self._notify_whole(callback)
|
|
1318
|
+
self.obj.discard()
|
|
1319
|
+
return
|
|
1320
|
+
|
|
1321
|
+
components = self._fragments(coords, diagonal)
|
|
1322
|
+
if len(components) < 2:
|
|
1323
|
+
self._notify_whole(callback)
|
|
1324
|
+
return
|
|
1325
|
+
|
|
1326
|
+
self._split_cb = self._spawn_from_components(components, pixels, callback)
|
|
1327
|
+
return self._split_cb
|
|
1328
|
+
|
|
1329
|
+
def split_at(self, wall_pixels, callback=None, diagonal=True):
|
|
1330
|
+
if getattr(self.obj, 'transform', None) is not self:
|
|
1331
|
+
return
|
|
1332
|
+
pixels = self.parse_pixels(self._vbase)
|
|
1333
|
+
coords = {pos for pos, (char, _) in pixels.items() if char != ' '}
|
|
1334
|
+
|
|
1335
|
+
valid_walls = set(wall_pixels) & coords
|
|
1336
|
+
if not valid_walls:
|
|
1337
|
+
self._notify_whole(callback)
|
|
1338
|
+
return
|
|
1339
|
+
|
|
1340
|
+
components = self._fragments(coords - valid_walls, diagonal, valid_walls)
|
|
1341
|
+
if len(components) < 2:
|
|
1342
|
+
self._notify_whole(callback)
|
|
1343
|
+
return
|
|
1344
|
+
|
|
1345
|
+
neighbours = _NEIGHBOURS8 if diagonal else _NEIGHBOURS4
|
|
1346
|
+
remaining = set(valid_walls)
|
|
1347
|
+
grew = True
|
|
1348
|
+
while remaining and grew:
|
|
1349
|
+
grew = False
|
|
1350
|
+
for wp in sorted(remaining):
|
|
1351
|
+
cx, cy = wp
|
|
1352
|
+
for comp in components:
|
|
1353
|
+
if any((cx + dx, cy + dy) in comp for dx, dy in neighbours):
|
|
1354
|
+
comp.add(wp)
|
|
1355
|
+
remaining.discard(wp)
|
|
1356
|
+
grew = True
|
|
1357
|
+
break
|
|
1358
|
+
if remaining:
|
|
1359
|
+
components.extend(self._fragments(remaining, diagonal))
|
|
1360
|
+
|
|
1361
|
+
self._split_cb = self._spawn_from_components(components, pixels, callback)
|
|
1362
|
+
return self._split_cb
|
|
1363
|
+
|
|
1364
|
+
def onsplit(self, cb, once=False):
|
|
1365
|
+
if once:
|
|
1366
|
+
l = _argslen(cb)
|
|
1367
|
+
fired = [False]
|
|
1368
|
+
orig = cb
|
|
1369
|
+
def wrapped(parts=None, obj_ref=None):
|
|
1370
|
+
if fired[0]: return
|
|
1371
|
+
fired[0] = True
|
|
1372
|
+
if wrapped in self._onsplit:
|
|
1373
|
+
del self._onsplit[wrapped]
|
|
1374
|
+
if l > 1: orig(parts, obj_ref)
|
|
1375
|
+
elif l > 0: orig(parts)
|
|
1376
|
+
else: orig()
|
|
1377
|
+
cb = wrapped
|
|
1378
|
+
self._onsplit[cb] = 2
|
|
1379
|
+
else:
|
|
1380
|
+
self._onsplit[cb] = _argslen(cb)
|
|
1381
|
+
return cb
|
|
1382
|
+
|
|
1383
|
+
def offsplit(self, cb):
|
|
1384
|
+
self._onsplit = {f:par for f,par in self._onsplit.items() if f is not cb}
|
|
1385
|
+
|
|
1386
|
+
def xlen(self, text=None):
|
|
1387
|
+
if text is None:
|
|
1388
|
+
if self._xlen_cache is not None:
|
|
1389
|
+
return self._xlen_cache
|
|
1390
|
+
text = self._text
|
|
1391
|
+
lens = []
|
|
1392
|
+
for row in text.splitlines():
|
|
1393
|
+
for i, char in enumerate(row):
|
|
1394
|
+
if char != ' ':
|
|
1395
|
+
lens.append(i)
|
|
1396
|
+
result = abs(max(lens) - (min(lens) - 1)) if lens else 0
|
|
1397
|
+
self._xlen_cache = result
|
|
1398
|
+
return result
|
|
1399
|
+
lens = []
|
|
1400
|
+
for row in text.splitlines():
|
|
1401
|
+
for i, char in enumerate(row):
|
|
1402
|
+
if char != ' ':
|
|
1403
|
+
lens.append(i)
|
|
1404
|
+
return abs(max(lens) - (min(lens) - 1)) if lens else 0
|
|
1405
|
+
|
|
1406
|
+
def ylen(self, text=None):
|
|
1407
|
+
if text is None:
|
|
1408
|
+
if self._ylen_cache is not None:
|
|
1409
|
+
return self._ylen_cache
|
|
1410
|
+
text = self._text
|
|
1411
|
+
count = 0
|
|
1412
|
+
for row in text.splitlines():
|
|
1413
|
+
if any(c != ' ' and c != '\n' for c in row):
|
|
1414
|
+
count += 1
|
|
1415
|
+
self._ylen_cache = count
|
|
1416
|
+
return count
|
|
1417
|
+
count = 0
|
|
1418
|
+
for row in text.splitlines():
|
|
1419
|
+
if any(c != ' ' and c != '\n' for c in row):
|
|
1420
|
+
count += 1
|
|
1421
|
+
return count
|
|
1422
|
+
|
|
1423
|
+
def xc(self, text=None):
|
|
1424
|
+
return self.xlen(text) / 2
|
|
1425
|
+
|
|
1426
|
+
def yc(self, text=None):
|
|
1427
|
+
return self.ylen(text) / 2
|
|
1428
|
+
|
|
1429
|
+
def extent(self, local=False):
|
|
1430
|
+
rows = self._text.splitlines()
|
|
1431
|
+
cols = [i for row in rows for i, char in enumerate(row) if char != ' ']
|
|
1432
|
+
px, py = (0, 0) if local else (self._x + self.vx, self._y + self.vy)
|
|
1433
|
+
if not cols:
|
|
1434
|
+
return px, py, 0, 0
|
|
1435
|
+
lo, hi = min(cols), max(cols) + 1
|
|
1436
|
+
return (px + (lo + hi) / 2, py - len(rows) / 2,
|
|
1437
|
+
(hi - lo) / 2, len(rows) / 2)
|
|
1438
|
+
|
|
1439
|
+
def center(self, local=False):
|
|
1440
|
+
return self.extent(local)[:2]
|
|
1441
|
+
|
|
1442
|
+
def forward(self, move=1, offset=0, angle=None):
|
|
1443
|
+
angle = math.radians((self._rotation if angle is None else angle) + offset)
|
|
1444
|
+
x = math.sin(angle) * move
|
|
1445
|
+
y = math.cos(angle) * self.aspect * move
|
|
1446
|
+
return (x, y)
|
|
1447
|
+
|
|
1448
|
+
def pivot(self, local=False):
|
|
1449
|
+
m = self._rotmap
|
|
1450
|
+
if m is None:
|
|
1451
|
+
return (0.0, 0.0) if local else (self._x + self.vx, self._y + self.vy)
|
|
1452
|
+
if local:
|
|
1453
|
+
return m[0], -m[1]
|
|
1454
|
+
ox, oy = self._origin()
|
|
1455
|
+
return ox + m[0] - m[2], oy - (m[1] - m[3])
|
|
1456
|
+
|
|
1457
|
+
def point(self, tx, ty, offset=0, origin=None):
|
|
1458
|
+
ox, oy = self.pivot() if origin is None else origin
|
|
1459
|
+
dx = tx - ox
|
|
1460
|
+
dy = ty - oy
|
|
1461
|
+
ASPECT = self.aspect
|
|
1462
|
+
angle = math.degrees(math.atan2(-dy, dx * ASPECT))+90
|
|
1463
|
+
return angle + offset
|
|
1464
|
+
|
|
1465
|
+
def aim(self, obj, offset=0):
|
|
1466
|
+
return self.point(*obj.transform.pivot(), offset=offset)
|
|
1467
|
+
|
|
1468
|
+
def alerp(self, angle):
|
|
1469
|
+
return (angle - self._rotation + 540) % 360 - 180
|
|
1470
|
+
|
|
1471
|
+
def bounce(self, normal, angle=None):
|
|
1472
|
+
if isinstance(normal, (tuple, list)):
|
|
1473
|
+
normal = math.degrees(math.atan2(normal[0], normal[1]))
|
|
1474
|
+
heading = self._rotation if angle is None else angle
|
|
1475
|
+
return (2 * normal - heading + 180) % 360
|
|
1476
|
+
|
|
1477
|
+
@property
|
|
1478
|
+
def animating(self):
|
|
1479
|
+
return self._anim is not None
|
|
1480
|
+
|
|
1481
|
+
def _frame(self, frame):
|
|
1482
|
+
self.rtext = self.vhtext if frame is None else frame
|
|
1483
|
+
|
|
1484
|
+
def animate(self, frames, delay=1, loop=True, callback=None, restore=False):
|
|
1485
|
+
frames = list(frames)
|
|
1486
|
+
if not frames:
|
|
1487
|
+
return self.unanimate()
|
|
1488
|
+
delays = list(delay) if isinstance(delay, (list, tuple)) else [delay] * len(frames)
|
|
1489
|
+
if len(delays) != len(frames):
|
|
1490
|
+
raise ValueError('delay must be a number or have one entry per frame')
|
|
1491
|
+
if any(d <= 0 for d in delays):
|
|
1492
|
+
raise ValueError('delay must be positive')
|
|
1493
|
+
passes = None if loop is True else max(1, int(loop))
|
|
1494
|
+
self._anim = {'frames': frames, 'delays': delays, 'index': 0, 'passes': passes,
|
|
1495
|
+
'cb': callback, 'restore': restore, 'wait': delays[0]}
|
|
1496
|
+
self._frame(frames[0])
|
|
1497
|
+
return self
|
|
1498
|
+
|
|
1499
|
+
def unanimate(self, restore=True):
|
|
1500
|
+
was = self._anim is not None
|
|
1501
|
+
self._anim = None
|
|
1502
|
+
if was and restore:
|
|
1503
|
+
self.rtext = self.vhtext
|
|
1504
|
+
return self
|
|
1505
|
+
|
|
1506
|
+
def _tick(self):
|
|
1507
|
+
if self._fade is not None:
|
|
1508
|
+
self._tickfade()
|
|
1509
|
+
self._tickanim()
|
|
1510
|
+
|
|
1511
|
+
@property
|
|
1512
|
+
def fading(self):
|
|
1513
|
+
return self._fade is not None
|
|
1514
|
+
|
|
1515
|
+
def fade(self, alpha=0, time=1, delay=0, bgalpha=None, start=None, callback=None, discard=False, ease='linear'):
|
|
1516
|
+
if time < 0 or delay < 0:
|
|
1517
|
+
raise ValueError('time and delay must not be negative')
|
|
1518
|
+
curve = _easing(ease)
|
|
1519
|
+
a0 = 1.0 if self._alpha is None else self._alpha
|
|
1520
|
+
b0 = 1.0 if self._bgalpha is None else self._bgalpha
|
|
1521
|
+
if start is not None:
|
|
1522
|
+
a0, b0 = start if isinstance(start, (tuple, list)) else (start, start)
|
|
1523
|
+
self._fade = None
|
|
1524
|
+
self._applystyle(alpha=a0, bgalpha=b0)
|
|
1525
|
+
self._fade = {'a0': a0, 'b0': b0, 'a1': alpha, 'b1': alpha if bgalpha is None else bgalpha,
|
|
1526
|
+
'time': time, 'delay': delay, 'elapsed': 0.0, 'cb': callback,
|
|
1527
|
+
'discard': discard, 'ease': curve}
|
|
1528
|
+
if time <= 0 and delay <= 0:
|
|
1529
|
+
self._finishfade()
|
|
1530
|
+
return self
|
|
1531
|
+
|
|
1532
|
+
def unfade(self, finish=False):
|
|
1533
|
+
f = self._fade
|
|
1534
|
+
self._fade = None
|
|
1535
|
+
if f is not None and finish:
|
|
1536
|
+
self._fadeto(f, 1.0)
|
|
1537
|
+
return self
|
|
1538
|
+
|
|
1539
|
+
def _fadeto(self, f, progress):
|
|
1540
|
+
if progress >= 1:
|
|
1541
|
+
a, b = f['a1'], f['b1']
|
|
1542
|
+
else:
|
|
1543
|
+
p = f['ease'](progress)
|
|
1544
|
+
a = f['a0'] + (f['a1'] - f['a0']) * p
|
|
1545
|
+
b = f['b0'] + (f['b1'] - f['b0']) * p
|
|
1546
|
+
a = max(0.0, min(1.0, a))
|
|
1547
|
+
b = max(0.0, min(1.0, b))
|
|
1548
|
+
self._applystyle(alpha=None if a >= 1 else a, bgalpha=None if b >= 1 else b)
|
|
1549
|
+
|
|
1550
|
+
def _finishfade(self):
|
|
1551
|
+
f = self._fade
|
|
1552
|
+
self._fade = None
|
|
1553
|
+
self._fadeto(f, 1.0)
|
|
1554
|
+
cb = f['cb']
|
|
1555
|
+
if callable(cb):
|
|
1556
|
+
if _argslen(cb):
|
|
1557
|
+
cb(self.obj)
|
|
1558
|
+
else:
|
|
1559
|
+
cb()
|
|
1560
|
+
if f['discard'] and getattr(self.obj, 'transform', None) is self:
|
|
1561
|
+
self.obj.discard()
|
|
1562
|
+
|
|
1563
|
+
def _tickfade(self):
|
|
1564
|
+
f = self._fade
|
|
1565
|
+
dt = 1 / self.scene.framerate
|
|
1566
|
+
if f['delay'] > 0:
|
|
1567
|
+
f['delay'] -= dt
|
|
1568
|
+
if f['delay'] > 0:
|
|
1569
|
+
return
|
|
1570
|
+
dt = -f['delay']
|
|
1571
|
+
f['delay'] = 0
|
|
1572
|
+
f['elapsed'] += dt
|
|
1573
|
+
if f['elapsed'] >= f['time']:
|
|
1574
|
+
self._finishfade()
|
|
1575
|
+
else:
|
|
1576
|
+
self._fadeto(f, f['elapsed'] / f['time'])
|
|
1577
|
+
|
|
1578
|
+
def _tickanim(self):
|
|
1579
|
+
a = self._anim
|
|
1580
|
+
if a is None:
|
|
1581
|
+
return
|
|
1582
|
+
a['wait'] -= 1
|
|
1583
|
+
while a['wait'] <= 0 and self._anim is a:
|
|
1584
|
+
a['index'] += 1
|
|
1585
|
+
if a['index'] >= len(a['frames']):
|
|
1586
|
+
if a['passes'] is not None:
|
|
1587
|
+
a['passes'] -= 1
|
|
1588
|
+
if a['passes'] <= 0:
|
|
1589
|
+
self._anim = None
|
|
1590
|
+
if a['restore']:
|
|
1591
|
+
self.rtext = self.vhtext
|
|
1592
|
+
cb = a['cb']
|
|
1593
|
+
if callable(cb):
|
|
1594
|
+
if _argslen(cb):
|
|
1595
|
+
cb(self.obj)
|
|
1596
|
+
else:
|
|
1597
|
+
cb()
|
|
1598
|
+
return
|
|
1599
|
+
a['index'] = 0
|
|
1600
|
+
self._frame(a['frames'][a['index']])
|
|
1601
|
+
a['wait'] += a['delays'][a['index']]
|
|
1602
|
+
|
|
1603
|
+
def _lerpbase(self, key):
|
|
1604
|
+
if key in ('alpha', 'bgalpha'):
|
|
1605
|
+
return 1.0
|
|
1606
|
+
if key == 'fg':
|
|
1607
|
+
return tuple(self.scene.fgcolor)
|
|
1608
|
+
if key == 'bg':
|
|
1609
|
+
return tuple(self.scene.bgcolor)
|
|
1610
|
+
return None
|
|
1611
|
+
|
|
1612
|
+
def _lerpkeep(self, key, value):
|
|
1613
|
+
if key in ('alpha', 'bgalpha') and value is not None:
|
|
1614
|
+
value = max(0.0, value)
|
|
1615
|
+
return None if value >= 1 else value
|
|
1616
|
+
return value
|
|
1617
|
+
|
|
1618
|
+
def opacities(self):
|
|
1619
|
+
text = self.vtext
|
|
1620
|
+
cached = self._opacity_cache
|
|
1621
|
+
if cached is not None and cached[0] is text:
|
|
1622
|
+
return cached[1]
|
|
1623
|
+
found = {}
|
|
1624
|
+
if text and alphamark.search(text):
|
|
1625
|
+
recomp = self.scene.recomp
|
|
1626
|
+
for ri, row in enumerate(text.splitlines()):
|
|
1627
|
+
ci = 0
|
|
1628
|
+
active = ''
|
|
1629
|
+
for part in recomp.split(row):
|
|
1630
|
+
if not part:
|
|
1631
|
+
continue
|
|
1632
|
+
if recomp.match(part):
|
|
1633
|
+
active = '' if part in ansi.RESETS else active + part
|
|
1634
|
+
continue
|
|
1635
|
+
fa, _, ba = _ALPHAS(active)
|
|
1636
|
+
if fa is not None or ba is not None:
|
|
1637
|
+
own = _BGCODE.search(active) is not None
|
|
1638
|
+
value = (1.0 if ba is None else ba) if own else (1.0 if fa is None else fa)
|
|
1639
|
+
if value < 1.0:
|
|
1640
|
+
for k, char in enumerate(part):
|
|
1641
|
+
if own or char != ' ':
|
|
1642
|
+
found[(ci + k, ri)] = value
|
|
1643
|
+
ci += len(part)
|
|
1644
|
+
self._opacity_cache = (text, found)
|
|
1645
|
+
return found
|
|
1646
|
+
|
|
1647
|
+
|
|
1648
|
+
class rendering(_lerpable):
|
|
1649
|
+
_owned = ['show', 'renderspaces', 'z']
|
|
1650
|
+
_required = [transform]
|
|
1651
|
+
_default = True
|
|
1652
|
+
_settings = ('show', 'spaces', 'z')
|
|
1653
|
+
_aliases = {'spaces': 'renderspaces'}
|
|
1654
|
+
|
|
1655
|
+
def __init__(self, obj, **kwargs):
|
|
1656
|
+
self.obj = obj
|
|
1657
|
+
self.t = obj.transform
|
|
1658
|
+
self.scene = obj.scene
|
|
1659
|
+
self._litkey = None
|
|
1660
|
+
self._lit = ''
|
|
1661
|
+
self.show = kwargs.get('show', True)
|
|
1662
|
+
self.renderspaces = kwargs.get('renderspaces', False)
|
|
1663
|
+
self.z = kwargs.get('z', 0)
|
|
1664
|
+
|
|
1665
|
+
@property
|
|
1666
|
+
def z(self):
|
|
1667
|
+
return self._z
|
|
1668
|
+
|
|
1669
|
+
@z.setter
|
|
1670
|
+
def z(self, value):
|
|
1671
|
+
self._z = value
|
|
1672
|
+
if self._z > self.scene._lastz: self.scene._lastz = self._z
|
|
1673
|
+
if self._z < self.scene._firstz: self.scene._firstz = self._z
|
|
1674
|
+
|
|
1675
|
+
@property
|
|
1676
|
+
def spaces(self):
|
|
1677
|
+
return self.renderspaces
|
|
1678
|
+
@spaces.setter
|
|
1679
|
+
def spaces(self, value):
|
|
1680
|
+
self.renderspaces = value
|
|
1681
|
+
|
|
1682
|
+
def _shown(self):
|
|
1683
|
+
self.t._stylecheck()
|
|
1684
|
+
text = self.t.vtext
|
|
1685
|
+
sc = self.scene
|
|
1686
|
+
if not text or (not sc.lights and sc.ambient >= 1):
|
|
1687
|
+
return text
|
|
1688
|
+
lights = tuple((id(l), l._version) for l in sc.lights if l.on and l._affects(self.obj))
|
|
1689
|
+
key = (text, self.t._origin(), sc.ambient, sc.bgcolor, sc.fgcolor, lights)
|
|
1690
|
+
if self._litkey != key:
|
|
1691
|
+
self._litkey = key
|
|
1692
|
+
self._lit = light.shade(sc, self.obj, text)
|
|
1693
|
+
return self._lit
|
|
1694
|
+
|
|
1695
|
+
def state(self, camx, camy, xoff, yoff):
|
|
1696
|
+
if not self.show:
|
|
1697
|
+
return (0, 0, '', 0, 0)
|
|
1698
|
+
t = self.t
|
|
1699
|
+
return (
|
|
1700
|
+
int((t.x + t.vx - camx + 0.5) // 1),
|
|
1701
|
+
-int((t.y + t.vy - camy + 0.5) // 1),
|
|
1702
|
+
self._shown(),
|
|
1703
|
+
xoff,
|
|
1704
|
+
yoff,
|
|
1705
|
+
)
|
|
1706
|
+
|
|
1707
|
+
def front(self):
|
|
1708
|
+
self.z = self.scene._lastz +1
|
|
1709
|
+
|
|
1710
|
+
def back(self):
|
|
1711
|
+
self.z = self.scene._firstz -1
|
|
1712
|
+
|
|
1713
|
+
|
|
1714
|
+
class physics(_change_callback, _lerpable):
|
|
1715
|
+
_owned = [
|
|
1716
|
+
'xvel', 'yvel', 'avel',
|
|
1717
|
+
'dynamic',
|
|
1718
|
+
'bounciness',
|
|
1719
|
+
'friction',
|
|
1720
|
+
'mass', 'automass',
|
|
1721
|
+
'solid',
|
|
1722
|
+
'hitbox',
|
|
1723
|
+
'touch',
|
|
1724
|
+
'phantom',
|
|
1725
|
+
'gravity',
|
|
1726
|
+
'drag',
|
|
1727
|
+
'adrag',
|
|
1728
|
+
'precision',
|
|
1729
|
+
'rprecision',
|
|
1730
|
+
'slope',
|
|
1731
|
+
]
|
|
1732
|
+
_required = [transform]
|
|
1733
|
+
_settings = ('xvel', 'yvel', 'avel', 'dynamic', 'bounciness', 'friction', 'mass', 'automass', 'solid',
|
|
1734
|
+
'hitbox', 'phantom', 'gravity', 'drag', 'adrag', 'precision', 'rprecision', 'slope')
|
|
1735
|
+
_running = 0
|
|
1736
|
+
def on_discard(self):
|
|
1737
|
+
self.scene.solids = [s for s in self.scene.solids if s is not self.obj]
|
|
1738
|
+
discarded = self.obj
|
|
1739
|
+
for other in self.scene.objs:
|
|
1740
|
+
if other is discarded or not hasattr(other, 'physics'):
|
|
1741
|
+
continue
|
|
1742
|
+
p = other.physics
|
|
1743
|
+
p._touch_callbacks = [
|
|
1744
|
+
cb for cb in p._touch_callbacks
|
|
1745
|
+
if not _closure_refs(cb, discarded)
|
|
1746
|
+
]
|
|
1747
|
+
other._ghosts[:] = [
|
|
1748
|
+
g for g in other._ghosts
|
|
1749
|
+
if g is not discarded and not (getattr(g, '__closure__', None) and _closure_refs(g, discarded))
|
|
1750
|
+
]
|
|
1751
|
+
|
|
1752
|
+
def __init__(self, obj, **kwargs):
|
|
1753
|
+
self.obj = obj
|
|
1754
|
+
self.t = obj.transform
|
|
1755
|
+
self.scene = obj.scene
|
|
1756
|
+
self._axises = ('x', 'y')
|
|
1757
|
+
self._xvel = kwargs.get('xvel', 0)
|
|
1758
|
+
self._yvel = kwargs.get('yvel', 0)
|
|
1759
|
+
self._avel = kwargs.get('avel', 0)
|
|
1760
|
+
self.dynamic = kwargs.get('dynamic', True)
|
|
1761
|
+
self.bounciness = kwargs.get('bounciness', 0)
|
|
1762
|
+
self.friction = kwargs.get('friction', 0)
|
|
1763
|
+
gravity = kwargs.get('gravity', None)
|
|
1764
|
+
self._gravity = None if gravity is None else tuple(gravity)
|
|
1765
|
+
if self._gravity is not None and len(self._gravity) != 2:
|
|
1766
|
+
raise ValueError('gravity must be a (x, y) pair or None')
|
|
1767
|
+
self._drag = kwargs.get('drag', None)
|
|
1768
|
+
self._adrag = kwargs.get('adrag', None)
|
|
1769
|
+
self._precision = kwargs.get('precision', None)
|
|
1770
|
+
self._rprecision = kwargs.get('rprecision', 3)
|
|
1771
|
+
if (self._precision is not None and self._precision <= 0) or self._rprecision <= 0:
|
|
1772
|
+
raise ValueError('precision and rprecision must be positive')
|
|
1773
|
+
self.mass = kwargs.get('mass', None)
|
|
1774
|
+
self.automass = kwargs.get('automass', self.mass is None)
|
|
1775
|
+
def masscal(prop='text'):
|
|
1776
|
+
if self.automass and prop.endswith('text'):
|
|
1777
|
+
self.mass = sum(1 for c in self.t._htext if c not in (' ', '\n'))
|
|
1778
|
+
if self.mass is None:
|
|
1779
|
+
self.mass = sum(1 for c in self.t._htext if c not in (' ', '\n'))
|
|
1780
|
+
self.t.onchange(masscal)
|
|
1781
|
+
self.solid = kwargs.get('solid', True)
|
|
1782
|
+
self.hitbox = kwargs.get('hitbox', False)
|
|
1783
|
+
self.phantom = kwargs.get('phantom', False)
|
|
1784
|
+
self.slope = kwargs.get('slope', True)
|
|
1785
|
+
self.touch = (False, False)
|
|
1786
|
+
self.sides = (False, False, False, False)
|
|
1787
|
+
self._touch_callbacks = []
|
|
1788
|
+
self._pixel_cache = None
|
|
1789
|
+
self._bounds_cache = None
|
|
1790
|
+
self._body_cache = None
|
|
1791
|
+
self._ext_cache = None
|
|
1792
|
+
self._good = None
|
|
1793
|
+
self._depenskip = 0
|
|
1794
|
+
def dirty(attr):
|
|
1795
|
+
self._dirty(attr)
|
|
1796
|
+
self.t.onchange(dirty)
|
|
1797
|
+
|
|
1798
|
+
def __getitem__(self, key):
|
|
1799
|
+
return getattr(self, key)
|
|
1800
|
+
|
|
1801
|
+
def __setitem__(self, key, value):
|
|
1802
|
+
setattr(self, key, value)
|
|
1803
|
+
|
|
1804
|
+
@property
|
|
1805
|
+
def solid(self):
|
|
1806
|
+
return self._solid
|
|
1807
|
+
@solid.setter
|
|
1808
|
+
def solid(self, value):
|
|
1809
|
+
self._solid = value
|
|
1810
|
+
if value:
|
|
1811
|
+
if not any(o is self.obj for o in self.scene.solids):
|
|
1812
|
+
self.scene.solids.append(self.obj)
|
|
1813
|
+
else:
|
|
1814
|
+
self.scene.solids = [o for o in self.scene.solids if o is not self.obj]
|
|
1815
|
+
|
|
1816
|
+
@property
|
|
1817
|
+
def hitbox(self):
|
|
1818
|
+
return self._hitbox
|
|
1819
|
+
@hitbox.setter
|
|
1820
|
+
def hitbox(self, value):
|
|
1821
|
+
old = getattr(self, '_hitbox', None)
|
|
1822
|
+
self._hitbox = value
|
|
1823
|
+
self._dirty()
|
|
1824
|
+
if old is not None and old != value:
|
|
1825
|
+
self._change('hitbox', old)
|
|
1826
|
+
|
|
1827
|
+
@property
|
|
1828
|
+
def xvel(self):
|
|
1829
|
+
return self._xvel
|
|
1830
|
+
@xvel.setter
|
|
1831
|
+
def xvel(self, value):
|
|
1832
|
+
old = self._xvel
|
|
1833
|
+
self._xvel = value
|
|
1834
|
+
self._change('xvel', old)
|
|
1835
|
+
|
|
1836
|
+
@property
|
|
1837
|
+
def yvel(self):
|
|
1838
|
+
return self._yvel
|
|
1839
|
+
@yvel.setter
|
|
1840
|
+
def yvel(self, value):
|
|
1841
|
+
old = self._yvel
|
|
1842
|
+
self._yvel = value
|
|
1843
|
+
self._change('yvel', old)
|
|
1844
|
+
|
|
1845
|
+
@property
|
|
1846
|
+
def avel(self):
|
|
1847
|
+
return self._avel
|
|
1848
|
+
@avel.setter
|
|
1849
|
+
def avel(self, value):
|
|
1850
|
+
old = self._avel
|
|
1851
|
+
self._avel = value
|
|
1852
|
+
self._change('avel', old)
|
|
1853
|
+
|
|
1854
|
+
@property
|
|
1855
|
+
def mass(self):
|
|
1856
|
+
return self._mass
|
|
1857
|
+
@mass.setter
|
|
1858
|
+
def mass(self, value):
|
|
1859
|
+
self._mass = max(1, value) if isinstance(value, (int, float)) else value
|
|
1860
|
+
|
|
1861
|
+
@property
|
|
1862
|
+
def gravity(self):
|
|
1863
|
+
return self.scene.gravity if self._gravity is None else self._gravity
|
|
1864
|
+
@gravity.setter
|
|
1865
|
+
def gravity(self, value):
|
|
1866
|
+
old = self._gravity
|
|
1867
|
+
if value is not None:
|
|
1868
|
+
value = tuple(value)
|
|
1869
|
+
if len(value) != 2:
|
|
1870
|
+
raise ValueError('gravity must be a (x, y) pair or None')
|
|
1871
|
+
self._gravity = value
|
|
1872
|
+
self._change('gravity', old)
|
|
1873
|
+
|
|
1874
|
+
@property
|
|
1875
|
+
def drag(self):
|
|
1876
|
+
return self.scene.drag if self._drag is None else self._drag
|
|
1877
|
+
@drag.setter
|
|
1878
|
+
def drag(self, value):
|
|
1879
|
+
old = self._drag
|
|
1880
|
+
self._drag = value
|
|
1881
|
+
self._change('drag', old)
|
|
1882
|
+
|
|
1883
|
+
@property
|
|
1884
|
+
def adrag(self):
|
|
1885
|
+
return self.drag if self._adrag is None else self._adrag
|
|
1886
|
+
@adrag.setter
|
|
1887
|
+
def adrag(self, value):
|
|
1888
|
+
old = self._adrag
|
|
1889
|
+
self._adrag = value
|
|
1890
|
+
self._change('adrag', old)
|
|
1891
|
+
|
|
1892
|
+
@property
|
|
1893
|
+
def precision(self):
|
|
1894
|
+
return self.scene.precision if self._precision is None else self._precision
|
|
1895
|
+
@precision.setter
|
|
1896
|
+
def precision(self, value):
|
|
1897
|
+
if value is not None and value <= 0:
|
|
1898
|
+
raise ValueError('precision must be positive or None')
|
|
1899
|
+
old = self._precision
|
|
1900
|
+
self._precision = value
|
|
1901
|
+
self._change('precision', old)
|
|
1902
|
+
|
|
1903
|
+
@property
|
|
1904
|
+
def rprecision(self):
|
|
1905
|
+
return self._rprecision
|
|
1906
|
+
@rprecision.setter
|
|
1907
|
+
def rprecision(self, value):
|
|
1908
|
+
if value <= 0:
|
|
1909
|
+
raise ValueError('rprecision must be positive')
|
|
1910
|
+
old = self._rprecision
|
|
1911
|
+
self._rprecision = value
|
|
1912
|
+
self._change('rprecision', old)
|
|
1913
|
+
|
|
1914
|
+
def _dirty(self, attr=None):
|
|
1915
|
+
self._pixel_cache = None
|
|
1916
|
+
self._bounds_cache = None
|
|
1917
|
+
self._body_cache = None
|
|
1918
|
+
if attr != 'x' and attr != 'y':
|
|
1919
|
+
self._ext_cache = None
|
|
1920
|
+
|
|
1921
|
+
def _extent(self):
|
|
1922
|
+
ext = self._ext_cache
|
|
1923
|
+
if ext is None:
|
|
1924
|
+
hitbox = self.hitbox
|
|
1925
|
+
text = self.t._htext if hitbox else self.t._ptext
|
|
1926
|
+
lo = hi = top = bottom = None
|
|
1927
|
+
for i, row in enumerate(text.splitlines()):
|
|
1928
|
+
if hitbox:
|
|
1929
|
+
if not row:
|
|
1930
|
+
continue
|
|
1931
|
+
a, b = 0, len(row) - 1
|
|
1932
|
+
else:
|
|
1933
|
+
if not row.strip(' '):
|
|
1934
|
+
continue
|
|
1935
|
+
a = len(row) - len(row.lstrip(' '))
|
|
1936
|
+
b = len(row.rstrip(' ')) - 1
|
|
1937
|
+
if lo is None or a < lo:
|
|
1938
|
+
lo = a
|
|
1939
|
+
if hi is None or b > hi:
|
|
1940
|
+
hi = b
|
|
1941
|
+
if top is None:
|
|
1942
|
+
top = i
|
|
1943
|
+
bottom = i
|
|
1944
|
+
ext = (lo, hi, top, bottom) if lo is not None else ()
|
|
1945
|
+
self._ext_cache = ext
|
|
1946
|
+
return ext
|
|
1947
|
+
|
|
1948
|
+
def _extent_bounds(self, x=None, y=None):
|
|
1949
|
+
ext = self._extent()
|
|
1950
|
+
if not ext:
|
|
1951
|
+
return None
|
|
1952
|
+
t = self.t
|
|
1953
|
+
x = t.x if x is None else x
|
|
1954
|
+
y = t.y if y is None else y
|
|
1955
|
+
if self.hitbox:
|
|
1956
|
+
ox = oy = 0
|
|
1957
|
+
else:
|
|
1958
|
+
ox, oy = t._pvx, t._pvy
|
|
1959
|
+
rx = round(x + ox)
|
|
1960
|
+
ry = round(y + oy)
|
|
1961
|
+
return (rx + ext[0], rx + ext[1], ry - ext[3], ry - ext[2])
|
|
1962
|
+
|
|
1963
|
+
def ontouch(self, callback, when=None, once=False):
|
|
1964
|
+
l = _argslen(callback)
|
|
1965
|
+
fired = [False]
|
|
1966
|
+
def wrapped(obj2, intersection):
|
|
1967
|
+
if once and fired[0]: return
|
|
1968
|
+
if when is None or when(obj2):
|
|
1969
|
+
if once: fired[0] = True
|
|
1970
|
+
if l > 1: callback(obj2, intersection)
|
|
1971
|
+
elif l > 0: callback(obj2)
|
|
1972
|
+
else: callback()
|
|
1973
|
+
if once and wrapped in self._touch_callbacks:
|
|
1974
|
+
self._touch_callbacks.remove(wrapped)
|
|
1975
|
+
self._touch_callbacks.append(wrapped)
|
|
1976
|
+
return callback
|
|
1977
|
+
|
|
1978
|
+
def offtouch(self, callback):
|
|
1979
|
+
self._touch_callbacks = [
|
|
1980
|
+
cb for cb in self._touch_callbacks if cb.__closure__ and
|
|
1981
|
+
cb.__closure__[0].cell_contents is not callback
|
|
1982
|
+
]
|
|
1983
|
+
|
|
1984
|
+
def ghost(self, other):
|
|
1985
|
+
if not any(g is other for g in self.obj._ghosts):
|
|
1986
|
+
self.obj._ghosts.append(other)
|
|
1987
|
+
|
|
1988
|
+
def getpixels(self, x=None, y=None, text=None):
|
|
1989
|
+
default_call = x is None and y is None and text is None
|
|
1990
|
+
if default_call and self._pixel_cache is not None:
|
|
1991
|
+
return self._pixel_cache
|
|
1992
|
+
|
|
1993
|
+
x = self.t.x if x is None else x
|
|
1994
|
+
y = self.t.y if y is None else y
|
|
1995
|
+
if self.hitbox:
|
|
1996
|
+
text = self.t._htext
|
|
1997
|
+
ox = oy = 0
|
|
1998
|
+
elif text is None:
|
|
1999
|
+
text = self.t._ptext
|
|
2000
|
+
ox, oy = self.t._pvx, self.t._pvy
|
|
2001
|
+
else:
|
|
2002
|
+
ox, oy = self.t.vx, self.t.vy
|
|
2003
|
+
rx = round(x + ox)
|
|
2004
|
+
ry = round(y + oy)
|
|
2005
|
+
pixels = set()
|
|
2006
|
+
for i, row in enumerate(text.splitlines()):
|
|
2007
|
+
for j, char in enumerate(row):
|
|
2008
|
+
if (char != ' ' and char != '\n' and not self.hitbox) or self.hitbox:
|
|
2009
|
+
pixels.add((rx + j, ry - i))
|
|
2010
|
+
|
|
2011
|
+
if default_call:
|
|
2012
|
+
self._pixel_cache = pixels
|
|
2013
|
+
self._bounds_cache = None
|
|
2014
|
+
return pixels
|
|
2015
|
+
|
|
2016
|
+
def bounds(self, pixels=None):
|
|
2017
|
+
if pixels is None:
|
|
2018
|
+
if self._bounds_cache is not None:
|
|
2019
|
+
return self._bounds_cache
|
|
2020
|
+
if self._pixel_cache is None:
|
|
2021
|
+
result = self._extent_bounds()
|
|
2022
|
+
self._bounds_cache = result
|
|
2023
|
+
return result
|
|
2024
|
+
pixels = self._pixel_cache
|
|
2025
|
+
if not pixels:
|
|
2026
|
+
result = None
|
|
2027
|
+
else:
|
|
2028
|
+
xs = [p[0] for p in pixels]
|
|
2029
|
+
ys = [p[1] for p in pixels]
|
|
2030
|
+
result = (min(xs), max(xs), min(ys), max(ys))
|
|
2031
|
+
if pixels is self._pixel_cache:
|
|
2032
|
+
self._bounds_cache = result
|
|
2033
|
+
return result
|
|
2034
|
+
|
|
2035
|
+
def collides(self, objs, x=None, y=None, text=None, full=False):
|
|
2036
|
+
if text is None:
|
|
2037
|
+
b1 = self._extent_bounds(x, y)
|
|
2038
|
+
if b1 is None:
|
|
2039
|
+
return {}
|
|
2040
|
+
pixels1 = None
|
|
2041
|
+
else:
|
|
2042
|
+
pixels1 = self.getpixels(x, y, text)
|
|
2043
|
+
if not pixels1:
|
|
2044
|
+
return {}
|
|
2045
|
+
b1 = self.bounds(pixels1)
|
|
2046
|
+
|
|
2047
|
+
if not isinstance(objs, list):
|
|
2048
|
+
objs = [objs]
|
|
2049
|
+
|
|
2050
|
+
result = {}
|
|
2051
|
+
for obj2 in objs:
|
|
2052
|
+
if obj2 is self.obj:
|
|
2053
|
+
continue
|
|
2054
|
+
p2 = obj2.physics
|
|
2055
|
+
b2 = p2.bounds()
|
|
2056
|
+
if b2 is None or b2[1] < b1[0] or b2[0] > b1[1] or b2[3] < b1[2] or b2[2] > b1[3]:
|
|
2057
|
+
continue
|
|
2058
|
+
if pixels1 is None:
|
|
2059
|
+
pixels1 = self.getpixels(x, y)
|
|
2060
|
+
intersection = pixels1 & p2.getpixels()
|
|
2061
|
+
if intersection:
|
|
2062
|
+
result[obj2] = intersection
|
|
2063
|
+
if not self.phantom and not full:
|
|
2064
|
+
break
|
|
2065
|
+
return result
|
|
2066
|
+
|
|
2067
|
+
def overlaps(self, pixels=None):
|
|
2068
|
+
b1 = self.bounds() if pixels is None else self.bounds(pixels)
|
|
2069
|
+
found = {}
|
|
2070
|
+
if b1 is None:
|
|
2071
|
+
return found
|
|
2072
|
+
for obj2 in self.scene.solids:
|
|
2073
|
+
if obj2 is self.obj:
|
|
2074
|
+
continue
|
|
2075
|
+
p2 = getattr(obj2, 'physics', None)
|
|
2076
|
+
if p2 is None or p2.phantom or not p2.solid:
|
|
2077
|
+
continue
|
|
2078
|
+
b2 = p2.bounds()
|
|
2079
|
+
if b2 is None or b2[1] < b1[0] or b2[0] > b1[1] or b2[3] < b1[2] or b2[2] > b1[3]:
|
|
2080
|
+
continue
|
|
2081
|
+
if pixels is None:
|
|
2082
|
+
pixels = self.getpixels()
|
|
2083
|
+
inter = pixels & p2.getpixels()
|
|
2084
|
+
if inter and not self._ghosted(obj2, inter):
|
|
2085
|
+
found[obj2] = inter
|
|
2086
|
+
return found
|
|
2087
|
+
|
|
2088
|
+
def body(self):
|
|
2089
|
+
if self._body_cache is None:
|
|
2090
|
+
pixels = self.getpixels()
|
|
2091
|
+
n = len(pixels)
|
|
2092
|
+
if n == 0:
|
|
2093
|
+
self._body_cache = (0.0, 0.0, 1.0)
|
|
2094
|
+
else:
|
|
2095
|
+
A = self.scene.aspect
|
|
2096
|
+
cx = sum(p[0] for p in pixels) / n
|
|
2097
|
+
cy = sum(p[1] for p in pixels) / n
|
|
2098
|
+
spread = sum(((p[0] - cx) * A) ** 2 + (p[1] - cy) ** 2 for p in pixels) / n
|
|
2099
|
+
self._body_cache = (cx, cy, spread + (A * A + 1) / 12)
|
|
2100
|
+
return self._body_cache
|
|
2101
|
+
|
|
2102
|
+
def _invinertia(self):
|
|
2103
|
+
if self.hitbox or not self.slope or not self.dynamic or not self.solid or len(self.getpixels()) < 2:
|
|
2104
|
+
return 0.0
|
|
2105
|
+
return 1.0 / (self.mass * self.body()[2])
|
|
2106
|
+
|
|
2107
|
+
def _velocity(self):
|
|
2108
|
+
A = self.scene.aspect
|
|
2109
|
+
return self.xvel / self.mass * A, self.yvel / self.mass
|
|
2110
|
+
|
|
2111
|
+
def _omega(self):
|
|
2112
|
+
return -math.radians(self.avel / self.mass)
|
|
2113
|
+
|
|
2114
|
+
def _impulse(self, jx, jy, rx=0.0, ry=0.0):
|
|
2115
|
+
A = self.scene.aspect
|
|
2116
|
+
self.xvel += jx / A
|
|
2117
|
+
self.yvel += jy
|
|
2118
|
+
if rx or ry:
|
|
2119
|
+
self._spin(rx * jy - ry * jx)
|
|
2120
|
+
|
|
2121
|
+
def _spin(self, torque):
|
|
2122
|
+
inv = self._invinertia()
|
|
2123
|
+
if inv and torque:
|
|
2124
|
+
self.avel -= math.degrees(torque * inv) * self.mass
|
|
2125
|
+
|
|
2126
|
+
def _arm(self, point):
|
|
2127
|
+
A = self.scene.aspect
|
|
2128
|
+
cx, cy, _ = self.body()
|
|
2129
|
+
return (point[0] - cx) * A, point[1] - cy
|
|
2130
|
+
|
|
2131
|
+
def _shifted(self, dx, dy):
|
|
2132
|
+
return {(p[0] + dx, p[1] + dy) for p in self.getpixels()}
|
|
2133
|
+
|
|
2134
|
+
def _ghosted(self, other, intersection=None):
|
|
2135
|
+
for owner, partner in ((self.obj, other), (other, self.obj)):
|
|
2136
|
+
for ghost in list(getattr(owner, '_ghosts', ())):
|
|
2137
|
+
if ghost is partner:
|
|
2138
|
+
return True
|
|
2139
|
+
if callable(ghost):
|
|
2140
|
+
count = _argslen(ghost)
|
|
2141
|
+
if count > 1:
|
|
2142
|
+
result = ghost(partner, intersection)
|
|
2143
|
+
elif count > 0:
|
|
2144
|
+
result = ghost(partner)
|
|
2145
|
+
else:
|
|
2146
|
+
result = ghost()
|
|
2147
|
+
if result:
|
|
2148
|
+
return True
|
|
2149
|
+
if intersection:
|
|
2150
|
+
jt = getattr(self.obj, 'joint', None)
|
|
2151
|
+
if jt is not None and jt._seam(other, intersection):
|
|
2152
|
+
return True
|
|
2153
|
+
return False
|
|
2154
|
+
|
|
2155
|
+
def _blocked(self, dx, dy, exclude=()):
|
|
2156
|
+
b = self.bounds()
|
|
2157
|
+
hits = []
|
|
2158
|
+
if b is None:
|
|
2159
|
+
return hits
|
|
2160
|
+
shifted = None
|
|
2161
|
+
for obj2 in self.scene.solids:
|
|
2162
|
+
if obj2 is self.obj or obj2 in exclude:
|
|
2163
|
+
continue
|
|
2164
|
+
p2 = getattr(obj2, 'physics', None)
|
|
2165
|
+
if p2 is None or p2.phantom or not p2.solid:
|
|
2166
|
+
continue
|
|
2167
|
+
b2 = p2.bounds()
|
|
2168
|
+
if b2 is None or b2[1] < b[0] + dx or b2[0] > b[1] + dx or b2[3] < b[2] + dy or b2[2] > b[3] + dy:
|
|
2169
|
+
continue
|
|
2170
|
+
if shifted is None:
|
|
2171
|
+
shifted = self._shifted(dx, dy)
|
|
2172
|
+
inter = shifted & p2.getpixels()
|
|
2173
|
+
if inter and not self._ghosted(obj2, inter):
|
|
2174
|
+
hits.append(obj2)
|
|
2175
|
+
return hits
|
|
2176
|
+
|
|
2177
|
+
def cluster(self, start, dx, dy):
|
|
2178
|
+
members = [start]
|
|
2179
|
+
seen = {id(start.obj), id(self.obj)}
|
|
2180
|
+
queue = [start]
|
|
2181
|
+
while queue:
|
|
2182
|
+
body = queue.pop()
|
|
2183
|
+
if not body.dynamic:
|
|
2184
|
+
return None
|
|
2185
|
+
for obj2 in body._blocked(dx, dy, exclude=[m.obj for m in members] + [self.obj]):
|
|
2186
|
+
p2 = obj2.physics
|
|
2187
|
+
if id(obj2) in seen:
|
|
2188
|
+
continue
|
|
2189
|
+
if not p2.dynamic:
|
|
2190
|
+
return None
|
|
2191
|
+
seen.add(id(obj2))
|
|
2192
|
+
members.append(p2)
|
|
2193
|
+
queue.append(p2)
|
|
2194
|
+
return members
|
|
2195
|
+
|
|
2196
|
+
def _solve(self, other, group, point, nx, ny, restitution=None):
|
|
2197
|
+
A = self.scene.aspect
|
|
2198
|
+
ra = self._arm(point)
|
|
2199
|
+
va = self._velocity()
|
|
2200
|
+
wa = self._omega()
|
|
2201
|
+
vax = va[0] - wa * ra[1]
|
|
2202
|
+
vay = va[1] + wa * ra[0]
|
|
2203
|
+
|
|
2204
|
+
if group is None:
|
|
2205
|
+
rb = (0.0, 0.0)
|
|
2206
|
+
vbx = vby = 0.0
|
|
2207
|
+
invmb = 0.0
|
|
2208
|
+
invib = 0.0
|
|
2209
|
+
e_other = other.bounciness
|
|
2210
|
+
mu_other = other.friction
|
|
2211
|
+
single = None
|
|
2212
|
+
else:
|
|
2213
|
+
single = group[0] if len(group) == 1 else None
|
|
2214
|
+
total = sum(m.mass for m in group)
|
|
2215
|
+
invmb = 1.0 / total
|
|
2216
|
+
if single is not None:
|
|
2217
|
+
rb = single._arm(point)
|
|
2218
|
+
vb = single._velocity()
|
|
2219
|
+
wb = single._omega()
|
|
2220
|
+
vbx = vb[0] - wb * rb[1]
|
|
2221
|
+
vby = vb[1] + wb * rb[0]
|
|
2222
|
+
invib = single._invinertia()
|
|
2223
|
+
else:
|
|
2224
|
+
rb = (0.0, 0.0)
|
|
2225
|
+
vbx = sum(m.xvel / m.mass * A * m.mass for m in group) / total
|
|
2226
|
+
vby = sum(m.yvel for m in group) / total
|
|
2227
|
+
invib = 0.0
|
|
2228
|
+
e_other = max(m.bounciness for m in group)
|
|
2229
|
+
mu_other = max(m.friction for m in group)
|
|
2230
|
+
|
|
2231
|
+
vn = (vax - vbx) * nx + (vay - vby) * ny
|
|
2232
|
+
if vn >= 0:
|
|
2233
|
+
return False
|
|
2234
|
+
|
|
2235
|
+
e = max(self.bounciness, e_other) if restitution is None else restitution
|
|
2236
|
+
if -vn < 0.25:
|
|
2237
|
+
e = 0.0
|
|
2238
|
+
|
|
2239
|
+
inva = 1.0 / self.mass
|
|
2240
|
+
ia = self._invinertia()
|
|
2241
|
+
can = (ra[0] * ny - ra[1] * nx) ** 2 * ia
|
|
2242
|
+
cbn = (rb[0] * ny - rb[1] * nx) ** 2 * invib
|
|
2243
|
+
denom = inva + invmb + can + cbn
|
|
2244
|
+
gravity = self.gravity
|
|
2245
|
+
boost = e * max(0.0, -(gravity[0] * A * nx + gravity[1] * ny))
|
|
2246
|
+
j = (-(1 + e) * vn + boost) / denom
|
|
2247
|
+
|
|
2248
|
+
self._impulse(j * nx, j * ny, *ra)
|
|
2249
|
+
self._apply_other(group, single, -j * nx, -j * ny, rb)
|
|
2250
|
+
|
|
2251
|
+
mu = max(self.friction, mu_other)
|
|
2252
|
+
if mu:
|
|
2253
|
+
tx, ty = -ny, nx
|
|
2254
|
+
va = self._velocity()
|
|
2255
|
+
wa = self._omega()
|
|
2256
|
+
vax = va[0] - wa * ra[1]
|
|
2257
|
+
vay = va[1] + wa * ra[0]
|
|
2258
|
+
if group is None:
|
|
2259
|
+
vbx = vby = 0.0
|
|
2260
|
+
elif single is not None:
|
|
2261
|
+
vb = single._velocity()
|
|
2262
|
+
wb = single._omega()
|
|
2263
|
+
vbx = vb[0] - wb * rb[1]
|
|
2264
|
+
vby = vb[1] + wb * rb[0]
|
|
2265
|
+
else:
|
|
2266
|
+
vbx = sum(m.xvel * A / 1.0 for m in group) / total
|
|
2267
|
+
vby = sum(m.yvel for m in group) / total
|
|
2268
|
+
vt = (vax - vbx) * tx + (vay - vby) * ty
|
|
2269
|
+
cat = (ra[0] * ty - ra[1] * tx) ** 2 * ia
|
|
2270
|
+
cbt = (rb[0] * ty - rb[1] * tx) ** 2 * invib
|
|
2271
|
+
jt = -vt / (inva + invmb + cat + cbt)
|
|
2272
|
+
limit = mu * j
|
|
2273
|
+
jt = max(-limit, min(limit, jt))
|
|
2274
|
+
self._impulse(jt * tx, jt * ty, *ra)
|
|
2275
|
+
self._apply_other(group, single, -jt * tx, -jt * ty, rb)
|
|
2276
|
+
return True
|
|
2277
|
+
|
|
2278
|
+
def _apply_other(self, group, single, jx, jy, rb):
|
|
2279
|
+
if group is None:
|
|
2280
|
+
return
|
|
2281
|
+
if single is not None:
|
|
2282
|
+
single._impulse(jx, jy, *rb)
|
|
2283
|
+
return
|
|
2284
|
+
total = sum(m.mass for m in group)
|
|
2285
|
+
for m in group:
|
|
2286
|
+
share = m.mass / total
|
|
2287
|
+
m._impulse(jx * share, jy * share)
|
|
2288
|
+
|
|
2289
|
+
def _contact_point(self, pixels, nx=None, ny=None):
|
|
2290
|
+
return _contact_point_cells(pixels, nx, ny, self.body()[:2])
|
|
2291
|
+
|
|
2292
|
+
def _contact_normal(self, other, point, nx, ny, inside):
|
|
2293
|
+
return _contact_normal_cells(self.getpixels(), other.getpixels(), point, nx, ny, inside,
|
|
2294
|
+
self.slope and other.slope, self.scene.aspect)
|
|
2295
|
+
|
|
2296
|
+
def normal(self, other, local=None):
|
|
2297
|
+
return self.t.normal(other, local)
|
|
2298
|
+
|
|
2299
|
+
def _mtv(self, other_pixels):
|
|
2300
|
+
return _mtv_cells(self.getpixels(), other_pixels, self.scene.aspect)
|
|
2301
|
+
|
|
2302
|
+
def _remember(self):
|
|
2303
|
+
self._good = (self.t.x, self.t.y, self.t.rotation)
|
|
2304
|
+
|
|
2305
|
+
def _settle(self, dx, dy):
|
|
2306
|
+
if dx and self.xvel * dx < 0:
|
|
2307
|
+
self.xvel = 0
|
|
2308
|
+
if dy and self.yvel * dy < 0:
|
|
2309
|
+
self.yvel = 0
|
|
2310
|
+
|
|
2311
|
+
def _tunnels(self, found, dx, dy):
|
|
2312
|
+
cx, cy, _ = self.body()
|
|
2313
|
+
for obj2 in found:
|
|
2314
|
+
p2 = obj2.physics
|
|
2315
|
+
if p2.dynamic:
|
|
2316
|
+
continue
|
|
2317
|
+
b = p2.bounds()
|
|
2318
|
+
if b is None:
|
|
2319
|
+
continue
|
|
2320
|
+
mx, my = (b[0] + b[1]) / 2, (b[2] + b[3]) / 2
|
|
2321
|
+
if dx and (cx - mx) * (cx + dx - mx) < 0:
|
|
2322
|
+
return True
|
|
2323
|
+
if dy and (cy - my) * (cy + dy - my) < 0:
|
|
2324
|
+
return True
|
|
2325
|
+
return False
|
|
2326
|
+
|
|
2327
|
+
def _unstick(self):
|
|
2328
|
+
t = self.t
|
|
2329
|
+
found = self.overlaps()
|
|
2330
|
+
good = self._good
|
|
2331
|
+
if good is not None:
|
|
2332
|
+
x, y, r = t.x, t.y, t.rotation
|
|
2333
|
+
if r != good[2]:
|
|
2334
|
+
t.rotation = good[2]
|
|
2335
|
+
if not self.overlaps():
|
|
2336
|
+
self.avel = 0
|
|
2337
|
+
self._remember()
|
|
2338
|
+
return
|
|
2339
|
+
t.x, t.y = good[0], good[1]
|
|
2340
|
+
if not self.overlaps():
|
|
2341
|
+
self.xvel = self.yvel = self.avel = 0
|
|
2342
|
+
self._remember()
|
|
2343
|
+
return
|
|
2344
|
+
t.x, t.y = x, y
|
|
2345
|
+
if t.rotation != r:
|
|
2346
|
+
t.rotation = r
|
|
2347
|
+
for dx, dy in _offsets(6, self.scene.aspect):
|
|
2348
|
+
if not self._tunnels(found, dx, dy) and not self.overlaps(self._shifted(dx, dy)):
|
|
2349
|
+
t.x += dx
|
|
2350
|
+
t.y += dy
|
|
2351
|
+
self._settle(dx, dy)
|
|
2352
|
+
self._remember()
|
|
2353
|
+
return
|
|
2354
|
+
for dx, dy in ((0, 1), (1, 0), (-1, 0), (0, -1)):
|
|
2355
|
+
for k in range(7, 129):
|
|
2356
|
+
if not self._tunnels(found, dx * k, dy * k) and not self.overlaps(self._shifted(dx * k, dy * k)):
|
|
2357
|
+
t.x += dx * k
|
|
2358
|
+
t.y += dy * k
|
|
2359
|
+
self._settle(dx, dy)
|
|
2360
|
+
self._remember()
|
|
2361
|
+
return
|
|
2362
|
+
self._depenskip = 4
|
|
2363
|
+
|
|
2364
|
+
def _depenetrate(self):
|
|
2365
|
+
if self.phantom:
|
|
2366
|
+
return
|
|
2367
|
+
if self._depenskip > 0:
|
|
2368
|
+
self._depenskip -= 1
|
|
2369
|
+
return
|
|
2370
|
+
jt = getattr(self.obj, 'joint', None)
|
|
2371
|
+
pinned, owned = jt._hierarchy() if jt is not None else ((), ())
|
|
2372
|
+
budget = _RELIEF if pinned else None
|
|
2373
|
+
def hits():
|
|
2374
|
+
found = self.overlaps()
|
|
2375
|
+
if owned:
|
|
2376
|
+
return {o: v for o, v in found.items() if not any(o is d for d in owned)}
|
|
2377
|
+
return found
|
|
2378
|
+
seen = set()
|
|
2379
|
+
for _ in range(12):
|
|
2380
|
+
found = hits()
|
|
2381
|
+
if not found:
|
|
2382
|
+
self._remember()
|
|
2383
|
+
return
|
|
2384
|
+
key = (round(self.t.x, 4), round(self.t.y, 4), frozenset((id(o), round(o.transform.x, 4), round(o.transform.y, 4)) for o in found))
|
|
2385
|
+
if key in seen:
|
|
2386
|
+
break
|
|
2387
|
+
seen.add(key)
|
|
2388
|
+
obj2 = max(found, key=lambda o: len(found[o]))
|
|
2389
|
+
p2 = obj2.physics
|
|
2390
|
+
best = self._mtv(p2.getpixels())
|
|
2391
|
+
if best is None:
|
|
2392
|
+
break
|
|
2393
|
+
_, dx, dy, k = best
|
|
2394
|
+
if budget is not None:
|
|
2395
|
+
k = min(k, budget)
|
|
2396
|
+
budget -= k
|
|
2397
|
+
movable = p2.dynamic and not any(obj2 is a for a in pinned)
|
|
2398
|
+
if movable:
|
|
2399
|
+
mine_share = int(min(k, max(1, math.ceil(k * p2.mass / (self.mass + p2.mass)))))
|
|
2400
|
+
theirs_share = k - mine_share
|
|
2401
|
+
else:
|
|
2402
|
+
mine_share, theirs_share = k, 0
|
|
2403
|
+
if movable:
|
|
2404
|
+
if theirs_share and any(o is not self.obj for o in p2.overlaps(p2._shifted(-dx * theirs_share, -dy * theirs_share))):
|
|
2405
|
+
mine_share, theirs_share = k, 0
|
|
2406
|
+
if mine_share and any(o is not obj2 for o in self.overlaps(self._shifted(dx * mine_share, dy * mine_share))):
|
|
2407
|
+
if any(o is not self.obj for o in p2.overlaps(p2._shifted(-dx * k, -dy * k))):
|
|
2408
|
+
break
|
|
2409
|
+
mine_share, theirs_share = 0, k
|
|
2410
|
+
self.t.x += dx * mine_share
|
|
2411
|
+
self.t.y += dy * mine_share
|
|
2412
|
+
if theirs_share:
|
|
2413
|
+
p2.t.x -= dx * theirs_share
|
|
2414
|
+
p2.t.y -= dy * theirs_share
|
|
2415
|
+
for body, sign in ((self, 1), (p2, -1)):
|
|
2416
|
+
if body is p2 and not theirs_share:
|
|
2417
|
+
continue
|
|
2418
|
+
if dx and body.xvel * dx * sign < 0:
|
|
2419
|
+
body.xvel = 0
|
|
2420
|
+
if dy and body.yvel * dy * sign < 0:
|
|
2421
|
+
body.yvel = 0
|
|
2422
|
+
if budget is not None and budget <= 0:
|
|
2423
|
+
if not hits():
|
|
2424
|
+
self._remember()
|
|
2425
|
+
return
|
|
2426
|
+
if hits():
|
|
2427
|
+
self._unstick()
|
|
2428
|
+
else:
|
|
2429
|
+
self._remember()
|
|
2430
|
+
|
|
2431
|
+
@property
|
|
2432
|
+
def grounded(self):
|
|
2433
|
+
return self.sides[2]
|
|
2434
|
+
|
|
2435
|
+
@property
|
|
2436
|
+
def ceiling(self):
|
|
2437
|
+
return self.sides[3]
|
|
2438
|
+
|
|
2439
|
+
def _probe(self):
|
|
2440
|
+
touch = [False, False]
|
|
2441
|
+
sides = [False, False, False, False]
|
|
2442
|
+
if self.solid and not self.phantom:
|
|
2443
|
+
mine = None
|
|
2444
|
+
for ia in (0, 1):
|
|
2445
|
+
for s in (1, -1):
|
|
2446
|
+
dx, dy = (s, 0) if ia == 0 else (0, s)
|
|
2447
|
+
for obj2 in self._blocked(dx, dy):
|
|
2448
|
+
p2 = obj2.physics
|
|
2449
|
+
theirs = p2.getpixels()
|
|
2450
|
+
if mine is None:
|
|
2451
|
+
mine = self.getpixels()
|
|
2452
|
+
if not mine.isdisjoint(theirs):
|
|
2453
|
+
continue
|
|
2454
|
+
contact = [p for p in mine if (p[0] + dx, p[1] + dy) in theirs]
|
|
2455
|
+
if not contact:
|
|
2456
|
+
continue
|
|
2457
|
+
touch[ia] = True
|
|
2458
|
+
sides[ia * 2 + (0 if s == -1 else 1)] = True
|
|
2459
|
+
group = self.cluster(p2, dx, dy) if p2.dynamic else None
|
|
2460
|
+
point = self._contact_point(contact, -dx, -dy)
|
|
2461
|
+
cn = self._contact_normal(p2, point, -dx, -dy, False)
|
|
2462
|
+
self._solve(p2, group, point, cn[0], cn[1])
|
|
2463
|
+
self.touch = tuple(touch)
|
|
2464
|
+
self.sides = tuple(sides)
|
|
2465
|
+
if abs(self._xvel) < 1e-9: self._xvel = 0
|
|
2466
|
+
if abs(self._yvel) < 1e-9: self._yvel = 0
|
|
2467
|
+
if abs(self._avel) < 1e-9: self._avel = 0
|
|
2468
|
+
|
|
2469
|
+
def physics(self, axises=None):
|
|
2470
|
+
physics._running += 1
|
|
2471
|
+
try:
|
|
2472
|
+
self._physics(axises)
|
|
2473
|
+
finally:
|
|
2474
|
+
physics._running -= 1
|
|
2475
|
+
|
|
2476
|
+
def _physics(self, axises=None):
|
|
2477
|
+
axises = self._axises if axises is None else axises
|
|
2478
|
+
actualmass = self.mass
|
|
2479
|
+
gravity = self.gravity
|
|
2480
|
+
drag = self.drag
|
|
2481
|
+
precision = self.precision
|
|
2482
|
+
self._dirty('x')
|
|
2483
|
+
|
|
2484
|
+
for ia, a in enumerate(axises):
|
|
2485
|
+
self[a + 'vel'] *= drag
|
|
2486
|
+
self[a + 'vel'] += gravity[ia] * actualmass
|
|
2487
|
+
c = 1 if self[a + 'vel'] > 0 else -1 if self[a + 'vel'] < 0 else 0
|
|
2488
|
+
ch = abs(self[a + 'vel']) / actualmass
|
|
2489
|
+
d = (c, 0) if ia == 0 else (0, c)
|
|
2490
|
+
|
|
2491
|
+
while ch > 0:
|
|
2492
|
+
step = min(precision, ch)
|
|
2493
|
+
t = {a: self.t[a] + c * step}
|
|
2494
|
+
collisions = self.collides(self.scene.solids, full=True, **t)
|
|
2495
|
+
|
|
2496
|
+
physical = []
|
|
2497
|
+
for obj2, intersection in list(collisions.items()):
|
|
2498
|
+
obj2p = getattr(obj2, 'physics', None)
|
|
2499
|
+
if obj2p is None:
|
|
2500
|
+
continue
|
|
2501
|
+
for cb in list(self._touch_callbacks):
|
|
2502
|
+
cb(obj2, intersection)
|
|
2503
|
+
if getattr(self.obj, 'physics', None) is not self:
|
|
2504
|
+
return
|
|
2505
|
+
for cb in list(obj2p._touch_callbacks):
|
|
2506
|
+
cb(self.obj, intersection)
|
|
2507
|
+
if getattr(self.obj, 'physics', None) is not self:
|
|
2508
|
+
return
|
|
2509
|
+
if getattr(obj2, 'physics', None) is not obj2p:
|
|
2510
|
+
continue
|
|
2511
|
+
if self.solid and not self.phantom and not obj2p.phantom and not self._ghosted(obj2, intersection):
|
|
2512
|
+
physical.append((obj2, obj2p, intersection))
|
|
2513
|
+
|
|
2514
|
+
if physical:
|
|
2515
|
+
for obj2, obj2p, intersection in physical:
|
|
2516
|
+
if getattr(obj2, 'physics', None) is not obj2p:
|
|
2517
|
+
continue
|
|
2518
|
+
group = self.cluster(obj2p, d[0], d[1]) if obj2p.dynamic else None
|
|
2519
|
+
point = self._contact_point(intersection, -d[0], -d[1])
|
|
2520
|
+
cn = self._contact_normal(obj2p, point, -d[0], -d[1], True)
|
|
2521
|
+
self._solve(obj2p, group, point, cn[0], cn[1])
|
|
2522
|
+
break
|
|
2523
|
+
|
|
2524
|
+
self.t[a] += c * step
|
|
2525
|
+
ch -= step
|
|
2526
|
+
|
|
2527
|
+
self._depenetrate()
|
|
2528
|
+
|
|
2529
|
+
self.avel *= self.adrag
|
|
2530
|
+
degrees = self.avel / actualmass
|
|
2531
|
+
if degrees:
|
|
2532
|
+
pieces = max(1, int(math.ceil(abs(degrees) / self._rprecision)))
|
|
2533
|
+
da = degrees / pieces
|
|
2534
|
+
check = self.solid and not self.phantom and not self.hitbox
|
|
2535
|
+
for _ in range(pieces):
|
|
2536
|
+
self.t.rotation += da
|
|
2537
|
+
hit = self.overlaps() if check else None
|
|
2538
|
+
if not hit:
|
|
2539
|
+
continue
|
|
2540
|
+
contacts = []
|
|
2541
|
+
for obj2, intersection in hit.items():
|
|
2542
|
+
mtv = self._mtv(obj2.physics.getpixels())
|
|
2543
|
+
if mtv is None:
|
|
2544
|
+
continue
|
|
2545
|
+
contacts.append((obj2.physics, self._contact_point(intersection, mtv[1], mtv[2]), mtv[1], mtv[2]))
|
|
2546
|
+
self.t.rotation -= da
|
|
2547
|
+
applied = False
|
|
2548
|
+
for obj2p, point, nx, ny in contacts:
|
|
2549
|
+
group = [obj2p] if obj2p.dynamic else None
|
|
2550
|
+
if self._solve(obj2p, group, point, nx, ny):
|
|
2551
|
+
applied = True
|
|
2552
|
+
if applied:
|
|
2553
|
+
break
|
|
2554
|
+
if contacts:
|
|
2555
|
+
self.avel = 0
|
|
2556
|
+
break
|
|
2557
|
+
self.t.rotation += da
|
|
2558
|
+
|
|
2559
|
+
self._depenetrate()
|
|
2560
|
+
self._probe()
|
|
2561
|
+
|
|
2562
|
+
class pen(_lerpable):
|
|
2563
|
+
_owned = ['penchar', 'penx', 'peny', 'pradius', 'pshape']
|
|
2564
|
+
_required = [transform]
|
|
2565
|
+
_settings = ('char', 'x', 'y', 'radius', 'shape')
|
|
2566
|
+
_aliases = {'char': 'penchar', 'x': 'penx', 'y': 'peny', 'radius': 'pradius', 'shape': 'pshape'}
|
|
2567
|
+
|
|
2568
|
+
def __init__(self, obj, **kwargs):
|
|
2569
|
+
self.obj = obj
|
|
2570
|
+
self.scene = obj.scene
|
|
2571
|
+
self._x = kwargs.get('penx', 0)
|
|
2572
|
+
self._y = kwargs.get('peny', 0)
|
|
2573
|
+
self._px = self._x
|
|
2574
|
+
self._py = self._y
|
|
2575
|
+
self.char = kwargs.get('penchar', '█')
|
|
2576
|
+
self.pradius = kwargs.get('pradius', 1)
|
|
2577
|
+
self.pshape = kwargs.get('pshape', 'circle')
|
|
2578
|
+
self.active = False
|
|
2579
|
+
self.current_obj = None
|
|
2580
|
+
self._pixels = {}
|
|
2581
|
+
self._path = []
|
|
2582
|
+
self._hits = {}
|
|
2583
|
+
self._ox = 0
|
|
2584
|
+
self._oy = 0
|
|
2585
|
+
self._pcut = None
|
|
2586
|
+
self._cut_walls = {}
|
|
2587
|
+
|
|
2588
|
+
@property
|
|
2589
|
+
def x(self):
|
|
2590
|
+
return self._x
|
|
2591
|
+
@x.setter
|
|
2592
|
+
def x(self, value):
|
|
2593
|
+
self._x = value
|
|
2594
|
+
|
|
2595
|
+
@property
|
|
2596
|
+
def y(self):
|
|
2597
|
+
return self._y
|
|
2598
|
+
@y.setter
|
|
2599
|
+
def y(self, value):
|
|
2600
|
+
self._y = value
|
|
2601
|
+
|
|
2602
|
+
@property
|
|
2603
|
+
def penchar(self):
|
|
2604
|
+
return self.char
|
|
2605
|
+
@penchar.setter
|
|
2606
|
+
def penchar(self, value):
|
|
2607
|
+
self.char = value
|
|
2608
|
+
|
|
2609
|
+
@property
|
|
2610
|
+
def penx(self):
|
|
2611
|
+
return self._x
|
|
2612
|
+
@penx.setter
|
|
2613
|
+
def penx(self, value):
|
|
2614
|
+
self._x = value
|
|
2615
|
+
|
|
2616
|
+
@property
|
|
2617
|
+
def peny(self):
|
|
2618
|
+
return self._y
|
|
2619
|
+
@peny.setter
|
|
2620
|
+
def peny(self, value):
|
|
2621
|
+
self._y = value
|
|
2622
|
+
|
|
2623
|
+
@property
|
|
2624
|
+
def radius(self):
|
|
2625
|
+
return self._pradius
|
|
2626
|
+
@radius.setter
|
|
2627
|
+
def radius(self, value):
|
|
2628
|
+
self.pradius = value
|
|
2629
|
+
|
|
2630
|
+
@property
|
|
2631
|
+
def shape(self):
|
|
2632
|
+
return self._pshape
|
|
2633
|
+
@shape.setter
|
|
2634
|
+
def shape(self, value):
|
|
2635
|
+
self.pshape = value
|
|
2636
|
+
|
|
2637
|
+
@property
|
|
2638
|
+
def pradius(self):
|
|
2639
|
+
return self._pradius
|
|
2640
|
+
@pradius.setter
|
|
2641
|
+
def pradius(self, value):
|
|
2642
|
+
# same meaning as `r` in cut/cutcommit (1 = a single cell). Values below 1 are treated as 1.
|
|
2643
|
+
self._pradius = max(1, value)
|
|
2644
|
+
|
|
2645
|
+
@property
|
|
2646
|
+
def pshape(self):
|
|
2647
|
+
return self._pshape
|
|
2648
|
+
@pshape.setter
|
|
2649
|
+
def pshape(self, value):
|
|
2650
|
+
if value not in _SHAPES:
|
|
2651
|
+
raise ValueError('shape must be one of: ' + ', '.join(_SHAPES))
|
|
2652
|
+
self._pshape = value
|
|
2653
|
+
|
|
2654
|
+
def footprint(self, r=None, shape=None):
|
|
2655
|
+
return _footprint(self._pradius if r is None else r, self.scene.aspect, self._pshape if shape is None else shape)
|
|
2656
|
+
|
|
2657
|
+
@property
|
|
2658
|
+
def char(self):
|
|
2659
|
+
return self._char
|
|
2660
|
+
@char.setter
|
|
2661
|
+
def char(self, value):
|
|
2662
|
+
self._char = value
|
|
2663
|
+
self._pchar = value
|
|
2664
|
+
|
|
2665
|
+
def down(self, x=None, y=None, char=None, **kwargs):
|
|
2666
|
+
if self.active:
|
|
2667
|
+
return self.current_obj
|
|
2668
|
+
self.active = True
|
|
2669
|
+
self._pixels = {}
|
|
2670
|
+
if x is not None: self._x = x
|
|
2671
|
+
if y is not None: self._y = y
|
|
2672
|
+
self._ox = round(self._x)
|
|
2673
|
+
self._oy = round(self._y)
|
|
2674
|
+
self._px = self._ox
|
|
2675
|
+
self._py = self._oy
|
|
2676
|
+
self._pchar = char or self.char
|
|
2677
|
+
self._pixels = {}
|
|
2678
|
+
self._brush(0, 0)
|
|
2679
|
+
self._path = [(0, 0)]
|
|
2680
|
+
self._hits = {(0, 0): 1}
|
|
2681
|
+
for key, value in (('text', self._pchar), ('x', self._ox), ('y', self._oy)):
|
|
2682
|
+
kwargs.setdefault(key, value)
|
|
2683
|
+
self.current_obj = self.scene.obj().create(**kwargs)
|
|
2684
|
+
if self._pradius > 1:
|
|
2685
|
+
self._rebuild()
|
|
2686
|
+
return self.current_obj
|
|
2687
|
+
|
|
2688
|
+
def trail(self):
|
|
2689
|
+
return [(self._ox + x, self._oy + y) for (x, y) in self._pixels]
|
|
2690
|
+
|
|
2691
|
+
def commit(self, **kwargs):
|
|
2692
|
+
if self.current_obj is None or self.current_obj not in self.scene.objs:
|
|
2693
|
+
return None
|
|
2694
|
+
return self.current_obj.create(**kwargs)
|
|
2695
|
+
|
|
2696
|
+
def up(self, **kwargs):
|
|
2697
|
+
finished = self.commit(**kwargs)
|
|
2698
|
+
self.active = False
|
|
2699
|
+
self.current_obj = None
|
|
2700
|
+
self._pixels = {}
|
|
2701
|
+
return finished
|
|
2702
|
+
|
|
2703
|
+
def drop(self):
|
|
2704
|
+
if self.current_obj is not None and self.current_obj in self.scene.objs:
|
|
2705
|
+
self.current_obj.discard()
|
|
2706
|
+
self.active = False
|
|
2707
|
+
self.current_obj = None
|
|
2708
|
+
self._pixels = {}
|
|
2709
|
+
|
|
2710
|
+
def move(self, x, y, rebuild=True):
|
|
2711
|
+
self._x = x
|
|
2712
|
+
self._y = y
|
|
2713
|
+
if self.active:
|
|
2714
|
+
self._plot(rebuild)
|
|
2715
|
+
|
|
2716
|
+
def _plot(self, rebuild=True):
|
|
2717
|
+
nx = round(self._x)
|
|
2718
|
+
ny = round(self._y)
|
|
2719
|
+
for (ix, iy) in self._line(self._px, self._py, nx, ny):
|
|
2720
|
+
self._stamp(ix - self._ox, iy - self._oy)
|
|
2721
|
+
self._px = nx
|
|
2722
|
+
self._py = ny
|
|
2723
|
+
if rebuild:
|
|
2724
|
+
self._rebuild()
|
|
2725
|
+
|
|
2726
|
+
def _stamp(self, x, y):
|
|
2727
|
+
path = self._path
|
|
2728
|
+
if path and path[-1] == (x, y):
|
|
2729
|
+
return
|
|
2730
|
+
path.append((x, y))
|
|
2731
|
+
self._brush(x, y)
|
|
2732
|
+
self._hits[(x, y)] = self._hits.get((x, y), 0) + 1
|
|
2733
|
+
if len(path) < 3 or self._pradius > 1:
|
|
2734
|
+
return
|
|
2735
|
+
ax, ay = path[-3]
|
|
2736
|
+
bx, by = path[-2]
|
|
2737
|
+
if abs(x - ax) == 1 and abs(y - ay) == 1 and (bx, by) in ((ax, y), (x, ay)) and self._hits[(bx, by)] == 1:
|
|
2738
|
+
del self._pixels[(bx, by)]
|
|
2739
|
+
del self._hits[(bx, by)]
|
|
2740
|
+
del path[-2]
|
|
2741
|
+
|
|
2742
|
+
def _brush(self, x, y):
|
|
2743
|
+
for dx, dy in self.footprint():
|
|
2744
|
+
self._pixels[(x + dx, y + dy)] = self._pchar
|
|
2745
|
+
|
|
2746
|
+
def _line(self, x0, y0, x1, y1):
|
|
2747
|
+
dx = abs(x1 - x0)
|
|
2748
|
+
dy = -abs(y1 - y0)
|
|
2749
|
+
sx = 1 if x0 < x1 else -1
|
|
2750
|
+
sy = 1 if y0 < y1 else -1
|
|
2751
|
+
err = dx + dy
|
|
2752
|
+
points = []
|
|
2753
|
+
while True:
|
|
2754
|
+
points.append((x0, y0))
|
|
2755
|
+
if x0 == x1 and y0 == y1:
|
|
2756
|
+
return points
|
|
2757
|
+
e2 = 2 * err
|
|
2758
|
+
if e2 >= dy:
|
|
2759
|
+
err += dy
|
|
2760
|
+
x0 += sx
|
|
2761
|
+
if e2 <= dx:
|
|
2762
|
+
err += dx
|
|
2763
|
+
y0 += sy
|
|
2764
|
+
|
|
2765
|
+
def _rebuild(self):
|
|
2766
|
+
if not self._pixels or self.current_obj is None or self.current_obj not in self.scene.objs:
|
|
2767
|
+
return
|
|
2768
|
+
|
|
2769
|
+
xs = [p[0] for p in self._pixels]
|
|
2770
|
+
ys = [p[1] for p in self._pixels]
|
|
2771
|
+
min_x, max_x = min(xs), max(xs)
|
|
2772
|
+
min_y, max_y = min(ys), max(ys)
|
|
2773
|
+
|
|
2774
|
+
rows = []
|
|
2775
|
+
for row_y in range(max_y, min_y - 1, -1):
|
|
2776
|
+
row = ''
|
|
2777
|
+
for col_x in range(min_x, max_x + 1):
|
|
2778
|
+
row += self._pixels.get((col_x, row_y), ' ')
|
|
2779
|
+
rows.append(row)
|
|
2780
|
+
|
|
2781
|
+
t = self.current_obj.transform
|
|
2782
|
+
t.x = self._ox + min_x
|
|
2783
|
+
t.y = self._oy + max_y
|
|
2784
|
+
t.text = '\n'.join(rows)
|
|
2785
|
+
|
|
2786
|
+
def cut(self, wx, wy, r=0, when=True, shape=None):
|
|
2787
|
+
shape = self._pshape if shape is None else shape
|
|
2788
|
+
end = (round(wx), round(wy))
|
|
2789
|
+
start = self._pcut if self._pcut is not None else end
|
|
2790
|
+
self._pcut = end
|
|
2791
|
+
cut = False
|
|
2792
|
+
for o in list(self.scene.objs):
|
|
2793
|
+
if o is self.obj or not hasattr(o, 'transform') or not (when(o) if callable(when) else when):
|
|
2794
|
+
continue
|
|
2795
|
+
t = o.transform
|
|
2796
|
+
if r == 0:
|
|
2797
|
+
walls = t.cutline(start, end)
|
|
2798
|
+
if walls:
|
|
2799
|
+
entry = self._cut_walls.setdefault(id(o), (o, set()))
|
|
2800
|
+
if entry[1] is not None:
|
|
2801
|
+
entry[1].update(walls)
|
|
2802
|
+
cut = True
|
|
2803
|
+
else:
|
|
2804
|
+
for ix, iy in self._line(*start, *end):
|
|
2805
|
+
if t.cut(ix, iy, r=r, shape=shape):
|
|
2806
|
+
self._cut_walls.setdefault(id(o), (o, None))
|
|
2807
|
+
cut = True
|
|
2808
|
+
return cut
|
|
2809
|
+
|
|
2810
|
+
def cutcommit(self, r=0, callback=None, diagonal=True, keep=False, **kwargs):
|
|
2811
|
+
cut_objs = []
|
|
2812
|
+
for o, wall_pixels in list(self._cut_walls.values()):
|
|
2813
|
+
if o not in self.scene.objs:
|
|
2814
|
+
continue
|
|
2815
|
+
o.create(**kwargs)
|
|
2816
|
+
if r == 0 and wall_pixels:
|
|
2817
|
+
o.transform.split_at(wall_pixels, callback=callback, diagonal=diagonal)
|
|
2818
|
+
else:
|
|
2819
|
+
o.transform.split(callback=callback, diagonal=diagonal)
|
|
2820
|
+
cut_objs.append(o)
|
|
2821
|
+
self._cut_walls = {}
|
|
2822
|
+
if not keep:
|
|
2823
|
+
self._pcut = None
|
|
2824
|
+
return cut_objs
|
|
2825
|
+
|
|
2826
|
+
def cancel(self):
|
|
2827
|
+
self._pcut = None
|
|
2828
|
+
self._cut_walls = {}
|
|
2829
|
+
|
|
2830
|
+
_CONDITIONS = {
|
|
2831
|
+
'physical': lambda o, p: p is not None,
|
|
2832
|
+
'solid': lambda o, p: p is not None and bool(p.solid) and not p.phantom,
|
|
2833
|
+
'phantom': lambda o, p: p is not None and bool(p.phantom),
|
|
2834
|
+
'dynamic': lambda o, p: p is not None and bool(p.dynamic),
|
|
2835
|
+
'static': lambda o, p: p is not None and not p.dynamic,
|
|
2836
|
+
}
|
|
2837
|
+
|
|
2838
|
+
def _condition(name):
|
|
2839
|
+
negate = name.startswith('!')
|
|
2840
|
+
check = _CONDITIONS.get(name[1:] if negate else name)
|
|
2841
|
+
if check is None:
|
|
2842
|
+
raise ValueError('unknown affect condition: ' + name)
|
|
2843
|
+
return lambda o: check(o, getattr(o, 'physics', None)) != negate
|
|
2844
|
+
|
|
2845
|
+
def _matches(rule, o):
|
|
2846
|
+
if rule is None:
|
|
2847
|
+
return True
|
|
2848
|
+
if isinstance(rule, str):
|
|
2849
|
+
return _condition(rule)(o)
|
|
2850
|
+
if hasattr(rule, 'transform'):
|
|
2851
|
+
return rule is o
|
|
2852
|
+
if callable(rule):
|
|
2853
|
+
return bool(rule(o))
|
|
2854
|
+
return any(_matches(r, o) for r in rule)
|
|
2855
|
+
|
|
2856
|
+
def _compile(rule):
|
|
2857
|
+
if rule is None:
|
|
2858
|
+
return None
|
|
2859
|
+
ids = set()
|
|
2860
|
+
checks = []
|
|
2861
|
+
def walk(r):
|
|
2862
|
+
if isinstance(r, str):
|
|
2863
|
+
checks.append(_condition(r))
|
|
2864
|
+
elif hasattr(r, 'transform'):
|
|
2865
|
+
ids.add(id(r))
|
|
2866
|
+
elif callable(r):
|
|
2867
|
+
checks.append(lambda o, r=r: bool(r(o)))
|
|
2868
|
+
else:
|
|
2869
|
+
for item in r:
|
|
2870
|
+
walk(item)
|
|
2871
|
+
walk(rule)
|
|
2872
|
+
return lambda o: id(o) in ids or any(c(o) for c in checks)
|
|
2873
|
+
|
|
2874
|
+
class force(_lerpable):
|
|
2875
|
+
_owned = ['affect', 'cap', 'force', 'position', 'radius', 'torque', 'shield']
|
|
2876
|
+
_required = [transform]
|
|
2877
|
+
_settings = ('affect', 'cap', 'strength', 'position', 'radius', 'torque', 'shield')
|
|
2878
|
+
_aliases = {'strength': 'force'}
|
|
2879
|
+
|
|
2880
|
+
def __init__(self, obj, **kwargs):
|
|
2881
|
+
self.obj = obj
|
|
2882
|
+
self.t = obj.transform
|
|
2883
|
+
self.scene = obj.scene
|
|
2884
|
+
self.affect = kwargs.get('affect', None)
|
|
2885
|
+
self.cap = kwargs.get('cap', None)
|
|
2886
|
+
self.force = kwargs.get('force', 0)
|
|
2887
|
+
self.position = kwargs.get('position', None)
|
|
2888
|
+
self.radius = kwargs.get('radius', None)
|
|
2889
|
+
self.torque = kwargs.get('torque', True)
|
|
2890
|
+
self.shield = kwargs.get('shield', True)
|
|
2891
|
+
|
|
2892
|
+
@property
|
|
2893
|
+
def strength(self):
|
|
2894
|
+
return self.force
|
|
2895
|
+
@strength.setter
|
|
2896
|
+
def strength(self, value):
|
|
2897
|
+
self.force = value
|
|
2898
|
+
|
|
2899
|
+
@staticmethod
|
|
2900
|
+
def _resolve(value, o):
|
|
2901
|
+
if callable(value):
|
|
2902
|
+
return value(o) if _argslen(value) else value()
|
|
2903
|
+
return value
|
|
2904
|
+
|
|
2905
|
+
@staticmethod
|
|
2906
|
+
def _center(o):
|
|
2907
|
+
return o.transform.extent()[:2]
|
|
2908
|
+
|
|
2909
|
+
def targets(self):
|
|
2910
|
+
accept = _compile(self.affect)
|
|
2911
|
+
me = self.obj
|
|
2912
|
+
return [
|
|
2913
|
+
o for o in self.scene.objs
|
|
2914
|
+
if o is not me and hasattr(o, 'transform') and hasattr(o, 'physics') and o.physics.dynamic
|
|
2915
|
+
and (accept is None or accept(o))
|
|
2916
|
+
]
|
|
2917
|
+
|
|
2918
|
+
@staticmethod
|
|
2919
|
+
def _shadowed(cells, box, point, direction, reach, aspect):
|
|
2920
|
+
x, y = point
|
|
2921
|
+
step_x = direction[0] * 0.25 / aspect
|
|
2922
|
+
step_y = direction[1] * 0.25
|
|
2923
|
+
min_x, max_x, min_y, max_y = box
|
|
2924
|
+
for i in range(1, int(reach / 0.25) + 1):
|
|
2925
|
+
cell = (math.floor(x + step_x * i), math.ceil(y + step_y * i))
|
|
2926
|
+
if cell[0] < min_x or cell[0] > max_x or cell[1] < min_y or cell[1] > max_y:
|
|
2927
|
+
return False
|
|
2928
|
+
if cell in cells:
|
|
2929
|
+
return True
|
|
2930
|
+
return False
|
|
2931
|
+
|
|
2932
|
+
def apply(self, x=None, y=None):
|
|
2933
|
+
affected = []
|
|
2934
|
+
A = self.scene.aspect
|
|
2935
|
+
for o in self.targets():
|
|
2936
|
+
p = o.physics
|
|
2937
|
+
cells = p.getpixels()
|
|
2938
|
+
if not cells:
|
|
2939
|
+
continue
|
|
2940
|
+
if self.position is None:
|
|
2941
|
+
px, py = self._center(self.obj)
|
|
2942
|
+
else:
|
|
2943
|
+
px, py = self._resolve(self.position, o)
|
|
2944
|
+
if x is not None: px = x
|
|
2945
|
+
if y is not None: py = y
|
|
2946
|
+
|
|
2947
|
+
value = self._resolve(self.force, o)
|
|
2948
|
+
vector = isinstance(value, (tuple, list))
|
|
2949
|
+
if vector:
|
|
2950
|
+
fx, fy = value
|
|
2951
|
+
magnitude = math.hypot(fx * A, fy)
|
|
2952
|
+
if not magnitude:
|
|
2953
|
+
continue
|
|
2954
|
+
travel = (fx * A / magnitude, fy / magnitude)
|
|
2955
|
+
elif not value:
|
|
2956
|
+
continue
|
|
2957
|
+
aimed = self.position is not None or x is not None or y is not None or self.radius is not None
|
|
2958
|
+
uniform = vector and not aimed
|
|
2959
|
+
|
|
2960
|
+
faces = []
|
|
2961
|
+
for X, Y in cells:
|
|
2962
|
+
for nx, ny in _NEIGHBOURS4:
|
|
2963
|
+
if (X + nx, Y + ny) in cells:
|
|
2964
|
+
continue
|
|
2965
|
+
ex = X + 0.5 + 0.5 * nx
|
|
2966
|
+
ey = Y - 0.5 + 0.5 * ny
|
|
2967
|
+
dx = (ex - px) * A
|
|
2968
|
+
dy = ey - py
|
|
2969
|
+
dist = math.hypot(dx, dy)
|
|
2970
|
+
if self.radius is not None and dist > self.radius:
|
|
2971
|
+
continue
|
|
2972
|
+
if uniform or (vector and dist < 1e-9):
|
|
2973
|
+
ux, uy = travel
|
|
2974
|
+
elif dist < 1e-9:
|
|
2975
|
+
continue
|
|
2976
|
+
else:
|
|
2977
|
+
ux, uy = dx / dist, dy / dist
|
|
2978
|
+
cosine = -(nx * ux + ny * uy)
|
|
2979
|
+
if cosine <= 1e-9:
|
|
2980
|
+
continue
|
|
2981
|
+
faces.append((ex, ey, dist, ux, uy, cosine * (1.0 if nx else A)))
|
|
2982
|
+
if vector and aimed and self.radius is None and faces:
|
|
2983
|
+
nearest = min(f[2] for f in faces)
|
|
2984
|
+
faces = [f for f in faces if f[2] <= nearest + 1e-9]
|
|
2985
|
+
|
|
2986
|
+
xs = [c[0] for c in cells]
|
|
2987
|
+
ys = [c[1] for c in cells]
|
|
2988
|
+
box = (min(xs), max(xs), min(ys), max(ys))
|
|
2989
|
+
cx, cy, _ = p.body()
|
|
2990
|
+
weight_sum = jx_sum = jy_sum = torque_sum = 0.0
|
|
2991
|
+
for ex, ey, dist, ux, uy, weight in faces:
|
|
2992
|
+
if vector:
|
|
2993
|
+
jx, jy = fx * A, fy
|
|
2994
|
+
else:
|
|
2995
|
+
strength = value / (4 * math.pi * max(dist, 0.5))
|
|
2996
|
+
if self.cap is not None:
|
|
2997
|
+
strength = max(-self.cap, min(self.cap, strength))
|
|
2998
|
+
jx, jy = strength * ux, strength * uy
|
|
2999
|
+
reach = 1e9 if uniform else dist - 0.5
|
|
3000
|
+
if self.shield and self._shadowed(cells, box, (ex, ey), (-ux, -uy), reach, A):
|
|
3001
|
+
continue
|
|
3002
|
+
weight_sum += weight
|
|
3003
|
+
jx_sum += weight * jx
|
|
3004
|
+
jy_sum += weight * jy
|
|
3005
|
+
torque_sum += weight * ((ex - cx - 0.5) * A * jy - (ey - cy + 0.5) * jx)
|
|
3006
|
+
if weight_sum <= 0:
|
|
3007
|
+
continue
|
|
3008
|
+
|
|
3009
|
+
p._impulse(jx_sum / weight_sum, jy_sum / weight_sum)
|
|
3010
|
+
if self.torque:
|
|
3011
|
+
p._spin(torque_sum / weight_sum)
|
|
3012
|
+
affected.append(o)
|
|
3013
|
+
return affected
|
|
3014
|
+
|
|
3015
|
+
def _wrap(angle):
|
|
3016
|
+
return (angle + 180) % 360 - 180
|
|
3017
|
+
|
|
3018
|
+
def _sequence(value):
|
|
3019
|
+
return value if isinstance(value, (list, tuple)) else tuple(value)
|
|
3020
|
+
|
|
3021
|
+
def _band(value, keep=0.0):
|
|
3022
|
+
if value == 'keep':
|
|
3023
|
+
low = high = keep
|
|
3024
|
+
elif isinstance(value, (int, float)):
|
|
3025
|
+
low = high = value
|
|
3026
|
+
else:
|
|
3027
|
+
values = [keep if v == 'keep' else v for v in value]
|
|
3028
|
+
low, high = min(values), max(values)
|
|
3029
|
+
if low < 0:
|
|
3030
|
+
raise ValueError('radius must not be negative')
|
|
3031
|
+
return low, high
|
|
3032
|
+
|
|
3033
|
+
def _arc(value):
|
|
3034
|
+
if value is None:
|
|
3035
|
+
return None
|
|
3036
|
+
values = list(value)
|
|
3037
|
+
low, high = min(values), max(values)
|
|
3038
|
+
if high - low >= 360:
|
|
3039
|
+
return None
|
|
3040
|
+
return (low + high) / 2, (high - low) / 2
|
|
3041
|
+
|
|
3042
|
+
def _check_radius(value):
|
|
3043
|
+
if value is None:
|
|
3044
|
+
return 0
|
|
3045
|
+
if not isinstance(value, (int, float)) and value != 'keep':
|
|
3046
|
+
value = _sequence(value)
|
|
3047
|
+
_band(value)
|
|
3048
|
+
return value
|
|
3049
|
+
|
|
3050
|
+
def _check_range(value):
|
|
3051
|
+
if value is None:
|
|
3052
|
+
return None
|
|
3053
|
+
value = _sequence(value)
|
|
3054
|
+
_arc(value)
|
|
3055
|
+
return value
|
|
3056
|
+
|
|
3057
|
+
_KINDS = ('ball', 'hinge', 'fixed', 'slider')
|
|
3058
|
+
|
|
3059
|
+
def _check_kind(value):
|
|
3060
|
+
if value not in _KINDS:
|
|
3061
|
+
raise ValueError('kind must be one of: ' + ', '.join(_KINDS))
|
|
3062
|
+
return value
|
|
3063
|
+
|
|
3064
|
+
def _check_rest(value):
|
|
3065
|
+
if value is None or value == 'keep' or isinstance(value, (int, float)):
|
|
3066
|
+
return value
|
|
3067
|
+
raise ValueError("rest must be None, 'keep' or an angle")
|
|
3068
|
+
|
|
3069
|
+
def _check_slide(value):
|
|
3070
|
+
if value is None:
|
|
3071
|
+
return None
|
|
3072
|
+
if isinstance(value, (int, float)):
|
|
3073
|
+
return (0.0, value) if value >= 0 else (value, 0.0)
|
|
3074
|
+
values = list(value)
|
|
3075
|
+
return (min(values), max(values))
|
|
3076
|
+
|
|
3077
|
+
def _check_elasticity(value):
|
|
3078
|
+
if value <= 0:
|
|
3079
|
+
raise ValueError('elasticity must be positive')
|
|
3080
|
+
return value
|
|
3081
|
+
|
|
3082
|
+
def _check_durability(value):
|
|
3083
|
+
if value < 0:
|
|
3084
|
+
raise ValueError('durability must not be negative')
|
|
3085
|
+
return value
|
|
3086
|
+
|
|
3087
|
+
_PARAMS = {
|
|
3088
|
+
'xoffset': (0, None),
|
|
3089
|
+
'yoffset': (0, None),
|
|
3090
|
+
'radius': (0, _check_radius),
|
|
3091
|
+
'range': (None, _check_range),
|
|
3092
|
+
'elasticity': (1, _check_elasticity),
|
|
3093
|
+
'durability': (0, _check_durability),
|
|
3094
|
+
'physical': (True, bool),
|
|
3095
|
+
'vel': (0, None),
|
|
3096
|
+
'collide': (False, bool),
|
|
3097
|
+
'kind': ('ball', _check_kind),
|
|
3098
|
+
'rest': (None, _check_rest),
|
|
3099
|
+
'axis': (0, None),
|
|
3100
|
+
'slide': (None, _check_slide),
|
|
3101
|
+
'pinx': (0, None),
|
|
3102
|
+
'piny': (0, None),
|
|
3103
|
+
}
|
|
3104
|
+
_GEOMETRY = frozenset(('xoffset', 'yoffset', 'pinx', 'piny', 'radius', 'range', 'physical', 'kind', 'rest', 'axis', 'slide'))
|
|
3105
|
+
_POSE = frozenset(('xoffset', 'yoffset', 'pinx', 'piny', 'kind', 'axis'))
|
|
3106
|
+
_SWEEPS = 4
|
|
3107
|
+
_RELIEF = 1
|
|
3108
|
+
_SPINPIECES = 4
|
|
3109
|
+
_KEEP = object()
|
|
3110
|
+
|
|
3111
|
+
class _param():
|
|
3112
|
+
def __init__(self, name, check):
|
|
3113
|
+
self.name = name
|
|
3114
|
+
self.check = check
|
|
3115
|
+
|
|
3116
|
+
def __get__(self, inst, owner=None):
|
|
3117
|
+
if inst is None:
|
|
3118
|
+
return self
|
|
3119
|
+
return inst._value(self.name)
|
|
3120
|
+
|
|
3121
|
+
def __set__(self, inst, value):
|
|
3122
|
+
if self.check is not None:
|
|
3123
|
+
value = self.check(value)
|
|
3124
|
+
inst._store(self.name, value)
|
|
3125
|
+
|
|
3126
|
+
def __delete__(self, inst):
|
|
3127
|
+
inst._clear(self.name)
|
|
3128
|
+
|
|
3129
|
+
def _withparams(cls):
|
|
3130
|
+
for name, (_, check) in _PARAMS.items():
|
|
3131
|
+
setattr(cls, name, _param(name, check))
|
|
3132
|
+
return cls
|
|
3133
|
+
|
|
3134
|
+
def _ghost_hold(owner, partner):
|
|
3135
|
+
refs = owner.__dict__.setdefault('_ghostrefs', {})
|
|
3136
|
+
entry = refs.get(id(partner))
|
|
3137
|
+
if entry is None:
|
|
3138
|
+
added = not any(g is partner for g in owner._ghosts)
|
|
3139
|
+
if added:
|
|
3140
|
+
owner._ghosts.append(partner)
|
|
3141
|
+
refs[id(partner)] = [1, added]
|
|
3142
|
+
else:
|
|
3143
|
+
entry[0] += 1
|
|
3144
|
+
|
|
3145
|
+
def _ghost_drop(owner, partner):
|
|
3146
|
+
refs = owner.__dict__.get('_ghostrefs')
|
|
3147
|
+
entry = refs.get(id(partner)) if refs else None
|
|
3148
|
+
if entry is None:
|
|
3149
|
+
return
|
|
3150
|
+
entry[0] -= 1
|
|
3151
|
+
if entry[0] > 0:
|
|
3152
|
+
return
|
|
3153
|
+
del refs[id(partner)]
|
|
3154
|
+
if entry[1]:
|
|
3155
|
+
owner._ghosts[:] = [g for g in owner._ghosts if g is not partner]
|
|
3156
|
+
|
|
3157
|
+
class _jointlist(collections.abc.MutableSequence):
|
|
3158
|
+
def __init__(self, owner):
|
|
3159
|
+
self._owner = owner
|
|
3160
|
+
|
|
3161
|
+
def _items(self):
|
|
3162
|
+
owner = self._owner
|
|
3163
|
+
return [edge.other(owner).obj for edge in owner._edges.values()]
|
|
3164
|
+
|
|
3165
|
+
def __len__(self):
|
|
3166
|
+
return len(self._owner._edges)
|
|
3167
|
+
|
|
3168
|
+
def __iter__(self):
|
|
3169
|
+
return iter(self._items())
|
|
3170
|
+
|
|
3171
|
+
def __contains__(self, item):
|
|
3172
|
+
return id(item) in self._owner._edges
|
|
3173
|
+
|
|
3174
|
+
def __getitem__(self, index):
|
|
3175
|
+
return self._items()[index]
|
|
3176
|
+
|
|
3177
|
+
def __setitem__(self, index, value):
|
|
3178
|
+
items = self._items()
|
|
3179
|
+
items[index] = value
|
|
3180
|
+
self._owner._assign(items)
|
|
3181
|
+
|
|
3182
|
+
def __delitem__(self, index):
|
|
3183
|
+
items = self._items()
|
|
3184
|
+
del items[index]
|
|
3185
|
+
self._owner._assign(items)
|
|
3186
|
+
|
|
3187
|
+
def __eq__(self, other):
|
|
3188
|
+
if isinstance(other, (list, tuple, _jointlist)):
|
|
3189
|
+
return self._items() == list(other)
|
|
3190
|
+
return NotImplemented
|
|
3191
|
+
|
|
3192
|
+
__hash__ = None
|
|
3193
|
+
|
|
3194
|
+
def __repr__(self):
|
|
3195
|
+
return repr(self._items())
|
|
3196
|
+
|
|
3197
|
+
def __iadd__(self, items):
|
|
3198
|
+
self.extend(items)
|
|
3199
|
+
return self
|
|
3200
|
+
|
|
3201
|
+
def insert(self, index, item):
|
|
3202
|
+
items = self._items()
|
|
3203
|
+
items.insert(index, item)
|
|
3204
|
+
self._owner._assign(items)
|
|
3205
|
+
|
|
3206
|
+
def append(self, item):
|
|
3207
|
+
self._owner._assign(self._items() + [item])
|
|
3208
|
+
|
|
3209
|
+
def extend(self, items):
|
|
3210
|
+
self._owner._assign(self._items() + list(items))
|
|
3211
|
+
|
|
3212
|
+
def remove(self, item):
|
|
3213
|
+
self._owner.remove(item)
|
|
3214
|
+
|
|
3215
|
+
def pop(self, index=-1):
|
|
3216
|
+
item = self._items()[index]
|
|
3217
|
+
self._owner.remove(item)
|
|
3218
|
+
return item
|
|
3219
|
+
|
|
3220
|
+
def clear(self):
|
|
3221
|
+
self._owner._assign(())
|
|
3222
|
+
|
|
3223
|
+
class _edge():
|
|
3224
|
+
def __init__(self, origin, follower):
|
|
3225
|
+
self.origin = origin
|
|
3226
|
+
self.follower = follower
|
|
3227
|
+
self.alive = True
|
|
3228
|
+
self.held = False
|
|
3229
|
+
self.engaged = False
|
|
3230
|
+
self.pose = None
|
|
3231
|
+
self._collide = None
|
|
3232
|
+
|
|
3233
|
+
def other(self, comp):
|
|
3234
|
+
return self.follower if comp is self.origin else self.origin
|
|
3235
|
+
|
|
3236
|
+
@property
|
|
3237
|
+
def collide(self):
|
|
3238
|
+
return self._collide
|
|
3239
|
+
|
|
3240
|
+
@collide.setter
|
|
3241
|
+
def collide(self, value):
|
|
3242
|
+
self._collide = None if value is None else bool(value)
|
|
3243
|
+
self.rewire()
|
|
3244
|
+
|
|
3245
|
+
@property
|
|
3246
|
+
def colliding(self):
|
|
3247
|
+
return self.follower.collide if self._collide is None else self._collide
|
|
3248
|
+
|
|
3249
|
+
@property
|
|
3250
|
+
def physical(self):
|
|
3251
|
+
return self.follower.physical
|
|
3252
|
+
|
|
3253
|
+
def anchor(self):
|
|
3254
|
+
return self.origin._anchor(self.follower.xoffset, self.follower.yoffset)
|
|
3255
|
+
|
|
3256
|
+
def pin(self):
|
|
3257
|
+
f = self.follower
|
|
3258
|
+
if not (f.pinx or f.piny):
|
|
3259
|
+
return f.t.extent()[:2]
|
|
3260
|
+
return f._anchor(f.pinx, f.piny)
|
|
3261
|
+
|
|
3262
|
+
@property
|
|
3263
|
+
def pinned(self):
|
|
3264
|
+
return bool(self.follower.pinx or self.follower.piny)
|
|
3265
|
+
|
|
3266
|
+
def rewire(self):
|
|
3267
|
+
want = self.alive and not self.colliding
|
|
3268
|
+
if want == self.held:
|
|
3269
|
+
return
|
|
3270
|
+
self.held = want
|
|
3271
|
+
a, b = self.origin.obj, self.follower.obj
|
|
3272
|
+
if want:
|
|
3273
|
+
_ghost_hold(a, b)
|
|
3274
|
+
_ghost_hold(b, a)
|
|
3275
|
+
else:
|
|
3276
|
+
_ghost_drop(a, b)
|
|
3277
|
+
_ghost_drop(b, a)
|
|
3278
|
+
|
|
3279
|
+
def _geometry(self):
|
|
3280
|
+
f = self.follower
|
|
3281
|
+
A = f.scene.aspect
|
|
3282
|
+
ax, ay = self.anchor()
|
|
3283
|
+
cx, cy = self.pin()
|
|
3284
|
+
dx = (cx - ax) * A
|
|
3285
|
+
dy = cy - ay
|
|
3286
|
+
return A, ax, ay, cx, cy, dx, dy, math.hypot(dx, dy)
|
|
3287
|
+
|
|
3288
|
+
def capture(self):
|
|
3289
|
+
dx, dy, dist = self._geometry()[5:]
|
|
3290
|
+
rotation = self.origin.t.rotation
|
|
3291
|
+
rel = _wrap(math.degrees(math.atan2(dx, dy)) - rotation) if dist > 1e-9 else 0.0
|
|
3292
|
+
self.pose = (dist, rel, _wrap(self.follower.t.rotation - rotation))
|
|
3293
|
+
return self.pose
|
|
3294
|
+
|
|
3295
|
+
def error(self, motor):
|
|
3296
|
+
f = self.follower
|
|
3297
|
+
pose = self.pose or self.capture()
|
|
3298
|
+
A, ax, ay, cx, cy, dx, dy, dist = self._geometry()
|
|
3299
|
+
rotation = self.origin.t.rotation
|
|
3300
|
+
kind = f.kind
|
|
3301
|
+
if kind == 'slider':
|
|
3302
|
+
heading = math.radians(rotation + f.axis)
|
|
3303
|
+
ux, uy = math.sin(heading), math.cos(heading)
|
|
3304
|
+
target = dx * ux + dy * uy + (f.vel if motor else 0)
|
|
3305
|
+
slide = f.slide
|
|
3306
|
+
if slide is not None:
|
|
3307
|
+
target = min(max(target, slide[0]), slide[1])
|
|
3308
|
+
ex = ax + target * ux / A - cx
|
|
3309
|
+
ey = ay + target * uy - cy
|
|
3310
|
+
miss = math.hypot(ex * A, ey)
|
|
3311
|
+
return (0.0, 0.0, 0.0) if miss < 1e-9 else (ex, ey, miss)
|
|
3312
|
+
if kind == 'fixed':
|
|
3313
|
+
heading = math.radians(rotation + pose[1])
|
|
3314
|
+
ex = ax + pose[0] * math.sin(heading) / A - cx
|
|
3315
|
+
ey = ay + pose[0] * math.cos(heading) - cy
|
|
3316
|
+
miss = math.hypot(ex * A, ey)
|
|
3317
|
+
return (0.0, 0.0, 0.0) if miss < 1e-9 else (ex, ey, miss)
|
|
3318
|
+
low, high = _band(f.radius, pose[0])
|
|
3319
|
+
arc = _arc(f.range)
|
|
3320
|
+
base = 0.0 if f.rest is None else pose[1] if f.rest == 'keep' else f.rest
|
|
3321
|
+
mid = arc[0] + base if arc else base
|
|
3322
|
+
if dist < 1e-9:
|
|
3323
|
+
rel = mid
|
|
3324
|
+
else:
|
|
3325
|
+
rel = _wrap(math.degrees(math.atan2(dx, dy)) - rotation)
|
|
3326
|
+
target_rel = rel + (f.vel if motor else 0)
|
|
3327
|
+
if arc is not None:
|
|
3328
|
+
off = _wrap(target_rel - mid)
|
|
3329
|
+
if abs(off) > arc[1]:
|
|
3330
|
+
target_rel = mid + math.copysign(arc[1], off)
|
|
3331
|
+
target_dist = min(max(dist, low), high)
|
|
3332
|
+
if abs(target_dist - dist) < 1e-9 and abs(_wrap(target_rel - rel)) < 1e-9:
|
|
3333
|
+
return 0.0, 0.0, dist
|
|
3334
|
+
heading = math.radians(rotation + target_rel)
|
|
3335
|
+
ex = ax + target_dist * math.sin(heading) / A - cx
|
|
3336
|
+
ey = ay + target_dist * math.cos(heading) - cy
|
|
3337
|
+
if math.hypot(ex * A, ey) < 1e-9:
|
|
3338
|
+
return 0.0, 0.0, dist
|
|
3339
|
+
return ex, ey, dist
|
|
3340
|
+
|
|
3341
|
+
def beyond(self):
|
|
3342
|
+
f = self.follower
|
|
3343
|
+
pose = self.pose or self.capture()
|
|
3344
|
+
A, ax, ay, cx, cy, dx, dy, dist = self._geometry()
|
|
3345
|
+
rotation = self.origin.t.rotation
|
|
3346
|
+
if f.kind == 'slider':
|
|
3347
|
+
if f.slide is None:
|
|
3348
|
+
return False
|
|
3349
|
+
heading = math.radians(rotation + f.axis)
|
|
3350
|
+
s = dx * math.sin(heading) + dy * math.cos(heading)
|
|
3351
|
+
return s < f.slide[0] - 1e-6 or s > f.slide[1] + 1e-6
|
|
3352
|
+
arc = _arc(f.range)
|
|
3353
|
+
if arc is None or f.kind == 'fixed' or dist < 1e-9:
|
|
3354
|
+
return False
|
|
3355
|
+
base = 0.0 if f.rest is None else pose[1] if f.rest == 'keep' else f.rest
|
|
3356
|
+
rel = _wrap(math.degrees(math.atan2(dx, dy)) - rotation)
|
|
3357
|
+
return abs(_wrap(rel - arc[0] - base)) > arc[1] + 1e-6
|
|
3358
|
+
|
|
3359
|
+
def spin(self):
|
|
3360
|
+
kind = self.follower.kind
|
|
3361
|
+
if kind == 'ball':
|
|
3362
|
+
return None
|
|
3363
|
+
pose = self.pose or self.capture()
|
|
3364
|
+
rotation = self.origin.t.rotation
|
|
3365
|
+
if kind == 'hinge':
|
|
3366
|
+
dx, dy, dist = self._geometry()[5:]
|
|
3367
|
+
rel = _wrap(math.degrees(math.atan2(dx, dy)) - rotation) if dist > 1e-9 else pose[1]
|
|
3368
|
+
return rotation + pose[2] + _wrap(rel - pose[1])
|
|
3369
|
+
return rotation + pose[2]
|
|
3370
|
+
|
|
3371
|
+
def pull(self, ex, ey):
|
|
3372
|
+
o, f = self.origin, self.follower
|
|
3373
|
+
if o.movable and f.movable:
|
|
3374
|
+
total = o.mass + f.mass
|
|
3375
|
+
wc, wp = o.mass / total, f.mass / total
|
|
3376
|
+
elif f.movable:
|
|
3377
|
+
wc, wp = 1.0, 0.0
|
|
3378
|
+
elif o.movable:
|
|
3379
|
+
wc, wp = 0.0, 1.0
|
|
3380
|
+
else:
|
|
3381
|
+
return
|
|
3382
|
+
A = f.scene.aspect
|
|
3383
|
+
k = min(1.0, 1.0 / f.elasticity)
|
|
3384
|
+
damp = 1.0 - (1.0 - math.sqrt(k)) ** 2
|
|
3385
|
+
pinned = self.pinned and f.kind == 'ball'
|
|
3386
|
+
fkick = pinned
|
|
3387
|
+
okick = pinned or bool(f.xoffset or f.yoffset)
|
|
3388
|
+
fpoint = self.pin()
|
|
3389
|
+
opoint = self.anchor()
|
|
3390
|
+
fvx, fvy = f._pointvel(fpoint) if fkick else f.velocity
|
|
3391
|
+
ovx, ovy = o._pointvel(opoint) if okick else o.velocity
|
|
3392
|
+
rx, ry = fvx - ovx, fvy - ovy
|
|
3393
|
+
fx = fy = 0.0
|
|
3394
|
+
ux, uy = ex * A, ey
|
|
3395
|
+
n = math.hypot(ux, uy)
|
|
3396
|
+
full = f.kind in ('fixed', 'slider') or n <= 1e-9
|
|
3397
|
+
if not full:
|
|
3398
|
+
low, high = _band(f.radius, (self.pose or self.capture())[0])
|
|
3399
|
+
full = high <= 1e-9
|
|
3400
|
+
if full:
|
|
3401
|
+
fx = -rx * damp
|
|
3402
|
+
fy = -ry * damp
|
|
3403
|
+
else:
|
|
3404
|
+
along = (rx * A * ux + ry * uy) / n
|
|
3405
|
+
if along < 0 or f.kind != 'ball' or high - low <= 1e-9:
|
|
3406
|
+
fx = -along * ux / n / A * damp
|
|
3407
|
+
fy = -along * uy / n * damp
|
|
3408
|
+
for comp, point, w, kick in ((f, fpoint, wc, fkick), (o, opoint, -wp, okick)):
|
|
3409
|
+
p = getattr(comp.obj, 'physics', None)
|
|
3410
|
+
if p is not None and p.dynamic:
|
|
3411
|
+
if w:
|
|
3412
|
+
gx = (ex * k + fx) * w
|
|
3413
|
+
gy = (ey * k + fy) * w
|
|
3414
|
+
if kick:
|
|
3415
|
+
comp._kick(point, gx, gy)
|
|
3416
|
+
else:
|
|
3417
|
+
p.xvel += gx * p.mass
|
|
3418
|
+
p.yvel += gy * p.mass
|
|
3419
|
+
else:
|
|
3420
|
+
comp._push(ex * w * k, ey * w * k)
|
|
3421
|
+
|
|
3422
|
+
@_withparams
|
|
3423
|
+
class joint(_lerpable):
|
|
3424
|
+
_angles = frozenset(('rest', 'axis'))
|
|
3425
|
+
_owned = ['joints', 'xoffset', 'yoffset', 'pinx', 'piny', 'jradius', 'range', 'elasticity',
|
|
3426
|
+
'durability', 'physical', 'vel', 'collide', 'kind', 'rest', 'axis', 'slide', 'preset']
|
|
3427
|
+
_required = [transform]
|
|
3428
|
+
_settings = ('preset', 'joints', 'xoffset', 'yoffset', 'pinx', 'piny', 'radius', 'range', 'elasticity',
|
|
3429
|
+
'durability', 'physical', 'vel', 'collide', 'kind', 'rest', 'axis', 'slide')
|
|
3430
|
+
_aliases = {'radius': 'jradius'}
|
|
3431
|
+
_watch = frozenset(('x', 'y', 'rotation', 'text'))
|
|
3432
|
+
_active = False
|
|
3433
|
+
presets = {
|
|
3434
|
+
'ball': {'kind': 'ball'},
|
|
3435
|
+
'rod': {'kind': 'ball', 'radius': 'keep'},
|
|
3436
|
+
'rope': {'kind': 'ball', 'radius': (0, 'keep'), 'physical': False},
|
|
3437
|
+
'spring': {'kind': 'ball', 'radius': 'keep', 'elasticity': 8},
|
|
3438
|
+
'hinge': {'kind': 'hinge', 'radius': 'keep'},
|
|
3439
|
+
'fixed': {'kind': 'fixed'},
|
|
3440
|
+
'slider': {'kind': 'slider'},
|
|
3441
|
+
'neck': {'kind': 'hinge', 'radius': 'keep', 'rest': 'keep', 'range': (-45, 45)},
|
|
3442
|
+
'spine': {'kind': 'hinge', 'radius': 'keep', 'rest': 'keep', 'range': (-25, 25)},
|
|
3443
|
+
'shoulder': {'kind': 'hinge', 'radius': 'keep', 'rest': 'keep', 'range': (-170, 170)},
|
|
3444
|
+
'elbow': {'kind': 'hinge', 'radius': 'keep', 'rest': 'keep', 'range': (-150, 0)},
|
|
3445
|
+
'wrist': {'kind': 'hinge', 'radius': 'keep', 'rest': 'keep', 'range': (-70, 70)},
|
|
3446
|
+
'hip': {'kind': 'hinge', 'radius': 'keep', 'rest': 'keep', 'range': (-100, 100)},
|
|
3447
|
+
'knee': {'kind': 'hinge', 'radius': 'keep', 'rest': 'keep', 'range': (0, 140)},
|
|
3448
|
+
'ankle': {'kind': 'hinge', 'radius': 'keep', 'rest': 'keep', 'range': (-40, 40)},
|
|
3449
|
+
}
|
|
3450
|
+
|
|
3451
|
+
def __init__(self, obj, **kwargs):
|
|
3452
|
+
self.obj = obj
|
|
3453
|
+
self.t = obj.transform
|
|
3454
|
+
self.scene = obj.scene
|
|
3455
|
+
self._params = {}
|
|
3456
|
+
self._edges = {}
|
|
3457
|
+
self._onbreak = []
|
|
3458
|
+
self._preset = None
|
|
3459
|
+
self._stamp = None
|
|
3460
|
+
self._spun = None
|
|
3461
|
+
self._joints = _jointlist(self)
|
|
3462
|
+
if kwargs.get('preset') is not None:
|
|
3463
|
+
self.apply(kwargs['preset'])
|
|
3464
|
+
for name in _PARAMS:
|
|
3465
|
+
key = 'jradius' if name == 'radius' else name
|
|
3466
|
+
if key in kwargs:
|
|
3467
|
+
setattr(self, name, kwargs[key])
|
|
3468
|
+
def change(attr):
|
|
3469
|
+
self._on_change(attr)
|
|
3470
|
+
self._pcb = change
|
|
3471
|
+
self.t.onchange(change)
|
|
3472
|
+
def sibling(other):
|
|
3473
|
+
theirs = getattr(other, 'joint', None)
|
|
3474
|
+
if theirs is None or theirs is self:
|
|
3475
|
+
return False
|
|
3476
|
+
for mine in self._edges.values():
|
|
3477
|
+
if mine.follower is not self or not mine.alive or mine.colliding:
|
|
3478
|
+
continue
|
|
3479
|
+
shared = theirs._edges.get(id(mine.origin.obj))
|
|
3480
|
+
if shared is not None and shared.follower is theirs and shared.alive and not shared.colliding:
|
|
3481
|
+
return True
|
|
3482
|
+
return False
|
|
3483
|
+
self._ghostcb = sibling
|
|
3484
|
+
obj._ghosts.append(sibling)
|
|
3485
|
+
self._assign(kwargs.get('joints', ()))
|
|
3486
|
+
|
|
3487
|
+
def __getitem__(self, key):
|
|
3488
|
+
return getattr(self, key)
|
|
3489
|
+
|
|
3490
|
+
def __setitem__(self, key, value):
|
|
3491
|
+
setattr(self, key, value)
|
|
3492
|
+
|
|
3493
|
+
def _value(self, name):
|
|
3494
|
+
if name in self._params:
|
|
3495
|
+
return self._params[name]
|
|
3496
|
+
return _PARAMS[name][0]
|
|
3497
|
+
|
|
3498
|
+
def _store(self, name, value):
|
|
3499
|
+
self._params[name] = value
|
|
3500
|
+
self._sync({name})
|
|
3501
|
+
|
|
3502
|
+
def _clear(self, name):
|
|
3503
|
+
if name in self._params:
|
|
3504
|
+
del self._params[name]
|
|
3505
|
+
self._sync({name})
|
|
3506
|
+
|
|
3507
|
+
def _sync(self, names):
|
|
3508
|
+
edges = [e for e in self._edges.values() if e.follower is self]
|
|
3509
|
+
if 'collide' in names:
|
|
3510
|
+
for edge in edges:
|
|
3511
|
+
edge.rewire()
|
|
3512
|
+
if _POSE & names:
|
|
3513
|
+
for edge in edges:
|
|
3514
|
+
edge.capture()
|
|
3515
|
+
if _GEOMETRY & names:
|
|
3516
|
+
for edge in edges:
|
|
3517
|
+
self._realign(edge)
|
|
3518
|
+
|
|
3519
|
+
@staticmethod
|
|
3520
|
+
def mirrored(spec):
|
|
3521
|
+
if isinstance(spec, str):
|
|
3522
|
+
spec = joint.presets[spec]
|
|
3523
|
+
spec = dict(spec)
|
|
3524
|
+
if spec.get('range') is not None:
|
|
3525
|
+
spec['range'] = tuple(-v for v in spec['range'])
|
|
3526
|
+
if spec.get('rest') not in (None, 'keep'):
|
|
3527
|
+
spec['rest'] = -spec['rest']
|
|
3528
|
+
if spec.get('axis'):
|
|
3529
|
+
spec['axis'] = -spec['axis']
|
|
3530
|
+
if spec.get('xoffset'):
|
|
3531
|
+
spec['xoffset'] = -spec['xoffset']
|
|
3532
|
+
return spec
|
|
3533
|
+
|
|
3534
|
+
@property
|
|
3535
|
+
def preset(self):
|
|
3536
|
+
return self._preset
|
|
3537
|
+
|
|
3538
|
+
@preset.setter
|
|
3539
|
+
def preset(self, value):
|
|
3540
|
+
self.apply(value)
|
|
3541
|
+
|
|
3542
|
+
def apply(self, preset=None, **settings):
|
|
3543
|
+
spec = {}
|
|
3544
|
+
if preset is not None:
|
|
3545
|
+
if isinstance(preset, str):
|
|
3546
|
+
if preset not in joint.presets:
|
|
3547
|
+
raise KeyError('unknown joint preset: ' + preset)
|
|
3548
|
+
spec.update(joint.presets[preset])
|
|
3549
|
+
self._preset = preset
|
|
3550
|
+
else:
|
|
3551
|
+
spec.update(preset)
|
|
3552
|
+
self._preset = None
|
|
3553
|
+
spec.update(settings)
|
|
3554
|
+
for key in spec:
|
|
3555
|
+
if key not in _PARAMS and key != 'jradius':
|
|
3556
|
+
raise KeyError('unknown joint setting: ' + str(key))
|
|
3557
|
+
for key, value in spec.items():
|
|
3558
|
+
setattr(self, 'radius' if key == 'jradius' else key, value)
|
|
3559
|
+
return self
|
|
3560
|
+
|
|
3561
|
+
def attach(self, item, preset=None, link_collide=_KEEP, at=None, **settings):
|
|
3562
|
+
if getattr(item, 'transform', None) is None:
|
|
3563
|
+
try:
|
|
3564
|
+
items = list(item)
|
|
3565
|
+
except TypeError:
|
|
3566
|
+
raise TypeError('a jointed object needs a transform component') from None
|
|
3567
|
+
for each in items:
|
|
3568
|
+
if getattr(each, 'transform', None) is None:
|
|
3569
|
+
raise TypeError('a jointed object needs a transform component')
|
|
3570
|
+
if each is self.obj:
|
|
3571
|
+
raise ValueError('an object cannot be jointed to itself')
|
|
3572
|
+
return [self.attach(each, preset, link_collide, at, **settings) for each in items]
|
|
3573
|
+
if getattr(item, 'joint', None) is None:
|
|
3574
|
+
item.set(joint)
|
|
3575
|
+
item.joint.apply(preset, **settings)
|
|
3576
|
+
if at is not None:
|
|
3577
|
+
if id(item) not in self._edges:
|
|
3578
|
+
self._connect(item)
|
|
3579
|
+
self.anchor_at(item, at)
|
|
3580
|
+
item.joint._realign(self._edges[id(item)])
|
|
3581
|
+
elif id(item) not in self._edges:
|
|
3582
|
+
self._joints.append(item)
|
|
3583
|
+
if link_collide is not _KEEP:
|
|
3584
|
+
self._edges[id(item)].collide = link_collide
|
|
3585
|
+
return item.joint
|
|
3586
|
+
|
|
3587
|
+
def _local(self, point):
|
|
3588
|
+
# world point -> (xoffset, yoffset) in this object's frame (inverse of _anchor)
|
|
3589
|
+
cx, cy = self.t.extent()[:2]
|
|
3590
|
+
A = self.scene.aspect
|
|
3591
|
+
r = math.radians(self.t.rotation)
|
|
3592
|
+
dx, dy = (point[0] - cx) * A, point[1] - cy
|
|
3593
|
+
return (dx * math.cos(r) - dy * math.sin(r)) / A, dx * math.sin(r) + dy * math.cos(r)
|
|
3594
|
+
|
|
3595
|
+
def _seam(self, other, cells):
|
|
3596
|
+
edge = self._edges.get(id(other))
|
|
3597
|
+
if edge is None or not edge.alive or not edge.colliding:
|
|
3598
|
+
return False
|
|
3599
|
+
A = self.scene.aspect
|
|
3600
|
+
points = (edge.anchor(), edge.pin())
|
|
3601
|
+
frontier = [c for c in cells
|
|
3602
|
+
if any(math.hypot((c[0] + 0.5 - p[0]) * A, c[1] - 0.5 - p[1]) <= 1.5 for p in points)]
|
|
3603
|
+
if not frontier:
|
|
3604
|
+
return False
|
|
3605
|
+
seen = set(frontier)
|
|
3606
|
+
while frontier:
|
|
3607
|
+
x, y = frontier.pop()
|
|
3608
|
+
for dx in (-1, 0, 1):
|
|
3609
|
+
for dy in (-1, 0, 1):
|
|
3610
|
+
n = (x + dx, y + dy)
|
|
3611
|
+
if n in cells and n not in seen:
|
|
3612
|
+
seen.add(n)
|
|
3613
|
+
frontier.append(n)
|
|
3614
|
+
return len(seen) == len(cells)
|
|
3615
|
+
|
|
3616
|
+
def _connect(self, item):
|
|
3617
|
+
# link self -> item without moving anything (offsets are expected to be set afterwards)
|
|
3618
|
+
edge = _edge(self, item.joint)
|
|
3619
|
+
self._edges[id(item)] = edge
|
|
3620
|
+
item.joint._edges[id(self.obj)] = edge
|
|
3621
|
+
edge.rewire()
|
|
3622
|
+
return edge
|
|
3623
|
+
|
|
3624
|
+
def anchor_at(self, item, point):
|
|
3625
|
+
# join self and item at one world point: the anchor sits on self and the pin on item,
|
|
3626
|
+
# so the pair meets at that point (e.g. the edge of two sticks) instead of at item's centre
|
|
3627
|
+
edge = self._edges.get(id(item))
|
|
3628
|
+
if edge is None or edge.origin is not self:
|
|
3629
|
+
raise KeyError('object is not a follower of this joint')
|
|
3630
|
+
f = edge.follower
|
|
3631
|
+
f._params['xoffset'], f._params['yoffset'] = self._local(point)
|
|
3632
|
+
f._params['pinx'], f._params['piny'] = f._local(point)
|
|
3633
|
+
edge.capture()
|
|
3634
|
+
return edge
|
|
3635
|
+
|
|
3636
|
+
def group(self):
|
|
3637
|
+
# every object connected to this one through joints (this one included)
|
|
3638
|
+
seen = {id(self.obj): self.obj}
|
|
3639
|
+
queue = [self]
|
|
3640
|
+
for comp in queue:
|
|
3641
|
+
for edge in list(comp._edges.values()):
|
|
3642
|
+
if not edge.alive:
|
|
3643
|
+
continue
|
|
3644
|
+
other = edge.other(comp)
|
|
3645
|
+
if id(other.obj) not in seen:
|
|
3646
|
+
seen[id(other.obj)] = other.obj
|
|
3647
|
+
queue.append(other)
|
|
3648
|
+
return list(seen.values())
|
|
3649
|
+
|
|
3650
|
+
def teleport(self, dx, dy, stop=True):
|
|
3651
|
+
# move the whole connected group rigidly. Moving members one by one lets the joints see
|
|
3652
|
+
# a huge stretch (the first member is already there, the others are not) and fling them.
|
|
3653
|
+
members = self.group()
|
|
3654
|
+
joint._active = True
|
|
3655
|
+
try:
|
|
3656
|
+
for o in members:
|
|
3657
|
+
t = o.transform
|
|
3658
|
+
t.x += dx
|
|
3659
|
+
t.y += dy
|
|
3660
|
+
p = getattr(o, 'physics', None)
|
|
3661
|
+
if stop and p is not None:
|
|
3662
|
+
p.xvel = p.yvel = p.avel = 0
|
|
3663
|
+
finally:
|
|
3664
|
+
joint._active = False
|
|
3665
|
+
return members
|
|
3666
|
+
|
|
3667
|
+
@staticmethod
|
|
3668
|
+
def transfer(old, parts):
|
|
3669
|
+
# `old` is being split into `parts` (and then discarded): hand each of its joints to the
|
|
3670
|
+
# part that holds the joint point, so that part stays joined to the same partner.
|
|
3671
|
+
jt = getattr(old, 'joint', None)
|
|
3672
|
+
if jt is None or not jt._edges:
|
|
3673
|
+
return
|
|
3674
|
+
A = old.scene.aspect
|
|
3675
|
+
cells = []
|
|
3676
|
+
for part in parts:
|
|
3677
|
+
t = getattr(part, 'transform', None)
|
|
3678
|
+
cells.append(((wx + 0.5, wy - 0.5) for wx, wy in t.worldcells()) if t is not None else ())
|
|
3679
|
+
cells = [list(c) for c in cells]
|
|
3680
|
+
plan = []
|
|
3681
|
+
for edge in list(jt._edges.values()):
|
|
3682
|
+
if not edge.alive:
|
|
3683
|
+
continue
|
|
3684
|
+
as_origin = edge.origin is jt
|
|
3685
|
+
point = edge.anchor() if as_origin else edge.pin()
|
|
3686
|
+
best, bd = None, None
|
|
3687
|
+
for part, pts in zip(parts, cells):
|
|
3688
|
+
if not pts:
|
|
3689
|
+
continue
|
|
3690
|
+
d = min(((px - point[0]) * A) ** 2 + (py - point[1]) ** 2 for px, py in pts)
|
|
3691
|
+
if bd is None or d < bd - 1e-9 or (abs(d - bd) <= 1e-9 and len(pts) > len(cells[parts.index(best)])):
|
|
3692
|
+
best, bd = part, d
|
|
3693
|
+
if best is not None:
|
|
3694
|
+
plan.append((edge, as_origin, point, best))
|
|
3695
|
+
for edge, as_origin, point, part in plan:
|
|
3696
|
+
if getattr(part, 'joint', None) is None:
|
|
3697
|
+
part.set(joint)
|
|
3698
|
+
pj = part.joint
|
|
3699
|
+
if as_origin:
|
|
3700
|
+
f = edge.follower
|
|
3701
|
+
new = pj._connect(f.obj)
|
|
3702
|
+
f._params['xoffset'], f._params['yoffset'] = pj._local(point)
|
|
3703
|
+
else:
|
|
3704
|
+
o = edge.origin
|
|
3705
|
+
new = o._connect(part)
|
|
3706
|
+
pj._params.update(jt._params)
|
|
3707
|
+
pj._preset = jt._preset
|
|
3708
|
+
pj._onbreak = list(jt._onbreak)
|
|
3709
|
+
pj._params['pinx'], pj._params['piny'] = pj._local(point)
|
|
3710
|
+
new._collide = edge._collide
|
|
3711
|
+
new.rewire()
|
|
3712
|
+
new.capture()
|
|
3713
|
+
|
|
3714
|
+
def link(self, item):
|
|
3715
|
+
edge = self._edges.get(id(item))
|
|
3716
|
+
if edge is None:
|
|
3717
|
+
raise KeyError('object is not jointed')
|
|
3718
|
+
return edge
|
|
3719
|
+
|
|
3720
|
+
def repose(self, item=None):
|
|
3721
|
+
for edge in list(self._edges.values()):
|
|
3722
|
+
if item is None or edge.other(self).obj is item:
|
|
3723
|
+
edge.capture()
|
|
3724
|
+
return self
|
|
3725
|
+
|
|
3726
|
+
@staticmethod
|
|
3727
|
+
def rope(scene, start=None, end=None, segments=10, char='o', anchor=None, tail=None, pin=None,
|
|
3728
|
+
preset='rope', selfcollide=False, anchoroffset=(0, 0), tailoffset=(0, 0), **kwargs):
|
|
3729
|
+
if segments < 1:
|
|
3730
|
+
raise ValueError('a rope needs at least one segment')
|
|
3731
|
+
def endpoint(item, offset):
|
|
3732
|
+
if getattr(item, 'joint', None) is None:
|
|
3733
|
+
item.set(joint)
|
|
3734
|
+
x, y = item.joint._anchor(*offset)
|
|
3735
|
+
return x - 0.5, y + 0.5
|
|
3736
|
+
if start is None:
|
|
3737
|
+
if anchor is None:
|
|
3738
|
+
raise ValueError('a rope needs a start point or an anchor')
|
|
3739
|
+
start = endpoint(anchor, anchoroffset)
|
|
3740
|
+
if end is None:
|
|
3741
|
+
if tail is None:
|
|
3742
|
+
raise ValueError('a rope needs an end point or a tail')
|
|
3743
|
+
end = endpoint(tail, tailoffset)
|
|
3744
|
+
chars = char if callable(char) else (lambda i: char)
|
|
3745
|
+
pin = anchor is None if pin is None else pin
|
|
3746
|
+
parts = []
|
|
3747
|
+
last = max(1, segments - 1)
|
|
3748
|
+
for i in range(segments):
|
|
3749
|
+
k = i / last if segments > 1 else 0
|
|
3750
|
+
x = start[0] + (end[0] - start[0]) * k
|
|
3751
|
+
y = start[1] + (end[1] - start[1]) * k
|
|
3752
|
+
seg = dict(kwargs)
|
|
3753
|
+
seg['dynamic'] = not (i == 0 and pin) and seg.get('dynamic', True)
|
|
3754
|
+
parts.append(scene.obj().create(chars(i), x, y, **seg))
|
|
3755
|
+
links = list(zip(parts, parts[1:]))
|
|
3756
|
+
if anchor is not None:
|
|
3757
|
+
links.insert(0, (anchor, parts[0]))
|
|
3758
|
+
if tail is not None:
|
|
3759
|
+
links.append((parts[-1], tail))
|
|
3760
|
+
for parent, child in links:
|
|
3761
|
+
if getattr(parent, 'joint', None) is None:
|
|
3762
|
+
parent.set(joint)
|
|
3763
|
+
ends = {}
|
|
3764
|
+
if parent is anchor and child is parts[0] and any(anchoroffset):
|
|
3765
|
+
ends = {'xoffset': anchoroffset[0], 'yoffset': anchoroffset[1]}
|
|
3766
|
+
elif child is tail and parent is parts[-1] and any(tailoffset):
|
|
3767
|
+
ends = {'pinx': tailoffset[0], 'piny': tailoffset[1]}
|
|
3768
|
+
parent.joint.attach(child, preset, **ends)
|
|
3769
|
+
if not selfcollide:
|
|
3770
|
+
for i, a in enumerate(parts):
|
|
3771
|
+
for b in parts[i + 2:]:
|
|
3772
|
+
if not any(g is b for g in a._ghosts):
|
|
3773
|
+
a._ghosts.append(b)
|
|
3774
|
+
if not any(g is a for g in b._ghosts):
|
|
3775
|
+
b._ghosts.append(a)
|
|
3776
|
+
return parts
|
|
3777
|
+
|
|
3778
|
+
@property
|
|
3779
|
+
def jradius(self):
|
|
3780
|
+
return self.radius
|
|
3781
|
+
|
|
3782
|
+
@jradius.setter
|
|
3783
|
+
def jradius(self, value):
|
|
3784
|
+
self.radius = value
|
|
3785
|
+
|
|
3786
|
+
@property
|
|
3787
|
+
def joints(self):
|
|
3788
|
+
return self._joints
|
|
3789
|
+
|
|
3790
|
+
@joints.setter
|
|
3791
|
+
def joints(self, value):
|
|
3792
|
+
self._assign(value)
|
|
3793
|
+
|
|
3794
|
+
@property
|
|
3795
|
+
def movable(self):
|
|
3796
|
+
p = getattr(self.obj, 'physics', None)
|
|
3797
|
+
return p is None or p.dynamic
|
|
3798
|
+
|
|
3799
|
+
@property
|
|
3800
|
+
def mass(self):
|
|
3801
|
+
p = getattr(self.obj, 'physics', None)
|
|
3802
|
+
return p.mass if p is not None and p.dynamic else 1.0
|
|
3803
|
+
|
|
3804
|
+
@property
|
|
3805
|
+
def velocity(self):
|
|
3806
|
+
p = getattr(self.obj, 'physics', None)
|
|
3807
|
+
if p is None or not p.dynamic:
|
|
3808
|
+
return 0.0, 0.0
|
|
3809
|
+
return p.xvel / p.mass, p.yvel / p.mass
|
|
3810
|
+
|
|
3811
|
+
def remove(self, item):
|
|
3812
|
+
edge = self._edges.get(id(item))
|
|
3813
|
+
if edge is None:
|
|
3814
|
+
raise ValueError('object is not jointed')
|
|
3815
|
+
self._unlink(edge)
|
|
3816
|
+
|
|
3817
|
+
def anchor(self, other):
|
|
3818
|
+
edge = self._edges.get(id(other))
|
|
3819
|
+
if edge is None:
|
|
3820
|
+
raise KeyError('object is not jointed')
|
|
3821
|
+
return edge.anchor()
|
|
3822
|
+
|
|
3823
|
+
def onbreak(self, cb):
|
|
3824
|
+
self._onbreak.append(cb)
|
|
3825
|
+
return cb
|
|
3826
|
+
|
|
3827
|
+
def offbreak(self, cb):
|
|
3828
|
+
self._onbreak = [f for f in self._onbreak if f is not cb]
|
|
3829
|
+
|
|
3830
|
+
def on_discard(self):
|
|
3831
|
+
self.t.offchange(self._pcb)
|
|
3832
|
+
self.obj._ghosts[:] = [g for g in self.obj._ghosts if g is not self._ghostcb]
|
|
3833
|
+
for edge in list(self._edges.values()):
|
|
3834
|
+
self._unlink(edge)
|
|
3835
|
+
|
|
3836
|
+
def _hierarchy(self):
|
|
3837
|
+
up = []
|
|
3838
|
+
down = []
|
|
3839
|
+
for e in self._edges.values():
|
|
3840
|
+
if not e.alive or e.follower.kind == 'ball':
|
|
3841
|
+
continue
|
|
3842
|
+
if e.follower is self:
|
|
3843
|
+
up.append(e.origin.obj)
|
|
3844
|
+
else:
|
|
3845
|
+
down.append(e.follower.obj)
|
|
3846
|
+
return up, down
|
|
3847
|
+
|
|
3848
|
+
def _accepted(self, spec):
|
|
3849
|
+
items = []
|
|
3850
|
+
seen = set()
|
|
3851
|
+
for item in spec:
|
|
3852
|
+
if item is self.obj:
|
|
3853
|
+
raise ValueError('an object cannot be jointed to itself')
|
|
3854
|
+
if getattr(item, 'transform', None) is None:
|
|
3855
|
+
raise TypeError('a jointed object needs a transform component')
|
|
3856
|
+
if id(item) not in seen:
|
|
3857
|
+
seen.add(id(item))
|
|
3858
|
+
items.append(item)
|
|
3859
|
+
return items
|
|
3860
|
+
|
|
3861
|
+
def _assign(self, spec):
|
|
3862
|
+
items = self._accepted(spec)
|
|
3863
|
+
wanted = {id(item) for item in items}
|
|
3864
|
+
for edge in [e for e in self._edges.values() if id(e.other(self).obj) not in wanted]:
|
|
3865
|
+
self._unlink(edge)
|
|
3866
|
+
ordered = {}
|
|
3867
|
+
fresh = []
|
|
3868
|
+
for item in items:
|
|
3869
|
+
edge = self._edges.get(id(item))
|
|
3870
|
+
if edge is None:
|
|
3871
|
+
if getattr(item, 'joint', None) is None:
|
|
3872
|
+
item.set(joint)
|
|
3873
|
+
edge = _edge(self, item.joint)
|
|
3874
|
+
item.joint._edges[id(self.obj)] = edge
|
|
3875
|
+
edge.capture()
|
|
3876
|
+
fresh.append(edge)
|
|
3877
|
+
ordered[id(item)] = edge
|
|
3878
|
+
self._edges = ordered
|
|
3879
|
+
for edge in fresh:
|
|
3880
|
+
edge.rewire()
|
|
3881
|
+
for edge in fresh:
|
|
3882
|
+
self._realign(edge)
|
|
3883
|
+
|
|
3884
|
+
def _unlink(self, edge):
|
|
3885
|
+
edge.alive = False
|
|
3886
|
+
edge.rewire()
|
|
3887
|
+
for comp in (edge.origin, edge.follower):
|
|
3888
|
+
other = edge.other(comp)
|
|
3889
|
+
if comp._edges.get(id(other.obj)) is edge:
|
|
3890
|
+
del comp._edges[id(other.obj)]
|
|
3891
|
+
f = edge.follower
|
|
3892
|
+
p = getattr(f.obj, 'physics', None)
|
|
3893
|
+
if p is not None and f.kind != 'ball' and not any(e.follower is f for e in f._edges.values()):
|
|
3894
|
+
p.avel = 0
|
|
3895
|
+
f._spun = None
|
|
3896
|
+
|
|
3897
|
+
def _break(self, edge):
|
|
3898
|
+
o, f = edge.origin, edge.follower
|
|
3899
|
+
self._unlink(edge)
|
|
3900
|
+
for comp, other in ((o, f.obj), (f, o.obj)):
|
|
3901
|
+
for cb in list(comp._onbreak):
|
|
3902
|
+
if _argslen(cb):
|
|
3903
|
+
cb(other)
|
|
3904
|
+
else:
|
|
3905
|
+
cb()
|
|
3906
|
+
|
|
3907
|
+
def _anchor(self, xoffset, yoffset):
|
|
3908
|
+
cx, cy = self.t.extent()[:2]
|
|
3909
|
+
A = self.scene.aspect
|
|
3910
|
+
r = math.radians(self.t.rotation)
|
|
3911
|
+
u = xoffset * A
|
|
3912
|
+
v = yoffset
|
|
3913
|
+
return (cx + (u * math.cos(r) + v * math.sin(r)) / A,
|
|
3914
|
+
cy + (-u * math.sin(r) + v * math.cos(r)))
|
|
3915
|
+
|
|
3916
|
+
def _move(self, dx, dy):
|
|
3917
|
+
o = self.obj
|
|
3918
|
+
t = o.transform
|
|
3919
|
+
p = getattr(o, 'physics', None)
|
|
3920
|
+
x, y = t.x, t.y
|
|
3921
|
+
if p is not None and p.dynamic and p.solid and not p.phantom:
|
|
3922
|
+
def depth(px, py):
|
|
3923
|
+
return sum(len(v) for v in p.overlaps(p.getpixels(px, py)).values())
|
|
3924
|
+
inside = depth(x, y)
|
|
3925
|
+
steps = min(200, max(1, int(math.ceil(max(abs(dx), abs(dy))))))
|
|
3926
|
+
sx, sy = dx / steps, dy / steps
|
|
3927
|
+
for _ in range(steps):
|
|
3928
|
+
moved = False
|
|
3929
|
+
if sx:
|
|
3930
|
+
d = depth(x + sx, y)
|
|
3931
|
+
if d == 0 or d < inside:
|
|
3932
|
+
x += sx
|
|
3933
|
+
inside = d
|
|
3934
|
+
moved = True
|
|
3935
|
+
if sy:
|
|
3936
|
+
d = depth(x, y + sy)
|
|
3937
|
+
if d == 0 or d < inside:
|
|
3938
|
+
y += sy
|
|
3939
|
+
inside = d
|
|
3940
|
+
moved = True
|
|
3941
|
+
if not moved:
|
|
3942
|
+
break
|
|
3943
|
+
else:
|
|
3944
|
+
x, y = x + dx, y + dy
|
|
3945
|
+
if x != t.x:
|
|
3946
|
+
t.x = x
|
|
3947
|
+
if y != t.y:
|
|
3948
|
+
t.y = y
|
|
3949
|
+
|
|
3950
|
+
def _push(self, dx, dy):
|
|
3951
|
+
p = getattr(self.obj, 'physics', None)
|
|
3952
|
+
if p is not None and p.dynamic:
|
|
3953
|
+
p.xvel += dx * p.mass
|
|
3954
|
+
p.yvel += dy * p.mass
|
|
3955
|
+
else:
|
|
3956
|
+
self._move(dx, dy)
|
|
3957
|
+
|
|
3958
|
+
def _pointvel(self, point):
|
|
3959
|
+
p = getattr(self.obj, 'physics', None)
|
|
3960
|
+
if p is None or not p.dynamic:
|
|
3961
|
+
return 0.0, 0.0
|
|
3962
|
+
A = self.scene.aspect
|
|
3963
|
+
cx, cy, _ = p.body()
|
|
3964
|
+
rx, ry = (point[0] - cx - 0.5) * A, point[1] - cy + 0.5
|
|
3965
|
+
om = p._omega()
|
|
3966
|
+
return p.xvel / p.mass - om * ry / A, p.yvel / p.mass + om * rx
|
|
3967
|
+
|
|
3968
|
+
def _kick(self, point, gx, gy):
|
|
3969
|
+
# change the velocity of `point` on this body by (gx, gy), spreading it between
|
|
3970
|
+
# linear and angular motion so the point itself moves by about that much
|
|
3971
|
+
p = self.obj.physics
|
|
3972
|
+
if p._invinertia():
|
|
3973
|
+
A = self.scene.aspect
|
|
3974
|
+
cx, cy, spread = p.body()
|
|
3975
|
+
rx, ry = (point[0] - cx - 0.5) * A, point[1] - cy + 0.5
|
|
3976
|
+
n = math.hypot(gx * A, gy)
|
|
3977
|
+
if n > 1e-12:
|
|
3978
|
+
cross = (rx * gy - ry * gx * A) / n
|
|
3979
|
+
s = 1.0 / (1.0 + cross * cross / spread)
|
|
3980
|
+
gx *= s
|
|
3981
|
+
gy *= s
|
|
3982
|
+
p._spin((rx * gy - ry * gx * A) * p.mass)
|
|
3983
|
+
p.xvel += gx * p.mass
|
|
3984
|
+
p.yvel += gy * p.mass
|
|
3985
|
+
|
|
3986
|
+
def _absorb(self, ex, ey):
|
|
3987
|
+
p = getattr(self.obj, 'physics', None)
|
|
3988
|
+
if p is None or not p.dynamic:
|
|
3989
|
+
return
|
|
3990
|
+
A = self.scene.aspect
|
|
3991
|
+
ux, uy = ex * A, ey
|
|
3992
|
+
n = math.hypot(ux, uy)
|
|
3993
|
+
if n < 1e-9:
|
|
3994
|
+
return
|
|
3995
|
+
ux /= n
|
|
3996
|
+
uy /= n
|
|
3997
|
+
along = (p.xvel / p.mass * A) * ux + (p.yvel / p.mass) * uy
|
|
3998
|
+
if along < 0:
|
|
3999
|
+
p.xvel -= along * ux / A * p.mass
|
|
4000
|
+
p.yvel -= along * uy * p.mass
|
|
4001
|
+
|
|
4002
|
+
def _order(self):
|
|
4003
|
+
depth = {id(self): 0}
|
|
4004
|
+
queue = [self]
|
|
4005
|
+
seen = set()
|
|
4006
|
+
result = []
|
|
4007
|
+
for comp in queue:
|
|
4008
|
+
for edge in list(comp._edges.values()):
|
|
4009
|
+
if edge.physical or edge in seen:
|
|
4010
|
+
continue
|
|
4011
|
+
seen.add(edge)
|
|
4012
|
+
other = edge.other(comp)
|
|
4013
|
+
if id(other) not in depth:
|
|
4014
|
+
depth[id(other)] = depth[id(comp)] + 1
|
|
4015
|
+
queue.append(other)
|
|
4016
|
+
result.append((edge, comp, other))
|
|
4017
|
+
elif depth[id(comp)] <= depth[id(other)]:
|
|
4018
|
+
result.append((edge, comp, other))
|
|
4019
|
+
else:
|
|
4020
|
+
result.append((edge, other, comp))
|
|
4021
|
+
return result
|
|
4022
|
+
|
|
4023
|
+
def _orient(self, edge):
|
|
4024
|
+
angle = edge.spin()
|
|
4025
|
+
if angle is None:
|
|
4026
|
+
return False
|
|
4027
|
+
t = self.t
|
|
4028
|
+
p = getattr(self.obj, 'physics', None)
|
|
4029
|
+
solid = p is not None and p.dynamic and p.solid and not p.phantom and not p.hitbox
|
|
4030
|
+
turn = _wrap(angle - t.rotation)
|
|
4031
|
+
step = 0 if self._spun is None else _wrap(angle - self._spun)
|
|
4032
|
+
self._spun = angle
|
|
4033
|
+
if p is not None and p.dynamic:
|
|
4034
|
+
want = step * p.mass if edge.physical and abs(step) <= p.rprecision * _SPINPIECES else 0
|
|
4035
|
+
if p.avel != want:
|
|
4036
|
+
p.avel = want
|
|
4037
|
+
if abs(turn) < 1e-6:
|
|
4038
|
+
return False
|
|
4039
|
+
t.rotation = angle
|
|
4040
|
+
if solid and p.overlaps():
|
|
4041
|
+
physics._running += 1
|
|
4042
|
+
try:
|
|
4043
|
+
p._depenetrate()
|
|
4044
|
+
finally:
|
|
4045
|
+
physics._running -= 1
|
|
4046
|
+
return True
|
|
4047
|
+
|
|
4048
|
+
def _fix(self, edge, near, far, motor, strict):
|
|
4049
|
+
ex, ey, dist = edge.error(motor)
|
|
4050
|
+
f = edge.follower
|
|
4051
|
+
if strict and f.durability and dist > f.durability:
|
|
4052
|
+
self._break(edge)
|
|
4053
|
+
return False
|
|
4054
|
+
moved = False
|
|
4055
|
+
if ex or ey:
|
|
4056
|
+
mover = far if far.movable else near
|
|
4057
|
+
if mover.movable:
|
|
4058
|
+
dx, dy = (ex, ey) if mover is f else (-ex, -ey)
|
|
4059
|
+
mover._absorb(dx, dy)
|
|
4060
|
+
mover._move(dx, dy)
|
|
4061
|
+
moved = True
|
|
4062
|
+
if f._orient(edge):
|
|
4063
|
+
moved = True
|
|
4064
|
+
return moved
|
|
4065
|
+
|
|
4066
|
+
def _settle(self, motor=False, strict=True, stamp=None):
|
|
4067
|
+
if joint._active:
|
|
4068
|
+
return
|
|
4069
|
+
joint._active = True
|
|
4070
|
+
try:
|
|
4071
|
+
order = self._order()
|
|
4072
|
+
if stamp is not None:
|
|
4073
|
+
self._stamp = stamp
|
|
4074
|
+
for edge, near, far in order:
|
|
4075
|
+
near._stamp = far._stamp = stamp
|
|
4076
|
+
for sweep in range(_SWEEPS):
|
|
4077
|
+
moved = False
|
|
4078
|
+
first = sweep == 0
|
|
4079
|
+
for edge, near, far in order:
|
|
4080
|
+
if edge.alive and self._fix(edge, near, far, first and motor, strict and first):
|
|
4081
|
+
moved = True
|
|
4082
|
+
if not moved:
|
|
4083
|
+
break
|
|
4084
|
+
finally:
|
|
4085
|
+
joint._active = False
|
|
4086
|
+
|
|
4087
|
+
def _realign(self, edge):
|
|
4088
|
+
if edge.alive and not edge.physical:
|
|
4089
|
+
edge.origin._settle(strict=False)
|
|
4090
|
+
|
|
4091
|
+
def _on_change(self, attr):
|
|
4092
|
+
if joint._active or physics._running or not self._edges or attr not in self._watch:
|
|
4093
|
+
return
|
|
4094
|
+
self._settle()
|
|
4095
|
+
|
|
4096
|
+
def step(self):
|
|
4097
|
+
if joint._active or not self._edges:
|
|
4098
|
+
return
|
|
4099
|
+
owned = [e for e in list(self._edges.values()) if e.origin is self]
|
|
4100
|
+
for edge in owned:
|
|
4101
|
+
if edge.alive and edge.physical:
|
|
4102
|
+
ex, ey, dist = edge.error(False)
|
|
4103
|
+
f = edge.follower
|
|
4104
|
+
if f.durability and dist > f.durability:
|
|
4105
|
+
self._break(edge)
|
|
4106
|
+
else:
|
|
4107
|
+
if ex or ey:
|
|
4108
|
+
if edge.beyond():
|
|
4109
|
+
self._fix(edge, self, f, False, False)
|
|
4110
|
+
else:
|
|
4111
|
+
edge.engaged = True
|
|
4112
|
+
edge.pull(ex, ey)
|
|
4113
|
+
elif edge.engaged:
|
|
4114
|
+
edge.engaged = False
|
|
4115
|
+
edge.pull(0.0, 0.0)
|
|
4116
|
+
f._orient(edge)
|
|
4117
|
+
tick = self.scene._ticks
|
|
4118
|
+
if self._stamp != tick and any(e.alive and not e.physical for e in owned):
|
|
4119
|
+
self._settle(motor=True, stamp=tick)
|
|
4120
|
+
|
|
4121
|
+
|
|
4122
|
+
class light(_lerpable):
|
|
4123
|
+
_angles = frozenset(('direction',))
|
|
4124
|
+
_colors = frozenset(('color',))
|
|
4125
|
+
_owned = ['light']
|
|
4126
|
+
_required = [transform]
|
|
4127
|
+
_watch = frozenset(('x', 'y', 'rotation', 'text', 'rtext', 'htext', 'aspect'))
|
|
4128
|
+
_settings = ('color', 'alpha', 'radius', 'falloff', 'shadows', 'shadowalpha', 'selfshadow',
|
|
4129
|
+
'cone', 'direction', 'offset', 'mix', 'affect', 'on', 'glow', 'length', 'focus', 'translucent')
|
|
4130
|
+
_stamps = {}
|
|
4131
|
+
|
|
4132
|
+
def on_discard(self):
|
|
4133
|
+
self.scene.lights = [l for l in self.scene.lights if l is not self]
|
|
4134
|
+
|
|
4135
|
+
def __init__(self, obj, **kwargs):
|
|
4136
|
+
self.obj = obj
|
|
4137
|
+
self.t = obj.transform
|
|
4138
|
+
self.scene = obj.scene
|
|
4139
|
+
self._version = 0
|
|
4140
|
+
self._sig = None
|
|
4141
|
+
self._occ = None
|
|
4142
|
+
self._thin = {}
|
|
4143
|
+
self._boxes = []
|
|
4144
|
+
self._origin = (0.0, 0.0)
|
|
4145
|
+
self._heading = None
|
|
4146
|
+
self._normal = (0.0, 1.0)
|
|
4147
|
+
self._color = (255, 255, 255)
|
|
4148
|
+
self._alpha = 1.0
|
|
4149
|
+
self._radius = 15
|
|
4150
|
+
self._falloff = 1.0
|
|
4151
|
+
self._shadows = True
|
|
4152
|
+
self._shadowalpha = 1.0
|
|
4153
|
+
self._selfshadow = False
|
|
4154
|
+
self._translucent = True
|
|
4155
|
+
self._cone = None
|
|
4156
|
+
self._direction = 0
|
|
4157
|
+
self._offset = (0, 0)
|
|
4158
|
+
self._mix = 0.35
|
|
4159
|
+
self._affect = None
|
|
4160
|
+
self._on = True
|
|
4161
|
+
self._glow = False
|
|
4162
|
+
self._length = 0.0
|
|
4163
|
+
self._focus = 0.0
|
|
4164
|
+
self._glowkey = None
|
|
4165
|
+
self._glowrows = []
|
|
4166
|
+
self._cfg = kwargs.get('light', True)
|
|
4167
|
+
self.light = self._cfg
|
|
4168
|
+
def cb(attr):
|
|
4169
|
+
if attr in self._watch:
|
|
4170
|
+
self._touch()
|
|
4171
|
+
self._cb = cb
|
|
4172
|
+
self.t.onchange(cb)
|
|
4173
|
+
self.scene.lights.append(self)
|
|
4174
|
+
|
|
4175
|
+
def __getitem__(self, key):
|
|
4176
|
+
return getattr(self, key)
|
|
4177
|
+
|
|
4178
|
+
def __setitem__(self, key, value):
|
|
4179
|
+
setattr(self, key, value)
|
|
4180
|
+
|
|
4181
|
+
def _touch(self):
|
|
4182
|
+
self._version += 1
|
|
4183
|
+
self._occ = None
|
|
4184
|
+
|
|
4185
|
+
@property
|
|
4186
|
+
def light(self):
|
|
4187
|
+
return self._cfg
|
|
4188
|
+
@light.setter
|
|
4189
|
+
def light(self, value):
|
|
4190
|
+
self._cfg = value
|
|
4191
|
+
if isinstance(value, dict):
|
|
4192
|
+
for key, val in value.items():
|
|
4193
|
+
if key not in self._settings:
|
|
4194
|
+
raise TypeError('unknown light setting: ' + str(key))
|
|
4195
|
+
setattr(self, key, val)
|
|
4196
|
+
|
|
4197
|
+
@property
|
|
4198
|
+
def color(self):
|
|
4199
|
+
return self._color
|
|
4200
|
+
@color.setter
|
|
4201
|
+
def color(self, value):
|
|
4202
|
+
rgb = self.scene.styler.rgb(value)
|
|
4203
|
+
if rgb is None:
|
|
4204
|
+
raise ValueError('unknown light color: ' + str(value))
|
|
4205
|
+
self._color = rgb
|
|
4206
|
+
self._touch()
|
|
4207
|
+
|
|
4208
|
+
@property
|
|
4209
|
+
def alpha(self):
|
|
4210
|
+
return self._alpha
|
|
4211
|
+
@alpha.setter
|
|
4212
|
+
def alpha(self, value):
|
|
4213
|
+
self._alpha = max(0.0, value)
|
|
4214
|
+
self._touch()
|
|
4215
|
+
|
|
4216
|
+
@property
|
|
4217
|
+
def radius(self):
|
|
4218
|
+
return self._radius
|
|
4219
|
+
@radius.setter
|
|
4220
|
+
def radius(self, value):
|
|
4221
|
+
if value <= 0:
|
|
4222
|
+
raise ValueError('radius must be positive')
|
|
4223
|
+
self._radius = value
|
|
4224
|
+
self._touch()
|
|
4225
|
+
|
|
4226
|
+
@property
|
|
4227
|
+
def falloff(self):
|
|
4228
|
+
return self._falloff
|
|
4229
|
+
@falloff.setter
|
|
4230
|
+
def falloff(self, value):
|
|
4231
|
+
if value <= 0:
|
|
4232
|
+
raise ValueError('falloff must be positive')
|
|
4233
|
+
self._falloff = value
|
|
4234
|
+
self._touch()
|
|
4235
|
+
|
|
4236
|
+
@property
|
|
4237
|
+
def shadows(self):
|
|
4238
|
+
return self._shadows
|
|
4239
|
+
@shadows.setter
|
|
4240
|
+
def shadows(self, value):
|
|
4241
|
+
self._shadows = bool(value)
|
|
4242
|
+
self._touch()
|
|
4243
|
+
|
|
4244
|
+
@property
|
|
4245
|
+
def shadowalpha(self):
|
|
4246
|
+
return self._shadowalpha
|
|
4247
|
+
@shadowalpha.setter
|
|
4248
|
+
def shadowalpha(self, value):
|
|
4249
|
+
self._shadowalpha = max(0.0, min(1.0, value))
|
|
4250
|
+
self._touch()
|
|
4251
|
+
|
|
4252
|
+
@property
|
|
4253
|
+
def selfshadow(self):
|
|
4254
|
+
return self._selfshadow
|
|
4255
|
+
@selfshadow.setter
|
|
4256
|
+
def selfshadow(self, value):
|
|
4257
|
+
self._selfshadow = bool(value)
|
|
4258
|
+
self._touch()
|
|
4259
|
+
|
|
4260
|
+
@property
|
|
4261
|
+
def translucent(self):
|
|
4262
|
+
return self._translucent
|
|
4263
|
+
@translucent.setter
|
|
4264
|
+
def translucent(self, value):
|
|
4265
|
+
self._translucent = bool(value)
|
|
4266
|
+
self._touch()
|
|
4267
|
+
|
|
4268
|
+
@property
|
|
4269
|
+
def cone(self):
|
|
4270
|
+
return self._cone
|
|
4271
|
+
@cone.setter
|
|
4272
|
+
def cone(self, value):
|
|
4273
|
+
if value is not None and not 0 < value < 360:
|
|
4274
|
+
raise ValueError('cone must be between 0 and 360 degrees or None')
|
|
4275
|
+
self._cone = value
|
|
4276
|
+
self._touch()
|
|
4277
|
+
|
|
4278
|
+
@property
|
|
4279
|
+
def direction(self):
|
|
4280
|
+
return self._direction
|
|
4281
|
+
@direction.setter
|
|
4282
|
+
def direction(self, value):
|
|
4283
|
+
self._direction = value
|
|
4284
|
+
self._touch()
|
|
4285
|
+
|
|
4286
|
+
@property
|
|
4287
|
+
def offset(self):
|
|
4288
|
+
return self._offset
|
|
4289
|
+
@offset.setter
|
|
4290
|
+
def offset(self, value):
|
|
4291
|
+
x, y = value
|
|
4292
|
+
self._offset = (x, y)
|
|
4293
|
+
self._touch()
|
|
4294
|
+
|
|
4295
|
+
@property
|
|
4296
|
+
def mix(self):
|
|
4297
|
+
return self._mix
|
|
4298
|
+
@mix.setter
|
|
4299
|
+
def mix(self, value):
|
|
4300
|
+
self._mix = max(0.0, min(1.0, value))
|
|
4301
|
+
self._touch()
|
|
4302
|
+
|
|
4303
|
+
@property
|
|
4304
|
+
def affect(self):
|
|
4305
|
+
return self._affect
|
|
4306
|
+
@affect.setter
|
|
4307
|
+
def affect(self, value):
|
|
4308
|
+
self._affect = value
|
|
4309
|
+
self._touch()
|
|
4310
|
+
|
|
4311
|
+
@property
|
|
4312
|
+
def on(self):
|
|
4313
|
+
return self._on
|
|
4314
|
+
@on.setter
|
|
4315
|
+
def on(self, value):
|
|
4316
|
+
self._on = bool(value)
|
|
4317
|
+
self._touch()
|
|
4318
|
+
|
|
4319
|
+
@property
|
|
4320
|
+
def glow(self):
|
|
4321
|
+
return self._glow
|
|
4322
|
+
@glow.setter
|
|
4323
|
+
def glow(self, value):
|
|
4324
|
+
self._glow = bool(value)
|
|
4325
|
+
self._touch()
|
|
4326
|
+
|
|
4327
|
+
@property
|
|
4328
|
+
def length(self):
|
|
4329
|
+
return self._length
|
|
4330
|
+
@length.setter
|
|
4331
|
+
def length(self, value):
|
|
4332
|
+
if value < 0:
|
|
4333
|
+
raise ValueError('length must not be negative')
|
|
4334
|
+
self._length = float(value)
|
|
4335
|
+
self._touch()
|
|
4336
|
+
|
|
4337
|
+
@property
|
|
4338
|
+
def focus(self):
|
|
4339
|
+
return self._focus
|
|
4340
|
+
@focus.setter
|
|
4341
|
+
def focus(self, value):
|
|
4342
|
+
self._focus = max(0.0, min(1.0, value))
|
|
4343
|
+
self._touch()
|
|
4344
|
+
|
|
4345
|
+
def _affects(self, o):
|
|
4346
|
+
return _matches(self._affect, o)
|
|
4347
|
+
|
|
4348
|
+
def origin(self):
|
|
4349
|
+
cx, cy = self.t.extent()[:2]
|
|
4350
|
+
A = self.scene.aspect
|
|
4351
|
+
r = math.radians(self.t.rotation)
|
|
4352
|
+
u = self._offset[0] * A
|
|
4353
|
+
v = self._offset[1]
|
|
4354
|
+
return (cx + (u * math.cos(r) + v * math.sin(r)) / A,
|
|
4355
|
+
cy + (-u * math.sin(r) + v * math.cos(r)))
|
|
4356
|
+
|
|
4357
|
+
def _casters(self):
|
|
4358
|
+
ox, oy = self._origin
|
|
4359
|
+
span = self._radius + self._length / 2
|
|
4360
|
+
reach_x = span / self.scene.aspect + 2
|
|
4361
|
+
reach_y = span + 2
|
|
4362
|
+
found = []
|
|
4363
|
+
pool = list(self.scene.solids)
|
|
4364
|
+
listed = {id(o) for o in pool}
|
|
4365
|
+
pool.extend(o for o in self.scene.objs
|
|
4366
|
+
if id(o) not in listed and getattr(getattr(o, 'transform', None), '_shadow', None) is not None)
|
|
4367
|
+
for o in pool:
|
|
4368
|
+
if o is self.obj:
|
|
4369
|
+
continue
|
|
4370
|
+
t = getattr(o, 'transform', None)
|
|
4371
|
+
if t is None:
|
|
4372
|
+
continue
|
|
4373
|
+
p = getattr(o, 'physics', None)
|
|
4374
|
+
if t._shadow is None and (p is None or p.phantom or not p.solid):
|
|
4375
|
+
continue
|
|
4376
|
+
if t._shadow is None or (t._shadow == 'physics' and p is not None):
|
|
4377
|
+
b = p.bounds()
|
|
4378
|
+
else:
|
|
4379
|
+
cells = t.cells()
|
|
4380
|
+
b = (min(c[0] for c in cells), max(c[0] for c in cells),
|
|
4381
|
+
min(c[1] for c in cells), max(c[1] for c in cells)) if cells else None
|
|
4382
|
+
if b is None or b[1] < ox - reach_x or b[0] > ox + reach_x or b[3] < oy - reach_y or b[2] > oy + reach_y:
|
|
4383
|
+
continue
|
|
4384
|
+
found.append((o, t))
|
|
4385
|
+
return found
|
|
4386
|
+
|
|
4387
|
+
def _prepare(self):
|
|
4388
|
+
self._origin = self.origin()
|
|
4389
|
+
occ = {}
|
|
4390
|
+
thin = {}
|
|
4391
|
+
boxes = []
|
|
4392
|
+
if self._shadows:
|
|
4393
|
+
for o, t in self._casters():
|
|
4394
|
+
cells = t.cells()
|
|
4395
|
+
ops = t.opacities() if self._translucent else None
|
|
4396
|
+
if ops:
|
|
4397
|
+
ox, oy = t._origin()
|
|
4398
|
+
for pixel in cells:
|
|
4399
|
+
value = ops.get((pixel[0] - ox, oy - pixel[1]))
|
|
4400
|
+
if value is None:
|
|
4401
|
+
occ[pixel] = id(o)
|
|
4402
|
+
thin.pop(pixel, None)
|
|
4403
|
+
elif pixel in occ and pixel not in thin:
|
|
4404
|
+
continue
|
|
4405
|
+
else:
|
|
4406
|
+
old = thin.get(pixel)
|
|
4407
|
+
thin[pixel] = value if old is None else 1 - (1 - old) * (1 - value)
|
|
4408
|
+
occ[pixel] = id(o)
|
|
4409
|
+
else:
|
|
4410
|
+
for pixel in cells:
|
|
4411
|
+
occ[pixel] = id(o)
|
|
4412
|
+
if thin:
|
|
4413
|
+
for pixel in cells:
|
|
4414
|
+
thin.pop(pixel, None)
|
|
4415
|
+
if cells:
|
|
4416
|
+
xs = [c[0] for c in cells]
|
|
4417
|
+
ys = [c[1] for c in cells]
|
|
4418
|
+
boxes.append((min(xs), max(xs), min(ys), max(ys)))
|
|
4419
|
+
self._occ = occ
|
|
4420
|
+
self._thin = thin
|
|
4421
|
+
self._boxes = boxes
|
|
4422
|
+
|
|
4423
|
+
def step(self):
|
|
4424
|
+
if not self._on:
|
|
4425
|
+
return
|
|
4426
|
+
self._origin = self.origin()
|
|
4427
|
+
sig = ()
|
|
4428
|
+
if self._shadows:
|
|
4429
|
+
sig = tuple((id(o), o.transform._x, o.transform._y, o.transform.vx, o.transform.vy,
|
|
4430
|
+
o.transform._pvx, o.transform._pvy, hash(o.transform._text),
|
|
4431
|
+
hash(o.transform._ptext), hash(o.transform._htext), hash(o.transform.vtext),
|
|
4432
|
+
o.transform._shadow,
|
|
4433
|
+
getattr(getattr(o, 'physics', None), 'hitbox', None))
|
|
4434
|
+
for o, t in self._casters())
|
|
4435
|
+
if sig != self._sig:
|
|
4436
|
+
self._sig = sig
|
|
4437
|
+
self._touch()
|
|
4438
|
+
|
|
4439
|
+
def _shadowed(self, cx, cy, owner, origin=None):
|
|
4440
|
+
occ = self._occ
|
|
4441
|
+
if not occ:
|
|
4442
|
+
return 0.0
|
|
4443
|
+
ox, oy = origin or self._origin
|
|
4444
|
+
dx = cx - ox
|
|
4445
|
+
dy = cy - oy
|
|
4446
|
+
hit = False
|
|
4447
|
+
first = 1.0
|
|
4448
|
+
last = 0.0
|
|
4449
|
+
for bx0, bx1, by0, by1 in self._boxes:
|
|
4450
|
+
lo = 0.0
|
|
4451
|
+
hi = 1.0
|
|
4452
|
+
if dx:
|
|
4453
|
+
a = (bx0 - ox) / dx
|
|
4454
|
+
b = (bx1 + 1 - ox) / dx
|
|
4455
|
+
if a > b:
|
|
4456
|
+
a, b = b, a
|
|
4457
|
+
if a > lo:
|
|
4458
|
+
lo = a
|
|
4459
|
+
if b < hi:
|
|
4460
|
+
hi = b
|
|
4461
|
+
elif ox < bx0 or ox > bx1 + 1:
|
|
4462
|
+
continue
|
|
4463
|
+
if dy:
|
|
4464
|
+
a = (by0 - 1 - oy) / dy
|
|
4465
|
+
b = (by1 - oy) / dy
|
|
4466
|
+
if a > b:
|
|
4467
|
+
a, b = b, a
|
|
4468
|
+
if a > lo:
|
|
4469
|
+
lo = a
|
|
4470
|
+
if b < hi:
|
|
4471
|
+
hi = b
|
|
4472
|
+
elif oy < by0 - 1 or oy > by1:
|
|
4473
|
+
continue
|
|
4474
|
+
if lo <= hi + 1e-9:
|
|
4475
|
+
hit = True
|
|
4476
|
+
if lo < first:
|
|
4477
|
+
first = lo
|
|
4478
|
+
if hi > last:
|
|
4479
|
+
last = hi
|
|
4480
|
+
if not hit:
|
|
4481
|
+
return 0.0
|
|
4482
|
+
n = max(1, int(math.ceil(max(abs(cx - ox), abs(cy - oy)) * 2)))
|
|
4483
|
+
me = id(self.obj)
|
|
4484
|
+
thin = self._thin
|
|
4485
|
+
crossed = None
|
|
4486
|
+
for i in range(max(1, int(first * n) - 1), min(n, int(last * n) + 3)):
|
|
4487
|
+
f = i / n
|
|
4488
|
+
key = (math.floor(ox + (cx - ox) * f), math.ceil(oy + (cy - oy) * f))
|
|
4489
|
+
who = occ.get(key)
|
|
4490
|
+
if who is not None and who != me and (self._selfshadow or who != owner):
|
|
4491
|
+
opacity = thin.get(key)
|
|
4492
|
+
if opacity is None:
|
|
4493
|
+
return 1.0
|
|
4494
|
+
if crossed is None:
|
|
4495
|
+
crossed = {}
|
|
4496
|
+
if opacity > crossed.get(who, 0.0):
|
|
4497
|
+
crossed[who] = opacity
|
|
4498
|
+
if crossed is None:
|
|
4499
|
+
return 0.0
|
|
4500
|
+
clear = 1.0
|
|
4501
|
+
for opacity in crossed.values():
|
|
4502
|
+
clear *= 1 - opacity
|
|
4503
|
+
return 1 - clear
|
|
4504
|
+
|
|
4505
|
+
def _facing(self):
|
|
4506
|
+
angle = self.t.rotation + self._direction
|
|
4507
|
+
if angle != self._heading:
|
|
4508
|
+
self._heading = angle
|
|
4509
|
+
radians = math.radians(angle)
|
|
4510
|
+
self._normal = (math.sin(radians), math.cos(radians))
|
|
4511
|
+
return self._normal
|
|
4512
|
+
|
|
4513
|
+
def _foot(self, dx, dy):
|
|
4514
|
+
if not self._length:
|
|
4515
|
+
return 0.0, 0.0
|
|
4516
|
+
nx, ny = self._facing()
|
|
4517
|
+
half = self._length / 2
|
|
4518
|
+
s = max(-half, min(half, dx * ny - dy * nx))
|
|
4519
|
+
return s * ny, -s * nx
|
|
4520
|
+
|
|
4521
|
+
def _source(self, dx, dy, origin=None):
|
|
4522
|
+
if not self._length:
|
|
4523
|
+
return origin
|
|
4524
|
+
ox, oy = origin or self._origin
|
|
4525
|
+
qx, qy = self._foot(dx, dy)
|
|
4526
|
+
return (ox + qx / self.scene.aspect, oy + qy)
|
|
4527
|
+
|
|
4528
|
+
def _level(self, dx, dy):
|
|
4529
|
+
qx, qy = self._foot(dx, dy)
|
|
4530
|
+
dx -= qx
|
|
4531
|
+
dy -= qy
|
|
4532
|
+
dist = math.hypot(dx, dy)
|
|
4533
|
+
if dist >= self._radius:
|
|
4534
|
+
return 0.0
|
|
4535
|
+
amount = self._alpha * (1 - dist / self._radius) ** self._falloff
|
|
4536
|
+
if dist > 0.5:
|
|
4537
|
+
if self._cone is not None:
|
|
4538
|
+
diff = abs(_wrap(math.degrees(math.atan2(dx, dy)) - (self.t.rotation + self._direction)))
|
|
4539
|
+
half = self._cone / 2
|
|
4540
|
+
if diff >= half:
|
|
4541
|
+
return 0.0
|
|
4542
|
+
amount *= min(1.0, (half - diff) / (self._cone * 0.1))
|
|
4543
|
+
if self._focus:
|
|
4544
|
+
nx, ny = self._facing()
|
|
4545
|
+
amount *= 1 - self._focus + self._focus * max(0.0, (dx * nx + dy * ny) / dist)
|
|
4546
|
+
return max(amount, 0.0)
|
|
4547
|
+
|
|
4548
|
+
def contribution(self, cx, cy, owner=None):
|
|
4549
|
+
if self._occ is None:
|
|
4550
|
+
self._prepare()
|
|
4551
|
+
ox, oy = self._origin
|
|
4552
|
+
dx = (cx - ox) * self.scene.aspect
|
|
4553
|
+
dy = cy - oy
|
|
4554
|
+
amount = self._level(dx, dy)
|
|
4555
|
+
if amount <= 0:
|
|
4556
|
+
return 0.0
|
|
4557
|
+
if self._shadows:
|
|
4558
|
+
blocked = self._shadowed(cx, cy, owner, self._source(dx, dy))
|
|
4559
|
+
if blocked:
|
|
4560
|
+
amount *= 1 - self._shadowalpha * blocked
|
|
4561
|
+
return amount
|
|
4562
|
+
|
|
4563
|
+
def glowing(self):
|
|
4564
|
+
return self._glow and self._on
|
|
4565
|
+
|
|
4566
|
+
def _stamp(self):
|
|
4567
|
+
aspect = self.scene.aspect
|
|
4568
|
+
angular = self._cone is not None or self._focus or self._length
|
|
4569
|
+
key = (self._radius, self._alpha, self._falloff, aspect, self._cone, self._length, self._focus,
|
|
4570
|
+
round((self.t.rotation + self._direction) % 360) if angular else 0)
|
|
4571
|
+
stamp = light._stamps.get(key)
|
|
4572
|
+
if stamp is None:
|
|
4573
|
+
if len(light._stamps) > 64:
|
|
4574
|
+
light._stamps.clear()
|
|
4575
|
+
span = self._radius + self._length / 2
|
|
4576
|
+
reach_x = int(span / aspect) + 1
|
|
4577
|
+
reach_y = int(span) + 1
|
|
4578
|
+
rows = []
|
|
4579
|
+
for j in range(-reach_y, reach_y + 1):
|
|
4580
|
+
amounts = [self._level(i * aspect, j) for i in range(-reach_x, reach_x + 1)]
|
|
4581
|
+
lit = [k for k, a in enumerate(amounts) if a > 0]
|
|
4582
|
+
if lit:
|
|
4583
|
+
rows.append((j, lit[0] - reach_x, amounts[lit[0]:lit[-1] + 1]))
|
|
4584
|
+
stamp = light._stamps[key] = (key, rows, reach_x, reach_y)
|
|
4585
|
+
return stamp
|
|
4586
|
+
|
|
4587
|
+
def _spot(self):
|
|
4588
|
+
if self._occ is None:
|
|
4589
|
+
self._prepare()
|
|
4590
|
+
return int(math.floor(self._origin[0])), int(math.ceil(self._origin[1]))
|
|
4591
|
+
|
|
4592
|
+
def glowbox(self, camx, camy, xoff, yoff, view):
|
|
4593
|
+
gx, gy = self._spot()
|
|
4594
|
+
_, _, reach_x, reach_y = self._stamp()
|
|
4595
|
+
return (max(view[0], gx - reach_x - camx + xoff),
|
|
4596
|
+
max(view[1], camy - gy - reach_y - yoff),
|
|
4597
|
+
min(view[2], gx + reach_x - camx + xoff + 1),
|
|
4598
|
+
min(view[3], camy - gy + reach_y - yoff + 1))
|
|
4599
|
+
|
|
4600
|
+
def glowkey(self, camx, camy, xoff, yoff, view):
|
|
4601
|
+
gx, gy = self._spot()
|
|
4602
|
+
return (gx, gy, self._stamp()[0], self._version if self._occ else 0,
|
|
4603
|
+
self._color, self._mix, camx, camy, xoff, yoff, view)
|
|
4604
|
+
|
|
4605
|
+
def glowrows(self, camx, camy, xoff, yoff, view):
|
|
4606
|
+
key = self.glowkey(camx, camy, xoff, yoff, view)
|
|
4607
|
+
if key == self._glowkey:
|
|
4608
|
+
return self._glowrows
|
|
4609
|
+
gx, gy = self._spot()
|
|
4610
|
+
rows = self._stamp()[1]
|
|
4611
|
+
aspect = self.scene.aspect
|
|
4612
|
+
origin = (gx + 0.5, gy - 0.5)
|
|
4613
|
+
out = []
|
|
4614
|
+
for j, i0, amounts in rows:
|
|
4615
|
+
y = camy - gy - j - yoff
|
|
4616
|
+
if y < view[1] or y >= view[3]:
|
|
4617
|
+
continue
|
|
4618
|
+
x = gx + i0 - camx + xoff
|
|
4619
|
+
lo = max(view[0] - x, 0)
|
|
4620
|
+
hi = min(view[2] - x, len(amounts))
|
|
4621
|
+
if lo >= hi:
|
|
4622
|
+
continue
|
|
4623
|
+
if lo or hi < len(amounts):
|
|
4624
|
+
amounts = amounts[lo:hi]
|
|
4625
|
+
x += lo
|
|
4626
|
+
if self._occ:
|
|
4627
|
+
cy = gy + j - 0.5
|
|
4628
|
+
base = gx + i0 + lo
|
|
4629
|
+
softened = []
|
|
4630
|
+
for k, a in enumerate(amounts):
|
|
4631
|
+
if a:
|
|
4632
|
+
blocked = self._shadowed(base + k + 0.5, cy, None,
|
|
4633
|
+
self._source((base + k + 0.5 - origin[0]) * aspect, j, origin))
|
|
4634
|
+
if blocked:
|
|
4635
|
+
a *= 1 - self._shadowalpha * blocked
|
|
4636
|
+
softened.append(a)
|
|
4637
|
+
amounts = softened
|
|
4638
|
+
out.append((y, x, amounts))
|
|
4639
|
+
self._glowkey = key
|
|
4640
|
+
self._glowrows = out
|
|
4641
|
+
return out
|
|
4642
|
+
|
|
4643
|
+
@staticmethod
|
|
4644
|
+
def shade(scene, obj, text):
|
|
4645
|
+
lights = [l for l in scene.lights if l.on and l._affects(obj)]
|
|
4646
|
+
ambient = max(0.0, min(1.0, scene.ambient))
|
|
4647
|
+
if not lights and ambient >= 1:
|
|
4648
|
+
return text
|
|
4649
|
+
t = obj.transform
|
|
4650
|
+
pixels = t.parse_pixels(text)
|
|
4651
|
+
ox, oy = t._origin()
|
|
4652
|
+
fgdef = tuple(scene.fgcolor)
|
|
4653
|
+
bgdef = tuple(scene.bgcolor)
|
|
4654
|
+
owner = id(obj)
|
|
4655
|
+
shaded = {}
|
|
4656
|
+
changed = False
|
|
4657
|
+
for (ci, ri), (char, style) in pixels.items():
|
|
4658
|
+
hasbg = bool(style) and t._has_background(style)
|
|
4659
|
+
if char == ' ' and not hasbg:
|
|
4660
|
+
shaded[(ci, ri)] = (char, style)
|
|
4661
|
+
continue
|
|
4662
|
+
total = 0.0
|
|
4663
|
+
mixsum = 0.0
|
|
4664
|
+
red = green = blue = 0.0
|
|
4665
|
+
for l in lights:
|
|
4666
|
+
c = l.contribution(ox + ci + 0.5, oy - ri - 0.5, owner)
|
|
4667
|
+
if c > 0:
|
|
4668
|
+
total += c
|
|
4669
|
+
mixsum += c * l._mix
|
|
4670
|
+
red += c * l._color[0]
|
|
4671
|
+
green += c * l._color[1]
|
|
4672
|
+
blue += c * l._color[2]
|
|
4673
|
+
level = min(1.0, ambient + total)
|
|
4674
|
+
if level >= 1.0 and total <= 0:
|
|
4675
|
+
shaded[(ci, ri)] = (char, style)
|
|
4676
|
+
continue
|
|
4677
|
+
if total > 0:
|
|
4678
|
+
tint = (red / total, green / total, blue / total)
|
|
4679
|
+
amount = (mixsum / total) * min(1.0, total)
|
|
4680
|
+
else:
|
|
4681
|
+
tint = None
|
|
4682
|
+
amount = 0.0
|
|
4683
|
+
|
|
4684
|
+
def blend(base):
|
|
4685
|
+
out = [bgdef[k] + (base[k] - bgdef[k]) * level for k in range(3)]
|
|
4686
|
+
if tint is not None:
|
|
4687
|
+
out = [out[k] + (tint[k] - out[k]) * amount for k in range(3)]
|
|
4688
|
+
return tuple(max(0, min(255, int(round(v)))) for v in out)
|
|
4689
|
+
|
|
4690
|
+
fa, back, ba = _ALPHAS(style) if style else (None, False, None)
|
|
4691
|
+
fgm = _FGCODE.search(style)
|
|
4692
|
+
fg = tuple(int(g) for g in fgm.groups()) if fgm else fgdef
|
|
4693
|
+
newstyle = _BGCODE.sub('', _FGCODE.sub('', style))
|
|
4694
|
+
bgm = _BGCODE.search(style)
|
|
4695
|
+
bgold = bgnew = bgdef
|
|
4696
|
+
if bgm:
|
|
4697
|
+
bgold = tuple(int(g) for g in bgm.groups())
|
|
4698
|
+
if ba is not None:
|
|
4699
|
+
bgnew = _rebake(bgdef, blend(_unbake(bgold, bgdef, ba)), ba)
|
|
4700
|
+
else:
|
|
4701
|
+
bgnew = blend(bgold)
|
|
4702
|
+
newstyle += ansi.bg(*bgnew)
|
|
4703
|
+
if char != ' ':
|
|
4704
|
+
if fa is not None:
|
|
4705
|
+
refold, refnew = (bgdef, bgdef) if back or not bgm else (bgold, bgnew)
|
|
4706
|
+
fgnew = _rebake(refnew, blend(_unbake(fg, refold, fa)), fa)
|
|
4707
|
+
else:
|
|
4708
|
+
fgnew = blend(fg)
|
|
4709
|
+
newstyle += ansi.fg(*fgnew)
|
|
4710
|
+
elif fgm:
|
|
4711
|
+
newstyle += fgm.group(0)
|
|
4712
|
+
shaded[(ci, ri)] = (char, newstyle)
|
|
4713
|
+
changed = True
|
|
4714
|
+
if not changed:
|
|
4715
|
+
return text
|
|
4716
|
+
xs = [p[0] for p in shaded]
|
|
4717
|
+
ys = [p[1] for p in shaded]
|
|
4718
|
+
return t.build_rows(shaded, min(xs), max(xs), min(ys), max(ys))
|