COMPO · 2.07 · LINE LENGTH / ROLE FIT

Длина линии
и её роль
в композиции.

Compo 2.01 измеряет длину. Compo 2.07 решает другое: соответствует ли эта длина назначению линии, нужно ли линию укоротить, удлинить или оставить без изменения, и какие варианты длины передать глобальному solver.

API-контракт

00 / consumes → provides

CONSUMES

Line.role Line.subrole? Line.rigidity? Line.pathGeometry.pathLengthPx Line.pathGeometry.pathLengthDiagN Line.pathGeometry.pathLengthShortN Line.pathGeometry.spatialExtentShortN Line.pathGeometry.boundsN Line.pathGeometry.startPointN / endPointN Line.sourceId? / targetId? Canvas.widthPx / heightPx StylePreset? OPTIONAL CONTEXT: anchorDistancePx regionSpanPx freeCorridorSpanPx ownerBounds

PROVIDES

Line.pathLengthClass Line.spatialSpanClass Line.roleLengthFit Line.targetInterval Line.coverageRatio? Line.lengthAction Line.lengthCandidates[] Line.lengthPenalty Line.lengthDescriptor Line.targetInterval = {metric, min, max, feather}

DOES NOT OWN

path geometry // Compo 2.01 / 2.04 / 2.05 lineVisualWeight // Compo 2.06 eyeFlowScore // DEFERRED balanceScore // DEFERRED negativeSpace // DEFERRED semanticPriority // upstream semantic layer

Длина пути ≠ пространственный охват

01 / two independent descriptors

Path length

pathLengthPx pathLengthShortN pathLengthDiagN

Сколько реального пути проходит stroke. Для curve/polyline может быть значительно больше прямого chord.

Spatial span

spatialExtentShortN = max(boundsWidthPx,boundsHeightPx) / min(W,H)

Какую долю поля path реально охватывает. Не растёт только потому, что линия делает петли внутри маленькой области.

ANTI-DUPLICATION: 2.07 — канонический владелец pathLengthClass, spatialSpanClass и roleLengthFit. Старое поле spanClass удалено как неоднозначное.

Канонические нормализации

02 / explicit semantics
PL
pathLengthShortN
PLS = pathLengthPx / min(W,H)

Длина stroke/path.

PD
pathLengthDiagN
PLD = pathLengthPx / diagonalPx

Межформатное сравнение длины пути.

SS
spatialExtentShortN
SS = max(boundsW,boundsH)/min(W,H)

Охват поля.

C
coverageRatio
coverage = coveredSpanPx / referenceSpanPx

Функциональный охват конкретного owner/region.

Никакого универсального Lnorm. В каждом API явно указано, измеряем мы длину пути, spatial extent или coverage относительно владельца.

Два набора классов

03 / path vs field

pathLengthClass

S : PLS < 0.15 M : 0.15 ≤ PLS < 0.40 L : 0.40 ≤ PLS < 0.75 XL : PLS ≥ 0.75

Используется для detour, ink amount, траекторий и curve/polyline.

spatialSpanClass

S : SS < 0.15 M : 0.15 ≤ SS < 0.40 L : 0.40 ≤ SS < 0.75 XL : SS ≥ 0.75

Используется для структурного охвата поля, axis/divider/accent.

INITIAL HEURISTIC Пороги пока одинаковы только для простоты калибровки. Это две разные переменные и позже могут иметь разные профили.

Роль определяет смысл длины

04 / role profiles
RoleЧто считается правильной длинойКак рассчитывать targetСтатус
accentлокальная, явно короче структурных линийдиапазон spatialSpanClass из StylePreset / стартовый профиль S–MSTYLE
axisпокрывает объект/группу и имеет контролируемый overhangtarget = ownerSpan + 2×overhangDERIVED + STYLE
dividerреально разделяет выбранную областьcoverageRatio = length / regionSpanCONTEXT
baselineсвязана с шириной/границей текстового владельцаполучает target от typography moduleDEFERRED
connectorдостаточна, чтобы соединить anchors; лишняя длина = detourdetourRatio = pathLength / directAnchorDistanceDERIVED
annotationсоединяет объект и label без бессмысленного хвостаanchor → text attachment distanceCONTEXT
trajectorypathLength подчинён маршруту; spatialSpan описывает охват поляpath coverage + eye-flow позжеDEFERRED
rhythmважна не одна длина, а распределение длин в сериипередать в Compo 2.17DEFERRED
textureконтролируется плотностью и повторомStylePreset + texture systemSTYLE

Role-length fit

05 / local scoring

Для интервального target

function intervalFit(L, a, b, feather): if a <= L <= b: return 1 if L < a: return clamp(1 - (a-L)/feather, 0, 1) if L > b: return clamp(1 - (L-b)/feather, 0, 1) roleLengthFit = intervalFit( Line.pathGeometry.pathLengthShortN, target.min, target.max, target.feather )

Плато внутри допустимого диапазона не заставляет solver искать одну «магическую» длину.

Для reference-span target

coverage = lengthPx / referenceSpanPx fit = 1 - clamp( abs(coverage - targetCoverage) / tolerance, 0, 1 )

Так оцениваются axis/divider, если известна конкретная область работы.

Не использовать один target для всех ролей. Короткий connector между близкими объектами может быть идеален, а короткий divider того же размера — функционально провален.

Укоротить или удлинить

06 / corrective proposals

KEEP

if roleLengthFit >= keepThreshold: action = KEEP

HEURISTIC стартовый keepThreshold=0.82.

SHORTEN

if L > target.max: generate candidates by moving free endpoint toward fixed anchor OR shrinking symmetrically

Способ зависит от роли и закреплённости endpoints.

EXTEND

if L < target.min: extend along unitVector until target interval while respecting protected zones

После изменения endpoints Compo 2.01 обязан пересчитать геометрию.

ENDPOINT POLICY if sourceLocked && targetLocked: length is SEMANTICALLY_FIXED do not resize; only report mismatch if sourceLocked && !targetLocked: move P1 along permitted direction if !sourceLocked && targetLocked: move P0 if !sourceLocked && !targetLocked: generate variants: A) preserve midpoint B) preserve P0 C) preserve P1 All variants → HARD FILTER → local fit → global solver

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

07 / exact examples
ownerSpan = 300 axis = ownerSpan + overhang
AXIS: длина выводится из bounds владельца плюс overhang, а не выбирается из абстрактной категории «long».
input coverage = 0.64 candidate coverage = 1.00
DIVIDER: если линия должна разделять весь активный corridor, слишком короткий вариант получает низкий coverage fit.
direct detour
CONNECTOR: прямой anchor distance — нижняя геометрическая граница. Лишняя длина должна быть оправдана routing constraints, иначе возникает detour penalty.
too structural accent candidate
ACCENT: при одинаковой толщине чрезмерно длинный штрих начинает работать как структурная масса и меняет роль.

Candidate generation

08 / deterministic + bounded random
function proposeLengths(line, target, locks, rng): measured = line.pathGeometry.pathLengthShortN if locks.p0 && locks.p1: return [{ action:"REPORT_ONLY", length:measured }] anchors = [ target.min, target.mid, target.max ] for each Ltarget in anchors: candidate = resizeAccordingToEndpointPolicy(line, Ltarget) candidate += boundedStyleJitter(rng, StylePreset.lengthJitter) recomputeGeometry(candidate) // Compo 2.01 hardFilter(candidate) scoreRoleLengthFit(candidate) return candidates
Random — только после target generation. Он может добавить небольшую вариативность внутри допустимого интервала, но не имеет права превращать semantic connector или baseline в произвольный штрих.

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

09 / regression cases

TEST 01 · axis

ownerSpanPx = 400 overhangPx = 40 each side expected targetPx = 480 input lengthPx = 479 expected action = KEEP

TEST 02 · divider

regionSpanPx = 600 input lengthPx = 360 coverage = 0.60 targetCoverage = 0.95..1.00 expected action = EXTEND

TEST 03 · connector

directAnchorDistance = 250 pathLength = 400 detourRatio = 1.60 expected = penalty unless routing obstacle explains detour

TEST 04 · locked

p0.locked = true p1.locked = true roleLengthFit = 0.22 expected action = REPORT_ONLY must not mutate endpoints

TEST 05 · normalization

W=1080 H=1920 lengthPx=540 pathLengthShortN=0.50 pathLengthDiagN≈0.2449 spatialExtentShortN=0.50 expected pathLengthClass = L expected spatialSpanClass = L

TEST 06 · recompute

resize path geometry → geometry owner recompute → new LinePathGeometry → rerun Compo 2.07 no stale geometry allowed

TEST 07 · curve split

geometryType = curve pathLengthShortN = 0.90 spatialExtentShortN = 0.30 expected: pathLengthClass = XL spatialSpanClass = M must NOT collapse to one spanClass

DATA FOR LAYOUT ENGINE

10 / machine contract
{ "module": "Compo 2.07", "name": "line_length_role_fit", "version": "2.0", "owns_metrics": [ "line.pathLengthClass", "line.spatialSpanClass", "line.roleLengthFit", "line.coverageRatio", "line.lengthPenalty" ], "consumes": [ "Compo0.Canvas", "Compo2.00.Line.role", "Compo2.00.Line.subrole?", "Compo2.00.Line.rigidity?", "Compo2.00.LinePathGeometry.pathLengthPx", "Compo2.00.LinePathGeometry.pathLengthDiagN", "Compo2.00.LinePathGeometry.pathLengthShortN", "Compo2.00.LinePathGeometry.spatialExtentShortN", "Compo2.00.LinePathGeometry.boundsN", "Line.endpointLocks?", "context.referenceSpanPx?", "context.anchorDistancePx?", "StylePreset.lineLength?" ], "provides": [ "Line.pathLengthClass", "Line.spatialSpanClass", "Line.targetInterval", "Line.roleLengthFit", "Line.coverageRatio?", "Line.lengthAction", "Line.lengthCandidates[]", "Line.lengthDescriptor" ], "target_interval_schema": { "metric": "pathLengthShortN | spatialExtentShortN | coverageRatio", "min": "number", "max": "number", "feather": "number" }, "canonical_normalization": { "pathLengthClass": "LinePathGeometry.pathLengthShortN", "spatialSpanClass": "LinePathGeometry.spatialExtentShortN", "globalPathComparison": "LinePathGeometry.pathLengthDiagN", "roleCoverage": "coveredSpanPx/referenceSpanPx" }, "path_length_classes": { "S": {"range":[0.00,0.15], "source":"INITIAL_HEURISTIC"}, "M": {"range":[0.15,0.40], "source":"INITIAL_HEURISTIC"}, "L": {"range":[0.40,0.75], "source":"INITIAL_HEURISTIC"}, "XL": {"range":[0.75,null], "source":"INITIAL_HEURISTIC"} }, "spatial_span_classes": { "S": {"range":[0.00,0.15], "source":"INITIAL_HEURISTIC"}, "M": {"range":[0.15,0.40], "source":"INITIAL_HEURISTIC"}, "L": {"range":[0.40,0.75], "source":"INITIAL_HEURISTIC"}, "XL": {"range":[0.75,null], "source":"INITIAL_HEURISTIC"} }, "actions": [ "KEEP", "SHORTEN", "EXTEND", "REPORT_ONLY", "REGENERATE" ], "hard_rules": [ "must_not_recompute_path_geometry_independently_of_geometry_owner", "locked_endpoints_must_not_move", "semantic_connector_endpoints_must_be_preserved", "all_resize_or_reroute_candidates_must_be_recomputed_by_2.01_or_2.04_or_2.05_according_to_geometryType" ], "deferred_inputs": [ "global.eyeFlowScore", "global.balanceScore", "global.negativeSpaceScore", "typography.baselineTarget", "routing.obstacleJustification" ], "migration_note": "Compo 2.07 is canonical owner of pathLengthClass, spatialSpanClass and roleLengthFit. Old ambiguous spanClass is removed; Compo 2.02 consumes spatialSpanClass instead of owning length classes.", "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.