COMPO · 1.04 · POINT CLUSTER SYSTEM

Кластеры
точек

Модуль превращает 3+ точек в вычислимую групповую сущность. Он владеет геометрией кластера, локальной связностью, распределением плотности и формой облака — но не присваивает себе глобальный баланс, негативное пространство или общую иерархию композиции.

API-контракт

00 / consumes → provides

CONSUMES

Compo0.Canvas Compo0.Constraint[] Point[] Point.role Point.positionN Point.radiusShortN Point.visualWeightBase Point.visualWeightContextual? PointPairGeometry[]? ClusterProfile?

PROVIDES

PointClusterGeometry cluster.centroidN cluster.weightedCentroidN cluster.radius95ShortN cluster.spreadMeanShortN cluster.spreadSigmaShortN cluster.principalAxisDeg cluster.anisotropy cluster.localDensityDescriptor cluster.localMetrics PointClusterCandidate[]

DEFERRED INPUTS

point.conflict_score ← 1.09 point.local_contrast ← 1.10 grid.fit_score ← 1.06 global.negative_space_score ← future global.balance_score ← future global.hierarchy_score ← future global.eye_flow_score ← future text/image interaction metrics ← future
Исправление архитектуры: кластер больше не включает global_balance_contribution, negative_space_preservation, global_hierarchy или eye_flow в собственный score. Эти значения приходят только от владельцев соответствующих метрик.

Геометрия кластера

01 / deterministic
C
centroid

Среднее положение центров точек.

Cw
weighted centroid

Центр, взвешенный локальным visual weight.

R95
robust radius

95-й перцентиль расстояний от centroid.

σ
spread

Средний и стандартный разброс.

shortSide = min(W,H) for each point i: Xi = (xN_i * W, yN_i * H) centroidPx = mean(Xi) centroidN = (centroidPx.x/W, centroidPx.y/H) // Weight source policy: wi = Point.visualWeightContextual if available else Point.visualWeightBase weightedCentroidPx = Σ(wi * Xi) / Σ(wi) ri = distance(Xi, centroidPx) R95px = percentile(ri, 95) radius95ShortN = R95px / shortSide spreadMeanPx = mean(ri) spreadSigmaPx = stddev(ri)
DERIVED: centroid, R95, spread и covariance следуют из уже размещённых точек. Если точки перемещены или их contextual weight изменился, модуль пересчитывает descriptor, но не принимает глобального решения сам.

Форма облака

02 / covariance shape

Covariance → principal axis

dx_i = Xi.x - C.x dy_i = Xi.y - C.y Sxx = mean(dx_i²) Syy = mean(dy_i²) Sxy = mean(dx_i*dy_i) λ1 ≥ λ2 = eigenvalues([[Sxx,Sxy],[Sxy,Syy]]) principalAxis = eigenvector(λ1) anisotropy = sqrt(λ1 / max(λ2, EPS))

DERIVED Это геометрическое описание, не эстетическая оценка.

ClusterProfile

ClusterProfile { role, shapeTarget?: compact|elongated|radial|tail|bimodal, pointCountRange?, anisotropyRange?, densityProfile?, localHierarchyProfile?, spacingProfile?, weights }

Профиль задаёт желаемое поведение. Диапазоны — HEURISTIC или STYLE_PRESET, а не универсальные законы.

Shape targetСтартовая эвристикаНазначение
compactanisotropy 1.00..1.35локальное сгущение
elongated1.35..2.50направленное облако
tailcore + monotonically thinning extensionпереход / направление
bimodal2 локальных maximaдвойной узел

Плотность

03 / local-only descriptor

kNN density

d_k(i) = distance to k-th nearest point localDensity_i = 1 / max(d_k(i), EPS)^2 // normalize only inside cluster: densityN_i = localDensity_i / max(localDensity)

Не смешивает px² и normalized area.

Core density

core = points where distance(Xi,C) ≤ q * R95 q default = 0.45 source = INITIAL_HEURISTIC

Core — локальный дескриптор, а не «правильная» композиция.

Gradient

densityGradient = corr( distanceFromCentroid, densityN ) negative → density decreases outward

Используется только если profile требует градиент.

Локальная иерархия

04 / cluster-internal only

Что можно считать здесь

weights = [point.visualWeightContextual ?? point.visualWeightBase] w1 = max(weights) w2 = secondMax(weights) meanRest = mean(weights excluding w1) anchorDominance = w1 / max(meanRest, EPS) secondaryRatio = w2 / max(w1, EPS)

Это только внутреннее распределение веса в группе.

Что запрещено считать здесь

global_hierarchy_score composition_dominance_score headline_competition image_dominance balance_contribution negative_space_preservation

Эти метрики принадлежат другим модулям.

Кластер может иметь сильную локальную anchor-точку и при этом оставаться второстепенным относительно фотографии или заголовка. Поэтому anchorDominanceglobal.hierarchy_score.

Интервалы и связность

05 / local topology

Nearest-neighbour

nn_i = min_j≠i distance(Xi,Xj) meanNN = mean(nn_i) sigmaNN = stddev(nn_i) cvNN = sigmaNN / max(meanNN,EPS)

DERIVED descriptor.

Connectivity

edge(i,j) if distance(Xi,Xj) ≤ connectRadius components = connectedComponents(graph) cohesionDescriptor = 1 / components

connectRadius задаётся profile/preset.

Outlier

z_i = (ri - mean(r)) / max(stddev(r),EPS) outlierCandidate if z_i > zThreshold zThreshold default = 1.8 source = INITIAL_HEURISTIC

Outlier не удаляется автоматически: только маркируется.

Metric ownership

06 / no double counting
MetricOwnerСмыслСтатус
cluster.shape_fit1.04соответствие covariance shape целевому profileLOCAL
cluster.cohesion_fit1.04локальная связность группыLOCAL
cluster.spacing_fit1.04соответствие spacing profileLOCAL
cluster.density_fit1.04локальный density targetLOCAL
cluster.local_hierarchy_fit1.04внутренняя anchor/support структураLOCAL
cluster.outlier_penalty1.04необъяснимые локальные выбросыLOCAL
global.balance_scorefuture ownerне вычислять здесьDEFERRED
global.negative_space_scorefuture ownerне вычислять здесьDEFERRED
global.hierarchy_scorefuture ownerне вычислять здесьDEFERRED
grid.fit_score1.06consume onlyEXTERNAL
point.conflict_score1.09collision/tangencyEXTERNAL

Локальный scoring

07 / applicable metrics only

Разрешённый score

localClusterScore = weightedApplicableMean({ shape_fit, cohesion_fit, spacing_fit, density_fit, local_hierarchy_fit, 1 - outlier_penalty }, ClusterProfile.weights) // not_applicable metrics are omitted // remaining weights are renormalized

Не добавлять

global_balance_score negative_space_score global_hierarchy_score global_eye_flow_score grid_fit_score external_collision_score contrast_score typography_score image_interaction_score

Context interactions

08 / override graph
OwnerЧто может сделатьЧто делает 1.04 после override
Compo 1.01пересчитать contextual weight точекпересчитать weighted centroid и local hierarchy descriptors
Compo 1.02переместить отдельную точкупересчитать cluster geometry
Compo 1.06предложить snap к gridоценить новое локальное shape/spacing состояние
Compo 1.09обнаружить collision/tangencyrepair/remove/move candidate point
Global negative-space ownerпотребовать уменьшить/сдвинуть кластергенерировать локально допустимые варианты под новый target
Global balance ownerизменить позицию/масштаб группыне спорить; пересчитать descriptor

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

09 / machine-readable examples
GOOD: compact cluster: одна anchor-точка, несколько supports, R95 очерчивает локальную группу.
RECLASSIFY: это регулярный pattern/grid, а не organic cluster. 1.04 не должен штрафовать его как «плохой кластер», если upstream роль — pattern.
TAIL PROFILE: локальная плотность и размер постепенно уменьшаются вдоль principal direction.

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

10 / local descriptor only
Лаборатория демонстрирует только локальные cluster descriptors. Она намеренно не показывает «финальную композицию»: balance, negative space и global hierarchy сюда не встроены.

Порядок выполнения

11 / local pipeline
1 INPUT
points + role + profile
2 GEOMETRY
centroid, R95, covariance
3 DESCRIBE
density, spacing, hierarchy
4 LOCAL SCORE
owned metrics only
5 HARD FILTER
known local hard violations
6 EXPORT
top-N candidates
7 CONTEXT
external owner metrics
8 RECOMPUTE
after any override
function evaluatePointCluster(points, profile, external = null): geometry = computeClusterGeometry(points) local = computeOwnedMetrics(geometry, points, profile) score = weightedApplicableMean(local, profile.weights) candidate = { points, geometry, localMetrics: local, localClusterScore: score, deferred: external ?? "UNRESOLVED" } return candidate // Global solver later may move/scale/repair the cluster. // After mutation call evaluatePointCluster() again. // Compo 1.04 never synthesizes missing global metrics.

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

12 / regression

TEST 01 — centroid

canvas 1000×1000 points: (0.2,0.4) (0.4,0.4) (0.6,0.4) expected centroidN = (0.4,0.4) source = DERIVED

TEST 02 — weight fallback

contextualWeight unavailable base weights = [1,2,1] expected: weighted centroid uses base weights NO fabricated contextual modifier

TEST 03 — reclassify pattern

regular 3×4 array role = pattern expected: 1.04 does not apply organic CV target spacing_fit = not_applicable forward to pattern/rhythm owner

TEST 04 — deferred global

localClusterScore = 0.91 global.balance_score = unresolved expected: local score remains 0.91 final score = NOT_FINAL no neutral guess inserted

TEST 05 — dirty propagation

point P3 moved invalidate: centroid R95 covariance density spacing local hierarchy if weights changed

TEST 06 — ownership

Compo 1.09 returns conflict score expected: 1.04 may consume it 1.04 MUST NOT recompute it or rename duplicate metric

DATA FOR LAYOUT ENGINE

13 / canonical contract
{ "module": "Compo 1.04", "name": "point_cluster_system", "version": "2.0", "finality": "LOCAL_PROPOSAL_ONLY", "consumes": [ "Compo0.Canvas", "Compo0.Constraint[]", "Compo1.00.Point.role", "Point.positionN", "Compo1.01.Point.radiusShortN", "Compo1.01.Point.visualWeightBase", "Compo1.01.Point.visualWeightContextual?", "Compo1.03.PointPairGeometry[]?", "ClusterProfile?" ], "provides": [ "PointClusterGeometry", "cluster.centroidN", "cluster.weightedCentroidN", "cluster.radius95ShortN", "cluster.spreadMeanShortN", "cluster.spreadSigmaShortN", "cluster.principalAxisDeg", "cluster.anisotropy", "cluster.localDensityDescriptor", "cluster.localMetrics", "cluster.localClusterScore", "PointClusterCandidate[]" ], "owner_of": [ "cluster.shape_fit", "cluster.cohesion_fit", "cluster.spacing_fit", "cluster.density_fit", "cluster.local_hierarchy_fit", "cluster.outlier_penalty" ], "forbidden_metric_ownership": [ "global.balance_score", "global.negative_space_score", "global.hierarchy_score", "global.eye_flow_score", "grid.fit_score", "point.conflict_score", "point.local_contrast" ], "deferred_inputs": { "point.conflict_score": "Compo 1.09", "point.local_contrast": "Compo 1.10", "grid.fit_score": "Compo 1.06", "global.balance.score": "future owner", "global.negative_space.score": "future owner", "global.hierarchy.score": "future owner", "global.eye_flow.score": "future owner" }, "initial_heuristics": { "compact_anisotropy_max": 1.35, "elongated_anisotropy_range": [1.35, 2.50], "core_radius_ratio": 0.45, "outlier_z_threshold": 1.8, "candidate_count_default": 32 }, "rules": [ "use contextual point weight only when owner has computed it", "fallback to base point weight without inventing context", "not_applicable metrics are omitted and remaining local weights renormalized", "external metrics are consumed, never recreated", "after any point movement or weight mutation recompute dependent cluster descriptors", "global solver may override local cluster proposal" ] }

Финальная проверка

14 / migration checklist
  • У кластера задан profile/role?
  • Centroid и weighted centroid пересчитаны после изменения точек?
  • Contextual weight используется только если уже рассчитан владельцем?
  • Все локальные метрики имеют owner = 1.04?
  • Global balance не считается локально?
  • Negative space не считается локально?
  • Grid fit не дублируется?
  • Collision/tangency не дублируются?
  • Regular pattern не штрафуется как organic cluster без основания?
  • HEURISTIC числа промаркированы и калибруемы?
  • После override выполняется recompute?
  • Локальный score не выдаётся за финальный composition score?