From 2a77426658bd22d5271463575bb2433b6c396a27 Mon Sep 17 00:00:00 2001 From: kosuke55 Date: Mon, 3 Aug 2026 11:15:14 +0900 Subject: [PATCH] OpenDRIVE import: expose signal facing as headingRad on traffic signs --- .../__tests__/exporter/odrToShapes.test.ts | 36 +++++++++++++++++++ .../exporter/opendriveParser.test.ts | 2 +- .../src/exporter/odrToShapes.ts | 4 +++ .../src/exporter/opendriveParser.ts | 9 +++++ .../src/exporter/osmToShapes.ts | 8 +++++ 5 files changed, 58 insertions(+), 1 deletion(-) diff --git a/packages/drawtonomy-sdk/__tests__/exporter/odrToShapes.test.ts b/packages/drawtonomy-sdk/__tests__/exporter/odrToShapes.test.ts index 55b2b4e..53b8488 100644 --- a/packages/drawtonomy-sdk/__tests__/exporter/odrToShapes.test.ts +++ b/packages/drawtonomy-sdk/__tests__/exporter/odrToShapes.test.ts @@ -663,3 +663,39 @@ describe('esmini parking_demo (fixture)', () => { } ) }) + +describe('odrToShapes signal heading', () => { + const SIGNAL_HEADING_MAP = ` + +
+ + + + + + + + + + + + + + + + +` + + it('derives headingRad from reference heading, orientation and hOffset', () => { + const result = odrToShapes(parseOpenDriveXml(SIGNAL_HEADING_MAP)) + const byId = new Map(result.trafficSigns.map(ts => [ts.attributes.odr_signal_id, ts])) + // Road runs along +x (heading 0). + expect(byId.get('s1')?.headingRad).toBeCloseTo(0, 9) + // orientation="-" flips by pi. + expect(byId.get('s2')?.headingRad).toBeCloseTo(Math.PI, 9) + // hOffset adds on top of the oriented direction. + expect(byId.get('s3')?.headingRad).toBeCloseTo(0.5, 9) + // Missing orientation/hOffset default to the reference direction. + expect(byId.get('s4')?.headingRad).toBeCloseTo(0, 9) + }) +}) diff --git a/packages/drawtonomy-sdk/__tests__/exporter/opendriveParser.test.ts b/packages/drawtonomy-sdk/__tests__/exporter/opendriveParser.test.ts index 91de4ed..6632a27 100644 --- a/packages/drawtonomy-sdk/__tests__/exporter/opendriveParser.test.ts +++ b/packages/drawtonomy-sdk/__tests__/exporter/opendriveParser.test.ts @@ -150,7 +150,7 @@ describe('parseOpenDriveXml', () => { const road = parseOpenDriveXml(SAMPLE).roads[0] expect(road.hasElevation).toBe(true) expect(road.signals).toEqual([ - { id: 'sig1', s: 95, t: -6, type: '1000001', subtype: '-1', name: 'tl', dynamic: '', country: '', width: 0, height: 0, validity: [], userData: {} }, + { id: 'sig1', s: 95, t: -6, type: '1000001', subtype: '-1', name: 'tl', dynamic: '', country: '', width: 0, height: 0, orientation: '', hOffset: 0, validity: [], userData: {} }, ]) expect(road.objects[0].type).toBe('crosswalk') }) diff --git a/packages/drawtonomy-sdk/src/exporter/odrToShapes.ts b/packages/drawtonomy-sdk/src/exporter/odrToShapes.ts index 82bfaaf..1ef9b11 100644 --- a/packages/drawtonomy-sdk/src/exporter/odrToShapes.ts +++ b/packages/drawtonomy-sdk/src/exporter/odrToShapes.ts @@ -615,6 +615,9 @@ export function odrToShapes(map: OdrMap, options: OdrToShapesOptions = {}): OdrI // Unit normal toward +t (left of the reference direction in ENU). const ex = pose.x - Math.sin(pose.hdg) * sig.t const ey = pose.y + Math.cos(pose.hdg) * sig.t + // World heading the signal applies to (ENU radians): reference-line + // heading, flipped for orientation="-", plus the signal's hOffset. + const headingRad = pose.hdg + (sig.orientation === '-' ? Math.PI : 0) + sig.hOffset const affected: string[] = [] resolveAffectedLanes(road, sig.s, sig.validity, affected) @@ -683,6 +686,7 @@ export function odrToShapes(map: OdrMap, options: OdrToShapesOptions = {}): OdrI affectedLaneIds: affected, stopLineId: materializeStopLine(sig.userData['stopLine']), attributes, + headingRad, } result.trafficSigns.push(data) convertedSignalCount++ diff --git a/packages/drawtonomy-sdk/src/exporter/opendriveParser.ts b/packages/drawtonomy-sdk/src/exporter/opendriveParser.ts index febdfc6..d25079a 100644 --- a/packages/drawtonomy-sdk/src/exporter/opendriveParser.ts +++ b/packages/drawtonomy-sdk/src/exporter/opendriveParser.ts @@ -136,6 +136,13 @@ export interface OdrSignal { /** Physical size (m); 0 when absent. */ width: number height: number + /** + * Facing relative to the reference-line direction: "+" applies to traffic + * running in the positive s direction, "-" against it ("" when absent). + */ + orientation: string + /** Heading offset in radians relative to the (oriented) s direction; 0 when absent. */ + hOffset: number /** Lane ranges restricting which lanes the signal applies to (empty = all). */ validity: OdrSignalValidity[] /** records attached to the signal (code -> value). */ @@ -686,6 +693,8 @@ function parseRoad(el: XmlNode): OdrRoad { country: sig.attrs.country ?? '', width: numAttr(sig, 'width', 0), height: numAttr(sig, 'height', 0), + orientation: sig.attrs.orientation ?? '', + hOffset: numAttr(sig, 'hOffset', 0), validity: parseValidity(sig), userData: parseUserData(sig), })) diff --git a/packages/drawtonomy-sdk/src/exporter/osmToShapes.ts b/packages/drawtonomy-sdk/src/exporter/osmToShapes.ts index 1512b4f..7d7ecf4 100644 --- a/packages/drawtonomy-sdk/src/exporter/osmToShapes.ts +++ b/packages/drawtonomy-sdk/src/exporter/osmToShapes.ts @@ -115,6 +115,14 @@ export interface ImportedTrafficSign { * keeps a speed limit value (e.g. "50 km/h") when present. */ attributes: Record + /** + * World heading the signal applies to, in ENU radians (counter-clockwise + * from +x, y-up): reference-line heading at s, plus pi when + * orientation="-", plus hOffset. Only set by the OpenDRIVE import path; + * undefined for OSM/Lanelet2 sources. Renderers drawing surface markings + * (arrows etc.) should align the artwork with this direction. + */ + headingRad?: number } export interface ImportedCrosswalk {