API-контракт
00 / ownership
CONSUMES
Line[]
Line.role
Line.subrole?
Line.pathGeometry
Line.dashArray?
Line.dashOffset?
Line.strokeWidthPx?
OPTIONAL:
Compo2.12.ParallelGroupDescriptor
Compo2.12.offsets[]
Compo2.12.gaps[]
Compo2.15.MotifGroupDescriptor
Compo2.15.raw gaps/order
StylePreset.lineRhythm?
PROVIDES
StrokePatternDescriptor
LineRhythmDescriptor
RhythmModelFit[]
RhythmEvent[]
RhythmCandidate[]
RhythmCorrectionRequest[]
line.rhythm.patternFit
line.rhythm.phaseCoherence
line.rhythm.intervalModelFit
line.rhythm.variationFit
line.rhythm.localRhythmScore
line.rhythm.confidence
DOES NOT OWN
line visual weight // 2.06
parallel grouping geometry // 2.12
graphic grammar // 2.15
line color // 2.16
curvature tension // 2.18
global composition rhythm // Topic 26
global hierarchy/balance
global eye-flow
Два уровня: 2.17 работает с intra-line rhythm (dash/gap вдоль path) и local inter-line rhythm (интервалы внутри уже определённой line/motif group). Общий ритм всей композиции остаётся отдельной глобальной темой.
Stroke pattern
01 / pattern along arc length
Canonical pattern
StrokePatternDescriptor {
dashArrayPx[],
normalizedDashArray[],
cycleLengthPx,
dashOffsetPx,
phase01,
inkDutyCycle,
segmentCountPerCycle,
patternClass,
confidence
}
Cycle length
C =
Σ pattern_i
if CSS-like dash list
has odd number of values:
duplicate list once
before canonicalization
После canonicalization цикл всегда имеет чёткое dash/gap чередование.
Duty cycle
inkDutyCycle =
Σ dashLengths /
cycleLength
dash indices:
0,2,4,...
gap indices:
1,3,5,...
inkDutyCycle — descriptor. 2.06 может потреблять его при пересчёте effective visual weight.
normalizePattern(pattern):
1. reject negative values
2. if empty → SOLID
3. if odd length → concatenate(pattern, pattern)
4. C = sum(pattern)
5. if C <= eps → INVALID / SOLID_BY_RENDERER_POLICY
6. normalized_i = pattern_i / C
IMPORTANT:
pattern proportions are independent
from absolute cycle size.
[20,10] and [40,20]
have same normalized pattern,
but different spatial frequency.
Phase и синхронизация
02 / cyclic alignment
Normalized phase
phasePx =
positiveModulo(
dashOffsetPx,
cycleLengthPx
)
phase01 =
phasePx /
cycleLengthPx
Знак dashOffset должен следовать renderer contract. Внутри 2.17 хранится одна каноническая convention.
Circular distance
d =
abs(phaseA - phaseB)
phaseDistance =
min(d, 1-d)
phaseFit =
1 - clamp(
phaseDistance / phaseTau,
0,1
)
DERIVED + HEURISTIC τ Учитывает wrap-around: 0.98 и 0.02 близки, а не далеки.
Phase applicability: сравнивать phase имеет смысл только у совместимых pattern families. Для разных cycle structures phaseCoherence=N/A, если StylePreset не определяет явное отношение.
Конец path и обрезанный цикл
03 / terminal rhythm
Cycle remainder
usable =
pathLengthPx + phaseShiftPx
remainder =
positiveModulo(
usable,
cycleLengthPx
)
Terminal fragment
evaluate final visible
dash/gap fragment
terminalFragmentRatio =
fragmentLength /
nominalSegmentLength
Очень маленький случайный фрагмент может выглядеть как rendering artifact.
Repair options
SHIFT_PHASE
SCALE_CYCLE
FIT_INTEGER_CYCLES
KEEP_AS_IS
selected by
StylePreset policy
Не принуждать: «целое число циклов на path» — не универсальная норма. Это один из style modes. Иногда намеренно обрезанный dash создаёт нужное напряжение.
Ритм группы линий
04 / ordered interval sequence
Input order
preferred:
Compo2.12 sorted offsets[]
gaps_i =
offset[i+1] - offset[i]
or:
Compo2.15 motif order
+ raw positions
2.17 must not
rebuild group membership.
Normalize gaps
scale =
median(gaps)
gapN_i =
gaps_i / scale
if median <= eps:
invalid / overlapping
defer geometry repair
Нормализация позволяет сравнивать ритм при равномерном масштабировании группы.
LineRhythmDescriptor {
groupId,
orderedMemberIds[],
gapsPx[],
gapsN[],
medianGapPx,
robustGapSpread,
modelFits[],
selectedModel,
variationEvents[],
phaseCoherence?,
localRhythmScore,
confidence
}
Модели ритма
05 / model fitting
A A A
regular
g_i ≈ μ
error =
robustResidual(
g_i - μ
)
A B A
alternating
g_even ≈ a
g_odd ≈ b
period = 2
residual normalized
by median gap
↗
progressive
linear:
g_i ≈ a + b*i
optional geometric:
log g_i ≈ a+b*i
if g_i > 0
≋
controlled irregular
target distribution /
bounded deviations /
explicit event pattern
requires preset
or semantic intent
RhythmModelFit {
model:
REGULAR |
ALTERNATING |
PROGRESSIVE_LINEAR |
PROGRESSIVE_GEOMETRIC |
CONTROLLED_IRREGULAR |
FREE,
normalizedResidual,
fitScore,
parameters,
applicability,
complexity,
confidence
}
selectedModel =
best applicable model
under StylePreset / semantic target.
Lowest residual alone
does NOT define aesthetic preference.
Вариация и акцент
06 / deviation as event
Baseline residual
r_i =
observed_i -
predicted_i
rN_i =
r_i /
medianGap
Residual показывает отклонение от выбранной модели.
Rhythm event
RhythmEvent {
index,
type:
ACCENT_GAP |
COMPRESSED_GAP |
PHASE_SHIFT |
PATTERN_CHANGE,
magnitude,
intentional?,
provenance
}
Variation budget
StylePreset:
maxEvents?
eventMagnitudeRange?
allowedIndices?
periodicBreaks?
randomBudget?
variationFit =
fit(events,target)
Отклонение ≠ ошибка: единичный большой gap может быть syncopation/accent, а не дефект. Без style/semantic target 2.17 обязан хранить его как descriptor/event и снижать уверенность в интерпретации, а не автоматически штрафовать.
Pattern frequency
07 / absolute vs normalized rhythm
Normalized structure
[20,10] → [0.667,0.333]
[40,20] → [0.667,0.333]
same proportions
Absolute frequency
cyclesPer100Px =
100 / cycleLengthPx
cyclesPerPath =
pathLengthPx /
cycleLengthPx
Scale-relative
cycleShortN =
cycleLengthPx /
canvasShortSidePx
or relative to:
strokeWidthPx
font size
module size
Reference scale должен быть указан явно.
Local rhythm scoring
08 / ownership
OWNS
line.rhythm.patternFit
line.rhythm.phaseCoherence
line.rhythm.intervalModelFit
line.rhythm.variationFit
line.rhythm.localRhythmScore
line.rhythm.confidence
descriptors:
cycleLengthPx
normalizedDashArray[]
inkDutyCycle
gapsPx[]
gapsN[]
modelFits[]
RhythmEvent[]
DOES NOT OWN
line.visualWeight
line.parallel.localGroupCoherence
line.graphic.familyConsistency
line.color.localColorScore
line.curvature.tension
global.compositionRhythm
global.hierarchy
global.balance
global.density
localRhythmScore =
weightedMeanApplicable([
patternFit?,
phaseCoherence?,
intervalModelFit?,
variationFit?
])
BUT:
each component must be compared
to an explicit target profile.
If no target rhythm exists:
return descriptors
localRhythmScore = N/A
OR score only hard functional consistency
explicitly requested by role.
Главное: 2.17 измеряет «насколько последовательность соответствует выбранной ритмической модели», а не «насколько она регулярна». Это защищает генератор от превращения всех композиций в механически равномерные.
Correction requests
09 / controlled changes
ADJUST GAP
Сдвинуть одну/несколько линий в допустимом диапазоне.
ADJUST PHASE
Синхронизировать dash phase внутри совместимой группы.
ADJUST CYCLE
Изменить dash/gap proportions или absolute cycle length.
INSERT EVENT
Добавить осознанный break/accent только если это разрешено target profile.
RhythmCorrectionRequest {
targetIds[],
reasonCode:
PATTERN_OUT_OF_TARGET |
PHASE_MISMATCH |
INTERVAL_MODEL_MISMATCH |
UNINTENTIONAL_OUTLIER |
TERMINAL_FRAGMENT_TOO_SMALL |
MISSING_VARIATION_EVENT,
targetOwner:
"LINE_STYLE" |
"Compo2.12" |
"Compo2.15" |
"GLOBAL_SOLVER" |
"STYLE_PRESET",
requestedChange,
allowedRange?,
confidence
}
Визуальная лаборатория
10 / rhythm cases
Детерминированные тесты
11 / regression
TEST 01 · odd dash list
input=[10,5,2]
canonical:
[10,5,2,10,5,2]
expected:
cycle=34
TEST 02 · duty cycle
pattern=[20,10]
expected:
cycle=30
inkDutyCycle=2/3
TEST 03 · phase wrap
phaseA=.98
phaseB=.02
expected:
circularDistance=.04
not .96
TEST 04 · regular
gaps=[50,50,50,50]
expected:
regular residual=0
TEST 05 · alternating
gaps=[40,60,40,60,40]
expected:
period-2 residual=0
regular residual>0
TEST 06 · progressive
gaps=[45,60,75,90,105]
expected:
linear fit:
slope=15
residual≈0
TEST 07 · scale invariance
gaps A=[20,40,20]
gaps B=[40,80,40]
expected:
same normalized
interval pattern
TEST 08 · no target
valid irregular gaps
no style target
expected:
descriptors returned
localRhythmScore=N/A
TEST 09 · ownership
change line color only
expected:
2.17 rhythm metrics
unchanged
DATA FOR LAYOUT ENGINE
12 / machine contract
{
"module": "Compo 2.17",
"name": "line_rhythm_and_stroke_pattern",
"version": "1.0",
"scope": "LOCAL_LINE_RHYTHM",
"consumes": [
"Compo2.00.Line[]",
"Compo2.00.LinePathGeometry",
"Line.dashArray?",
"Line.dashOffset?",
"Line.strokeWidthPx?",
"Compo2.12.ParallelGroupDescriptor?",
"Compo2.12.offsets[]?",
"Compo2.12.gaps[]?",
"Compo2.15.MotifGroupDescriptor?",
"Compo2.15.rawGaps?",
"StylePreset.lineRhythm?"
],
"provides": [
"StrokePatternDescriptor",
"LineRhythmDescriptor",
"RhythmModelFit[]",
"RhythmEvent[]",
"RhythmCandidate[]",
"RhythmCorrectionRequest[]"
],
"rhythm_models": [
"REGULAR",
"ALTERNATING",
"PROGRESSIVE_LINEAR",
"PROGRESSIVE_GEOMETRIC",
"CONTROLLED_IRREGULAR",
"FREE"
],
"owns_metrics": [
"line.rhythm.patternFit",
"line.rhythm.phaseCoherence",
"line.rhythm.intervalModelFit",
"line.rhythm.variationFit",
"line.rhythm.localRhythmScore",
"line.rhythm.confidence"
],
"descriptors_only": [
"cycleLengthPx",
"normalizedDashArray[]",
"inkDutyCycle",
"phase01",
"cyclesPerPath",
"gapsPx[]",
"gapsN[]",
"modelFits[]",
"RhythmEvent[]",
"terminalFragmentRatio"
],
"metric_policy": {
"primary_vote": "line.rhythm.localRhythmScore",
"regularity": "NOT_MONOTONICALLY_BETTER",
"target_required_for_aesthetic_fit": true,
"no_target": "RETURN_DESCRIPTORS_AND_NA_SCORE",
"components": "diagnostic_only_by_default"
},
"initial_heuristics": {
"phase_tau": {
"value": 0.08,
"source": "INITIAL_HEURISTIC"
},
"terminal_fragment_min_ratio": {
"value": 0.20,
"source": "STYLE_DEPENDENT_INITIAL_HEURISTIC"
},
"regular_residual_tau": {
"value": 0.12,
"source": "INITIAL_HEURISTIC"
},
"variation_event_threshold_mad": {
"value": 2.5,
"source": "IMPLEMENTATION_HEURISTIC"
}
},
"hard_rules": [
"odd_dash_array_must_be_canonicalized_before_cycle_analysis",
"dash_pattern_values_must_not_be_negative",
"phase_distance_must_be_circular",
"group_membership_must_be_consumed_from_group_owner_not_rebuilt_here",
"rhythm_model_fit_must_not_assume_regular_is_best",
"no_explicit_target_means_no_universal_aesthetic_rhythm_score",
"line_color_changes_must_not_change_owned_rhythm_metrics",
"line_visual_weight_remains_owned_by_2.06",
"global_composition_rhythm_remains_outside_2.17",
"scale_normalized_and_absolute_frequency_descriptors_must_be_kept_separate"
],
"deferred_inputs": [
"Compo2.18.curvatureTension",
"global.compositionRhythm",
"global.hierarchyScore",
"global.balanceScore",
"global.densityScore",
"global.eyeFlowScore"
],
"recompute_when": [
"dashArray changes",
"dashOffset changes",
"LinePathGeometry/pathLength changes",
"2.12 group offsets or gaps change",
"2.15 motif membership/order changes",
"StylePreset.lineRhythm changes"
],
"finality": "LOCAL_LINE_RHYTHM_ONLY"
}
ARCHITECTURE CHECKPOINT: 2.17 отделяет ритм от простого равенства интервалов. Regular, alternating, progressive и controlled-irregular — разные модели, а не шкала «хорошо → плохо». Следующий Compo 2.18 должен заниматься кривизной и напряжением линии, не переоценивая уже принадлежащий 2.08 directional flow.