chartflow 0.1__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.
- chartflow/__init__.py +28 -0
- chartflow/_verify_importexport.py +169 -0
- chartflow/edit/__init__.py +119 -0
- chartflow/edit/code_editor.py +1007 -0
- chartflow/edit/code_folder.py +523 -0
- chartflow/edit/completer.py +2652 -0
- chartflow/edit/context_analyzer.py +521 -0
- chartflow/edit/demo.py +469 -0
- chartflow/edit/line_number_area.py +548 -0
- chartflow/edit/minimap.py +581 -0
- chartflow/edit/python_editor.py +509 -0
- chartflow/edit/syntax_highlighter.py +291 -0
- chartflow/edit/test_editor.py +190 -0
- chartflow/flow/__init__.py +31 -0
- chartflow/flow/_demo.py +98 -0
- chartflow/flow/chart.py +1101 -0
- chartflow/flow/editor.py +375 -0
- chartflow/flow/flow.py +6 -0
- chartflow/flow/qchart.py +1250 -0
- chartflow/flow/test/__init__.py +0 -0
- chartflow/flow/test/test_chart.py +537 -0
- chartflow/flow/test_decorator_sync.py +102 -0
- chartflow/fsm/__init__.py +84 -0
- chartflow/fsm/decorators.py +395 -0
- chartflow/fsm/demo.py +381 -0
- chartflow/fsm/demo_pyqt6.py +881 -0
- chartflow/fsm/demo_tree.py +46 -0
- chartflow/fsm/logic.py +447 -0
- chartflow/fsm/meta.py +569 -0
- chartflow/fsm/scheduler.py +427 -0
- chartflow/fsm/stage.py +2079 -0
- chartflow/fsm/stdio.py +66 -0
- chartflow/fsm/test/__init__.py +7 -0
- chartflow/fsm/test/test_decorators.py +210 -0
- chartflow/fsm/test/test_events.py +202 -0
- chartflow/fsm/test/test_integration.py +596 -0
- chartflow/fsm/test/test_logic.py +371 -0
- chartflow/fsm/test/test_meta.py +192 -0
- chartflow/fsm/test/test_runtime_param.py +336 -0
- chartflow/fsm/test/test_scheduler.py +280 -0
- chartflow/fsm/test/test_stage.py +314 -0
- chartflow/fsm/test_behavior_events.py +263 -0
- chartflow/fsm/test_nested_parallel.py +80 -0
- chartflow/fsm/test_stage_spec.py +448 -0
- chartflow/fsm/utils.py +34 -0
- chartflow/node/__init__.py +66 -0
- chartflow/node/_node_base.py +727 -0
- chartflow/node/_node_interaction.py +488 -0
- chartflow/node/_node_interface.py +426 -0
- chartflow/node/_node_markers.py +648 -0
- chartflow/node/_node_ports.py +536 -0
- chartflow/node/_port_interface.py +111 -0
- chartflow/node/arrow_item.py +287 -0
- chartflow/node/color_utils.py +113 -0
- chartflow/node/connection_item.py +939 -0
- chartflow/node/demo.py +258 -0
- chartflow/node/demo_arrow_drag.py +46 -0
- chartflow/node/demo_decorator.py +63 -0
- chartflow/node/demo_ports.py +69 -0
- chartflow/node/enums.py +35 -0
- chartflow/node/example.py +84 -0
- chartflow/node/layout_utils.py +307 -0
- chartflow/node/main_window.py +196 -0
- chartflow/node/menu_bar.py +240 -0
- chartflow/node/node_canvas.py +1730 -0
- chartflow/node/node_item.py +754 -0
- chartflow/node/popmenu.py +472 -0
- chartflow/node/port_item.py +591 -0
- chartflow/node/qnode_editor.py +73 -0
- chartflow/node/selection_rect.py +53 -0
- chartflow/node/style_panel.py +615 -0
- chartflow/node/styles.py +63 -0
- chartflow/node/test_qnode.py +2336 -0
- chartflow/showcase.py +528 -0
- chartflow/theme.py +508 -0
- chartflow-0.1.dist-info/METADATA +23 -0
- chartflow-0.1.dist-info/RECORD +79 -0
- chartflow-0.1.dist-info/WHEEL +5 -0
- chartflow-0.1.dist-info/top_level.txt +1 -0
chartflow/fsm/meta.py
ADDED
|
@@ -0,0 +1,569 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
"""
|
|
3
|
+
qfsm - Finite State Machine Metaclass
|
|
4
|
+
|
|
5
|
+
This module provides the StageMachineMeta metaclass for automatic state discovery,
|
|
6
|
+
class registration, and inheritance handling for state machines.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
import inspect
|
|
11
|
+
import weakref
|
|
12
|
+
from collections.abc import Callable
|
|
13
|
+
from typing import Any, ClassVar, Final, Self, TypeAlias
|
|
14
|
+
|
|
15
|
+
# Global prototype registry for all state machine classes
|
|
16
|
+
__protos__: dict[str, type] = {}
|
|
17
|
+
|
|
18
|
+
# Methods to ignore when discovering states
|
|
19
|
+
IGNORES: Final[frozenset[str]] = frozenset({
|
|
20
|
+
'ref', 'refLast', 'refNext', 'refAdjacent', 'refEnd', 'refHead',
|
|
21
|
+
'link', 'transform', 'listenFor', 'cancelListen', 'pushEvent',
|
|
22
|
+
'schedule', 'cancelSchedule', 'handleStage', 'onStart', 'onStop',
|
|
23
|
+
'onStep', 'onChanged', 'onKilled', 'onDraw', 'onLoading',
|
|
24
|
+
'productQueue', 'showQueue', 'jumpQueue', 'accelerate',
|
|
25
|
+
'goto', 'kill', 'enable', 'disable'
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
# Base classes that should not be registered
|
|
29
|
+
BASE_CLASSES: Final[frozenset[str]] = frozenset({'Stage', 'Logic'})
|
|
30
|
+
|
|
31
|
+
# Type aliases for class attribute structures
|
|
32
|
+
StateList: TypeAlias = list[str]
|
|
33
|
+
RecursiveSet: TypeAlias = set[str]
|
|
34
|
+
ListenDict: TypeAlias = dict[str, list[tuple[str, tuple[str, ...]]]]
|
|
35
|
+
BehaviorDict: TypeAlias = dict[str, dict[str, Any]]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class StageMachineMeta(type):
|
|
39
|
+
"""
|
|
40
|
+
Metaclass for finite state machines with automatic state discovery.
|
|
41
|
+
|
|
42
|
+
Features:
|
|
43
|
+
- Automatic discovery of state methods (callable, non-private, non-UPPERCASE, not in IGNORES)
|
|
44
|
+
- Class registration to global __protos__ registry
|
|
45
|
+
- Inheritance of __states__, __recursive__, __listens__, __behaviors__
|
|
46
|
+
- Tracking of decorator-marked methods
|
|
47
|
+
|
|
48
|
+
Class Attributes Created:
|
|
49
|
+
__states__: List of discovered state method names
|
|
50
|
+
__recursive__: Set of recursive state names
|
|
51
|
+
__listens__: Dict of event listeners {event_name: [(func_name, sources), ...]}
|
|
52
|
+
__behaviors__: Dict of behavior tree nodes {state_name: {children: ..., composite: ...}}
|
|
53
|
+
"""
|
|
54
|
+
__ID_PROTOS__ = {}
|
|
55
|
+
__INSTANCES__ = {}
|
|
56
|
+
IGNORES = IGNORES
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@staticmethod
|
|
60
|
+
def _remove_ref(_ref):
|
|
61
|
+
for _name, _d in list(StageMachineMeta.__INSTANCES__.items()):
|
|
62
|
+
for _id, _r in list(_d.items()):
|
|
63
|
+
if _r is _ref:
|
|
64
|
+
_d.pop(_id, None)
|
|
65
|
+
if not _d:
|
|
66
|
+
StageMachineMeta.__INSTANCES__.pop(_name, None)
|
|
67
|
+
return
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def __new__(
|
|
71
|
+
mcs,
|
|
72
|
+
name: str,
|
|
73
|
+
bases: tuple[type, ...],
|
|
74
|
+
namespace: dict[str, Any],
|
|
75
|
+
**kwargs: Any
|
|
76
|
+
) -> Self:
|
|
77
|
+
"""
|
|
78
|
+
Create a new state machine class with automatic state discovery.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
name: Class name
|
|
82
|
+
bases: Base classes
|
|
83
|
+
namespace: Class namespace
|
|
84
|
+
**kwargs: Additional keyword arguments
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
New class instance
|
|
88
|
+
"""
|
|
89
|
+
# Create the class first
|
|
90
|
+
cls = super().__new__(mcs, name, bases, namespace)
|
|
91
|
+
|
|
92
|
+
# Discover states from current class namespace
|
|
93
|
+
_extra_ignores = set(namespace.get('IGNORES', []))
|
|
94
|
+
current_states = mcs._discover_states(namespace, _extra_ignores)
|
|
95
|
+
|
|
96
|
+
# Inherit and merge attributes from base classes
|
|
97
|
+
cls.__states__ = mcs._merge_states(bases, current_states)
|
|
98
|
+
cls.__recursive__ = mcs._merge_recursive(bases, namespace)
|
|
99
|
+
cls.__listens__ = mcs._merge_listens(bases, namespace)
|
|
100
|
+
cls.__behaviors__ = mcs._merge_behaviors(bases, namespace)
|
|
101
|
+
|
|
102
|
+
# Validate behavior children exist (must happen after states and behaviors are set)
|
|
103
|
+
mcs._validate_behaviors(cls, name, current_states)
|
|
104
|
+
|
|
105
|
+
# Calculate max state name length for aligned output
|
|
106
|
+
if cls.__states__:
|
|
107
|
+
cls.__max_state_len__ = max(len(s) for s in cls.__states__)
|
|
108
|
+
else:
|
|
109
|
+
cls.__max_state_len__ = 0
|
|
110
|
+
|
|
111
|
+
# Register class if it has a NAME and is not a base class
|
|
112
|
+
mcs._register_class(cls, name)
|
|
113
|
+
|
|
114
|
+
return cls
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def __call__(cls, *args, **kwargs):
|
|
118
|
+
_skip_proto = getattr(cls, '__no_proto_call__', False)
|
|
119
|
+
if len(args) and isinstance(args[0], str) and not _skip_proto:
|
|
120
|
+
prototype = StageMachineMeta.proto(args[0])
|
|
121
|
+
if prototype is None:
|
|
122
|
+
raise TypeError(
|
|
123
|
+
f"[{cls.__name__}]: The given prototype name '{args[0]}' is not defined.\n\t* Please make sure import that file.")
|
|
124
|
+
return prototype(*args[1:], **kwargs)
|
|
125
|
+
instance = cls.__new__(cls, *args, **kwargs)
|
|
126
|
+
if isinstance(instance, cls):
|
|
127
|
+
instance.__init__(*args, **kwargs)
|
|
128
|
+
_name = getattr(instance, 'NAME', None)
|
|
129
|
+
if _name is not None:
|
|
130
|
+
_uid = getattr(instance, '_uid', id(instance))
|
|
131
|
+
ref = weakref.ref(instance, StageMachineMeta._remove_ref)
|
|
132
|
+
StageMachineMeta.__INSTANCES__.setdefault(_name, {})[_uid] = ref
|
|
133
|
+
return instance
|
|
134
|
+
|
|
135
|
+
@staticmethod
|
|
136
|
+
def _discover_states(namespace: dict[str, Any], extra_ignores: set[str] | None = None) -> StateList:
|
|
137
|
+
"""
|
|
138
|
+
Discover state methods from class namespace.
|
|
139
|
+
|
|
140
|
+
A method is considered a state if:
|
|
141
|
+
- It's callable
|
|
142
|
+
- Doesn't start with '_' (not private)
|
|
143
|
+
- Is not ALL UPPERCASE constant (lambda assignment)
|
|
144
|
+
- Is not in the IGNORES list
|
|
145
|
+
- Is not marked with @listen decorator
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
namespace: Class namespace dictionary
|
|
149
|
+
extra_ignores: Additional method names to ignore (from class-level IGNORES)
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
List of discovered state method names
|
|
153
|
+
"""
|
|
154
|
+
_ignores = IGNORES | (extra_ignores or set())
|
|
155
|
+
|
|
156
|
+
def _is_method(name: str, obj: Any) -> bool:
|
|
157
|
+
"""Check if obj is a method defined in this class (not lambda assignment)."""
|
|
158
|
+
if not callable(obj):
|
|
159
|
+
return False
|
|
160
|
+
if name.startswith('_'):
|
|
161
|
+
return False
|
|
162
|
+
if name in _ignores:
|
|
163
|
+
return False
|
|
164
|
+
if isinstance(obj, (staticmethod, classmethod)):
|
|
165
|
+
return False
|
|
166
|
+
if hasattr(obj, '_listen_event'):
|
|
167
|
+
return False
|
|
168
|
+
# Exclude ALL UPPERCASE method names (treat as constants)
|
|
169
|
+
if name.isupper():
|
|
170
|
+
return False
|
|
171
|
+
return True
|
|
172
|
+
|
|
173
|
+
return [
|
|
174
|
+
name for name, obj in namespace.items()
|
|
175
|
+
if _is_method(name, obj)
|
|
176
|
+
]
|
|
177
|
+
|
|
178
|
+
@staticmethod
|
|
179
|
+
def _merge_states(bases: tuple[type, ...], current_states: StateList) -> StateList:
|
|
180
|
+
"""
|
|
181
|
+
Merge states from base classes with current class states.
|
|
182
|
+
|
|
183
|
+
Base class states come first, then current class states.
|
|
184
|
+
Preserves order and removes duplicates.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
bases: Base classes
|
|
188
|
+
current_states: States discovered in current class
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
Merged list of state names
|
|
192
|
+
"""
|
|
193
|
+
inherited: StateList = []
|
|
194
|
+
|
|
195
|
+
# Collect states from base classes
|
|
196
|
+
for base in bases:
|
|
197
|
+
if hasattr(base, '__states__'):
|
|
198
|
+
for state in base.__states__:
|
|
199
|
+
if state not in inherited:
|
|
200
|
+
inherited.append(state)
|
|
201
|
+
|
|
202
|
+
# Add current class states (avoiding duplicates)
|
|
203
|
+
for state in current_states:
|
|
204
|
+
if state not in inherited:
|
|
205
|
+
inherited.append(state)
|
|
206
|
+
|
|
207
|
+
return inherited
|
|
208
|
+
|
|
209
|
+
@staticmethod
|
|
210
|
+
def _merge_recursive(bases: tuple[type, ...], namespace: dict[str, Any]) -> RecursiveSet:
|
|
211
|
+
"""
|
|
212
|
+
Merge recursive state markers from base classes and current class.
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
bases: Base classes
|
|
216
|
+
namespace: Class namespace
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
Set of recursive state names
|
|
220
|
+
"""
|
|
221
|
+
recursive: RecursiveSet = set()
|
|
222
|
+
|
|
223
|
+
# Inherit from base classes
|
|
224
|
+
for base in bases:
|
|
225
|
+
if hasattr(base, '__recursive__'):
|
|
226
|
+
recursive.update(base.__recursive__)
|
|
227
|
+
|
|
228
|
+
# Add from current class (methods marked with @recursive decorator)
|
|
229
|
+
for name, obj in namespace.items():
|
|
230
|
+
if name.startswith('_'):
|
|
231
|
+
continue
|
|
232
|
+
if callable(obj) and getattr(obj, '_recursive_marker', False):
|
|
233
|
+
recursive.add(name)
|
|
234
|
+
|
|
235
|
+
return recursive
|
|
236
|
+
|
|
237
|
+
@staticmethod
|
|
238
|
+
def _merge_listens(bases: tuple[type, ...], namespace: dict[str, Any]) -> ListenDict:
|
|
239
|
+
"""
|
|
240
|
+
Merge event listeners from base classes and current class.
|
|
241
|
+
|
|
242
|
+
Args:
|
|
243
|
+
bases: Base classes
|
|
244
|
+
namespace: Class namespace
|
|
245
|
+
|
|
246
|
+
Returns:
|
|
247
|
+
Dict of event listeners
|
|
248
|
+
"""
|
|
249
|
+
listens: ListenDict = {}
|
|
250
|
+
|
|
251
|
+
# Inherit from base classes
|
|
252
|
+
for base in bases:
|
|
253
|
+
if hasattr(base, '__listens__'):
|
|
254
|
+
for event, handlers in base.__listens__.items():
|
|
255
|
+
listens.setdefault(event, []).extend(handlers)
|
|
256
|
+
|
|
257
|
+
# Add from current class (methods marked with @listen decorator)
|
|
258
|
+
for name, obj in namespace.items():
|
|
259
|
+
if name.startswith('_'):
|
|
260
|
+
continue
|
|
261
|
+
if callable(obj):
|
|
262
|
+
listen_event = getattr(obj, '_listen_event', None)
|
|
263
|
+
if listen_event:
|
|
264
|
+
sources = getattr(obj, '_listen_sources', ())
|
|
265
|
+
listens.setdefault(listen_event, []).append((name, sources))
|
|
266
|
+
|
|
267
|
+
return listens
|
|
268
|
+
|
|
269
|
+
@staticmethod
|
|
270
|
+
def _merge_behaviors(bases: tuple[type, ...], namespace: dict[str, Any]) -> BehaviorDict:
|
|
271
|
+
"""
|
|
272
|
+
Merge behavior tree nodes from base classes and current class.
|
|
273
|
+
|
|
274
|
+
Args:
|
|
275
|
+
bases: Base classes
|
|
276
|
+
namespace: Class namespace
|
|
277
|
+
|
|
278
|
+
Returns:
|
|
279
|
+
Dict of behavior tree nodes
|
|
280
|
+
"""
|
|
281
|
+
behaviors: BehaviorDict = {}
|
|
282
|
+
|
|
283
|
+
# Inherit from base classes
|
|
284
|
+
for base in bases:
|
|
285
|
+
if hasattr(base, '__behaviors__'):
|
|
286
|
+
behaviors.update(base.__behaviors__)
|
|
287
|
+
|
|
288
|
+
# Add from current class (methods marked with behavior decorators)
|
|
289
|
+
for name, obj in namespace.items():
|
|
290
|
+
if name.startswith('_'):
|
|
291
|
+
continue
|
|
292
|
+
if callable(obj):
|
|
293
|
+
# Check for composite behaviors (@sequence, @selector, @parallel)
|
|
294
|
+
behavior_children = getattr(obj, '_behavior_children', None)
|
|
295
|
+
if behavior_children:
|
|
296
|
+
behavior_composite = getattr(obj, '_behavior_composite', None)
|
|
297
|
+
behaviors[name] = {
|
|
298
|
+
'children': behavior_children,
|
|
299
|
+
'composite': behavior_composite
|
|
300
|
+
}
|
|
301
|
+
else:
|
|
302
|
+
# Check for simple @behavior
|
|
303
|
+
behavior_target = getattr(obj, '_behavior_target', None)
|
|
304
|
+
if behavior_target:
|
|
305
|
+
behaviors[name] = {
|
|
306
|
+
'children': behavior_target,
|
|
307
|
+
'composite': None
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return behaviors
|
|
311
|
+
|
|
312
|
+
@staticmethod
|
|
313
|
+
def _validate_behaviors(cls: type, class_name: str, current_states: list[str]) -> None:
|
|
314
|
+
"""
|
|
315
|
+
Validate that all behavior children reference existing states.
|
|
316
|
+
|
|
317
|
+
This check happens at class definition time to catch errors early.
|
|
318
|
+
A behavior child must either be a regular state (in __states__) or
|
|
319
|
+
another behavior node (in __behaviors__).
|
|
320
|
+
|
|
321
|
+
Args:
|
|
322
|
+
cls: The class being created
|
|
323
|
+
class_name: The class name
|
|
324
|
+
current_states: States defined in the current class namespace
|
|
325
|
+
|
|
326
|
+
Raises:
|
|
327
|
+
TypeError: If a behavior references an unknown state
|
|
328
|
+
"""
|
|
329
|
+
valid_states = set(cls.__states__)
|
|
330
|
+
valid_behaviors = set(cls.__behaviors__.keys())
|
|
331
|
+
|
|
332
|
+
for behavior_name, behavior_info in cls.__behaviors__.items():
|
|
333
|
+
children = behavior_info.get('children', [])
|
|
334
|
+
# Handle both list and single string children
|
|
335
|
+
if isinstance(children, str):
|
|
336
|
+
children = [children]
|
|
337
|
+
elif not isinstance(children, (list, tuple)):
|
|
338
|
+
continue
|
|
339
|
+
|
|
340
|
+
for child in children:
|
|
341
|
+
if child not in valid_states and child not in valid_behaviors:
|
|
342
|
+
raise TypeError(
|
|
343
|
+
f"[{cls.__name__}]: Behavior '{behavior_name}' "
|
|
344
|
+
f"references unknown state '{child}'. "
|
|
345
|
+
f"Available states: {sorted(valid_states)}"
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
@staticmethod
|
|
349
|
+
def _register_class(cls: type, name: str) -> None:
|
|
350
|
+
"""
|
|
351
|
+
Register class to global prototype registry.
|
|
352
|
+
|
|
353
|
+
Registration rules:
|
|
354
|
+
- Class must have a 'NAME' attribute
|
|
355
|
+
- Class name must not be in BASE_CLASSES
|
|
356
|
+
- NAME must not end with a digit
|
|
357
|
+
|
|
358
|
+
Args:
|
|
359
|
+
cls: Class to register
|
|
360
|
+
name: Class name
|
|
361
|
+
"""
|
|
362
|
+
# Skip base classes
|
|
363
|
+
if name in BASE_CLASSES:
|
|
364
|
+
return
|
|
365
|
+
|
|
366
|
+
# Check for NAME attribute
|
|
367
|
+
class_name = getattr(cls, 'NAME', None)
|
|
368
|
+
if class_name is None:
|
|
369
|
+
return
|
|
370
|
+
|
|
371
|
+
# Validate NAME doesn't end with digit
|
|
372
|
+
if class_name and class_name[-1].isdigit():
|
|
373
|
+
raise ValueError(
|
|
374
|
+
f"Class '{name}' has invalid NAME '{class_name}': "
|
|
375
|
+
"NAME must not end with a digit"
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
# Register to global prototype registry
|
|
379
|
+
__protos__[class_name] = cls
|
|
380
|
+
|
|
381
|
+
@staticmethod
|
|
382
|
+
def proto(name:str):
|
|
383
|
+
return __protos__.get(name)
|
|
384
|
+
|
|
385
|
+
@staticmethod
|
|
386
|
+
def instances(name: str | type) -> list[Any]:
|
|
387
|
+
"""
|
|
388
|
+
Get all living instances with the given NAME.
|
|
389
|
+
|
|
390
|
+
Args:
|
|
391
|
+
name: Instance NAME (class NAME attribute) or class type
|
|
392
|
+
|
|
393
|
+
Returns:
|
|
394
|
+
List of living instances
|
|
395
|
+
"""
|
|
396
|
+
_n = name.NAME if isinstance(name, type) else name
|
|
397
|
+
_refs = StageMachineMeta.__INSTANCES__.get(_n, {})
|
|
398
|
+
_ret = []
|
|
399
|
+
for _ref in _refs.values():
|
|
400
|
+
_inst = _ref()
|
|
401
|
+
if _inst is not None:
|
|
402
|
+
_ret.append(_inst)
|
|
403
|
+
return _ret
|
|
404
|
+
|
|
405
|
+
@classmethod
|
|
406
|
+
def requireId(cls, name:str):
|
|
407
|
+
if not name:
|
|
408
|
+
raise ValueError("NAME cannot be empty")
|
|
409
|
+
ret = cls.__ID_PROTOS__[name] = cls.__ID_PROTOS__.get(name, -1) + 1
|
|
410
|
+
return ret
|
|
411
|
+
|
|
412
|
+
@staticmethod
|
|
413
|
+
def removeInstance(name: str | type, uid: int) -> bool:
|
|
414
|
+
"""从 __INSTANCES__ 中移除指定 uid 的实例。
|
|
415
|
+
|
|
416
|
+
Args:
|
|
417
|
+
name: Instance NAME (class NAME attribute) or class type
|
|
418
|
+
uid: 实例 uid (通常为 id(instance))
|
|
419
|
+
|
|
420
|
+
Returns:
|
|
421
|
+
True if removed, False if not found
|
|
422
|
+
"""
|
|
423
|
+
_n = name.NAME if isinstance(name, type) else name
|
|
424
|
+
_d = StageMachineMeta.__INSTANCES__.get(_n)
|
|
425
|
+
if _d is None:
|
|
426
|
+
return False
|
|
427
|
+
_removed = _d.pop(uid, None) is not None
|
|
428
|
+
if not _d:
|
|
429
|
+
StageMachineMeta.__INSTANCES__.pop(_n, None)
|
|
430
|
+
return _removed
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
# Decorator functions for marking methods
|
|
434
|
+
def recursive(func: Callable) -> Callable:
|
|
435
|
+
"""
|
|
436
|
+
Decorator to mark a state method as recursive.
|
|
437
|
+
|
|
438
|
+
Recursive states can transition to themselves.
|
|
439
|
+
|
|
440
|
+
Args:
|
|
441
|
+
func: Method to mark
|
|
442
|
+
|
|
443
|
+
Returns:
|
|
444
|
+
Marked method
|
|
445
|
+
"""
|
|
446
|
+
func._is_recursive = True # type: ignore[attr-defined]
|
|
447
|
+
return func
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
def listen(event: str, *sources: str) -> Callable[[Callable], Callable]:
|
|
451
|
+
"""
|
|
452
|
+
Decorator to mark a method as an event listener.
|
|
453
|
+
|
|
454
|
+
Args:
|
|
455
|
+
event: Event name to listen for
|
|
456
|
+
*sources: Optional source filters
|
|
457
|
+
|
|
458
|
+
Returns:
|
|
459
|
+
Decorator function
|
|
460
|
+
|
|
461
|
+
Example:
|
|
462
|
+
@listen('onComplete', 'loader')
|
|
463
|
+
def handle_complete(self, event): ...
|
|
464
|
+
"""
|
|
465
|
+
def decorator(func: Callable) -> Callable:
|
|
466
|
+
func._listen_info = (event, sources) # type: ignore[attr-defined]
|
|
467
|
+
return func
|
|
468
|
+
return decorator
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
def behavior(*, children: list[str] | None = None, composite: str | None = None) -> Callable[[Callable], Callable]:
|
|
472
|
+
"""
|
|
473
|
+
Decorator to mark a method as a behavior tree node.
|
|
474
|
+
|
|
475
|
+
Args:
|
|
476
|
+
children: List of child state names
|
|
477
|
+
composite: Composite type (e.g., 'selector', 'sequence')
|
|
478
|
+
|
|
479
|
+
Returns:
|
|
480
|
+
Decorator function
|
|
481
|
+
|
|
482
|
+
Example:
|
|
483
|
+
@behavior(children=['idle', 'walk', 'run'], composite='selector')
|
|
484
|
+
def movement(self): ...
|
|
485
|
+
"""
|
|
486
|
+
def decorator(func: Callable) -> Callable:
|
|
487
|
+
func._behavior_info = { # type: ignore[attr-defined]
|
|
488
|
+
'children': children or [],
|
|
489
|
+
'composite': composite
|
|
490
|
+
}
|
|
491
|
+
return func
|
|
492
|
+
return decorator
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
def state(func: Callable) -> Callable:
|
|
496
|
+
"""
|
|
497
|
+
Decorator to explicitly mark a method as a state.
|
|
498
|
+
|
|
499
|
+
This is optional since states are auto-discovered, but can be used
|
|
500
|
+
for clarity or to include methods that might otherwise be excluded.
|
|
501
|
+
|
|
502
|
+
Args:
|
|
503
|
+
func: Method to mark
|
|
504
|
+
|
|
505
|
+
Returns:
|
|
506
|
+
Marked method
|
|
507
|
+
"""
|
|
508
|
+
func._is_state = True # type: ignore[attr-defined]
|
|
509
|
+
return func
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
# Utility functions for prototype management
|
|
513
|
+
def GetPrototype(name: str) -> type | None:
|
|
514
|
+
"""
|
|
515
|
+
Get a registered prototype by name.
|
|
516
|
+
|
|
517
|
+
Args:
|
|
518
|
+
name: Prototype name (class NAME attribute)
|
|
519
|
+
|
|
520
|
+
Returns:
|
|
521
|
+
The prototype class or None if not found
|
|
522
|
+
"""
|
|
523
|
+
return __protos__.get(name)
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
# 小写别名,保持向后兼容
|
|
527
|
+
get_prototype = GetPrototype
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
def ListPrototypes() -> list[str]:
|
|
531
|
+
"""
|
|
532
|
+
List all registered prototype names.
|
|
533
|
+
|
|
534
|
+
Returns:
|
|
535
|
+
List of registered prototype names
|
|
536
|
+
"""
|
|
537
|
+
return list(__protos__.keys())
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
# 小写别名,保持向后兼容
|
|
541
|
+
list_prototypes = ListPrototypes
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
def ClearPrototypes() -> None:
|
|
545
|
+
"""Clear all registered prototypes. Mainly for testing."""
|
|
546
|
+
__protos__.clear()
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
# 小写别名,保持向后兼容
|
|
550
|
+
clear_prototypes = ClearPrototypes
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
# Export all public symbols
|
|
554
|
+
__all__ = [
|
|
555
|
+
'StageMachineMeta',
|
|
556
|
+
'recursive',
|
|
557
|
+
'listen',
|
|
558
|
+
'behavior',
|
|
559
|
+
'state',
|
|
560
|
+
'GetPrototype',
|
|
561
|
+
'get_prototype',
|
|
562
|
+
'ListPrototypes',
|
|
563
|
+
'list_prototypes',
|
|
564
|
+
'ClearPrototypes',
|
|
565
|
+
'clear_prototypes',
|
|
566
|
+
'__protos__',
|
|
567
|
+
'IGNORES',
|
|
568
|
+
'BASE_CLASSES',
|
|
569
|
+
]
|