API-контракт
00 / ownership
CONSUMES
Line
Line.pathGeometry
Line.strokeWidthPx?
Line.role
Line.rigidity
SceneObject
Object.boundsN
Object.shapeGeometry?
Object.shapeSDF?
Object.anchorHints?
OPTIONAL:
Compo2.09.AttachmentDescriptor
Compo2.09.ConnectionIntent
StylePreset.contacts?
PROVIDES
LineShapeContactRelation[]
ContactDescriptor:
objectId
pathPosition
pointN
type
signedDistancePx
tangentAngleDeg?
penetrationDepthPx?
contactLengthPx?
confidence
line.shapeContact.count
line.shapeContact.penalty
line.shapeContact.tangencyPenalty
line.shapeContact.penetrationPenalty
line.shapeContact.nearContactPenalty
ContactCorrectionRequest[]
DOES NOT OWN
line-line intersections // 2.10
semantic attachment intent // 2.09
line visual weight // 2.06
line color contrast // 2.16
grid alignment // 2.13
global hierarchy
global balance
global eye-flow
Boundary: 2.10 — линия против линии. 2.11 — линия против границы формы/объекта. Если connector специально прикреплён к объекту, 2.11 анализирует геометрию контакта, но semantic correctness attachment остаётся у 2.09.
Типы line-shape relation
01 / taxonomy
×
crossing
Линия проходит через границу формы и входит/выходит из interior.
○
tangent touch
Линия касается boundary без перехода внутрь.
≈
near tangency
Линия почти касается формы, но между stroke и границей остаётся малый зазор.
●
attachment
Endpoint намеренно закреплён на boundary/port объекта.
LineShapeContactType =
BOUNDARY_CROSSING |
TANGENT_TOUCH |
NEAR_TANGENCY |
ATTACHMENT |
INTERIOR_RUN |
NONE
Универсальная signed-distance модель
02 / preferred representation
Shape SDF
d(p) =
signedDistanceToShape(p)
d > 0 → outside
d = 0 → boundary
d < 0 → inside
Если объект предоставляет SDF или эквивалентный shape-distance API, это лучший канонический способ классифицировать контакт.
Path sampling
for q in arc fractions:
p = samplePath(q)
d[q] = shapeSDF(p)
sign change:
boundary crossing
local minimum |d|:
tangent / near-contact candidate
Для точного crossing после coarse sampling выполняется root refinement.
Fallback: если shape geometry неизвестна, можно анализировать bounds, но relation помечается confidence < 1. Нельзя считать bbox истинной формой объекта.
Точное касание
03 / tangent geometry
Boundary normal
n =
normalize(
∇SDF(contactPoint)
)
Нормаль направлена наружу из формы.
Line tangent
t =
normalize(
pathTangent(contactQ)
)
Берётся у владельца path geometry.
Tangency angle
normalAngle =
angleBetween(t,n)
tangentError =
abs(90° - normalAngle)
ideal tangency:
tangentError = 0
DERIVED Геометрическое касание означает, что tangent линии ортогонален normal границы.
Для визуальной оценки near-tangency важны два независимых признака: малый gap и малый tangentError. Один только маленький gap ещё не делает контакт похожим на касание.
Проникновение внутрь формы
04 / interior analysis
Penetration depth
penetrationDepthPx =
max_q(
max(0,-SDF(P(q)))
)
Максимальная глубина центра линии внутри формы.
Interior run
interiorArcLength =
∫ I[SDF(P(s)) < 0] ds
interiorFraction =
interiorArcLength /
pathLength
Показывает, насколько значительная часть path проходит внутри объекта.
Stroke-aware
effectiveDistance =
SDF(centerline)
- 0.5*strokeWidthPx
effectiveDistance < 0
→ stroke overlaps shape
Толстая линия может визуально заходить на форму, даже если её centerline остаётся снаружи.
Не путать с attachment: connector может специально входить на несколько пикселей внутрь объекта из-за rendering cap/arrowhead. Если 2.09 помечает attachment как намеренный, 2.11 применяет другой policy, а не автоматически штрафует.
Near-tangency и случайный зазор
05 / accidental contact detector
Clearance
centerClearance =
min_q SDF(P(q))
strokeClearance =
centerClearance
- 0.5*strokeWidthPx
Для внешнего path положительное значение означает реальный зазор между stroke и shape.
Near-tangency penalty
gapTerm =
1 - clamp(
strokeClearance /
nearGapThresholdPx,
0,1
)
angleTerm =
1 - clamp(
tangentError /
angleThresholdDeg,
0,1
)
nearTangencyPenalty =
gapTerm * angleTerm
HEURISTIC Сильный penalty возникает только когда линия одновременно близка и почти параллельна boundary tangent.
Если strokeWidthPx неизвестен, near-contact может считаться по centerline distance только как диагностическая метрика с пониженной confidence.
Local contact scoring
06 / role-aware
OWNS
line.shapeContact.contactCount
line.shapeContact.boundaryCrossingCount
line.shapeContact.tangentTouchCount
line.shapeContact.nearTangencyCount
line.shapeContact.interiorRunFraction
line.shapeContact.penetrationPenalty
line.shapeContact.tangencyPenalty
line.shapeContact.nearContactPenalty
line.shapeContact.localContactScore
DEFERRED / FOREIGN
2.09 attachmentSemanticFit
2.10 lineIntersectionPenalty
2.06 visualWeight
2.16 contrast
global hierarchy
global balance
global eye-flow
object importance
if contact.type == ATTACHMENT
and attachmentIntent is valid:
use attachmentContactProfile
else:
localContactScore =
1 - clamp(
wP*penetrationPenalty
+ wT*tangencyPenalty
+ wN*nearContactPenalty,
0,1
)
// role pair / object policy comes from STYLE_PRESET
// measurement stays deterministic
Composite-first: будущий 2.20 использует line.shapeContact.localContactScore как основной vote. Penetration/tangency/nearContact остаются evidence-компонентами, чтобы не задвоить один и тот же контакт.
Role-aware policies
07 / contact meaning
| Line role | Boundary crossing | Attachment | Near tangency |
connector | обычно нежелательно кроме owner endpoint | часто ожидается | штрафовать случайные почти-касания |
annotation | обычно избегать | ожидается у target/source | высокая чувствительность |
trajectory | может пересекать объект только по контексту | не обязательно | средняя чувствительность |
accent | может быть намеренно | редко семантическое | STYLE_PRESET |
texture | может проходить под/через объекты | не применимо | обычно низкая чувствительность |
divider | обычно не режет semantic objects | не применимо | средняя чувствительность |
INITIAL HEURISTIC Таблица задаёт стартовые policy-направления, а не универсальные правила дизайна.
Correction requests
08 / no foreign mutation
MOVE
Сдвинуть path или endpoint от случайного контакта.
REROUTE
Попросить 2.04/2.05 изменить траекторию.
SNAP
Если почти-contact должен быть attachment — предложить 2.09 привязать к валидному anchor.
ALLOW
Зафиксировать intentional overlap/crossing reason.
ContactCorrectionRequest {
lineId,
objectId,
relationId,
reasonCode:
ACCIDENTAL_NEAR_TANGENCY |
UNINTENTIONAL_PENETRATION |
IMMEDIATE_REENTRY |
MISALIGNED_ATTACHMENT |
INTENTIONAL_CONTACT,
targetOwner:
"Compo2.04" |
"Compo2.05" |
"Compo2.09" |
"GLOBAL_SOLVER",
requestedChange,
bounds?,
confidence
}
Визуальная лаборатория
09 / contact cases
Детерминированные тесты
10 / regression
TEST 01 · circle tangent
circle c=(0,0), r=10
line y=10
expected:
TANGENT_TOUCH
min signed distance=0
no interior run
TEST 02 · circle crossing
circle r=10
line y=0
expected:
2 boundary crossings
penetrationDepth=10
TEST 03 · near tangent
circle r=10
line y=11
strokeWidth=0
expected:
clearance=1
nearTangency if
threshold > 1
TEST 04 · stroke overlap
circle r=10
line y=11
strokeWidth=4
center clearance=1
effective clearance=-1
expected:
stroke overlaps shape
TEST 05 · attachment
endpoint=(10,0)
circle r=10
2.09 says valid attachment
expected:
contact type=ATTACHMENT
not generic penalty
TEST 06 · fallback
shape missing
bounds present
expected:
bounds relation allowed
confidence reduced
TEST 07 · no leak
change:
global.balanceScore
expected:
contact geometry
UNCHANGED
TEST 08 · ownership
near tangency found
2.11 MAY:
emit correction request
MUST NOT:
move Bézier controls itself
TEST 09 · angle
boundary normal=(1,0)
line tangent=(0,1)
expected:
normalAngle=90°
tangentError=0
DATA FOR LAYOUT ENGINE
11 / machine contract
{
"module": "Compo 2.11",
"name": "line_shape_tangency_and_contact",
"version": "1.0",
"scope": "LOCAL_LINE_SHAPE_CONTACT",
"consumes": [
"Compo2.00.Line",
"Compo2.00.LinePathGeometry",
"Compo0.SceneObject",
"Object.boundsN",
"Object.shapeGeometry?",
"Object.shapeSDF?",
"Compo2.06.Line.strokeWidthPx?",
"Compo2.09.AttachmentDescriptor?",
"Compo2.09.ConnectionIntent?",
"StylePreset.contacts?"
],
"provides": [
"LineShapeContactRelation[]",
"ContactDescriptor",
"ContactCorrectionRequest[]"
],
"contact_types": [
"BOUNDARY_CROSSING",
"TANGENT_TOUCH",
"NEAR_TANGENCY",
"ATTACHMENT",
"INTERIOR_RUN",
"NONE"
],
"owns_metrics": [
"line.shapeContact.contactCount",
"line.shapeContact.boundaryCrossingCount",
"line.shapeContact.tangentTouchCount",
"line.shapeContact.nearTangencyCount",
"line.shapeContact.interiorRunFraction",
"line.shapeContact.penetrationPenalty",
"line.shapeContact.tangencyPenalty",
"line.shapeContact.nearContactPenalty",
"line.shapeContact.localContactScore"
],
"hard_rules": [
"line_shape_contact_must_not_be_mixed_with_line_line_intersection",
"semantic_attachment_status_must_come_from_2.09",
"exact_shape_geometry_preferred_over_bounds_fallback",
"missing_shape_geometry_must_reduce_confidence",
"missing_stroke_width_must_not_be_invented",
"path_geometry_must_come_from_geometry_owner",
"foreign_geometry_changes_must_be_emitted_as_requests"
],
"initial_heuristics": {
"near_gap_threshold_width_multiplier": {"value":1.5,"source":"INITIAL_HEURISTIC"},
"near_tangent_angle_threshold_deg": {"value":12,"source":"INITIAL_HEURISTIC"},
"sdf_root_refine_iterations": {"value":12,"source":"IMPLEMENTATION_HEURISTIC"}
},
"deferred_inputs": [
"Compo2.09.attachmentSemanticFit",
"Compo2.10.lineIntersectionPenalty",
"Compo2.16.lineColorContrast",
"global.eyeFlowScore",
"global.hierarchyScore",
"global.balanceScore"
],
"metric_policy": {
"primary_vote": "line.shapeContact.localContactScore",
"components": "diagnostic_only_by_default"
},
"recompute_when": [
"LinePathGeometry changes",
"Object shape/bounds changes",
"stroke width changes",
"2.09 attachment intent changes",
"Line.role or rigidity changes",
"StylePreset.contacts changes"
],
"finality": "LOCAL_CONTACT_ANALYSIS_ONLY"
}
ARCHITECTURE CHECKPOINT: 2.11 замыкает базовое разделение контактов: 2.10 владеет line-line relations, 2.11 — line-shape relations, 2.09 — semantic attachment intent. Эти три слоя связаны, но не дублируют ownership друг друга.