COMPO 1.06 / Point-grid coupling / Snap & occupancy

Точки
и сетка

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

Назначение

00 / scope

Что решает этот модуль

Сетка превращает свободные координаты в систему опор. Для каждой точки модуль решает:

  • нужно ли привязывать её к узлу;
  • какой узел предпочтителен;
  • какое отклонение допустимо;
  • не перегружен ли выбранный модуль;
  • не создаётся ли слишком механичная раскладка;
  • можно ли нарушить сетку ради иерархии или баланса.

Ключевой принцип

Сетка не является абсолютным законом. Она задаёт дискретное пространство кандидатов, а итоговая позиция определяется контекстом.

base_position = nearest_allowed_grid_node(point) final_position = base_position + controlled_deviation where deviation may be changed by: hierarchy balance negative_space collision eye_flow typography

Математическая модель

01 / model
Gx
columns

Число колонок.

Gy
rows

Число рядов.

δ
deviation

Отклонение от узла.

O
occupancy

Занятость ячейки/узла.

Canvas: W × H Normalized coordinate system: x,y ∈ [0,1] Grid origin: (ox, oy) Grid steps: sx = usable_width / Gx sy = usable_height / Gy Node(i,j): Nx = ox + i × sx Ny = oy + j × sy Nearest node: i* = round((x - ox) / sx) j* = round((y - oy) / sy) Snap distance: d_snap = distance(P, N(i*,j*)) d_norm = d_snap / min(sx, sy) Controlled deviation: δx = (x_final - Nx) / sx δy = (y_final - Ny) / sy Recommended default: |δx|, |δy| ≤ 0.18 cell Hard snap: δ = 0 Soft snap: 0 < |δ| ≤ allowed_deviation

Типы сетки

02 / grid systems
Grid typeПараметрыДля точкиРекомендация
uniform_rectравные sx, syузлы / ячейкибазовый режим
columnGx + guttersпривязка по x, свободнее по yeditorial / типографика
modularGx × Gyполная 2D привязкатехнические постеры
baselineшаг по yвертикальный snapточки рядом с текстом
radialцентр + rings + anglesузлы на орбитахрадиальные системы
custom_nodesсписок координатsnap к разрешённым опорамнестандартная система
Важно: этот документ работает с любой сеткой, если она сведена к списку разрешённых узлов и/или линий привязки.

Режимы привязки

03 / snap modes

Hard snap

Точка обязана попасть точно в узел.

P_final = N_best δ = 0

Для: anchors, технических маркеров, точек-коннекторов.

Soft snap

Узел задаёт базовую позицию, но разрешается локальный сдвиг.

P_final = N_best + Δ |Δx| ≤ 0.18 sx |Δy| ≤ 0.18 sy

Для: декоративных точек и живой editorial-композиции.

Snap candidate

Сетка только предлагает несколько узлов, а solver выбирает лучший.

C = k nearest allowed nodes best = argmax(score(node))

Для: доминантных точек и конфликтных областей.

Snap decision: if role in [anchor, connector_node, measurement_marker]: mode = HARD elif role in [support, rhythm, micro_marker]: mode = SOFT elif role == focus: mode = CANDIDATE else: mode = SOFT Hard constraints may force a different mode.

Контролируемое отклонение

04 / deviation

Почему отклонение нужно

Если каждая точка строго сидит в узле, композиция быстро становится механической. Поэтому вторичным элементам разрешается небольшое смещение.

RoleDefault max δ
anchor0
focus0.00–0.12 cell
support0.00–0.18 cell
rhythm0.00–0.14 cell
micro noise0.00–0.28 cell

Deviation function

δ_max(role, context) = δ_base(role) × hierarchy_modifier × density_modifier × style_modifier Suggested modifiers: technical style: 0.40..0.75 editorial style: 0.85..1.15 experimental: 1.00..1.45 high density: 0.65..0.90 low density: 0.90..1.20
Запрет: отклонение не должно использоваться для маскировки плохого выбора узла. Если точке требуется сдвиг более 0.30 размера ячейки, алгоритм должен выбрать другой узел или отказаться от snap.

Occupancy / занятость

05 / density control

Занятость ячейки

Каждый объект увеличивает нагрузку на окружающие ячейки. Точка не должна автоматически размещаться в уже перегруженной зоне.

For cell c: O(c) = Σ influence(object_k, c) influence = visual_weight_k × spatial_kernel(distance) Gaussian kernel example: K(d) = exp( -d² / (2σ²) ) normalized occupancy: O_norm(c) = clamp(O(c) / O_target, 0, 1.5)

Пороговые режимы

O_normСостояниеДействие
0–0.35тихая зонаподходит для focus/support
0.35–0.70нормальнаяобычное размещение
0.70–1.00плотнаяуменьшить точку / повысить требования
>1.00перегруженнаяштраф или запрет
Node occupancy score: occupancy_score = 1 - clamp(O_norm(node), 0, 1) If role == focus: prefer O_norm ≈ 0.10..0.55 If role == support: prefer O_norm ≈ 0.20..0.70 If role == micro_noise: allow O_norm ≈ 0.35..0.85

Кандидаты + внешний контекст

06 / local vs deferred

Focus

Получает несколько grid-кандидатов. Compo 1.06 ранжирует их только по собственным grid-метрикам и не решает, где глобально должна находиться доминанта.

LOCAL CANDIDATES

Support

Может получить ближайшие допустимые узлы и bounded deviation. Выбор между ними после локального ранжирования может быть изменён global solver.

LOCAL PROPOSAL

Noise / marker

Допускает быстрый локальный выбор, если роль разрешает это и все hard constraints уже подтверждены владельцами соответствующих проверок.

FAST PATH
Compo 1.06 DOES NOT OWN: free_space_score hierarchy_score balance_score alignment_score eye_flow_score negative_space_score These are DEFERRED_INPUTS. They may be attached to GridCandidate later by their owners. Local candidate result: GridCandidate { nodeId, snappedPosition, deviation, components: { snap_distance_score, occupancy_fit, deviation_score, mechanicality_penalty }, grid_fit_score, deferredContext: null | ExternalMetricMap }
Архитектурное правило: Compo 1.06 не смешивает grid-fit с hierarchy/balance/eye-flow. Он отдаёт чистую локальную grid-оценку и список кандидатов; глобальный solver добавляет внешние метрики отдельно.

Контекстные взаимодействия

07 / interactions
УсловиеКоррекцияПричина
Рядом крупная доминантаиспользовать узлы, усиливающие связь, но не касаниеподчинение и группировка
Рядом текстовый блокпредпочитать общий baseline / column edgeсвязь с типографикой
Высокая локальная плотностьснизить deviation и/или выбрать соседний модульконтроль clutter
Пустая зона композицииразрешить более крупную deviationизбежать чрезмерной механики
Точка является anchor линииhard snapгеометрическая целостность
Grid node создаёт плохое касаниезапретить узел даже при минимальном расстоянииtangency penalty выше snap reward
Глобальный баланс плохразрешить переход в соседнюю ячейкуglobal rule overrides local snap

Правила приоритетов и overrides

08 / rule priority

Hard constraints — выше сетки

  • collision с защищённым объектом;
  • опасное касание;
  • выход за safe-area, если запрещён;
  • закрытие текста/лица/ключевой детали;
  • невыполнимый минимальный gap.

Global rules могут отменить snap

  • Compo 05 — negative space;
  • Compo 13 — dominance;
  • Compo 19 — hierarchy;
  • Compo 22 — asymmetrical balance;
  • Compo 27 — eye movement;
  • Compo 49 — global scoring.
Priority order: 1. hard safety / protected content 2. semantic constraints 3. global composition constraints 4. hierarchy / balance / eye flow 5. grid alignment 6. local aesthetic variation 7. randomization Therefore: GRID MUST NEVER OVERRIDE A BAD COMPOSITION.

Локальный scoring

09 / owned metrics only

Primitive grid metrics

d = distance(P_final, nearest_allowed_node) cell = min(sx, sy) q = d / max(0.5 * cell, EPS) snap_distance_score = 1 - smoothstep(0, 1, clamp(q,0,1)) δ = length(P_final - node) / cell deviation_score = 1 - smoothstep(0, δ_max(role), δ) occupancy_fit = roleOccupancyFit(role, O_norm(node))

Все три значения принадлежат Compo 1.06. Их диапазон после нормализации: [0..1].

Mechanicality penalty

For a set of comparable points: exactSnapRatio = count(δ≈0) / N repeatedOffsetRatio = dominant_offset_pattern / N mechanicality_raw = max(exactSnapRatio, repeatedOffsetRatio) mechanicality_penalty = style_requires_precision ? 0 : smoothstep(M0, M1, mechanicality_raw) M0,M1 = STYLE_PRESET / HEURISTIC

Это групповая grid-метрика. Она не заменяет rhythm/hierarchy metrics из других модулей.

GRID FIT AGGREGATE active = { snap_distance_score, occupancy_fit, deviation_score } w = normalizeActiveWeights(stylePreset.gridWeights) grid_fit_score = w.snap * snap_distance_score + w.occupancy * occupancy_fit + w.deviation * deviation_score - w.mechanicality * mechanicality_penalty grid_fit_score = clamp(grid_fit_score, 0, 1) IMPORTANT ANTI-DOUBLE-COUNTING: Global solver may consume EITHER: A) grid_fit_score OR B) its component metrics but MUST NOT sum A + B together.
Удалено из локальной формулы: hierarchy, balance, eye-flow, free-space, alignment, tangency и collision. Эти оценки принадлежат другим владельцам. Compo 1.06 может получить их как DEFERRED_INPUT либо запросить hard-validation, но не пересчитывает сам.

Примеры

10 / visual tests
GOOD / HARD + SOFT SNAP soft candidate
focus = nodesupport = nodesoft candidate shown
BAD / GRID BECAME THE COMPOSITION uniform occupancy + zero hierarchy + zero deviation
mechanicality highhierarchy low
soft snap: δ≈0.05 cell
bad tangency → reject node
grid relation + controlled deviation

Лаборатория

11 / interactive

Пошаговый алгоритм

12 / deterministic pipeline
01
Build grid
Сформировать узлы, линии и допустимую область.
02
Map occupancy
Рассчитать только grid occupancy от уже размещённых объектов.
03
Generate candidates
Получить k ближайших разрешённых узлов.
04
Request hard validation
Protected zones / collision / tangency проверяют их owner-модули.
05
Grid metrics
Snap distance, occupancy fit, deviation.
06
Local rank
Ранжировать только по grid_fit_score.
07
Export
Передать candidates + components + audit trail.
08
Global decision
Внешний solver добавляет deferred metrics и может override.
function proposePointGridCandidates(point, grid, scene, style): nodes = kNearestAllowedNodes(point.preferredPosition, grid, k=8) candidates = [] for node in nodes: validation = requestHardValidation(point, node, scene) if validation.status == REJECT: continue mode = resolveSnapMode(point.role, style) offsets = generateAllowedOffsets(node, mode, point.role, style) for offset in offsets: pos = node.position + offset snapScore = computeSnapDistanceScore(pos, node, grid) occFit = computeRoleOccupancyFit(point.role, node, scene.gridOccupancy) devScore = computeDeviationScore(offset, grid, point.role, style) gridFit = aggregateGridFit( snapScore, occFit, devScore, currentMechanicalityState(), style ) candidates.push({ nodeId: node.id, position: pos, snapMode: mode, gridFitScore: gridFit, components: {snapScore, occFit, devScore}, deferredContext: null }) if candidates is empty: return {status:"NO_GRID_CANDIDATE", fallback:"SOLVE_WITHOUT_GRID"} sortDescending(candidates, by=gridFitScore) return { status:"LOCAL_PROPOSAL", candidates, finalDecision:false } // NEXT STAGE, OUTSIDE COMPO 1.06: // owners attach hierarchy/balance/negative-space/eye-flow/etc. // global solver accepts, moves, re-ranks or rejects candidates.

Тест-кейсы

13 / validation
T01
Hard anchor
Input: 1080×1350, 6×8 grid, role=anchor, preferred=(0.48,0.52).
Expected: candidate at exact allowed node, δ=0, snap_distance_score=1. Final global placement is not asserted here.
T02
Soft support
Input: support point, editorial style.
Expected: exact-node candidate + bounded offset candidates |δ|≤0.18 cell; Compo 1.06 ranks them only by grid metrics. Alignment/hierarchy are absent until owner modules attach them.
T03
Occupied node
Input: nearest node has O_norm=1.12 and support role.
Expected: low occupancy_fit; candidate may remain locally valid unless an external hard rule rejects it.
T04
Bad tangency
Input: node would place point in forbidden tangency band.
Expected: Compo 1.06 requests owner validation (Compo 1.09 / geometry validator); if REJECT is returned, candidate is removed. 1.06 does not compute tangency penalty itself.
T05
Mechanicality
Input: 12 comparable support points, editorial style, all hard-snapped.
Expected: mechanicality_penalty > 0; local ranking favors bounded variation where available.
T06
Global override
Input: locally best grid candidate later receives poor global.balance_score.
Expected: Compo 1.06 output remains unchanged as local evidence; global solver may select another candidate. Audit log records the override owner.
T07
Anti-double-counting
Input: global solver receives grid_fit_score and its components.
Expected: aggregation uses either the aggregate or the components, never both. Duplicate aggregation must fail validation.

Data for layout engine

14 / machine block
{ "document_id": "Compo 1.06", "version": "2.0", "module": "point_grid_coupling", "finality": "LOCAL_PROPOSAL_ONLY", "coordinate_system": "normalized_0_1", "owns_metrics": [ "grid.snap_distance_score", "grid.node_occupancy", "grid.occupancy_fit", "grid.deviation_score", "grid.mechanicality_penalty", "grid.fit_score" ], "metric_groups": { "grid.fit_score": { "components": [ "grid.snap_distance_score", "grid.occupancy_fit", "grid.deviation_score", "grid.mechanicality_penalty" ], "aggregation_rule": "consume aggregate OR components, never both" } }, "grid": { "types": ["uniform_rect","column","modular","baseline","radial","custom_nodes"], "default_columns": {"value":6,"status":"HEURISTIC"}, "default_rows": {"value":8,"status":"HEURISTIC"}, "safe_margin_pct": {"value":[0.04,0.08],"status":"HEURISTIC"} }, "snap_modes": { "anchor": "hard", "connector_node": "hard", "focus": "candidate", "support": "soft", "rhythm": "soft", "noise": "soft" }, "deviation": { "focus_cell_fraction": {"value":[0.00,0.12],"status":"HEURISTIC"}, "support_cell_fraction": {"value":[0.00,0.18],"status":"HEURISTIC"}, "rhythm_cell_fraction": {"value":[0.00,0.14],"status":"HEURISTIC"}, "noise_cell_fraction": {"value":[0.00,0.28],"status":"HEURISTIC"}, "reject_if_over": {"value":0.30,"status":"HEURISTIC"} }, "deferred_inputs": [ "global.hierarchy_score", "global.balance_score", "global.eye_flow_score", "global.negative_space_score", "global.free_space_score", "global.alignment_score" ], "external_validators": [ "protected_content_validator", "Compo1.09.clearance_or_tangency_validator" ], "forbidden_local_redefinitions": [ "hierarchy_score", "balance_score", "eye_flow_score", "negative_space_score", "free_space_score", "alignment_score", "tangency_penalty", "collision_penalty" ], "override_policy": { "global_may_override_local_grid": true, "grid_may_override_hard_constraint": false, "random_may_override_grid": false } }

Итоговый чек-лист

15 / final
  • Сетка приведена к узлам/линиям, которые алгоритм умеет вычислять.
  • Для каждой точки определён snap-mode.
  • Nearest node не принимается без проверки контекста.
  • Occupancy-карта содержит только метрики, принадлежащие этому модулю, и рассчитывается до локального grid ranking.
  • Hard constraints проверяются до scoring их владельцами; Compo 1.06 потребляет результат validation.
  • Soft deviation ограничен размером ячейки.
  • Отклонение >0.30 cell приводит к выбору нового узла.
  • Глобальные правила могут отменить локально лучший snap без изменения локальных evidence-метрик.
  • Технический стиль допускает больше hard-snap.
  • Editorial/experimental стиль контролирует mechanicality.
  • Randomization применяется только после всех ограничений.
  • Все коэффициенты имеют DERIVED / HEURISTIC / STYLE status; внешние метрики не пересчитываются локально.
  • Global aggregation не суммирует grid_fit_score вместе с его компонентами.

Core contract

COMPO 0.00 / API · v2

Compo 1.06 / SCOPE

Только point↔grid coupling: узлы, snap, controlled deviation, occupancy map и локальный grid-fit.

Статус: LOCAL_PROPOSAL_ONLY. Модуль не принимает финального композиционного решения.

OWNS_METRICS

- grid.snap_distance_score - grid.node_occupancy - grid.occupancy_fit - grid.deviation_score - grid.mechanicality_penalty - grid.fit_score Aggregation group: grid.fit_score = aggregate(components) Global consumer uses aggregate OR components, never both.

CONSUMES

- Compo0.Canvas - Compo0.Grid - Point[] - Point.role / subrole / rigidity - Point.visualWeightBase - Point.visualWeightContextual? (if fresh) - occupied scene objects - StylePreset - external hard-validation results

PROVIDES

- Point.gridCandidates[] - GridCandidate.nodeId - GridCandidate.position - GridCandidate.snapMode - GridCandidate.gridDeviation - GridCandidate.gridFitScore - GridCandidate.gridMetricComponents - Grid.nodeOccupancyMap - audit evidence

DEFERRED_INPUTS

Могут использоваться только после вычисления их владельцами.

- global.hierarchy_score - global.balance_score - global.eye_flow_score - global.negative_space_score - global.free_space_score - global.alignment_score Policy: owner absent → metric omitted owner present → attach to candidate as external evidence NEVER redefine inside 1.06

OVERRIDES_ALLOWED_BY

- protected-content hard constraints - collision / tangency owner validation - hierarchy owner - balance owner - eye-flow owner - negative-space owner - global composition solver

WEIGHT INPUT POLICY

effectiveExistingWeight(object): if contextualWeight exists AND contextualWeight.version is fresh: return contextualWeight else: return visualWeightBase Candidate point itself is NOT added to occupancy map until candidate evaluation requests a hypothetical map.

DIRTY / RECOMPUTE

recompute 1.06 when: grid changes candidate point position/role changes occupied object geometry changes relevant effective visual weight changes protected/allowed grid nodes change Do NOT recompute because a deferred global score changed; only global ranking changes unless solver moves geometry.
Anti-double-counting: grid.fit_score является агрегатом собственных grid-компонентов. Если глобальный solver использует агрегат, компоненты не добавляются повторно. Tangency/collision и global metrics принадлежат другим owner-модулям.