@forgeax/engine-scene 0.1.23 → 0.1.24

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.
package/dist/index.mjs CHANGED
@@ -1234,15 +1234,30 @@ function sceneTopoSort(nodes) {
1234
1234
  import {
1235
1235
  defineSystem,
1236
1236
  defineSystemSet,
1237
+ ENTITY_NULL_RAW as ENTITY_NULL_RAW2,
1237
1238
  FixedUpdate,
1238
1239
  Update
1239
1240
  } from "@forgeax/engine-ecs";
1241
+ import {
1242
+ getDerivedWriter,
1243
+ worldInternal
1244
+ } from "@forgeax/engine-ecs/internal";
1240
1245
  import { mat4 } from "@forgeax/engine-math";
1241
1246
  import { err as err5, ok as ok5 } from "@forgeax/engine-types";
1242
1247
  var PROPAGATE_TRANSFORMS_SYSTEM = "propagateTransforms";
1243
1248
  var PROPAGATE_TRANSFORMS_FIXED_SYSTEM = "propagateTransformsFixed";
1244
1249
  var TransformSet = defineSystemSet({ name: "transform" });
1245
1250
  var TransformFixedSet = defineSystemSet({ name: "transform-fixed" });
1251
+ var propagationTrace;
1252
+ var hierarchyRootCursorAllocationCount = 0;
1253
+ function createHierarchyRootCursor() {
1254
+ hierarchyRootCursorAllocationCount += 1;
1255
+ return { bindingIndex: -1, row: -1 };
1256
+ }
1257
+ function countPropagation(name) {
1258
+ const trace = propagationTrace;
1259
+ if (trace !== void 0) trace[name] += 1;
1260
+ }
1246
1261
  var SCRATCH = /* @__PURE__ */ new WeakMap();
1247
1262
  var REGISTRATION_LEASES = /* @__PURE__ */ new WeakMap();
1248
1263
  function pairError(entity, expected) {
@@ -1256,7 +1271,7 @@ function pairError(entity, expected) {
1256
1271
  );
1257
1272
  }
1258
1273
  function ensureQueries(world, scratch) {
1259
- if (scratch.flatQuery !== void 0 && scratch.outputQuery !== void 0 && scratch.hierarchyQuery !== void 0 && scratch.missingGlobalQuery !== void 0 && scratch.missingTransformQuery !== void 0) {
1274
+ if (scratch.flatQuery !== void 0 && scratch.hierarchyQuery !== void 0 && scratch.transformQuery !== void 0 && scratch.hierarchyWriter !== void 0 && scratch.transformWriter !== void 0 && scratch.missingGlobalQuery !== void 0 && scratch.missingTransformQuery !== void 0) {
1260
1275
  return ok5(void 0);
1261
1276
  }
1262
1277
  const flatOutput = world.query({
@@ -1265,16 +1280,23 @@ function ensureQueries(world, scratch) {
1265
1280
  without: [ChildOf],
1266
1281
  changed: [Transform]
1267
1282
  });
1268
- const output = world.query({ write: [GlobalTransform] });
1269
- const hierarchy = world.query({ read: [Transform, ChildOf] });
1283
+ const hierarchy = world.query({ read: [Transform, ChildOf], write: [GlobalTransform] });
1284
+ const transform = world.query({ read: [Transform], write: [GlobalTransform] });
1270
1285
  const missingGlobal = world.query({ with: [Transform], without: [GlobalTransform] });
1271
1286
  const missingTransform = world.query({ with: [GlobalTransform], without: [Transform] });
1272
- if (!flatOutput.ok || !output.ok || !hierarchy.ok || !missingGlobal.ok || !missingTransform.ok) {
1287
+ if (!flatOutput.ok || !hierarchy.ok || !transform.ok || !missingGlobal.ok || !missingTransform.ok) {
1273
1288
  return pairError(0, "valid Transform and GlobalTransform pair queries");
1274
1289
  }
1290
+ const hierarchyWriter = getDerivedWriter(hierarchy.value, GlobalTransform);
1291
+ const transformWriter = getDerivedWriter(transform.value, GlobalTransform);
1292
+ if (!hierarchyWriter.ok || !transformWriter.ok) {
1293
+ return pairError(0, "dense Transform and ChildOf derived bindings");
1294
+ }
1275
1295
  scratch.flatQuery = flatOutput.value;
1276
- scratch.outputQuery = output.value;
1277
1296
  scratch.hierarchyQuery = hierarchy.value;
1297
+ scratch.transformQuery = transform.value;
1298
+ scratch.hierarchyWriter = hierarchyWriter.value;
1299
+ scratch.transformWriter = transformWriter.value;
1278
1300
  scratch.missingGlobalQuery = missingGlobal.value;
1279
1301
  scratch.missingTransformQuery = missingTransform.value;
1280
1302
  return ok5(void 0);
@@ -1303,7 +1325,25 @@ function scratchFor(world) {
1303
1325
  rotation: new Float32Array(4),
1304
1326
  scale: new Float32Array(3),
1305
1327
  local: mat4.create(),
1306
- hierarchyPath: []
1328
+ parent: mat4.create(),
1329
+ candidate: mat4.create(),
1330
+ hierarchyStackEntities: [],
1331
+ hierarchyStackChildren: [],
1332
+ hierarchyProbeEntities: [],
1333
+ hierarchyCurrentCursor: { bindingIndex: -1, row: -1 },
1334
+ hierarchyParentCursor: { bindingIndex: -1, row: -1 },
1335
+ hierarchyChildCursor: { bindingIndex: -1, row: -1 },
1336
+ hierarchyResidualCursor: { bindingIndex: -1, row: -1 },
1337
+ hierarchyResidualParentCursor: { bindingIndex: -1, row: -1 },
1338
+ hierarchyRootCursor: createHierarchyRootCursor(),
1339
+ hierarchyStates: [],
1340
+ hierarchyChanged: [],
1341
+ hierarchyBindingTables: [],
1342
+ hierarchyBindingRows: [],
1343
+ flatChanged: [],
1344
+ flatBindingTables: [],
1345
+ flatBindingRows: [],
1346
+ flatStructureEpoch: -1
1307
1347
  };
1308
1348
  SCRATCH.set(world, created);
1309
1349
  return created;
@@ -1363,40 +1403,7 @@ function composeFlatColumns(positions, rotations, scales, worlds, count) {
1363
1403
  worlds[world + 15] = 1;
1364
1404
  }
1365
1405
  }
1366
- function writeWorld(world, output, entity, value) {
1367
- const row = output.at(entity);
1368
- if (row === void 0) {
1369
- return err5(
1370
- new SceneError({
1371
- code: "hierarchy-broken",
1372
- expected: "each Transform entity to carry a GlobalTransform pair",
1373
- hint: "attach GlobalTransform at scene authoring or import time, then retry propagation",
1374
- detail: { entity, parent: entity }
1375
- })
1376
- );
1377
- }
1378
- const current = world.get(entity, GlobalTransform);
1379
- if (!current.ok)
1380
- return err5(
1381
- new SceneError({
1382
- code: "hierarchy-broken",
1383
- expected: "each Transform entity to carry a GlobalTransform pair",
1384
- hint: "attach GlobalTransform at scene authoring or import time, then retry propagation",
1385
- detail: { entity, parent: entity }
1386
- })
1387
- );
1388
- let changed = false;
1389
- for (let index = 0; index < value.length; index += 1) {
1390
- if (current.value.world[index] !== value[index]) {
1391
- changed = true;
1392
- break;
1393
- }
1394
- }
1395
- if (!changed) return ok5(void 0);
1396
- row.mut(GlobalTransform).world.set(value);
1397
- return ok5(void 0);
1398
- }
1399
- function propagateFlat(scratch) {
1406
+ function propagateFlat(world, scratch) {
1400
1407
  const query = scratch.flatQuery;
1401
1408
  if (query === void 0)
1402
1409
  return err5(
@@ -1412,161 +1419,674 @@ function propagateFlat(scratch) {
1412
1419
  try {
1413
1420
  for (const span of spans.value) {
1414
1421
  const local = span.get(Transform);
1415
- const world = span.mut(GlobalTransform).world;
1416
- composeFlatColumns(local.pos, local.quat, local.scale, world, span.length);
1422
+ const world2 = span.mut(GlobalTransform).world;
1423
+ composeFlatColumns(local.pos, local.quat, local.scale, world2, span.length);
1417
1424
  bindingIndex += 1;
1418
1425
  }
1419
1426
  } catch (cause) {
1420
1427
  const error = cause;
1421
- return err5(
1422
- new SceneError({
1423
- code: "hierarchy-broken",
1424
- expected: "derived GlobalTransform range publication to succeed",
1425
- hint: error.hint,
1426
- detail: {
1427
- kind: "derived-write",
1428
- entity: 0,
1429
- parent: 0,
1430
- bindingIndex,
1431
- base: 0,
1432
- start: 0,
1433
- count: 0,
1434
- cause: error
1435
- }
1436
- })
1437
- );
1428
+ return err5(derivedWriteError(error, bindingIndex));
1429
+ }
1430
+ const transformWriter = scratch.transformWriter;
1431
+ if (transformWriter === void 0) {
1432
+ return pairError(0, "dense Transform and GlobalTransform derived bindings");
1433
+ }
1434
+ const transformBindings = transformWriter.bindings;
1435
+ const structureEpoch = world[worldInternal].getStructureEpoch();
1436
+ if (scratch.flatStructureEpoch === structureEpoch) return ok5(void 0);
1437
+ ensureFlatBuffers(scratch, transformBindings);
1438
+ resetFlatBuffers(scratch);
1439
+ for (let bindingIndex2 = 0; bindingIndex2 < transformBindings.length; bindingIndex2 += 1) {
1440
+ const binding = transformBindings[bindingIndex2];
1441
+ const changed = scratch.flatChanged[bindingIndex2];
1442
+ if (binding === void 0 || changed === void 0) continue;
1443
+ for (let row = 0; row < binding.rowCapacity; row += 1) {
1444
+ const entity = binding.entities[row] ?? 0;
1445
+ const parentRaw = world[worldInternal].getFieldValue(entity, ChildOf, "parent");
1446
+ if (parentRaw !== void 0 && parentRaw !== ENTITY_NULL_RAW2) continue;
1447
+ countPropagation("flatStructuralRootRows");
1448
+ composeBindingRow(binding, row, void 0, 0, scratch, changed);
1449
+ }
1450
+ }
1451
+ for (let bindingIndex2 = 0; bindingIndex2 < transformBindings.length; bindingIndex2 += 1) {
1452
+ const changed = scratch.flatChanged[bindingIndex2];
1453
+ if (changed === void 0) continue;
1454
+ const published = transformWriter.publishChangedRows(bindingIndex2, changed);
1455
+ if (!published.ok) return err5(derivedWriteError(published.error, bindingIndex2));
1438
1456
  }
1457
+ scratch.flatStructureEpoch = structureEpoch;
1439
1458
  return ok5(void 0);
1440
1459
  }
1441
- function findEntry(entries, entity) {
1442
- for (let index = 0; index < entries.length; index += 1) {
1443
- if (entries[index]?.entity === entity) return index;
1460
+ function transformColumns(binding) {
1461
+ return binding.read;
1462
+ }
1463
+ function hierarchyColumns(binding) {
1464
+ return binding.read;
1465
+ }
1466
+ function worldColumn(binding) {
1467
+ return binding.write.world;
1468
+ }
1469
+ function hierarchyError(code, entity, parent, expected, hint) {
1470
+ return new SceneError({ code, expected, hint, detail: { entity, parent } });
1471
+ }
1472
+ function writeCandidate(binding, row, candidate, changed) {
1473
+ const worlds = worldColumn(binding);
1474
+ const base = row * 16;
1475
+ for (let index = 0; index < 16; index += 1) {
1476
+ if (worlds[base + index] !== candidate[index]) {
1477
+ worlds.set(candidate, base);
1478
+ changed[row] = 1;
1479
+ return;
1480
+ }
1444
1481
  }
1445
- return -1;
1446
1482
  }
1447
- function resolveEntry(index, entries, world, output, scratch) {
1448
- const entry = entries[index];
1449
- if (entry === void 0 || entry.state === 2) return ok5(void 0);
1450
- if (entry.state === 1) {
1451
- const cycleParent = entry.parent ?? entry.entity;
1452
- const cycleStart = scratch.hierarchyPath.indexOf(index);
1453
- const start = cycleStart < 0 ? 0 : cycleStart;
1454
- for (let pathIndex = start; pathIndex < scratch.hierarchyPath.length; pathIndex += 1) {
1455
- const cycleEntry = entries[scratch.hierarchyPath[pathIndex] ?? -1];
1456
- if (cycleEntry !== void 0) cycleEntry.parent = void 0;
1483
+ function composeBindingRow(binding, row, parentBinding, parentRow, scratch, changed) {
1484
+ if ("parent" in binding.read) countPropagation("hierarchyRowsEvaluated");
1485
+ const local = transformColumns(binding);
1486
+ const offset = row * 3;
1487
+ composeColumns(local.pos, local.quat, local.scale, scratch.local, scratch, offset, row * 4);
1488
+ if (parentBinding === void 0) {
1489
+ scratch.candidate.set(scratch.local);
1490
+ } else {
1491
+ const parentWorld = worldColumn(parentBinding);
1492
+ const parentOffset = parentRow * 16;
1493
+ for (let index = 0; index < 16; index += 1) {
1494
+ scratch.parent[index] = parentWorld[parentOffset + index] ?? 0;
1457
1495
  }
1458
- return err5(
1459
- new SceneError({
1460
- code: "hierarchy-cycle",
1461
- expected: "a parent-before-child Transform hierarchy",
1462
- hint: "repair the ChildOf cycle and retry TransformPropagation",
1463
- detail: { entity: entry.entity, parent: cycleParent }
1464
- })
1465
- );
1496
+ mat4.multiply(scratch.candidate, scratch.parent, scratch.local);
1466
1497
  }
1467
- entry.state = 1;
1468
- scratch.hierarchyPath.push(index);
1469
- let parentIndex = entry.parent === void 0 ? -1 : findEntry(entries, entry.parent);
1470
- let failure;
1471
- if (entry.parent !== void 0 && parentIndex < 0) {
1472
- const local = world.get(entry.parent, Transform);
1473
- const global = world.get(entry.parent, GlobalTransform);
1474
- if (!local.ok || !global.ok) {
1475
- failure = new SceneError({
1476
- code: "hierarchy-broken",
1477
- expected: "each ChildOf parent to carry Transform and GlobalTransform",
1478
- hint: "repair the missing parent pair before retrying propagation",
1479
- detail: { entity: entry.entity, parent: entry.parent }
1480
- });
1481
- entry.parent = void 0;
1482
- parentIndex = -1;
1498
+ writeCandidate(binding, row, scratch.candidate, changed);
1499
+ }
1500
+ function ensureHierarchyBuffers(scratch, bindings) {
1501
+ let same = scratch.hierarchyBindingTables.length === bindings.length;
1502
+ if (same) {
1503
+ for (let index = 0; index < bindings.length; index += 1) {
1504
+ const binding = bindings[index];
1505
+ if (binding === void 0 || scratch.hierarchyBindingTables[index] !== binding.tableId || scratch.hierarchyBindingRows[index] !== binding.rowCapacity) {
1506
+ same = false;
1507
+ break;
1508
+ }
1509
+ }
1510
+ }
1511
+ if (same) return;
1512
+ scratch.hierarchyBindingTables = bindings.map((binding) => binding.tableId);
1513
+ scratch.hierarchyBindingRows = bindings.map((binding) => binding.rowCapacity);
1514
+ scratch.hierarchyStates = bindings.map((binding) => new Uint8Array(binding.rowCapacity));
1515
+ scratch.hierarchyChanged = bindings.map((binding) => new Uint8Array(binding.rowCapacity));
1516
+ }
1517
+ function resetHierarchyBuffers(scratch) {
1518
+ for (let index = 0; index < scratch.hierarchyStates.length; index += 1) {
1519
+ scratch.hierarchyStates[index]?.fill(0);
1520
+ scratch.hierarchyChanged[index]?.fill(0);
1521
+ }
1522
+ }
1523
+ function ensureFlatBuffers(scratch, bindings) {
1524
+ let same = scratch.flatBindingTables.length === bindings.length;
1525
+ if (same) {
1526
+ for (let index = 0; index < bindings.length; index += 1) {
1527
+ const binding = bindings[index];
1528
+ if (binding === void 0 || scratch.flatBindingTables[index] !== binding.tableId || scratch.flatBindingRows[index] !== binding.rowCapacity) {
1529
+ same = false;
1530
+ break;
1531
+ }
1532
+ }
1533
+ }
1534
+ if (same) return;
1535
+ scratch.flatBindingTables = bindings.map((binding) => binding.tableId);
1536
+ scratch.flatBindingRows = bindings.map((binding) => binding.rowCapacity);
1537
+ scratch.flatChanged = bindings.map((binding) => new Uint8Array(binding.rowCapacity));
1538
+ }
1539
+ function resetFlatBuffers(scratch) {
1540
+ for (const changed of scratch.flatChanged) changed.fill(0);
1541
+ }
1542
+ function derivedWriteError(cause, bindingIndex) {
1543
+ return new SceneError({
1544
+ code: "hierarchy-broken",
1545
+ expected: "derived GlobalTransform range publication to succeed",
1546
+ hint: cause.hint ?? "retry propagation on a healthy World",
1547
+ detail: {
1548
+ kind: "derived-write",
1549
+ entity: 0,
1550
+ parent: 0,
1551
+ bindingIndex,
1552
+ base: 0,
1553
+ start: 0,
1554
+ count: 0,
1555
+ cause
1556
+ }
1557
+ });
1558
+ }
1559
+ function findHierarchyLocation(writer, bindings, entity, cursor) {
1560
+ countPropagation("hierarchyEntityLookups");
1561
+ if (!writer.locateEntity(entity, cursor)) return void 0;
1562
+ return bindings[cursor.bindingIndex];
1563
+ }
1564
+ function findTransformLocation(writer, bindings, entity, cursor) {
1565
+ countPropagation("hierarchyEntityLookups");
1566
+ if (!writer.locateEntity(entity, cursor)) return void 0;
1567
+ return bindings[cursor.bindingIndex];
1568
+ }
1569
+ function countPublishedRows(changed) {
1570
+ const trace = propagationTrace;
1571
+ if (trace === void 0) return;
1572
+ let runOpen = false;
1573
+ for (let row = 0; row < changed.length; row += 1) {
1574
+ if ((changed[row] ?? 0) !== 0) {
1575
+ trace.hierarchyPublishedRows += 1;
1576
+ if (!runOpen) {
1577
+ trace.hierarchyPublishedRuns += 1;
1578
+ runOpen = true;
1579
+ }
1483
1580
  } else {
1484
- const relation = world.get(entry.parent, ChildOf);
1485
- entries.push({
1486
- entity: entry.parent,
1487
- parent: relation.ok && relation.value.parent !== null ? relation.value.parent : void 0,
1488
- position: new Float32Array(local.value.pos),
1489
- rotation: new Float32Array(local.value.quat),
1490
- scale: new Float32Array(local.value.scale),
1491
- world: mat4.create(),
1492
- state: 0
1493
- });
1494
- parentIndex = entries.length - 1;
1581
+ runOpen = false;
1582
+ }
1583
+ }
1584
+ }
1585
+ function noteHierarchyError(current, next) {
1586
+ if (current === void 0) return next;
1587
+ const currentEntity = Number(current.detail?.entity ?? Number.MAX_SAFE_INTEGER);
1588
+ const nextEntity = Number(next.detail?.entity ?? Number.MAX_SAFE_INTEGER);
1589
+ return nextEntity < currentEntity || nextEntity === currentEntity && next.code.localeCompare(current.code) < 0 ? next : current;
1590
+ }
1591
+ function composeLocalHierarchyEntity(writer, bindings, entity, scratch, changed) {
1592
+ const cursor = scratch.hierarchyCurrentCursor;
1593
+ const binding = findHierarchyLocation(writer, bindings, entity, cursor);
1594
+ if (binding === void 0) return false;
1595
+ composeBindingRow(
1596
+ binding,
1597
+ cursor.row,
1598
+ void 0,
1599
+ 0,
1600
+ scratch,
1601
+ changed[cursor.bindingIndex]
1602
+ );
1603
+ const states = scratch.hierarchyStates[cursor.bindingIndex];
1604
+ if (states !== void 0) states[cursor.row] = 3;
1605
+ return true;
1606
+ }
1607
+ function handleActiveCycle(repeated, stackEntities, stackChildren, writer, bindings, scratch, changed, report) {
1608
+ let cycleStart = -1;
1609
+ for (let index = 0; index < stackEntities.length; index += 1) {
1610
+ if (stackEntities[index] === repeated) {
1611
+ cycleStart = index;
1612
+ break;
1495
1613
  }
1496
1614
  }
1497
- if (parentIndex >= 0) {
1498
- const parentResult = resolveEntry(parentIndex, entries, world, output, scratch);
1499
- if (!parentResult.ok) failure = parentResult.error;
1500
- parentIndex = entry.parent === void 0 ? -1 : findEntry(entries, entry.parent);
1501
- if (parentIndex >= 0 && entries[parentIndex]?.state !== 2) parentIndex = -1;
1615
+ if (cycleStart < 0) return;
1616
+ const repeatedCursor = { bindingIndex: -1, row: -1 };
1617
+ const repeatedBinding = findHierarchyLocation(writer, bindings, repeated, repeatedCursor);
1618
+ const repeatedParent = repeatedBinding === void 0 ? repeated : hierarchyColumns(repeatedBinding).parent[repeatedCursor.row] ?? ENTITY_NULL_RAW2;
1619
+ report(
1620
+ hierarchyError(
1621
+ "hierarchy-cycle",
1622
+ repeated,
1623
+ repeatedParent,
1624
+ "a parent-before-child Transform hierarchy",
1625
+ "repair the ChildOf cycle and retry TransformPropagation"
1626
+ )
1627
+ );
1628
+ for (let index = cycleStart; index < stackEntities.length; index += 1) {
1629
+ const cycleEntity = stackEntities[index];
1630
+ if (cycleEntity === void 0) continue;
1631
+ composeLocalHierarchyEntity(writer, bindings, cycleEntity, scratch, changed);
1632
+ stackChildren[index] = 0;
1502
1633
  }
1503
- composeColumns(entry.position, entry.rotation, entry.scale, scratch.local, scratch);
1504
- if (parentIndex >= 0) {
1505
- const parent = entries[parentIndex];
1506
- if (parent !== void 0) mat4.multiply(entry.world, parent.world, scratch.local);
1634
+ }
1635
+ function walkChildren(world, root, writer, bindings, transformWriter, transformBindings, scratch, report, allowCompletedRoot) {
1636
+ countPropagation("hierarchyRootInvocations");
1637
+ const stackEntities = scratch.hierarchyStackEntities;
1638
+ const stackChildren = scratch.hierarchyStackChildren;
1639
+ stackEntities.length = 0;
1640
+ stackChildren.length = 0;
1641
+ if (allowCompletedRoot) {
1642
+ const rootCursor = scratch.hierarchyRootCursor;
1643
+ const rootBinding = findHierarchyLocation(writer, bindings, root, rootCursor);
1644
+ if (rootBinding !== void 0) {
1645
+ const state = scratch.hierarchyStates[rootCursor.bindingIndex]?.[rootCursor.row] ?? 0;
1646
+ if (state === 4) return;
1647
+ }
1507
1648
  } else {
1508
- entry.world.set(scratch.local);
1649
+ const rootCursor = scratch.hierarchyRootCursor;
1650
+ countPropagation("hierarchyRootCursorReuses");
1651
+ const rootBinding = findHierarchyLocation(writer, bindings, root, rootCursor);
1652
+ if (rootBinding !== void 0) {
1653
+ const state = scratch.hierarchyStates[rootCursor.bindingIndex]?.[rootCursor.row] ?? 0;
1654
+ if (state !== 0) return;
1655
+ }
1509
1656
  }
1510
- const write = writeWorld(world, output, entry.entity, entry.world);
1511
- if (!write.ok) {
1512
- scratch.hierarchyPath.pop();
1513
- return write;
1657
+ stackEntities.push(root);
1658
+ stackChildren.push(-1);
1659
+ const currentCursor = scratch.hierarchyCurrentCursor;
1660
+ const parentCursor = scratch.hierarchyParentCursor;
1661
+ const childCursor = scratch.hierarchyChildCursor;
1662
+ while (stackEntities.length > 0) {
1663
+ const top = stackEntities.length - 1;
1664
+ const current = stackEntities[top];
1665
+ if (current === void 0) {
1666
+ stackEntities.pop();
1667
+ stackChildren.pop();
1668
+ continue;
1669
+ }
1670
+ const hierarchyBinding = findHierarchyLocation(writer, bindings, current, currentCursor);
1671
+ const nextChild = stackChildren[top] ?? -1;
1672
+ if (nextChild < 0) {
1673
+ if (hierarchyBinding !== void 0) {
1674
+ const state = scratch.hierarchyStates[currentCursor.bindingIndex]?.[currentCursor.row] ?? 0;
1675
+ if (state === 0) {
1676
+ const states = scratch.hierarchyStates[currentCursor.bindingIndex];
1677
+ if (states !== void 0) states[currentCursor.row] = 1;
1678
+ const parentRaw = hierarchyColumns(hierarchyBinding).parent[currentCursor.row] ?? ENTITY_NULL_RAW2;
1679
+ let completed = false;
1680
+ if (parentRaw === ENTITY_NULL_RAW2) {
1681
+ composeBindingRow(
1682
+ hierarchyBinding,
1683
+ currentCursor.row,
1684
+ void 0,
1685
+ 0,
1686
+ scratch,
1687
+ scratch.hierarchyChanged[currentCursor.bindingIndex]
1688
+ );
1689
+ completed = true;
1690
+ } else {
1691
+ const parent = parentRaw;
1692
+ const parentBinding = findTransformLocation(
1693
+ transformWriter,
1694
+ transformBindings,
1695
+ parent,
1696
+ parentCursor
1697
+ );
1698
+ const parentHierarchy = findHierarchyLocation(writer, bindings, parent, childCursor);
1699
+ if (parentBinding === void 0) {
1700
+ report(
1701
+ hierarchyError(
1702
+ "hierarchy-broken",
1703
+ current,
1704
+ parent,
1705
+ "each ChildOf parent to carry Transform and GlobalTransform",
1706
+ "repair the missing parent pair before retrying propagation"
1707
+ )
1708
+ );
1709
+ composeBindingRow(
1710
+ hierarchyBinding,
1711
+ currentCursor.row,
1712
+ void 0,
1713
+ 0,
1714
+ scratch,
1715
+ scratch.hierarchyChanged[currentCursor.bindingIndex]
1716
+ );
1717
+ completed = true;
1718
+ } else if (parentHierarchy !== void 0) {
1719
+ const parentState = scratch.hierarchyStates[childCursor.bindingIndex]?.[childCursor.row] ?? 0;
1720
+ if (parentState === 1) {
1721
+ handleActiveCycle(
1722
+ parent,
1723
+ stackEntities,
1724
+ stackChildren,
1725
+ writer,
1726
+ bindings,
1727
+ scratch,
1728
+ scratch.hierarchyChanged,
1729
+ report
1730
+ );
1731
+ completed = true;
1732
+ } else if (parentState === 0) {
1733
+ report(
1734
+ hierarchyError(
1735
+ "hierarchy-broken",
1736
+ current,
1737
+ parent,
1738
+ "Children to enumerate every parent-before-child edge",
1739
+ "repair the Children mirror and retry TransformPropagation"
1740
+ )
1741
+ );
1742
+ composeBindingRow(
1743
+ hierarchyBinding,
1744
+ currentCursor.row,
1745
+ void 0,
1746
+ 0,
1747
+ scratch,
1748
+ scratch.hierarchyChanged[currentCursor.bindingIndex]
1749
+ );
1750
+ completed = true;
1751
+ } else {
1752
+ composeBindingRow(
1753
+ hierarchyBinding,
1754
+ currentCursor.row,
1755
+ parentBinding,
1756
+ parentCursor.row,
1757
+ scratch,
1758
+ scratch.hierarchyChanged[currentCursor.bindingIndex]
1759
+ );
1760
+ completed = true;
1761
+ }
1762
+ } else {
1763
+ composeBindingRow(
1764
+ hierarchyBinding,
1765
+ currentCursor.row,
1766
+ parentBinding,
1767
+ parentCursor.row,
1768
+ scratch,
1769
+ scratch.hierarchyChanged[currentCursor.bindingIndex]
1770
+ );
1771
+ completed = true;
1772
+ }
1773
+ }
1774
+ if (completed && states !== void 0 && states[currentCursor.row] === 1) {
1775
+ states[currentCursor.row] = 2;
1776
+ }
1777
+ }
1778
+ }
1779
+ stackChildren[top] = 0;
1780
+ continue;
1781
+ }
1782
+ const childrenLength = world[worldInternal].getArrayLength(current, Children, "entities") ?? 0;
1783
+ if (nextChild >= childrenLength) {
1784
+ stackEntities.pop();
1785
+ stackChildren.pop();
1786
+ if (allowCompletedRoot) {
1787
+ const completedCursor = scratch.hierarchyResidualCursor;
1788
+ const completedBinding = findHierarchyLocation(writer, bindings, current, completedCursor);
1789
+ const completedStates = completedBinding === void 0 ? void 0 : scratch.hierarchyStates[completedCursor.bindingIndex];
1790
+ if (completedStates !== void 0 && completedStates[completedCursor.row] === 3) {
1791
+ completedStates[completedCursor.row] = 4;
1792
+ }
1793
+ }
1794
+ continue;
1795
+ }
1796
+ stackChildren[top] = nextChild + 1;
1797
+ countPropagation("hierarchyEdgesVisited");
1798
+ const childRaw = world[worldInternal].getArrayElement(current, Children, "entities", nextChild);
1799
+ if (childRaw === void 0 || childRaw === ENTITY_NULL_RAW2) {
1800
+ report(
1801
+ hierarchyError(
1802
+ "hierarchy-broken",
1803
+ current,
1804
+ current,
1805
+ "Children.entities to contain live child handles",
1806
+ "repair the Children mirror and retry TransformPropagation"
1807
+ )
1808
+ );
1809
+ continue;
1810
+ }
1811
+ const child = childRaw;
1812
+ const childBinding = findHierarchyLocation(writer, bindings, child, childCursor);
1813
+ if (childBinding === void 0) {
1814
+ report(
1815
+ hierarchyError(
1816
+ "hierarchy-broken",
1817
+ child,
1818
+ current,
1819
+ "each Children entry to carry Transform, GlobalTransform, and ChildOf",
1820
+ "repair the child component pair before retrying propagation"
1821
+ )
1822
+ );
1823
+ continue;
1824
+ }
1825
+ const childParent = hierarchyColumns(childBinding).parent[childCursor.row] ?? ENTITY_NULL_RAW2;
1826
+ const childState = scratch.hierarchyStates[childCursor.bindingIndex]?.[childCursor.row] ?? 0;
1827
+ if (childParent !== current) {
1828
+ if (childState === 0) continue;
1829
+ report(
1830
+ hierarchyError(
1831
+ "hierarchy-broken",
1832
+ child,
1833
+ current,
1834
+ "Children and ChildOf to describe the same parent",
1835
+ "repair the relationship mirror and retry TransformPropagation"
1836
+ )
1837
+ );
1838
+ continue;
1839
+ }
1840
+ if (childState === 1) {
1841
+ handleActiveCycle(
1842
+ child,
1843
+ stackEntities,
1844
+ stackChildren,
1845
+ writer,
1846
+ bindings,
1847
+ scratch,
1848
+ scratch.hierarchyChanged,
1849
+ report
1850
+ );
1851
+ } else if (childState === 0) {
1852
+ stackEntities.push(child);
1853
+ stackChildren.push(-1);
1854
+ }
1855
+ }
1856
+ }
1857
+ function fallbackResidualPath(world, path, writer, bindings, transformWriter, transformBindings, scratch, report) {
1858
+ for (const entity of path) {
1859
+ composeLocalHierarchyEntity(writer, bindings, entity, scratch, scratch.hierarchyChanged);
1860
+ }
1861
+ for (const entity of path) {
1862
+ walkChildren(
1863
+ world,
1864
+ entity,
1865
+ writer,
1866
+ bindings,
1867
+ transformWriter,
1868
+ transformBindings,
1869
+ scratch,
1870
+ report,
1871
+ true
1872
+ );
1873
+ }
1874
+ }
1875
+ function resolveResidualPath(world, entity, writer, bindings, transformWriter, transformBindings, scratch, report) {
1876
+ const path = scratch.hierarchyProbeEntities;
1877
+ path.length = 0;
1878
+ const cursor = scratch.hierarchyResidualCursor;
1879
+ const parentTransformCursor = scratch.hierarchyParentCursor;
1880
+ const parentHierarchyCursor = scratch.hierarchyResidualParentCursor;
1881
+ let current = entity;
1882
+ while (true) {
1883
+ countPropagation("hierarchyResidualParentProbes");
1884
+ const binding = findHierarchyLocation(writer, bindings, current, cursor);
1885
+ if (binding === void 0) break;
1886
+ const states = scratch.hierarchyStates[cursor.bindingIndex];
1887
+ const state = states?.[cursor.row] ?? 0;
1888
+ if (state !== 0) {
1889
+ const columns = hierarchyColumns(binding);
1890
+ const parentRaw2 = columns.parent[cursor.row] ?? ENTITY_NULL_RAW2;
1891
+ report(
1892
+ hierarchyError(
1893
+ "hierarchy-cycle",
1894
+ current,
1895
+ parentRaw2 === ENTITY_NULL_RAW2 ? current : parentRaw2,
1896
+ "a parent-before-child Transform hierarchy",
1897
+ "repair the ChildOf cycle and retry TransformPropagation"
1898
+ )
1899
+ );
1900
+ fallbackResidualPath(
1901
+ world,
1902
+ path,
1903
+ writer,
1904
+ bindings,
1905
+ transformWriter,
1906
+ transformBindings,
1907
+ scratch,
1908
+ report
1909
+ );
1910
+ return;
1911
+ }
1912
+ if (states !== void 0) states[cursor.row] = 1;
1913
+ path.push(current);
1914
+ const parentRaw = hierarchyColumns(binding).parent[cursor.row] ?? ENTITY_NULL_RAW2;
1915
+ if (parentRaw === ENTITY_NULL_RAW2) {
1916
+ report(
1917
+ hierarchyError(
1918
+ "hierarchy-broken",
1919
+ current,
1920
+ current,
1921
+ "Children to enumerate every parent-before-child edge",
1922
+ "repair the Children mirror and retry TransformPropagation"
1923
+ )
1924
+ );
1925
+ fallbackResidualPath(
1926
+ world,
1927
+ path,
1928
+ writer,
1929
+ bindings,
1930
+ transformWriter,
1931
+ transformBindings,
1932
+ scratch,
1933
+ report
1934
+ );
1935
+ return;
1936
+ }
1937
+ const parent = parentRaw;
1938
+ const parentTransform = findTransformLocation(
1939
+ transformWriter,
1940
+ transformBindings,
1941
+ parent,
1942
+ parentTransformCursor
1943
+ );
1944
+ const parentHierarchy = findHierarchyLocation(writer, bindings, parent, parentHierarchyCursor);
1945
+ if (parentHierarchy === void 0) {
1946
+ report(
1947
+ hierarchyError(
1948
+ "hierarchy-broken",
1949
+ current,
1950
+ parent,
1951
+ parentTransform === void 0 ? "each ChildOf parent to carry Transform and GlobalTransform" : "Children to enumerate every parent-before-child edge",
1952
+ parentTransform === void 0 ? "repair the missing parent pair before retrying propagation" : "repair the Children mirror and retry TransformPropagation"
1953
+ )
1954
+ );
1955
+ fallbackResidualPath(
1956
+ world,
1957
+ path,
1958
+ writer,
1959
+ bindings,
1960
+ transformWriter,
1961
+ transformBindings,
1962
+ scratch,
1963
+ report
1964
+ );
1965
+ return;
1966
+ }
1967
+ const parentState = scratch.hierarchyStates[parentHierarchyCursor.bindingIndex]?.[parentHierarchyCursor.row] ?? 0;
1968
+ if (parentState === 0) {
1969
+ current = parent;
1970
+ continue;
1971
+ }
1972
+ if (parentState === 1) {
1973
+ report(
1974
+ hierarchyError(
1975
+ "hierarchy-cycle",
1976
+ parent,
1977
+ current,
1978
+ "a parent-before-child Transform hierarchy",
1979
+ "repair the ChildOf cycle and retry TransformPropagation"
1980
+ )
1981
+ );
1982
+ } else {
1983
+ report(
1984
+ hierarchyError(
1985
+ "hierarchy-broken",
1986
+ current,
1987
+ parent,
1988
+ "Children to enumerate every parent-before-child edge",
1989
+ "repair the Children mirror and retry TransformPropagation"
1990
+ )
1991
+ );
1992
+ }
1993
+ fallbackResidualPath(
1994
+ world,
1995
+ path,
1996
+ writer,
1997
+ bindings,
1998
+ transformWriter,
1999
+ transformBindings,
2000
+ scratch,
2001
+ report
2002
+ );
2003
+ return;
1514
2004
  }
1515
- entry.state = 2;
1516
- scratch.hierarchyPath.pop();
1517
- return failure === void 0 ? ok5(void 0) : err5(failure);
1518
2005
  }
1519
2006
  function propagateHierarchy(world, scratch) {
1520
- const query = scratch.hierarchyQuery;
1521
- const output = scratch.outputQuery;
1522
- if (query === void 0 || output === void 0) {
2007
+ const hierarchyWriter = scratch.hierarchyWriter;
2008
+ const transformWriter = scratch.transformWriter;
2009
+ if (hierarchyWriter === void 0 || transformWriter === void 0) {
1523
2010
  return err5(
1524
2011
  new SceneError({
1525
2012
  code: "hierarchy-broken",
1526
- expected: "paired Transform and GlobalTransform query access",
2013
+ expected: "paired Transform and GlobalTransform derived bindings",
1527
2014
  hint: "register the scene components before running TransformPropagation"
1528
2015
  })
1529
2016
  );
1530
2017
  }
1531
- const entries = [];
1532
- for (const row of query) {
1533
- if (findEntry(entries, row.entity) >= 0) continue;
1534
- const local = row.get(Transform);
1535
- const parentValue = row.get(ChildOf).parent;
1536
- const parent = parentValue === null ? void 0 : parentValue;
1537
- entries.push({
1538
- entity: row.entity,
1539
- parent,
1540
- position: new Float32Array(local.pos),
1541
- rotation: new Float32Array(local.quat),
1542
- scale: new Float32Array(local.scale),
1543
- world: mat4.create(),
1544
- state: 0
1545
- });
2018
+ const hierarchyBindings = hierarchyWriter.bindings;
2019
+ const transformBindings = transformWriter.bindings;
2020
+ if (hierarchyBindings.length === 0) {
2021
+ scratch.hierarchyBindingTables.length = 0;
2022
+ scratch.hierarchyBindingRows.length = 0;
2023
+ scratch.hierarchyStates.length = 0;
2024
+ scratch.hierarchyChanged.length = 0;
2025
+ return ok5(void 0);
1546
2026
  }
1547
- scratch.hierarchyPath.length = 0;
2027
+ ensureHierarchyBuffers(scratch, hierarchyBindings);
2028
+ resetHierarchyBuffers(scratch);
1548
2029
  let firstError;
1549
- for (let index = 0; index < entries.length; index += 1) {
1550
- const result = resolveEntry(index, entries, world, output, scratch);
1551
- if (!result.ok) {
1552
- const currentEntity = Number(result.error.detail?.entity ?? Number.MAX_SAFE_INTEGER);
1553
- if (firstError === void 0) {
1554
- firstError = result.error;
1555
- } else {
1556
- const firstEntity = Number(firstError.detail?.entity ?? Number.MAX_SAFE_INTEGER);
1557
- if (currentEntity < firstEntity || currentEntity === firstEntity && result.error.code.localeCompare(firstError.code) < 0) {
1558
- firstError = result.error;
1559
- }
2030
+ const report = (error) => {
2031
+ firstError = noteHierarchyError(firstError, error);
2032
+ };
2033
+ for (const binding of transformBindings) {
2034
+ const entities = binding.entities;
2035
+ for (let row = 0; row < binding.rowCapacity; row += 1) {
2036
+ const entity = entities[row] ?? 0;
2037
+ const parentRaw = world[worldInternal].getFieldValue(entity, ChildOf, "parent");
2038
+ if (parentRaw === void 0 || parentRaw === ENTITY_NULL_RAW2) {
2039
+ walkChildren(
2040
+ world,
2041
+ entity,
2042
+ hierarchyWriter,
2043
+ hierarchyBindings,
2044
+ transformWriter,
2045
+ transformBindings,
2046
+ scratch,
2047
+ report,
2048
+ false
2049
+ );
1560
2050
  }
1561
2051
  }
1562
2052
  }
2053
+ for (let bindingIndex = 0; bindingIndex < hierarchyBindings.length; bindingIndex += 1) {
2054
+ const binding = hierarchyBindings[bindingIndex];
2055
+ if (binding === void 0) continue;
2056
+ const entities = binding.entities;
2057
+ const states = scratch.hierarchyStates[bindingIndex];
2058
+ for (let row = 0; row < binding.rowCapacity; row += 1) {
2059
+ if ((states?.[row] ?? 0) !== 0) continue;
2060
+ const entity = entities[row] ?? 0;
2061
+ resolveResidualPath(
2062
+ world,
2063
+ entity,
2064
+ hierarchyWriter,
2065
+ hierarchyBindings,
2066
+ transformWriter,
2067
+ transformBindings,
2068
+ scratch,
2069
+ report
2070
+ );
2071
+ }
2072
+ }
2073
+ for (let bindingIndex = 0; bindingIndex < hierarchyBindings.length; bindingIndex += 1) {
2074
+ const changed = scratch.hierarchyChanged[bindingIndex];
2075
+ if (changed === void 0) continue;
2076
+ countPublishedRows(changed);
2077
+ const published = hierarchyWriter.publishChangedRows(bindingIndex, changed);
2078
+ if (!published.ok) {
2079
+ const cause = published.error;
2080
+ report(derivedWriteError(cause, bindingIndex));
2081
+ }
2082
+ }
1563
2083
  return firstError === void 0 ? ok5(void 0) : err5(firstError);
1564
2084
  }
1565
2085
  function propagateTransforms(world) {
1566
2086
  const scratch = scratchFor(world);
1567
2087
  const pairs = validateTransformPairs(world, scratch);
1568
2088
  if (!pairs.ok) return pairs;
1569
- const flat = propagateFlat(scratch);
2089
+ const flat = propagateFlat(world, scratch);
1570
2090
  if (!flat.ok) return flat;
1571
2091
  return propagateHierarchy(world, scratch);
1572
2092
  }