COMPO · 1.03 · POINT-PAIR RELATION

Две точки.
Одно отношение.

Исправленная версия pair-модуля. Он владеет только геометрией и локальными отношениями пары: расстоянием, углом, отношением размеров, поверхностным зазором и соответствием заранее заданному pair-profile. Баланс, иерархия всей сцены, сетка, eye-flow и конфликты с посторонними объектами — внешние метрики.

API-контракт

00 / consumes → provides

CONSUMES

Compo0.Canvas Compo0.Constraint[] Compo1.00.Point.role Compo1.01.Point.radiusShortN Compo1.01.Point.visualWeightBase Compo1.02.PointPlacementCandidate? PointA.positionN PointB.positionN PairProfile?

PROVIDES

PointPairGeometry pair.centerDistanceShortN pair.angle360Deg pair.angle180Deg pair.radiusRatioBA pair.surfaceGapShortN pair.surfaceGapMinRadiusN pair.overlapDepthShortN pair.localMetrics PointPairCandidate[]

DEFERRED INPUTS

point.conflict_score ← 1.09 point.local_contrast ← 1.10 global.negative_space_score ← future global.balance_score ← future global.hierarchy_score ← future global.eye_flow_score ← future grid.fit_score ← 1.06 / future grid text/image interactions ← future
Исправление: Compo 1.03 больше не считает balance_contribution, global hierarchy, grid_alignment, style_fit или collision с внешними объектами частью собственного score. Он экспортирует пару, а владельцы этих метрик оценивают её позже.

Геометрия пары

01 / deterministic
d
center distance

Физическое расстояние между центрами.

θ
pair direction

Вектор A→B в экранной системе координат.

ρ
radius ratio

rB/rA; направленное отношение размеров.

g
surface gap

Зазор между поверхностями; отрицательный при overlap.

shortSide = min(W,H) Ax = A.xN * W Ay = A.yN * H Bx = B.xN * W By = B.yN * H rApx = A.radiusShortN * shortSide rBpx = B.radiusShortN * shortSide dx = Bx - Ax dy = By - Ay dPx = sqrt(dx² + dy²) centerDistanceShortN = dPx / shortSide angle360Deg = ((atan2(dy,dx) * 180/π) + 360) % 360 angle180Deg = angle360Deg % 180 radiusRatioBA = rBpx / rApx surfaceGapPx = dPx - rApx - rBpx surfaceGapShortN = surfaceGapPx / shortSide surfaceGapMinRadiusN = surfaceGapPx / min(rApx,rBpx) overlapDepthShortN = max(0, -surfaceGapPx) / shortSide
DERIVED: эти величины не имеют эстетической трактовки. Они строго следуют из координат и размеров. После любого перемещения A/B или изменения радиуса геометрия пары пересчитывается.

Pair profile

02 / target relation

Зачем профиль

Модуль не может знать, должна ли пара быть «доминанта + спутник», «равноправная пара», «маркер + узел» или «разнесённые anchors». Это задаётся входным профилем.

PairProfile { relationType, radiusRatioRange?, gapRangeMinRadius?, angleTargetsDeg?, allowOverlap, directed, weights }

Источники профиля

ИсточникСтатус
назначенные роли A/BINPUT
стандартный engine profileHEURISTIC
визуальный стильSTYLE_PRESET
глобальный solverOVERRIDE / target
Example INITIAL_HEURISTIC profiles: dominant_support: radiusRatioBA = 0.20 .. 0.62 gapMinRadius = 0.6 .. 3.2 allowOverlap = false equal_pair: radiusRatioBA = 0.78 .. 1.28 gapMinRadius = 0.8 .. 4.0 anchor_pair: radiusRatioBA = 0.55 .. 1.45 gapMinRadius = 2.0 .. 12.0 // Эти диапазоны НЕ универсальные законы. // Они нужны только как стартовые candidate ranges и подлежат калибровке.

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

03 / metric ownership
MetricOwnerФункцияСтатус
pair.ratio_fit1.03насколько radiusRatioBA попадает в profile rangeLOCAL
pair.gap_fit1.03насколько зазор соответствует pair profileLOCAL
pair.angle_fit1.03отклонение от заданных target angles, если targets естьLOCAL
pair.role_relation_fit1.03соответствие назначенным ролям A/B и base-weight ratioLOCAL
pair.proximity_descriptor1.03геометрическая близость пары без глобального Gestalt-решенияLOCAL
global.balance_scorefuture global ownerне вычислять здесьDEFERRED
global.hierarchy_scorefuture global ownerне вычислять здесьDEFERRED
grid.fit_score1.06 / grid ownerне дублироватьCONSUME ONLY
point.conflict_score1.09касания/коллизии с внешними объектамиDEFERRED

Локальный scoring

04 / no double counting

Разрешено

localPairScore = wRatio * ratio_fit + wGap * gap_fit + wAngle * angle_fit + wRole * role_relation_fit // weights belong to PairProfile/engine preset // absent target → metric marked not_applicable, // not replaced with guessed value

Запрещено

// DO NOT add here: global_balance_score negative_space_score global_hierarchy_score eye_flow_score grid_fit_score external_collision_score contrast_score typography_score image_interaction_score
Финальный ranking пары выполняется вне 1.03: global solver объединяет localPairScore с owner-provided внешними метриками и hard constraints.

Контекст и overrides

05 / external owners
OwnerЧто может изменитьПосле изменения
Compo 1.09 conflictsпозицию B / reject кандидатапересчитать 1.03
Compo 1.10 contrastне геометрию напрямую, а contextual weight через 1.01при значимом Δweight возможен rerun
Grid ownersnap A/Bпересчитать distance/angle/gap
Global hierarchytarget size relation1.01 меняет radius → 1.03 recompute
Negative spaceразнести/сблизить пару1.02 + 1.03 rerun
Eye-flowtarget angle / orientationPairProfile angleTargets обновляется
Typography / imageprotected zones / external placement targetscandidate regeneration

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

06 / exact relations
dominant_support candidateρ = 0.414
Локально: отношение размеров и зазор можно проверить против pair-profile. Никаких выводов о глобальном балансе.
equal_pair / tiny positive gapρ = 0.958
Это может быть допустимо для equal_pair, но случайное «почти касание» с другими объектами оценивает 1.09. Pair-profile не заменяет conflict owner.
anchor_pair / large distance
Большое расстояние не считается ошибкой само по себе. Для anchor-profile это может быть целевой геометрией.
TEXTpair locally valid; external conflict pending
1.03 не штрафует типографический конфликт. Он сохраняет кандидата до внешнего typography/conflict pass.

Интерактивная лаборатория

07 / local pair only

Параметры

Алгоритм выполнения

08 / local pipeline
1 INPUT
A/B + radii + roles + PairProfile
2 GEOMETRY
d, θ, ρ, gap, overlap
3 GENERATE
pair candidates inside profile ranges
4 HARD LOCAL
finite geometry, allowed overlap
5 LOCAL SCORE
ratio/gap/angle/role fit
6 EXPORT
top-N + full local metrics
7 CONTEXT
external owner metrics
8 RECOMPUTE
if A/B/radius changed
function evaluatePointPair(A, B, profile, canvas) { geometry = computePairGeometry(A, B, canvas) if (!geometry.valid) return REJECT("INVALID_GEOMETRY") if (!profile.allowOverlap && geometry.surfaceGapShortN < 0) return REJECT("PAIR_OVERLAP_NOT_ALLOWED") metrics = { ratio_fit: fitRange(geometry.radiusRatioBA, profile.radiusRatioRange), gap_fit: fitRange(geometry.surfaceGapMinRadiusN, profile.gapRangeMinRadius), angle_fit: profile.angleTargetsDeg ? fitAngleTargets(geometry.angle360Deg, profile.angleTargetsDeg) : NOT_APPLICABLE, role_relation_fit: fitRoleRelation(A, B, profile) } localPairScore = weightedApplicableMean(metrics, profile.weights) return { geometry, metrics, localPairScore, deferredRequired: ["conflicts","global_balance","negative_space","hierarchy","eye_flow"] } }

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

09 / regression

TEST 01 — geometry

canvas = 1000×1000 A=(0.20,0.50), rA=0.05 B=(0.50,0.50), rB=0.025 expected: d = 0.30 shortSide ρ = 0.50 gap = 0.225 shortSide gap/minRadius = 9.0 angle = 0°

TEST 02 — overlap

A r=0.05 B r=0.05 centerDistance=0.08 surfaceGap=-0.02 allowOverlap=false expected: REJECT allowOverlap=true expected: VALID_LOCAL

TEST 03 — ownership

evaluatePointPair(...) expected output MUST NOT contain: global_balance_score global_hierarchy_score grid_fit_score external_collision_score negative_space_score

TEST 04 — no angle target

profile.angleTargetsDeg = null expected: angle_fit.status = NOT_APPLICABLE local score renormalizes remaining metric weights

TEST 05 — recompute

move B.xN += 0.1 expected: dirty: pair.geometry recompute: d,θ,gap invalidate: pair.localMetrics

TEST 06 — external override

1.09 marks candidate hard conflict expected: 1.03 does not recompute conflict candidate rejected by owner/global filter

DATA FOR LAYOUT ENGINE

10 / canonical contract
{ "module": "Compo 1.03", "version": "2.0", "name": "point_pair_relation", "scope": "two-point local geometry and pair-profile fit only", "consumes": [ "Compo0.Canvas", "Compo0.Constraint[]", "Compo1.00.Point.role", "Compo1.01.Point.radiusShortN", "Compo1.01.Point.visualWeightBase", "Compo1.02.PointPlacementCandidate?", "PointA.positionN", "PointB.positionN", "PairProfile?" ], "provides": [ "PointPairGeometry", "pair.centerDistanceShortN", "pair.angle360Deg", "pair.angle180Deg", "pair.radiusRatioBA", "pair.surfaceGapShortN", "pair.surfaceGapMinRadiusN", "pair.overlapDepthShortN", "pair.localMetrics", "pair.localPairScore", "PointPairCandidate[]" ], "owner_of": [ "pair.ratio_fit", "pair.gap_fit", "pair.angle_fit", "pair.role_relation_fit", "pair.localPairScore" ], "derived_geometry": [ "centerDistanceShortN", "angle360Deg", "angle180Deg", "radiusRatioBA", "surfaceGapShortN", "surfaceGapMinRadiusN", "overlapDepthShortN" ], "deferred_inputs": { "point.conflict_score": "Compo 1.09", "point.local_contrast": "Compo 1.10", "grid.fit_score": "Compo 1.06 / future grid owner", "global.negative_space_score": "future owner", "global.balance_score": "future owner", "global.hierarchy_score": "future owner", "global.eye_flow_score": "future owner", "typography_interaction": "future owner", "image_interaction": "future owner" }, "forbidden_metric_ownership": [ "global.balance_score", "global.hierarchy_score", "global.negative_space_score", "global.eye_flow_score", "grid.fit_score", "point.conflict_score", "point.local_contrast" ], "hard_constraints": [ "finite pair geometry", "positive radii", "overlap must respect PairProfile.allowOverlap" ], "heuristics": { "defaultPairProfiles": "INITIAL_HEURISTIC", "candidateCount": {"value": 48, "source": "INITIAL_HEURISTIC"} }, "recompute_when": [ "A.position changes", "B.position changes", "A.radius changes", "B.radius changes", "canvas dimensions change", "PairProfile changes" ], "finality": "LOCAL_PROPOSAL_ONLY", "rule": "external metrics are consumed after their owners compute them; Compo 1.03 never recreates them" }