@multiplekex/shallot 0.2.5 → 0.3.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 (41) hide show
  1. package/package.json +1 -1
  2. package/src/core/component.ts +1 -1
  3. package/src/core/index.ts +1 -13
  4. package/src/core/math.ts +186 -0
  5. package/src/core/state.ts +1 -1
  6. package/src/core/xml.ts +56 -41
  7. package/src/extras/orbit/index.ts +1 -1
  8. package/src/extras/text/index.ts +10 -65
  9. package/src/extras/{water.ts → water/index.ts} +59 -4
  10. package/src/standard/raster/batch.ts +149 -0
  11. package/src/standard/raster/forward.ts +832 -0
  12. package/src/standard/raster/index.ts +146 -472
  13. package/src/standard/raster/shadow.ts +408 -0
  14. package/src/standard/raytracing/bvh/blas.ts +335 -87
  15. package/src/standard/raytracing/bvh/radix.ts +225 -228
  16. package/src/standard/raytracing/bvh/refit.ts +711 -0
  17. package/src/standard/raytracing/bvh/structs.ts +0 -55
  18. package/src/standard/raytracing/bvh/tlas.ts +153 -141
  19. package/src/standard/raytracing/bvh/traverse.ts +72 -64
  20. package/src/standard/raytracing/index.ts +233 -204
  21. package/src/standard/raytracing/instance.ts +30 -18
  22. package/src/standard/raytracing/ray.ts +1 -1
  23. package/src/standard/raytracing/shaders.ts +23 -40
  24. package/src/standard/render/camera.ts +10 -28
  25. package/src/standard/render/data.ts +1 -1
  26. package/src/standard/render/index.ts +68 -12
  27. package/src/standard/render/light.ts +2 -2
  28. package/src/standard/render/mesh.ts +404 -0
  29. package/src/standard/render/overlay.ts +5 -2
  30. package/src/standard/render/postprocess.ts +263 -267
  31. package/src/standard/render/surface/index.ts +81 -12
  32. package/src/standard/render/surface/shaders.ts +265 -11
  33. package/src/standard/render/surface/structs.ts +10 -0
  34. package/src/standard/tween/tween.ts +44 -115
  35. package/src/standard/render/mesh/box.ts +0 -20
  36. package/src/standard/render/mesh/index.ts +0 -315
  37. package/src/standard/render/mesh/plane.ts +0 -11
  38. package/src/standard/render/mesh/sphere.ts +0 -40
  39. package/src/standard/render/mesh/unified.ts +0 -96
  40. package/src/standard/render/surface/compile.ts +0 -65
  41. package/src/standard/render/surface/noise.ts +0 -58
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@multiplekex/shallot",
3
- "version": "0.2.5",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -11,7 +11,7 @@ export interface FieldAccessor {
11
11
 
12
12
  export interface ComponentTraits {
13
13
  defaults?: () => Record<string, number>;
14
- adapter?: (attrs: Record<string, string>, eid: number) => Record<string, number>;
14
+ parse?: Record<string, (value: string) => number | undefined>;
15
15
  }
16
16
 
17
17
  const traitsMap = new WeakMap<ComponentLike, ComponentTraits>();
package/src/core/index.ts CHANGED
@@ -79,16 +79,4 @@ export { toKebabCase, toCamelCase } from "./strings";
79
79
 
80
80
  export { initRuntime, getRuntime, resetRuntime, type Runtime, type RuntimeTarget } from "./runtime";
81
81
 
82
- export {
83
- parse,
84
- serialize,
85
- load,
86
- registerPostLoadHook,
87
- unregisterPostLoadHook,
88
- type Node,
89
- type Attr,
90
- type Ref,
91
- type ParseError,
92
- type PostLoadHook,
93
- type PostLoadContext,
94
- } from "./xml";
82
+ export { parse, serialize, load, type Node, type Attr, type Ref, type ParseError } from "./xml";
package/src/core/math.ts CHANGED
@@ -284,6 +284,192 @@ export function extractFrustumPlanes(viewProj: Float32Array): Float32Array {
284
284
  return planes;
285
285
  }
286
286
 
287
+ export function lookAtMatrix(
288
+ eyeX: number,
289
+ eyeY: number,
290
+ eyeZ: number,
291
+ targetX: number,
292
+ targetY: number,
293
+ targetZ: number,
294
+ upX = 0,
295
+ upY = 1,
296
+ upZ = 0
297
+ ): Float32Array {
298
+ let zx = eyeX - targetX;
299
+ let zy = eyeY - targetY;
300
+ let zz = eyeZ - targetZ;
301
+ let zLen = Math.sqrt(zx * zx + zy * zy + zz * zz);
302
+
303
+ if (zLen < 1e-6) {
304
+ zx = 0;
305
+ zy = 0;
306
+ zz = 1;
307
+ } else {
308
+ zLen = 1 / zLen;
309
+ zx *= zLen;
310
+ zy *= zLen;
311
+ zz *= zLen;
312
+ }
313
+
314
+ let xx = upY * zz - upZ * zy;
315
+ let xy = upZ * zx - upX * zz;
316
+ let xz = upX * zy - upY * zx;
317
+ let xLen = Math.sqrt(xx * xx + xy * xy + xz * xz);
318
+
319
+ if (xLen < 1e-6) {
320
+ if (Math.abs(zy) > 0.9) {
321
+ xx = 1;
322
+ xy = 0;
323
+ xz = 0;
324
+ } else {
325
+ xx = -zz;
326
+ xy = 0;
327
+ xz = zx;
328
+ }
329
+ xLen = Math.sqrt(xx * xx + xy * xy + xz * xz);
330
+ }
331
+
332
+ xLen = 1 / xLen;
333
+ xx *= xLen;
334
+ xy *= xLen;
335
+ xz *= xLen;
336
+
337
+ const yx = zy * xz - zz * xy;
338
+ const yy = zz * xx - zx * xz;
339
+ const yz = zx * xy - zy * xx;
340
+
341
+ const tx = -(xx * eyeX + xy * eyeY + xz * eyeZ);
342
+ const ty = -(yx * eyeX + yy * eyeY + yz * eyeZ);
343
+ const tz = -(zx * eyeX + zy * eyeY + zz * eyeZ);
344
+
345
+ return new Float32Array([xx, yx, zx, 0, xy, yy, zy, 0, xz, yz, zz, 0, tx, ty, tz, 1]);
346
+ }
347
+
348
+ export function orthographicBounds(
349
+ left: number,
350
+ right: number,
351
+ bottom: number,
352
+ top: number,
353
+ near: number,
354
+ far: number
355
+ ): Float32Array {
356
+ const lr = 1 / (right - left);
357
+ const bt = 1 / (top - bottom);
358
+ const nf = 1 / (near - far);
359
+
360
+ return new Float32Array([
361
+ 2 * lr,
362
+ 0,
363
+ 0,
364
+ 0,
365
+ 0,
366
+ 2 * bt,
367
+ 0,
368
+ 0,
369
+ 0,
370
+ 0,
371
+ nf,
372
+ 0,
373
+ -(right + left) * lr,
374
+ -(top + bottom) * bt,
375
+ near * nf,
376
+ 1,
377
+ ]);
378
+ }
379
+
380
+ export function extractFrustumCorners(
381
+ invViewProj: Float32Array,
382
+ nearZ: number,
383
+ farZ: number
384
+ ): Float32Array {
385
+ const corners = new Float32Array(24);
386
+ const ndcCorners = [
387
+ [-1, -1, nearZ],
388
+ [1, -1, nearZ],
389
+ [-1, 1, nearZ],
390
+ [1, 1, nearZ],
391
+ [-1, -1, farZ],
392
+ [1, -1, farZ],
393
+ [-1, 1, farZ],
394
+ [1, 1, farZ],
395
+ ];
396
+
397
+ for (let i = 0; i < 8; i++) {
398
+ const [nx, ny, nz] = ndcCorners[i];
399
+ const m = invViewProj;
400
+
401
+ const wx = m[0] * nx + m[4] * ny + m[8] * nz + m[12];
402
+ const wy = m[1] * nx + m[5] * ny + m[9] * nz + m[13];
403
+ const wz = m[2] * nx + m[6] * ny + m[10] * nz + m[14];
404
+ const ww = m[3] * nx + m[7] * ny + m[11] * nz + m[15];
405
+
406
+ corners[i * 3] = wx / ww;
407
+ corners[i * 3 + 1] = wy / ww;
408
+ corners[i * 3 + 2] = wz / ww;
409
+ }
410
+
411
+ return corners;
412
+ }
413
+
414
+ export function invertMatrix(m: Float32Array): Float32Array {
415
+ const out = new Float32Array(16);
416
+
417
+ const a00 = m[0],
418
+ a01 = m[1],
419
+ a02 = m[2],
420
+ a03 = m[3];
421
+ const a10 = m[4],
422
+ a11 = m[5],
423
+ a12 = m[6],
424
+ a13 = m[7];
425
+ const a20 = m[8],
426
+ a21 = m[9],
427
+ a22 = m[10],
428
+ a23 = m[11];
429
+ const a30 = m[12],
430
+ a31 = m[13],
431
+ a32 = m[14],
432
+ a33 = m[15];
433
+
434
+ const b00 = a00 * a11 - a01 * a10;
435
+ const b01 = a00 * a12 - a02 * a10;
436
+ const b02 = a00 * a13 - a03 * a10;
437
+ const b03 = a01 * a12 - a02 * a11;
438
+ const b04 = a01 * a13 - a03 * a11;
439
+ const b05 = a02 * a13 - a03 * a12;
440
+ const b06 = a20 * a31 - a21 * a30;
441
+ const b07 = a20 * a32 - a22 * a30;
442
+ const b08 = a20 * a33 - a23 * a30;
443
+ const b09 = a21 * a32 - a22 * a31;
444
+ const b10 = a21 * a33 - a23 * a31;
445
+ const b11 = a22 * a33 - a23 * a32;
446
+
447
+ let det = b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06;
448
+ if (Math.abs(det) < 1e-10) {
449
+ return out;
450
+ }
451
+ det = 1 / det;
452
+
453
+ out[0] = (a11 * b11 - a12 * b10 + a13 * b09) * det;
454
+ out[1] = (a02 * b10 - a01 * b11 - a03 * b09) * det;
455
+ out[2] = (a31 * b05 - a32 * b04 + a33 * b03) * det;
456
+ out[3] = (a22 * b04 - a21 * b05 - a23 * b03) * det;
457
+ out[4] = (a12 * b08 - a10 * b11 - a13 * b07) * det;
458
+ out[5] = (a00 * b11 - a02 * b08 + a03 * b07) * det;
459
+ out[6] = (a32 * b02 - a30 * b05 - a33 * b01) * det;
460
+ out[7] = (a20 * b05 - a22 * b02 + a23 * b01) * det;
461
+ out[8] = (a10 * b10 - a11 * b08 + a13 * b06) * det;
462
+ out[9] = (a01 * b08 - a00 * b10 - a03 * b06) * det;
463
+ out[10] = (a30 * b04 - a31 * b02 + a33 * b00) * det;
464
+ out[11] = (a21 * b02 - a20 * b04 - a23 * b00) * det;
465
+ out[12] = (a11 * b07 - a10 * b09 - a12 * b06) * det;
466
+ out[13] = (a00 * b09 - a01 * b07 + a02 * b06) * det;
467
+ out[14] = (a31 * b01 - a30 * b03 - a32 * b00) * det;
468
+ out[15] = (a20 * b03 - a21 * b01 + a22 * b00) * det;
469
+
470
+ return out;
471
+ }
472
+
287
473
  export function lookAt(
288
474
  eyeX: number,
289
475
  eyeY: number,
package/src/core/state.ts CHANGED
@@ -20,7 +20,7 @@ import type { RelationDef } from "./relation";
20
20
  import type { Plugin, StateBuilder } from "./builder";
21
21
  import { initRuntime, type Runtime } from "./runtime";
22
22
  import { registerComponent, getTraits, type ComponentData } from "./component";
23
- import { type ResourceKey } from "./resource";
23
+ import type { ResourceKey } from "./resource";
24
24
 
25
25
  export const MAX_ENTITIES = 65536;
26
26
 
package/src/core/xml.ts CHANGED
@@ -1,6 +1,11 @@
1
1
  import { addComponent, Pair } from "bitecs";
2
2
  import type { State } from "./state";
3
- import { getRegisteredComponent, type ComponentLike, type RegisteredComponent } from "./component";
3
+ import {
4
+ getRegisteredComponent,
5
+ getTraits,
6
+ type ComponentLike,
7
+ type RegisteredComponent,
8
+ } from "./component";
4
9
  import { getRelationDef, ChildOf } from "./relation";
5
10
  import { toKebabCase, toCamelCase } from "./strings";
6
11
 
@@ -136,7 +141,7 @@ function tokenize(xml: string): Token[] {
136
141
 
137
142
  function parseTagAttrs(tag: string): Record<string, string> {
138
143
  const attrs: Record<string, string> = {};
139
- const attrRegex = /([^\s=<>\/]+)(?:\s*=\s*"([^"]*)")?/g;
144
+ const attrRegex = /([^\s=<>/]+)(?:\s*=\s*"([^"]*)")?/g;
140
145
  const inner = tag.replace(/^<\s*\w+/, "").replace(/\/?>$/, "");
141
146
  let match: RegExpExecArray | null;
142
147
 
@@ -312,23 +317,6 @@ function parseNodeFromTokens(
312
317
  };
313
318
  }
314
319
 
315
- export interface PostLoadContext {
316
- getEntityByName(name: string): number | null;
317
- }
318
-
319
- export type PostLoadHook = (state: State, context: PostLoadContext) => void;
320
-
321
- const postLoadHooks: PostLoadHook[] = [];
322
-
323
- export function registerPostLoadHook(hook: PostLoadHook): void {
324
- postLoadHooks.push(hook);
325
- }
326
-
327
- export function unregisterPostLoadHook(hook: PostLoadHook): void {
328
- const idx = postLoadHooks.indexOf(hook);
329
- if (idx !== -1) postLoadHooks.splice(idx, 1);
330
- }
331
-
332
320
  interface QueuedEntity {
333
321
  node: Node;
334
322
  eid: number;
@@ -376,13 +364,6 @@ export function load(nodes: Node[], state: State): Map<Node, number> {
376
364
  setFieldValue(ref.component, ref.field, ref.eid, targetEid);
377
365
  }
378
366
 
379
- const context: PostLoadContext = {
380
- getEntityByName: (name) => nameToEntity.get(name) ?? null,
381
- };
382
- for (const hook of postLoadHooks) {
383
- hook(state, context);
384
- }
385
-
386
367
  if (errors.length > 0) {
387
368
  throw new Error(errors.map((e) => e.message).join("\n"));
388
369
  }
@@ -487,24 +468,22 @@ function applyComponent(
487
468
  props["_value"] = value;
488
469
  }
489
470
 
490
- let values: Record<string, number>;
491
- let entityRefs: { field: string; targetName: string }[] = [];
492
-
493
- if (traits?.adapter) {
494
- values = traits.adapter(props, eid);
495
- } else {
496
- const result = parseAttrs(def, props);
497
- values = result.values;
498
- entityRefs = result.entityRefs;
499
- for (const err of result.errors) {
500
- errors.push({ message: `<${name}> ${err}` });
501
- }
471
+ const result = parseAttrs(def, props);
472
+ const values = result.values;
473
+ const strings = result.strings;
474
+ const entityRefs = result.entityRefs;
475
+ for (const err of result.errors) {
476
+ errors.push({ message: `<${name}> ${err}` });
502
477
  }
503
478
 
504
479
  for (const [field, val] of Object.entries(values)) {
505
480
  setFieldValue(component, field, eid, val);
506
481
  }
507
482
 
483
+ for (const [field, val] of Object.entries(strings)) {
484
+ setString(component, field, eid, val);
485
+ }
486
+
508
487
  for (const ref of entityRefs) {
509
488
  pendingFieldRefs.push({
510
489
  eid,
@@ -520,10 +499,12 @@ function parseAttrs(
520
499
  props: Record<string, string>
521
500
  ): {
522
501
  values: Record<string, number>;
502
+ strings: Record<string, string>;
523
503
  entityRefs: { field: string; targetName: string }[];
524
504
  errors: string[];
525
505
  } {
526
506
  const allValues: Record<string, number> = {};
507
+ const allStrings: Record<string, string> = {};
527
508
  const allEntityRefs: { field: string; targetName: string }[] = [];
528
509
  const allErrors: string[] = [];
529
510
 
@@ -531,6 +512,7 @@ function parseAttrs(
531
512
  if (isCSSAttrSyntax(props._value)) {
532
513
  const result = parsePropertyString(def.name, props._value, def.component);
533
514
  Object.assign(allValues, result.values);
515
+ Object.assign(allStrings, result.strings);
534
516
  allEntityRefs.push(...result.entityRefs);
535
517
  allErrors.push(...result.errors);
536
518
  }
@@ -543,6 +525,7 @@ function parseAttrs(
543
525
  if (isCSSAttrSyntax(propValue)) {
544
526
  const result = parsePropertyString(def.name, propValue, def.component);
545
527
  Object.assign(allValues, result.values);
528
+ Object.assign(allStrings, result.strings);
546
529
  allEntityRefs.push(...result.entityRefs);
547
530
  allErrors.push(...result.errors);
548
531
  } else {
@@ -552,12 +535,13 @@ function parseAttrs(
552
535
  def.component
553
536
  );
554
537
  Object.assign(allValues, result.values);
538
+ Object.assign(allStrings, result.strings);
555
539
  allEntityRefs.push(...result.entityRefs);
556
540
  allErrors.push(...result.errors);
557
541
  }
558
542
  }
559
543
 
560
- return { values: allValues, entityRefs: allEntityRefs, errors: allErrors };
544
+ return { values: allValues, strings: allStrings, entityRefs: allEntityRefs, errors: allErrors };
561
545
  }
562
546
 
563
547
  function setFieldValue(component: ComponentLike, field: string, eid: number, value: number): void {
@@ -567,6 +551,17 @@ function setFieldValue(component: ComponentLike, field: string, eid: number, val
567
551
  }
568
552
  }
569
553
 
554
+ function isStringField(component: ComponentLike, field: string): boolean {
555
+ const val = component[field];
556
+ if (val == null) return false;
557
+ if (ArrayBuffer.isView(val) || Array.isArray(val)) return false;
558
+ return typeof val === "object";
559
+ }
560
+
561
+ function setString(component: ComponentLike, field: string, eid: number, value: string): void {
562
+ (component[field] as Record<number, string>)[eid] = value;
563
+ }
564
+
570
565
  function detectVec2(component: ComponentLike, base: string): boolean {
571
566
  return `${base}X` in component && `${base}Y` in component;
572
567
  }
@@ -594,7 +589,7 @@ function parseNumber(value: string): number | null {
594
589
  if (value === "false") return 0;
595
590
 
596
591
  const num = parseFloat(value);
597
- return isNaN(num) ? null : num;
592
+ return Number.isNaN(num) ? null : num;
598
593
  }
599
594
 
600
595
  function parseValues(valueStr: string): (number | null)[] {
@@ -633,10 +628,12 @@ function parsePropertyString(
633
628
  component: ComponentLike
634
629
  ): {
635
630
  values: Record<string, number>;
631
+ strings: Record<string, string>;
636
632
  entityRefs: { field: string; targetName: string }[];
637
633
  errors: string[];
638
634
  } {
639
635
  const values: Record<string, number> = {};
636
+ const strings: Record<string, string> = {};
640
637
  const entityRefs: { field: string; targetName: string }[] = [];
641
638
  const errors: string[] = [];
642
639
 
@@ -679,6 +676,24 @@ function parsePropertyString(
679
676
  const parsed = parseValues(valueStr);
680
677
 
681
678
  if (parsed.some((v) => v === null)) {
679
+ const rawValue = valueStr.trim();
680
+
681
+ if (name in component && isStringField(component, name)) {
682
+ strings[name] = rawValue;
683
+ continue;
684
+ }
685
+
686
+ if (parsed.length === 1) {
687
+ const traits = getTraits(component);
688
+ const parseFn = traits?.parse?.[name];
689
+ if (parseFn) {
690
+ const resolved = parseFn(rawValue);
691
+ if (resolved !== undefined) {
692
+ values[name] = resolved;
693
+ continue;
694
+ }
695
+ }
696
+ }
682
697
  errors.push(`Invalid number in "${prop}"`);
683
698
  continue;
684
699
  }
@@ -756,7 +771,7 @@ function parsePropertyString(
756
771
  }
757
772
  }
758
773
 
759
- return { values, entityRefs, errors };
774
+ return { values, strings, entityRefs, errors };
760
775
  }
761
776
 
762
777
  function isCSSAttrSyntax(value: string): boolean {
@@ -45,7 +45,7 @@ setTraits(Orbit, {
45
45
  minPitch: -Math.PI / 2 + 0.01,
46
46
  maxPitch: Math.PI / 2 - 0.01,
47
47
  minDistance: 1,
48
- maxDistance: 25,
48
+ maxDistance: 30,
49
49
  minSize: 0.5,
50
50
  maxSize: 50,
51
51
  targetSize: 5,
@@ -3,12 +3,10 @@ import { SDFGenerator } from "./sdf";
3
3
  import {
4
4
  MAX_ENTITIES,
5
5
  resource,
6
- registerPostLoadHook,
7
6
  createFieldProxy,
8
7
  type Plugin,
9
8
  type State,
10
9
  type System,
11
- type PostLoadContext,
12
10
  type FieldProxy,
13
11
  } from "../../core";
14
12
  import { setTraits } from "../../core/component";
@@ -31,14 +29,16 @@ const SDF_SIZE = 96;
31
29
  const SDF_EXPONENT = 9;
32
30
  const fontUrls: string[] = [];
33
31
  const loadedFonts: (Font | null)[] = [];
32
+ const fontNames = new Map<string, number>();
34
33
 
35
34
  export const DEFAULT_FONT =
36
35
  "https://fonts.gstatic.com/s/inter/v20/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfMZg.ttf";
37
36
 
38
- export function font(url: string): number {
37
+ export function font(url: string, name?: string): number {
39
38
  const id = fontUrls.length;
40
39
  fontUrls.push(url);
41
40
  loadedFonts.push(null);
41
+ if (name) fontNames.set(name, id);
42
42
  return id;
43
43
  }
44
44
 
@@ -46,9 +46,14 @@ export function getFont(id: number): Font | null {
46
46
  return loadedFonts[id] ?? null;
47
47
  }
48
48
 
49
+ export function getFontByName(name: string): number | undefined {
50
+ return fontNames.get(name);
51
+ }
52
+
49
53
  export function resetFonts(): void {
50
54
  fontUrls.length = 0;
51
55
  loadedFonts.length = 0;
56
+ fontNames.clear();
52
57
  }
53
58
 
54
59
  async function loadFonts(): Promise<void> {
@@ -141,35 +146,6 @@ export const Text = {
141
146
  colorB: createFieldProxy(data, 12, 10),
142
147
  };
143
148
 
144
- interface PendingText {
145
- readonly eid: number;
146
- readonly content: string;
147
- }
148
-
149
- let pendingTextContent: PendingText[] = [];
150
-
151
- function parseTextAttrs(attrs: Record<string, string>): Record<string, string> {
152
- if (attrs._value) {
153
- const parsed: Record<string, string> = {};
154
- for (const part of attrs._value.split(";")) {
155
- const colonIdx = part.indexOf(":");
156
- if (colonIdx === -1) continue;
157
- const key = part.slice(0, colonIdx).trim();
158
- const value = part.slice(colonIdx + 1).trim();
159
- if (key && value) parsed[key] = value;
160
- }
161
- return parsed;
162
- }
163
- return attrs;
164
- }
165
-
166
- function finalizePendingText(_state: State, _context: PostLoadContext): void {
167
- for (const pending of pendingTextContent) {
168
- Text.content[pending.eid] = pending.content;
169
- }
170
- pendingTextContent = [];
171
- }
172
-
173
149
  setTraits(Text, {
174
150
  defaults: () => ({
175
151
  font: 0,
@@ -180,36 +156,7 @@ setTraits(Text, {
180
156
  anchorY: 0,
181
157
  color: 0xffffff,
182
158
  }),
183
- adapter: (attrs: Record<string, string>, eid: number) => {
184
- const parsed = parseTextAttrs(attrs);
185
- const result: Record<string, number> = {};
186
-
187
- if (parsed.content) {
188
- pendingTextContent.push({ eid, content: parsed.content });
189
- }
190
-
191
- if (parsed.font) result.font = parseInt(parsed.font, 10);
192
- if (parsed["font-size"]) result.fontSize = parseFloat(parsed["font-size"]);
193
- if (parsed.fontSize) result.fontSize = parseFloat(parsed.fontSize);
194
- if (parsed.opacity) result.opacity = parseFloat(parsed.opacity);
195
- if (parsed.visible) result.visible = parseFloat(parsed.visible);
196
- if (parsed["anchor-x"]) result.anchorX = parseFloat(parsed["anchor-x"]);
197
- if (parsed.anchorX) result.anchorX = parseFloat(parsed.anchorX);
198
- if (parsed["anchor-y"]) result.anchorY = parseFloat(parsed["anchor-y"]);
199
- if (parsed.anchorY) result.anchorY = parseFloat(parsed.anchorY);
200
- if (parsed.color) {
201
- const colorStr = parsed.color;
202
- if (colorStr.startsWith("0x") || colorStr.startsWith("0X")) {
203
- result.color = parseInt(colorStr, 16);
204
- } else if (colorStr.startsWith("#")) {
205
- result.color = parseInt(colorStr.slice(1), 16);
206
- } else {
207
- result.color = parseInt(colorStr, 10);
208
- }
209
- }
210
-
211
- return result;
212
- },
159
+ parse: { font: getFontByName },
213
160
  });
214
161
 
215
162
  interface GlyphMetrics {
@@ -669,7 +616,7 @@ interface PendingGlyph {
669
616
  a: number;
670
617
  }
671
618
 
672
- let glyphsByFont: PendingGlyph[][] = [];
619
+ const glyphsByFont: PendingGlyph[][] = [];
673
620
 
674
621
  const TextSystem: System = {
675
622
  group: "draw",
@@ -788,8 +735,6 @@ export const TextPlugin: Plugin = {
788
735
  dependencies: [ComputePlugin, RenderPlugin],
789
736
 
790
737
  async initialize(state: State) {
791
- registerPostLoadHook(finalizePendingText);
792
-
793
738
  const compute = Compute.from(state);
794
739
  const render = Render.from(state);
795
740
  if (!compute || !render) return;
@@ -1,10 +1,13 @@
1
- import { surface } from "../standard/render/surface";
2
- import type { MeshData } from "../standard/render/mesh";
1
+ import type { Plugin, State, System } from "../../core";
2
+ import { setTraits } from "../../core/component";
3
+ import { Transform } from "../../standard/transforms";
4
+ import { Mesh, Volume, mesh, Surface, surface, RenderPlugin } from "../../standard/render";
5
+ import type { MeshData } from "../../standard/render";
3
6
 
4
7
  const SEED1 = "vec2(127.1, 311.7)";
5
8
  const SEED2 = "vec2(269.5, 183.3)";
6
9
 
7
- export const Water = surface({
10
+ const waterSurface = surface({
8
11
  fragment: `
9
12
  let p = (*surface).worldPos.xz * 3.0;
10
13
  let t = scene.time;
@@ -21,7 +24,7 @@ export const Water = surface({
21
24
  (*surface).normal = normalize(vec3(-dx * 0.015, 1.0, -dz * 0.015));`,
22
25
  });
23
26
 
24
- export function createSubdividedPlane(subdivisions: number = 32): MeshData {
27
+ function createSubdividedPlane(subdivisions: number = 32): MeshData {
25
28
  const segments = subdivisions;
26
29
  const vertexCount = (segments + 1) * (segments + 1);
27
30
  const indexCount = segments * segments * 6;
@@ -62,3 +65,55 @@ export function createSubdividedPlane(subdivisions: number = 32): MeshData {
62
65
 
63
66
  return { vertices, indices, vertexCount, indexCount };
64
67
  }
68
+
69
+ const waterMesh = mesh(createSubdividedPlane(64));
70
+
71
+ export const Water = {
72
+ color: [] as number[],
73
+ opacity: [] as number[],
74
+ roughness: [] as number[],
75
+ metallic: [] as number[],
76
+ ior: [] as number[],
77
+ level: [] as number[],
78
+ };
79
+ setTraits(Water, {
80
+ defaults: () => ({
81
+ color: 0x4090a0,
82
+ opacity: 0.5,
83
+ roughness: 0.05,
84
+ metallic: 0.8,
85
+ ior: 1.33,
86
+ level: 0,
87
+ }),
88
+ });
89
+
90
+ const WaterSystem: System = {
91
+ group: "simulation",
92
+
93
+ update(state: State) {
94
+ for (const eid of state.query([Water])) {
95
+ if (!state.hasComponent(eid, Transform)) state.addComponent(eid, Transform);
96
+ if (!state.hasComponent(eid, Mesh)) state.addComponent(eid, Mesh);
97
+ if (!state.hasComponent(eid, Surface)) state.addComponent(eid, Surface);
98
+
99
+ Transform.posY[eid] = Water.level[eid];
100
+ Mesh.shape[eid] = waterMesh;
101
+ Mesh.color[eid] = Water.color[eid];
102
+ Mesh.opacity[eid] = Water.opacity[eid];
103
+ Mesh.roughness[eid] = Water.roughness[eid];
104
+ Mesh.metallic[eid] = Water.metallic[eid];
105
+ Mesh.ior[eid] = Water.ior[eid];
106
+ Mesh.sizeX[eid] = 40;
107
+ Mesh.sizeY[eid] = 1;
108
+ Mesh.sizeZ[eid] = 40;
109
+ Mesh.volume[eid] = Volume.HalfSpace;
110
+ Surface.type[eid] = waterSurface;
111
+ }
112
+ },
113
+ };
114
+
115
+ export const WaterPlugin: Plugin = {
116
+ systems: [WaterSystem],
117
+ components: { Water },
118
+ dependencies: [RenderPlugin],
119
+ };