diff --git a/Quantum.IO/TrackLayout/V2/TrackLayoutPackageV2Dtos.cs b/Quantum.IO/TrackLayout/V2/TrackLayoutPackageV2Dtos.cs
new file mode 100644
index 0000000..381a819
--- /dev/null
+++ b/Quantum.IO/TrackLayout/V2/TrackLayoutPackageV2Dtos.cs
@@ -0,0 +1,126 @@
+using System;
+using System.Text.Json.Serialization;
+
+namespace Quantum.IO.TrackLayout.V2
+{
+ ///
+ /// Versioned backend-only JSON DTO for authored coaster track layouts.
+ ///
+ public sealed class TrackLayoutPackageV2Dto
+ {
+ public const string ContractName = "quantum.track_layout_package";
+
+ public const int ContractVersion = 2;
+
+ public string Contract { get; set; } = ContractName;
+
+ public int Version { get; set; } = ContractVersion;
+
+ public TrackLayoutMetadataV2Dto Metadata { get; set; } = new TrackLayoutMetadataV2Dto();
+
+ public TrackStartPoseV2Dto StartPose { get; set; } = new TrackStartPoseV2Dto();
+
+ public TrackLayoutSectionV2Dto[] Sections { get; set; } =
+ Array.Empty();
+
+ public TrackBankingV2Dto? Banking { get; set; }
+
+ public TrackHeartlineV2Dto? Heartline { get; set; }
+ }
+
+ public sealed class TrackLayoutMetadataV2Dto
+ {
+ public string Units { get; set; } = "meters";
+
+ public string? SourceName { get; set; }
+
+ public string? LayoutId { get; set; }
+ }
+
+ public sealed class TrackStartPoseV2Dto
+ {
+ public TrackLayoutVector3dV2Dto Position { get; set; } = new TrackLayoutVector3dV2Dto();
+
+ public TrackLayoutVector3dV2Dto Tangent { get; set; } =
+ new TrackLayoutVector3dV2Dto { X = 1.0 };
+
+ public TrackLayoutVector3dV2Dto Normal { get; set; } =
+ new TrackLayoutVector3dV2Dto { Y = 1.0 };
+
+ public TrackLayoutVector3dV2Dto Binormal { get; set; } =
+ new TrackLayoutVector3dV2Dto { Z = 1.0 };
+ }
+
+ public sealed class TrackLayoutVector3dV2Dto
+ {
+ public double X { get; set; }
+
+ public double Y { get; set; }
+
+ public double Z { get; set; }
+ }
+
+ public sealed class TrackLayoutSectionV2Dto
+ {
+ public string Kind { get; set; } = string.Empty;
+
+ public string Id { get; set; } = string.Empty;
+
+ public double Length { get; set; }
+
+ public double RollRadians { get; set; }
+
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public double? Radius { get; set; }
+
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public double? StartCurvature { get; set; }
+
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public double? EndCurvature { get; set; }
+
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public string? InterpolationMode { get; set; }
+
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public int? Degree { get; set; }
+
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public TrackLayoutVector3dV2Dto[]? ControlPoints { get; set; }
+
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public double[]? Weights { get; set; }
+ }
+
+ public sealed class TrackBankingV2Dto
+ {
+ public TrackBankingKeyV2Dto[] Keys { get; set; } =
+ Array.Empty();
+ }
+
+ public sealed class TrackBankingKeyV2Dto
+ {
+ public double Distance { get; set; }
+
+ public double RollRadians { get; set; }
+
+ public string InterpolationToNext { get; set; } =
+ TrackLayoutPackageV2Vocabulary.BankingInterpolationConstant;
+ }
+
+ public sealed class TrackHeartlineV2Dto
+ {
+ public string Kind { get; set; } =
+ TrackLayoutPackageV2Vocabulary.HeartlineKindConstantOffset;
+
+ public string DistanceDomain { get; set; } =
+ TrackLayoutPackageV2Vocabulary.HeartlineDistanceDomainCenterlineStation;
+
+ public string AxisSource { get; set; } =
+ TrackLayoutPackageV2Vocabulary.HeartlineAxisSourceSampledFrame;
+
+ public double NormalOffset { get; set; }
+
+ public double LateralOffset { get; set; }
+ }
+}
diff --git a/Quantum.IO/TrackLayout/V2/TrackLayoutPackageV2Json.cs b/Quantum.IO/TrackLayout/V2/TrackLayoutPackageV2Json.cs
new file mode 100644
index 0000000..8be6d99
--- /dev/null
+++ b/Quantum.IO/TrackLayout/V2/TrackLayoutPackageV2Json.cs
@@ -0,0 +1,61 @@
+using System;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Quantum.IO.TrackLayout.V2
+{
+ ///
+ /// JSON reader/writer for the TrackLayoutPackageV2 authored-layout contract.
+ ///
+ public static class TrackLayoutPackageV2Json
+ {
+ private static readonly JsonSerializerOptions CompactOptions = CreateOptions(indented: false);
+ private static readonly JsonSerializerOptions IndentedOptions = CreateOptions(indented: true);
+
+ public static string Serialize(TrackLayoutPackageV2Dto dto, bool indented = false)
+ {
+ if (dto == null)
+ {
+ throw new ArgumentNullException(nameof(dto));
+ }
+
+ return JsonSerializer.Serialize(dto, indented ? IndentedOptions : CompactOptions);
+ }
+
+ public static TrackLayoutPackageV2Dto Deserialize(string json)
+ {
+ if (json == null)
+ {
+ throw new ArgumentNullException(nameof(json));
+ }
+
+ TrackLayoutPackageV2Dto? dto;
+
+ try
+ {
+ dto = JsonSerializer.Deserialize(json, CompactOptions);
+ }
+ catch (JsonException ex)
+ {
+ throw new JsonException("Failed to deserialize TrackLayoutPackageV2Dto: malformed JSON.", ex);
+ }
+
+ if (dto == null)
+ {
+ throw new JsonException("Failed to deserialize TrackLayoutPackageV2Dto: JSON payload was null.");
+ }
+
+ return dto;
+ }
+
+ private static JsonSerializerOptions CreateOptions(bool indented)
+ {
+ return new JsonSerializerOptions
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ WriteIndented = indented,
+ UnmappedMemberHandling = JsonUnmappedMemberHandling.Disallow
+ };
+ }
+ }
+}
diff --git a/Quantum.IO/TrackLayout/V2/TrackLayoutPackageV2Vocabulary.cs b/Quantum.IO/TrackLayout/V2/TrackLayoutPackageV2Vocabulary.cs
new file mode 100644
index 0000000..7749109
--- /dev/null
+++ b/Quantum.IO/TrackLayout/V2/TrackLayoutPackageV2Vocabulary.cs
@@ -0,0 +1,67 @@
+using System;
+
+namespace Quantum.IO.TrackLayout.V2
+{
+ public static class TrackLayoutPackageV2Vocabulary
+ {
+ public const string StraightSectionKind = "straight";
+ public const string ConstantCurvatureSectionKind = "constantCurvature";
+ public const string CurvatureTransitionSectionKind = "curvatureTransition";
+ public const string SpatialSectionKind = "spatial";
+
+ public const string CurvatureInterpolationLinear = "linear";
+
+ public const string BankingInterpolationConstant = "constant";
+ public const string BankingInterpolationLinear = "linear";
+ public const string BankingInterpolationSmoothStep = "smoothStep";
+ public const string BankingInterpolationQuadratic = "quadratic";
+ public const string BankingInterpolationCubic = "cubic";
+ public const string BankingInterpolationQuartic = "quartic";
+ public const string BankingInterpolationQuintic = "quintic";
+ public const string BankingInterpolationSinusoidal = "sinusoidal";
+
+ public const string HeartlineKindConstantOffset = "constantOffset";
+ public const string HeartlineDistanceDomainCenterlineStation = "centerlineStation";
+ public const string HeartlineAxisSourceSampledFrame = "sampledFrame";
+
+ public static bool IsKnownSectionKind(string? value)
+ {
+ return string.Equals(value, StraightSectionKind, StringComparison.Ordinal) ||
+ string.Equals(value, ConstantCurvatureSectionKind, StringComparison.Ordinal) ||
+ string.Equals(value, CurvatureTransitionSectionKind, StringComparison.Ordinal) ||
+ string.Equals(value, SpatialSectionKind, StringComparison.Ordinal);
+ }
+
+ public static bool IsKnownCurvatureTransitionInterpolation(string? value)
+ {
+ return string.Equals(value, CurvatureInterpolationLinear, StringComparison.Ordinal);
+ }
+
+ public static bool IsKnownBankingInterpolation(string? value)
+ {
+ return string.Equals(value, BankingInterpolationConstant, StringComparison.Ordinal) ||
+ string.Equals(value, BankingInterpolationLinear, StringComparison.Ordinal) ||
+ string.Equals(value, BankingInterpolationSmoothStep, StringComparison.Ordinal) ||
+ string.Equals(value, BankingInterpolationQuadratic, StringComparison.Ordinal) ||
+ string.Equals(value, BankingInterpolationCubic, StringComparison.Ordinal) ||
+ string.Equals(value, BankingInterpolationQuartic, StringComparison.Ordinal) ||
+ string.Equals(value, BankingInterpolationQuintic, StringComparison.Ordinal) ||
+ string.Equals(value, BankingInterpolationSinusoidal, StringComparison.Ordinal);
+ }
+
+ public static bool IsKnownHeartlineKind(string? value)
+ {
+ return string.Equals(value, HeartlineKindConstantOffset, StringComparison.Ordinal);
+ }
+
+ public static bool IsKnownHeartlineDistanceDomain(string? value)
+ {
+ return string.Equals(value, HeartlineDistanceDomainCenterlineStation, StringComparison.Ordinal);
+ }
+
+ public static bool IsKnownHeartlineAxisSource(string? value)
+ {
+ return string.Equals(value, HeartlineAxisSourceSampledFrame, StringComparison.Ordinal);
+ }
+ }
+}
diff --git a/Quantum.Tests/IO/TrackLayoutPackageV2Tests.cs b/Quantum.Tests/IO/TrackLayoutPackageV2Tests.cs
new file mode 100644
index 0000000..55e26e1
--- /dev/null
+++ b/Quantum.Tests/IO/TrackLayoutPackageV2Tests.cs
@@ -0,0 +1,253 @@
+using System;
+using System.IO;
+using System.Text.Json;
+using System.Text.Json.Nodes;
+using Json.Schema;
+using Quantum.IO.TrackLayout.V2;
+
+namespace Quantum.Tests;
+
+public sealed class TrackLayoutPackageV2Tests
+{
+ private static readonly Lazy TrackLayoutPackageSchema = new(CreateTrackLayoutPackageSchema);
+
+ [Fact]
+ public void ContractConstantsAndVocabulary_AreStable()
+ {
+ var dto = new TrackLayoutPackageV2Dto();
+
+ Assert.Equal("quantum.track_layout_package", TrackLayoutPackageV2Dto.ContractName);
+ Assert.Equal(2, TrackLayoutPackageV2Dto.ContractVersion);
+ Assert.Equal(TrackLayoutPackageV2Dto.ContractName, dto.Contract);
+ Assert.Equal(TrackLayoutPackageV2Dto.ContractVersion, dto.Version);
+
+ Assert.Equal("straight", TrackLayoutPackageV2Vocabulary.StraightSectionKind);
+ Assert.Equal("constantCurvature", TrackLayoutPackageV2Vocabulary.ConstantCurvatureSectionKind);
+ Assert.Equal("curvatureTransition", TrackLayoutPackageV2Vocabulary.CurvatureTransitionSectionKind);
+ Assert.Equal("spatial", TrackLayoutPackageV2Vocabulary.SpatialSectionKind);
+ Assert.Equal("linear", TrackLayoutPackageV2Vocabulary.CurvatureInterpolationLinear);
+ Assert.Equal("constantOffset", TrackLayoutPackageV2Vocabulary.HeartlineKindConstantOffset);
+ Assert.Equal(
+ "centerlineStation",
+ TrackLayoutPackageV2Vocabulary.HeartlineDistanceDomainCenterlineStation);
+ Assert.Equal("sampledFrame", TrackLayoutPackageV2Vocabulary.HeartlineAxisSourceSampledFrame);
+
+ Assert.True(TrackLayoutPackageV2Vocabulary.IsKnownSectionKind("spatial"));
+ Assert.True(TrackLayoutPackageV2Vocabulary.IsKnownCurvatureTransitionInterpolation("linear"));
+ Assert.False(TrackLayoutPackageV2Vocabulary.IsKnownCurvatureTransitionInterpolation("smoothStep"));
+ Assert.True(TrackLayoutPackageV2Vocabulary.IsKnownBankingInterpolation("sinusoidal"));
+ Assert.True(TrackLayoutPackageV2Vocabulary.IsKnownHeartlineKind("constantOffset"));
+ Assert.True(TrackLayoutPackageV2Vocabulary.IsKnownHeartlineDistanceDomain("centerlineStation"));
+ Assert.True(TrackLayoutPackageV2Vocabulary.IsKnownHeartlineAxisSource("sampledFrame"));
+ }
+
+ [Fact]
+ public void DefaultDtoShape_IsStable()
+ {
+ var dto = new TrackLayoutPackageV2Dto();
+
+ Assert.Equal("meters", dto.Metadata.Units);
+ Assert.Null(dto.Metadata.SourceName);
+ Assert.Null(dto.Metadata.LayoutId);
+ Assert.Equal(1.0, dto.StartPose.Tangent.X);
+ Assert.Equal(1.0, dto.StartPose.Normal.Y);
+ Assert.Equal(1.0, dto.StartPose.Binormal.Z);
+ Assert.Empty(dto.Sections);
+ Assert.Null(dto.Banking);
+ Assert.Null(dto.Heartline);
+
+ string json = TrackLayoutPackageV2Json.Serialize(dto, indented: true);
+ Assert.Contains("\"layoutId\": null", json);
+ Assert.Contains("\"banking\": null", json);
+ Assert.Contains("\"heartline\": null", json);
+ }
+
+ [Fact]
+ public void Serialize_RepresentativePackage_IsDeterministicCamelCaseJson()
+ {
+ TrackLayoutPackageV2Dto dto = CreateConstantHeartlineDto();
+
+ string first = NormalizeLineEndings(TrackLayoutPackageV2Json.Serialize(dto, indented: true)).TrimEnd();
+ string second = NormalizeLineEndings(TrackLayoutPackageV2Json.Serialize(dto, indented: true)).TrimEnd();
+ string roundtrip = NormalizeLineEndings(
+ TrackLayoutPackageV2Json.Serialize(
+ TrackLayoutPackageV2Json.Deserialize(first),
+ indented: true)).TrimEnd();
+
+ Assert.Equal(first, second);
+ Assert.Equal(first, roundtrip);
+ Assert.Contains("\"startPose\":", first);
+ Assert.Contains("\"layoutId\":", first);
+ Assert.Contains("\"constantOffset\"", first);
+ Assert.Contains("\"normalOffset\":", first);
+ Assert.DoesNotContain("\"StartPose\":", first);
+ Assert.DoesNotContain("\"NormalOffset\":", first);
+ }
+
+ [Fact]
+ public void SchemaValidation_MinimalV2Json_IsValid()
+ {
+ string json = TrackLayoutPackageV2Json.Serialize(CreateMinimalDto());
+
+ Assert.True(IsValidAgainstSchema(json));
+ }
+
+ [Fact]
+ public void SchemaValidation_ConstantHeartlineV2Json_IsValid()
+ {
+ string json = TrackLayoutPackageV2Json.Serialize(CreateConstantHeartlineDto());
+
+ Assert.True(IsValidAgainstSchema(json));
+ }
+
+ [Fact]
+ public void SchemaValidation_RejectsWrongVersion()
+ {
+ JsonObject json = ParseJsonObject(TrackLayoutPackageV2Json.Serialize(CreateMinimalDto()));
+ json["version"] = 1;
+
+ Assert.False(IsValidAgainstSchema(json.ToJsonString()));
+ }
+
+ [Fact]
+ public void SchemaValidation_RejectsUnknownRootFields()
+ {
+ JsonObject json = ParseJsonObject(TrackLayoutPackageV2Json.Serialize(CreateMinimalDto()));
+ json["train"] = new JsonObject();
+
+ Assert.False(IsValidAgainstSchema(json.ToJsonString()));
+ }
+
+ private static TrackLayoutPackageV2Dto CreateMinimalDto()
+ {
+ return new TrackLayoutPackageV2Dto
+ {
+ Metadata = new TrackLayoutMetadataV2Dto
+ {
+ Units = "meters",
+ SourceName = "Minimal V2 layout",
+ LayoutId = null
+ },
+ Sections = new[]
+ {
+ new TrackLayoutSectionV2Dto
+ {
+ Kind = TrackLayoutPackageV2Vocabulary.StraightSectionKind,
+ Id = "entry",
+ Length = 12.0,
+ RollRadians = 0.0
+ }
+ },
+ Banking = null,
+ Heartline = null
+ };
+ }
+
+ private static TrackLayoutPackageV2Dto CreateConstantHeartlineDto()
+ {
+ TrackLayoutPackageV2Dto dto = CreateMinimalDto();
+ dto.Metadata.SourceName = "Constant heartline V2 layout";
+ dto.Metadata.LayoutId = "layout.m147.constant-heartline";
+ dto.Sections = new[]
+ {
+ new TrackLayoutSectionV2Dto
+ {
+ Kind = TrackLayoutPackageV2Vocabulary.StraightSectionKind,
+ Id = "entry",
+ Length = 12.0,
+ RollRadians = 0.0
+ },
+ new TrackLayoutSectionV2Dto
+ {
+ Kind = TrackLayoutPackageV2Vocabulary.ConstantCurvatureSectionKind,
+ Id = "turn",
+ Length = 18.0,
+ RollRadians = 0.2,
+ Radius = 30.0
+ }
+ };
+ dto.Banking = new TrackBankingV2Dto
+ {
+ Keys = new[]
+ {
+ new TrackBankingKeyV2Dto
+ {
+ Distance = 0.0,
+ RollRadians = 0.0,
+ InterpolationToNext = TrackLayoutPackageV2Vocabulary.BankingInterpolationLinear
+ },
+ new TrackBankingKeyV2Dto
+ {
+ Distance = 12.0,
+ RollRadians = 0.2,
+ InterpolationToNext = TrackLayoutPackageV2Vocabulary.BankingInterpolationConstant
+ },
+ new TrackBankingKeyV2Dto
+ {
+ Distance = 30.0,
+ RollRadians = 0.2,
+ InterpolationToNext = TrackLayoutPackageV2Vocabulary.BankingInterpolationConstant
+ }
+ }
+ };
+ dto.Heartline = new TrackHeartlineV2Dto
+ {
+ NormalOffset = 1.1,
+ LateralOffset = 0.0
+ };
+
+ return dto;
+ }
+
+ private static bool IsValidAgainstSchema(string instanceJson)
+ {
+ using JsonDocument instanceDocument = JsonDocument.Parse(instanceJson);
+
+ EvaluationResults results = TrackLayoutPackageSchema.Value.Evaluate(
+ instanceDocument.RootElement,
+ new EvaluationOptions
+ {
+ OutputFormat = OutputFormat.List
+ });
+
+ return results.IsValid;
+ }
+
+ private static JsonSchema CreateTrackLayoutPackageSchema()
+ {
+ string schemaPath = FindContractFile("track-layout-package-v2.schema.json");
+ string schemaJson = File.ReadAllText(schemaPath);
+ return JsonSchema.FromText(schemaJson);
+ }
+
+ private static string FindContractFile(string fileName)
+ {
+ DirectoryInfo? current = new(AppContext.BaseDirectory);
+
+ while (current != null)
+ {
+ string candidate = Path.Combine(current.FullName, "docs", "contracts", fileName);
+ if (File.Exists(candidate))
+ {
+ return candidate;
+ }
+
+ current = current.Parent;
+ }
+
+ throw new FileNotFoundException(
+ "Contract file '" + fileName + "' was not found from '" + AppContext.BaseDirectory + "'.");
+ }
+
+ private static JsonObject ParseJsonObject(string json)
+ {
+ JsonNode? node = JsonNode.Parse(json);
+ Assert.NotNull(node);
+ return Assert.IsType(node);
+ }
+
+ private static string NormalizeLineEndings(string value)
+ {
+ return value.ReplaceLineEndings("\n");
+ }
+}
diff --git a/docs/contracts/track-layout-package-v2.schema.json b/docs/contracts/track-layout-package-v2.schema.json
new file mode 100644
index 0000000..f9a4551
--- /dev/null
+++ b/docs/contracts/track-layout-package-v2.schema.json
@@ -0,0 +1,370 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://quantumcoasterworks.dev/schemas/track-layout-package-v2.schema.json",
+ "title": "TrackLayoutPackageV2",
+ "description": "Backend-only JSON contract skeleton for authored coaster track layouts with nullable banking and heartline roots.",
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "contract",
+ "version",
+ "metadata",
+ "startPose",
+ "sections",
+ "banking",
+ "heartline"
+ ],
+ "properties": {
+ "contract": {
+ "description": "Stable contract discriminator.",
+ "type": "string",
+ "const": "quantum.track_layout_package"
+ },
+ "version": {
+ "description": "Contract version.",
+ "type": "integer",
+ "const": 2
+ },
+ "metadata": {
+ "$ref": "#/$defs/metadata"
+ },
+ "startPose": {
+ "$ref": "#/$defs/startPose"
+ },
+ "sections": {
+ "description": "Ordered authored geometric section definitions.",
+ "type": "array",
+ "minItems": 1,
+ "items": {
+ "oneOf": [
+ {
+ "$ref": "#/$defs/straightSection"
+ },
+ {
+ "$ref": "#/$defs/constantCurvatureSection"
+ },
+ {
+ "$ref": "#/$defs/curvatureTransitionSection"
+ },
+ {
+ "$ref": "#/$defs/spatialSection"
+ }
+ ]
+ }
+ },
+ "banking": {
+ "description": "Optional explicit authored banking. Null means no explicit banking is persisted.",
+ "oneOf": [
+ {
+ "type": "null"
+ },
+ {
+ "$ref": "#/$defs/banking"
+ }
+ ]
+ },
+ "heartline": {
+ "description": "Optional authored heartline offset. Null means no authored heartline offset is persisted.",
+ "oneOf": [
+ {
+ "type": "null"
+ },
+ {
+ "$ref": "#/$defs/constantHeartline"
+ }
+ ]
+ }
+ },
+ "$defs": {
+ "metadata": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "units",
+ "sourceName"
+ ],
+ "properties": {
+ "units": {
+ "description": "Distance units used by authored scalar values.",
+ "type": "string"
+ },
+ "sourceName": {
+ "description": "Optional source label for the authored layout package.",
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "layoutId": {
+ "description": "Optional stable layout identity for companion contracts and manifests.",
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ }
+ },
+ "startPose": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "position",
+ "tangent",
+ "normal",
+ "binormal"
+ ],
+ "properties": {
+ "position": {
+ "$ref": "#/$defs/vector3d"
+ },
+ "tangent": {
+ "$ref": "#/$defs/vector3d"
+ },
+ "normal": {
+ "$ref": "#/$defs/vector3d"
+ },
+ "binormal": {
+ "$ref": "#/$defs/vector3d"
+ }
+ }
+ },
+ "vector3d": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "x",
+ "y",
+ "z"
+ ],
+ "properties": {
+ "x": {
+ "type": "number"
+ },
+ "y": {
+ "type": "number"
+ },
+ "z": {
+ "type": "number"
+ }
+ }
+ },
+ "straightSection": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "kind",
+ "id",
+ "length",
+ "rollRadians"
+ ],
+ "properties": {
+ "kind": {
+ "type": "string",
+ "const": "straight"
+ },
+ "id": {
+ "type": "string"
+ },
+ "length": {
+ "type": "number"
+ },
+ "rollRadians": {
+ "type": "number"
+ }
+ }
+ },
+ "constantCurvatureSection": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "kind",
+ "id",
+ "length",
+ "rollRadians",
+ "radius"
+ ],
+ "properties": {
+ "kind": {
+ "type": "string",
+ "const": "constantCurvature"
+ },
+ "id": {
+ "type": "string"
+ },
+ "length": {
+ "type": "number"
+ },
+ "rollRadians": {
+ "type": "number"
+ },
+ "radius": {
+ "type": "number"
+ }
+ }
+ },
+ "curvatureTransitionSection": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "kind",
+ "id",
+ "length",
+ "rollRadians",
+ "startCurvature",
+ "endCurvature",
+ "interpolationMode"
+ ],
+ "properties": {
+ "kind": {
+ "type": "string",
+ "const": "curvatureTransition"
+ },
+ "id": {
+ "type": "string"
+ },
+ "length": {
+ "type": "number"
+ },
+ "rollRadians": {
+ "type": "number"
+ },
+ "startCurvature": {
+ "type": "number"
+ },
+ "endCurvature": {
+ "type": "number"
+ },
+ "interpolationMode": {
+ "type": "string",
+ "enum": [
+ "linear"
+ ]
+ }
+ }
+ },
+ "spatialSection": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "kind",
+ "id",
+ "length",
+ "rollRadians",
+ "degree",
+ "controlPoints",
+ "weights"
+ ],
+ "properties": {
+ "kind": {
+ "type": "string",
+ "const": "spatial"
+ },
+ "id": {
+ "type": "string"
+ },
+ "length": {
+ "type": "number"
+ },
+ "rollRadians": {
+ "type": "number"
+ },
+ "degree": {
+ "type": "integer",
+ "minimum": 1
+ },
+ "controlPoints": {
+ "type": "array",
+ "minItems": 2,
+ "items": {
+ "$ref": "#/$defs/vector3d"
+ }
+ },
+ "weights": {
+ "type": "array",
+ "minItems": 2,
+ "items": {
+ "type": "number",
+ "exclusiveMinimum": 0
+ }
+ }
+ }
+ },
+ "banking": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "keys"
+ ],
+ "properties": {
+ "keys": {
+ "type": "array",
+ "minItems": 2,
+ "items": {
+ "$ref": "#/$defs/bankingKey"
+ }
+ }
+ }
+ },
+ "bankingKey": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "distance",
+ "rollRadians",
+ "interpolationToNext"
+ ],
+ "properties": {
+ "distance": {
+ "type": "number"
+ },
+ "rollRadians": {
+ "type": "number"
+ },
+ "interpolationToNext": {
+ "type": "string",
+ "enum": [
+ "constant",
+ "linear",
+ "smoothStep",
+ "quadratic",
+ "cubic",
+ "quartic",
+ "quintic",
+ "sinusoidal"
+ ]
+ }
+ }
+ },
+ "constantHeartline": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "kind",
+ "distanceDomain",
+ "axisSource",
+ "normalOffset",
+ "lateralOffset"
+ ],
+ "properties": {
+ "kind": {
+ "type": "string",
+ "const": "constantOffset"
+ },
+ "distanceDomain": {
+ "type": "string",
+ "const": "centerlineStation"
+ },
+ "axisSource": {
+ "type": "string",
+ "const": "sampledFrame"
+ },
+ "normalOffset": {
+ "type": "number"
+ },
+ "lateralOffset": {
+ "type": "number"
+ }
+ }
+ }
+ }
+}