API-контракт модуля
00 / consumes → provides
CONSUMES
Canvas.widthPx
Canvas.heightPx
Canvas.diagonalPx
Line.role
Line.p0 = {xN, yN}
Line.p1 = {xN, yN}
Line.directed
DERIVED INPUT Координаты предполагаются уже приведёнными к системе Compo 0.00.
PROVIDES
Line.dxPx / Line.dyPx
Line.lengthPx
Line.lengthDiagN
Line.lengthShortN
Line.angleRad
Line.angle360
Line.angle180
Line.unitVector
Line.normalVector
Line.midpointN
Line.boundsN
Line.pathGeometry : LinePathGeometry
Line.geometryValid
Эти значения принадлежат Compo 2.01. Остальные модули их читают, но не пересчитывают независимо.
METRIC OWNERSHIP: длина и угол — не «оценочные» метрики. Они вычисляются строго по геометрии и не могут быть переопределены стилем, балансом, иерархией или глобальным solver. Глобальная система может изменить endpoints, после чего Compo 2.01 просто пересчитывает геометрию.
Координаты endpoints
01 / coordinate system
P₀
start point
Начальная точка линии. Для направленной линии определяет source.
P₁
end point
Конечная точка. Для directed-линии определяет target.
[0..1]
normalized canvas
xN=0 — левый край, xN=1 — правый; yN=0 — верх, yN=1 — низ.
P0_N = (x0N, y0N)
P1_N = (x1N, y1N)
P0_PX = (x0N * W, y0N * H)
P1_PX = (x1N * W, y1N * H)
dxPx = (x1N - x0N) * W
dyPx = (y1N - y0N) * H
Важно: для прямоугольного холста нельзя вычислять реальный угол из dxN и dyN напрямую. Нормализованные оси имеют разный физический масштаб, если W ≠ H. Угол вычисляется по пиксельным дельтам или по aspect-corrected координатам.
Длина линии
02 / deterministic geometry
Физическая длина
dx = (x1N-x0N) * W
dy = (y1N-y0N) * H
lengthPx = sqrt(dx² + dy²)
Это единственная корректная длина в пикселях.
Нормализованная длина
diag = sqrt(W² + H²)
lengthDiagN = lengthPx / diag
shortSide = min(W,H)
lengthShortN = lengthPx / shortSide
lengthDiagN удобно для межформатного сравнения; lengthShortN — для stroke/spacing систем.
| Поле | Формула | Диапазон | Источник |
lengthPx | Euclidean distance | 0..diagPx | DERIVED |
lengthDiagN | lengthPx / diagonal | 0..1 | DERIVED |
lengthShortN | lengthPx / min(W,H) | 0..sqrt(1+aspect²) | DERIVED |
minRenderableLengthPx | renderer threshold | implementation-dependent | STYLE / RENDER PRESET |
Segment → LinePathGeometry
02b / canonical path adapter
Для прямого segment
pathLengthPx = lengthPx
pathLengthShortN = lengthShortN
pathLengthDiagN = lengthDiagN
chordLengthPx = lengthPx
startTangent = unitVector
endTangent = unitVector
Spatial extent
extentXPx = abs(dxPx)
extentYPx = abs(dyPx)
spatialExtentShortN =
max(extentXPx, extentYPx) / min(W,H)
boundsN = segment bounds
Это отдельная величина от path length. Она понадобится Compo 2.07.
Compo 2.04 и 2.05 обязаны выдавать тот же интерфейс для curve/polyline. После этого downstream-модули работают с Line.pathGeometry одинаково.
Угол и направление
03 / angle conventions
Directed line
θrad = atan2(dy, dx)
θdeg = θrad * 180/π
θ360 = (θdeg + 360) mod 360
0° → вправо, 90° → вниз в экранной системе координат.
Undirected line
θ180 = θ360 mod 180
0° ≡ 180°
45° ≡ 225°
Подходит для осей, divider и иных линий без смыслового source/target.
Unit vector
u = (dx,dy) / lengthPx
ux = dx / lengthPx
uy = dy / lengthPx
Единичный вектор нужен для offset, parallel lines, normals и trajectories.
НЕ СМЕШИВАТЬ: экранная система Y вниз и математическая система Y вверх дают противоположный знак вертикальной компоненты. В Compo-движке фиксируем экранную конвенцию: +Y = down. Любой модуль, использующий углы, обязан следовать ей.
Производные геометрические признаки
04 / secondary geometry
M
midpoint
Mx=(x0+x1)/2
My=(y0+y1)/2
N
normal
normal=(-uy, ux)
alt=(uy,-ux)
BBox
bounds
minX=min(x0,x1)
maxX=max(x0,x1)
minY=min(y0,y1)
maxY=max(y0,y1)
AR
slope safety
if |dx| < ε:
slope = INF
else:
slope = dy/dx
Почему это здесь: midpoint, normal и bounds однозначно следуют из endpoints и нужны множеству будущих модулей. Поэтому их выгоднее вычислять один раз владельцем геометрии, а не дублировать.
Визуальная лаборатория
05 / exact examples
Aspect-ratio trap
06 / common implementation error
Неправильно
dxN = 0.2
dyN = 0.2
θ = atan2(0.2,0.2) = 45°
На холсте 1080×1350 это не 45° физически.
Правильно
dxPx = 0.2*1080 = 216
dyPx = 0.2*1350 = 270
θ = atan2(270,216)
θ ≈ 51.34°
Любая угловая логика должна использовать aspect-corrected geometry.
Validation и edge cases
07 / hard rules
| Проверка | Условие | Действие | Класс |
| finite endpoints | все координаты finite | иначе reject | HARD |
| normalized bounds | xN,yN∈[0,1] если line не может bleed | clamp/reject по upstream policy | UPSTREAM |
| zero length | lengthPx ≤ ε | mark invalid | HARD |
| direction | directed line имеет source/target semantic order | не сортировать endpoints автоматически | HARD |
| undirected canonicalization | для cache можно привести P0/P1 к детерминированному порядку | lexicographic sort only if undirected | IMPLEMENTATION |
| very short line | геометрически valid, но render может быть бессмысленным | передать в Compo 2.07 | DEFERRED |
Что этот модуль НЕ решает
08 / contextual boundaries
Не решает «хороший угол»
Выбор горизонтали, вертикали или диагонали принадлежит Compo 2.02–2.03 и глобальному контексту.
DEFERRED INPUT
Не решает визуальный вес
Толщина, контраст и роль линии будут определяться Compo 2.06, 2.16 и иерархией.
DEFERRED INPUT
Не решает eye-flow
Вектор экспортируется, но его влияние на маршрут взгляда оценивает отдельный модуль.
DEFERRED INPUT
Правильная архитектура: глобальный solver имеет право изменить P0 и P1. После любого изменения endpoints Compo 2.01 обязан пересчитать все производные поля. Он не спорит с solver и не хранит «старый» угол как самостоятельное решение.
Пошаговый алгоритм
09 / deterministic pass
function computeLineGeometry(line, canvas):
assert finite(line.p0.xN, line.p0.yN, line.p1.xN, line.p1.yN)
x0 = line.p0.xN * canvas.widthPx
y0 = line.p0.yN * canvas.heightPx
x1 = line.p1.xN * canvas.widthPx
y1 = line.p1.yN * canvas.heightPx
dx = x1 - x0
dy = y1 - y0
L = sqrt(dx*dx + dy*dy)
if L <= EPS_PX:
return { valid:false, reason:"ZERO_LENGTH" }
ux = dx / L
uy = dy / L
angleRad = atan2(dy, dx)
angle360 = ((angleRad * 180/PI) + 360) % 360
angle180 = angle360 % 180
midpointN = ((x0+x1)/(2*W), (y0+y1)/(2*H))
normal = (-uy, ux)
boundsN = {
minX:min(p0.xN,p1.xN), maxX:max(p0.xN,p1.xN),
minY:min(p0.yN,p1.yN), maxY:max(p0.yN,p1.yN)
}
return {
valid:true,
dxPx:dx, dyPx:dy,
lengthPx:L,
lengthDiagN:L / canvas.diagonalPx,
lengthShortN:L / min(W,H),
unit:{x:ux,y:uy},
normal:{x:-uy,y:ux},
angleRad,
angle360,
angle180,
midpointN,
boundsN
}
Численный тест
10 / exact expected output
INPUT
canvas = 1080 × 1350
P0 = (0.20, 0.70)
P1 = (0.80, 0.30)
directed = true
EXPECTED
P0px = (216, 945)
P1px = (864, 405)
dx = 648
dy = -540
lengthPx ≈ 843.506965
angle360 ≈ 320.1944°
angle180 ≈ 140.1944°
midpointN = (0.50, 0.50)
unit ≈ (0.768221, -0.640184)
DATA FOR LAYOUT ENGINE
11 / machine contract
{
"module": "Compo 2.01",
"owner_of": [
"line.dxPx",
"line.dyPx",
"line.lengthPx",
"line.lengthDiagN",
"line.lengthShortN",
"line.angleRad",
"line.angle360",
"line.angle180",
"line.unitVector",
"line.normalVector",
"line.midpointN",
"line.boundsN",
"line.pathGeometry(segment-adapter)"
],
"consumes": [
"Compo0.Canvas",
"Compo0.Vec2",
"Compo2.00.Line.role",
"Line.p0",
"Line.p1",
"Line.directed"
],
"provenance": {
"all_geometry_fields": "DERIVED",
"EPS_PX": "IMPLEMENTATION_HEURISTIC"
},
"hard_constraints": [
"finite_endpoints",
"lengthPx > EPS_PX",
"directed_endpoint_order_must_be_preserved"
],
"provides": [
"Line.pathGeometry.geometryType=segment",
"Line.pathGeometry.pathLengthPx",
"Line.pathGeometry.pathLengthShortN",
"Line.pathGeometry.pathLengthDiagN",
"Line.pathGeometry.chordLengthPx",
"Line.pathGeometry.spatialExtentShortN",
"Line.pathGeometry.startTangent",
"Line.pathGeometry.endTangent"
],
"deferred_to": {
"angle_preference": ["Compo 2.02", "Compo 2.03"],
"length_role": ["Compo 2.07"],
"visual_weight": ["Compo 2.06", "Compo 2.16"],
"eye_flow": ["future global eye-flow module"],
"grid_snap": ["Compo 2.13"]
},
"recompute_when": [
"p0 changes",
"p1 changes",
"canvas width changes",
"canvas height changes"
]
}
MIGRATION 2.00–2.07 / pre-2.08: canonical LinePathGeometry, role/subrole/rigidity, metric ownership cleanup, path-length vs spatial-span separation.