@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.
- package/dist/device.js +113 -5
- package/dist/device.js.map +1 -1
- package/dist/deviceActionValidation.js +1 -1
- package/dist/deviceActionValidation.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/plugins/beamng-vehicle/bin/__pycache__/beamng_abs.cpython-312.pyc +0 -0
- package/plugins/beamng-vehicle/bin/__pycache__/beamng_audio.cpython-312.pyc +0 -0
- package/plugins/beamng-vehicle/bin/__pycache__/beamng_engine.cpython-312.pyc +0 -0
- package/plugins/beamng-vehicle/bin/__pycache__/beamng_expression.cpython-312.pyc +0 -0
- package/plugins/beamng-vehicle/bin/__pycache__/beamng_jbeam.cpython-312.pyc +0 -0
- package/plugins/beamng-vehicle/bin/__pycache__/beamng_materials.cpython-312.pyc +0 -0
- package/plugins/beamng-vehicle/bin/__pycache__/beamng_stock_events.cpython-312.pyc +0 -0
- package/plugins/beamng-vehicle/bin/__pycache__/fmod_studio.cpython-312.pyc +0 -0
- package/plugins/beamng-vehicle/bin/__pycache__/fsb5.cpython-312.pyc +0 -0
- package/plugins/beamng-vehicle/bin/__pycache__/sim_audio.cpython-312.pyc +0 -0
- package/plugins/beamng-vehicle/bin/__pycache__/sim_audio_ac.cpython-312.pyc +0 -0
- package/plugins/beamng-vehicle/bin/__pycache__/sim_audio_beamng.cpython-312.pyc +0 -0
- package/plugins/beamng-vehicle/bin/__pycache__/sim_audio_fixtures.cpython-312.pyc +0 -0
- package/plugins/beamng-vehicle/bin/__pycache__/test_beamng_engine.cpython-312.pyc +0 -0
- 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
|
|
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
|
-
|
|
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)}"`);
|