@multiplekex/shallot 0.1.0

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.
Files changed (196) hide show
  1. package/dist/core/builder.d.ts +25 -0
  2. package/dist/core/builder.d.ts.map +1 -0
  3. package/dist/core/builder.js +88 -0
  4. package/dist/core/builder.js.map +1 -0
  5. package/dist/core/component.d.ts +29 -0
  6. package/dist/core/component.d.ts.map +1 -0
  7. package/dist/core/component.js +36 -0
  8. package/dist/core/component.js.map +1 -0
  9. package/dist/core/index.d.ts +13 -0
  10. package/dist/core/index.d.ts.map +1 -0
  11. package/dist/core/math.d.ts +32 -0
  12. package/dist/core/math.d.ts.map +1 -0
  13. package/dist/core/math.js +39 -0
  14. package/dist/core/math.js.map +1 -0
  15. package/dist/core/relation.d.ts +16 -0
  16. package/dist/core/relation.d.ts.map +1 -0
  17. package/dist/core/relation.js +32 -0
  18. package/dist/core/relation.js.map +1 -0
  19. package/dist/core/resource.d.ts +9 -0
  20. package/dist/core/resource.d.ts.map +1 -0
  21. package/dist/core/resource.js +12 -0
  22. package/dist/core/resource.js.map +1 -0
  23. package/dist/core/runtime.d.ts +13 -0
  24. package/dist/core/runtime.d.ts.map +1 -0
  25. package/dist/core/runtime.js +118 -0
  26. package/dist/core/runtime.js.map +1 -0
  27. package/dist/core/scheduler.d.ts +47 -0
  28. package/dist/core/scheduler.d.ts.map +1 -0
  29. package/dist/core/scheduler.js +138 -0
  30. package/dist/core/scheduler.js.map +1 -0
  31. package/dist/core/state.d.ts +62 -0
  32. package/dist/core/state.d.ts.map +1 -0
  33. package/dist/core/state.js +185 -0
  34. package/dist/core/state.js.map +1 -0
  35. package/dist/core/strings.d.ts +3 -0
  36. package/dist/core/strings.d.ts.map +1 -0
  37. package/dist/core/strings.js +11 -0
  38. package/dist/core/strings.js.map +1 -0
  39. package/dist/core/types.d.ts +33 -0
  40. package/dist/core/types.d.ts.map +1 -0
  41. package/dist/core/xml.d.ts +42 -0
  42. package/dist/core/xml.d.ts.map +1 -0
  43. package/dist/core/xml.js +349 -0
  44. package/dist/core/xml.js.map +1 -0
  45. package/dist/extras/arrows/index.d.ts +33 -0
  46. package/dist/extras/arrows/index.d.ts.map +1 -0
  47. package/dist/extras/arrows/index.js +288 -0
  48. package/dist/extras/arrows/index.js.map +1 -0
  49. package/dist/extras/index.d.ts +5 -0
  50. package/dist/extras/index.d.ts.map +1 -0
  51. package/dist/extras/index.js +31 -0
  52. package/dist/extras/index.js.map +1 -0
  53. package/dist/extras/lines/index.d.ts +36 -0
  54. package/dist/extras/lines/index.d.ts.map +1 -0
  55. package/dist/extras/lines/index.js +288 -0
  56. package/dist/extras/lines/index.js.map +1 -0
  57. package/dist/extras/orbit/index.d.ts +20 -0
  58. package/dist/extras/orbit/index.d.ts.map +1 -0
  59. package/dist/extras/orbit/index.js +93 -0
  60. package/dist/extras/orbit/index.js.map +1 -0
  61. package/dist/extras/text/index.d.ts +64 -0
  62. package/dist/extras/text/index.d.ts.map +1 -0
  63. package/dist/extras/text/index.js +423 -0
  64. package/dist/extras/text/index.js.map +1 -0
  65. package/dist/index.d.ts +4 -0
  66. package/dist/index.d.ts.map +1 -0
  67. package/dist/index.js +187 -0
  68. package/dist/index.js.map +1 -0
  69. package/dist/rust/transforms/pkg/shallot_transforms.js +107 -0
  70. package/dist/rust/transforms/pkg/shallot_transforms.js.map +1 -0
  71. package/dist/standard/compute/graph.d.ts +37 -0
  72. package/dist/standard/compute/graph.d.ts.map +1 -0
  73. package/dist/standard/compute/graph.js +85 -0
  74. package/dist/standard/compute/graph.js.map +1 -0
  75. package/dist/standard/compute/index.d.ts +21 -0
  76. package/dist/standard/compute/index.d.ts.map +1 -0
  77. package/dist/standard/compute/index.js +81 -0
  78. package/dist/standard/compute/index.js.map +1 -0
  79. package/dist/standard/defaults.d.ts +3 -0
  80. package/dist/standard/defaults.d.ts.map +1 -0
  81. package/dist/standard/defaults.js +18 -0
  82. package/dist/standard/defaults.js.map +1 -0
  83. package/dist/standard/index.d.ts +8 -0
  84. package/dist/standard/index.d.ts.map +1 -0
  85. package/dist/standard/input/index.d.ts +5 -0
  86. package/dist/standard/input/index.d.ts.map +1 -0
  87. package/dist/standard/input/index.js +70 -0
  88. package/dist/standard/input/index.js.map +1 -0
  89. package/dist/standard/loading/index.d.ts +7 -0
  90. package/dist/standard/loading/index.d.ts.map +1 -0
  91. package/dist/standard/loading/index.js +91 -0
  92. package/dist/standard/loading/index.js.map +1 -0
  93. package/dist/standard/render/camera.d.ts +36 -0
  94. package/dist/standard/render/camera.d.ts.map +1 -0
  95. package/dist/standard/render/camera.js +71 -0
  96. package/dist/standard/render/camera.js.map +1 -0
  97. package/dist/standard/render/forward.d.ts +30 -0
  98. package/dist/standard/render/forward.d.ts.map +1 -0
  99. package/dist/standard/render/forward.js +158 -0
  100. package/dist/standard/render/forward.js.map +1 -0
  101. package/dist/standard/render/index.d.ts +22 -0
  102. package/dist/standard/render/index.d.ts.map +1 -0
  103. package/dist/standard/render/index.js +153 -0
  104. package/dist/standard/render/index.js.map +1 -0
  105. package/dist/standard/render/light.d.ts +25 -0
  106. package/dist/standard/render/light.d.ts.map +1 -0
  107. package/dist/standard/render/light.js +48 -0
  108. package/dist/standard/render/light.js.map +1 -0
  109. package/dist/standard/render/mesh/box.d.ts +3 -0
  110. package/dist/standard/render/mesh/box.d.ts.map +1 -0
  111. package/dist/standard/render/mesh/box.js +190 -0
  112. package/dist/standard/render/mesh/box.js.map +1 -0
  113. package/dist/standard/render/mesh/index.d.ts +52 -0
  114. package/dist/standard/render/mesh/index.d.ts.map +1 -0
  115. package/dist/standard/render/mesh/index.js +158 -0
  116. package/dist/standard/render/mesh/index.js.map +1 -0
  117. package/dist/standard/render/mesh/plane.d.ts +3 -0
  118. package/dist/standard/render/mesh/plane.d.ts.map +1 -0
  119. package/dist/standard/render/mesh/plane.js +33 -0
  120. package/dist/standard/render/mesh/plane.js.map +1 -0
  121. package/dist/standard/render/mesh/sphere.d.ts +3 -0
  122. package/dist/standard/render/mesh/sphere.d.ts.map +1 -0
  123. package/dist/standard/render/mesh/sphere.js +25 -0
  124. package/dist/standard/render/mesh/sphere.js.map +1 -0
  125. package/dist/standard/render/postprocess.d.ts +11 -0
  126. package/dist/standard/render/postprocess.d.ts.map +1 -0
  127. package/dist/standard/render/postprocess.js +190 -0
  128. package/dist/standard/render/postprocess.js.map +1 -0
  129. package/dist/standard/render/scene.d.ts +8 -0
  130. package/dist/standard/render/scene.d.ts.map +1 -0
  131. package/dist/standard/render/scene.js +67 -0
  132. package/dist/standard/render/scene.js.map +1 -0
  133. package/dist/standard/transforms/index.d.ts +27 -0
  134. package/dist/standard/transforms/index.d.ts.map +1 -0
  135. package/dist/standard/transforms/index.js +122 -0
  136. package/dist/standard/transforms/index.js.map +1 -0
  137. package/dist/standard/transforms/wasm.d.ts +17 -0
  138. package/dist/standard/transforms/wasm.d.ts.map +1 -0
  139. package/dist/standard/transforms/wasm.js +31 -0
  140. package/dist/standard/transforms/wasm.js.map +1 -0
  141. package/dist/standard/tween/easing.d.ts +5 -0
  142. package/dist/standard/tween/easing.d.ts.map +1 -0
  143. package/dist/standard/tween/easing.js +80 -0
  144. package/dist/standard/tween/easing.js.map +1 -0
  145. package/dist/standard/tween/index.d.ts +4 -0
  146. package/dist/standard/tween/index.d.ts.map +1 -0
  147. package/dist/standard/tween/sequence.d.ts +20 -0
  148. package/dist/standard/tween/sequence.d.ts.map +1 -0
  149. package/dist/standard/tween/sequence.js +95 -0
  150. package/dist/standard/tween/sequence.js.map +1 -0
  151. package/dist/standard/tween/tween.d.ts +28 -0
  152. package/dist/standard/tween/tween.d.ts.map +1 -0
  153. package/dist/standard/tween/tween.js +136 -0
  154. package/dist/standard/tween/tween.js.map +1 -0
  155. package/package.json +63 -0
  156. package/src/core/builder.ts +148 -0
  157. package/src/core/component.ts +71 -0
  158. package/src/core/index.ts +92 -0
  159. package/src/core/math.ts +128 -0
  160. package/src/core/relation.ts +46 -0
  161. package/src/core/resource.ts +18 -0
  162. package/src/core/runtime.ts +185 -0
  163. package/src/core/scheduler.ts +238 -0
  164. package/src/core/state.ts +295 -0
  165. package/src/core/strings.ts +10 -0
  166. package/src/core/types.ts +37 -0
  167. package/src/core/xml.ts +676 -0
  168. package/src/extras/arrows/index.ts +363 -0
  169. package/src/extras/index.ts +4 -0
  170. package/src/extras/lines/index.ts +368 -0
  171. package/src/extras/orbit/index.ts +133 -0
  172. package/src/extras/text/index.ts +641 -0
  173. package/src/index.ts +3 -0
  174. package/src/standard/compute/graph.ts +165 -0
  175. package/src/standard/compute/index.ts +116 -0
  176. package/src/standard/defaults.ts +17 -0
  177. package/src/standard/index.ts +7 -0
  178. package/src/standard/input/index.ts +142 -0
  179. package/src/standard/loading/index.ts +136 -0
  180. package/src/standard/render/camera.ts +87 -0
  181. package/src/standard/render/forward.ts +212 -0
  182. package/src/standard/render/index.ts +175 -0
  183. package/src/standard/render/light.ts +81 -0
  184. package/src/standard/render/mesh/box.ts +20 -0
  185. package/src/standard/render/mesh/index.ts +227 -0
  186. package/src/standard/render/mesh/plane.ts +11 -0
  187. package/src/standard/render/mesh/sphere.ts +40 -0
  188. package/src/standard/render/postprocess.ts +235 -0
  189. package/src/standard/render/scene.ts +116 -0
  190. package/src/standard/transforms/index.ts +184 -0
  191. package/src/standard/transforms/wasm.ts +61 -0
  192. package/src/standard/tween/easing.ts +169 -0
  193. package/src/standard/tween/index.ts +13 -0
  194. package/src/standard/tween/sequence.ts +142 -0
  195. package/src/standard/tween/tween.ts +265 -0
  196. package/src/vite-env.d.ts +6 -0
@@ -0,0 +1,676 @@
1
+ import { XMLParser } from "fast-xml-parser";
2
+ import { addComponent, Pair } from "bitecs";
3
+ import type { State } from "./state";
4
+ import {
5
+ getRegisteredComponent,
6
+ type ParseContext,
7
+ type ComponentData,
8
+ type RegisteredComponent,
9
+ } from "./component";
10
+ import { getRelationDef, ChildOf } from "./relation";
11
+ import { toKebabCase, toCamelCase } from "./strings";
12
+
13
+ function levenshtein(a: string, b: string): number {
14
+ if (a.length === 0) return b.length;
15
+ if (b.length === 0) return a.length;
16
+
17
+ const matrix: number[][] = [];
18
+ for (let i = 0; i <= b.length; i++) {
19
+ matrix[i] = [i];
20
+ }
21
+ for (let j = 0; j <= a.length; j++) {
22
+ matrix[0][j] = j;
23
+ }
24
+
25
+ for (let i = 1; i <= b.length; i++) {
26
+ for (let j = 1; j <= a.length; j++) {
27
+ const cost = a[j - 1] === b[i - 1] ? 0 : 1;
28
+ matrix[i][j] = Math.min(
29
+ matrix[i - 1][j] + 1,
30
+ matrix[i][j - 1] + 1,
31
+ matrix[i - 1][j - 1] + cost
32
+ );
33
+ }
34
+ }
35
+
36
+ return matrix[b.length][a.length];
37
+ }
38
+
39
+ function findClosestMatch(input: string, candidates: string[]): string | null {
40
+ const inputKebab = toKebabCase(input);
41
+
42
+ let bestMatch: string | null = null;
43
+ let bestScore = Infinity;
44
+
45
+ for (const candidate of candidates) {
46
+ const candidateKebab = toKebabCase(candidate);
47
+
48
+ if (inputKebab === candidateKebab) {
49
+ return candidate;
50
+ }
51
+
52
+ if (inputKebab.endsWith(candidateKebab) || inputKebab.endsWith("-" + candidateKebab)) {
53
+ return candidate;
54
+ }
55
+
56
+ const distance = levenshtein(inputKebab, candidateKebab);
57
+ const maxLen = Math.max(inputKebab.length, candidateKebab.length);
58
+ const threshold = Math.ceil(maxLen * 0.5);
59
+
60
+ if (distance < bestScore && distance <= threshold) {
61
+ bestScore = distance;
62
+ bestMatch = candidate;
63
+ }
64
+ }
65
+
66
+ return bestMatch;
67
+ }
68
+
69
+ export interface ParsedElement {
70
+ readonly tag: string;
71
+ readonly attrs: Readonly<Record<string, string>>;
72
+ readonly children: readonly ParsedElement[];
73
+ }
74
+
75
+ export interface EntityRef {
76
+ readonly attrName: string;
77
+ readonly targetName: string;
78
+ }
79
+
80
+ export interface EntityDef {
81
+ readonly id?: string;
82
+ readonly components: readonly ComponentDef[];
83
+ readonly children: readonly EntityDef[];
84
+ readonly entityRefs: readonly EntityRef[];
85
+ }
86
+
87
+ export interface ComponentDef {
88
+ readonly def: RegisteredComponent;
89
+ readonly attrs: Record<string, string>;
90
+ }
91
+
92
+ export interface ParseResult {
93
+ readonly entities: readonly EntityDef[];
94
+ readonly errors: readonly ParseError[];
95
+ readonly warnings: readonly string[];
96
+ }
97
+
98
+ export interface ParseError {
99
+ readonly message: string;
100
+ readonly path?: string;
101
+ }
102
+
103
+ export function parseXml(xml: string): ParseResult {
104
+ const errors: ParseError[] = [];
105
+ const warnings: string[] = [];
106
+
107
+ const parser = new XMLParser({
108
+ ignoreAttributes: false,
109
+ attributeNamePrefix: "",
110
+ preserveOrder: true,
111
+ trimValues: true,
112
+ allowBooleanAttributes: true,
113
+ });
114
+
115
+ let raw: unknown;
116
+ try {
117
+ raw = parser.parse(xml, { allowBooleanAttributes: true });
118
+ } catch (e) {
119
+ return {
120
+ entities: [],
121
+ errors: [{ message: `xml parse error: ${(e as Error).message}` }],
122
+ warnings: [],
123
+ };
124
+ }
125
+
126
+ const elements = normalizeRaw(raw);
127
+ const entities: EntityDef[] = [];
128
+
129
+ for (const el of elements) {
130
+ if (el.tag === "scene") {
131
+ for (const child of el.children) {
132
+ const result = parseEntityElement(child, errors, warnings);
133
+ if (result) entities.push(result);
134
+ }
135
+ } else if (el.tag === "a") {
136
+ const result = parseEntityElement(el, errors, warnings);
137
+ if (result) entities.push(result);
138
+ } else if (el.tag.toLowerCase() === "scene" || el.tag.toLowerCase() === "world") {
139
+ errors.push({ message: `Invalid tag "${el.tag}". Use lowercase <scene>` });
140
+ }
141
+ }
142
+
143
+ return { entities, errors, warnings };
144
+ }
145
+
146
+ function isEntityRef(value: string): boolean {
147
+ return value.startsWith("@") && value.length > 1;
148
+ }
149
+
150
+ function parseEntityElement(
151
+ el: ParsedElement,
152
+ errors: ParseError[],
153
+ warnings: string[]
154
+ ): EntityDef | null {
155
+ if (el.tag !== "a") {
156
+ if (el.tag.toLowerCase() === "a") {
157
+ errors.push({ message: `Invalid tag "${el.tag}". Use lowercase <a>` });
158
+ }
159
+ return null;
160
+ }
161
+
162
+ const components: ComponentDef[] = [];
163
+ const children: EntityDef[] = [];
164
+ const entityRefs: EntityRef[] = [];
165
+ const shorthands: Array<{ name: string; value: string }> = [];
166
+ let entityId: string | undefined;
167
+
168
+ for (const [attrName, attrValue] of Object.entries(el.attrs)) {
169
+ if (attrName === "id") {
170
+ entityId = attrValue;
171
+ continue;
172
+ }
173
+
174
+ if (typeof attrValue === "string" && isEntityRef(attrValue)) {
175
+ entityRefs.push({
176
+ attrName,
177
+ targetName: attrValue.slice(1),
178
+ });
179
+ continue;
180
+ }
181
+
182
+ const registered = getRegisteredComponent(attrName);
183
+ if (registered) {
184
+ const attrs: Record<string, string> = {};
185
+ if (typeof attrValue === "string" && attrValue !== "") {
186
+ attrs["_value"] = attrValue;
187
+ }
188
+ components.push({ def: registered, attrs });
189
+ continue;
190
+ }
191
+
192
+ if (typeof attrValue === "string" && attrValue !== "") {
193
+ shorthands.push({ name: attrName, value: attrValue });
194
+ }
195
+ }
196
+
197
+ for (const { name, value } of shorthands) {
198
+ const camel = toCamelCase(name);
199
+ let matched = false;
200
+
201
+ for (const comp of components) {
202
+ const component = comp.def.component;
203
+ const hasField = camel in component || `${camel}X` in component;
204
+
205
+ if (hasField) {
206
+ matched = true;
207
+ if (!comp.attrs._value) {
208
+ comp.attrs[name] = value;
209
+ }
210
+ }
211
+ }
212
+
213
+ if (!matched) {
214
+ const id = entityId ? ` (${entityId})` : "";
215
+ warnings.push(`shorthand "${name}" matches no declared component${id}`);
216
+ }
217
+ }
218
+
219
+ for (const child of el.children) {
220
+ if (child.tag === "a") {
221
+ const childEntity = parseEntityElement(child, errors, warnings);
222
+ if (childEntity) children.push(childEntity);
223
+ } else if (child.tag.toLowerCase() === "a") {
224
+ errors.push({ message: `Invalid tag "${child.tag}". Use lowercase <a>` });
225
+ } else {
226
+ errors.push({ message: `Only <a> children allowed, found <${child.tag}>` });
227
+ }
228
+ }
229
+
230
+ return { id: entityId, components, children, entityRefs };
231
+ }
232
+
233
+ interface RawElement {
234
+ [key: string]: unknown;
235
+ ":@"?: Record<string, string>;
236
+ }
237
+
238
+ function normalizeRaw(raw: unknown): ParsedElement[] {
239
+ if (!Array.isArray(raw)) return [];
240
+
241
+ const result: ParsedElement[] = [];
242
+
243
+ for (const item of raw as RawElement[]) {
244
+ if (typeof item !== "object" || item === null) continue;
245
+
246
+ for (const [key, value] of Object.entries(item)) {
247
+ if (key === ":@") continue;
248
+
249
+ const rawAttrs = item[":@"] ?? {};
250
+ const attrs: Record<string, string> = {};
251
+
252
+ for (const [attrKey, attrVal] of Object.entries(rawAttrs)) {
253
+ if (typeof attrVal === "string") {
254
+ attrs[attrKey] = attrVal;
255
+ } else if (attrVal === true || attrVal === "") {
256
+ attrs[attrKey] = "";
257
+ }
258
+ }
259
+
260
+ const children = normalizeRaw(value as unknown);
261
+
262
+ result.push({
263
+ tag: key,
264
+ attrs,
265
+ children,
266
+ });
267
+ }
268
+ }
269
+
270
+ return result;
271
+ }
272
+
273
+ export interface LoadResult {
274
+ readonly entities: Map<string, number>;
275
+ readonly roots: readonly number[];
276
+ readonly errors: readonly ParseError[];
277
+ }
278
+
279
+ export type PostLoadHook = (state: State, context: ParseContext) => void;
280
+
281
+ const postLoadHooks: PostLoadHook[] = [];
282
+
283
+ export function registerPostLoadHook(hook: PostLoadHook): void {
284
+ postLoadHooks.push(hook);
285
+ }
286
+
287
+ export function unregisterPostLoadHook(hook: PostLoadHook): void {
288
+ const idx = postLoadHooks.indexOf(hook);
289
+ if (idx !== -1) postLoadHooks.splice(idx, 1);
290
+ }
291
+
292
+ class LoadParseContext implements ParseContext {
293
+ private readonly nameToEntity = new Map<string, number>();
294
+ private _currentEid = 0;
295
+
296
+ get currentEid(): number {
297
+ return this._currentEid;
298
+ }
299
+
300
+ setCurrentEid(eid: number): void {
301
+ this._currentEid = eid;
302
+ }
303
+
304
+ getEntityByName(name: string): number | null {
305
+ return this.nameToEntity.get(name) ?? null;
306
+ }
307
+
308
+ setName(name: string, eid: number): void {
309
+ this.nameToEntity.set(name, eid);
310
+ }
311
+
312
+ getEntityMap(): Map<string, number> {
313
+ return new Map(this.nameToEntity);
314
+ }
315
+ }
316
+
317
+ export function loadScene(state: State, xml: string): LoadResult {
318
+ const parseResult = parseXml(xml);
319
+ return instantiateScene(state, parseResult.entities, parseResult.errors);
320
+ }
321
+
322
+ export async function loadSceneFile(
323
+ state: State,
324
+ path: string,
325
+ readFile: (path: string) => Promise<string>
326
+ ): Promise<LoadResult> {
327
+ const xml = await readFile(path);
328
+ return loadScene(state, xml);
329
+ }
330
+
331
+ interface QueuedEntity {
332
+ readonly def: EntityDef;
333
+ readonly eid: number;
334
+ readonly parent?: number;
335
+ }
336
+
337
+ function instantiateScene(
338
+ state: State,
339
+ entityDefs: readonly EntityDef[],
340
+ parseErrors: readonly ParseError[]
341
+ ): LoadResult {
342
+ const context = new LoadParseContext();
343
+ const errors: ParseError[] = [...parseErrors];
344
+ const queue: QueuedEntity[] = [];
345
+ const roots: number[] = [];
346
+
347
+ for (const entityDef of entityDefs) {
348
+ const eid = createEntityTree(state, entityDef, context, undefined, queue);
349
+ roots.push(eid);
350
+ }
351
+
352
+ for (const { def, eid, parent } of queue) {
353
+ if (parent !== undefined) {
354
+ addComponent(state.world, eid, Pair(ChildOf.relation, parent));
355
+ }
356
+
357
+ for (const ref of def.entityRefs) {
358
+ applyRelation(state, eid, ref, context, errors);
359
+ }
360
+
361
+ for (const compDef of def.components) {
362
+ applyComponent(state, eid, compDef, context, errors);
363
+ }
364
+ }
365
+
366
+ for (const hook of postLoadHooks) {
367
+ hook(state, context);
368
+ }
369
+
370
+ return {
371
+ entities: context.getEntityMap(),
372
+ roots,
373
+ errors,
374
+ };
375
+ }
376
+
377
+ function createEntityTree(
378
+ state: State,
379
+ def: EntityDef,
380
+ context: LoadParseContext,
381
+ parent: number | undefined,
382
+ queue: QueuedEntity[]
383
+ ): number {
384
+ const eid = state.addEntity();
385
+
386
+ if (def.id) {
387
+ context.setName(def.id, eid);
388
+ }
389
+
390
+ queue.push({ def, eid, parent });
391
+
392
+ for (const childDef of def.children) {
393
+ createEntityTree(state, childDef, context, eid, queue);
394
+ }
395
+
396
+ return eid;
397
+ }
398
+
399
+ function applyRelation(
400
+ state: State,
401
+ eid: number,
402
+ ref: EntityRef,
403
+ context: LoadParseContext,
404
+ errors: ParseError[]
405
+ ): void {
406
+ const relationDef = getRelationDef(ref.attrName);
407
+ if (!relationDef) {
408
+ errors.push({ message: `Unknown relation: "${ref.attrName}"` });
409
+ return;
410
+ }
411
+
412
+ const targetEid = context.getEntityByName(ref.targetName);
413
+ if (targetEid === null) {
414
+ errors.push({ message: `Unknown entity: "@${ref.targetName}"` });
415
+ return;
416
+ }
417
+
418
+ state.addRelation(eid, relationDef, targetEid);
419
+ }
420
+
421
+ function applyComponent(
422
+ state: State,
423
+ eid: number,
424
+ compDef: ComponentDef,
425
+ context: LoadParseContext,
426
+ errors: ParseError[]
427
+ ): void {
428
+ const { def, attrs } = compDef;
429
+ const { component, name, traits } = def;
430
+
431
+ state.addComponent(eid, component as never);
432
+
433
+ const defaults = traits?.defaults?.() ?? {};
434
+ for (const [field, value] of Object.entries(defaults)) {
435
+ setFieldValue(component, field, eid, value as number);
436
+ }
437
+
438
+ let values: Record<string, number>;
439
+
440
+ context.setCurrentEid(eid);
441
+ if (traits?.adapter) {
442
+ values = traits.adapter(attrs, state, context) as Record<string, number>;
443
+ } else {
444
+ const result = parseAttrs(def, attrs);
445
+ values = result.values;
446
+ for (const err of result.errors) {
447
+ errors.push({ message: `<${name}> ${err}` });
448
+ }
449
+ }
450
+
451
+ for (const [field, value] of Object.entries(values)) {
452
+ setFieldValue(component, field, eid, value);
453
+ }
454
+ }
455
+
456
+ function parseAttrs(
457
+ def: RegisteredComponent,
458
+ attrs: Record<string, string>
459
+ ): { values: Record<string, number>; errors: string[] } {
460
+ const allValues: Record<string, number> = {};
461
+ const allErrors: string[] = [];
462
+
463
+ if (attrs._value) {
464
+ if (isCSSAttrSyntax(attrs._value)) {
465
+ const result = parsePropertyString(def.name, attrs._value, def.component);
466
+ Object.assign(allValues, result.values);
467
+ allErrors.push(...result.errors);
468
+ }
469
+ }
470
+
471
+ for (const [attrName, attrValue] of Object.entries(attrs)) {
472
+ if (attrName === "_value") continue;
473
+ if (!attrValue) continue;
474
+
475
+ if (isCSSAttrSyntax(attrValue)) {
476
+ const result = parsePropertyString(def.name, attrValue, def.component);
477
+ Object.assign(allValues, result.values);
478
+ allErrors.push(...result.errors);
479
+ } else {
480
+ const result = parsePropertyString(
481
+ def.name,
482
+ `${attrName}: ${attrValue}`,
483
+ def.component
484
+ );
485
+ Object.assign(allValues, result.values);
486
+ allErrors.push(...result.errors);
487
+ }
488
+ }
489
+
490
+ return { values: allValues, errors: allErrors };
491
+ }
492
+
493
+ function setFieldValue(component: ComponentData, field: string, eid: number, value: number): void {
494
+ const arr = component[field];
495
+ if (arr != null && (ArrayBuffer.isView(arr) || Array.isArray(arr))) {
496
+ arr[eid] = value;
497
+ }
498
+ }
499
+
500
+ interface PropertyParseResult {
501
+ readonly values: Record<string, number>;
502
+ readonly errors: readonly string[];
503
+ }
504
+
505
+ function detectVec2(component: ComponentData, base: string): boolean {
506
+ return `${base}X` in component && `${base}Y` in component;
507
+ }
508
+
509
+ function detectVec3(component: ComponentData, base: string): boolean {
510
+ return detectVec2(component, base) && `${base}Z` in component;
511
+ }
512
+
513
+ function detectVec4(component: ComponentData, base: string): boolean {
514
+ return detectVec3(component, base) && `${base}W` in component;
515
+ }
516
+
517
+ function parseNumber(value: string): number | null {
518
+ value = value.trim();
519
+
520
+ if (value.startsWith("0x") || value.startsWith("0X")) {
521
+ return parseInt(value, 16);
522
+ }
523
+
524
+ if (value.startsWith("#")) {
525
+ return parseInt(value.slice(1), 16);
526
+ }
527
+
528
+ if (value === "true") return 1;
529
+ if (value === "false") return 0;
530
+
531
+ const num = parseFloat(value);
532
+ return isNaN(num) ? null : num;
533
+ }
534
+
535
+ function parseValues(valueStr: string): (number | null)[] {
536
+ const result: (number | null)[] = [];
537
+ const trimmed = valueStr.trim();
538
+ let start = 0;
539
+ for (let i = 0; i <= trimmed.length; i++) {
540
+ const isWhitespace = i < trimmed.length && /\s/.test(trimmed[i]);
541
+ const isEnd = i === trimmed.length;
542
+ if (isWhitespace || isEnd) {
543
+ if (start < i) {
544
+ result.push(parseNumber(trimmed.slice(start, i)));
545
+ }
546
+ start = i + 1;
547
+ }
548
+ }
549
+ return result;
550
+ }
551
+
552
+ function splitProperties(str: string): string[] {
553
+ const result: string[] = [];
554
+ let start = 0;
555
+ for (let i = 0; i <= str.length; i++) {
556
+ if (i === str.length || str[i] === ";") {
557
+ const prop = str.slice(start, i).trim();
558
+ if (prop) result.push(prop);
559
+ start = i + 1;
560
+ }
561
+ }
562
+ return result;
563
+ }
564
+
565
+ function parsePropertyString(
566
+ componentName: string,
567
+ propertyString: string,
568
+ component: ComponentData
569
+ ): PropertyParseResult {
570
+ const values: Record<string, number> = {};
571
+ const errors: string[] = [];
572
+
573
+ const properties = splitProperties(propertyString);
574
+
575
+ for (const prop of properties) {
576
+ const colonIdx = prop.indexOf(":");
577
+ if (colonIdx === -1) {
578
+ errors.push(`Invalid syntax: "${prop}" (expected "field: value")`);
579
+ continue;
580
+ }
581
+
582
+ const rawName = prop.slice(0, colonIdx).trim();
583
+ const valueStr = prop.slice(colonIdx + 1).trim();
584
+
585
+ if (!rawName || !valueStr) {
586
+ errors.push(`Invalid syntax: "${prop}" (empty field or value)`);
587
+ continue;
588
+ }
589
+
590
+ const name = toCamelCase(rawName);
591
+ const parsed = parseValues(valueStr);
592
+
593
+ if (parsed.some((v) => v === null)) {
594
+ errors.push(`Invalid number in "${prop}"`);
595
+ continue;
596
+ }
597
+
598
+ const nums = parsed as number[];
599
+
600
+ if (detectVec4(component, name)) {
601
+ if (nums.length === 4) {
602
+ values[`${name}X`] = nums[0];
603
+ values[`${name}Y`] = nums[1];
604
+ values[`${name}Z`] = nums[2];
605
+ values[`${name}W`] = nums[3];
606
+ } else if (nums.length === 1) {
607
+ values[`${name}X`] = nums[0];
608
+ values[`${name}Y`] = nums[0];
609
+ values[`${name}Z`] = nums[0];
610
+ values[`${name}W`] = nums[0];
611
+ } else {
612
+ errors.push(
613
+ `${componentName}.${rawName}: expected 1 or 4 values, got ${nums.length}`
614
+ );
615
+ }
616
+ continue;
617
+ }
618
+
619
+ if (detectVec3(component, name)) {
620
+ if (nums.length === 3) {
621
+ values[`${name}X`] = nums[0];
622
+ values[`${name}Y`] = nums[1];
623
+ values[`${name}Z`] = nums[2];
624
+ } else if (nums.length === 1) {
625
+ values[`${name}X`] = nums[0];
626
+ values[`${name}Y`] = nums[0];
627
+ values[`${name}Z`] = nums[0];
628
+ } else {
629
+ errors.push(
630
+ `${componentName}.${rawName}: expected 1 or 3 values, got ${nums.length}`
631
+ );
632
+ }
633
+ continue;
634
+ }
635
+
636
+ if (detectVec2(component, name)) {
637
+ if (nums.length === 2) {
638
+ values[`${name}X`] = nums[0];
639
+ values[`${name}Y`] = nums[1];
640
+ } else if (nums.length === 1) {
641
+ values[`${name}X`] = nums[0];
642
+ values[`${name}Y`] = nums[0];
643
+ } else {
644
+ errors.push(
645
+ `${componentName}.${rawName}: expected 1 or 2 values, got ${nums.length}`
646
+ );
647
+ }
648
+ continue;
649
+ }
650
+
651
+ if (name in component) {
652
+ if (nums.length === 1) {
653
+ values[name] = nums[0];
654
+ } else {
655
+ errors.push(`${componentName}.${rawName}: expected 1 value, got ${nums.length}`);
656
+ }
657
+ continue;
658
+ }
659
+
660
+ const fieldNames = Object.keys(component);
661
+ const suggestion = findClosestMatch(rawName, fieldNames);
662
+ if (suggestion) {
663
+ errors.push(
664
+ `${componentName}: unknown field "${rawName}", did you mean "${toKebabCase(suggestion)}"?`
665
+ );
666
+ } else {
667
+ errors.push(`${componentName}: unknown field "${rawName}"`);
668
+ }
669
+ }
670
+
671
+ return { values, errors };
672
+ }
673
+
674
+ function isCSSAttrSyntax(value: string): boolean {
675
+ return value.includes(":") && (value.includes(";") || /^[\w-]+\s*:/.test(value));
676
+ }