COMPO · 2.01 · LINE GEOMETRY

Начало.
Конец.
Длина.
Угол.

Первый вычислительный модуль линии. Он не оценивает «красоту» и не решает глобальную композицию. Его задача — без неоднозначности превратить две координаты в геометрические признаки, которые затем используют остальные модули.

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 систем.

ПолеФормулаДиапазонИсточник
lengthPxEuclidean distance0..diagPxDERIVED
lengthDiagNlengthPx / diagonal0..1DERIVED
lengthShortNlengthPx / min(W,H)0..sqrt(1+aspect²)DERIVED
minRenderableLengthPxrenderer thresholdimplementation-dependentSTYLE / 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
P0 → P1 / θ = 0° P0P1
Горизонталь: dy=0, θ=0°, u=(1,0).
dx > 0 / dy < 0 screen-space θ
Диагональ: направление вычисляется через atan2(dy,dx), а не через slope.
normal from midpoint N
Нормаль используется для параллельных offset-линий, подписей и внешних маркеров.

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иначе rejectHARD
normalized boundsxN,yN∈[0,1] если line не может bleedclamp/reject по upstream policyUPSTREAM
zero lengthlengthPx ≤ εmark invalidHARD
directiondirected line имеет source/target semantic orderне сортировать endpoints автоматическиHARD
undirected canonicalizationдля cache можно привести P0/P1 к детерминированному порядкуlexicographic sort only if undirectedIMPLEMENTATION
very short lineгеометрически valid, но render может быть бессмысленнымпередать в Compo 2.07DEFERRED

Что этот модуль НЕ решает

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.