Section Line Indicator Detection Pipeline#
This document describes the internal detection pipeline used by
ViewSectionLineDetector to find section line indicators (renvois de vues) in
technical drawings. It is intended for developers working on or extending the
detection logic.
Note
For user-facing documentation on working with section line indicators, see Featured Classes Guide.
What is a Section Line Indicator?#
A section line indicator is a symbol on a technical drawing that shows where a section cut or auxiliary view is taken. It consists of:
Two collinear arrows pointing in the same direction
A letter identifier on each arrow (e.g., “A”, “B”, “K”)
Optionally, a cross-reference to a sheet number (e.g., “4/5”)
A A
↓ ↓
──────┼────────────┼──────
↓ 4/5 ↓
These symbols indicate where section views (COUPE A-A), detail views, or auxiliary views are cut from.
Pipeline Overview#
ViewSectionLineDetector finds indicators through two independent branches,
similar to how SheetTableDetector detects tables from multiple sources:
from_symbols —
TypeNoteannotations with 2+ collinearFillArrowleaders and duplicated letter textfrom_composites —
CompositeEntityarrows paired with nearby standaloneTypeNoteletter labels
ViewSectionLineDetector.detect_all()
│
├── _extract_from_symbols() → list[SectionLineIndicator]
│ ├── ViewCollector.type.by_types(Symbol/TypeNote) → all_notes
│ └── for each note:
│ ├── _get_valid_symbol_arrow_pair(note) → symbol_arrows | None
│ │ ├── _filter_visible_symbol_arrows(note) → remove ghosts
│ │ ├── _has_exactly_two(symbol_arrows)
│ │ └── _are_collinear(symbol_arrows)
│ ├── get_text_primitives(note) → text_primitives | None
│ ├── _extract_duplicated_identifier_info(text_primitives)
│ │ ├── match_identifier_texts(text_primitives)
│ │ └── _find_first_duplicate()
│ ├── _complete_identifier_info_with_duplicated_xref()
│ │ └── _extract_duplicated_standalone_xref()
│ └── SectionLineIndicator(symbol_arrows + identifier_info)
│
└── _extract_from_composites() → list[SectionLineIndicator]
├── Step 1: _get_composite_arrow_candidates()
├── Step 2: _collect_identifier_notes()
│ └── match_text_against_pattern()
├── Step 3: _match_arrows_to_identifiers() → list[_IdentifiedArrow]
│ └── _find_nearest_arrow()
├── Step 4: _group_identified_arrows() → list[list[_IdentifiedArrow]]
│ ├── _belong_to_same_section_line()
│ │ ├── same identifier + same cross-reference
│ │ ├── collinear directions
│ │ └── _are_tips_on_same_perpendicular_line()
│ └── nx.connected_components()
└── Step 5: _build_indicators_from_groups()
Source Annotation Structure#
Understanding the source data is essential for following the detection logic.
Symbol representation (from_symbols)#
In this representation, a single Symbol annotation (entity_type="TypeNote")
carries both the arrows and the text labels:
Symbol (entity_type="TypeNote")
├── annotation: Annotation
│ └── leaders: list[Leader] (2 for a section line indicator)
│ ├── leader[0].arrow_head
│ │ ├── arrow_type: "TerminatorTypeFillArrow"
│ │ ├── location: Point2D (arrow tip position)
│ │ └── direction: Vector2D (arrow direction, collinear with leader[1])
│ └── leader[1].arrow_head (same structure)
│
└── text: CompositeText
└── get_text_primitives() → list[Text]
Text primitives are duplicated (one set per arrow/leader):
Without cross-reference (2 primitives):
Text("E"), Text("E")
With standalone cross-reference (4 primitives):
Text("K"), Text("4/5"), Text("K"), Text("4/5")
With empty texts (6 primitives):
Text("G"), Text(""), Text("4/5"), Text("G"), Text(""), Text("4/5")
This duplication is why the parsing requires an identifier appearing at least twice.
Composite representation (from_composites)#
In this representation, each arrow is a separate CompositeEntity annotation and
the letter labels are standalone TypeNote annotations positioned nearby:
View annotations:
├── CompositeEntity (14 geometries) ← arrow 1
│ └── geometries: LineSegment2D × 14 (shaft + arrow head lines)
├── Symbol/TypeNote ← label "A" near arrow 1
│ └── text: "A"
├── CompositeEntity (14 geometries) ← arrow 2
│ └── geometries: LineSegment2D × 14
└── Symbol/TypeNote ← label "A" near arrow 2
└── text: "A"
Result Class: SectionLineIndicator#
Each detected indicator is a SectionLineIndicator (subclass of
DessiaObject) with the following attributes:
Attribute |
Type |
Description |
|---|---|---|
|
|
Full identifier (e.g., |
|
|
Letter part only (e.g., |
|
|
Numeric suffix (e.g., |
|
|
Positions of the two arrow tips |
|
|
Shared direction vector of both arrows |
|
|
Sheet cross-reference string (e.g., |
|
|
Left part of cross-reference (e.g., |
|
|
Right part of cross-reference (e.g., |
|
|
Detection branch: |
|
|
Source dessia_drawing entities. Symbols: |
Text Patterns#
Patterns reuse IDENTIFIER_PATTERN and BARE_CROSS_REFERENCE_PATTERN from
default_language_configs.py. Regex group extraction uses
IdentifierInfo.from_match() from title_reader.py:
from drawing_tools.config.default_language_configs import (
BARE_CROSS_REFERENCE_PATTERN, IDENTIFIER_PATTERN,
)
# Full indicator label: "A", "B2", "K 4/5", "A2 1/3"
DEFAULT_INDICATOR_PATTERN = re.compile(
rf"^{IDENTIFIER_PATTERN}(?:\s+{BARE_CROSS_REFERENCE_PATTERN})?$"
)
# Standalone cross-reference: "4/5", "1/3"
DEFAULT_STANDALONE_XREF_PATTERN = re.compile(
rf"^{BARE_CROSS_REFERENCE_PATTERN}$"
)
Branch 1: from_symbols#
This branch detects section line indicators encoded as a single TypeNote
annotation that carries both arrows and text labels in one entity.
Step 1.1 — Collect all TypeNote annotations#
A ViewCollector is used to find all Symbol annotations of sub-type
TypeNote:
all_notes = (
ViewCollector(view)
.type.by_types(
options=ViewCollectorOptions(
target="annotations",
include_types=["Symbol"],
entities_subtypes={"Symbol": ["TypeNote"]},
)
)
.annotations
)
Step 1.2 — Validate arrows: _get_valid_symbol_arrow_pair(note)#
Orchestrates three validation sub-methods in order:
_filter_visible_symbol_arrows: extractsFillArrowleaders (filtered byself.symbol_arrow_type) and removes ghost leaders with no visual primitives (CAD conversion artifacts)._has_exactly_two: checks that exactly 2 arrows remain._are_collinear: verifies the 2 arrow directions are collinear withinself.direction_tolerance(defaultsin(5°) ≈ 0.087).
Returns the list of 2 valid symbol arrows, or None.
Step 1.3 — Extract identifier#
Accesses the TypeNote’s CompositeText via get_text_primitives(note)
(from identifier_info.py) to get individual Text objects. Then runs a
two-step process to produce an IdentifierInfo:
Step 1 — Identifier extraction (_extract_duplicated_identifier_info):
match_identifier_texts(text_primitives, pattern)(fromidentifier_info.py) matches each stripped text primitive against the indicator pattern._find_first_duplicate()finds the first identifier appearing at least twice (e.g., two"A"or two"B2"). The duplication comes from the TypeNote structure which repeats texts once per arrow/leader.
Step 2 — Cross-reference completion (_complete_identifier_info_with_duplicated_xref):
Only called if the IdentifierInfo from Step 1 has no cross-reference.
Searches for standalone n/n texts appearing at least twice via
_extract_duplicated_standalone_xref.
Example TypeNote text primitives (case with standalone xref):
┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐
│ "K" │ │ "4/5" │ │ "K" │ │ "4/5" │
└────────┘ └────────┘ └────────┘ └────────┘
↓ ↓ ↓ ↓
letter="K" xref="4/5" letter="K" xref="4/5"
└── duplicated_identifier="K" ──┘
└── duplicated xref ──┘
Step 1.4 — Build SectionLineIndicator#
If both symbol_arrows and identifier_info are available, a
SectionLineIndicator is created with detection_source="symbols".
If _extract_duplicated_identifier_info returns None, a warning is logged
and the note is skipped.
Branch 2: from_composites#
This branch detects section line indicators where the arrows are encoded as
separate CompositeEntity objects and the letter labels are standalone
TypeNote annotations positioned nearby. The detection follows a four-step
pipeline orchestrated by _extract_from_composites.
Step 2.1 — _get_composite_arrow_candidates()#
Returns pre-computed composite arrow candidates (from FeaturedView._composite_arrows)
or collects them via collect_composite_arrows(). Each candidate is a
(CompositeEntity, Point2D, Vector2D) tuple with the arrow tip and direction.
Step 2.2 — _collect_identifier_notes()#
Collects all TypeNote annotations in the view whose text matches
self.indicator_pattern via match_text_against_pattern() from
identifier_info.py. Returns two parallel lists:
(identifier_notes, identifier_infos).
Step 2.3 — _match_arrows_to_identifiers()#
For each identifier note, computes its bounding rectangle center and finds the
nearest unassigned arrow candidate via _find_nearest_arrow using
BoundingRectangle.distance_to_b_rectangle (max distance: 30 units). Each arrow can only be assigned to one note (tracked via
used_arrows). Returns a flat list of _IdentifiedArrow, each grouping
a composite arrow entity with its matched label note and IdentifierInfo.
Step 2.4 — _group_identified_arrows()#
Builds a graph (similar to build_balloon_graph in helpers/balloon/grouping.py)
where each _IdentifiedArrow is a node and edges connect arrows that satisfy
_belong_to_same_section_line. Four conditions are checked:
Same identifier (letter + optional number).
Same cross-reference (e.g., both
"2/5"or bothNone).Collinear directions (within
self.direction_tolerance).Tips on the same perpendicular line (
_are_tips_on_same_perpendicular_line): the vector tip_a→tip_b must be perpendicular to the arrow direction. This separates two indicators with the same identifier at different positions along the arrow axis.
Returns the connected components via nx.connected_components().
Step 2.5 — _build_indicators_from_groups()#
For each group of exactly 2 identified arrows, _build_indicator_from_group
creates a SectionLineIndicator. Cross-references are merged from both arrows
(info_i.cross_reference or info_j.cross_reference). Groups with more than 2
arrows are logged as warnings and skipped.
The resulting SectionLineIndicator has:
arrow_locations= tips of both arrowsarrow_direction= direction of the first arrowdetection_source="composites"source_entities=[composite_i, composite_j, label_entity_i, label_entity_j]
Configurable Parameters#
Detection parameters are grouped in SectionLineDetectionConfig (a frozen dataclass),
passed to ViewSectionLineDetector.__init__(view, config=...):
|
Default |
Purpose |
|---|---|---|
|
|
Arrow type string to filter relevant leaders |
|
|
Tolerance for |
|
|
Expected number of arrows per indicator |
|
|
Maximum bounding-rectangle distance between label and arrow |
|
(compiled regex) |
Pattern for indicator label matching (letter + optional number + optional xref) |
|
(compiled regex) |
Pattern for standalone cross-reference matching ( |
Additional __init__ parameters (not in the config):
Parameter |
Default |
Purpose |
|---|---|---|
|
8-color palette |
Colors for overlay visualization |
|
|
Pre-collected arrows from |
Visualization & Overlay#
Each SectionLineIndicator produces overlay primitives via
overlay_primitives(color):
Computes a bounding rectangle from arrow locations and source entities
Delegates to
build_overlay_primitives()(shared helper) to create:A colored highlight rectangle around the bounding box
Text labels showing identifier, cross-reference, detection_source, and direction
ViewSectionLineDetector.collect_overlay_primitives() cycles through a
palette of 8 colors to distinguish multiple indicators on the same view.
The overlay is integrated at every level of the hierarchy:
FeaturedDrawing.plot_data_section_lines()
└── for each FeaturedSheet:
FeaturedSheet.plot_data_section_lines()
└── FeaturedSheet.collect_section_line_overlay_primitives()
└── for each FeaturedView:
FeaturedView.collect_section_line_overlay_primitives()
└── ViewSectionLineDetector.collect_overlay_primitives()
└── for each SectionLineIndicator:
indicator.overlay_primitives(color)
Usage Example#
from drawing_tools import FeaturedDrawing
from drawing_tools.config.default_language_configs import DEFAULT_FRENCH_CONFIG
featured_drawing = FeaturedDrawing(
drawing=drawing,
language_configs=[DEFAULT_FRENCH_CONFIG],
)
# Access indicators on a specific view
for sheet in featured_drawing.sheets:
for view in sheet.views:
for indicator in view.section_line_indicators:
print(f"{indicator.identifier} → "
f"cross_ref={indicator.cross_reference}, "
f"detection_source={indicator.detection_source}")
# Visualize all section lines across the drawing
featured_drawing.plot_data_section_lines().plot()