COMPO · 1.02 · SINGLE POINT PLACEMENT · v2.0

Размещение
одиночной
точки

Локальный placement-модуль. Он генерирует и оценивает допустимые координаты точки, но не присваивает себе баланс, негативное пространство, иерархию, eye-flow, collision или типографическую оценку. Эти данные приходят извне и могут переопределить локальный результат.

Контракт модуля

00 / consumes → provides

CONSUMES

Compo0.Canvas Compo0.ProtectedZones Compo1.00.PointRoleDescriptor Compo1.01.Point.radiusShortN Compo1.01.Point.visualWeightBase Grid? optional StylePreset?

PROVIDES

PointPlacementCandidate[] candidate.xN / yN candidate.source candidate.localScore candidate.reasonCodes[] candidate.hardStatus candidate.recomputeFlags[]

OWNS METRICS

point.safe_clearance_score point.landmark_fit_score point.anchor_fit_score point.edge_relation_score point.local_role_fit point.local_placement_score

DEFERRED INPUTS

point.conflict_score ← 1.09 point.local_contrast ← 1.10 pair/cluster context ← 1.03/1.04 global.negative_space global.balance global.hierarchy global.eye_flow typography/image conflicts
Ключевая корректировка: Compo 1.02 больше не вычисляет balance_score, negative_space_score, reading_path_score, hierarchy_score или text_interference. Это внешние метрики. Модуль может только потреблять их на более позднем contextual pass.

Координатная система

01 / deterministic geometry
xN
normalized X
xN = xPx / W 0 ≤ xN ≤ 1
yN
normalized Y
yN = yPx / H 0 ≤ yN ≤ 1
rN
radius / short side
rPx = radiusShortN × min(W,H)
// canonical conversion xPx = xN * Canvas.widthPx yPx = yN * Canvas.heightPx rPx = radiusShortN * min(Canvas.widthPx, Canvas.heightPx) // no aspect-ratio ambiguity: placement coordinates remain normalized, but all physical clearances are checked in pixels.

Safe bounds и hard geometry

02 / deterministic filter

Center bounds

left = safeArea.leftPx + rPx right = W - safeArea.rightPx - rPx top = safeArea.topPx + rPx bottom = H - safeArea.bottomPx - rPx insideSafeBounds = left ≤ xPx ≤ right AND top ≤ yPx ≤ bottom

Это DERIVED правило, если объект не имеет explicit bleed permission.

Protected zones

for zone in ProtectedZones: if zone.policy == HARD_EXCLUDE AND circleIntersects(point, zone): reject candidate

Явно защищённые области — hard constraint ядра. Но общие collision/tangency между объектами принадлежат Compo 1.09.

Не смешивать: safe bounds и explicit protected zones можно проверить сразу. «Красиво ли рядом с другим объектом» — это не задача 1.02.

Генераторы кандидатов

03 / proposal space
GeneratorКогда используетсяСтатусЧто выдаёт
role_anchor_candidatesanchor / node / markerRELATION-DERIVEDкоординаты связанных сущностей
grid_node_candidatesесли Grid доступен и роль допускает snapGRID-DERIVEDузлы/пересечения сетки
style_landmark_candidatesfree/focus/supportSTYLE_PRESETthirds, 0.382/0.618, центр и др.
edge_band_candidatesтолько explicit edge-accent / bleed stylesSTYLE_PRESETпозиции в разрешённой edge-band
external_candidatesfuture negative-space / balance / eye-flow modulesDEFERREDдополнительные coordinates
seeded_jitterпосле базового набора, только для diversityBOUNDED RANDOMмалые воспроизводимые offsets
candidates = union( relationCandidates(roleDescriptor), gridCandidates(Grid), styleLandmarks(StylePreset), edgeCandidates_if_allowed(), externalCandidates[] ) candidates = dedupe(candidates, epsilonPx) candidates = addSeededJitter(candidates, seed, jitterBounds)
Thirds / golden-like positions: это не законы композиции. В движке они только STYLE_PRESET candidate providers. Их нельзя использовать как hard preference для любого постера.

Role-aware grid snap

04 / functional priority

HARD-like relation

if functionalRole in [anchor,node] AND relation supplies exact position: anchor_fit = 1 candidate = relationPosition landmark preference ignored

SOFT snap

d = distancePx(candidate, nearestGridNode) thresholdPx = snapThresholdN * min(W,H) if d ≤ thresholdPx: create snapped variant create unsnapped variant

Решение не мутирует исходный candidate автоматически.

Controlled offset

offsetMaxX = style.gridJitterX * cellW offsetMaxY = style.gridJitterY * cellH jitter = PRNG(seed) within bounded interval

STYLE_PRESET, а не универсальная норма.

Только локальный scoring

05 / metric ownership

Собственная модель

localPlacementScore = w_role * local_role_fit + w_anchor * anchor_fit_score + w_landmark * landmark_fit_score + w_edge * edge_relation_score + w_safe * safe_clearance_score - w_move * relocation_cost // all weights are ENGINE_DEFAULT or STYLE_PRESET // this score is NOT final composition score

Что означают метрики

local_role_fitсоответствует ли тип позиции functional/visual role proposal
anchor_fitнасколько кандидат совпадает с требуемой relation position
landmark_fitблизость к активному style landmark, если preset его использует
edge_relationсоответствует ли edge policy роли
safe_clearanceгеометрический запас до hard boundaries
relocation_costстоимость перемещения, если исходная точка была supplied/locked
Если нет активного style landmark, landmark_fit_score получает applicability=0, а не искусственный neutral score. Это защищает итоговый score от фиктивных данных.

Context pass: чужие метрики

06 / deferred inputs

READ ONLY

Compo1.09 → conflict/tangency Compo1.10 → local contrast future Space → negative_space future Balance → global balance future Hierarchy → hierarchy future EyeFlow → eye flow future Typography → text interaction future Image → image/protected semantic interaction

Global ranking

// outside Compo 1.02 ownership contextualCandidateScore = combine( candidate.localPlacementScore, deferred MetricResults[] ) Global solver may: ACCEPT MOVE REJECT REGENERATE
Anti-cycle: 1.02 не требует contextual visual weight из 1.01 для первичного размещения. Он использует radius + base role data. После 1.09/1.10 и global passes возможен corrective placement pass, затем 1.01 пересчитывает contextual weight.

Визуальные примеры

07 / exact behavior
SAFE candidate
PASS: центр точки с учётом радиуса находится внутри safe bounds.
center inside ≠ shape inside
REJECT: проверяется не только центр; радиус должен полностью помещаться в hard-safe area.
anchor: grid candidate wins
Для anchor/node relation/grid candidate имеет приоритет над thirds/golden landmark.
style landmarks active
Thirds-like points используются только потому, что preset подключил этот candidate provider.
edge position supplied by relation
Периферийная позиция может быть правильной из-за relation; 1.02 не штрафует её просто за «нецентричность».
DEFER to Compo 1.09
Случайное касание с объектом не должно дублироваться локальным scoring. Его owner — модуль конфликтов 1.09.

Лаборатория локального placement

08 / local only

Здесь намеренно нет balance / negative-space / hierarchy. Лаборатория показывает только локальные метрики 1.02.

Порядок работы

09 / solver pass
1 INPUT
role + radius + scene
2 BOUNDS
safe / protected
3 GENERATE
candidate providers
4 HARD FILTER
geometry only
5 LOCAL SCORE
owned metrics
6 EXPORT
top local candidates
7 CONTEXT
deferred owners
8 CORRECT
global solver / rerun
function proposeSinglePointPlacement(scene, point, roleDescriptor): radius = point.radiusShortN bounds = deriveSafeCenterBounds(scene.canvas, radius) candidates = generateRoleCandidates(roleDescriptor, scene) candidates += generateGridCandidates_if_available(scene.grid) candidates += generateStyleLandmarks(scene.stylePreset) candidates += scene.externalPlacementCandidates candidates = seededDedupeAndJitter(candidates, scene.seed) valid = [] for c in candidates: if !insideHardSafeBounds(c, bounds): continue if intersectsHardProtectedZone(c, point, scene): continue c.metrics = computeOwnedLocalMetrics(c, point, roleDescriptor, scene) c.localPlacementScore = combineOwnedMetrics(c.metrics) valid.push(c) return topLocalCandidates(valid) // Later, outside 1.02 ownership: contextual = globalSolver.combine(valid, deferredMetricResults) if winner.positionChanged: rerun Compo 1.02 geometry checks rerun affected dependent modules

Тест-кейсы

10 / regression
T01
Safe bounds include radius.
W=1000,H=1000,safe=50px,r=20px → минимальный допустимый center X/Y = 70px, максимальный = 930px.
T02
Anchor relation overrides landmark.
Если anchor target = (0.71,0.24), а thirds landmark = (0.667,0.333), relation candidate получает anchor_fit=1. Landmark не имеет права самовольно сместить anchor.
T03
No global metric ownership.
В output 1.02 отсутствуют вычисленные balance_score, negative_space_score, hierarchy_score, eye_flow_score.
T04
Style landmark disabled.
Если preset не объявляет thirds/golden landmarks, landmark_fit.applicability=0; модуль не создаёт их по умолчанию как «правильные точки».
T05
Conflict deferred.
Кандидат около другого shape не получает собственного tangency penalty в 1.02. После Compo 1.09 он может быть отклонён или перемещён global solver.
T06
Corrective rerun.
Если global solver переместил точку, 1.02 заново проверяет safe/protected geometry и обновляет local metrics; старые local metrics становятся invalid.

DATA FOR LAYOUT ENGINE

11 / canonical contract
{ "module": "Compo 1.02", "version": "2.0", "name": "single_point_placement", "scope": "local candidate placement only", "consumes": [ "Compo0.Canvas", "Compo0.ProtectedZones", "Compo1.00.PointRoleDescriptor", "Compo1.01.Point.radiusShortN", "Compo1.01.Point.visualWeightBase", "Grid?", "StylePreset?" ], "provides": [ "PointPlacementCandidate[]", "candidate.xN", "candidate.yN", "candidate.source", "candidate.localPlacementScore", "candidate.reasonCodes[]", "candidate.hardStatus" ], "owns_metrics": [ "point.safe_clearance_score", "point.landmark_fit_score", "point.anchor_fit_score", "point.edge_relation_score", "point.local_role_fit", "point.local_placement_score" ], "deferred_inputs": { "point.conflict_score": "Compo 1.09", "point.local_contrast": "Compo 1.10", "pair_context": "Compo 1.03", "cluster_context": "Compo 1.04", "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" }, "hard_constraints": [ "point circle must fit hard safe bounds unless explicit bleed is allowed", "hard protected zones may not be intersected", "locked relation positions may not be silently mutated" ], "candidate_providers": { "role_relation": "DERIVED", "grid_nodes": "DERIVED_IF_GRID_EXISTS", "style_landmarks": "STYLE_PRESET", "edge_band": "STYLE_PRESET", "external": "DEFERRED_PROVIDER", "seeded_jitter": "BOUNDED_HEURISTIC" }, "initial_heuristics": { "candidate_dedupe_epsilon_shortN": 0.002, "default_top_local_candidates": 8, "jitter_shortN_max": 0.015 }, "forbidden_metric_ownership": [ "global.balance_score", "global.negative_space_score", "global.hierarchy_score", "global.eye_flow_score", "point.conflict_score", "point.local_contrast" ], "recompute_when": [ "point radius changes", "point role descriptor changes", "point position changes", "canvas changes", "safe area changes", "protected zones change", "grid changes", "style preset changes" ], "finality": "LOCAL_PROPOSAL_ONLY" }