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/scene.py ADDED
@@ -0,0 +1,614 @@
1
+ import inspect
2
+ def _argslen(cb):
3
+ params = inspect.signature(cb).parameters
4
+ l = len(params) - (1 if hasattr(cb, '__self__') else 0)
5
+ return l
6
+ def get_components(module):
7
+ return [
8
+ cls for _, cls in inspect.getmembers(module, inspect.isclass)
9
+ if cls.__module__ == module.__name__
10
+ ]
11
+ def wrap_property(component_attr: str, prop: property) -> property:
12
+ def fget(self):
13
+ return prop.fget(getattr(self, component_attr))
14
+ def fset(self, value):
15
+ prop.fset(getattr(self, component_attr), value)
16
+ return property(fget, fset if prop.fset else None)
17
+
18
+ from . import ansi
19
+ from . import region as _regions
20
+ from . import components as _components
21
+ from .ascii_styler import Styler
22
+ from .compositor import Compositor
23
+ _comps = []
24
+ _attrs = {}
25
+ _temps = []
26
+ _allcomps = {}
27
+ _defls = []
28
+ for _cls in get_components(_components):
29
+ defls = hasattr(_cls, '_owned')
30
+ reqrs = hasattr(_cls, '_required')
31
+ defl = hasattr(_cls, '_default')
32
+ _allcomps[_cls] = _argslen(_cls)
33
+ if defls or reqrs or defl:
34
+ _comps.append(_cls)
35
+ if defls:
36
+ for _attr in _cls._owned:
37
+ _attrs[_attr] = _cls
38
+ if defl:
39
+ if _cls._default: _defls.append(_cls)
40
+ else:
41
+ _temps.append(_cls)
42
+
43
+
44
+ def _findcomp(name):
45
+ for comp, params in _allcomps.items():
46
+ if comp.__name__ == name:
47
+ return comp, params
48
+ raise KeyError(f'Unknown component: {name!r}')
49
+
50
+ def _scopes():
51
+ found = {}
52
+ for cls in _comps:
53
+ names = getattr(cls, '_settings', None)
54
+ owned = getattr(cls, '_owned', None)
55
+ if not names or not owned:
56
+ continue
57
+ aliases = getattr(cls, '_aliases', {})
58
+ mapping = {name: aliases.get(name, name) for name in names}
59
+ if all(flat in owned for flat in mapping.values()):
60
+ found[cls.__name__] = (cls, mapping)
61
+ return found
62
+ _scoped = _scopes()
63
+
64
+ def _expand(kwargs):
65
+ if not any(name in kwargs for name in _scoped):
66
+ return kwargs, ()
67
+ flat = {}
68
+ scoped = {}
69
+ forced = []
70
+ for key, value in kwargs.items():
71
+ scope = _scoped.get(key)
72
+ if scope is None or not (isinstance(value, dict) or (value is True and key not in scope[0]._owned)):
73
+ flat[key] = value
74
+ continue
75
+ cls, mapping = scope
76
+ forced.append(cls)
77
+ if isinstance(value, dict):
78
+ for name, val in value.items():
79
+ if name not in mapping:
80
+ raise TypeError(f'unknown {key} setting: {name}')
81
+ scoped[mapping[name]] = val
82
+ flat.update(scoped)
83
+ return flat, forced
84
+
85
+ def _comname(c):
86
+ if isinstance(c, str):
87
+ return c
88
+ return c.__name__ if inspect.isclass(c) else type(c).__name__
89
+
90
+
91
+ class obj:
92
+ def __init__(self, scene):
93
+ self.scene = scene
94
+ self._ondiscardcb = []
95
+ self._ghosts = []
96
+
97
+ def __getitem__(self, key):
98
+ return getattr(getattr(self, _attrs[key].__name__), key)
99
+
100
+ def __setitem__(self, key, value):
101
+ setattr(getattr(self, _attrs[key].__name__), key, value)
102
+
103
+ def add(self, com, **kwargs):
104
+ kwargs, _ = _expand(kwargs)
105
+ if isinstance(com, str):
106
+ comp, params = _findcomp(com)
107
+ else:
108
+ comp = com
109
+ params = _argslen(com)
110
+ if params > 1:
111
+ compins = comp(self, **kwargs)
112
+ elif params > 0:
113
+ compins = comp(self)
114
+ else:
115
+ compins = comp()
116
+ ow = getattr(compins, '_owned', None)
117
+ ch = getattr(compins, '_changed', None)
118
+
119
+ for k, v in kwargs.items():
120
+ if ow and k in ow:
121
+ val = getattr(compins, k, None)
122
+ if val == v:
123
+ if not ch:
124
+ compins._changed = {k: v}
125
+ ch = compins._changed
126
+ else:
127
+ ch.update({k: v})
128
+ elif k not in _attrs:
129
+ setattr(self, k, v)
130
+
131
+ compinsattr = comp.__name__
132
+ setattr(self, compinsattr, compins)
133
+ return compins
134
+
135
+ def set(self, comn, **kwargs):
136
+ kwargs, _ = _expand(kwargs)
137
+ com = comn if isinstance(comn, (tuple, list)) else [comn]
138
+ for c in com:
139
+ if isinstance(c, str):
140
+ c, _ = _findcomp(c)
141
+ if inspect.isclass(c):
142
+ if not hasattr(self, c.__name__):
143
+ for rcom in getattr(c, '_required', ()):
144
+ if not hasattr(self, rcom.__name__):
145
+ self.add(rcom, **kwargs)
146
+ self.add(c, **kwargs)
147
+ c = getattr(self, c.__name__)
148
+ ow = getattr(c, '_owned', None)
149
+ ch = getattr(c, '_changed', None)
150
+ for k, v in kwargs.items():
151
+ if (ow and k in ow) or (not ow and hasattr(c, k)):
152
+ setattr(c, k, v)
153
+ if ow and k in ow:
154
+ if not ch:
155
+ c._changed = {k: v}
156
+ ch = c._changed
157
+ else:
158
+ ch.update({k: v})
159
+ elif k not in _attrs:
160
+ setattr(self, k, v)
161
+
162
+ def kwargs(self, coms=None):
163
+ if coms is None:
164
+ names = None
165
+ else:
166
+ if isinstance(coms, str) or inspect.isclass(coms):
167
+ coms = [coms]
168
+ names = {_comname(c) for c in coms}
169
+ result = {}
170
+ for at, cl in _attrs.items():
171
+ if names is not None and cl.__name__ not in names:
172
+ continue
173
+ com = getattr(self, cl.__name__, None)
174
+ changed = getattr(com, '_changed', None)
175
+ if at in result or not com or not changed: continue
176
+ result.update({k: getattr(com, k) for k in changed})
177
+ return result
178
+
179
+ def clone(self, coms=None, **overrides):
180
+ kwargs = self.kwargs(coms)
181
+ kwargs.update(overrides)
182
+ return self.scene.obj().create(**kwargs)
183
+
184
+ def _ensure(self, comp, kwargs, processed):
185
+ if comp in processed:
186
+ return
187
+ if not hasattr(self, comp.__name__):
188
+ for rcom in getattr(comp, '_required', ()):
189
+ if rcom not in processed:
190
+ if not hasattr(self, rcom.__name__):
191
+ self.add(rcom, **kwargs)
192
+ else:
193
+ self.set(rcom, **kwargs)
194
+ processed.add(rcom)
195
+ self.add(comp, **kwargs)
196
+ else:
197
+ self.set(comp, **kwargs)
198
+ processed.add(comp)
199
+
200
+ def create(self, text=None, x=None, y=None, **kwargs):
201
+ kwargs.update({k: v for k, v in {'text': text, 'x': x, 'y': y}.items() if v is not None})
202
+ kwargs, forced = _expand(kwargs)
203
+ processed = set()
204
+
205
+ for com in _defls:
206
+ self._ensure(com, kwargs, processed)
207
+
208
+ for k in list(kwargs.keys()):
209
+ comp = _attrs.get(k)
210
+ if comp:
211
+ self._ensure(comp, kwargs, processed)
212
+ else:
213
+ setattr(self, k, kwargs[k])
214
+
215
+ for comp in forced:
216
+ self._ensure(comp, kwargs, processed)
217
+
218
+ if self not in self.scene.objs:
219
+ self.scene.objs.append(self)
220
+ return self
221
+
222
+ def ondiscard(self, callback):
223
+ self._ondiscardcb.append(callback)
224
+ return callback
225
+
226
+ def offdiscard(self, callback):
227
+ self._ondiscardcb = [cb for cb in self._ondiscardcb if cb is not callback]
228
+
229
+ def discard(self, clean=True):
230
+ for dscb in list(self._ondiscardcb):
231
+ dscb()
232
+ self._ondiscardcb = []
233
+ owned = []
234
+ for co in _allcomps:
235
+ own = getattr(self, co.__name__, None)
236
+ if own is None: continue
237
+ owned.append((co.__name__, own))
238
+ for _, com in owned:
239
+ if hasattr(com, 'on_discard'):
240
+ com.on_discard()
241
+ if self in self.scene.objs:
242
+ self.scene.objs.remove(self)
243
+ for name, _ in owned:
244
+ delattr(self, name)
245
+ self.scene._getlast(True)
246
+
247
+
248
+ class Scene:
249
+ def __init__(self):
250
+ self.objs = []
251
+ self.dinfo = []
252
+ self.pending = []
253
+ self._maxframerate = 60
254
+ self.framerate = 60
255
+ self._oncamcb = {}
256
+ self._ondimcb = {}
257
+ self._ondimoffsetcb = {}
258
+ self._firing = False
259
+ self.termx = 120
260
+ self.termy = 30
261
+ self._dimx = self.termx
262
+ self._dimy = self.termy
263
+ self._dimxoffset = 0
264
+ self._dimyoffset = 0
265
+ self._last = {}
266
+ self._getlast(True)
267
+ self.solids = []
268
+ self.gravity = (0, 0)
269
+ self.drag = 1
270
+ self.precision = 0.5
271
+ self.aspect = 0.5
272
+ self.lights = []
273
+ self.ambient = 1.0
274
+ self._camx = 0
275
+ self._camy = 0
276
+ self._camrotation = 0.0
277
+ self._bgcolor = (0, 0, 0)
278
+ self._fade = None
279
+ self._ticks = 0
280
+ self._compositor = None
281
+ self.fgcolor = (204, 204, 204)
282
+ self.styler = Styler(self)
283
+ self.recomp = ansi.SEQUENCE
284
+ self._lastz = 0
285
+ self._firstz = 0
286
+
287
+ @property
288
+ def dimx(self):
289
+ return self._dimx
290
+ @dimx.setter
291
+ def dimx(self, value):
292
+ old = self._dimx
293
+ self._dimx = self.iround(value)
294
+ if self._dimx != old:
295
+ self._fire(self._ondimcb, 'x', old)
296
+
297
+ @property
298
+ def dimy(self):
299
+ return self._dimy
300
+ @dimy.setter
301
+ def dimy(self, value):
302
+ old = self._dimy
303
+ self._dimy = self.iround(value)
304
+ if self._dimy != old:
305
+ self._fire(self._ondimcb, 'y', old)
306
+
307
+ @property
308
+ def dimxoffset(self):
309
+ return self._dimxoffset
310
+ @dimxoffset.setter
311
+ def dimxoffset(self, value):
312
+ old = self._dimxoffset
313
+ self._dimxoffset = self.iround(value)
314
+ if self._dimxoffset != old:
315
+ self._fire(self._ondimoffsetcb, 'x', old)
316
+
317
+ @property
318
+ def dimyoffset(self):
319
+ return self._dimyoffset
320
+ @dimyoffset.setter
321
+ def dimyoffset(self, value):
322
+ old = self._dimyoffset
323
+ self._dimyoffset = self.iround(value)
324
+ if self._dimyoffset != old:
325
+ self._fire(self._ondimoffsetcb, 'y', old)
326
+
327
+ @property
328
+ def canvas(self):
329
+ return (self.termx, self.termy)
330
+ @canvas.setter
331
+ def canvas(self, value):
332
+ self.termx, self.termy = (max(0, self.iround(v)) for v in value)
333
+
334
+ @property
335
+ def view(self):
336
+ return _regions.Region(self._dimxoffset, -self._dimyoffset, self._dimx, self._dimy)
337
+ @view.setter
338
+ def view(self, value):
339
+ x, y, w, h = _regions.resolve(value, self.termx, self.termy)
340
+ self.dimxoffset = x
341
+ self.dimyoffset = -y
342
+ self.dimx = w
343
+ self.dimy = h
344
+
345
+ @property
346
+ def framerate(self):
347
+ return self._framerate
348
+ @framerate.setter
349
+ def framerate(self, value):
350
+ if not value > 0:
351
+ raise ValueError('framerate must be positive')
352
+ self._framerate = min(self._maxframerate, value)
353
+
354
+ @property
355
+ def maxframerate(self):
356
+ return self._maxframerate
357
+ @maxframerate.setter
358
+ def maxframerate(self, value):
359
+ if not value > 0:
360
+ raise ValueError('maxframerate must be positive')
361
+ self._maxframerate = value
362
+ self._framerate = min(value, self._framerate)
363
+
364
+ @property
365
+ def bgcolor(self):
366
+ return self._bgcolor
367
+ @bgcolor.setter
368
+ def bgcolor(self, value):
369
+ self._bgcolor = self._color(value)
370
+ self._getlast(True)
371
+
372
+ @property
373
+ def fading(self):
374
+ return self._fade is not None
375
+
376
+ def _color(self, value):
377
+ if isinstance(value, (int, float)):
378
+ value = int(value)
379
+ return (value, value, value)
380
+ color = Styler.rgb(tuple(value) if isinstance(value, list) else value)
381
+ if color is None:
382
+ raise ValueError('unknown color: %r' % (value,))
383
+ return color
384
+
385
+ def fade(self, color, time=1, delay=0, start=None, callback=None, ease='linear'):
386
+ if time < 0 or delay < 0:
387
+ raise ValueError('time and delay must not be negative')
388
+ curve = _components._easing(ease)
389
+ target = self._color(color)
390
+ origin = self._color(self._bgcolor if start is None else start)
391
+ if start is not None:
392
+ self._fade = None
393
+ self.bgcolor = origin
394
+ self._fade = {'from': origin, 'to': target, 'time': time, 'delay': delay,
395
+ 'elapsed': 0.0, 'cb': callback, 'ease': curve}
396
+ if time <= 0 and delay <= 0:
397
+ self._finishfade()
398
+ return self
399
+
400
+ def unfade(self, finish=False):
401
+ f = self._fade
402
+ self._fade = None
403
+ if f is not None and finish:
404
+ self.bgcolor = f['to']
405
+ return self
406
+
407
+ def _finishfade(self):
408
+ f = self._fade
409
+ self._fade = None
410
+ self.bgcolor = f['to']
411
+ cb = f['cb']
412
+ if callable(cb):
413
+ if _argslen(cb):
414
+ cb(self)
415
+ else:
416
+ cb()
417
+
418
+ def tick(self, physics=True):
419
+ if physics:
420
+ for o in list(self.objs):
421
+ phy = getattr(o, 'physics', None)
422
+ if phy is not None and phy.dynamic:
423
+ phy.physics()
424
+ self._ticks += 1
425
+ self._tickfade()
426
+ for o in list(self.objs):
427
+ tf = getattr(o, 'transform', None)
428
+ if tf is not None:
429
+ tf._tick()
430
+ for o in list(self.objs):
431
+ jnt = getattr(o, 'joint', None)
432
+ if physics and jnt is not None:
433
+ jnt.step()
434
+ lgt = getattr(o, 'light', None)
435
+ if lgt is not None:
436
+ lgt.step()
437
+
438
+ @property
439
+ def compositor(self):
440
+ if self._compositor is None:
441
+ self._compositor = Compositor()
442
+ return self._compositor
443
+
444
+ def render(self, region=None, outer=None, autobg=True, full=False):
445
+ comp = self.compositor
446
+ if full:
447
+ comp.invalidate()
448
+ x, y, w, h = region if region is not None else (0, 0, self.termx, self.termy)
449
+ return comp.compose([self], [self], (x, y, w, h), (x + w, y + h), outer, autobg)
450
+
451
+ def _tickfade(self):
452
+ f = self._fade
453
+ if f is None:
454
+ return
455
+ dt = 1 / self.framerate
456
+ if f['delay'] > 0:
457
+ f['delay'] -= dt
458
+ if f['delay'] > 0:
459
+ return
460
+ dt = -f['delay']
461
+ f['delay'] = 0
462
+ f['elapsed'] += dt
463
+ if f['elapsed'] >= f['time']:
464
+ self._finishfade()
465
+ return
466
+ p = f['ease'](f['elapsed'] / f['time'])
467
+ self.bgcolor = tuple(max(0, min(255, int(a + (b - a) * p))) for a, b in zip(f['from'], f['to']))
468
+
469
+ @property
470
+ def camx(self):
471
+ return self._camx
472
+ @camx.setter
473
+ def camx(self, value):
474
+ old = self._camx
475
+ self._camx = value
476
+ if value != old:
477
+ self._fire(self._oncamcb, 'x', old)
478
+
479
+ @property
480
+ def camy(self):
481
+ return self._camy
482
+ @camy.setter
483
+ def camy(self, value):
484
+ old = self._camy
485
+ self._camy = value
486
+ if value != old:
487
+ self._fire(self._oncamcb, 'y', old)
488
+
489
+ @property
490
+ def camrotation(self):
491
+ return self._camrotation
492
+ @camrotation.setter
493
+ def camrotation(self, value):
494
+ old = self._camrotation
495
+ self._camrotation = float(value) % 360.0
496
+ if self._camrotation != old:
497
+ self._fire(self._oncamcb, 'rotation', old)
498
+
499
+ def focus(self, target):
500
+ t = getattr(target, 'transform', target)
501
+ pivot = getattr(t, 'pivot', None)
502
+ if pivot is None:
503
+ return target[0], target[1]
504
+ return pivot()
505
+
506
+ def follow(self, target=None, y=None, smooth=1, offset=(0, 0)):
507
+ if target is None or isinstance(target, (int, float)):
508
+ px, py = target, y
509
+ else:
510
+ px, py = self.focus(target)
511
+ dx = dy = 0
512
+ if px is not None:
513
+ goal = px + offset[0] - self._dimx / 2
514
+ dx = goal - self._camx
515
+ dx = dx / smooth if smooth > 1 and abs(dx) > 0.01 else dx
516
+ if py is not None:
517
+ goal = py + offset[1] + self._dimy / 2
518
+ dy = goal - self._camy
519
+ dy = dy / smooth if smooth > 1 and abs(dy) > 0.01 else dy
520
+ if dx:
521
+ self.camx = self._camx + dx
522
+ if dy:
523
+ self.camy = self._camy + dy
524
+ return dx, dy
525
+
526
+ def _fire(self, callbacks, att='', old=None):
527
+ internal = self._firing
528
+ self._firing = True
529
+ try:
530
+ for cb, params in list(callbacks.items()):
531
+ if params > 2:
532
+ cb(att, old, internal)
533
+ elif params > 1:
534
+ cb(att, old)
535
+ elif params > 0:
536
+ cb(att)
537
+ else:
538
+ cb()
539
+ finally:
540
+ self._firing = internal
541
+
542
+ def _register_once(self, cb, store, once):
543
+ match_att = once if isinstance(once, str) else None
544
+ l = _argslen(cb)
545
+ fired = [False]
546
+ orig = cb
547
+ def wrapped(att=None, old=None, internal=False):
548
+ if fired[0]: return
549
+ if match_att is not None and att != match_att: return
550
+ fired[0] = True
551
+ if wrapped in store:
552
+ del store[wrapped]
553
+ if l > 2: orig(att, old, internal)
554
+ elif l > 1: orig(att, old)
555
+ elif l > 0: orig(att)
556
+ else: orig()
557
+ wrapped.__wrapped__ = orig
558
+ store[wrapped] = 3
559
+ return wrapped
560
+
561
+ def _register(self, callback, store, once):
562
+ if once:
563
+ self._register_once(callback, store, once)
564
+ else:
565
+ store[callback] = _argslen(callback)
566
+ return callback
567
+
568
+ @staticmethod
569
+ def _unregister(store, callback):
570
+ return {cb: pars for cb, pars in store.items()
571
+ if cb is not callback and getattr(cb, '__wrapped__', None) is not callback}
572
+
573
+ def oncam(self, callback, once=False):
574
+ return self._register(callback, self._oncamcb, once)
575
+
576
+ def offcam(self, callback):
577
+ self._oncamcb = self._unregister(self._oncamcb, callback)
578
+
579
+ def ondim(self, callback, once=False):
580
+ return self._register(callback, self._ondimcb, once)
581
+
582
+ def offdim(self, callback):
583
+ self._ondimcb = self._unregister(self._ondimcb, callback)
584
+
585
+ def ondimoffset(self, callback, once=False):
586
+ return self._register(callback, self._ondimoffsetcb, once)
587
+
588
+ def offdimoffset(self, callback):
589
+ self._ondimoffsetcb = self._unregister(self._ondimoffsetcb, callback)
590
+
591
+ def iround(self, x):
592
+ return int((x + 0.5) // 1)
593
+
594
+ def obj(self):
595
+ return obj(self)
596
+
597
+ def flush(self):
598
+ for func in self.pending:
599
+ func()
600
+ self.pending = []
601
+
602
+ def _getlast(self, none=False):
603
+ for k,v in vars(self).items():
604
+ if k.startswith('term') or k.startswith('_dim'):
605
+ self._last[k] = v if not none else None
606
+
607
+ def checkres(self, reg=True):
608
+ changed = False
609
+ for k,v in self._last.items():
610
+ val = getattr(self, k)
611
+ if val != v:
612
+ changed = True
613
+ if reg: self._last[k] = val
614
+ return changed