@helix3/helix-cli 0.1.14-helix3.200 → 0.1.14-helix3.202
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/package.json +2 -2
- package/plugins/beamng-vehicle/README.md +8 -2
- 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/beamng-vehicle.mjs +385 -4
- package/plugins/beamng-vehicle/bin/beamng_expression.py +373 -0
- package/plugins/beamng-vehicle/bin/lower-beamng.py +1080 -81
- package/plugins/beamng-vehicle/bridge-plugin.json +2 -2
- package/plugins/beamng-vehicle/lib/vehicle-source.mjs +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@helix3/helix-cli",
|
|
3
|
-
"version": "0.1.14-helix3.
|
|
3
|
+
"version": "0.1.14-helix3.202",
|
|
4
4
|
"description": "helix — the HELIX creator CLI: scaffold, validate, and publish Instant Worlds",
|
|
5
5
|
"main": "dist/lib.js",
|
|
6
6
|
"types": "dist/lib.d.ts",
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
"@gltf-transform/extensions": "^4.4.0",
|
|
64
64
|
"@gltf-transform/functions": "^4.4.0",
|
|
65
65
|
"@hypersoniclabs/helix-manifest": "npm:@helix3/helix-manifest@0.3.25-helix3.68",
|
|
66
|
-
"@hypersoniclabs/helix-sdk": "npm:@helix3/helix-sdk@0.1.
|
|
66
|
+
"@hypersoniclabs/helix-sdk": "npm:@helix3/helix-sdk@0.1.6-helix3.101",
|
|
67
67
|
"commander": "^13.0.0",
|
|
68
68
|
"ktx2-encoder": "^0.5.3",
|
|
69
69
|
"meshoptimizer": "^0.22.0",
|
|
@@ -41,8 +41,14 @@ External-plugin execution still requires the Bridge plan key and all permissions
|
|
|
41
41
|
|
|
42
42
|
The converter reads all four BeamNG material stages. Later authored stages win, while conflicts, declaration provenance, null sentinels, unknown maps, and dynamic `@...` render targets remain explicit diagnostics. Texture resolution is deterministic: selected vehicle files, then mod files, then the reviewed base-game common snapshot; same-tier ambiguity refuses the binding. This parser is packaged with the plugin and never imports engine-repository code at runtime.
|
|
43
43
|
|
|
44
|
-
Each successful terminal package emits
|
|
44
|
+
Each successful terminal package emits six assets: the canonical GLB, `<vehicle>.vehicle-source.json`, the versioned `<vehicle>.vehicle-lowering.json` diagnostic aggregate, `<vehicle>.vehicle-package.json`, `<vehicle>.vehicle-addons.json`, and `<vehicle>.vehicle-build.json`. The lowering aggregate makes source semantics, structural exclusions, runtime blockers, functional-domain status, and provenance available to CLI and agent consumers instead of discarding them after packaging. The source manifest remains embedded in GLB extras and records the exact configuration and slot closure, fingerprints, material-resolution provenance, and coverage ledger.
|
|
45
45
|
|
|
46
|
-
`lower-beamng.py` compiles the exact selected `.pc` and admitted JBeam closure into `helix.vehicle-package/2`, stable add-on records, and a source Build. Source literals cover the torque curve, idle/limiter, engine losses, gearbox and final drive, differentials, lights/electrics, audio event references, seats/cameras, and rigid openables when present.
|
|
46
|
+
`lower-beamng.py` compiles the exact selected `.pc` and admitted JBeam closure into `helix.vehicle-package/2`, stable add-on records, and a source Build. Source literals cover the torque curve, idle/limiter, engine losses, gearbox and final drive, differentials, selected front/rear `pressureWheels` tire/brake values, lights/electrics, audio event references, seats/cameras, and rigid openables when present. Brake, suspension, tire, and differential expressions use a bounded data-only resolver for selected numeric variables, arithmetic, comparisons, boolean conditionals, and `case()`; direct selected-variable references are supported, source code is never evaluated, and unknown variables, calls, or tokens remain explicitly unsupported. HELIX total brake torque is derived from BeamNG's per-wheel axle values and the exact `.pc` variable overrides take precedence over selected JBeam defaults.
|
|
47
|
+
|
|
48
|
+
Driveline topology is derived from the selected device graph: each connected wheel is traced upstream to the exact selected gearbox, so FWD and RWD packages contain only their driven axle and AWD requires an actual common centre differential. Duplicate devices, cycles, half-connected axles, missing final drives, unknown differential types, invalid fields, and ambiguous output ports retain source receipts but suppress runtime differentials instead of inventing a topology. Open, locked, LSD, and unit-exponent viscous devices lower exactly when every selected behavior field is representable. Nonzero BeamNG torque-loss, virtual-inertia, speed-limit, active-lock/controller, and non-unit viscous-exponent semantics remain explicit parity blockers; a topology-safe approximation stays runnable with `runtimeStatus=approximate` while `functionalDomains.differentials=false`.
|
|
49
|
+
|
|
50
|
+
Selected tires lower one exact common or vehicle-local leaf per axle. Pressure, radius, width, friction, sliding friction, and load-sensitivity values keep literal/derived/unsupported receipts. HELIX's engine-wide exponential load kernel is deterministically sampled at 24 log-spaced points from 100 N to 40 kN for its compact `muLut`; Pacejka-style shape coefficients remain explicitly identified HELIX priors. Complete selected extraction sets `extractionComplete=true` and uses the selected radius/load data, but remains `functionalDomains.wheelsTyres=false` because the current runtime has no pressure or tire-width fields and still uses shape priors. Missing, duplicate, invalid, or unresolved tire semantics instead produce a complete prior runtime fallback, never a parity claim.
|
|
51
|
+
|
|
52
|
+
Selected suspension evidence is lowered only from the chosen strut/coilover leaf part on each axle. Matching left/right spring, slow/fast bump, slow/fast rebound, velocity-split, precompression, and bound rows are retained with beam-space frequency and damping-ratio derivations. Structural parent/subframe beams and swaybar construction beams are explicitly excluded by part custody. Selected torsionbar stiffness is retained in `N.m/rad`, but the runtime suspension remains fail-closed: BeamNG element rates need motion-ratio geometry to become HELIX wheel rates, and the current package has one global damping triplet with no axle-specific fast/slow channels. The emitted HELIX suspension therefore remains an explicit fallback and `functionalDomains.suspension` remains false until those exact runtime mappings exist. Raw Lua/controller and other unsupported expressions remain explicit provenance instead of being presented as literal runtime parity. Soft-body deformation, fluids, wear, and detailed damage remain out of scope. Rigid panel poses are semantic on every device, physical bodies begin at `medium`, and detachment remains disabled on every production tier while source break groups are retained as future eligibility. Simulation, authority, package identity, and Build selection are quality-invariant. Bridge currently seals one canonical source GLB and truthfully maps every tier to it; optimized mesh/texture rendition IDs must not be selected until the optimizer has materialized and sealed those artifacts.
|
|
47
53
|
|
|
48
54
|
The engine consumer seam is `vehiclePackageToTuning(package, { landSolver: 'v2' })`. The source Build intentionally contains stable source identities rather than invented Continuum CIDs; after publication resolves immutable Package references, the existing `createPrivateBuildDraft` → `freezeBuildDraft` workflow is the canonical Build seam.
|
|
Binary file
|
|
Binary file
|
|
@@ -514,6 +514,386 @@ function assertValidVehiclePackage(pkg, boundary) {
|
|
|
514
514
|
}
|
|
515
515
|
}
|
|
516
516
|
|
|
517
|
+
function record(value) {
|
|
518
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function assertValidFunctionalLowering(lowering, boundary) {
|
|
522
|
+
const semantics = lowering?.sourceSemantics;
|
|
523
|
+
const suspension = semantics?.suspension;
|
|
524
|
+
const coverage = lowering?.coverage;
|
|
525
|
+
const suspensionCounts = suspension?.counts;
|
|
526
|
+
const suspensionProvenance = coverage?.suspensionProvenance;
|
|
527
|
+
const requiredSuspensionCounts = ['selectedLeafParts', 'excludedStructuralParts', 'resolvedAxles', 'sourceRows'];
|
|
528
|
+
const requiredProvenanceCounts = ['source-literal', 'derived', 'defaulted', 'unsupported'];
|
|
529
|
+
const nonempty = (value) => typeof value === 'string' && value.length > 0;
|
|
530
|
+
const explicitUnsupported = (value) => record(value) && value.status === 'unsupported' && nonempty(value.reason);
|
|
531
|
+
const semanticReceipt = (value) => record(value)
|
|
532
|
+
&& ['literal', 'derived', 'defaulted', 'unsupported'].includes(value.status) && nonempty(value.unit)
|
|
533
|
+
&& Array.isArray(value.variables)
|
|
534
|
+
&& (value.status === 'unsupported' ? nonempty(value.reason) : Number.isFinite(value.value));
|
|
535
|
+
const selectedReceipt = (value) => semanticReceipt(value) && (value.status === 'unsupported'
|
|
536
|
+
|| (Object.hasOwn(value, 'raw') && value.symmetricRows === 2));
|
|
537
|
+
const dampingReceipt = (value) => semanticReceipt(value) && (value.status === 'unsupported'
|
|
538
|
+
|| (Object.hasOwn(value, 'raw') && value.symmetricRows === 2
|
|
539
|
+
&& semanticReceipt(value.velocitySplit) && Object.hasOwn(value.velocitySplit, 'raw')
|
|
540
|
+
&& value.velocitySplit.symmetricRows === 2 && value.velocitySplit.unit === 'm/s'
|
|
541
|
+
&& value.velocitySplit.status !== 'unsupported' && value.velocitySplit.value >= 0));
|
|
542
|
+
const torsionReceipt = (value) => semanticReceipt(value)
|
|
543
|
+
&& (value.status === 'unsupported' || Object.hasOwn(value, 'raw'));
|
|
544
|
+
const suspensionAxle = (value) => {
|
|
545
|
+
if (explicitUnsupported(value) && Array.isArray(value.candidates)) return true;
|
|
546
|
+
if (!record(value) || !record(value.damping)) return false;
|
|
547
|
+
const receipts = [value.springRate, value.precompressionRange, value.bumpLimit,
|
|
548
|
+
...['slowBump', 'fastBump', 'slowRebound', 'fastRebound'].map((name) => value.damping[name])];
|
|
549
|
+
const receiptShapeValid = receipts.slice(0, 3).every(selectedReceipt)
|
|
550
|
+
&& receipts.slice(3).every(dampingReceipt);
|
|
551
|
+
const expectedStatus = receipts.every((receipt) => receipt?.status !== 'unsupported')
|
|
552
|
+
? 'resolved' : 'unsupported';
|
|
553
|
+
return value.status === expectedStatus
|
|
554
|
+
&& nonempty(value.slotType) && nonempty(value.partName) && nonempty(value.source)
|
|
555
|
+
&& receiptShapeValid && record(value.derived) && record(value.rowCounts)
|
|
556
|
+
&& ['spring', 'damper', 'travelLimit'].every((name) => Number.isInteger(value.rowCounts[name])
|
|
557
|
+
&& value.rowCounts[name] >= 0);
|
|
558
|
+
};
|
|
559
|
+
const antiRollAxle = (value) => explicitUnsupported(value) || (record(value)
|
|
560
|
+
&& nonempty(value.slotType) && nonempty(value.partName) && nonempty(value.source)
|
|
561
|
+
&& torsionReceipt(value.torsionSpring) && value.runtimeStatus === 'unsupported' && nonempty(value.reason));
|
|
562
|
+
const tyreFields = ['pressurePSI', 'radius', 'tireWidth', 'frictionCoef', 'slidingFrictionCoef',
|
|
563
|
+
'noLoadCoef', 'fullLoadCoef', 'loadSensitivitySlope'];
|
|
564
|
+
const tyreAxle = (value) => {
|
|
565
|
+
if (explicitUnsupported(value) && Array.isArray(value.candidates)) return true;
|
|
566
|
+
if (!record(value) || !nonempty(value.partName) || !nonempty(value.source)
|
|
567
|
+
|| !tyreFields.every((name) => semanticReceipt(value[name]) && Object.hasOwn(value[name], 'raw'))) return false;
|
|
568
|
+
const expectedStatus = tyreFields.every((name) => value[name].status !== 'unsupported')
|
|
569
|
+
? 'functional' : 'unsupported';
|
|
570
|
+
return value.status === expectedStatus;
|
|
571
|
+
};
|
|
572
|
+
const tyres = semantics?.wheelsTyres;
|
|
573
|
+
const tyreFormula = 'frictionCoef * (fullLoadCoef + (noLoadCoef - fullLoadCoef) * exp(-loadSensitivitySlope * fz))';
|
|
574
|
+
const expectedTyreLoads = Array.from({ length: 24 }, (_, index) => Math.round(Math.exp(
|
|
575
|
+
Math.log(100) + ((Math.log(40000) - Math.log(100)) * index) / 23,
|
|
576
|
+
) * 1e9) / 1e9);
|
|
577
|
+
const selectedTyresComplete = ['front', 'rear'].every((axle) => record(tyres?.axles?.[axle])
|
|
578
|
+
&& tyres.axles[axle].status === 'functional'
|
|
579
|
+
&& tyreFields.every((name) => tyres.axles[axle][name]?.status !== 'unsupported'));
|
|
580
|
+
const runtimeTyres = lowering?.vehiclePackage?.simulation?.land?.tyreByAxle;
|
|
581
|
+
const approximately = (left, right, epsilon = 1e-8) => Number.isFinite(left) && Number.isFinite(right)
|
|
582
|
+
&& Math.abs(left - right) <= epsilon;
|
|
583
|
+
const tyreShapePrior = { alphaPeakRad: 0.139626, cxMult: 1.05, sigmaX: 0.048,
|
|
584
|
+
sigmaY: 0.08, camberGain: 0.15, dxOverDy: 1, sFall: 0.3,
|
|
585
|
+
rollingResistance: 0.013, angularInertia: 1.3 };
|
|
586
|
+
const validRuntimeTyre = (runtime, axle) => {
|
|
587
|
+
const source = tyres?.axles?.[axle];
|
|
588
|
+
if (!record(runtime) || !record(source) || !record(runtime.loadSensitivity)
|
|
589
|
+
|| !record(runtime.muLut) || runtime.calibration !== 'prior'
|
|
590
|
+
|| !approximately(runtime.radius, source.radius?.value)
|
|
591
|
+
|| !Array.isArray(runtime.muLut.fz) || !equalJson(runtime.muLut.fz, tyres?.lut?.fz)
|
|
592
|
+
|| !Array.isArray(runtime.muLut.mu) || runtime.muLut.mu.length !== runtime.muLut.fz.length
|
|
593
|
+
|| !Object.entries(tyreShapePrior).every(([name, value]) => approximately(runtime[name], value))) return false;
|
|
594
|
+
for (const field of ['frictionCoef', 'slidingFrictionCoef', 'noLoadCoef', 'fullLoadCoef', 'loadSensitivitySlope']) {
|
|
595
|
+
if (!approximately(runtime.loadSensitivity[field], source[field]?.value)) return false;
|
|
596
|
+
}
|
|
597
|
+
const slideRatio = source.slidingFrictionCoef.value / source.frictionCoef.value;
|
|
598
|
+
if (!approximately(runtime.muLut.slideRatio, Math.round(slideRatio * 1e6) / 1e6)) return false;
|
|
599
|
+
return runtime.muLut.fz.every((load, index) => approximately(runtime.muLut.mu[index],
|
|
600
|
+
Math.round((source.frictionCoef.value * (source.fullLoadCoef.value
|
|
601
|
+
+ (source.noLoadCoef.value - source.fullLoadCoef.value)
|
|
602
|
+
* Math.exp(-source.loadSensitivitySlope.value * load))) * 1e9) / 1e9));
|
|
603
|
+
};
|
|
604
|
+
const validFallbackTyre = (runtime) => record(runtime) && record(runtime.loadSensitivity)
|
|
605
|
+
&& record(runtime.muLut) && runtime.calibration === 'prior'
|
|
606
|
+
&& approximately(runtime.radius, 0.3466)
|
|
607
|
+
&& Object.entries(tyreShapePrior).every(([name, value]) => approximately(runtime[name], value))
|
|
608
|
+
&& equalJson(runtime.muLut.fz, tyres?.lut?.fz)
|
|
609
|
+
&& Array.isArray(runtime.muLut.mu) && Array.isArray(runtime.muLut.fz)
|
|
610
|
+
&& runtime.muLut.mu.length === runtime.muLut.fz.length
|
|
611
|
+
&& runtime.muLut.mu.every((value) => approximately(value, 1.1))
|
|
612
|
+
&& approximately(runtime.muLut.slideRatio, 0.85)
|
|
613
|
+
&& equalJson(runtime.loadSensitivity, { frictionCoef: 1, slidingFrictionCoef: 0.85,
|
|
614
|
+
noLoadCoef: 1.1, fullLoadCoef: 1.1, loadSensitivitySlope: 0 });
|
|
615
|
+
const validTyres = record(tyres) && tyres.status === 'unsupported'
|
|
616
|
+
&& record(tyres.axles) && tyreAxle(tyres.axles.front) && tyreAxle(tyres.axles.rear)
|
|
617
|
+
&& record(tyres.variables) && record(tyres.lut) && Array.isArray(tyres.lut.fz)
|
|
618
|
+
&& equalJson(tyres.lut.fz, expectedTyreLoads) && tyres.lut.formula === tyreFormula
|
|
619
|
+
&& Array.isArray(tyres.runtimeBlockers) && tyres.runtimeBlockers.length > 0
|
|
620
|
+
&& typeof tyres.extractionComplete === 'boolean'
|
|
621
|
+
&& tyres.extractionComplete === selectedTyresComplete
|
|
622
|
+
&& Array.isArray(runtimeTyres) && runtimeTyres.length === 2
|
|
623
|
+
&& (tyres.extractionComplete
|
|
624
|
+
? validRuntimeTyre(runtimeTyres[0], 'front') && validRuntimeTyre(runtimeTyres[1], 'rear')
|
|
625
|
+
: runtimeTyres.every(validFallbackTyre));
|
|
626
|
+
const driveline = semantics?.driveline;
|
|
627
|
+
const differentialFields = ['lockTorque', 'activeLockTorque', 'viscousCoef', 'viscousTorque', 'lsdPreload',
|
|
628
|
+
'lsdLockCoef', 'lsdRevLockCoef', 'friction', 'dynamicFriction', 'torqueLossCoef',
|
|
629
|
+
'defaultVirtualInertia', 'speedLimitCoef', 'diffTorqueSplit'];
|
|
630
|
+
const differential = (value) => record(value) && nonempty(value.deviceName)
|
|
631
|
+
&& nonempty(value.partName) && nonempty(value.source) && nonempty(value.type)
|
|
632
|
+
&& differentialFields.every((name) => semanticReceipt(value.fields?.[name])
|
|
633
|
+
&& Object.hasOwn(value.fields[name], 'raw'))
|
|
634
|
+
&& Array.isArray(value.controllers) && value.controllers.every(nonempty)
|
|
635
|
+
&& Array.isArray(value.overlaySources) && value.overlaySources.every(nonempty)
|
|
636
|
+
&& record(value.overlayFieldSources)
|
|
637
|
+
&& Object.values(value.overlayFieldSources).every((sources) => Array.isArray(sources)
|
|
638
|
+
&& sources.length > 0 && sources.every(nonempty))
|
|
639
|
+
&& Array.isArray(value.overlayConflicts)
|
|
640
|
+
&& value.overlayConflicts.every((conflict) => record(conflict) && nonempty(conflict.field)
|
|
641
|
+
&& Array.isArray(conflict.sources) && conflict.sources.length > 1 && conflict.sources.every(nonempty))
|
|
642
|
+
&& (value.type !== 'viscous' || (semanticReceipt(value.fields?.viscousExponent)
|
|
643
|
+
&& Object.hasOwn(value.fields.viscousExponent, 'raw')))
|
|
644
|
+
&& semanticReceipt(value.gearRatio) && record(value.outputPorts)
|
|
645
|
+
&& [value.outputPorts.a, value.outputPorts.b].every((port) => port === 1 || port === 2)
|
|
646
|
+
&& value.outputPorts.a !== value.outputPorts.b;
|
|
647
|
+
const drivenAxles = Array.isArray(driveline?.drivenAxles) ? driveline.drivenAxles : [];
|
|
648
|
+
const differentialEntries = record(driveline?.differentials)
|
|
649
|
+
? Object.entries(driveline.differentials) : [];
|
|
650
|
+
const runtimeDriveline = lowering?.vehiclePackage?.simulation?.land?.driveline;
|
|
651
|
+
const hasRuntimeDiff = (name) => record(runtimeDriveline) && record(runtimeDriveline[name]);
|
|
652
|
+
const expectedDiffCount = drivenAxles.length + (drivenAxles.length === 2 ? 1 : 0);
|
|
653
|
+
const expectedLocations = [...drivenAxles, ...(drivenAxles.length === 2 ? ['centre'] : [])].sort();
|
|
654
|
+
const semanticLocations = differentialEntries.map(([location]) => location).sort();
|
|
655
|
+
const runtimeLocations = ['front', 'rear', 'centre'].filter(hasRuntimeDiff).sort();
|
|
656
|
+
const runtimeFields = {
|
|
657
|
+
lockTorque: 'lockTorque', viscousCoef: 'viscousCoef', viscousTorque: 'viscousTorque',
|
|
658
|
+
lsdPreload: 'lsdPreload', lsdLockCoef: 'lsdLockCoef', lsdRevLockCoef: 'lsdRevLockCoef',
|
|
659
|
+
friction: 'friction', dynamicFriction: 'dynamicFriction',
|
|
660
|
+
};
|
|
661
|
+
const runtimeCriticalDifferentialFields = new Set([
|
|
662
|
+
'type', 'name', 'inputName', 'inputIndex', 'diffType', 'gearRatio', 'viscousExponent',
|
|
663
|
+
...differentialFields,
|
|
664
|
+
]);
|
|
665
|
+
const axleWheels = { front: ['FL', 'FR'], rear: ['RL', 'RR'] };
|
|
666
|
+
const expectedWheels = drivenAxles.flatMap((axle) => axleWheels[axle]).sort();
|
|
667
|
+
const wheelPathEntries = record(driveline?.wheelPaths) ? Object.entries(driveline.wheelPaths) : [];
|
|
668
|
+
const validWheelPaths = nonempty(driveline?.transmissionDevice)
|
|
669
|
+
&& equalJson(wheelPathEntries.map(([wheel]) => wheel).sort(), expectedWheels)
|
|
670
|
+
&& wheelPathEntries.every(([, path]) => Array.isArray(path) && path.length > 0
|
|
671
|
+
&& path.every(nonempty) && new Set(path).size === path.length
|
|
672
|
+
&& path.at(-1) === driveline.transmissionDevice)
|
|
673
|
+
&& drivenAxles.every((axle) => axleWheels[axle].every((wheel) =>
|
|
674
|
+
driveline.wheelPaths[wheel].includes(driveline.differentials[axle]?.deviceName)))
|
|
675
|
+
&& (drivenAxles.length !== 2 || expectedWheels.every((wheel) =>
|
|
676
|
+
driveline.wheelPaths[wheel].includes(driveline.differentials.centre?.deviceName)))
|
|
677
|
+
&& expectedWheels.every((wheel) => {
|
|
678
|
+
const path = driveline.wheelPaths[wheel];
|
|
679
|
+
const axle = wheel.startsWith('F') ? 'front' : 'rear';
|
|
680
|
+
const axleIndex = path.indexOf(driveline.differentials[axle].deviceName);
|
|
681
|
+
const gearboxIndex = path.indexOf(driveline.transmissionDevice);
|
|
682
|
+
if (drivenAxles.length !== 2) return axleIndex >= 0 && axleIndex < gearboxIndex;
|
|
683
|
+
const centreIndex = path.indexOf(driveline.differentials.centre.deviceName);
|
|
684
|
+
return axleIndex >= 0 && axleIndex < centreIndex && centreIndex < gearboxIndex;
|
|
685
|
+
});
|
|
686
|
+
const gearboxKinds = { manualGearbox: 'manual', sequentialGearbox: 'sequential',
|
|
687
|
+
dctGearbox: 'dct', cvtGearbox: 'cvt', automaticGearbox: 'automatic' };
|
|
688
|
+
const validRuntimeGearbox = record(driveline?.gearbox) && driveline.gearbox.status === 'derived'
|
|
689
|
+
&& nonempty(driveline.gearbox.deviceName) && nonempty(driveline.gearbox.deviceType)
|
|
690
|
+
&& nonempty(driveline.gearbox.partName) && nonempty(driveline.gearbox.source)
|
|
691
|
+
&& gearboxKinds[driveline.gearbox.deviceType] === driveline.gearbox.kind
|
|
692
|
+
&& runtimeDriveline?.gearbox?.kind === driveline.gearbox.kind
|
|
693
|
+
&& driveline.transmissionDevice === driveline.gearbox.deviceName;
|
|
694
|
+
const expectedDriveBias = driveline?.runtimeStatus === 'unsupported' ? 0.5
|
|
695
|
+
: drivenAxles.length === 1 ? (drivenAxles[0] === 'front' ? 1 : 0)
|
|
696
|
+
: runtimeDriveline?.centre?.torqueSplit;
|
|
697
|
+
const validLegacyDriveline = semanticReceipt(driveline?.legacyFinalDrive)
|
|
698
|
+
&& approximately(lowering?.vehiclePackage?.physics?.driveBiasFront?.value, expectedDriveBias)
|
|
699
|
+
&& (driveline.legacyFinalDrive.status === 'unsupported'
|
|
700
|
+
|| approximately(lowering?.vehiclePackage?.physics?.gearbox?.finalDrive?.value,
|
|
701
|
+
driveline.legacyFinalDrive.value));
|
|
702
|
+
const validRuntimeDiff = ([location, source], requireParity) => {
|
|
703
|
+
const runtime = runtimeDriveline?.[location];
|
|
704
|
+
const fields = source.fields;
|
|
705
|
+
const supportedType = ['open', 'locked', 'viscous', 'lsd'].includes(source.type);
|
|
706
|
+
const resolved = [...differentialFields.map((name) => fields[name]), source.gearRatio]
|
|
707
|
+
.every((receipt) => receipt.status !== 'unsupported');
|
|
708
|
+
const activeController = source.controllers.some((name) => name.includes('activeCenterDiff'));
|
|
709
|
+
const runtimeConflict = source.overlayConflicts.some((conflict) =>
|
|
710
|
+
runtimeCriticalDifferentialFields.has(conflict.field));
|
|
711
|
+
const representableLosses = ['torqueLossCoef', 'defaultVirtualInertia', 'speedLimitCoef']
|
|
712
|
+
.every((name) => fields[name].value === 0);
|
|
713
|
+
const typeParametersValid = source.type !== 'locked' || fields.lockTorque.value > 0;
|
|
714
|
+
const viscousRuntimeSafe = source.type !== 'viscous'
|
|
715
|
+
|| (fields.viscousCoef.value > 0 && fields.viscousTorque.value > 0
|
|
716
|
+
&& Number.isFinite(fields.viscousExponent.value));
|
|
717
|
+
const viscousParity = viscousRuntimeSafe
|
|
718
|
+
&& (source.type !== 'viscous'
|
|
719
|
+
|| (fields.viscousExponent.status !== 'unsupported' && fields.viscousExponent.value === 1));
|
|
720
|
+
if (!record(runtime) || !supportedType || !resolved || !typeParametersValid || !viscousRuntimeSafe
|
|
721
|
+
|| runtimeConflict
|
|
722
|
+
|| !(source.gearRatio.value > 0)
|
|
723
|
+
|| !(fields.diffTorqueSplit.value >= 0 && fields.diffTorqueSplit.value <= 1)
|
|
724
|
+
|| runtime.type !== source.type
|
|
725
|
+
|| !approximately(runtime.gearRatio, source.gearRatio.value)) return false;
|
|
726
|
+
if (requireParity && (activeController || !representableLosses
|
|
727
|
+
|| fields.activeLockTorque.value !== 0 || !viscousParity)) return false;
|
|
728
|
+
const sourceSplit = source.fields.diffTorqueSplit.value;
|
|
729
|
+
const expectedSplit = source.outputPorts.a === 1 ? sourceSplit : Math.round((1 - sourceSplit) * 1e8) / 1e8;
|
|
730
|
+
if (!approximately(runtime.torqueSplit, expectedSplit)) return false;
|
|
731
|
+
return Object.entries(runtimeFields).every(([runtimeField, receiptField]) =>
|
|
732
|
+
approximately(runtime[runtimeField], source.fields[receiptField].value));
|
|
733
|
+
};
|
|
734
|
+
const validDriveline = record(driveline) && ['functional', 'unsupported'].includes(driveline.status)
|
|
735
|
+
&& ['functional', 'approximate', 'unsupported'].includes(driveline.runtimeStatus)
|
|
736
|
+
&& drivenAxles.every((name) => ['front', 'rear'].includes(name))
|
|
737
|
+
&& new Set(drivenAxles).size === drivenAxles.length && record(driveline.wheelPaths)
|
|
738
|
+
&& differentialEntries.every(([location, value]) => ['front', 'rear', 'centre'].includes(location)
|
|
739
|
+
&& differential(value))
|
|
740
|
+
&& record(driveline.variables) && Array.isArray(driveline.runtimeBlockers)
|
|
741
|
+
&& (explicitUnsupported(driveline.gearbox) || validRuntimeGearbox)
|
|
742
|
+
&& validLegacyDriveline
|
|
743
|
+
&& (driveline.status === 'functional') === (driveline.runtimeBlockers.length === 0)
|
|
744
|
+
&& (driveline.status === 'functional' ? driveline.runtimeStatus === 'functional'
|
|
745
|
+
: driveline.runtimeStatus !== 'functional')
|
|
746
|
+
&& (driveline.runtimeStatus !== 'unsupported'
|
|
747
|
+
? differentialEntries.length === expectedDiffCount
|
|
748
|
+
&& equalJson(semanticLocations, expectedLocations)
|
|
749
|
+
&& equalJson(runtimeLocations, expectedLocations)
|
|
750
|
+
&& validRuntimeGearbox
|
|
751
|
+
&& validWheelPaths
|
|
752
|
+
&& differentialEntries.every((entry) => validRuntimeDiff(entry, driveline.status === 'functional'))
|
|
753
|
+
: !hasRuntimeDiff('front') && !hasRuntimeDiff('rear') && !hasRuntimeDiff('centre'));
|
|
754
|
+
const sourceExtractionComplete = suspension?.axles?.front?.status === 'resolved'
|
|
755
|
+
&& suspension?.axles?.rear?.status === 'resolved'
|
|
756
|
+
&& suspension?.antiRoll?.front?.torsionSpring?.status !== 'unsupported'
|
|
757
|
+
&& suspension?.antiRoll?.front?.torsionSpring !== undefined
|
|
758
|
+
&& suspension?.antiRoll?.rear?.torsionSpring?.status !== 'unsupported'
|
|
759
|
+
&& suspension?.antiRoll?.rear?.torsionSpring !== undefined;
|
|
760
|
+
const countedSuspensionProvenance = Object.fromEntries(requiredProvenanceCounts.map((disposition) => [
|
|
761
|
+
disposition,
|
|
762
|
+
Array.isArray(lowering?.provenance) ? lowering.provenance.filter((row) => record(row)
|
|
763
|
+
&& row.disposition === disposition
|
|
764
|
+
&& typeof row.target === 'string'
|
|
765
|
+
&& (row.target.startsWith('physics.suspension')
|
|
766
|
+
|| row.target.startsWith('sourceSemantics.suspension'))).length : -1,
|
|
767
|
+
]));
|
|
768
|
+
const requiredSemanticTargets = new Map();
|
|
769
|
+
const receiptDisposition = (receipt) => receipt?.status === 'literal' ? 'source-literal' : receipt?.status;
|
|
770
|
+
const expectedEvidence = (receipt, source) => ({ disposition: receiptDisposition(receipt), source });
|
|
771
|
+
for (const axle of ['front', 'rear']) {
|
|
772
|
+
const row = suspension?.axles?.[axle];
|
|
773
|
+
if (record(row) && nonempty(row.partName)) {
|
|
774
|
+
requiredSemanticTargets.set(`sourceSemantics.suspension.axles.${axle}.springRate`,
|
|
775
|
+
expectedEvidence(row.springRate, row.source));
|
|
776
|
+
requiredSemanticTargets.set(`sourceSemantics.suspension.axles.${axle}.precompressionRange`,
|
|
777
|
+
expectedEvidence(row.precompressionRange, row.source));
|
|
778
|
+
requiredSemanticTargets.set(`sourceSemantics.suspension.axles.${axle}.bumpLimit`,
|
|
779
|
+
expectedEvidence(row.bumpLimit, row.source));
|
|
780
|
+
for (const field of ['slowBump', 'fastBump', 'slowRebound', 'fastRebound']) {
|
|
781
|
+
requiredSemanticTargets.set(`sourceSemantics.suspension.axles.${axle}.damping.${field}`,
|
|
782
|
+
expectedEvidence(row.damping?.[field], row.source));
|
|
783
|
+
}
|
|
784
|
+
if (Object.keys(row.derived ?? {}).length > 0) {
|
|
785
|
+
requiredSemanticTargets.set(`sourceSemantics.suspension.axles.${axle}.derived`,
|
|
786
|
+
{ disposition: 'derived', source: row.source });
|
|
787
|
+
}
|
|
788
|
+
} else if (explicitUnsupported(row)) {
|
|
789
|
+
requiredSemanticTargets.set(`sourceSemantics.suspension.axles.${axle}`,
|
|
790
|
+
{ disposition: 'unsupported', source: 'selected JBeam part custody' });
|
|
791
|
+
}
|
|
792
|
+
if (record(suspension?.antiRoll?.[axle]) && nonempty(suspension.antiRoll[axle].partName)) {
|
|
793
|
+
requiredSemanticTargets.set(`sourceSemantics.suspension.antiRoll.${axle}.torsionSpring`,
|
|
794
|
+
expectedEvidence(suspension.antiRoll[axle].torsionSpring, suspension.antiRoll[axle].source));
|
|
795
|
+
}
|
|
796
|
+
const tyre = tyres?.axles?.[axle];
|
|
797
|
+
if (record(tyre) && nonempty(tyre.partName)) {
|
|
798
|
+
for (const field of tyreFields) {
|
|
799
|
+
requiredSemanticTargets.set(`sourceSemantics.wheelsTyres.axles.${axle}.${field}`,
|
|
800
|
+
expectedEvidence(tyre[field], tyre.source));
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
if (record(driveline?.gearbox) && driveline.gearbox.status === 'derived') {
|
|
805
|
+
requiredSemanticTargets.set('simulation.land.driveline.gearbox.kind',
|
|
806
|
+
{ disposition: 'source-literal', source: driveline.gearbox.source });
|
|
807
|
+
}
|
|
808
|
+
for (const [location, row] of differentialEntries) {
|
|
809
|
+
requiredSemanticTargets.set(`sourceSemantics.driveline.differentials.${location}.type`, {
|
|
810
|
+
disposition: ['open', 'locked', 'viscous', 'lsd'].includes(row.type) ? 'source-literal' : 'unsupported',
|
|
811
|
+
source: row.source,
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
requiredSemanticTargets.set('simulation.land.driveline.differentials', {
|
|
815
|
+
disposition: driveline?.status === 'functional' ? 'derived' : 'unsupported',
|
|
816
|
+
source: 'selected JBeam powertrain graph',
|
|
817
|
+
});
|
|
818
|
+
requiredSemanticTargets.set('simulation.land.tyreByAxle', {
|
|
819
|
+
disposition: 'unsupported',
|
|
820
|
+
source: 'selected JBeam pressureWheels',
|
|
821
|
+
});
|
|
822
|
+
const provenanceByTarget = new Map();
|
|
823
|
+
if (Array.isArray(lowering?.provenance)) {
|
|
824
|
+
for (const row of lowering.provenance) {
|
|
825
|
+
if (record(row) && typeof row.target === 'string') {
|
|
826
|
+
const rows = provenanceByTarget.get(row.target) ?? [];
|
|
827
|
+
rows.push(row);
|
|
828
|
+
provenanceByTarget.set(row.target, rows);
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
const validSemanticTargets = [...requiredSemanticTargets].every(([target, expected]) => {
|
|
833
|
+
const rows = provenanceByTarget.get(target);
|
|
834
|
+
return rows?.length === 1 && rows[0].disposition === expected.disposition
|
|
835
|
+
&& rows[0].source === expected.source;
|
|
836
|
+
});
|
|
837
|
+
const axleRows = [suspension?.axles?.front, suspension?.axles?.rear];
|
|
838
|
+
const expectedSuspensionCounts = {
|
|
839
|
+
selectedLeafParts: axleRows.reduce((count, row) => count + (Array.isArray(row?.candidates)
|
|
840
|
+
? row.candidates.length : record(row) && nonempty(row.partName) ? 1 : 0), 0),
|
|
841
|
+
excludedStructuralParts: Array.isArray(suspension?.excludedStructuralParts)
|
|
842
|
+
? suspension.excludedStructuralParts.length : -1,
|
|
843
|
+
resolvedAxles: axleRows.filter((row) => row?.status === 'resolved').length,
|
|
844
|
+
sourceRows: axleRows.reduce((count, row) => count + (record(row?.rowCounts)
|
|
845
|
+
? Object.values(row.rowCounts).reduce((sum, value) => sum + (Number.isInteger(value) ? value : 0), 0) : 0), 0),
|
|
846
|
+
};
|
|
847
|
+
const validSemantics = record(semantics)
|
|
848
|
+
&& record(semantics.mass) && Number.isInteger(semantics.mass.uniqueNodes)
|
|
849
|
+
&& Number.isFinite(semantics.mass.selectedNodeMassKg)
|
|
850
|
+
&& Array.isArray(semantics.seats)
|
|
851
|
+
&& Array.isArray(semantics.cameras)
|
|
852
|
+
&& record(semantics.brakes) && record(semantics.brakes.axles)
|
|
853
|
+
&& record(suspension)
|
|
854
|
+
&& record(suspension.axles) && suspensionAxle(suspension.axles.front) && suspensionAxle(suspension.axles.rear)
|
|
855
|
+
&& record(suspension.antiRoll) && antiRollAxle(suspension.antiRoll.front) && antiRollAxle(suspension.antiRoll.rear)
|
|
856
|
+
&& Array.isArray(suspension.excludedStructuralParts)
|
|
857
|
+
&& Array.isArray(suspension.runtimeBlockers) && suspension.runtimeBlockers.length > 0
|
|
858
|
+
&& typeof suspension.extractionComplete === 'boolean'
|
|
859
|
+
&& suspension.extractionComplete === sourceExtractionComplete
|
|
860
|
+
&& record(suspensionCounts)
|
|
861
|
+
&& requiredSuspensionCounts.every((name) => Number.isInteger(suspensionCounts[name])
|
|
862
|
+
&& suspensionCounts[name] >= 0 && suspensionCounts[name] === expectedSuspensionCounts[name])
|
|
863
|
+
&& validTyres && Array.isArray(semantics.wheelsTyres.selectedParts)
|
|
864
|
+
&& validDriveline
|
|
865
|
+
&& Array.isArray(semantics.electrics)
|
|
866
|
+
&& Array.isArray(semantics.audioEvents)
|
|
867
|
+
&& Array.isArray(semantics.openableControllers);
|
|
868
|
+
const validCoverage = record(coverage)
|
|
869
|
+
&& record(coverage.functionalDomains)
|
|
870
|
+
&& coverage.functionalDomains.suspension === false
|
|
871
|
+
&& coverage.functionalDomains.wheelsTyres === false
|
|
872
|
+
&& coverage.functionalDomains.differentials === (driveline?.status === 'functional')
|
|
873
|
+
&& record(suspensionProvenance)
|
|
874
|
+
&& requiredProvenanceCounts.every((name) => Number.isInteger(suspensionProvenance[name])
|
|
875
|
+
&& suspensionProvenance[name] >= 0
|
|
876
|
+
&& suspensionProvenance[name] === countedSuspensionProvenance[name]);
|
|
877
|
+
const validCapabilities = Array.isArray(lowering?.capabilities) && lowering.capabilities.length > 0
|
|
878
|
+
&& lowering.capabilities.every((row) => record(row) && typeof row.capability === 'string'
|
|
879
|
+
&& typeof row.disposition === 'string' && Array.isArray(row.source) && typeof row.note === 'string');
|
|
880
|
+
const validProvenance = Array.isArray(lowering?.provenance) && lowering.provenance.length > 0
|
|
881
|
+
&& lowering.provenance.every((row) => record(row) && typeof row.target === 'string'
|
|
882
|
+
&& typeof row.source === 'string'
|
|
883
|
+
&& ['source-literal', 'derived', 'defaulted', 'unsupported', 'out-of-scope',
|
|
884
|
+
'missing-source', 'discovered-not-lowered'].includes(row.disposition)
|
|
885
|
+
&& typeof row.detail === 'string');
|
|
886
|
+
const invalid = [
|
|
887
|
+
[lowering?.schemaVersion === 'helix.beamng-functional-lowering/2', 'schema'],
|
|
888
|
+
[validSemantics, 'sourceSemantics'], [validCapabilities, 'capabilities'],
|
|
889
|
+
[validProvenance, 'provenance'], [validSemanticTargets, 'semanticProvenance'],
|
|
890
|
+
[validCoverage, 'coverage'],
|
|
891
|
+
].filter(([valid]) => !valid).map(([, name]) => name);
|
|
892
|
+
if (invalid.length > 0) {
|
|
893
|
+
throw new Error(`${boundary} has an incompatible schema or incomplete helix.beamng-functional-lowering/2 diagnostics: ${invalid.join(', ')}`);
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
|
|
517
897
|
function lowerFunctionalVehicle({ source, candidate, sourceManifest, conversion, outputDir, commonRoot,
|
|
518
898
|
selectedPartIndexArtifact }) {
|
|
519
899
|
const configurationFile = conversion.importQuality?.configuration?.file;
|
|
@@ -543,8 +923,8 @@ function lowerFunctionalVehicle({ source, candidate, sourceManifest, conversion,
|
|
|
543
923
|
rmSync(sourcePath, { force: true });
|
|
544
924
|
rmSync(geometryPath, { force: true });
|
|
545
925
|
}
|
|
546
|
-
|
|
547
|
-
|
|
926
|
+
assertValidFunctionalLowering(bundle, 'functional BeamNG lowerer');
|
|
927
|
+
if (bundle?.vehiclePackage?.schemaVersion !== 'helix.vehicle-package/2'
|
|
548
928
|
|| bundle?.addOnPackages?.schemaVersion !== 'helix.vehicle-addon-packages/1'
|
|
549
929
|
|| bundle?.build?.schemaVersion !== 'helix.vehicle-build-source/1'
|
|
550
930
|
|| bundle?.identity?.candidateId !== candidate.id
|
|
@@ -563,8 +943,8 @@ function packageVehicleSource(request) {
|
|
|
563
943
|
}
|
|
564
944
|
const functional = input?.metadata?.functionalVehicle;
|
|
565
945
|
const candidateId = portableCandidateId(manifest?.vehicle?.candidateId);
|
|
566
|
-
|
|
567
|
-
|
|
946
|
+
assertValidFunctionalLowering(functional, 'BeamNG functional package metadata');
|
|
947
|
+
if (functional.vehiclePackage?.schemaVersion !== 'helix.vehicle-package/2'
|
|
568
948
|
|| functional.addOnPackages?.schemaVersion !== 'helix.vehicle-addon-packages/1'
|
|
569
949
|
|| functional.build?.schemaVersion !== 'helix.vehicle-build-source/1'
|
|
570
950
|
|| functional.identity?.candidateId !== manifest.vehicle.candidateId
|
|
@@ -617,6 +997,7 @@ function packageVehicleSource(request) {
|
|
|
617
997
|
vehicle: candidateId } },
|
|
618
998
|
];
|
|
619
999
|
for (const [suffix, value, role] of [
|
|
1000
|
+
['vehicle-lowering', functional, 'vehicle-functional-lowering'],
|
|
620
1001
|
['vehicle-package', functional.vehiclePackage, 'vehicle-package'],
|
|
621
1002
|
['vehicle-addons', functional.addOnPackages, 'vehicle-add-on-packages'],
|
|
622
1003
|
['vehicle-build', functional.build, 'vehicle-build-source'],
|