COMPO · 2.13 · LINE × GRID RELATIONS

Линии
и сетка.

Модуль измеряет отношение линии к уже существующей сетке: совпадение оси, расстояние до ближайшей grid-line, привязку endpoints к узлам и локальную согласованность с grid family. Он не проектирует саму сетку и не владеет глобальной оценкой alignment.

API-контракт

00 / ownership

CONSUMES

Line Line.role / subrole / rigidity Line.geometryType Line.pathGeometry Line.endpointLocks? Grid Grid.families[] Grid.axes[]? Grid.nodes[]? Grid.boundsN Grid.visibility? OPTIONAL: Compo2.12.ParallelGroupDescriptor StylePreset.lineGrid?

PROVIDES

LineGridRelation: gridId familyId? axisId? axisAngleDeviationDeg nearestOffsetPx nearestOffsetShortN projectionCoverage? sourceNodeDistancePx? targetNodeDistancePx? segmentConformity? applicability confidence Line.gridCandidates[] line.grid.axisFit line.grid.offsetFit line.grid.endpointFit line.grid.localGridFit line.grid.snapCost

DOES NOT OWN

Grid geometry/schema owner Grid node occupancy global grid quality global alignment score rhythm score // 2.17 parallel group score // 2.12 line length // 2.07 line visual weight // 2.06 global hierarchy global balance
Architecture: 2.13 оценивает линию относительно сетки. Он не создаёт собственную параллельную Grid-модель и не решает, хороша ли сама сетка для композиции.

Минимальный Grid adapter

01 / external contract

GridFamily

GridFamily { id, axisUnit, normalUnit, spacingPx?, phasePx?, finiteAxes[]?, role? }

Семейство параллельных grid-lines.

GridAxis

GridAxis { id, familyId, pointN, axisUnit, extent? }

Конкретная ось, если сетка конечная или немодульная.

GridNode

GridNode { id, pointN, familyIds[], locked? }

Точка пересечения осей / разрешённый snap-anchor.

Если будущий grid-owner использует другую внутреннюю структуру, он обязан предоставить адаптер с эквивалентными операциями: nearestAxis(), nearestNode(), familyDirection().

Совпадение направления

02 / orientation modulo 180°

Axis deviation

θL = line axis angle θG = grid family angle raw = abs(θL - θG) axisDeviation = min(raw, 180°-raw) axisFit = 1 - clamp( axisDeviation / axisTauDeg, 0,1 )

Для прямого segment используется его axis. Для curve/polyline — отдельная политика ниже.

Applicability

role=axis/grid: applicability ≈ high divider: medium/high connector: contextual accent: style-dependent trajectory: usually lower

Низкий grid-fit не является ошибкой, если роль линии не должна следовать сетке.

INITIAL HEURISTIC axisTauDeg нужен для candidate generation. Он не должен превращать все линии композиции в H/V или в одну grid family.

Расстояние до grid-line

03 / signed normal coordinate

Periodic family

n = family.normalUnit s = dot(lineRepresentativePoint, n) relative = s - phasePx k = round(relative / spacingPx) nearestGridCoordinate = phasePx + k*spacingPx signedOffsetPx = s - nearestGridCoordinate

DERIVED Для бесконечного регулярного family.

Offset fit

absOffset = abs(signedOffsetPx) offsetFit = 1 - clamp( absOffset / offsetTauPx, 0,1 )

offsetTauPx должен масштабироваться через short side / stroke / grid spacing.

Не смешивать: axisFit отвечает за угол, offsetFit — за положение относительно ближайшей оси. Параллельная линии сетки линия может иметь axisFit=1, но offsetFit≈0.

Endpoints и grid nodes

04 / attachment to intersections

Nearest node

node = Grid.nearestNode(P) dNodePx = distance(P,node.point) nodeFit = 1 - clamp( dNodePx / nodeTauPx, 0,1 )

Locks first

if endpoint.locked: do NOT snap report only: nodeDistance possible mismatch

Сетка не имеет права ломать semantic endpoint connector-а.

Endpoint fit

endpointFit = weightedMean( sourceNodeFit?, targetNodeFit? ) N/A endpoints excluded by applicability

Не каждая линия должна начинаться и заканчиваться на узлах сетки.

Для axis или divider часто важнее совпадение с grid-line, чем endpoint-to-node snap. Для connector ситуация может быть обратной.

Polyline и curve

05 / geometry-specific conformity

Polyline

for each segment: find nearest family compute axisFit compute offsetFit segmentConformity = weightedMean( segmentGridFit, weights=segmentLength ) vertexNodeFit optional

Ломаная может следовать нескольким grid families, например orthogonal H/V.

Curve

curveGridFit applicability usually low unless style explicitly requests it sample tangents: compare only if curved_grid_family exists otherwise: axisFit = N/A

Обычную Bézier-кривую нельзя штрафовать за то, что её tangent постоянно отклоняется от прямоугольной сетки.

N/A policy: неприменимость должна уменьшать applicability, а не превращаться в нулевой score. Иначе система начнёт систематически уничтожать кривые и свободные accents.

Projection coverage

06 / finite grid axes

Line projection

project line path onto grid axis u I_line=[a,b]

Axis extent

finite grid axis: I_grid=[g0,g1]

Coverage

overlap = length(intersect( I_line,I_grid )) projectionCoverage = overlap / max(length(I_line),EPS)

Полезно, чтобы line не «snapнулась» к оси, которая находится далеко по длине.

Local grid scoring

07 / metric ownership

OWNS

line.grid.axisFit line.grid.offsetFit line.grid.endpointFit line.grid.segmentConformity line.grid.projectionCoverageFit line.grid.snapCost line.grid.localGridFit line.grid.confidence

DOES NOT OWN

grid.globalQuality grid.globalAlignmentScore grid.nodeOccupancy rhythm.score parallel.localGroupCoherence global.balance global.hierarchy global.negativeSpace
localGridFit = weightedMeanApplicable([ axisFit, offsetFit, endpointFit, segmentConformity, projectionCoverageFit ]) candidateScore = localGridFit - wSnap * snapCost // applicability depends on role/subrole/style. // N/A metrics are excluded, not set to zero.
Composite-first: 2.20 должен использовать line.grid.localGridFit как основной grid-related vote. axis/offset/endpoint components — evidence по умолчанию.

Candidate generation

08 / snap requests

ROTATE

Приблизить line axis к grid family.

SHIFT

Сдвинуть параллельно normal до ближайшей оси.

SNAP ENDPOINT

Переместить свободный endpoint в grid node.

ORTHOGONALIZE

Для polyline предложить сегментам две совместимые grid families.

LineGridCandidate { lineId, gridId, familyId?, axisId?, snapMode: NONE | ROTATE | SHIFT | ROTATE_AND_SHIFT | SNAP_SOURCE | SNAP_TARGET | SNAP_BOTH | ORTHOGONALIZE, proposedTransform?, targetNodes?, localGridFit, snapCost, hardStatus, reasonCodes[] } // 2.13 generates proposal. // Geometry owner applies and recomputes LinePathGeometry.

Визуальная лаборатория

09 / grid cases
AXIS + OFFSET FIT: линия лежит прямо на grid-line.
axisFit=1, но offset=11 px. SHIFT-кандидат может быть дешёвым.
POLYLINE: сегменты следуют двум grid families, вершины лежат на nodes.
Grid family может быть повёрнутой. 2.13 работает с произвольным axisUnit, а не только H/V.
Свободная curve не получает нулевой grid-score: для обычной rectilinear grid её axis applicability может быть N/A.

Детерминированные тесты

10 / regression

TEST 01 · angle

line axis=29° grid family=30° expected: axisDeviation=1°

TEST 02 · periodic offset

spacing=50 phase=0 line coordinate=151 expected: nearest=150 signedOffset=+1

TEST 03 · wrap

line axis=179° grid axis=1° expected: axisDeviation=2° not 178°

TEST 04 · locked endpoint

source.locked=true nearestNodeDistance=2 expected: REPORT_ONLY must not snap source

TEST 05 · N/A curve

curve rectilinear grid style.followGrid=false expected: axisFit=N/A not 0

TEST 06 · no grid ownership

Grid.spacing changes expected: 2.13 recomputes relation MUST NOT: rewrite Grid itself

TEST 07 · orthogonal polyline

segment angles=[0,90,0] grid families=[0,90] expected: segmentConformity=high

TEST 08 · offset independent

parallel to grid offset=large expected: axisFit=1 offsetFit low

TEST 09 · no global leak

change: global.balanceScore expected: LineGridRelation UNCHANGED

LIVE REFERENCE TESTS

IDInputComputedStatus

DATA FOR LAYOUT ENGINE

11 / machine contract
{ "module": "Compo 2.13", "name": "lines_and_grid", "version": "1.0", "scope": "LOCAL_LINE_GRID_RELATION", "consumes": [ "Compo2.00.Line", "Compo2.00.LinePathGeometry", "Grid", "GridFamily[]", "GridAxis[]?", "GridNode[]?", "Compo2.12.ParallelGroupDescriptor?", "StylePreset.lineGrid?" ], "provides": [ "LineGridRelation", "Line.gridCandidates[]" ], "owns_metrics": [ "line.grid.axisFit", "line.grid.offsetFit", "line.grid.endpointFit", "line.grid.segmentConformity", "line.grid.projectionCoverageFit", "line.grid.snapCost", "line.grid.localGridFit", "line.grid.confidence" ], "metric_policy": { "primary_vote": "line.grid.localGridFit", "components": "diagnostic_only_by_default", "not_applicable": "EXCLUDE_FROM_WEIGHTED_MEAN", "global_grid_score": "FORBIDDEN_TO_OWN" }, "initial_heuristics": { "axis_tau_deg": {"value":3,"source":"INITIAL_HEURISTIC"}, "offset_tau_shortN": {"value":0.012,"source":"INITIAL_HEURISTIC"}, "node_tau_shortN": {"value":0.018,"source":"INITIAL_HEURISTIC"}, "projection_coverage_min": {"value":0.25,"source":"INITIAL_HEURISTIC"} }, "hard_rules": [ "grid_geometry_must_come_from_grid_owner", "orientation_comparison_must_be_modulo_180", "axis_fit_and_offset_fit_must_remain_separate", "locked_endpoints_must_not_be_snapped", "semantic_connector_endpoints_must_not_be_overridden_by_grid", "curve_grid_nonapplicability_must_be_NA_not_zero", "foreign_geometry_changes_must_be_candidates_or_requests", "normalized_and_pixel_distances_must_not_be_mixed" ], "deferred_inputs": [ "grid.globalQuality", "grid.globalAlignmentScore", "grid.nodeOccupancy", "Compo2.17.rhythmScore", "global.balanceScore", "global.hierarchyScore", "global.negativeSpaceScore" ], "recompute_when": [ "LinePathGeometry changes", "Grid geometry changes", "GridFamily parameters change", "endpoint locks change", "Line role/subrole/rigidity changes", "StylePreset.lineGrid changes" ], "finality": "LOCAL_GRID_RELATION_ONLY" }
ARCHITECTURE CHECKPOINT: 2.13 использует результаты 2.12 и подготавливает данные для будущей общей grid-системы, но ownership сохраняется чистым: 2.12 — параллельная группа, 2.13 — локальная связь line↔grid, 2.17 — ритм интервалов, global grid owner — качество сетки целиком.