Контракт модуля
01 · machine interface
CONSUMES
Line.role
Line.directed
Curve.type
Curve.P0, P1
Curve.C1?, C2?
Curve.center?, radius?, angles?
Canvas.widthPx
Canvas.heightPx
StylePreset?
PROVIDES
Curve.arcLengthPx
Curve.arcLengthDiagN
Curve.tangentSamples[]
Curve.curvatureSamples[]
Curve.signedCurvatureSamples[]
Curve.maxCurvature
Curve.meanAbsCurvature
Curve.inflectionCount
Curve.turningAngleAbs
Curve.continuityClass
Curve.candidates[]
DEFERRED INPUTS
eye_flow_score
global_balance_score
negative_space_score
hierarchy_score
object_connection_score
protected_zone_route_score
Геометрия измеряется здесь; композиционная полезность — нет.
Правило ownership: модуль 2.04 владеет только локальной геометрией кривой. Он не имеет права сам объявлять кривую “динамичной”, “мягкой”, “элегантной” или “удачной”. Такие оценки должны раскладываться на измеримые признаки и/или принадлежать другим модулям.
Типы кривых
02 · curve taxonomy
Q
quadratic Bézier
2 endpoints + 1 control point.
B(t)=(1-t)²P0 + 2(1-t)tC + t²P1
C
cubic Bézier
2 endpoints + 2 control points.
B(t)=(1-t)³P0 + 3(1-t)²tC1 + 3(1-t)t²C2 + t³P1
A
circular arc
Центр, радиус, start/end angle.
P(φ)=center + r(cosφ,sinφ)
S
spline/path
Несколько сегментов с правилами continuity.
segments[0..n-1]
join constraints
Для первого production-движка достаточно поддержать cubic Bézier + circular arc. Остальные виды можно приводить к кусочно-кубическим сегментам.
Кубическая Bézier
03 · deterministic geometry
Положение
B(t) =
(1-t)^3 P0
+ 3(1-t)^2 t C1
+ 3(1-t)t^2 C2
+ t^3 P1
t ∈ [0,1]
DERIVED Положение любой точки кривой определяется однозначно.
Касательная
B'(t) =
3(1-t)^2(C1-P0)
+ 6(1-t)t(C2-C1)
+ 3t^2(P1-C2)
T(t)=normalize(B'(t))
Касательная задаёт локальное направление в каждой точке.
Entry direction
T0 = normalize(C1-P0)
Exit direction
T1 = normalize(P1-C2)
Chord
chord = P1-P0
chordLength = |P1-P0|
Используется для оценки “избыточности” изгиба.
Кривизна как число
04 · signed + absolute curvature
Signed curvature
κs(t) =
(x'(t)y''(t)-y'(t)x''(t))
/
(x'(t)^2+y'(t)^2)^(3/2)
DERIVED Знак нужен для определения стороны изгиба и inflection.
Absolute descriptors
κ(t) = abs(κs(t))
maxCurvature = max κ(t_i)
meanAbsCurvature = mean κ(t_i)
curvatureVariance = var κ(t_i)
turningAngleAbs = Σ |Δ angle(T_i)|
Сэмплирование выполняется в aspect-corrected pixel-space.
INFLECTION COUNT
samples = signedCurvatureSamples
ignore |κs| < curvatureEpsilon
count sign changes between consecutive non-zero samples
inflectionCount = number_of_sign_changes
Не использовать только |κ| для inflection. Абсолютная кривизна теряет знак. Для сравнения масштаба хранить также κN = κ × min(W,H).
Длина дуги
05 · path length
L
arc length
L = ∫₀¹ |B'(t)| dt
Для cubic Bézier обычно считается численно.
R
excess ratio
curveExcess = arcLength / chordLength
1.0 = почти прямая; больше 1 = более длинный маршрут.
Δ
deviation
maxDeviation = max distance(B(t), chord)
Полезно для оценки силы изгиба без семантических слов.
IMPLEMENTATION HEURISTIC Для первого движка arc length можно считать adaptive subdivision или 32–128 samples. Точность рендера и scoring не требуют аналитического интеграла.
Curve → LinePathGeometry
05b · canonical adapter
Line.pathGeometry = {
geometryType: "curve",
pathLengthPx: Curve.arcLengthPx,
pathLengthShortN: Curve.arcLengthPx / min(W,H),
pathLengthDiagN: Curve.arcLengthPx / diagonalPx,
chordLengthPx: distance(P0,P1),
boundsN: sampledCurveBounds,
spatialExtentX_N: boundsWidthPx / min(W,H),
spatialExtentY_N: boundsHeightPx / min(W,H),
spatialExtentShortN: max(boundsWidthPx,boundsHeightPx)/min(W,H),
startPointN: P0,
endPointN: P1,
startTangent: T(0),
endTangent: T(1)
}
Downstream-модули 2.06+ должны использовать этот интерфейс вместо прямой зависимости от Curve.arcLength*.
Continuity между сегментами
06 · join rules
| Класс | Условие | Что гарантирует | Применение |
C0 | end A = start B | геометрическая непрерывность позиции | минимум для любого составного path |
G1 | касательные коллинеарны | визуально плавный переход направления | большинство декоративных траекторий |
C1 | первые производные равны | равная скорость параметризации на стыке | более строгие кривые |
C2 | вторые производные равны | плавная кривизна | редко нужен как hard constraint |
joinAngle = angleBetween(T_left, T_right)
if role in [trajectory, connector, annotation]:
prefer joinAngle ≤ joinSoftTau
if role == strict_curve_axis:
require joinAngle = 0
joinSoftTau — HEURISTIC/STYLE_PRESET. Модуль измеряет joinAngle точно, но допустимость конкретного значения зависит от роли.
Candidate generation
07 · controlled curve search
Control-point offsets
C1' = C1 + n * δ1
C2' = C2 + n * δ2
δ ∈ bounded range
Меняет изгиб, сохраняя endpoints.
Tangent-preserving
C1' = P0 + T0 * h1
C2' = P1 - T1 * h2
Меняет “ручки”, сохраняя входное и выходное направление.
Arc substitution
if curvatureVariance low:
propose circularArcApprox
Упрощает почти круговую кривую.
candidate generation rules:
1. preserve P0/P1 unless upstream allows reroute
2. generate bounded C1/C2 offsets
3. reject self-intersection if role forbids it
4. reject protected-zone collisions
5. recompute curvature + arc length
6. local score
7. forward candidates to global solver
Визуальная лаборатория
08 · measurable examples
Локальный scoring
09 · metric ownership
Собственные метрики
curveValidity
continuityFit
curvatureRoleFit
arcExcessFit
controlHandleCost
selfIntersectionPenalty
Все они относятся только к локальной геометрии кривой.
Не вычислять здесь
eye_flow_score // DEFERRED
global_balance_score // DEFERRED
negative_space_score // DEFERRED
hierarchy_score // DEFERRED
semantic_connection_score // DEFERRED
localScore =
w_valid * curveValidity
+ w_cont * continuityFit
+ w_curv * curvatureRoleFit
+ w_excess* arcExcessFit
- w_handle* controlHandleCost
- w_self * selfIntersectionPenalty
// weights = STYLE_PRESET / engine defaults
// localScore != final composition score
Context interactions
10 · override graph
| Модуль | Может изменить | Причина |
| Connector / object relations | endpoints + entry/exit tangent | линия должна подходить к объектам корректно |
| Protected zones | control points / route | обход лица, текста, товара, важного контента |
| Eye-flow | кривизну / направление | если путь должен вести между semantic anchors |
| Negative space | амплитуду изгиба | чтобы path не разрушал спокойную область |
| Hierarchy | толщину / контраст | геометрия остаётся, визуальный вес меняется другим модулем |
PASS ORDER
1. validate curve schema
2. sample aspect-corrected geometry
3. compute arc length + tangents + curvature
4. classify bends / inflections / continuity
5. generate bounded control-point candidates
6. hard-filter self-intersections / protected zones
7. local score
8. request DEFERRED context metrics
9. global solver accepts / bends / reroutes / rejects
10. recompute all derived curve descriptors
Тесты
11 · deterministic cases
TEST 01
type = cubicBezier
P0=(0.1,0.5)
C1=(0.3,0.5)
C2=(0.7,0.5)
P1=(0.9,0.5)
expected:
maxCurvature ≈ 0
inflectionCount = 0
curveExcess ≈ 1
TEST 02
type = cubicBezier
P0=(0.1,0.7)
C1=(0.2,0.1)
C2=(0.8,0.9)
P1=(0.9,0.3)
expected:
inflectionCount >= 1
arcLength > chordLength
TEST 03
segment A end = segment B start
joinAngle = 18°
role = strict_curve_axis
expected:
continuityFit < 1
repair proposal = true
DATA FOR LAYOUT ENGINE
12 · implementation block
{
"module": "Compo 2.04",
"name": "curved_lines",
"owner_of": [
"curve.arcLengthPx",
"curve.arcLengthDiagN",
"curve.tangentSamples",
"curve.curvatureSamples",
"curve.signedCurvatureSamples",
"curve.maxCurvature",
"curve.meanAbsCurvature",
"curve.curvatureVariance",
"curve.inflectionCount",
"curve.turningAngleAbs",
"curve.continuityClass",
"curve.joinAngleDeg",
"curve.curveExcess",
"curve.maxDeviationFromChord"
],
"consumes": [
"Compo0.Canvas",
"Compo0.ProtectedZones",
"Compo2.00.Line.role",
"Curve.type",
"Curve.P0",
"Curve.P1",
"Curve.C1?",
"Curve.C2?",
"Curve.segmentList?"
],
"provides": [
"Curve.geometryDescriptor",
"Curve.curvatureDescriptor",
"Curve.continuityDescriptor",
"Curve.candidates",
"Line.pathGeometry"
],
"provenance": {
"geometry_descriptors": "DERIVED",
"sample_count": "IMPLEMENTATION_HEURISTIC",
"joinSoftTau": "STYLE_PRESET_OR_HEURISTIC",
"curvatureEpsilon": "IMPLEMENTATION_HEURISTIC"
},
"initial_heuristics": {
"sample_count_min": 32,
"sample_count_max": 128,
"control_point_offset_limit_shortSideN": 0.12,
"joinSoftTauDeg": 8
},
"hard_rules": [
"finite_curve_parameters",
"nonzero_path_length",
"C0 continuity required for joined visible path",
"protected zones respected unless role allows crossing"
],
"deferred_inputs": [
"eye_flow_score",
"global_balance_score",
"negative_space_score",
"hierarchy_score",
"semantic_connection_score"
],
"recompute_when": [
"P0 changes",
"P1 changes",
"C1 changes",
"C2 changes",
"canvas changes",
"segment topology changes"
],
"finality": "LOCAL_PROPOSAL_ONLY"
}
MIGRATION 2.00–2.07 / pre-2.08: canonical LinePathGeometry, role/subrole/rigidity, metric ownership cleanup, path-length vs spatial-span separation.