COMPO · 2.05 · POLYLINE / SEGMENT SYSTEM

Ломаные
и сегменты.

Модуль превращает ломаную из произвольного набора штрихов в вычислимую структуру: сегменты, длины, углы поворота, точки излома, повтор, вариативность, самопересечения и допустимая сложность. Цель — дать машине правила, по которым ломаная выглядит намеренной, а не случайно “поломанной”.

API-контракт

00 / consumes → provides

CONSUMES

Polyline.points[0..n] Line.role Line.directed Canvas ProtectedZones[] StylePreset? Compo2.01 segment geometry

PROVIDES

segmentCount segmentLength[] segmentRatio[] turnAngle[] turnSign[] bendDensity selfIntersectionCount polylineBounds segmentationDescriptor

DEFERRED INPUTS

eye_flow_score global_balance_score negative_space_score hierarchy_score semantic_connection_score

Модуль не владеет глобальной композицией.

Разделение ответственности: Compo 2.01 вычисляет геометрию каждого отдельного сегмента. Compo 2.05 владеет отношениями между сегментами и топологией всей ломаной.

Модель ломаной

01 / deterministic structure
P₀…Pₙ
vertices

Упорядоченные вершины. Для открытой ломаной требуется минимум 2 точки.

n−1
segments

Количество сегментов строго выводится из числа вершин.

Δθ
turn angle

Поворот между соседними сегментами.

ΣL
path length

Суммарная длина всех сегментов.

for i = 0 .. n-2: segment[i] = P[i] → P[i+1] geometry[i] = Compo2.01(segment[i]) segmentCount = n - 1 pathLengthPx = Σ geometry[i].lengthPx straightDistancePx = distance(P0, Pn) pathEfficiency = straightDistancePx / pathLengthPx
HARD: соседние вершины не могут совпадать. Если distance(P[i],P[i+1]) ≤ EPS, кандидат должен быть repair/reject до оценки.

Угол поворота

02 / bend geometry

Signed turn

u = unit(segment[i]) v = unit(segment[i+1]) cross = u.x*v.y - u.y*v.x dot = u.x*v.x + u.y*v.y turnAngle = atan2(cross, dot) turnDeg = turnAngle * 180/PI

Знак поворота фиксирован для канонической экранной системы +X=right, +Y=down.

SCREEN-SPACE TURN CONVENTION cross > 0 → CLOCKWISE / RIGHT TURN cross < 0 → COUNTER-CLOCKWISE / LEFT TURN cross = 0 → COLLINEAR

Absolute bend

bendDeg = abs(turnDeg) 0° = прямое продолжение 90° = прямой угол 180° = разворот назад

Для оценки «намеренности» важен абсолютный угол, а для маршрута — ещё и знак.

Диапазон bendМашинная трактовкаТипичное действиеСтатус
0–3°почти прямаяmerge segments, если излом не семантическийHEURISTIC
3–12°слабый изломдопускать только если стиль/роль это оправдываютHEURISTIC
12–35°мягкая ломанаяобычный bendHEURISTIC
35–100°явный поворотсильная структурная вершинаHEURISTIC
100–160°резкий возвратповышенный conflict checkHEURISTIC
160–180°почти разворотобычно repair/reject, кроме специальной ролиHEURISTIC

Соотношения сегментов

03 / proportion system

Нормализация

Lmean = mean(L[i]) segmentRatio[i] = L[i] / Lmean

Позволяет сравнивать ломаные разных размеров.

Variation

CV = stdev(L) / mean(L) CV ≈ 0 → равные сегменты CV ↑ → вариативность

DERIVED CV — измерение, а не эстетическая оценка.

Extreme ratio

ratioMax = max(L)/min(L)

Помогает ловить один микросегмент среди больших или один гигантский выброс.

Не навязывать равенство: одинаковые длины могут быть правильными для технической схемы, но скучными для декоративной траектории. Значение targetCV всегда зависит от role/subrole/style preset.
0–.08
regular

почти равный модульный ритм

.08–.25
controlled

умеренная вариативность

.25–.55
expressive

заметная разница длин

>.55
unstable

требуется проверка намеренности

INITIAL_HEURISTIC Диапазоны выше — стартовые классы candidate generation, не универсальный закон.

Polyline ≠ dashed line

04 / segmentation types

Геометрическая сегментация

Сегменты меняют направление. У каждой вершины есть геометрический смысл.

type = POLYLINE points = [P0,P1,...Pn] segments = explicit geometry turnAngle[] exists

Визуальная сегментация

Геометрия может быть прямой, но stroke разбит на dash/gap.

type = STROKE_PATTERN basePath = segment|curve|polyline dash = [dashPx, gapPx, ...] phase = offsetPx
Ownership: Compo 2.05 владеет топологией polyline и базовыми правилами сегментированного stroke. Детальный ритм повторяющихся линий и dash-паттернов позже может быть уточнён в Compo 2.17.

Polyline → LinePathGeometry

04b / canonical adapter
Line.pathGeometry = { geometryType: "polyline", pathLengthPx: Σ segment[i].lengthPx, pathLengthShortN: pathLengthPx / min(W,H), pathLengthDiagN: pathLengthPx / diagonalPx, chordLengthPx: distance(P0,Pn), boundsN: bounds(all vertices), spatialExtentX_N: boundsWidthPx / min(W,H), spatialExtentY_N: boundsHeightPx / min(W,H), spatialExtentShortN: max(boundsWidthPx,boundsHeightPx)/min(W,H), startPointN: P0, endPointN: Pn, startTangent: unit(segment[0]), endTangent: unit(segment[last]) }
Таким образом segment, curve и polyline становятся взаимозаменяемыми для модулей 2.06+.

Топология и конфликты

05 / self-intersection

Self-intersection

for each non-adjacent segment pair (i,j): if intersect(seg[i],seg[j]): selfIntersectionCount += 1

Пересечение не всегда запрещено, но обязано быть явно учтено.

Near-touch

d = segmentSegmentDistance(a,b) if d < clearanceThreshold: nearConflict += 1

Позволяет ловить «почти касания», которые выглядят случайно.

Bend density

bendDensity = significantBends / max(pathLengthShortN, EPS)

Слишком много изломов на коротком пути = высокая сложность.

RoleSelf-intersectionBend densityТипичная политика
connectorобычно запрещенонизкаякратчайший читаемый маршрут
trajectoryнежелательнонизкая/средняяне создавать ложных развилок
annotationзапрещено по умолчаниюнизкая1–2 поворота
accentможет бытьсредняяSTYLE_PRESET
textureдопустимоможет быть высокойнизкий контраст, background priority

Визуальная лаборатория

06 / good vs bad
3 segments / deliberate bends
GOOD: каждый поворот читается, длины различаются, но не образуют случайного микросегмента.
micro-segments / weak bends
BAD: слабые изломы + короткие куски. Алгоритм должен предложить merge/remove vertices.
self-intersection = 1+
RISK: самопересечение для connector/annotation даёт сильный штраф или reject.
orthogonal technical route
GOOD: для technical/grid style повороты 90° образуют ясный системный язык.
dash=24 / gap=12
Сегментированный stroke остаётся одной геометрической линией. Паттерн dash/gap — отдельный descriptor.
high bend density
Высокая bend density допустима как accent/texture, но для semantic connector обычно слишком шумна.

Локальный scoring

07 / metric ownership

Собственные метрики

segment_validity turn_intent_score segment_ratio_score complexity_fit self_intersection_penalty near_touch_penalty role_topology_fit segmentation_fit

Это локальная ответственность Compo 2.05.

Не считать здесь

eye_flow_score // DEFERRED global_balance_score // DEFERRED negative_space_score // DEFERRED hierarchy_score // DEFERRED global_rhythm_score // DEFERRED

Эти метрики приходят от владельцев на context/global pass.

localScore = w_valid * segment_validity + w_turn * turn_intent_score + w_ratio * segment_ratio_score + w_role * role_topology_fit + w_seg * segmentation_fit - w_self * self_intersection_penalty - w_near * near_touch_penalty - w_complex * complexity_excess // weights = STYLE_PRESET / engine defaults // localScore != final composition score

Алгоритм модуля

08 / local pass
function analyzePolyline(polyline, scene): assert points.length >= 2 // 1. SEGMENTS segments = buildSegments(points) for each s in segments: geometry[s] = Compo2.01.compute(s) if !geometry[s].valid: return REPAIR_OR_REJECT // 2. TURNS for i = 0 .. segmentCount-2: turn[i] = signedAngle(unit[i], unit[i+1]) bend[i] = abs(turn[i]) // 3. REDUNDANT VERTICES if bend[i] < mergeThresholdDeg AND vertex[i+1] is not semanticAnchor: propose REMOVE_VERTEX(i+1) // 4. PROPORTIONS compute segmentRatio[] compute lengthCV detect microSegments // 5. TOPOLOGY count selfIntersections count nearTouches compute bendDensity // 6. ROLE POLICY apply connector / annotation / trajectory / accent / texture rules // 7. LOCAL SCORE score candidates // 8. CORRECTIONS candidates += simplify() candidates += moveVertex() candidates += removeVertex() candidates += orthogonalize() if style allows candidates += respaceDash() if segmented stroke // 9. CONTEXT request DEFERRED_INPUT metrics provide candidates to global solver
Corrective pass priority: сначала удалить геометрический мусор (нулевые/микро-сегменты, слабые случайные bends), только потом менять стилистику маршрута. Иначе solver будет оптимизировать дефектную топологию.

Context interactions

09 / override graph
МодульМожет изменитьПричина
Grid / future Compovertices / 90° bendsможет ортогонализировать техническую ломаную
Connector module / Compo 2.09route topologyсемантические anchors важнее локальной красоты
Intersection module / Compo 2.10+rerouteобход других объектов и линий
Negative spaceчисло bend / spanможет упростить путь, если он дробит пустоту
Eye-flowпорядок/направление сегментовглобальный маршрут чтения имеет более высокий приоритет
Style presetangle families / CV / dashtechnical, editorial, brutalist и organic используют разные локальные правила

Детерминированные тесты

10 / expected behavior

TEST 01 — REDUNDANT

P0=(0.1,0.5) P1=(0.4,0.49) P2=(0.8,0.5) bend < 3° P1.semanticAnchor=false expected: propose REMOVE P1

TEST 02 — ORTHOGONAL

turns = [90°, -90°, 90°] role = connector style = technical selfIntersection=0 expected: role_topology_fit = high no simplify by angle

TEST 03 — SELF CROSS

role = annotation selfIntersectionCount = 1 expected: HARD or very high penalty propose reroute finality = local

TEST 04 — MICRO SEGMENT

length ratios = [1.0, 0.06, 0.92] role = trajectory expected: microSegment=true propose merge/remove

TEST 05 — DASH

basePath = straight strokePattern = [24,12] expected: segmentCount geometry = 1 dashCount != geometry segmentCount

TEST 06 — STYLE OVERRIDE

bends = [7°, 9°, 6°] style = hand_drawn expected: weak bends may remain because threshold is STYLE_PRESET

DATA FOR LAYOUT ENGINE

11 / machine contract
{ "module": "Compo 2.05", "name": "polyline_and_segmented_lines", "version": "1.0", "consumes": [ "Compo0.Canvas", "Compo0.ProtectedZones", "Compo2.00.Line.role", "Compo2.01.SegmentGeometry", "Polyline.points", "Line.directed", "StylePreset?" ], "owner_of": [ "polyline.segmentCount", "polyline.pathLengthPx", "polyline.pathEfficiency", "polyline.turnAngleDeg[]", "polyline.turnSign[]", "polyline.segmentRatio[]", "polyline.lengthCV", "polyline.bendDensity", "polyline.selfIntersectionCount", "polyline.nearTouchCount", "polyline.segmentationDescriptor", "line.pathGeometry(polyline-adapter)" ], "provides": [ "PolylineDescriptor", "PolylineRepairProposal[]", "PolylineCandidate[]", "SegmentedStrokeDescriptor", "Line.pathGeometry" ], "initial_heuristics": { "merge_bend_threshold_deg": 3, "weak_bend_upper_deg": 12, "near_reverse_deg": 160, "regular_length_cv_max": 0.08, "controlled_length_cv_max": 0.25, "expressive_length_cv_max": 0.55, "micro_segment_ratio": 0.10 }, "deferred_inputs": [ "global.eye_flow_score", "global.balance_score", "global.negative_space_score", "global.hierarchy_score", "global.rhythm_score" ], "turn_sign_convention": "cross>0 CLOCKWISE_RIGHT; cross<0 COUNTER_CLOCKWISE_LEFT in +Y down screen space", "hard_rules": [ "adjacent vertices must not coincide", "semantic anchors cannot be removed by simplification", "annotation self-intersection is forbidden by default", "connector protected-zone policy must be respected" ], "corrective_actions": [ "remove_redundant_vertex", "merge_micro_segment", "move_vertex", "orthogonalize_if_allowed", "reroute", "simplify", "respace_dash_pattern" ], "finality": "LOCAL_PROPOSAL_ONLY" }
MIGRATION 2.00–2.07 / pre-2.08: canonical LinePathGeometry, role/subrole/rigidity, metric ownership cleanup, path-length vs spatial-span separation.