API-контракт
00 / ownership
CONSUMES
Line
Line.role
Line.subrole?
Line.geometryType
Line.pathGeometry
Line.pathLengthPx
Line.startTangent
Line.endTangent
GeometryOwner API:
sampleAtArcLength(s)
tangentAtArcLength(s)
curvatureAtArcLength?(s)
derivative1?(t)
derivative2?(t)
OPTIONAL:
StylePreset.lineTension?
Compo2.08.flowGuideDescriptor?
PROVIDES
CurvatureProfile
InflectionDescriptor[]
TensionDescriptor
CurvatureCandidate[]
TensionCorrectionRequest[]
line.curvature.totalAbsTurn
line.curvature.meanAbsCurvature
line.curvature.maxAbsCurvature
line.curvature.curvatureConcentration
line.curvature.inflectionComplexity
line.curvature.bendingEnergy
line.curvature.tensionFit
line.curvature.localTensionScore
line.curvature.confidence
DOES NOT OWN
path geometry // 2.04/2.05
eye-flow direction // 2.08
line-shape tangency // 2.11
parallel group curvature // 2.12
line rhythm // 2.17
global motion
global hierarchy
global balance
global beauty
Архитектурная граница: кривизна — геометрический факт. «Напряжённая», «спокойная», «резкая», «плавная» линия — интерпретация этого факта относительно role/style target.
Signed curvature
01 / continuous path
Parametric curve
For r(t) = (x(t), y(t)):
κ(t) =
[x'(t)y''(t) - y'(t)x''(t)]
/
[x'(t)^2 + y'(t)^2]^(3/2)
signed κ:
sign = turn orientation
magnitude = bend intensity
DERIVED Для screen coordinates (+Y down) знак надо зафиксировать отдельно в geometry contract.
Arc-length form
If parameter = arc length s:
T(s) = unit tangent
θ(s) = tangent angle
κ(s) = dθ/ds
units:
1 / px
normalized:
κN = κ * shortSidePx
Нормализованная кривизна позволяет сравнивать композиции разных размеров.
Segment: идеальный straight segment имеет κ=0 на внутреннем интервале. Endpoint/corner singularities не надо притворно усреднять как обычную continuous curvature.
Sampling-invariant оценка
02 / arc-length integration
Equal arc-length
s_i =
i / (N-1) * L
P_i =
sampleAtArcLength(s_i)
T_i =
tangentAtArcLength(s_i)
Количество samples влияет на численную точность, но не должно менять смысл метрики.
Numerical turn
Δθ_i =
signedAngle(
T_i,
T_{i+1}
)
Δs_i =
s_{i+1} - s_i
κ_i ≈
Δθ_i / Δs_i
Integration
∫ f(s) ds
≈
Σ 0.5 *
(f_i + f_{i+1}) *
Δs_i
normalize by L
when metric should be
length-independent
Не использовать: простое среднее mean(|Δθ_i|) как меру напряжения — оно искусственно уменьшается при увеличении числа samples. Используем интегральные или density-normalized величины.
Total turn
03 / accumulated bending
Signed total turn
signedTurn =
∫ κ(s) ds
≈
unwrap(
θ_end - θ_start
)
for simple smooth path
without branch ambiguity
Показывает net rotation tangent.
Total absolute turn
totalAbsTurn =
∫ |κ(s)| ds
≈ Σ |Δθ_i|
units:
radians or degrees
В отличие от signed turn, не отменяет повороты противоположных знаков.
Turn density
turnDensity =
totalAbsTurn /
pathLengthPx
normalized:
turnDensityN =
turnDensity *
shortSidePx
Отделяет «длинная плавная дуга» от «короткого резкого изгиба».
Точки перегиба
04 / signed curvature structure
Canonical detection
candidate inflection:
κ(s_i) and κ(s_{i+1})
have opposite reliable signs
requirements:
abs(κ) must exceed
noise floor on both sides
refine zero crossing
between samples
Нулевая кривизна на длинном straight plateau не является автоматически inflection.
Descriptor
InflectionDescriptor {
sPx,
q01,
positionN,
signBefore,
signAfter,
strengthBefore,
strengthAfter,
confidence
}
Количество перегибов — descriptor; больше перегибов не значит хуже.
inflectionComplexity descriptor may include:
inflectionCount
inflectionsPerPath
meanSpacingBetweenInflections
minSpacingBetweenInflections
signRunLengths[]
curvatureSignEntropy?
AESTHETIC INTERPRETATION:
requires target profile.
Bending energy
05 / curvature intensity
Raw energy
E =
∫ κ(s)^2 ds
units:
1 / px
higher E:
more concentrated or
stronger bending
Length-normalized
meanKappa2 =
(1/L) *
∫ κ(s)^2 ds
RMS curvature =
sqrt(meanKappa2)
Удобно сравнивать линии разной длины.
Scale-normalized
κN = κ * shortSide
meanKappa2N =
mean(κN^2)
dimensionless
for normalized canvas
Energy ≠ beauty: высокое bending energy может быть у выразительной каллиграфической линии и у случайно ломаного path. 2.18 измеряет интенсивность и profile fit, а не «качество» само по себе.
Curvature concentration
06 / where the bend lives
Energy distribution
e(s) = κ(s)^2
E_total = ∫e ds
window energy:
E_w(q) =
∫window(q) e ds
peakShare =
max_q(E_w(q)) /
E_total
Большой peakShare означает, что изгиб сосредоточен в небольшой части path.
Concentration descriptor
curvatureConcentration:
peakShare
top10ArcShare?
energyCentroidQ
energySpreadQ
NOT:
generic penalty
Нужен для различения «мягко распределённой дуги» и «почти прямой линии с одним резким kink».
Напряжение как target profile
07 / interpretation
| Tension profile | Типичные геометрические признаки | Важно |
CALM | низкая RMS curvature, малая concentration, мало abrupt sign changes | не означает «лучше» |
FLOWING | умеренная distributed curvature, smooth sign runs, controlled inflections | может иметь большой total turn |
TAUT | малый spatial detour, заметный directional bend, небольшое число сильных участков | role/style dependent |
ENERGETIC | высокая turn density, несколько сильных curvature events | не путать с шумом |
ANGULAR | для polyline: corner turns вместо continuous κ | обрабатывается дискретной моделью |
FREE | нет эстетического target | только descriptors |
TensionTarget {
profile,
totalAbsTurnRange?,
turnDensityRange?,
rmsCurvatureRange?,
concentrationRange?,
inflectionCountRange?,
maxCurvatureRange?,
cornerTurnProfile?,
provenance:
STYLE_PRESET |
SEMANTIC |
EMPIRICAL |
HEURISTIC
}
tensionFit =
fit(
measured curvature descriptors,
target ranges
)
Polyline / corners
08 / discrete curvature
Corner turn
for vertex i:
u = unit(P_i-P_{i-1})
v = unit(P_{i+1}-P_i)
cornerTurn_i =
signedAngle(u,v)
Corner density
totalAbsCornerTurn =
Σ |cornerTurn_i|
cornerTurnDensity =
totalAbsCornerTurn /
pathLengthPx
No fake smoothing
polyline corner:
curvature is
distribution-like singularity
DO NOT invent
finite smooth κ
unless geometry owner
explicitly rounds corner
Local tension scoring
09 / ownership
OWNS
line.curvature.totalAbsTurn
line.curvature.meanAbsCurvature
line.curvature.maxAbsCurvature
line.curvature.curvatureConcentration
line.curvature.inflectionComplexity
line.curvature.bendingEnergy
line.curvature.tensionFit
line.curvature.localTensionScore
line.curvature.confidence
DOES NOT OWN
line.flow.localCueStrength
line.flow.reversalPenalty
line.length.roleLengthFit
line.rhythm.localRhythmScore
global motion
global balance
global hierarchy
global beauty / elegance
if explicit TensionTarget exists:
localTensionScore =
weightedMeanApplicable([
tensionFit,
curvatureContinuityFit?,
inflectionProfileFit?
])
if no TensionTarget:
localTensionScore = N/A
return curvature descriptors only
raw:
totalAbsTurn
bendingEnergy
maxCurvature
MUST NOT be treated as
monotonic quality signals.
Связь с 2.08: 2.18 может экспортировать sampling-invariant descriptors, которые 2.08 позже сможет consume при corrective pass. Но 2.18 не принимает решение о preferred eye-flow direction.
Correction requests
10 / controlled geometry change
SMOOTH
Снизить локальную curvature concentration.
INCREASE BEND
Добавить изгиб, если target profile требует большей динамики.
MOVE INFLECTION
Сместить перегиб в разрешённом диапазоне.
ROUND CORNER
Попросить geometry owner заменить дискретный corner на smooth transition.
TensionCorrectionRequest {
lineId,
reasonCode:
CURVATURE_TOO_LOW_FOR_TARGET |
CURVATURE_TOO_HIGH_FOR_TARGET |
CURVATURE_OVERCONCENTRATED |
INFLECTION_PROFILE_MISMATCH |
ABRUPT_CORNER_NOT_ALLOWED |
EXCESSIVE_MICRO_WOBBLE,
targetOwner:
"Compo2.04" |
"Compo2.05" |
"GLOBAL_SOLVER" |
"STYLE_PRESET",
requestedChange,
affectedArcRange?,
allowedRange?,
confidence
}
Визуальная лаборатория
11 / curvature cases
Детерминированные тесты
12 / regression
TEST 01 · straight
segment:
(0,0)→(100,0)
expected:
totalAbsTurn=0
bendingEnergy=0
TEST 02 · quarter circle
radius R
arc angle=π/2
κ=1/R
expected:
totalAbsTurn=π/2
E=(π/2)/R
TEST 03 · semicircle
radius R
arc angle=π
expected:
totalAbsTurn=π
turnDensity=1/R
TEST 04 · S-curve
signed curvature
changes + → -
expected:
>=1 reliable inflection
when noise threshold met
TEST 05 · sampling invariance
same circular arc
N=16 and N=128
expected:
totalAbsTurn
approximately equal
within tolerance
TEST 06 · polyline corner
segments:
east → south
expected:
abs cornerTurn=90°
no fake finite κ
TEST 07 · no target
valid curve
no TensionTarget
expected:
descriptors returned
localTensionScore=N/A
TEST 08 · no eye-flow leak
reverse path direction
expected:
abs curvature metrics
stable
preferred eye-flow
not decided here
TEST 09 · scale normalization
same normalized circle
canvas scale ×2
expected:
raw κ halves
κN remains stable
DATA FOR LAYOUT ENGINE
13 / machine contract
{
"module": "Compo 2.18",
"name": "line_curvature_and_tension",
"version": "1.0",
"scope": "LOCAL_CURVATURE_AND_TENSION",
"consumes": [
"Compo2.00.Line",
"Compo2.00.LinePathGeometry",
"GeometryOwner.sampleAtArcLength",
"GeometryOwner.tangentAtArcLength",
"GeometryOwner.curvatureAtArcLength?",
"GeometryOwner.derivative1?",
"GeometryOwner.derivative2?",
"Compo2.08.flowGuideDescriptor?",
"StylePreset.lineTension?"
],
"provides": [
"CurvatureProfile",
"InflectionDescriptor[]",
"TensionDescriptor",
"CurvatureCandidate[]",
"TensionCorrectionRequest[]"
],
"owns_metrics": [
"line.curvature.totalAbsTurn",
"line.curvature.meanAbsCurvature",
"line.curvature.maxAbsCurvature",
"line.curvature.curvatureConcentration",
"line.curvature.inflectionComplexity",
"line.curvature.bendingEnergy",
"line.curvature.tensionFit",
"line.curvature.localTensionScore",
"line.curvature.confidence"
],
"descriptors_only": [
"signedTotalTurn",
"turnDensity",
"rmsCurvature",
"inflectionCount",
"inflectionPositions[]",
"energyCentroidQ",
"energySpreadQ",
"cornerTurns[]"
],
"metric_policy": {
"primary_vote": "line.curvature.localTensionScore",
"raw_curvature": "DESCRIPTOR_NOT_MONOTONIC_QUALITY",
"target_required_for_aesthetic_fit": true,
"no_target": "RETURN_DESCRIPTORS_AND_NA_SCORE",
"sampling": "ARC_LENGTH_INTEGRATION",
"components": "diagnostic_only_by_default"
},
"tension_profiles": [
"CALM",
"FLOWING",
"TAUT",
"ENERGETIC",
"ANGULAR",
"FREE"
],
"initial_heuristics": {
"curvature_noise_floor_shortN_inv": {
"value": 0.02,
"source": "INITIAL_HEURISTIC"
},
"inflection_min_separation_shortN": {
"value": 0.03,
"source": "INITIAL_HEURISTIC"
},
"curvature_concentration_window_q": {
"value": 0.10,
"source": "IMPLEMENTATION_HEURISTIC"
},
"minimum_samples": {
"value": 32,
"source": "IMPLEMENTATION_HEURISTIC"
}
},
"hard_rules": [
"continuous_curve_curvature_must_use_derivatives_or_arc_length_consistent_numerics",
"mean_delta_angle_must_not_be_used_as_sampling_dependent_tension_metric",
"total_abs_turn_must_be_integrated_or_accumulated_independent_of_sample_count",
"inflection_requires_reliable_signed_curvature_change_not_zero_plateau",
"polyline_corners_must_not_be_faked_as_finite_smooth_curvature",
"raw_curvature_or_energy_must_not_be_treated_as_monotonically_better_or_worse",
"no_TensionTarget_means_no_universal_aesthetic_tension_score",
"preferred_eye_flow_direction_remains_owned_by_2.08",
"path_geometry_changes_must_be_requested_from_geometry_owner"
],
"deferred_inputs": [
"global.motion",
"global.eyeFlow",
"global.hierarchyScore",
"global.balanceScore",
"global.styleCoherence"
],
"recompute_when": [
"LinePathGeometry changes",
"curve control geometry changes",
"polyline vertices change",
"canvas normalization scale changes",
"role/subrole changes",
"StylePreset.lineTension changes"
],
"finality": "LOCAL_CURVATURE_TENSION_ONLY"
}
ARCHITECTURE CHECKPOINT: 2.18 вводит sampling-invariant curvature descriptors и отделяет геометрическое напряжение от eye-flow. Следующий Compo 2.19 — автоматический генератор линейной структуры: он должен собирать кандидатов из уже существующих line modules, а не изобретать заново их метрики.