@hypersoniclabs/helix-cli 0.1.25-helix3.523 → 0.1.25-helix3.525

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 (21) hide show
  1. package/dist/device.js +113 -5
  2. package/dist/device.js.map +1 -1
  3. package/dist/deviceActionValidation.js +1 -1
  4. package/dist/deviceActionValidation.js.map +1 -1
  5. package/dist/tsconfig.build.tsbuildinfo +1 -1
  6. package/package.json +1 -1
  7. package/plugins/beamng-vehicle/bin/__pycache__/beamng_abs.cpython-312.pyc +0 -0
  8. package/plugins/beamng-vehicle/bin/__pycache__/beamng_audio.cpython-312.pyc +0 -0
  9. package/plugins/beamng-vehicle/bin/__pycache__/beamng_engine.cpython-312.pyc +0 -0
  10. package/plugins/beamng-vehicle/bin/__pycache__/beamng_expression.cpython-312.pyc +0 -0
  11. package/plugins/beamng-vehicle/bin/__pycache__/beamng_jbeam.cpython-312.pyc +0 -0
  12. package/plugins/beamng-vehicle/bin/__pycache__/beamng_materials.cpython-312.pyc +0 -0
  13. package/plugins/beamng-vehicle/bin/__pycache__/beamng_stock_events.cpython-312.pyc +0 -0
  14. package/plugins/beamng-vehicle/bin/__pycache__/fmod_studio.cpython-312.pyc +0 -0
  15. package/plugins/beamng-vehicle/bin/__pycache__/fsb5.cpython-312.pyc +0 -0
  16. package/plugins/beamng-vehicle/bin/__pycache__/sim_audio.cpython-312.pyc +0 -0
  17. package/plugins/beamng-vehicle/bin/__pycache__/sim_audio_ac.cpython-312.pyc +0 -0
  18. package/plugins/beamng-vehicle/bin/__pycache__/sim_audio_beamng.cpython-312.pyc +0 -0
  19. package/plugins/beamng-vehicle/bin/__pycache__/sim_audio_fixtures.cpython-312.pyc +0 -0
  20. package/plugins/beamng-vehicle/bin/__pycache__/test_beamng_engine.cpython-312.pyc +0 -0
  21. package/plugins/beamng-vehicle/bin/__pycache__/test_sim_audio.cpython-312.pyc +0 -0
package/dist/device.js CHANGED
@@ -364,6 +364,9 @@ const DEVICE_GRAPH_MAX_BRANCH_DEPTH = 8;
364
364
  * The interaction `display` hint: `{ kind: 'time', stateKey }` renders an
365
365
  * epoch-millisecond state value in the VIEWER's local time ("rings 7:00 AM"),
366
366
  * and "Off" when it is 0 or absent. The hint is presentation only.
367
+ *
368
+ * `zoneOffsetStateKey` (optional) names an integer/float state holding the UTC offset in minutes of the zone the value was set in
369
+ * (engine 0.3.417+): the row then reads in that zone, labelled ("11:34 PM, UTC-7"), for every viewer instead of each viewer's own.
367
370
  */
368
371
  function validateInteractionDisplay(raw, id, state, errors) {
369
372
  const path = `interaction ${id} display`;
@@ -376,7 +379,7 @@ function validateInteractionDisplay(raw, id, state, errors) {
376
379
  errors.push(`${path}.kind must be "time"`);
377
380
  }
378
381
  for (const key of Object.keys(display)) {
379
- if (key !== 'kind' && key !== 'stateKey') {
382
+ if (key !== 'kind' && key !== 'stateKey' && key !== 'zoneOffsetStateKey') {
380
383
  errors.push(`${path}: unknown field "${key}"`);
381
384
  }
382
385
  }
@@ -392,6 +395,19 @@ function validateInteractionDisplay(raw, id, state, errors) {
392
395
  errors.push(`${path}.stateKey "${String(display.stateKey)}" must be a float or integer state holding epoch milliseconds`);
393
396
  }
394
397
  }
398
+ if (display.zoneOffsetStateKey !== undefined) {
399
+ const zoneKey = String(display.zoneOffsetStateKey);
400
+ const zoneSpec = object(state[zoneKey]);
401
+ if (!isNonEmptyName(display.zoneOffsetStateKey)) {
402
+ errors.push(`${path}.zoneOffsetStateKey must be a non-empty state key`);
403
+ }
404
+ else if (state[zoneKey] === undefined) {
405
+ errors.push(`${path}.zoneOffsetStateKey "${zoneKey}" is not declared in state`);
406
+ }
407
+ else if (zoneSpec.type !== 'float' && zoneSpec.type !== 'integer') {
408
+ errors.push(`${path}.zoneOffsetStateKey "${zoneKey}" must be a float or integer state holding minutes east of UTC`);
409
+ }
410
+ }
395
411
  }
396
412
  /**
397
413
  * Validate one behavior-graph step list, recursing into every nested list.
@@ -461,8 +477,36 @@ function validateGraphSteps(steps, context, path, depth) {
461
477
  graphNode(context, step.node, stepPath);
462
478
  if (!['x', 'y', 'z'].includes(String(step.axis)))
463
479
  context.errors.push(`${stepPath}: rotateNode axis must be x, y or z`);
464
- if (!['toAngle', 'stop'].includes(String(step.mode)))
465
- context.errors.push(`${stepPath}: rotateNode mode must be toAngle or stop`);
480
+ if (!['toAngle', 'stop', 'spin', 'sweep'].includes(String(step.mode)))
481
+ context.errors.push(`${stepPath}: rotateNode mode must be toAngle, stop, spin or sweep`);
482
+ // spin turns continuously at `rpm` (an expression, so Speed can drive it); sweep swings `amplitudeDeg` either side every `periodMs`
483
+ // (device audio r7 engine). Both are presentation only.
484
+ if (step.mode === 'spin') {
485
+ if (step.rpm === undefined)
486
+ context.errors.push(`${stepPath}: rotateNode spin needs rpm`);
487
+ else
488
+ validateGraphExpr(step.rpm, context, `${stepPath}.rpm`);
489
+ }
490
+ else if (step.rpm !== undefined) {
491
+ context.errors.push(`${stepPath}: rotateNode rpm applies to mode "spin" only`);
492
+ }
493
+ if (step.mode === 'sweep') {
494
+ if (step.amplitudeDeg === undefined || step.periodMs === undefined)
495
+ context.errors.push(`${stepPath}: rotateNode sweep needs amplitudeDeg and periodMs`);
496
+ if (step.amplitudeDeg !== undefined &&
497
+ (!isFiniteNumber(step.amplitudeDeg) ||
498
+ step.amplitudeDeg < 0 ||
499
+ step.amplitudeDeg > 180))
500
+ context.errors.push(`${stepPath}: rotateNode amplitudeDeg must be a number from 0 to 180`);
501
+ if (step.periodMs !== undefined &&
502
+ (!isFiniteNumber(step.periodMs) ||
503
+ step.periodMs < 200 ||
504
+ step.periodMs > 60_000))
505
+ context.errors.push(`${stepPath}: rotateNode periodMs must be a number from 200 to 60000`);
506
+ }
507
+ else if (step.amplitudeDeg !== undefined || step.periodMs !== undefined) {
508
+ context.errors.push(`${stepPath}: rotateNode amplitudeDeg and periodMs apply to mode "sweep" only`);
509
+ }
466
510
  if (step.targetDeg !== undefined)
467
511
  validateGraphExpr(step.targetDeg, context, `${stepPath}.targetDeg`);
468
512
  if (step.durationMs !== undefined && !isFiniteNumber(step.durationMs))
@@ -1240,7 +1284,8 @@ function applySteps(steps, definition, state, budget) {
1240
1284
  }
1241
1285
  function invoke(definition, state, interaction, value) {
1242
1286
  const control = object(interaction.control);
1243
- if (control.stateKey && value !== undefined) {
1287
+ // a time control's value is a time of day, not the state value: its action writes the instant
1288
+ if (control.stateKey && control.kind !== 'time' && value !== undefined) {
1244
1289
  const key = String(control.stateKey);
1245
1290
  state[key] = coerce(object(object(definition.state)[key]), value);
1246
1291
  }
@@ -1258,6 +1303,8 @@ function exerciseValue(interaction, state) {
1258
1303
  const control = object(interaction.control);
1259
1304
  if (control.kind === 'number')
1260
1305
  return Number(control.max ?? control.min ?? 1);
1306
+ if (control.kind === 'time')
1307
+ return 7 * 3600 + 35 * 60; // seconds after the viewer's local midnight
1261
1308
  if (control.kind === 'choice') {
1262
1309
  const options = Array.isArray(control.options)
1263
1310
  ? control.options.map(object)
@@ -1470,6 +1517,41 @@ function validateDeviceProject(input, options = {}) {
1470
1517
  if (part.pivot !== undefined && part.pivot !== 'bottom') {
1471
1518
  errors.push(`${partPath}.pivot must be bottom`);
1472
1519
  }
1520
+ // `parent` (device audio r7 engine): position and rotationDeg are then in that part's frame and a step that moves the parent carries its subtree.
1521
+ if (part.parent !== undefined) {
1522
+ if (typeof part.parent !== 'string' || !part.parent.length || part.parent.length > 64) {
1523
+ errors.push(`${partPath}.parent must be the name of another part`);
1524
+ }
1525
+ else if (part.parent === name) {
1526
+ errors.push(`${partPath}.parent cannot be the part itself`);
1527
+ }
1528
+ else if (!Object.prototype.hasOwnProperty.call(parts, part.parent)) {
1529
+ errors.push(`${partPath}.parent "${part.parent}" is not a part of this model`);
1530
+ }
1531
+ else {
1532
+ let depth = 0;
1533
+ let cursor = name;
1534
+ const seen = new Set();
1535
+ while (cursor !== undefined && object(parts[cursor]).parent !== undefined) {
1536
+ if (seen.has(cursor)) {
1537
+ errors.push(`${partPath}.parent chain loops back on "${cursor}"`);
1538
+ break;
1539
+ }
1540
+ seen.add(cursor);
1541
+ const next = object(parts[cursor]).parent;
1542
+ cursor = typeof next === 'string' ? next : undefined;
1543
+ depth += 1;
1544
+ if (depth > 8) {
1545
+ errors.push(`${partPath}.parent chain is deeper than 8`);
1546
+ break;
1547
+ }
1548
+ }
1549
+ }
1550
+ }
1551
+ // `origin`: the shape-local point that sits at `position` and that the part (and its children) turns about (generalises pivot "bottom").
1552
+ if (part.origin !== undefined && !isNumberTriple(part.origin)) {
1553
+ errors.push(`${partPath}.origin must be three numbers`);
1554
+ }
1473
1555
  if (part.color !== undefined && !Number.isInteger(part.color)) {
1474
1556
  errors.push(`${partPath}.color must be an integer`);
1475
1557
  }
@@ -1650,6 +1732,32 @@ function validateDeviceProject(input, options = {}) {
1650
1732
  errors.push(`interaction ${id} disabled choice "${String(option.value)}" needs a reason`);
1651
1733
  }
1652
1734
  }
1735
+ else if (control.kind === 'time') {
1736
+ // A time of day set and shown in the VIEWER's zone (device audio r7 engine). The room keeps one absolute instant in `stateKey`; the control's
1737
+ // `value` is seconds after the viewer's local midnight and is NOT written to the state: the action turns it into the instant.
1738
+ if (!['float', 'integer'].includes(String(spec.type)))
1739
+ errors.push(`interaction ${id} time control stateKey "${key}" must be a declared integer or float state (epoch milliseconds)`);
1740
+ if (control.setStateKey !== undefined) {
1741
+ const setSpec = object(state[String(control.setStateKey)]);
1742
+ if (!['float', 'integer'].includes(String(setSpec.type)))
1743
+ errors.push(`interaction ${id} time control setStateKey "${String(control.setStateKey)}" must be a declared integer or float state (epoch milliseconds)`);
1744
+ }
1745
+ if (control.stepMinutes !== undefined &&
1746
+ (!Number.isInteger(control.stepMinutes) ||
1747
+ Number(control.stepMinutes) < 1 ||
1748
+ Number(control.stepMinutes) > 60))
1749
+ errors.push(`interaction ${id} time stepMinutes must be an integer 1..60`);
1750
+ const action = object(interaction.action);
1751
+ if (interaction.action === undefined) {
1752
+ errors.push(`interaction ${id} a time control needs an action that turns value (seconds after the viewer's local midnight) into the instant`);
1753
+ }
1754
+ else if (!Object.values(object(action.args)).some((arg) => {
1755
+ const a = object(arg);
1756
+ return a.type === 'number' && a.fill === 'viewer.localMidnightSec';
1757
+ })) {
1758
+ errors.push(`interaction ${id} a time control's action needs a number arg with fill "viewer.localMidnightSec" to turn the time of day into an instant`);
1759
+ }
1760
+ }
1653
1761
  else if (control.kind === 'text') {
1654
1762
  if (spec.type !== 'string')
1655
1763
  errors.push(`interaction ${id} text control must target string state`);
@@ -1668,7 +1776,7 @@ function validateDeviceProject(input, options = {}) {
1668
1776
  }
1669
1777
  }
1670
1778
  else if (control.kind === undefined) {
1671
- errors.push(`interaction ${id} control requires a kind (number|choice|text)`);
1779
+ errors.push(`interaction ${id} control requires a kind (number|choice|text|time)`);
1672
1780
  }
1673
1781
  else {
1674
1782
  errors.push(`interaction ${id} uses unsupported control kind "${String(control.kind)}"`);