struct2ui 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.
- struct2ui/__init__.py +4 -0
- struct2ui/editor.py +1171 -0
- struct2ui/exporters/__init__.py +15 -0
- struct2ui/exporters/bin_emitter.py +254 -0
- struct2ui/exporters/c_emitter.py +233 -0
- struct2ui/exporters/c_parser.py +204 -0
- struct2ui/exporters/elf_verifier.py +341 -0
- struct2ui/exporters/json_format.py +137 -0
- struct2ui/icons/c2j.png +0 -0
- struct2ui/icons/elf.png +0 -0
- struct2ui/icons/export_notes_24dp_000000_FILL0_wght400_GRAD0_opsz24.png +0 -0
- struct2ui/icons/flowchart_24dp_000000_FILL0_wght400_GRAD0_opsz24.png +0 -0
- struct2ui/icons/refresh_24dp_000000_FILL0_wght400_GRAD0_opsz24.png +0 -0
- struct2ui/icons/report_24dp_000000_FILL0_wght400_GRAD0_opsz24.png +0 -0
- struct2ui/icons/save_24dp_000000_FILL0_wght400_GRAD0_opsz24.png +0 -0
- struct2ui/icons/save_as_24dp_000000_FILL0_wght400_GRAD0_opsz24.png +0 -0
- struct2ui/icons/settings_24dp_000000_FILL0_wght400_GRAD0_opsz24.png +0 -0
- struct2ui/icons/widgets_24dp_000000_FILL0_wght400_GRAD0_opsz24.png +0 -0
- struct2ui/schema.py +1118 -0
- struct2ui/ui/__init__.py +36 -0
- struct2ui/ui/renderers.py +304 -0
- struct2ui/ui/tables.py +207 -0
- struct2ui/ui/widgets.py +907 -0
- struct2ui-0.1.0.dist-info/METADATA +167 -0
- struct2ui-0.1.0.dist-info/RECORD +28 -0
- struct2ui-0.1.0.dist-info/WHEEL +5 -0
- struct2ui-0.1.0.dist-info/licenses/LICENSE +21 -0
- struct2ui-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Exporters: turn collected UI values + schema into target file formats.
|
|
2
|
+
|
|
3
|
+
Public API (stable; safe to import from outside):
|
|
4
|
+
emit_c - (sections, registry) -> C source string
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .c_emitter import emit_c
|
|
8
|
+
from .json_format import dumps as dumps_json
|
|
9
|
+
from .bin_emitter import emit_bin, merge_abi
|
|
10
|
+
from .elf_verifier import verify_sections
|
|
11
|
+
from .c_parser import parse_c_source, build_schema_dict
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
__all__ = ['emit_c', 'dumps_json', 'emit_bin', 'merge_abi', 'verify_sections',
|
|
15
|
+
'parse_c_source', 'build_schema_dict']
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
"""JSON/UI values + schema -> raw .bin emitter that mirrors the C memory layout.
|
|
2
|
+
|
|
3
|
+
The output is a byte-for-byte image of the target's struct memory, so the chip
|
|
4
|
+
SDK can memcpy it straight into the algorithm's config struct.
|
|
5
|
+
|
|
6
|
+
Layout model (C ABI simulation):
|
|
7
|
+
- Little-endian.
|
|
8
|
+
- Natural alignment: each member starts at an offset aligned to its
|
|
9
|
+
alignof; gaps are zero padding.
|
|
10
|
+
- A struct's alignof is the max alignof of its members; its size is padded
|
|
11
|
+
up to a multiple of that alignof (tail padding).
|
|
12
|
+
- `pack` is an alignment *cap*: effective align = min(natural, pack).
|
|
13
|
+
- enum -> fixed enum_size bytes (default 4).
|
|
14
|
+
- char[N] / arrays -> fixed length.
|
|
15
|
+
|
|
16
|
+
ABI knobs come from a dict (built-in defaults, overridable by a document-level
|
|
17
|
+
`abi` block). Defaults: little-endian, natural alignment, enum_size=4,
|
|
18
|
+
double 8-aligned, no pack.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import re
|
|
24
|
+
from typing import Any, Dict, List, Optional, Sequence, Tuple
|
|
25
|
+
|
|
26
|
+
from ..schema import (
|
|
27
|
+
Field, ScalarField, EnumField, StructField, ArrayField, SchemaRegistry,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
Section = Tuple[str, str, Any] # (label, type_name, values)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# Built-in default ABI: name -> (size, natural_align, struct format char).
|
|
35
|
+
_DEFAULT_TYPES: Dict[str, Tuple[int, int, str]] = {
|
|
36
|
+
'bool': (1, 1, 'B'), '_bool': (1, 1, 'B'),
|
|
37
|
+
'char': (1, 1, 'b'),
|
|
38
|
+
'int8_t': (1, 1, 'b'), 'uint8_t': (1, 1, 'B'),
|
|
39
|
+
'int16_t': (2, 2, 'h'), 'uint16_t': (2, 2, 'H'),
|
|
40
|
+
'short': (2, 2, 'h'),
|
|
41
|
+
'int32_t': (4, 4, 'i'), 'uint32_t': (4, 4, 'I'),
|
|
42
|
+
'int': (4, 4, 'i'), 'unsigned': (4, 4, 'I'),
|
|
43
|
+
'long': (4, 4, 'i'),
|
|
44
|
+
'int64_t': (8, 8, 'q'), 'uint64_t': (8, 8, 'Q'),
|
|
45
|
+
'float': (4, 4, 'f'), 'double': (8, 8, 'd'),
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
DEFAULT_ABI: Dict[str, Any] = {
|
|
49
|
+
'endian': 'little',
|
|
50
|
+
'pack': None,
|
|
51
|
+
'enum_size': 4,
|
|
52
|
+
'enum_signed': True,
|
|
53
|
+
'types': _DEFAULT_TYPES,
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def merge_abi(overlay: Optional[Dict[str, Any]]) -> Dict[str, Any]:
|
|
58
|
+
"""Merge a document-level abi overlay over the built-in defaults."""
|
|
59
|
+
abi = dict(DEFAULT_ABI)
|
|
60
|
+
abi['types'] = dict(_DEFAULT_TYPES)
|
|
61
|
+
if not overlay:
|
|
62
|
+
return abi
|
|
63
|
+
for key, value in overlay.items():
|
|
64
|
+
if key == 'types' and isinstance(value, dict):
|
|
65
|
+
for tname, spec in value.items():
|
|
66
|
+
abi['types'][tname] = _coerce_type_spec(spec)
|
|
67
|
+
else:
|
|
68
|
+
abi[key] = value
|
|
69
|
+
return abi
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _coerce_type_spec(spec: Any) -> Tuple[int, int, str]:
|
|
73
|
+
if isinstance(spec, (list, tuple)):
|
|
74
|
+
return (int(spec[0]), int(spec[1]), str(spec[2]))
|
|
75
|
+
size = int(spec['size'])
|
|
76
|
+
align = int(spec.get('align', size))
|
|
77
|
+
fmt = str(spec['fmt'])
|
|
78
|
+
return (size, align, fmt)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def emit_bin(sections: Sequence[Section], registry: SchemaRegistry,
|
|
82
|
+
abi: Optional[Dict[str, Any]] = None) -> bytes:
|
|
83
|
+
"""Render `sections` as a raw little-endian C-ABI byte image."""
|
|
84
|
+
abi = abi if abi is not None else merge_abi(None)
|
|
85
|
+
out = bytearray()
|
|
86
|
+
for label, type_name, values in sections:
|
|
87
|
+
root = registry.build(type_name)
|
|
88
|
+
_emit_struct(root, values if isinstance(values, dict) else {}, abi, out)
|
|
89
|
+
return bytes(out)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _endian_prefix(abi: Dict[str, Any]) -> str:
|
|
93
|
+
return '>' if abi.get('endian') == 'big' else '<'
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _cap(natural: int, abi: Dict[str, Any]) -> int:
|
|
97
|
+
pack = abi.get('pack')
|
|
98
|
+
return min(natural, pack) if pack else natural
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _alignof(field: Field, abi: Dict[str, Any]) -> int:
|
|
102
|
+
if isinstance(field, ArrayField):
|
|
103
|
+
return _alignof(field.element, abi)
|
|
104
|
+
if isinstance(field, StructField):
|
|
105
|
+
align = 1
|
|
106
|
+
for child in field.children:
|
|
107
|
+
align = max(align, _alignof(child, abi))
|
|
108
|
+
return _cap(align, abi)
|
|
109
|
+
if isinstance(field, EnumField):
|
|
110
|
+
return _cap(int(abi.get('enum_size', 4)), abi)
|
|
111
|
+
natural = _scalar_size_align(field, abi)[1]
|
|
112
|
+
return _cap(natural, abi)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _scalar_size_align(field: ScalarField, abi: Dict[str, Any]) -> Tuple[int, int]:
|
|
116
|
+
m = re.match(r'char\[(\d+)\]', field.c_type or '')
|
|
117
|
+
if m:
|
|
118
|
+
return (int(m.group(1)), 1)
|
|
119
|
+
size, align, _ = _lookup_type(field.c_type, abi)
|
|
120
|
+
return (size, align)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _lookup_type(c_type: str, abi: Dict[str, Any]) -> Tuple[int, int, str]:
|
|
124
|
+
spec = abi['types'].get((c_type or '').lower())
|
|
125
|
+
if spec is None:
|
|
126
|
+
raise ValueError(f'unknown C type for .bin layout: {c_type!r}')
|
|
127
|
+
return spec
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _pad_to(buf: bytearray, align: int) -> None:
|
|
131
|
+
rem = len(buf) % align
|
|
132
|
+
if rem:
|
|
133
|
+
buf.extend(b'\x00' * (align - rem))
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _emit_struct(node: StructField, values: Any, abi: Dict[str, Any],
|
|
137
|
+
out: bytearray) -> None:
|
|
138
|
+
if not isinstance(values, dict):
|
|
139
|
+
values = {}
|
|
140
|
+
start = len(out)
|
|
141
|
+
struct_align = _alignof(node, abi)
|
|
142
|
+
for child in node.children:
|
|
143
|
+
_pad_to_within(out, start, _alignof(child, abi))
|
|
144
|
+
val = values.get(child.name, child.default)
|
|
145
|
+
_emit_field(child, val, abi, out)
|
|
146
|
+
# Tail padding so the struct size is a multiple of its alignment.
|
|
147
|
+
_pad_to_within(out, start, struct_align)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _pad_to_within(out: bytearray, start: int, align: int) -> None:
|
|
151
|
+
rem = (len(out) - start) % align
|
|
152
|
+
if rem:
|
|
153
|
+
out.extend(b'\x00' * (align - rem))
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _emit_field(field: Field, value: Any, abi: Dict[str, Any],
|
|
157
|
+
out: bytearray) -> None:
|
|
158
|
+
if isinstance(field, ArrayField):
|
|
159
|
+
_emit_array(field, value, abi, out)
|
|
160
|
+
elif isinstance(field, StructField):
|
|
161
|
+
_emit_struct(field, value, abi, out)
|
|
162
|
+
elif isinstance(field, EnumField):
|
|
163
|
+
_emit_enum(field, value, abi, out)
|
|
164
|
+
elif isinstance(field, ScalarField):
|
|
165
|
+
_emit_scalar(field, value, abi, out)
|
|
166
|
+
else:
|
|
167
|
+
raise ValueError(f'cannot serialise field {field!r}')
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _emit_array(field: ArrayField, value: Any, abi: Dict[str, Any],
|
|
171
|
+
out: bytearray) -> None:
|
|
172
|
+
elem = field.element
|
|
173
|
+
items = value if isinstance(value, list) else []
|
|
174
|
+
start = len(out)
|
|
175
|
+
stride = _sizeof(elem, abi)
|
|
176
|
+
for i in range(field.count):
|
|
177
|
+
target = start + i * stride
|
|
178
|
+
if len(out) < target:
|
|
179
|
+
out.extend(b'\x00' * (target - len(out)))
|
|
180
|
+
val = items[i] if i < len(items) else (
|
|
181
|
+
elem.default if elem is not None else None)
|
|
182
|
+
_emit_field(elem, val, abi, out)
|
|
183
|
+
if len(out) < start + (i + 1) * stride:
|
|
184
|
+
out.extend(b'\x00' * (start + (i + 1) * stride - len(out)))
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _emit_enum(field: EnumField, value: Any, abi: Dict[str, Any],
|
|
188
|
+
out: bytearray) -> None:
|
|
189
|
+
num = _enum_to_int(field, value)
|
|
190
|
+
size = int(abi.get('enum_size', 4))
|
|
191
|
+
signed = bool(abi.get('enum_signed', True))
|
|
192
|
+
out.extend(num.to_bytes(size, _byteorder(abi), signed=signed))
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _emit_scalar(field: ScalarField, value: Any, abi: Dict[str, Any],
|
|
196
|
+
out: bytearray) -> None:
|
|
197
|
+
import struct
|
|
198
|
+
m = re.match(r'char\[(\d+)\]', field.c_type or '')
|
|
199
|
+
if m:
|
|
200
|
+
n = int(m.group(1))
|
|
201
|
+
raw = (value if isinstance(value, str) else '').encode('utf-8')[:n]
|
|
202
|
+
out.extend(raw + b'\x00' * (n - len(raw)))
|
|
203
|
+
return
|
|
204
|
+
size, _, fmt = _lookup_type(field.c_type, abi)
|
|
205
|
+
coerced = _coerce_scalar(field.ui_type, value)
|
|
206
|
+
out.extend(struct.pack(_endian_prefix(abi) + fmt, coerced))
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _coerce_scalar(ui_type: str, value: Any) -> Any:
|
|
210
|
+
if ui_type == 'bool':
|
|
211
|
+
return 1 if value else 0
|
|
212
|
+
if ui_type == 'int':
|
|
213
|
+
return int(value if value is not None else 0)
|
|
214
|
+
if ui_type == 'float':
|
|
215
|
+
return float(value if value is not None else 0.0)
|
|
216
|
+
return int(value if value is not None else 0)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _enum_to_int(field: EnumField, value: Any) -> int:
|
|
220
|
+
if isinstance(value, str):
|
|
221
|
+
if value in field.items:
|
|
222
|
+
return int(field.items[value])
|
|
223
|
+
return 0
|
|
224
|
+
if isinstance(value, bool):
|
|
225
|
+
return int(value)
|
|
226
|
+
if value is None:
|
|
227
|
+
return 0
|
|
228
|
+
return int(value)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _byteorder(abi: Dict[str, Any]) -> str:
|
|
232
|
+
return 'big' if abi.get('endian') == 'big' else 'little'
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _sizeof(field: Field, abi: Dict[str, Any]) -> int:
|
|
236
|
+
"""Aligned size (stride) of a field, including struct/array padding."""
|
|
237
|
+
if isinstance(field, ArrayField):
|
|
238
|
+
return field.count * _sizeof(field.element, abi)
|
|
239
|
+
if isinstance(field, StructField):
|
|
240
|
+
size = 0
|
|
241
|
+
for child in field.children:
|
|
242
|
+
calign = _alignof(child, abi)
|
|
243
|
+
rem = size % calign
|
|
244
|
+
if rem:
|
|
245
|
+
size += calign - rem
|
|
246
|
+
size += _sizeof(child, abi)
|
|
247
|
+
salign = _alignof(field, abi)
|
|
248
|
+
rem = size % salign
|
|
249
|
+
if rem:
|
|
250
|
+
size += salign - rem
|
|
251
|
+
return size
|
|
252
|
+
if isinstance(field, EnumField):
|
|
253
|
+
return int(abi.get('enum_size', 4))
|
|
254
|
+
return _scalar_size_align(field, abi)[0]
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
"""JSON/UI values + schema -> C source emitter.
|
|
2
|
+
|
|
3
|
+
Pure functions, no Qt. Input is the collected values (plain dicts/lists, as
|
|
4
|
+
produced by the UI read-back) plus the schema Field tree (from
|
|
5
|
+
SchemaRegistry.build). Output is a C source string.
|
|
6
|
+
|
|
7
|
+
Emit policy (agreed with the project owner):
|
|
8
|
+
- One struct instance per pipeline section.
|
|
9
|
+
- Designated initialisers ({ .field = value }) for struct instances, so the
|
|
10
|
+
output is field-order independent and readable.
|
|
11
|
+
- Flat struct arrays that the UI renders as a *table* use compact positional
|
|
12
|
+
initialisers ({ {50, A_MODE_HIGH}, ... }); other struct arrays keep
|
|
13
|
+
designated initialisers per element.
|
|
14
|
+
- Enum values are written as their symbol (A_MODE_HIGH), not the integer.
|
|
15
|
+
- Booleans -> true/false (assumes <stdbool.h>).
|
|
16
|
+
- Strings (char[N]) -> "quoted".
|
|
17
|
+
- Only .c is produced (no header).
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
from typing import Any, List, Sequence, Tuple
|
|
23
|
+
|
|
24
|
+
from ..schema import (
|
|
25
|
+
Field, ScalarField, EnumField, StructField, ArrayField, SchemaRegistry,
|
|
26
|
+
)
|
|
27
|
+
from ..ui.tables import is_flat_struct_array
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
Section = Tuple[str, str, Any] # (variable_label, type_name, values)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def emit_c(sections: Sequence[Section], registry: SchemaRegistry) -> str:
|
|
34
|
+
"""Render `sections` as a C source string.
|
|
35
|
+
|
|
36
|
+
sections: ordered list of (label, type_name, values). `label` becomes the
|
|
37
|
+
C variable name, `type_name` is resolved against `registry` to a
|
|
38
|
+
StructField, `values` is the collected per-field dict.
|
|
39
|
+
"""
|
|
40
|
+
blocks: List[str] = []
|
|
41
|
+
for label, type_name, values in sections:
|
|
42
|
+
root = registry.build(type_name)
|
|
43
|
+
var = _c_identifier(label)
|
|
44
|
+
init = _emit_struct(root, values, indent=0)
|
|
45
|
+
blocks.append(f'{type_name} {var} = {init};')
|
|
46
|
+
|
|
47
|
+
body = '\n\n'.join(blocks)
|
|
48
|
+
return _HEADER + body + '\n'
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
_HEADER = (
|
|
52
|
+
'/* Auto-generated by struct2ui. Do not edit by hand. */\n'
|
|
53
|
+
'#include <stdbool.h>\n'
|
|
54
|
+
'#include <stdint.h>\n\n'
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _c_identifier(label: str) -> str:
|
|
59
|
+
out = ''.join(c if c.isalnum() or c == '_' else '_' for c in label)
|
|
60
|
+
if out and out[0].isdigit():
|
|
61
|
+
out = '_' + out
|
|
62
|
+
return out or '_'
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _emit_struct(node: StructField, values: Any, indent: int) -> str:
|
|
66
|
+
"""Designated initialiser for a struct: { .a = .., .b = .. }."""
|
|
67
|
+
if not isinstance(values, dict):
|
|
68
|
+
values = {}
|
|
69
|
+
pad = ' ' * (indent + 1)
|
|
70
|
+
close_pad = ' ' * indent
|
|
71
|
+
lines: List[str] = []
|
|
72
|
+
for child in node.children:
|
|
73
|
+
val = values.get(child.name, child.default)
|
|
74
|
+
rendered = _emit_field(child, val, indent + 1)
|
|
75
|
+
lines.append(f'{pad}.{child.name} = {rendered}')
|
|
76
|
+
if not lines:
|
|
77
|
+
return '{ 0 }'
|
|
78
|
+
return '{\n' + ',\n'.join(lines) + f'\n{close_pad}}}'
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _emit_field(field: Field, value: Any, indent: int) -> str:
|
|
82
|
+
if isinstance(field, ArrayField):
|
|
83
|
+
return _emit_array(field, value, indent)
|
|
84
|
+
if isinstance(field, StructField):
|
|
85
|
+
return _emit_struct(field, value, indent)
|
|
86
|
+
if isinstance(field, EnumField):
|
|
87
|
+
return _emit_enum(field, value)
|
|
88
|
+
if isinstance(field, ScalarField):
|
|
89
|
+
return _emit_scalar(field, value)
|
|
90
|
+
return _emit_literal(value)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _emit_array(field: ArrayField, value: Any, indent: int) -> str:
|
|
94
|
+
elem = field.element
|
|
95
|
+
items = value if isinstance(value, list) else []
|
|
96
|
+
|
|
97
|
+
def at(i: int) -> Any:
|
|
98
|
+
if i < len(items):
|
|
99
|
+
return items[i]
|
|
100
|
+
return elem.default if elem is not None else None
|
|
101
|
+
|
|
102
|
+
# Flat struct array rendered as a table -> compact positional initialiser,
|
|
103
|
+
# one element per line so wide tables stay readable.
|
|
104
|
+
if isinstance(elem, StructField) and _renders_as_table(field):
|
|
105
|
+
pad = ' ' * (indent + 1)
|
|
106
|
+
close_pad = ' ' * indent
|
|
107
|
+
grid: List[List[str]] = []
|
|
108
|
+
for i in range(field.count):
|
|
109
|
+
row = at(i) if isinstance(at(i), dict) else {}
|
|
110
|
+
grid.append([_emit_field(c, row.get(c.name, c.default), indent + 1)
|
|
111
|
+
for c in elem.children])
|
|
112
|
+
widths = _column_widths(grid)
|
|
113
|
+
rows: List[str] = []
|
|
114
|
+
for cells in grid:
|
|
115
|
+
padded = [c.ljust(widths[j]) for j, c in enumerate(cells)]
|
|
116
|
+
rows.append(pad + '{' + ', '.join(padded).rstrip() + '}')
|
|
117
|
+
return '{\n' + ',\n'.join(rows) + f'\n{close_pad}}}'
|
|
118
|
+
|
|
119
|
+
# Other struct array -> designated initialiser per element.
|
|
120
|
+
if isinstance(elem, StructField):
|
|
121
|
+
pad = ' ' * (indent + 1)
|
|
122
|
+
close_pad = ' ' * indent
|
|
123
|
+
rows = [pad + _emit_struct(elem, at(i), indent + 1)
|
|
124
|
+
for i in range(field.count)]
|
|
125
|
+
return '{\n' + ',\n'.join(rows) + f'\n{close_pad}}}'
|
|
126
|
+
|
|
127
|
+
# Scalar/enum array. widget=multiline breaks the values into rows by the
|
|
128
|
+
# innermost dimension; otherwise long arrays are wrapped at MAX_ROW so a
|
|
129
|
+
# single line never runs to hundreds of values. Short arrays stay on one
|
|
130
|
+
# line: { a, b, c }.
|
|
131
|
+
cells = [_emit_field(elem, at(i), indent) for i in range(field.count)]
|
|
132
|
+
row_size = _multiline_row_size(field)
|
|
133
|
+
if row_size > 0:
|
|
134
|
+
return _wrap_cells_aligned(cells, row_size, indent)
|
|
135
|
+
if len(cells) > MAX_ROW:
|
|
136
|
+
return _wrap_cells_aligned(cells, MAX_ROW, indent)
|
|
137
|
+
return '{ ' + ', '.join(cells) + ' }'
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# Wrap scalar/enum arrays longer than this many elements, even without an
|
|
141
|
+
# explicit widget=multiline. Keeps generated lines readable.
|
|
142
|
+
MAX_ROW = 64
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _wrap_cells_aligned(cells: List[str], row_size: int, indent: int) -> str:
|
|
146
|
+
"""Lay out `cells` row_size-per-line inside braces, padding each column to
|
|
147
|
+
its max width so values line up vertically."""
|
|
148
|
+
pad = ' ' * (indent + 1)
|
|
149
|
+
close_pad = ' ' * indent
|
|
150
|
+
grid = [cells[i:i + row_size] for i in range(0, len(cells), row_size)]
|
|
151
|
+
widths = _column_widths(grid)
|
|
152
|
+
rows = []
|
|
153
|
+
for row in grid:
|
|
154
|
+
padded = [c.ljust(widths[j]) for j, c in enumerate(row)]
|
|
155
|
+
rows.append(pad + ', '.join(padded).rstrip())
|
|
156
|
+
return '{\n' + ',\n'.join(rows) + f'\n{close_pad}}}'
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _column_widths(grid: List[List[str]]) -> List[int]:
|
|
160
|
+
"""Max rendered width of each column across all rows (for table alignment)."""
|
|
161
|
+
if not grid:
|
|
162
|
+
return []
|
|
163
|
+
ncols = max(len(row) for row in grid)
|
|
164
|
+
widths = [0] * ncols
|
|
165
|
+
for row in grid:
|
|
166
|
+
for j, cell in enumerate(row):
|
|
167
|
+
if len(cell) > widths[j]:
|
|
168
|
+
widths[j] = len(cell)
|
|
169
|
+
return widths
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _multiline_row_size(field: ArrayField) -> int:
|
|
173
|
+
"""Values-per-row for a widget=multiline scalar array.
|
|
174
|
+
|
|
175
|
+
Uses the last (innermost) dimension of meta.shape as the column count so
|
|
176
|
+
a float[3][4] prints 4 numbers per line. Returns 0 when the field is not
|
|
177
|
+
multiline (caller keeps single-line output).
|
|
178
|
+
"""
|
|
179
|
+
if field.meta.get('widget') != 'multiline':
|
|
180
|
+
return 0
|
|
181
|
+
shape = field.meta.get('shape')
|
|
182
|
+
if isinstance(shape, (list, tuple)) and shape:
|
|
183
|
+
return int(shape[-1])
|
|
184
|
+
return 0
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _renders_as_table(field: ArrayField) -> bool:
|
|
188
|
+
"""Mirror the UI's table-vs-tree decision (ui.renderers).
|
|
189
|
+
|
|
190
|
+
meta.render='tree' forces tree; otherwise a flat struct array is a table.
|
|
191
|
+
Kept in lock-step with TreeRenderer._should_render_as_table so the .c
|
|
192
|
+
layout matches what the user sees.
|
|
193
|
+
"""
|
|
194
|
+
if field.meta.get('render') == 'tree':
|
|
195
|
+
return False
|
|
196
|
+
return is_flat_struct_array(field)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _emit_enum(field: EnumField, value: Any) -> str:
|
|
200
|
+
# Symbol passes through; integer is reverse-looked-up to its symbol.
|
|
201
|
+
if isinstance(value, str) and value in field.items:
|
|
202
|
+
return value
|
|
203
|
+
for symbol, num in field.items.items():
|
|
204
|
+
if num == value:
|
|
205
|
+
return symbol
|
|
206
|
+
if isinstance(value, str) and value:
|
|
207
|
+
return value
|
|
208
|
+
return str(value if value is not None else 0)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _emit_scalar(field: ScalarField, value: Any) -> str:
|
|
212
|
+
if field.ui_type == 'bool':
|
|
213
|
+
return 'true' if value else 'false'
|
|
214
|
+
if field.ui_type == 'str':
|
|
215
|
+
return _c_string(value if value is not None else '')
|
|
216
|
+
return _emit_literal(value if value is not None else 0)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _emit_literal(value: Any) -> str:
|
|
220
|
+
if isinstance(value, bool):
|
|
221
|
+
return 'true' if value else 'false'
|
|
222
|
+
if isinstance(value, str):
|
|
223
|
+
return _c_string(value)
|
|
224
|
+
return str(value)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _c_string(text: str) -> str:
|
|
228
|
+
escaped = (str(text)
|
|
229
|
+
.replace('\\', '\\\\')
|
|
230
|
+
.replace('"', '\\"')
|
|
231
|
+
.replace('\n', '\\n')
|
|
232
|
+
.replace('\t', '\\t'))
|
|
233
|
+
return f'"{escaped}"'
|