COMPO · 2.09 · OBJECT CONNECTIONS

Линии
между
объектами.

Модуль описывает семантическое соединение объектов: какие сущности связаны, где линия может к ним прикрепиться, насколько корректно выбраны anchors и существует ли валидный маршрут между ними. Он не владеет пересечениями, касаниями, цветом или глобальным eye-flow — только отношением object ↔ connector ↔ object.

API-контракт

00 / semantic edge contract

CONSUMES

SceneObject source SceneObject target ConnectionIntent? Line.role = connector | annotation Line.directed Line.sourceId / targetId Line.pathGeometry Object.boundsN Object.shapeGeometry? Object.anchorHints? ProtectedZones[] StylePreset.connection? OPTIONAL: Compo2.07.roleLengthFit Compo2.07.detourRatio Compo2.08.localCueStrength

PROVIDES

ConnectionIntent AnchorCandidate[] sourceAnchors AnchorCandidate[] targetAnchors AttachmentDescriptor sourceAttachment AttachmentDescriptor targetAttachment ConnectionCandidate[] ConnectionTopologyDescriptor line.connection.localScore line.connection.confidence ConnectionCorrectionRequest[]

DOES NOT OWN

2.10 line-line intersections 2.11 line-shape tangency aesthetics 2.13 grid alignment 2.16 color/contrast global.eyeFlowScore global.balanceScore global.hierarchyScore object.semanticPriority

2.09 может потреблять результаты этих владельцев позже, но не пересчитывает их внутри себя.

HARD: connector без идентифицируемых endpoints не является валидной семантической связью. Если смысл связи неизвестен, линия должна иметь другую роль — например accent или trajectory.

Connection intent

01 / what is being connected

Object → Object

sourceObjectId targetObjectId relationType: association sequence dependency comparison annotation navigation

Тип отношения приходит из семантического слоя, а не угадывается геометрией.

Directed

if relation is directional: Line.directed = true sourceId = A targetId = B source/target order must remain stable

Глобальный solver может изменить маршрут, но не должен инвертировать смысл связи.

Undirected

if relation symmetric: Line.directed = false endpoints still belong to A and B, but flow hypothesis may be unresolved

Недиректированная связь всё равно имеет два семантических владельца.

ConnectionIntent { id, sourceObjectId, targetObjectId, relationType, directed, importance? // semantic input, NOT owned here preferredAnchorTags?, forbiddenAnchorTags?, allowMultiEdge?, allowSharedAnchor?, provenance }

Anchor candidates

02 / where a connector may attach
C
center

Центр объекта — стабильный reference, но редко финальный визуальный attachment для видимой линии.

E
edge projection

Точка пересечения луча center→otherCenter с границей объекта.

P
ports

Явные semantic ports: left/right/top/bottom, callout point, data-node port.

H
hint anchors

Заданные upstream точки: лицо, заголовок, маркер, hotspot, конкретная деталь изображения.

Для прямоугольника

cA = center(boundsA) cB = center(boundsB) d = normalize(cB - cA) sourceEdge = rayRectIntersection(cA,d,boundsA) targetEdge = rayRectIntersection(cB,-d,boundsB)

DERIVED Это хороший стартовый candidate, потому что линия выходит с ближайшей стороны.

Для круга

sourceAnchor = centerA + radiusA * d targetAnchor = centerB - radiusB * d

Для произвольной формы используется intersection с shape outline/SDF/маской, а не bounding-box, если такая геометрия доступна.

Fallback policy: если точная форма объекта неизвестна, разрешён bounds-based anchor с пониженной confidence. Нельзя притворяться, что attachment к bounding-box совпадает с реальным силуэтом.

Attachment geometry

03 / endpoint relation to owner

Boundary distance

boundaryErrorPx = distance( endpoint, nearestPointOnOwnerBoundary ) boundaryFit = exp(-boundaryErrorPx / sigma)

sigma — HEURISTIC/STYLE. Для строго закреплённого порта error может быть hard ≈ 0.

Departure angle

n = outwardNormal(anchor) t = path startTangent departureAngle = angleBetween(t,n) departureFit = roleProfile(departureAngle)

Connector часто читается чище, когда выходит из границы в разумном направлении, но 90° не является универсальным законом.

Immediate re-entry

sample path after anchor if path enters owner interior again within short arc: reentryPenalty ↑

Линия не должна сразу после выхода снова нырять внутрь своего объекта без специальной причины.

AttachmentDescriptor { objectId, anchorId?, pointN, anchorSource: explicit_port | semantic_hint | shape_projection | bounds_projection | fallback, boundaryErrorPx, outwardNormal?, tangentAngleToNormalDeg?, immediateReentry, locked, confidence }

Маршрут между anchors

04 / candidate families

DIRECT

segment(A,B)

Минимальная длина. Хороший baseline-кандидат.

ORTHOGONAL

A → bend → B 1–2 bends

Полезно для схем, диаграмм, технических связей.

CURVE

cubicBezier A,C1,C2,B

Позволяет обойти препятствие или получить более мягкий вход/выход.

POLYLINE

A → P1 → ... → B

Когда нужно маршрутизировать вокруг нескольких областей.

Ownership boundary: 2.09 создаёт семантически валидные route families и anchors. Точные line-line intersection penalties принадлежат 2.10, tangency/contact — 2.11, curve geometry — 2.04, polyline topology — 2.05. После каждого изменения маршрута соответствующий geometry-owner пересчитывает LinePathGeometry.
ROUTE CANDIDATE ORDER 1. exact semantic anchors / explicit ports 2. nearest compatible boundary anchors 3. direct path 4. bounded orthogonal / curve / polyline alternatives 5. geometry-owner recompute 6. protected-zone hard filter 7. 2.07 length/detour evaluation 8. 2.08 local directional cue 9. 2.10 / 2.11 conflict checks when available 10. 2.09 semantic connection score 11. global solver

Shared anchors и multi-edge

05 / local topology

Single edge

degree(anchor)=1

Обычный connector между двумя объектами.

Shared port

degree(anchor)>1 only if: allowSharedAnchor=true

Допустимо в диаграммах и технических схемах; иначе связи могут сливаться визуально.

Parallel relations

same A,B multiple edges require: allowMultiEdge=true relation IDs distinct

Иначе дублирующий connector должен быть merge/reject.

Локальный scoring

06 / semantic fit only

OWNS

line.connection.sourceAttachmentFit line.connection.targetAttachmentFit line.connection.endpointSemanticFit line.connection.anchorCompatibility line.connection.routeEconomyFit line.connection.duplicateEdgePenalty line.connection.localScore line.connection.confidence

routeEconomyFit может потреблять detour из 2.07, но не пересчитывает длину пути самостоятельно.

DOES NOT OWN

line.intersectionPenalty // 2.10 line.tangencyPenalty // 2.11 line.gridFit // 2.13 line.visualWeight // 2.06 line.colorContrast // 2.16 global.eyeFlowScore global.balanceScore
localScore = wS * sourceAttachmentFit + wT * targetAttachmentFit + wE * endpointSemanticFit + wA * anchorCompatibility + wR * routeEconomyFit - wD * duplicateEdgePenalty // weights = STYLE_PRESET / INITIAL_HEURISTIC // no global aesthetics here
Composite-first: в будущем Compo 2.20 основной голос — line.connection.localScore; внутренние компоненты остаются evidence/diagnostics, если scorer явно не разрешает отдельное голосование.

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

07 / attachment cases
boundary projection
GOOD: endpoints лежат на ближайших участках границы обоих объектов.
center-to-center visible stroke
BAD DEFAULT: видимый stroke проходит через внутренности объектов. Центры годятся как reference, но не как финальные attachments.
explicit right/left ports
PORT-LOCKED: явные semantic ports сильнее автоматической nearest-edge эвристики.
direct blocked → reroute family
2.09 может предложить curve/polyline reroute, но collision ownership остаётся у protected-zone/2.10/2.11.
same anchors / different route economy
Обе линии семантически соединяют A и B, но 2.07 может сообщить разный detour. 2.09 использует это как один из входов routeEconomyFit.

Pass order

08 / dependency-safe integration
INPUT: source object A target object B connection intent 1. validate semantic endpoints 2. normalize Line.role = connector|annotation 3. gather explicit ports / anchor hints 4. derive boundary anchor candidates 5. rank anchor compatibility locally 6. build route families 7. geometry owner creates path + LinePathGeometry 8. protected-zone HARD filter 9. consume 2.07 roleLengthFit / detour if available 10. consume 2.08 localCueStrength if applicable 11. request 2.10 intersection analysis 12. request 2.11 attachment/tangency conflict analysis 13. compute 2.09 local connection score 14. emit correction requests, not foreign mutations 15. forward feasible candidates to global solver RECOMPUTE WHEN: • object bounds/shape changes • anchor/port hints change • source/target changes • path geometry changes • detour/conflict results change • role/subrole/rigidity changes

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

09 / regression

TEST 01 · circles

A=(0,0), r=10 B=(40,0), r=5 expected: sourceAnchor=(10,0) targetAnchor=(35,0) boundary errors = 0

TEST 02 · semantic lock

relation directed=true source=A target=B expected: route may change anchors may change source/target order must NOT invert

TEST 03 · explicit port

A.rightPort locked B.leftPort locked expected: selected anchors = explicit ports auto projection must not override them

TEST 04 · missing target

role=connector sourceId=A targetId=null expected: HARD INVALID unless targetAnchor is explicit semantic entity

TEST 05 · fallback

shapeGeometry unavailable bounds available expected: bounds projection allowed anchorSource= bounds_projection confidence < exact-shape case

TEST 06 · duplicate

two edges A↔B same relation allowMultiEdge=false expected: duplicateEdgePenalty high merge/reject proposal

TEST 07 · no global leak

change: global.balanceScore global.hierarchyScore expected: attachment descriptors UNCHANGED

TEST 08 · detour input

same anchors candidate X detour=1.05 candidate Y detour=1.70 all else equal expected: routeEconomyFit(X) > routeEconomyFit(Y)

TEST 09 · ownership

2.09 detects need to avoid obstacle expected: emit reroute request MUST NOT: compute 2.10 intersection metric itself

LIVE REFERENCE TESTS

IDInputComputedStatus

Минимальные геометрические smoke-tests выполняются JavaScript при открытии файла.

DATA FOR LAYOUT ENGINE

10 / machine contract
{ "module": "Compo 2.09", "name": "lines_between_objects", "version": "1.0", "scope": "LOCAL_SEMANTIC_CONNECTION", "consumes": [ "Compo0.SceneObject", "Compo0.ProtectedZones", "Compo2.00.Line.role", "Compo2.00.Line.subrole?", "Compo2.00.Line.rigidity", "Compo2.00.Line.directed", "Compo2.00.Line.sourceId", "Compo2.00.Line.targetId", "Compo2.00.LinePathGeometry", "Object.boundsN", "Object.shapeGeometry?", "Object.anchorHints?", "ConnectionIntent?", "Compo2.07.Line.roleLengthFit?", "Compo2.07.detourRatio?", "Compo2.08.Line.flowGuideDescriptor?", "StylePreset.connection?" ], "provides": [ "ConnectionIntent", "AnchorCandidate[]", "AttachmentDescriptor", "ConnectionCandidate[]", "ConnectionTopologyDescriptor", "ConnectionCorrectionRequest[]" ], "owns_metrics": [ "line.connection.sourceAttachmentFit", "line.connection.targetAttachmentFit", "line.connection.endpointSemanticFit", "line.connection.anchorCompatibility", "line.connection.routeEconomyFit", "line.connection.duplicateEdgePenalty", "line.connection.localScore", "line.connection.confidence" ], "anchor_priority": [ "explicit_locked_port", "semantic_hint", "shape_boundary_projection", "bounds_projection", "fallback" ], "route_families": [ "direct_segment", "orthogonal_polyline", "bounded_curve", "bounded_polyline" ], "hard_rules": [ "connector_requires_semantic_endpoints", "directed_connection_preserves_source_target_order", "explicit_locked_ports_must_not_be_overridden", "route_geometry_must_be_recomputed_by_geometry_owner", "protected_zone_hard_policy_must_be_respected", "must_not_own_line_intersection_metrics", "must_not_own_line_shape_tangency_metrics", "must_not_invent_missing_object_shape_geometry" ], "deferred_inputs": [ "Compo2.10.lineIntersectionPenalty", "Compo2.11.lineShapeTangencyPenalty", "Compo2.13.gridAlignment", "Compo2.16.lineColorContrast", "global.eyeFlowScore", "global.balanceScore", "global.hierarchyScore" ], "metric_policy": { "primary_vote": "line.connection.localScore", "components": "diagnostic_only_by_default", "foreign_conflict_metrics": "consume_only" }, "recompute_when": [ "source object geometry changes", "target object geometry changes", "anchor hints or ports change", "sourceId or targetId changes", "LinePathGeometry changes", "Compo2.07 detour changes", "Compo2.10 or 2.11 conflict results change", "role/subrole/rigidity changes" ], "finality": "LOCAL_CONNECTION_PROPOSAL_ONLY" }
ARCHITECTURE CHECKPOINT: 2.09 добавляет семантический слой поверх общей геометрии линии. Он отвечает за «что с чем соединено» и «где разрешено прикрепиться», но не забирает ownership у будущих модулей пересечений, касаний, сетки, цвета и глобального eye-flow.