Handoff — HighwayData XML schema 1.1: downstream decomposer
Continuation target
HighwayData
schema-1.1 XML document and decomposes it into typed feature objects and geometry —
point symbols (position + Direction + Shapes + Annotation + Attributes),
polylines (incl. closed rings and A/B SortingCode pairs), splines, line segments and
cross-section point sets — dispatched per the §7 feature-class profiles, ready for the
Grasshopper/CAD-side 2D-plan processing at HBW.
Target language is not decided (likely Python, or C#/.NET if it must live inside a Grasshopper component). Everything below is language-agnostic: it is the wire contract, not an API. The writer side is done and frozen enough to code against; this handoff is the contract summary plus the exact numbers to assert in tests.
The XML is the only agreed interface between the ioLabs detection pipeline and HBW's Grasshopper script (spec R-1.1.1). Both sides are implemented independently; do not invent side channels.
Current state (producer side)
- done Writer implements schema 1.1 in full:
src/iolabs_point_cloud_modelling_export/write_xml.py(create_xml+prettify_xml) andfeature_classes.py(Profile,PROFILES, builders,check_profiles). - verified 1064 tests pass (producer repo test suite).
- verified Byte-identity guarantee (R-2.4.1): for a producer dict
containing no 1.1 key, 1.1 output equals the 0.9.0 output except for one inserted line
<SchemaVersion>1.1</SchemaVersion>as the last child ofMetaData. The example builder re-asserts this on real run7 data every time it runs. - done Example document built from real pipeline output + synthetic coverage of 26 of the 28 §7.1 classes and every 1.1 element (see Verification).
- Branch
ai3d-376-xml-schema-1-1, HEAD44428b8(wasc8e0c8bbefore this handoff's spec-HTML sync commit). History since67047e6(v0.9.0 release): S1 compat+SchemaVersion → S2 Feature+Point → S3 Polyline/Spline/Line → S4 profiles+fixture+docs → adversarial-review fixes → example XML. - not yet
pyproject.tomlstill saysversion = "0.9.0"; release 0.10.0 is not cut. Nothing consumer-visible depends on that, but the package you get from Nexus today is still the 1.0 writer. - Spec status: draft for review, 2026-08-28 revision 2. It is stable in structure; only the informative notes have moved recently (latest: N-8.8.4, numpy scalars).
Document structure & ordering guarantees
HighwayData
├─ MetaData CreationDate, Source, SoftwareVersion, PipelineVersions?, SchemaVersion
├─ Geoshift? X, Y, Z
└─ Features
└─ Feature* Type, ID, LaneID,
Lines, NumberOfLines,
Points, NumberOfPoints,
Polylines, NumberOfPolylines,
Splines, NumberOfSplines,
Attributes?
- Child order is normative (R-3.1) and a consumer MAY rely on it — but parse
by name, not by index. The one moving slot:
Polyline/Typesits immediately afterPolyline/LaneID(R-2.3.2), soPolylinechild index 2 is not reliablyRoadPoint. Every other NEW element is appended after all 1.0 children of its parent (R-2.3.1). - All four containers and their four
NumberOf*siblings are always emitted, interleaved as shown, empty (<Lines/>) and0when unused (R-2.2.1, R-3.3).NumberOf*equals the count in the preceding container (R-3.4) — cheap sanity check. - No XML namespaces. No attributes except:
Step/@name @date,SegmentRange/@segments @date,Package/@name @version,Attribute/@name @type,Shape/@role @type(R-3.6). No mixed content: every element is element-only or text-only (R-3.5). - UTF-8, declaration
<?xml version="1.0" encoding="UTF-8"?>, two-space pretty-printed, non-ASCII (ä ö ü ß Ø ⌀) as literal UTF-8, never character references (R-5.3.1–3). - Version:
MetaData/SchemaVersionis text1.1, matching^[0-9]+\.[0-9]+$. Absent ⇒ treat the document as schema 1.0 (R-2.1.2). A minor bump is additive; treat an unknown minor as 1.1 + ignorable extras, a major bump as unsupported. - Coordinates are metres in the geoshifted local right-handed export frame.
Absolute coordinate = export value + the matching
Geoshift/X|Y|Z.Geoshiftis optional. - There is no
Feature/Layer(R-2.2.4). The CAD layer is a drawing artefact the consumer derives fromFeature/Type— informative mapping in spec Appendix C (Tree → PLC_Laubbaum,Fence → pPLC_Zaun,Cross Section → PLC_Punkte_Symbole, …).Pedestrian Roadis the one class whose layer also depends on the container used (Polylines → pPLC_Gehweg,Splines → sPLC_Gehweg, R-7.12.1). Layer names are unconfirmed with HBW (O-21) — keep the table in one swappable lookup. Feature/Typeis the only class key (R-2.5.1); dispatch on it, never onPoint/FeatureClass. It is an open vocabulary — the writer never validates it, so new classes can appear without a writer release. UnknownType⇒ decompose generically, don't crash.
Per-element parsing rules
Scalars
- Numbers: Python
str(value)output — no fixed precision, no padding, no localisation. You will see0.1,1.0,12,-40.199348933220996. Decimal separator is always.in numeric elements (R-5.1.4). Parse with a plain float parse; never assume decimals. A decimal comma is legal only insideAnnotation/Text(display strings, e.g.Ø= 0,30/7,0m). - Booleans: exactly lower-case
true/false(R-5.2.1).True,1,yesare not valid and need not be accepted. - Text: the writer never trims. Strip leading/trailing whitespace when reading
(R-5.3.4).
& < >arrive XML-escaped; your XML library un-escapes them. - IDs: producer-supplied strings, intended unique per document, SHOULD match
^[A-Za-z0-9_.:-]+$. See tolerance — uniqueness is not guaranteed. LaneIDis text, not numeric."0"= not lane-bound (every 1.1 profile, R-7.0.3). Frozen classes carry real lane ids ("A","B"in the run7 example).
Point
Order: ID, LaneID, PointerNumber, X, Y, Z, Color, FeatureClass, FeatureID, RoadPoint?,
Direction?, Shapes?, Annotation?, Attributes?
PointerNumberis integer text — for Cross Section it is the cross-section plane index; for 1.1 point symbols the writer defaults it to the 1-based index within thePointscontainer.Color,FeatureClass,FeatureIDare always emitted; for 1.1 symbols they default to empty / owningFeature/Type/ owningFeature/ID. Legacy cross sections carry the literal stringNone— see tolerance.Point/RoadPointis a scalar (chainage along the road), unlike theFrom/Topair onLine/Polyline/Spline(R-4.17). It is optional in 1.1 (point symbols with no chainage omit it).
Point/Direction NEW
- Children
X, Y, Z: a full 3D unit vector in the export frame,|v| = 1 ± 1e-6.Zmay be non-zero (tilted plates) — do not assume planar. - Semantics depend on the class (R-5.5.5): for
Traffic SignandKM Signit is the plate normal pointing towards oncoming traffic; forGully,Shaft,Emergency Call Box,Terrain Point,Tree,Verge Postit is the road-axis tangent at the point. - For a 2D plan rotation: project onto XY and re-normalise there (N-5.5.4 — catalog rotation mode
VEC).Directionis present for every 1.1 point class even when the catalog rotation mode ignores it (NO,ORD,DRW) — R-5.5.6. - An
Attribute name="direction_source"with valuemeasured|fallbackmay accompany it (R-5.5.7).fallback= orientation was not measurable; treat the vector as a placeholder.
Point/Shapes/Shape NEW
@roleREQUIRED, free text scoped by class — known:trunk,crown,post.@typeREQUIRED, exactlycylinder|cone.cylinder⇒Base,Top,Diameter;cone⇒Base,Apex,Diameter. Exactly one ofTop/Apex, selected by@type(R-4.20.3).Base= centre of the bottom circle,Top= centre of the top circle,Apex= cone tip. Absolute points in the export frame, independent of the owningPoint's coordinates — the shape locks its own 3D position, leaning trunks included (R-5.8.1/2/3).Diameter= diameter of the base circle, metres, finite and > 0; constant along a cylinder's axis. Axis isBase→Top/Base→Apexand is non-degenerate (R-5.8.5).- Producer order within
Shapesis preserved.Treecarries exactly onetrunkcylinder + onecrowncylinder-or-cone (R-5.8.7) — but this is only a writer warning, so handle deviations.
Annotation NEW (on Point, Polyline, Spline, Line)
- Children in order:
Display(1, boolean),Legible(0..1, enum),Text(0..n). Display=truemeans render a label at this geometry. Whether the label text travels in the document is per-class (see the §7.1 table).Text= one element per rendered line, in draw order. Never contains\n/\r. Render verbatim: it is a finished display string produced upstream (decimal comma,Ø=, unit suffixes are all already in it) — do not re-format, re-round, localise or reorder (R-4.24.4).Textabsent whileDisplay=trueis legal and means one of two things: (a) the consumer derives the label itself —Terrain Pointand Cross Section elevation labels, rendered fromPoint/Z; or (b) it was unrecoverable (Legible=none).- Elevation label rendering (N-5.7.2, agreed but consumer-side): absolute height =
export-frame
Z+Geoshift/Z, exactly three decimals, dot separator, no unit suffix, no thousands separator — e.g.47.333. Textis never emitted whenDisplay=false.
Annotation/Legible — enumeration, not a boolean
| Value | Meaning | Text |
|---|---|---|
full | Every character of the physical text was read. | complete lines, no substitution marks |
partial | Some of the text was read. | the readable lines, unreadable characters marked; entirely unreadable lines omitted |
none | Nothing readable. | omitted — but Display stays true, so still place the symbol |
- Exactly one of
full|partial|none, lower-case (R-5.10.1). Enumerations are closed: an unknown value ⇒ treat the element as absent, never guess (N-5.10.4). - Only meaningful where the text is transcribed from a physical object — today
KM Signonly (Traffic Signplates later). It describes the legibility of the inscription, never detection confidence (N-4.23.5). - Substitution marks (R-4.24.5): under
partial, one unreadable character is a single?; an unreadable run of unknown length is?…(U+003F + U+2026). Render them verbatim — stripping them silently fabricates a complete label (N-4.24.6). KM Signtext is up to three lines, in order:KM <km>,Abs. <section>,Station <station>; an unknown line is omitted and the rest keep their order (R-5.7.4). The machine values also ride as attributeskm,section,station, so you can re-format without a re-export.
Attributes / Attribute NEW
- Always the last child of its parent (
Feature,Point,Polyline,Spline,Line). Never emitted empty. 1..nAttribute, sorted ascending by@name,@nameunique within the parent. @nameis snake_case with the unit in the name (length_m,height_m,top_height_p90_m;_mmetres,_degdegrees,_sseconds — a convention, not a grammar).@type∈string|integer|float|boolean; element text is the serialised value (boolean⇒true/false;float/integer⇒str(v)). Values are always scalar — no nesting is representable. ANone-valued attribute is skipped entirely, so an absent attribute means "no value", never "null".- Names seen in the example document:
detector_reviewed,direction_source,faces,guardrail_type,halfwidth_m,height_m,km,lateral_offset_m,length_m,outline_source,parent_guardrail_id,plate_class,post_confidence,post_count,post_spacing_m,section,side,species_class,station,top_height_p90_m,tree_count,width_m. Treat the set as open — build a generic bag, then read known keys out of it. - Legacy note: the guardrails stand-in exporter used non-uniform tags
(
<Guardrailtype>,<Topheightp90m>, …). Schema 1.1 never emits those; the uniform<Attribute name= type=>form replaces them (N-8.6.4).
Polyline
Order: ID, LaneID, Type?, RoadPoint?, Vertices, SortingCode?, Closed?, Annotation?, Attributes?
Verticesalways present;Vertex=ID, X, Y, Z. Vertex ids are writer-generated as{polyline_id}-{i}, 1-based (R-4.29.3) — informational only, don't key on them.Polyline/Typeis an open string, used today astop/bottomfor trench and slope outlines (human readability; theSortingCodeis the actual contract).Closed:true= the vertex list is a closed ring, the first vertex is not repeated as the last, and there are ≥3 vertices — the consumer closes it (R-4.27.1).SortingCode— regex, the single normative definition:^[A-Z]+(0|[1-9][0-9]*)\$[AB]$[A-Z]+= family (G= Graben/Grabensohle,B= Böschung); digits = pair index, unbounded non-negative integer, no leading zeros;$= separator; trailingA= top outline,B= bottom outline. Example values in the sample docs:G1$A,G1$B,B1$A,B1$B.- Pair index is unique per family per document; every
(family, index)should occur exactly twice — onceA, onceB. The two halves may come from different detectors or segments, so an incomplete pair is possible (writer only warns) — degrade gracefully: draw the outline, skip the hatch. - Hatches are never exported. The consumer derives the trench/slope hatch from the
A/B outline pair (§7.1 last row, N-C.2). Two
SortingCodevalues are never equal within a document, so the code is a safe pairing key. - Note the class asymmetry:
Trench(G<i>$A) andTrench Bottom(G<i>$B) are two separateFeatures;Slopeis oneFeaturewith twoPolylines (B<i>$A+B<i>$B).
- Pair index is unique per family per document; every
Spline and Line
Spline:ID, LaneID, Type?, RoadPoint?, Width?, Fit_Points, Control_Points, Annotation?, Attributes?.Fit_Pointsis always emitted and may be empty (<Fit_Points/>) so the child layout never varies;Control_Pointscarries the curve. Point ids run{spline_id}_{n+1}continuously across fit points then control points.- gap The spline is underspecified for exact 3D reconstruction (O-22, pre-existing since 1.0): degree, knots and weights are not carried. Producer and consumer must assume the same convention — pin yours explicitly and raise it with HBW.
Line:ID, LaneID, Type?, StartPoint, EndPoint, RoadPoint, Width?, Annotation?, Attributes?.StartPoint/EndPointhaveX, Y, Z;RoadPointhasFrom/To.Line/TypeandSpline/Typeare open strings; the vocabulary in use today issolid|dashed(lane markings) — informative only, do not validate.Width(onLineandSpline): optional element with optional childrenValue,Sigma,Status,Flagged— each present only when the producer had a value.Flaggedis lower-case boolean text.
§7.1 feature classes — what the decomposer must produce per Feature/Type
Geometry kind = which container carries the class. "Annot." = the required
Annotation/Display value, — = the class MUST NOT carry Annotation.
"Text" = whether label text travels in the document. Derived from spec §7.1 (cross-checked against the producer's profile table); this table is self-sufficient — no repo access needed.
| § | Feature/Type | Container | Required geometry | Annot. | Text | Direction | Shapes | Attributes |
|---|---|---|---|---|---|---|---|---|
| 7.2 | Tree Line | Splines | ≥2 control points | false | — | — | — | — |
| 7.3 | Tree Group | Polylines | ≥3 vertices, Closed=true | false | — | — | — | — |
| 7.4 | Bush | Splines | ≥2 control points | false | — | — | — | — |
| 7.5 | Tree | Points | position | true | 1 line | req. | trunk + crown | opt. species_class |
| 7.6 | Fence | Polylines | ≥2 vertices | false | — | — | — | — |
| 7.7 | Paving Edge | Polylines | ≥2 vertices | false | — | — | — | — |
| 7.8 | noise_wall | Polylines | ≥2 vertices | — | — | — | — | height_m, length_m |
| 7.9 | Trench | Polylines | ≥2 vertices, G<i>$A | false | — | — | — | — |
| 7.10 | Trench Bottom | Polylines | ≥2 vertices, G<i>$B | false | — | — | — | — |
| 7.11 | Slope | Polylines ×2 | B<i>$A + B<i>$B in one Feature | false | — | — | — | — |
| 7.12 | Pedestrian Road | Polylines or Splines | ≥2 vertices / control points | false | — | — | — | — |
| 7.13 | Terrain Point | Points | position | true | none — render from Z | req. | — | — |
| 7.14 | Traffic Sign | Points | position | false | — | req. (plate normal) | opt. post cylinder | opt. height_m, plate_class |
| 7.15 | Shaft | Points | position | false | — | req. | — | — |
| 7.16 | Emergency Call Box | Points | position | false | — | req. | — | — |
| 7.17 | Gully | Points | position | false | — | req. | — | width_m |
| 7.18 | KM Sign | Points | position | true + Legible | 0..3 lines | req. (plate normal) | — | km, section, station |
| 7.19 | Wall | Polylines | ≥2 vertices | false | — | — | — | height_m, length_m |
| 7.20 | guardrail | Polylines | as today (1.0 geometry) | true | 1 line (type label) | — | — | guardrail_type, migrated legacy keys |
| 7.24 | guardrail_support, guardrail_top_rail | Polylines | as today | — | — | — | — | cadence / corridor keys |
| 7.25 | Verge Post | Points | position | false | — | req. | opt. post cylinder | height_m, width_m |
| 7.21 | Axis of the Edge, Center Lines — FROZEN | Splines + Lines | as today (solid = Splines, dashed = Lines, per-segment Type/Width) | — | — | — | — | — |
| 7.22 | Central Axis, Both-Sides Central Axis, Single-Side Central Axis — FROZEN | Polylines | as today, RoadPoint present | — | — | — | — | — |
| 7.23 | Cross Section — FROZEN | Points | as today; PointerNumber = plane index, RoadPoint = distance along road | — | — | — | — | — |
| — | Hatches (Böschung / Graben) | — | never exported — derived by the consumer from the A/B SortingCode pair | — | — | — | — | — |
Frozen vs 1.1-touched. The three frozen groups (§7.21 lane markings, §7.22 road axes,
§7.23 cross sections) carry no NEW element whatsoever — no Direction, no
Shapes, no Annotation, no Attributes, no SortingCode,
no Closed, no Polyline/Type — and keep their real LaneID and
RoadPoint semantics. Everything else is "1.1-touched" and follows R-7.0.3:
LaneID = "0" and RoadPoint omitted, unless the profile says otherwise
(guardrail keeps its RoadPoint). Your decomposer can safely take a
frozen-path branch (pure 1.0 parsing) and a 1.1 path, keyed by Feature/Type.
What the decomposer must tolerate
Spec §6.1 defines a conforming document; §6.2 lists what the writer rejects; §6.3 lists what it only warns about and still emits. Everything in §6.3 can and does reach you. Be liberal: log and continue, never abort the document.
- Unknown elements at any depth — ignore them (R-2.5.3, N-2.3.4). All NEW elements are
appended after the 1.0 children of their parent precisely so that a name-based reader is unaffected.
Pre-release drafts may even contain a
Feature/Layerchild; ignore it. - Duplicate IDs are real. ID uniqueness is a producer obligation the writer only warns
about. The published example
(
run7_branch_001_schema_1_1.xml) contains 103 distinct duplicated geometry ids across 276 elements, all from the real frozen-type export. Never build a globalid → objectdictionary. Key by(feature index, container, geometry index)or(Feature/ID, geometry ID), and treatIDas a label. - Literal
"None"strings. Legacy cross-section points carry<Color>None</Color>,<FeatureClass>None</FeatureClass>,<FeatureID>None</FeatureID>— a Pythonstr(None)coercion preserved from 1.0 for byte-identity. Map the exact stringNoneto null for these three 1.0 text fields (do not apply this toAnnotation/Textor attribute values). - Empty text is legal for 1.0 elements (
<Color/>, an emptyStep/@date). NEW elements never carry empty/whitespace-only text. - Meaningless
RoadPointon realLine/Splinedata: the pipeline emitsFrom=0.0,To=0.0for lane-marking segments. Do not derive chainage from it. - §6.3 warn-class irregularities you may encounter: a
Polylinewith <2 vertices or aSplinewith <2 control points (legacy content only); aDirectionwhose length is outside1 ± 1e-6; an incomplete(family, index)sorting-code pair;Legibleon a class that should not carry it; aTextcontaining a newline;Closed=truewith a repeated closing vertex or fewer than 3 vertices; aTreewhose shapes are not exactly one trunk + one crown; a non-snake_case@name; a non-finite coordinate outside a NEW element; a 1.1 key on a frozen-profile feature; anAnnotationmissing where §7.1 requires it (a legacyguardrailpolyline that has not migrated yet is the common case). - Not all §7.1 classes appear in any one document, and classes outside §7.1 can appear at
any time —
Feature/Typeis an open vocabulary (R-4.9.1, R-6.4.1). - Legacy 1.0 documents (no
SchemaVersion) must still parse. For guard-rail labels in 1.0 files, the fallback is the legacyPoint/FeatureClassconventionGuard Rails_<type>(N-2.5.4); in 1.1 read theguardrail_typeattribute andAnnotation/Text.
Key artifacts
| What | Where |
|---|---|
| Normative spec (1698 lines) | miro-plans.pages.dev/pages/xml-schema-1-1-spec-20260828 (markdown source in the producer repo: docs/xml_schema_1_1_spec.md) |
| Spec page source | miro-plans.pages.dev/pages/xml-schema-1-1-spec-20260828 (source: docs/pages/xml-schema-1-1-spec-20260828.html) |
| Example document — 378 KB, 39 features, real + synthetic | miro-plans.pages.dev/pages/run7_branch_001_schema_1_1.xml (repo: docs/examples/run7_branch_001_schema_1_1.xml) |
| Reference fixture — 34 KB, one feature per profile, all 28 classes | miro-plans.pages.dev/pages/sample_v1_1.xml (repo: tests/fixtures/sample_v1_1.xml) |
| 1.0 golden (schema-1.0 shape, for legacy-path tests) | miro-plans.pages.dev/pages/golden_0_9_0.xml (repo: tests/fixtures/golden_0_9_0.xml) |
| The rows below live only in the producer repo — optional reference, not required for the decomposer. | |
| Writer (producer reference implementation) | src/iolabs_point_cloud_modelling_export/write_xml.py |
§7.1 profile table + builders + check_profiles | src/iolabs_point_cloud_modelling_export/feature_classes.py |
| Example builder (docstring + summary tables) | scripts/build_example_xml.py |
| Design predecessor of the spec | docs/hbw_feature_classes_xml_plan.md |
| Producer repo / branch (access optional) | iolabs-point-cloud-modelling-export, branch ai3d-376-xml-schema-1-1, HEAD 44428b8 |
| Data source of the real half | run 260722_run7_line_width, run7 branch_001 (lanefinder_abschnitt_1), first ~290 m, dual three-lane carriageway |
Sections worth reading first, in order: spec §3 (element tree), §4 (element reference), §5 (value types, regexes, enums), §7.1 + §7.2–§7.25 (profiles), §6.3 (what you must tolerate), §9 (six complete worked documents), Appendix C (layer mapping). §8 is the producer dict contract — read it only if you also emit documents.
Verification — numbers to assert against the published example
Parse run7_branch_001_schema_1_1.xml and assert the following. These are the exact
figures the build script prints; they are stable (the script is deterministic, jitter seeded 20260828).
File: 378 632 bytes, 11 806 lines. The real half is byte-identical to the 0.9.0 writer's
331 528-byte output apart from SchemaVersion.
Features per Type — 39 total (11 real + 28 synthetic), 26 of 28 §7.1 classes
Type | n | origin | Type | n | origin |
|---|---|---|---|---|---|
Axis of the Edge | 4 | frozen | Slope | 1 | synthetic |
Center Lines | 4 | frozen | Terrain Point | 3 | synthetic |
Central Axis | 1 | frozen | Traffic Sign | 1 | synthetic |
Cross Section | 2 | frozen | Tree | 1 | synthetic |
Bush | 1 | synthetic | Tree Group | 1 | synthetic |
Emergency Call Box | 1 | synthetic | Tree Line | 1 | synthetic |
Fence | 1 | synthetic | Trench | 1 | synthetic |
Gully | 2 | synthetic | Trench Bottom | 1 | synthetic |
KM Sign | 3 | synthetic | Verge Post | 2 | synthetic |
Paving Edge | 1 | synthetic | Wall | 1 | synthetic |
Pedestrian Road | 1 | synthetic | guardrail | 1 | synthetic |
Shaft | 1 | synthetic | guardrail_support | 1 | synthetic |
noise_wall | 1 | synthetic | guardrail_top_rail | 1 | synthetic |
Absent from this document (this run has none): Both-Sides Central Axis,
Single-Side Central Axis — both are present in sample_v1_1.xml.
Schema-1.1 element counts (example document)
| Element | n | Element | n |
|---|---|---|---|
MetaData/SchemaVersion | 1 | Shape/Base | 5 |
Feature/Attributes | 1 | Shape/Top | 4 |
Point/Attributes | 11 | Shape/Apex | 1 |
Polyline/Attributes | 5 | Shape/Diameter | 5 |
Attribute @type=string | 15 | Annotation | 26 |
Attribute @type=integer | 2 | Annotation/Display=true | 8 |
Attribute @type=float | 20 | Annotation/Display=false | 18 |
Attribute @type=boolean | 1 | Legible=full / partial / none | 1 / 1 / 1 |
Point/Direction | 14 | Annotation/Text | 7 |
Point/Shapes | 4 | Annotations with >1 Text | 2 |
Shape @type=cylinder | 4 | Polyline/Type | 4 (top×2, bottom×2) |
Shape @type=cone | 1 | Polyline/SortingCode | 4 (G1$A, G1$B, B1$A, B1$B) |
Shape roles | post/cylinder ×3, trunk/cylinder ×1, crown/cone ×1 | Polyline/Closed | 1 |
Polyline/RoadPoint (From|To) | 2 | Spline with non-empty Fit_Points | 1 |
Geometry totals
| Document | Features | Line | Point (in Points) | Polyline | Vertex | Spline | spline Points (fit+control) |
|---|---|---|---|---|---|---|---|
run7_branch_001_schema_1_1.xml | 39 | 60 | 292 | 13 | 126 | 11 | 820 |
sample_v1_1.xml | 28 | 2 | 11 | 16 | 38 | 4 | 15 |
Suggested consumer-side test set
- Parse both published documents; assert the tables above (feature counts per
Type, 1.1 element counts, geometry totals). Assert everyNumberOf*equals its container's count. sample_v1_1.xmlis the coverage fixture: exactly one feature per profile, all 28 classes includingBoth-Sides / Single-Side Central Axis. Use it to prove the dispatch table is complete; use the run7 example to prove robustness on real, messy data.- Assert 103 duplicated geometry ids / 276 elements in the run7 example are handled without collision or data loss (this is the regression test for the "no global id map" rule).
- Round-trip: decompose → re-emit → compare per-feature geometry counts and coordinates. Do not
aim for byte round-trip; number formatting is
str()-shaped and not reproducible from floats. - Negative tests: a document with no
SchemaVersion(treat as 1.0), an unknownFeature/Type, an unknownLegiblevalue, an unknown trailing child, aLegible=noneannotation with noText, an incompleteSortingCodepair. - Assert the elevation-label rendering of N-5.7.2 on a
Terrain Pointand a Cross Section point:Z + Geoshift.Z, 3 decimals, dot separator (e.g.47.333).
Next steps (ordered)
- Decide the language and the deployment shape first — a Python library, or a C#/.NET assembly loadable by a Grasshopper component. That decision drives everything else, and it interacts with open item O-18 (which XML component HBW uses).
- Read spec §3, §4, §5, §7.1 and mirror §7.1 into a single class-profile table
(
Type → container, geometry kind, expected optional elements, layer). The §7.1 table on this page already encodes exactly this in a compact form. - Write the structural reader: name-based traversal, unknown elements ignored,
NumberOf*cross-check as a warning (not a failure). No index-based child access anywhere. - Write the value layer: number/boolean/text parsing, the
SortingCoderegex, theLegibleandAttribute/@typeandShape/@typeenums, the"None"-literal mapping, the twoRoadPointshapes. - Build the typed objects: point symbol (position, direction, shapes, annotation, attributes), polyline (vertices, closed, sorting code, type), spline (fit + control points), line segment (start/end, width), cross-section point set. Keep a raw-attribute bag on every object so unknown keys survive.
- Dispatch on
Feature/Typeinto the profile table: frozen path (pure 1.0 parsing) vs 1.1 path. Unknown type ⇒ generic geometry + a diagnostic, never an exception. - Add the consumer-only derivations that the document deliberately does not carry:
layer from Appendix C; elevation labels from
Z(N-5.7.2); trench/slope hatches from the A/BSortingCodepairs; 2D rotation from the XY projection ofDirection; closing ofClosed=truerings; symbol placement alongTree Line/Bushsplines. - Run the acceptance pass with HBW: feed the published example XML through the real Grasshopper component and confirm it tolerates unknown trailing children (O-18). Do this before the 0.10.0 release — it is the one open item that could still force a schema change.
- Confirm the Appendix C layer names and the elevation-label format with HBW (O-21, O-20) and fold the answers back into the spec.
Risks & open questions
| ID | Question | Current position | Impact on the decomposer |
|---|---|---|---|
| O-18 highest | Which Grasshopper XML component HBW uses; does it tolerate unknown trailing children? | Assume name-based reading; one sample-file acceptance run before release (N-2.3.4) | The whole trailing-placement compatibility strategy rests on this. Verify early with the published example. |
| O-20 | Does HBW render elevation labels from Z per N-5.7.2 (absolute, 3 decimals, dot)? |
Consumer-side rendering; no Text in the document for Terrain Point / Cross Section |
The decomposer must implement this itself — it is not in the XML. Confirm the format. |
| O-21 | Appendix C Type → CAD layer table, full confirmation with HBW |
Poster/Excel values as listed; informative only | Keep the mapping in one swappable table; expect renames. |
| O-15 | Tree label glyph Ø (U+00D8) vs ⌀ (U+2300), spacing, precision, decimal comma |
Ø= 0,30/7,0m reference rendering, producer-formatted; the writer never substitutes |
Render Annotation/Text verbatim; do not normalise the glyph. Font must have it. |
| O-22 | Spline definition underspecified: Fit_Points/Control_Points given, degree, knots and weights not (pre-existing 1.0 gap) |
Consumer and producer assume the same convention (informative) | Pin your interpretation explicitly and write it down; exact 3D reconstruction is not guaranteed. |
| O-23 | 3D-model completeness: heights are optional attributes, symbol classes have no extents, no surface concept beyond Shape |
Not required by the 2D-plan contract | If a 3D deliverable appears, expect new Shapes / mandatory heights — keep the shape model generic. |
| O-19 | KM-sign km/section/station source: our chainage vs HBW's reference axis |
Attributes carry whatever the producer knows; unknown lines omitted | Do not assume the three attributes and the three text lines always agree or are always present. |
| O-16 | Verge Post class name and layer PLC_Randpfosten |
Provisional | The Feature/Type string may change; keep it a table entry, not a hard-coded literal. |
| O-14 | direction_source="fallback" when plate orientation is unknown |
Emitted as <Attribute name="direction_source"> |
Read it; a fallback direction should not drive a confident plan rotation. |
| O-17 | Noise-wall "object name" annotation content | No Annotation emitted for noise_wall (§7.8) |
Do not expect a label there today; it may arrive later as a new trailing element. |
Non-spec risks
- release
pyproject.tomlstill reads0.9.0; 0.10.0 has not been cut. The 1.1 writer only exists on branchai3d-376-xml-schema-1-1. Real pipeline output you receive today is still schema 1.0 — the legacy path is not hypothetical, build it first. - convention Every 1.1 profile in the example uses
LaneID = "0"(R-7.0.3, correct per spec). The example builder has aLANE_BOUNDswitch producing a side-bound variant ("A"/"B") for experiments — do not treatLaneIDas always"0"for 1.1 classes, and do not treat it as numeric. - data 28 of the 39 example features are synthetic (placed around the real road by the build script). They are spec-conformant but not detector output: coordinate distributions, attribute values and label strings are illustrative. The 11 real features are the frozen classes only — there is no real detector output yet for any 1.1 class.
- producer TBD
Tree Groupis specified but the outline detector has not shipped; documents will not contain it until then. Implement against the profile, but don't block on real data. - The spec is still draft for review. Track it by rule number (
R-x.y.z) in code comments — the numbering is stable across revisions, prose is not.