HBW 2D-plan feature classes → lane XML (schema 1.1)

2026-08-28 · Miro / AI3D pipeline · package iolabs-point-cloud-modelling-export 0.9.0 → 0.10.0 · generated by Claude

Summary

The customer (HBW) draws its 2D plans with a Grasshopper definition that reads our lane XML — the poster's yellow boxes are Grasshopper panels ({6;70} = data-tree paths). Per feature class it needs a short, ordered input list and joins the layer name against its own Data catalog (SYMB#### block, rotation mode DRW/OWN/NO/VEC/ORD). The catalog is customer-side; our XML only has to carry the input list.

All 18 poster columns collapse into three shapes — point symbol [layer, XYZ, direction, has-annotation, text], polyline [layer, has-description, sorting code, vertices], spline [layer, has-description, control points] — plus derived things we never export (hatches, tree-group symbol, guard-rail labels beyond a type attribute).

Proposal: schema 1.1, purely additive. Six optional elements appended after the last existing child of their parent (SchemaVersion, Layer, Direction, Annotation, Attributes, SortingCode, plus Polyline/Type). Feature/Type stays the semantic class id. ≈ 5 dev-days in this repo, none blocked on the customer; downstream producer PRs afterwards.

Sources: 2D plans overview.pdf (poster, source of truth), 240511_HBW - 2D plans.xlsx (priorities/status), 241213 HBW Presentation.pdf (KM-sign question). Markdown twin: docs/hbw_feature_classes_xml_plan.md.

Key decisions

  1. Purely additive, versioned. No existing element renamed, removed, reordered or made conditional. New elements are optional and appended after the last 1.0 child of their parent — safe for both name-based and index-based readers. Existing producers (cluster_stepper, guardrails) produce byte-identical output apart from MetaData/SchemaVersion.
  2. Feature/Type stays the stable class id; the customer layer gets its own <Layer>. guardrails/lane_xml.py and the customer definition filter on existing Type values (Axis of the Edge, Central Axis, Cross Section, guardrail…). Consumers resolve the catalog key as Layer if present, else Type (legacy DWG→XML files stored the layer in Type).
  3. One Feature = one customer layer = one container. Pedestrian roads may be polyline or spline — separate features, never the same path twice.
  4. Machine values in <Attributes>, display strings in <Annotation>. The writer never formats numbers (decimal comma, Ø=, line breaks); producers pass display-ready text and the raw numbers ride along as attributes.
  5. Uniform <Attribute name= type=> form, sorted by name — generalises the block the guardrails stand-in exporter already invented (and which create_xml currently drops); its TitleCase-tag form is retired with a key adapter.
  6. Sorting code is the pairing contract for trench/slope top+bottom (G2$A/G2$B, B1$A/B1$B); the writer validates grammar and completeness at document level. Hatches are derived by the customer, never exported.
  7. Writer ignores unknown dict keys (already true) and validates only what is load-bearing: sorting codes, annotation invariant, finite non-zero direction.
  8. Tree-group symbol (Nutzungsart) not exported — poster says "no input geometry"; customer derives it from the tree-line blobs.

Schema 1.1 — new elements

ParentNew childCard.PositionPurpose
MetaDataSchemaVersion = 1.11 (always)after PipelineVersionsreaders branch on it; absence = 1.0
FeatureLayer0..1after NumberOfSplinescustomer catalog key verbatim (PLC_Laubbaum, umlauts included)
FeatureAttributes0..1after Layerfeature-level metadata (rare)
PointDirection (X,Y,Z)0..1after RoadPointsymbol direction, unit vector in export frame, producer-normalised
Point, Polyline, Spline, LineAnnotation (Display, Text*)0..1after last 1.0 childposter "has annotation display" / "has a description" + text; one <Text> per line, no embedded newlines
Point, Polyline, Spline, LineAttributes (Attribute name= type=*)0..1lastextensible metadata
PolylineType0..1after LaneID (same slot as Line/Spline since 0.8.0)top/bottom, guardrail sub-kind
PolylineSortingCode0..1after Verticespairing token G2$B, B1$A

Become optional for new producers only: RoadPoint on Point/Polyline/Spline; Spline/Fit_Points may be empty (still emitted). Legacy point fields always emitted; defaults when absent: PointerNumber = 1-based index, Color empty, FeatureClass = Feature/Type, FeatureID = Feature/ID. LaneID always emitted; non-lane features pass "0" (guardrails precedent).

Annotation invariants: Display required; Text required & non-empty when true, omitted when false. HBW producers always pass an annotation (even false) so the poster boolean is explicit; the writer emits it iff the key is present → legacy output unchanged.

Attributes: type ∈ string|integer|float|boolean, lower-case booleans, None skipped, nested values rejected, units in the name (height_m). Writer also accepts the guardrails polyline top-level keys (side, guardrail_type, faces, length_m, feature_type, width_m, top_height_p90_m, parent_guardrail_id, post_spacing_m, post_count, post_confidence, halfwidth_m) as attributes; explicit attributes wins.

Example — point symbol (Tree)
<Feature>
  <Type>Tree</Type><ID>TREE-12</ID><LaneID>0</LaneID>
  <Lines/><NumberOfLines>0</NumberOfLines>
  <Points>
    <Point>
      <ID>TREE-12-1</ID><LaneID>0</LaneID><PointerNumber>1</PointerNumber>
      <X>-109.06</X><Y>-383.29</Y><Z>0.0</Z>
      <Color/><FeatureClass>Tree</FeatureClass><FeatureID>TREE-12</FeatureID>
      <Direction><X>0.974467</X><Y>-0.224529</Y><Z>0</Z></Direction>
      <Annotation><Display>true</Display><Text>Ø= 0,30/7,0m</Text></Annotation>
      <Attributes>
        <Attribute name="crown_diameter_m" type="float">7.0</Attribute>
        <Attribute name="trunk_diameter_m" type="float">0.3</Attribute>
      </Attributes>
    </Point>
  </Points><NumberOfPoints>1</NumberOfPoints>
  <Polylines/><NumberOfPolylines>0</NumberOfPolylines>
  <Splines/><NumberOfSplines>0</NumberOfSplines>
  <Layer>PLC_Laubbaum</Layer>
</Feature>

Poster mapping: 0→Layer, 1–3→X,Y,Z, 4→Direction, 5→Annotation/Display, 6→Annotation/Text.

Example — paired polylines with sorting code (Slope)
<Polyline>
  <ID>SLP-3-1</ID><LaneID>0</LaneID><Type>top</Type>
  <Vertices><Vertex><ID>SLP-3-1-1</ID><X>…</X><Y>…</Y><Z>…</Z></Vertex>…</Vertices>
  <SortingCode>B3$A</SortingCode>
  <Annotation><Display>false</Display></Annotation>
</Polyline>

Grammar ^[A-Z]+[0-9]+\$[AB]$; A = top, B = bottom; index unique per family per document. Writer accepts a ready string sorting_code or the triple (sort_class, pair_index, pair_role) and composes it; rejects duplicates and incomplete pairs. Trench = two features (pPLC_Graben G<i>$A, pPLC_Grabensohle G<i>$B); slope = one feature, two polylines on pPLC_Böschung.

Example — spline (Bush) and annotation-only (guard rail)
<Spline>
  <ID>HED-2-1</ID><LaneID>0</LaneID>
  <Fit_Points/>
  <Control_Points><Point><ID>HED-2-1_1</ID><X>…</X><Y>…</Y><Z>…</Z></Point>…</Control_Points>
  <Annotation><Display>false</Display></Annotation>
</Spline>

<!-- guard rail: existing guardrails-package polyline, Type=guardrail -->
<Polyline>
  <ID>segment_70_guardrail_00</ID><LaneID>0</LaneID>
  <RoadPoint><From>0</From><To>24.8</To></RoadPoint>
  <Vertices>…</Vertices>
  <Annotation><Display>true</Display><Text>ESP 2,0</Text></Annotation>
  <Attributes><Attribute name="guardrail_type" type="string">ESP 2.0</Attribute></Attributes>
</Polyline>

Cross-section points: Type stays Cross Section; add Layer (PLC_Punkte_Symbole, provisional), per-point Annotation (formatted Z) and Direction; PointerNumber/RoadPoint keep today's meaning.

Per-feature-class mapping

Type = English singular Title Case (house style of Cross Section); Layer = customer key verbatim. "Annot." = Annotation/Display.

#Poster columnFeature/TypeLayerContainerRequiredAnnot.Attributes / notes
1aTree linesTree LinesPLC_LaubbaumreiheSplinescontrol pointsfalsecrown symbols catalog-derived
1bTree group symbols— not exportedNutzungsartcustomer derives from tree-line blobs (Q10)
2BushesBushsPLC_HeckeSplinescontrol pointsfalse
3TreesTreePLC_LaubbaumPointsposition, directiontrue — Ø= 0,30/7,0mtrunk_diameter_m, crown_diameter_m, later species_class
4FencesFencepPLC_ZaunPolylines≥2 verticesfalse
5Paving edgesPaving EdgepPLC_PflasterkantePolylines≥2 verticesfalse
6Noise blocking wallsnoise_wall (existing) → Q7pPLC_Lärmschutzwand (Excel: polyline) vs sPLC_ (poster)Polylines todayverticesobject name once agreedheight_m, length_m
7Trenches (top)TrenchpPLC_GrabenPolylinesvertices, G<i>$Afalsehatch derived
7Trenches (bottom)Trench BottompPLC_GrabensohlePolylinesvertices, G<i>$Bfalsesame <i> ties the couple
8SlopesSlopepPLC_BöschungPolylines ×2B<i>$A + B<i>$B, Type top/bottomfalseslope direction from A/B + geometry (Q6)
9Pedestrian roadsPedestrian RoadpPLC_Gehweg / sPLC_GehwegPolylines or Splinesvertices / control pointsfalselayer must match container
10Terrain elevation pointsTerrain PointPLC_GeländepunktePointsposition, directiontrue — formatted Zcatalog mode NO ignores direction
11Traffic signsTraffic SignPLC_VerkehrszeichenPointsposition, directionfalselater height_m, plate_class
12ShaftsShaftPLC_SchachtPointsposition, directionfalse
13Cross section pointsCross Section (existing)PLC_Punkte_Symbole (Q4)Pointsas today + directiontrue — formatted Zadditive only
14Emergency call boxesEmergency Call BoxPLC_NotrufsäulePointsposition, directionfalse
15KM signsKM SignPLC_Kilometer-TafelnPointsposition, directiontrue — ≤3 lines (Q1)km, section, station
16GullysGullyPLC_GullysPointsposition, directionfalsewidth_m
17WallsWallpPLC_MauerwerkPolylines≥2 verticesfalseheight_m, length_m
18Guard rail annotationsguardrail (existing)SchutzeinrichtungPolylinesverticestrue — EDSP / ESP 2,0guardrail_type; no new geometry (Q8)
Hatches (7, 8)never exported
Verge posts (Excel only)Verge PostPLC_Randpfosten (TBD)Pointsposition, directionfalseslots into point shape
Lane markings (exists)Axis of the Edge, Center LinesQ12Splines + Linesas todayadd Layer only
Road axes (exists)Central AxisPolylinesas todayunchanged
Guard rails (exists)guardrail, guardrail_support, guardrail_top_railSchutzeinrichtungPolylinesas todaysee 18attributes finally survive the Nexus exporter

create_xml(data) contract

Dict keys per shape

feature: type (req), id (req, document-unique), lane_id ("0" if none), layer (req for HBW classes), attributes, lines|points|polylines|splines (absent ⇒ empty container + count 0).

point: id, position [x,y,z] req; lane_id, pointer_number, color, feature_class, feature_id, road_point optional (defaults above); direction [dx,dy,dz] req for HBW (finite, non-zero); annotation req for HBW; attributes.

polyline: id, vertices (≥2) req; lane_id, type, road_point [from,to], sorting_code or (sort_class, pair_index, pair_role), annotation (req for HBW), attributes; guardrails legacy keys accepted.

spline: id, control_points (≥2) req; fit_points (default []), lane_id, type, road_point, width, annotation, attributes.

line: unchanged + optional annotation, attributes.

annotation = {"display": bool, "text": str | list[str] | None}; attributes = Mapping[str, str|int|float|bool|None]; metadata.schema_version optional (writer defaults to "1.1").

Producer responsibilities: same geoshifted local frame/metres as today, unit direction vectors, display-ready text, globally unique ids.

Phases

Each phase = one PR in this repo. None needs a customer answer. ≈ 5 dev-days total.

1 · Compatibility freeze 0.5 d
  • Golden fixture of today's full output: cluster_stepper-shaped dict (cross sections, lanes, axes) + guardrails-shaped dict.
  • Snapshot test asserting exact element tree and child order.
2 · Writer plumbing + SchemaVersion 0.5 d
  • Private helpers: _text, _append_xyz, _append_annotation, _append_attributes, _append_direction, _sorting_code.
  • Emit MetaData/SchemaVersion unconditionally; snapshot passes modulo that element.
3 · Feature + Point extensions 1 d
  • Trailing Layer/Attributes on Feature; Direction, Annotation, Attributes on Point; optional legacy point keys with defaults; optional RoadPoint.
  • Tests: present/absent, ordering, Unicode layer/text, annotation invariant, zero-vector rejection, legacy-dict byte-identity.
4 · Polyline / Spline / Line extensions 1.5 d
  • Type, SortingCode (triple composition, grammar, document-level pairing check), Annotation, Attributes, optional RoadPoint, empty fit_points.
  • Guardrails legacy-key adapter → attributes.
  • Tests: trench/slope pairing, malformed/duplicate/incomplete codes, guardrail parity with the stand-in exporter.
5 · feature_classes.py registry + builders 0.75 d
  • Frozen map class_id → (feature_type, default_layer, container) for every mapping row; builders point_feature(), polyline_feature(), spline_feature() return ready dicts so downstream repos stop hard-coding PLC_….
  • Tests: registry covers the table; builders round-trip through create_xml.
6 · Docs + sample fixture 0.75 d
  • docs/xml_schema.md: element grammar, 1.0→1.1 diff, one example per shape, consumer LayerType fallback; README link.
  • tests/fixtures/sample_v1_1.xml exercising every shape; one test dict per mapping row.
7 · Release 0.10.0 + customer acceptance 0.25 d
  • /wrap-up (bump, build, publish to Nexus, Jira-tagged commit).
  • Send sample 1.1 XML to HBW for a Grasshopper run; lane_xml.py smoke test.
Downstream follow-ups (other repos, after 0.10.0)
  • guardrails: drop _standin_create_xml, route through create_xml with attributes/annotation 0.5 d.
  • cluster_stepper: cross-section points gain layer/direction/annotation; lane features gain layer; behind a config flag until Q4/Q5/Q12 are answered 0.5 d.
  • New detectors (trees, signs, …) target the registry builders.

Risks

Open questions

Answer each with number + default / alt 1 / alt 2. Defaults are what I'll build if you say nothing; most are internal decisions, a few must be confirmed by HBW.

  1. KM-sign annotation content (poster [TBA]; drawing shows KM 46.5 / Abs. 90 / Station 0,25). What do we emit?

    default Three <Text> lines in that exact order from attributes km, section, station; lines whose value is unknown are omitted. Ask HBW where Abs./Station come from; until then only KM … is populated.

    alt 1 Display=false + numeric attributes only until HBW specifies the text; customer formats it in Grasshopper.

    alt 2 Single line KM 46.5 only; drop section/station entirely.

  2. Tree annotation formatØ= 0,30/7,0m in the sample, ⌀=[trunk]/[crown]m in poster text.

    default Emit the sample string verbatim (Ø=, space, decimal comma, trunk 2 dp, crown 1 dp) and numeric attributes trunk_diameter_m/crown_diameter_m.

    alt 1 Numbers only (attributes), Display=true with no text — customer builds the string in Grasshopper.

    alt 2 Use the poster glyph and no space (⌀=0,30/7,0m).

  3. Reader tolerance — does the Grasshopper definition tolerate unknown trailing children / is it name-based?

    default Assume name-based and tolerant (true for lane_xml.py; 0.8.0 already inserted Type mid-element without complaint). Ship 1.1 after one acceptance run on a sample file.

    alt 1 Add a writer flag compat_schema="1.0" that suppresses every new element, so the same producer can serve both readers during transition.

    alt 2 Encode new data as XML attributes on existing elements (<Point layer="…" dir="x y z">) — zero child-count change, but ugly for Grasshopper's XML nodes.

  4. Cross-section layer name ("to be discussed as it is part of original XML").

    default Layer=PLC_Punkte_Symbole, Type stays Cross Section; confirm with HBW.

    alt 1 No Layer on cross sections — customer keeps deriving the layer from Type as today.

    alt 2 Two layers: PLC_Punkte_Symbole for annotated points, PLC_Punkte for plain ones (Excel lists both; Punkte marked non-relevant Dec 2024).

  5. Direction-vector semantics for VEC/OWN rotation.

    default Unit vector in the XY plane, Z=0: road-axis tangent at the point for cross-section/terrain/KM/gully/shaft; plate-facing normal for traffic signs; always emitted even for DRW/NO classes.

    alt 1 Full 3D vector straight from the detector (sign-post axis, tree lean); customer projects to XY.

    alt 2 Omit Direction for DRW/NO classes (cross section, terrain, tree, call box); only VEC/OWN classes carry it.

  6. Slope hatch direction — is A/B + the two outlines enough?

    default Yes; nothing extra. Top/bottom roles + 3D outlines determine the downhill side.

    alt 1 Add attribute downhill_direction (unit vector) on the top polyline.

    alt 2 Guarantee vertex correspondence (same count, same order) between the paired polylines so the customer can rule hatches 1:1.

  7. Noise walls — poster says spline sPLC_, Excel says polyline-based, our detector emits polylines as Type=noise_wall.

    default Keep polylines and the existing noise_wall type string; Layer=pPLC_Lärmschutzwand; ask HBW to add a polyline catalog entry; object-name annotation off until specified.

    alt 1 Convert to spline (control points = vertices) on sPLC_Lärmschutzwand to match the poster exactly.

    alt 2 Rename guardrails type strings to Title Case (Noise Wall, Guard Rail) in a coordinated bump of both packages.

  8. Guard-rail label source (EDSP, ESP 2,0).

    default guardrail_type attribute + Annotation/Text on the guardrail polyline; customer places the repeated label along the rail.

    alt 1 Additionally emit guard-rail cross-section Points with FeatureClass=Guard Rails_<type> — the legacy shape the customer's definition may already read.

    alt 2 Display=false until the detector reports the type reliably; attribute only.

  9. Elevation label value/format (terrain + cross-section points).

    default Absolute height (local Z + Geoshift Z), 3 decimals, dot separator (47.333) — matches the sample drawing.

    alt 1 2 decimals with decimal comma (47,33) for consistency with the tree label.

    alt 2 Geoshifted local Z; customer adds the shift in Grasshopper.

  10. Tree-group symbol (Nutzungsart).

    default Not exported — poster says "no input geometry"; customer derives it from closed tree-line blobs.

    alt 1 Export a closed outline polyline feature (Type=Tree Group, Layer=Nutzungsart, add <Closed>true</Closed>).

    alt 2 Export one point feature at the blob centroid on layer Nutzungsart.

  11. Sorting-code index scope.

    default Unique per family (G/B) per XML document; unbounded integer (G10$A fine).

    alt 1 Unique per layer only (Graben and Grabensohle each restart at 1).

    alt 2 Unique per road segment with a segment prefix (G7-2$A) — needs HBW grammar change.

  12. Layer names for the existing lane classes (Axis of the Edge mixes solid splines + dashed lines).

    default No Layer on lane/axis features in 0.10.0 — customer already maps them from Type.

    alt 1 Feature-level Layer from the dominant segment type (Fahrbahnmarkierung_durchgezogen / _gestrichelt).

    alt 2 Per-segment <Layer> on each Line/Spline, derived from its Type.


Poster inconsistencies noticed: column texts copy-pasted wrong (5 & 9 say "Pavement", 6 says "Bushes", 13 says "Shafts"); tree-line catalog numbered 1–4 vs 0–3 in the screenshot; Gullys has Display=False yet a leftover Text km placeholder; terrain/cross-section examples show literal Text points.