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° | мягкая ломаная | обычный bend | HEURISTIC |
| 35–100° | явный поворот | сильная структурная вершина | HEURISTIC |
| 100–160° | резкий возврат | повышенный conflict check | HEURISTIC |
| 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)
Слишком много изломов на коротком пути = высокая сложность.
| Role | Self-intersection | Bend density | Типичная политика |
| connector | обычно запрещено | низкая | кратчайший читаемый маршрут |
| trajectory | нежелательно | низкая/средняя | не создавать ложных развилок |
| annotation | запрещено по умолчанию | низкая | 1–2 поворота |
| accent | может быть | средняя | STYLE_PRESET |
| texture | допустимо | может быть высокой | низкий контраст, background priority |
Визуальная лаборатория
06 / good vs bad
Локальный 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 Compo | vertices / 90° bends | может ортогонализировать техническую ломаную |
| Connector module / Compo 2.09 | route topology | семантические anchors важнее локальной красоты |
| Intersection module / Compo 2.10+ | reroute | обход других объектов и линий |
| Negative space | число bend / span | может упростить путь, если он дробит пустоту |
| Eye-flow | порядок/направление сегментов | глобальный маршрут чтения имеет более высокий приоритет |
| Style preset | angle families / CV / dash | technical, 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.