COMPO · 2.02 · AXIS-ALIGNED LINES

Горизонталь.
Вертикаль.

Модуль фиксирует машинные правила для линий, выровненных по осям поля: как классифицировать почти горизонтальную или почти вертикальную линию, когда притягивать её к сетке, как выбирать длину, отступы и роль без «дизайнерской интуиции».

Контракт модуля

01 · machine interface

CONSUMES

Line.role Line.subrole? Line.rigidity Line.p0, Line.p1 Compo2.01.Line.angle180 Compo2.01.Line.boundsN Canvas.widthPx, Canvas.heightPx Grid? = optional ProtectedZones[] StylePreset?

PROVIDES

axisClass: H | V | NONE axisErrorDeg snap_candidate snap_distance_norm edge_relation placement_candidates[]

DEFERRED INPUTS

global_balance_score negative_space_score hierarchy_score reading_path_score typography_collision_score

Эти метрики принадлежат другим модулям и здесь не вычисляются.

Модуль не утверждает, что горизонталь «спокойная», а вертикаль «сильная». Такие слова слишком абстрактны для машины. Он работает только с измеримыми последствиями: привязкой к осям, отношением к краям поля, длиной, повтором и тем, как линия делит пространство.

Классификация

02 · derived geometry

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

a = Compo2.01.Line.angle180 errH = min(a, 180-a) errV = abs(a-90) axisErrorDeg = min(errH, errV) axisClass = H, if errH ≤ tau_axis V, if errV ≤ tau_axis NONE otherwise

HEURISTIC Начальный tau_axis = 3°. Для ручного стиля допускается 5–7°. Для строгой сеточной системы — 1–2°.

Не путать классификацию и snap

Линия может быть классифицирована как H при ошибке 2.4°, но ещё не обязана быть выровнена в 0°. Snap — отдельное решение.

classification != mutation measure first propose correction second apply only after constraints
horizontal target

или 180° после нормализации направления.

90°
vertical target

строгое направление по Y.

default tolerance

HEURISTIC

0
final error after hard snap

для role=axis, subrole=grid, rigidity=hard.

Snap к оси

03 · correction proposal

Hard snap

Применяется, если сама роль требует строгости.

if (role==axis && subrole==grid && rigidity==hard) || (role==baseline && rigidity==hard) || (role==divider && rigidity==hard): if axisClass == H: y0 = y1 = mean(y0,y1) if axisClass == V: x0 = x1 = mean(x0,x1) axisErrorDeg = 0

Это геометрическая операция. После неё снова вычисляется длина через Compo 2.01.

Soft snap

Для декоративной или связующей линии алгоритм только предлагает коррекцию.

snap_strength = 1 - clamp(axisErrorDeg/tau_snap, 0, 1) new_coord = lerp(original, axis_target, snap_strength * k) k = stylePreset.axisSnapStrength 0 ≤ k ≤ 1

STYLE_PRESET k не универсален.

Нельзя автоматически выпрямлять каждую почти горизонтальную линию. Это уничтожит намеренную нерегулярность. Snap обязан учитывать role, StylePreset и будущий global score.

Размещение в поле

04 · candidate generation

Горизонталь

candidate_y = { safeTop, 0.25H, 0.50H, 0.75H, safeBottom, gridRows[] }

Набор — не «правильные места», а стартовые кандидаты.

Вертикаль

candidate_x = { safeLeft, 0.25W, 0.50W, 0.75W, safeRight, gridColumns[] }

Финальный выбор зависит от занятости поля.

Свободное пространство

freeRatio(line) = free_length_on_axis / requested_span reject if freeRatio < minFreeRatio

HEURISTIC стартовый minFreeRatio = 0.85.

RolePreferred placementEdge relationNotes
dividerмежду двумя occupancy-зонамиможет доходить до safe boundsдолжен реально разделять, а не случайно пересекать объекты
baselineпо нижней/верхней границе текстового блокаобычно внутри safe areaподчинён типографике
axisчерез центр группы/объектаможет выходить за объектдлина определяется системой якорей
accent / decorative subroleв свободной зонеможет намеренно обрезаться краемтолько если это STYLE_PRESET

Span передан Compo 2.07

05 · ownership handoff

2.02 владеет

Line.axisClass Line.axisErrorDeg Line.snapCandidate Line.axisPlacementCandidates[]

2.02 только читает

Line.spatialSpanClass ← Compo 2.07 Line.roleLengthFit ← Compo 2.07 Line.coverageRatio ← Compo 2.07
ANTI-DUPLICATION: никаких локальных S/M/L/XL-классов длины в 2.02. Если placement зависит от охвата линии, модуль получает канонический spatialSpanClass из 2.07.

Математические примеры

06 · visual lab
input: +1.58° hard snap → 0°
Grid-axis role: ошибка меньше 3°, поэтому модуль предлагает snap. Если role=axis, subrole=grid, rigidity=hard, предложение становится hard constraint.
candidate V occupancy A occupancy B
Вертикальная линия выбирается между двумя occupancy-зонами. Позиция определяется не «красивой третью», а свободным коридором + deferred balance score.
GOOD: deliberate central axis. Объекты симметричны относительно неё.
axis error = 1.34°
BAD для strict role: почти горизонтально. Ошибка слишком мала, чтобы читаться как намеренный наклон.
angle = -13.2°
VALID: это уже диагональ, поэтому Compo 2.02 отдаёт axisClass=NONE и не исправляет её.

Локальный scoring

07 · metric ownership

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

axis_fit = 1 - clamp(axisErrorDeg / tau_axis, 0, 1) snap_cost = displacement_norm * rigidityFactor edge_fit = scoreEdgeRelation(role, bounds)

Только эти локальные метрики принадлежат модулю.

Не вычислять здесь

balance_score // DEFERRED hierarchy_score // DEFERRED negative_space_score // DEFERRED reading_path_score // DEFERRED grid_global_score // DEFERRED

Они лишь могут участвовать в финальном выборе кандидата позже.

localScore = w_axis * axis_fit + w_edge * edge_fit - w_snap * snap_cost // weights = STYLE_PRESET or engine defaults // localScore is NOT final composition score

Context interactions

08 · override graph
Другой модульМожет изменитьПравило
Grid / Compo 8–10 futureкоординату линииможет притянуть axis-line к реальной колонке/ряду
Negative space / futureпозицию и длинуможет укоротить линию, если она дробит спокойную пустоту
Hierarchy / futureтолщину/контраст через Compo 2.06+не позволяет служебной оси стать доминантой
Typography / futurebaseline и endpointsтекстовый блок имеет право диктовать положение линии
Image crop / futureedge relationлиния может стать швом/маской и намеренно доходить до bleed
PASS ORDER 1. Compo 2.01 measures angle + length 2. Compo 2.02 classifies H/V/NONE 3. generate axis candidates 4. apply HARD role constraints 5. propose soft snap 6. run local score 7. request DEFERRED context scores 8. global solver accepts / moves / shortens / rejects 9. recompute geometry via Compo 2.01 10. validate again

Data for layout engine

09 · implementation block
{ "module": "Compo 2.02", "name": "axis_aligned_lines", "metric_owner": [ "axis_fit", "snap_cost", "edge_fit" ], "consumes": [ "Compo0.Canvas", "Compo0.ProtectedZones", "Compo2.00.Line.role", "Compo2.01.Line.angle180", "Compo2.01.Line.boundsN", "Compo2.00.Line.subrole?", "Compo2.00.Line.rigidity" ], "provides": [ "Line.axisClass", "Line.axisErrorDeg", "Line.snap_candidate", "Line.axisPlacementCandidates" ], "defaults": { "tau_axis_deg": {"value": 3, "source": "INITIAL_HEURISTIC"}, "min_free_ratio": {"value": 0.85, "source": "INITIAL_HEURISTIC"}, "axis_snap_strength": {"value": 0.8, "source": "STYLE_PRESET"} }, "deferred_inputs": [ "global_balance_score", "negative_space_score", "hierarchy_score", "reading_path_score", "typography_collision_score", "Compo2.07.Line.spatialSpanClass", "Compo2.07.Line.roleLengthFit" ], "hard_rules": [ "axis/grid/hard must end with axisErrorDeg=0", "baseline must be collinear with its owning text baseline", "protected zones may not be crossed unless role explicitly allows it" ], "finality": "LOCAL_PROPOSAL_ONLY" }

Тесты

10 · deterministic cases

TEST 01

input angle = 1.2° role = axis subrole = grid rigidity = hard expected axisClass = H expected hard_snap = true expected final angle = 0°

TEST 02

input angle = 88.4° role = accent subrole = decorative rigidity = soft expected axisClass = V expected snap = proposal only expected finality = local

TEST 03

input angle = 12° tau_axis = 3° expected axisClass = NONE expected Compo2.02 mutation = none forward to Compo2.03
MIGRATION 2.00–2.07 / pre-2.08: canonical LinePathGeometry, role/subrole/rigidity, metric ownership cleanup, path-length vs spatial-span separation.