COMPO · 2.08 · LOCAL FLOW GUIDE

Линия как
направляющая
взгляда.

Модуль не пытается «предсказать глаза человека». Он измеряет только локальный направляющий сигнал линии: насколько путь непрерывен, читаемо ли его направление, есть ли развороты и двусмысленность, и достаточно ли линия заметна, чтобы вообще участвовать в маршруте. Глобальный eye-flow остаётся отдельной задачей.

API-контракт

00 / consumes → provides

CONSUMES

Line.role Line.subrole? Line.rigidity Line.directed Line.sourceId? / targetId? Line.geometry Line.pathGeometry: pathLengthPx pathLengthShortN chordLengthPx spatialExtentShortN startPointN endPointN startTangent endTangent Compo2.06.Line.contextualWeight? Compo2.07.Line.pathLengthClass? Compo2.07.Line.spatialSpanClass? geometryOwner.sampleAtArcFraction(q) StylePreset.flow?

PROVIDES

Line.flowGuideDescriptor Line.flowHypotheses[] Line.flowPreferredDirection Line.flowDirectionStatus line.flow.tangentContinuity line.flow.netProgression line.flow.reversalPenalty line.flow.directionAmbiguity line.flow.visibilityGate line.flow.localCueStrength line.flow.localCueConfidence FlowCorrectionRequest[]

DOES NOT OWN

global.eyeFlowScore global.attentionOrder global.hierarchyScore global.balanceScore global.negativeSpaceScore semantic.sourceTargetFit object.connectionQuality readingDirectionPreference faceGazeDirection

Эти величины могут использовать локальный cue как вход, но не вычисляются здесь.

HARD ARCHITECTURE: Compo 2.08 не имеет права утверждать «взгляд пойдёт сюда». Он экспортирует локальный направляющий сигнал. Итоговый маршрут складывается из линий, текста, визуального веса, объектов, лиц, пустот и других глобальных факторов.

Направляющий сигнал ≠ eye-flow

01 / ownership boundary

Что измеряет 2.08

LOCAL: • есть ли однозначное направление? • насколько путь непрерывен? • насколько часто он резко разворачивается? • движется ли он вперёд или петляет? • видим ли stroke достаточно, чтобы быть cue? • какие направления допустимы: forward / reverse / unresolved?

Что решает не 2.08

GLOBAL: • куда человек посмотрит первым? • что станет второй остановкой? • победит ли линия заголовок? • важнее ли лицо / товар / текст? • должен ли маршрут идти L→R? • хорош ли маршрут всей композиции?
Практический смысл: сильный локальный cue может быть намеренно подавлен глобальной иерархией. И наоборот: слабая линия может поддерживать уже существующий маршрут, не становясь его причиной.

Сэмплирование пути

02 / geometry-independent analysis

Равные доли длины дуги

q_i = i / (N-1), i=0..N-1 S_i = sampleAtArcFraction(q_i) P_i = S_i.position T_i = normalize(S_i.tangent) Δθ_i = wrapSigned( angle(T_{i+1}) - angle(T_i) ) ∈ [-180°,180°]

DERIVED Сэмплирование по arc-length, а не по параметру Bézier t, не даёт длинным параметрическим участкам искусственно доминировать в статистике.

Один интерфейс для всех geometryType

SEGMENT: sample analytically CURVE: use arc-length lookup / adaptive flattening POLYLINE: locate segment by cumulative length OUTPUT: same {position,tangent}

2.08 не должен знать, как внутри устроен path. Он просит владельца геометрии вернуть точку и касательную по доле пройденного пути.

IMPLEMENTATION CONTRACT: если geometry-owner пока не даёт sampleAtArcFraction(q), адаптер обязан строиться рядом с 2.01 / 2.04 / 2.05. 2.08 не копирует их геометрию и не создаёт вторую независимую длину пути.

Forward, reverse, unresolved

03 / direction hypotheses
directed
if Line.directed == true: hypotheses = [FORWARD] preferred = FORWARD status = LOCKED

Source→target нельзя переворачивать локально, если направление семантически закреплено.

undirected
if Line.directed == false: hypotheses = [ FORWARD, REVERSE ] preferred = UNRESOLVED

Обычная линия сама по себе не доказывает, что движение идёт слева направо.

?
context resolved
global solver may choose: FORWARD or REVERSE using: semantics reading direction hierarchy object order face gaze

Выбор глобального направления не меняет локальную геометрию пути.

Нельзя прошивать культурное правило «слева направо» внутрь 2.08. Направление чтения — внешний контекст и может отличаться по языку, носителю и конкретной композиции.

Локальные метрики cue

04 / measurable features
C
tangent continuity
c_i = (1 + cos(|Δθ_i|)) / 2 tangentContinuity = mean(c_i)

1 — плавное продолжение; ближе к 0 — частые сильные развороты.

P
net progression
netProgression = clamp( chordLengthPx / pathLengthPx, 0, 1 )

1 — путь почти прямой; ниже — больше обходов/изгибов. Это descriptor, не автоматический штраф.

R
reversal penalty
r_i = smoothstep( reversalStartDeg, 180°, |Δθ_i| ) reversalPenalty = mean(r_i)

HEURISTIC Большой разворот может ослаблять локальную однозначность, но не запрещается универсально.

A
direction ambiguity
directed line: ambiguity = 0 undirected line: ambiguity = 1 until context resolves

Это не «плохая» метрика, а сигнал о том, что существуют две равноправные гипотезы направления.

Visibility gate

if contextualWeight available: visibilityGate = saturateWeight( Compo2.06.contextualWeight ) else: visibilityGate = UNKNOWN confidence ↓

Линия может иметь идеальную геометрию направления, но при почти нулевом контрасте она слабый визуальный cue. Сам вес не пересчитывается — он принадлежит 2.06.

Local cue strength

geometryCue = wC * tangentContinuity + wP * progressionFit(role, netProgression) - wR * reversalPenalty if visibilityGate known: localCueStrength = geometryCue * visibilityGate else: localCueStrength = geometryCue confidence *= missingVisibilityPenalty

Вес w* и progressionFit — STYLE_PRESET / HEURISTIC. Straight connector и декоративная S-кривая не обязаны иметь одинаковый target.

Не превращать descriptor в закон

05 / anti-overfitting
ПризнакВысокое значение значитНЕ значит
tangentContinuityлокально плавные изменения касательной«композиция хорошая»
netProgressionмалый detour относительно chord«прямая всегда лучше кривой»
reversalPenaltyесть сильные локальные развороты«повороты запрещены»
visibilityGateлиния достаточно заметна как cue«она должна стать доминантой»
directionAmbiguityлокально не выбран знак пути«линия бесполезна»

Role → applicability

06 / cue relevance
RoleApplicability 2.08Что анализироватьЧто оставить другим
trajectory1.00направление, continuity, reversals, visibilityфинальный eye-flow
connector0.90локальная читаемость маршрутасвязь объектов — 2.09
annotation0.85однозначность локального ходаattachment object↔label — 2.09
accent0.55наличие directional cueнужен ли этот cue вообще — global solver
axis0.45геометрическое направлениеalignment/grid — 2.13
divider0.30побочный directional cueразделение областей — 2.14
rhythm0.25отдельная линия только частичносерийный flow — 2.17
texture0.10обычно только diagnosticполе/плотность texture
baseline0.00–0.20если линия видиматипографический flow
INITIAL HEURISTIC Applicability — не «важность роли». Это лишь коэффициент того, насколько данный модуль уместен для данного типа линии.

Candidate generation

07 / requests, not foreign mutations

SMOOTH

request: reduce sharp Δθ owner: 2.04 curve 2.05 polyline

2.08 не двигает control points сам.

SIMPLIFY

request: remove unnecessary reversal / micro-bend

Только если роль допускает упрощение.

REVERSE

allowed only if: directed == false semantics unlocked

Reverse меняет гипотезу направления, а не геометрию.

BOOST / REDUCE CUE

request to owners: width → 2.06 contrast → 2.16 span → 2.07

2.08 может попросить усилить cue, но не присваивает чужие параметры.

FlowCorrectionRequest { lineId, reasonCode: EXCESSIVE_REVERSAL | LOW_TANGENT_CONTINUITY | CUE_TOO_WEAK_FOR_ROLE | CUE_TOO_STRONG_FOR_ROLE | DIRECTION_UNRESOLVED, targetOwner: "Compo2.04" | "Compo2.05" | "Compo2.06" | "Compo2.07" | "Compo2.16" | "GLOBAL_SOLVER", requestedChange, bounds?, costEstimate?, provenance }

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

08 / same module, different cues
directed / straight
Высокая continuity + progression. Для trajectory/connector это сильный локальный cue.
smooth curve
Net progression ниже прямой, но касательная меняется плавно. Кривая не должна автоматически проигрывать segment.
reversal-heavy polyline
Высокая reversal load. Не reject автоматически; для connector это подозрительно, для expressive accent может быть допустимо.
undirected geometry → 2 hypotheses
FORWARD и REVERSE равноправны. Пока внешний контекст не выбрал направление, preferredDirection=UNRESOLVED.
good geometry / weak visibility gate
Геометрический cue может быть высоким, но localCueStrength снижается, если 2.06 сообщает очень низкую заметность.

Metric ownership и scoring

09 / composite-first

OWNS

line.flow.tangentContinuity line.flow.netProgression line.flow.reversalPenalty line.flow.directionAmbiguity line.flow.visibilityGate line.flow.localCueStrength line.flow.localCueConfidence PRIMARY VOTE: line.flow.localCueStrength COMPONENTS: diagnostic by default

При будущем Compo 2.20 scorer должен голосовать composite-first, чтобы continuity/progression/reversal не считались повторно вместе с localCueStrength.

DEFERRED / FOREIGN

global.eyeFlowScore global.attentionOrder global.hierarchyScore global.balanceScore global.negativeSpaceScore 2.09.semanticConnectionFit 2.10.intersectionConflict 2.13.gridAlignment 2.16.lineColorContrast

Изменение этих величин не должно переписывать собственные геометрические descriptor 2.08.

MetricResult example: { key: "line.flow.localCueStrength", owner: "Compo 2.08", value: 0..1, applicability: roleApplicability, confidence: localCueConfidence, status: "ok|warn|na|partial", source: "HEURISTIC", composite: true, evidence: { tangentContinuity, netProgression, reversalPenalty, visibilityGate }, repairHints: [...] }

Pass order

10 / dependency-safe pipeline
INPUT LineCandidate 1. require canonical LinePathGeometry 2. request geometryOwner.sampleAtArcFraction(q) 3. build equal-arc-length samples 4. compute local tangent deltas 5. build FORWARD / REVERSE hypotheses 6. compute: tangentContinuity netProgression reversalPenalty directionAmbiguity 7. consume Compo 2.06 contextualWeight if available 8. compute visibilityGate or reduce confidence 9. apply role applicability 10. compute localCueStrength 11. generate FlowCorrectionRequest[] 12. DO NOT compute global eye-flow 13. provide descriptor to global solver / future eye-flow owner RECOMPUTE WHEN: • LinePathGeometry changes • directed/source/target semantics change • contextualWeight changes • role/subrole changes • style flow preset changes

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

11 / regression

TEST 01 · straight

segment: P0=(0.1,0.7) P1=(0.9,0.3) directed=true expected: tangentContinuity = 1 netProgression = 1 reversalPenalty = 0 hypotheses = [FORWARD]

TEST 02 · undirected

same geometry directed=false expected: hypotheses = [FORWARD, REVERSE] preferredDirection = UNRESOLVED must NOT assume L→R

TEST 03 · curve

smooth curve: pathLength = 500 chordLength = 380 expected: netProgression = 0.76 tangentContinuity high must NOT fail only because progression < 1

TEST 04 · reversals

polyline turn deltas: [15°, 142°, 18°, 151°] expected: reversalPenalty > 0 tangentContinuity < smooth equivalent no hard reject for role=accent

TEST 05 · visibility

same geometryCue = 0.90 A visibilityGate=0.90 B visibilityGate=0.20 expected: cueStrength(A) > cueStrength(B) geometry metrics unchanged

TEST 06 · missing weight

contextualWeight = N/A expected: geometryCue computed localCueStrength partial confidence reduced must NOT invent contrast/weight

TEST 07 · invariance

A = straight segment B = collinear polyline with 5 vertices same visible path expected: flow descriptors approximately equal within tolerance

TEST 08 · no global leak

change only: global.balanceScore global.hierarchyScore global.negativeSpaceScore expected: 2.08 owned metrics UNCHANGED

TEST 09 · semantic lock

directed=true sourceId=A targetId=B expected: REVERSE candidate forbidden locally global may reroute but not invert semantics

LIVE REFERENCE TESTS

IDInputComputedStatus

Таблица ниже вычисляется встроенным JavaScript при открытии документа и служит минимальным executable smoke-test для формул 2.08.

DATA FOR LAYOUT ENGINE

12 / machine contract
{ "module": "Compo 2.08", "name": "line_as_local_flow_guide", "version": "1.0", "scope": "LOCAL_DIRECTIONAL_CUE_ONLY", "consumes": [ "Compo0.Canvas", "Compo2.00.Line.role", "Compo2.00.Line.subrole?", "Compo2.00.Line.rigidity", "Compo2.00.Line.directed", "Compo2.00.Line.sourceId?", "Compo2.00.Line.targetId?", "Compo2.00.Line.geometry", "Compo2.00.LinePathGeometry.pathLengthPx", "Compo2.00.LinePathGeometry.chordLengthPx", "Compo2.00.LinePathGeometry.spatialExtentShortN", "Compo2.00.LinePathGeometry.startTangent", "Compo2.00.LinePathGeometry.endTangent", "GeometryOwner.sampleAtArcFraction(q)", "Compo2.06.Line.contextualWeight?", "Compo2.07.Line.pathLengthClass?", "Compo2.07.Line.spatialSpanClass?", "StylePreset.flow?" ], "provides": [ "Line.flowGuideDescriptor", "Line.flowHypotheses[]", "Line.flowPreferredDirection", "Line.flowDirectionStatus", "FlowCorrectionRequest[]" ], "owns_metrics": [ "line.flow.tangentContinuity", "line.flow.netProgression", "line.flow.reversalPenalty", "line.flow.directionAmbiguity", "line.flow.visibilityGate", "line.flow.localCueStrength", "line.flow.localCueConfidence" ], "metric_policy": { "primary_vote": "line.flow.localCueStrength", "components": "diagnostic_only_by_default", "global_eye_flow": "FORBIDDEN_TO_OWN" }, "flow_hypothesis_policy": { "directed_true": ["FORWARD"], "directed_false": ["FORWARD","REVERSE"], "undirected_preferred_direction": "UNRESOLVED" }, "initial_heuristics": { "sample_count": { "value": 24, "source": "IMPLEMENTATION_HEURISTIC" }, "reversal_start_deg": { "value": 100, "source": "INITIAL_HEURISTIC" }, "missing_visibility_confidence_factor": { "value": 0.72, "source": "INITIAL_HEURISTIC" }, "role_applicability": { "trajectory": 1.00, "connector": 0.90, "annotation": 0.85, "accent": 0.55, "axis": 0.45, "divider": 0.30, "rhythm": 0.25, "texture": 0.10, "baseline": 0.10 } }, "hard_rules": [ "must_not_compute_global_eye_flow", "must_not_assume_left_to_right_for_undirected_line", "directed_semantic_line_must_preserve_source_target_order", "must_not_recompute_path_geometry_owned_by_2.01_2.04_2.05", "foreign_parameter_changes_must_be_emitted_as_correction_requests", "missing_context_must_reduce_confidence_not_be_invented" ], "deferred_inputs": [ "global.eyeFlowScore", "global.attentionOrder", "global.hierarchyScore", "global.balanceScore", "global.negativeSpaceScore", "Compo2.09.semanticConnectionFit", "Compo2.10.intersectionConflict", "Compo2.13.gridAlignment", "Compo2.16.lineColorContrast", "context.readingDirection", "context.faceGazeDirection" ], "recompute_when": [ "LinePathGeometry changes", "Line.directed changes", "source/target semantics change", "Compo2.06.contextualWeight changes", "Line.role or subrole changes", "StylePreset.flow changes" ], "finality": "LOCAL_CUE_PROPOSAL_ONLY" }
ARCHITECTURE CHECKPOINT: 2.08 продолжает мигрированную схему 2.00–2.07: единый LinePathGeometry, role/subrole/rigidity, один владелец каждой метрики, отдельные локальные и глобальные уровни, никаких скрытых предположений о направлении чтения.