API-контракт
00 / consumes → providesCONSUMES
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 / deterministicd
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/B | INPUT |
| стандартный engine profile | HEURISTIC |
| визуальный стиль | STYLE_PRESET |
| глобальный solver | OVERRIDE / 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| Metric | Owner | Функция | Статус |
|---|---|---|---|
pair.ratio_fit | 1.03 | насколько radiusRatioBA попадает в profile range | LOCAL |
pair.gap_fit | 1.03 | насколько зазор соответствует pair profile | LOCAL |
pair.angle_fit | 1.03 | отклонение от заданных target angles, если targets есть | LOCAL |
pair.role_relation_fit | 1.03 | соответствие назначенным ролям A/B и base-weight ratio | LOCAL |
pair.proximity_descriptor | 1.03 | геометрическая близость пары без глобального Gestalt-решения | LOCAL |
global.balance_score | future global owner | не вычислять здесь | DEFERRED |
global.hierarchy_score | future global owner | не вычислять здесь | DEFERRED |
grid.fit_score | 1.06 / grid owner | не дублировать | CONSUME ONLY |
point.conflict_score | 1.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 owner | snap A/B | пересчитать distance/angle/gap |
| Global hierarchy | target size relation | 1.01 меняет radius → 1.03 recompute |
| Negative space | разнести/сблизить пару | 1.02 + 1.03 rerun |
| Eye-flow | target angle / orientation | PairProfile angleTargets обновляется |
| Typography / image | protected zones / external placement targets | candidate regeneration |
Визуальная лаборатория
06 / exact relationsЛокально: отношение размеров и зазор можно проверить против pair-profile. Никаких выводов о глобальном балансе.
Это может быть допустимо для
equal_pair, но случайное «почти касание» с другими объектами оценивает 1.09. Pair-profile не заменяет conflict owner.Большое расстояние не считается ошибкой само по себе. Для anchor-profile это может быть целевой геометрией.
1.03 не штрафует типографический конфликт. Он сохраняет кандидата до внешнего typography/conflict pass.
Интерактивная лаборатория
07 / local pair onlyПараметры
Алгоритм выполнения
08 / local pipeline1 INPUT
A/B + radii + roles + PairProfile
A/B + radii + roles + PairProfile
2 GEOMETRY
d, θ, ρ, gap, overlap
d, θ, ρ, gap, overlap
3 GENERATE
pair candidates inside profile ranges
pair candidates inside profile ranges
4 HARD LOCAL
finite geometry, allowed overlap
finite geometry, allowed overlap
5 LOCAL SCORE
ratio/gap/angle/role fit
ratio/gap/angle/role fit
6 EXPORT
top-N + full local metrics
top-N + full local metrics
7 CONTEXT
external owner metrics
external owner metrics
8 RECOMPUTE
if A/B/radius changed
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 / regressionTEST 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"
}