COMPO · 2.04 · CURVE GEOMETRY

Кривые
линии.

Модуль переводит кривую из “пластического жеста” в вычислимую траекторию: параметризация, касательная, длина дуги, кривизна, контрольные точки, число перегибов и допустимая плавность. Он измеряет форму кривой и генерирует кандидатов, но не решает глобально, куда должен двигаться взгляд.

Контракт модуля

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
КлассУсловиеЧто гарантируетПрименение
C0end 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
curveExcess ≈ low
Низкая избыточность. Кривая близка к chord.
curveExcess = high
Большой arc-length и curvatureVariance. Не “плохо” автоматически — просто другой descriptor.
inflectionCount = 1
S-образная траектория содержит изменение знака signed curvature.
connector curve / endpoint-preserving
Connector использует curve geometry, но смысл source/target приходит из role и объектов.
C1/C2 control handles
Машина редактирует кривую через контрольные точки, а не “рисует её на глаз”.

Локальный 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 relationsendpoints + entry/exit tangentлиния должна подходить к объектам корректно
Protected zonescontrol 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.