Контракт модуля
00 / Compo 0.00 compatible
CONSUMES
Compo0.Canvas
Compo1.00.functionalRole
Compo1.00.visualRoleCandidate
Point.geometry
Point.fillRatio
Point.opacity
Point.strokeWidthN?
StylePreset.pointSize?
PROVIDES
Point.radiusShortN
Point.diameterShortN
Point.sizeClass
Point.visualWeightBase
Point.visualWeightContextual
Point.weightStatus
Point.weightComponents
OWNS METRICS
point.visual_weight_base
point.visual_weight_contextual
point.size_class
Другие модули могут поставлять modifiers, но итоговый point visual weight принадлежит только 1.01.
DEFERRED INPUTS
point.local_contrast ← 1.10
point.isolation_modifier ← 1.04/1.08
point.crowding_modifier ← 1.04/1.08
point.edge_modifier ← 1.02
global.hierarchy_target ← future
global.balance_score ← future
Главное исправление: Compo 1.01 больше не использует контраст, изоляцию, crowding или положение по краю в первом проходе. Эти данные ещё физически не существуют до placement/context passes.
Двухстадийная модель
01 / break the dependency cycle
PASS A — BASE
Вычисляется сразу после назначения роли. Только свойства самой точки.
INPUT:
radius / diameter
fill ratio
opacity
point primitive
style calibration
OUTPUT:
sizeClass
visualWeightBase
PASS B — CONTEXTUAL
Выполняется после размещения и контекстных модулей.
INPUT:
visualWeightBase
localContrast
isolationModifier
crowdingModifier
edgeModifier
OUTPUT:
visualWeightContextual
DEPENDENCY ORDER
1. Compo 1.00 assigns roles
2. Compo 1.01 PASS A → base size + base weight
3. Compo 1.02..1.10 → placement / relations / clusters / grid / contrast / context
4. Compo 1.01 PASS B → contextual weight
5. if |Wctx - Wprev| > ε_weight:
mark dependent candidates DIRTY
rerun only affected local modules
6. global solver decides whether point size/position must change
Размер точки
02 / deterministic geometry + presets
rₙ
radiusShortN
S = min(W,H)
rPx = physical radius
rN = rPx / S
Каноническая нормализация размера точки.
dₙ
diameterShortN
dN = 2 * rN
Используется для size classes и collision envelopes.
Aₙ
footprintShortN²
for filled circle:
Aeq = π * rN²
for other point primitives:
Aeq = primitiveArea / S²
Эквивалентная площадь — геометрическая величина, не итоговый visual weight.
| Class | diameterShortN | Назначение | Статус |
micro | < 0.006 | noise / technical micro-marker | STYLE PRESET |
small | 0.006–0.015 | support / marker / anchor | STYLE PRESET |
medium | 0.015–0.030 | node / visible support | STYLE PRESET |
large | 0.030–0.060 | local focus / strong node | STYLE PRESET |
oversize | 0.060–0.120 | point-like dominant only if style allows | STYLE PRESET |
promote_to_shape | > 0.120 | объект перестаёт быть «точкой» в базовой модели | INITIAL HEURISTIC |
Порог 0.120 не является законом восприятия. Это инженерная граница маршрутизации: выше неё объект выгоднее передать в модуль формы, чтобы не смешивать point-logic и shape-logic.
Базовый визуальный вес
03 / intrinsic only
Геометрическая масса
Aref = π * rRef²
massRatio = Aeq / Aref
sizeMass = massRatio ^ gammaSize
rRef — калибровочная reference-point из style/engine preset. gammaSize позволяет откалибровать нелинейность.
Base weight
intrinsicMass =
sizeMass
* fillFactor
* opacity^gammaOpacity
* primitiveFactor
visualWeightBase =
1 - exp(-kWeight * intrinsicMass)
Экспоненциальное сжатие гарантирует 0 ≤ Wbase < 1 и не требует ручного clamp после каждого множителя.
| Параметр | Тип | Источник | Комментарий |
Aeq | geometry | DERIVED | из реальной геометрии точки |
fillFactor | 0..1 | DERIVED | отношение фактически заполненной площади к envelope |
opacity | 0..1 | INPUT | альфа самого объекта, не contrast |
primitiveFactor | scalar | STYLE / CALIBRATION | оптическая поправка для circle/square/cross/ring; default=1 |
gammaSize | scalar | HEURISTIC | старт 0.65–1.00; калибруется |
kWeight | scalar | HEURISTIC | масштаб нормализации weight index |
Запрещено в PASS A: использовать local contrast, положение на поле, отрицательное пространство, соседей, кластерную плотность или global hierarchy. Это контекст, а не intrinsic property.
Контекстный визуальный вес
04 / recompute after context
C
local contrast
DEFERRED → 1.10
contrastModifier = f(C)
neutral default = 1.0
I
isolation
DEFERRED
isolationModifier
neutral default = 1.0
D
crowding
DEFERRED
crowdingModifier
neutral default = 1.0
E
edge context
DEFERRED → 1.02
edgeModifier
neutral default = 1.0
contextMultiplier = geometricMean(active modifiers)
// Example when all four exist:
contextMultiplier = (Cmod * Imod * Dmod * Emod)^(1/4)
// preserve bounded base-weight semantics:
baseMassEquivalent = -ln(1 - visualWeightBase)
contextualMass = baseMassEquivalent * contextMultiplier
visualWeightContextual = 1 - exp(-contextualMass)
// if any deferred metric is unavailable:
omit missing modifier
renormalize geometric mean over active modifiers
NEVER substitute an invented formula
Почему geometric mean: один экстремальный modifier не должен линейно «взорвать» результат. Это инженерный выбор, поэтому сама форма агрегатора имеет статус HEURISTIC и позже может быть откалибрована.
Размер как proposal, не приказ
05 / role targets
Visual role → target band
visualRoleCandidate = focus | support | neutral
focus:
target Wctx = stylePreset.focusWeightBand
support:
target Wctx = stylePreset.supportWeightBand
neutral:
no mandatory target
Target bands принадлежат style/global hierarchy system. Compo 1.01 лишь умеет подбирать размер под заданный target.
Inverse solve
given targetWeight T:
find rN such that
abs(weight(rN) - T) is minimal
method:
bounded binary search
or monotonic Newton/bisection
subject to:
minRadius ≤ rN ≤ maxRadius
Это позволяет глобальному solver сказать: «нужен вес 0.62», не придумывая радиус вручную.
Визуальные сравнения
06 / same size ≠ same weight
Живая лаборатория
07 / base vs contextual
Порядок работы
08 / solver integration
1 ROLE
1.00 assigns functional + visual roles
2 BASE
1.01 computes size + Wbase
3 PLACE
1.02+ generate geometry/context
4 CONTEXT
1.04/1.08/1.10 provide modifiers
5 REWEIGHT
1.01 computes Wctx
6 DIRTY?
if delta > epsilon, rerun affected modules
7 GLOBAL
solver may resize/move and repeat
DIRTY PROPAGATION
if radius changed:
dirty = [1.02,1.03,1.04,1.05,1.06,1.07,1.08,1.09,1.10]
if only contextual modifiers changed:
dirty = [1.01 PASS B]
if abs(Wctx_new - Wctx_old) > epsilonWeight:
notify global hierarchy/balance solver
No circular recomputation without a dirty-threshold and maxIterations.
Hard / soft / overrides
09 / rule classes
HARD
- finite radius
rN > 00 ≤ opacity ≤ 1- no NaN/∞ in modifiers
- weight owner remains 1.01
SOFT
- stay inside role-compatible size band
- avoid unstable size oscillation
- prefer small correction over full regeneration when feasible
OVERRIDES
- global hierarchy may request target weight
- global balance may request resize indirectly
- crop/semantic safety can cap radius
- style preset may redefine size classes
Тест-кейсы
10 / deterministic + regression
TEST 01 — base only
Canvas = 1080×1350
rPx = 13.5
S = 1080
expected rN = 0.0125
expected dN = 0.025
context unavailable
expected weightStatus = BASE_ONLY
TEST 02 — missing deferred
Cmod = 1.20
Imod = MISSING
Dmod = 0.80
Emod = MISSING
activeGM = sqrt(1.20*0.80)
expected no invented defaults
expected modifiersUsed = 2
TEST 03 — no cycle
Wprev = 0.441
Wnew = 0.446
epsilonWeight = 0.01
delta = 0.005
expected DIRTY = false
expected no placement rerun
TEST 04 — resize invalidates
old rN = 0.010
new rN = 0.018
expected geometryVersion++
expected dependent modules dirty
expected 1.01 PASS A rerun
TEST 05 — point → shape
dN = 0.145
pointMax = 0.120
expected route = SHAPE_MODULE
expected pointLogicFrozen = true
TEST 06 — ownership
Compo 1.10 provides localContrast
Compo 1.01 consumes localContrast
expected 1.10 MUST NOT write visualWeight
expected 1.01 owns final point weight
Data for layout engine
11 / canonical contract
{
"module": "Compo 1.01",
"name": "point_size_and_visual_weight",
"version": "2.0",
"owner_of": [
"point.size_class",
"point.visual_weight_base",
"point.visual_weight_contextual"
],
"consumes": [
"Compo0.Canvas",
"Compo1.00.PointRoleDescriptor",
"Point.geometry",
"Point.fillRatio",
"Point.opacity",
"StylePreset.pointSize"
],
"deferred_inputs": {
"point.local_contrast": "Compo 1.10",
"point.isolation_modifier": "Compo 1.04/1.08",
"point.crowding_modifier": "Compo 1.04/1.08",
"point.edge_modifier": "Compo 1.02",
"global.hierarchy_target": "future hierarchy module",
"global.balance_score": "future balance module"
},
"provides": [
"Point.radiusShortN",
"Point.diameterShortN",
"Point.sizeClass",
"Point.visualWeightBase",
"Point.visualWeightContextual",
"Point.weightStatus",
"Point.weightComponents"
],
"passes": {
"A_BASE": "intrinsic properties only",
"B_CONTEXTUAL": "requires available owner-provided modifiers"
},
"numeric_provenance": {
"radiusShortN": "DERIVED",
"diameterShortN": "DERIVED",
"equivalentArea": "DERIVED",
"sizeClassThresholds": "STYLE_PRESET",
"gammaSize": "INITIAL_HEURISTIC",
"kWeight": "INITIAL_HEURISTIC",
"contextAggregator": "INITIAL_HEURISTIC"
},
"rules": [
"PASS_A_MUST_NOT_USE_CONTEXT",
"MISSING_DEFERRED_INPUTS_ARE_OMITTED_NOT_INVENTED",
"CONTEXTUAL_WEIGHT_IS_RECOMPUTED_AFTER_CONTEXT_PASS",
"RESIZE_INVALIDATES_DEPENDENT_GEOMETRY_MODULES",
"POINT_WEIGHT_HAS_ONE_OWNER_ONLY"
],
"dirty_thresholds": {
"epsilonWeight": {"value":0.01,"source":"INITIAL_HEURISTIC"}
},
"routing": {
"diameterShortN_above_point_max": "PROMOTE_TO_SHAPE_MODULE"
},
"finality": "LOCAL_METRIC_ONLY_GLOBAL_SOLVER_MAY_REQUEST_RESIZE"
}
Финальная проверка
12 / checklist
Теперь правильно
- нет циклической зависимости «вес нужен до контекста, но контекст нужен после placement»;
- base и contextual weight разделены;
- метрики других модулей не переопределяются;
- отсутствующие deferred inputs не заменяются фантазийными коэффициентами;
- есть dirty propagation и порог повторного расчёта;
- global solver может запросить target weight, а 1.01 подбирает размер.
Что ещё будет калиброваться
gammaSize и kWeight;- shape/primitive optical factors;
- role-specific target bands;
- точная функция превращения owner metrics в modifiers;
- граница point → shape;
epsilonWeight для стабильной итерации.