Контракт модуля
00 / consumes → providesCONSUMES
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 geometryxN
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 filterCenter 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_candidates | anchor / node / marker | RELATION-DERIVED | координаты связанных сущностей |
grid_node_candidates | если Grid доступен и роль допускает snap | GRID-DERIVED | узлы/пересечения сетки |
style_landmark_candidates | free/focus/support | STYLE_PRESET | thirds, 0.382/0.618, центр и др. |
edge_band_candidates | только explicit edge-accent / bleed styles | STYLE_PRESET | позиции в разрешённой edge-band |
external_candidates | future negative-space / balance / eye-flow modules | DEFERRED | дополнительные coordinates |
seeded_jitter | после базового набора, только для diversity | BOUNDED 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 priorityHARD-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 inputsREAD 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 behaviorPASS: центр точки с учётом радиуса находится внутри safe bounds.
REJECT: проверяется не только центр; радиус должен полностью помещаться в hard-safe area.
Для
anchor/node relation/grid candidate имеет приоритет над thirds/golden landmark.Thirds-like points используются только потому, что preset подключил этот candidate provider.
Периферийная позиция может быть правильной из-за relation; 1.02 не штрафует её просто за «нецентричность».
Случайное касание с объектом не должно дублироваться локальным scoring. Его owner — модуль конфликтов 1.09.
Лаборатория локального placement
08 / local onlyЗдесь намеренно нет balance / negative-space / hierarchy. Лаборатория показывает только локальные метрики 1.02.
Порядок работы
09 / solver pass1 INPUT
role + radius + scene
role + radius + scene
2 BOUNDS
safe / protected
safe / protected
3 GENERATE
candidate providers
candidate providers
4 HARD FILTER
geometry only
geometry only
5 LOCAL SCORE
owned metrics
owned metrics
6 EXPORT
top local candidates
top local candidates
7 CONTEXT
deferred owners
deferred owners
8 CORRECT
global solver / rerun
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 / regressionT01
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 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 отсутствуют вычисленные
В output 1.02 отсутствуют вычисленные
balance_score, negative_space_score, hierarchy_score, eye_flow_score.T04
Style landmark disabled.
Если preset не объявляет thirds/golden landmarks,
Если preset не объявляет thirds/golden landmarks,
landmark_fit.applicability=0; модуль не создаёт их по умолчанию как «правильные точки».T05
Conflict deferred.
Кандидат около другого shape не получает собственного tangency penalty в 1.02. После Compo 1.09 он может быть отклонён или перемещён global solver.
Кандидат около другого 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.
Если 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"
}