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
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
Минимальные геометрические 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.