@gravity-ui/graph
Version:
Modern graph editor component
480 lines (479 loc) • 21.2 kB
JavaScript
/**
* Wheel input classification for camera routing.
*
* The library resolves **intent** (pan vs zoom), not device type. This boundary prepares
* for a future input event bus where raw DOM events (`wheel`, `pointerdown`, …) are
* normalized into semantic graph events (`camera:pan`, `camera:zoom`, …) before Camera
* handles them.
*/
/** Wheel input intent for camera routing (pan vs zoom). */
export var EWheelIntent;
(function (EWheelIntent) {
EWheelIntent["Pan"] = "pan";
EWheelIntent["Zoom"] = "zoom";
})(EWheelIntent || (EWheelIntent = {}));
/** Debug rule ids for {@link createWheelIntentResolver} (see `docs/system/wheel-intent.md`). */
export const WHEEL_INTENT_RULE = {
I1_PINCH: "I1:pinch",
I2_HORIZONTAL_OR_DIAGONAL: "I2:horizontal-or-diagonal",
I3_INPUT_DEVICE_TRACKPAD: "I3:input-device-trackpad",
I3_INTEGER_TRACKPAD: "I3:integer-trackpad",
I3_INTEGER_TRACKPAD_SLOW: "I3:integer-trackpad-slow",
I3_RAPID_SMALL: "I3:rapid-small",
I4_MOUSE_WHEEL_STEP: "I4:mouse-wheel-step",
I4_LARGE_STEP: "I4:large-step",
I4_FRACTIONAL_MOUSE: "I4:fractional-mouse",
I4_BURST_SMOOTHING: "I4-burst:smoothing",
I4_INPUT_DEVICE_MOUSE: "I4:input-device-mouse",
I5_LAST_INTENT: "I5:last-intent",
I5_STICKY_STREAM: "I5:sticky-stream",
};
const WHEEL_INTENT_I3_RULES = new Set([
WHEEL_INTENT_RULE.I3_INPUT_DEVICE_TRACKPAD,
WHEEL_INTENT_RULE.I3_INTEGER_TRACKPAD,
WHEEL_INTENT_RULE.I3_INTEGER_TRACKPAD_SLOW,
WHEEL_INTENT_RULE.I3_RAPID_SMALL,
]);
const WHEEL_INTENT_I4_RULES = new Set([
WHEEL_INTENT_RULE.I4_MOUSE_WHEEL_STEP,
WHEEL_INTENT_RULE.I4_LARGE_STEP,
WHEEL_INTENT_RULE.I4_FRACTIONAL_MOUSE,
WHEEL_INTENT_RULE.I4_BURST_SMOOTHING,
WHEEL_INTENT_RULE.I4_INPUT_DEVICE_MOUSE,
]);
/** Returns true when the rule id belongs to trackpad classification (I3). */
export function isI3WheelIntentRule(rule) {
return WHEEL_INTENT_I3_RULES.has(rule);
}
/** Returns true when the rule id belongs to mouse-wheel classification (I4). */
export function isI4WheelIntentRule(rule) {
return WHEEL_INTENT_I4_RULES.has(rule);
}
const WHEEL_INTENT_DEBUG_GLOBAL_KEY = "__graphWheelIntentDebugLogger__";
function getWheelIntentDebugLogger() {
return globalThis[WHEEL_INTENT_DEBUG_GLOBAL_KEY] ?? null;
}
function setWheelIntentDebugLogger(logger) {
globalThis[WHEEL_INTENT_DEBUG_GLOBAL_KEY] = logger;
}
function deltaModeLabel(deltaMode) {
return ["PIXEL", "LINE", "PAGE"][deltaMode] ?? String(deltaMode);
}
function formatDiagonalAxisRatio(normX, normY) {
const maxAxis = Math.max(normX, normY);
if (maxAxis < 0.001) {
return null;
}
return Math.min(normX, normY) / maxAxis;
}
function defaultDebugLogger(entry) {
const { input, session } = entry;
const summary = `[wheel-intent] ${entry.rule} → ${entry.result} | Δ(${input.deltaX.toFixed(2)}, ${input.deltaY.toFixed(2)}) ${input.deltaModeLabel} +${Math.round(session.timeSinceLastMs)}ms`;
// Stringify so logs are copy-pasteable as plain text (not expandable DevTools objects).
// eslint-disable-next-line no-console
console.log(summary);
// eslint-disable-next-line no-console
console.log(JSON.stringify(entry, null, 2));
}
/**
* Enables per-event debug logging for {@link createWheelIntentResolver}.
*
* Stored on `globalThis` so it works even when webpack loads duplicate module copies
* (e.g. Storybook preview vs story bundle).
*
* @example
* ```typescript
* import { enableWheelIntentDebug } from "@gravity-ui/graph";
* enableWheelIntentDebug(); // default console.log: summary + JSON string
* enableWheelIntentDebug(entry => myTelemetry.record(entry)); // custom logger
* enableWheelIntentDebug(null); // disable
* ```
*/
export function enableWheelIntentDebug(logger = defaultDebugLogger) {
setWheelIntentDebugLogger(logger);
if (logger !== null) {
// eslint-disable-next-line no-console
console.log("[wheel-intent] debug logging enabled");
}
}
/** After a classic mouse wheel tick, treat nearby vertical fractional events as wheel smoothing. */
const MOUSE_WHEEL_BURST_MS = 120;
/** Events closer than this are treated as a continuous stream (typical two-finger scroll / inertia). */
const RAPID_STREAM_MS = 38;
/** Both axes must contribute at least this share of the larger axis to count as diagonal pan. */
const DIAGONAL_AXIS_MIN_RATIO = 0.5;
/** Minimum absolute delta on both axes to count as diagonal scroll. */
const DIAGONAL_MIN_ABS = 2;
/** Normalized horizontal delta at or above this suggests pan (not a vertical-only wheel step). */
const MIN_HORIZONTAL_SCROLL_ABS = 2;
const SMALL_DELTA_THRESHOLD = 50;
/** Trackpad inertia: per-event normalized delta below this on both axes (I3). */
const TRACKPAD_I3_MAX_PX = 20;
/** Minimum normalized vertical delta for a discrete mouse wheel step (I4). */
const MOUSE_WHEEL_DISCRETE_MIN_PX = 20;
/** Smooth-scroll mouse notch band (slow fractional PIXEL step, e.g. Δy ≈ -4.000244). */
const MOUSE_WHEEL_NOTCH_MIN_PX = 3;
const MOUSE_WHEEL_NOTCH_MAX_PX = 5;
/** Approximate pixel equivalent of one scroll LINE (WheelEvent.deltaMode === 1). */
const LINE_TO_PIXEL_APPROX = 16;
/** Approximate pixel equivalent of one scroll PAGE (WheelEvent.deltaMode === 2). */
const PAGE_TO_PIXEL_APPROX = 600;
/**
* Converts a wheel delta value to approximate pixel units.
* WheelEvent.deltaMode: 0 = PIXEL, 1 = LINE, 2 = PAGE.
*/
function normalizeWheelDelta(delta, deltaMode) {
if (deltaMode === WheelEvent.DOM_DELTA_LINE)
return delta * LINE_TO_PIXEL_APPROX;
if (deltaMode === WheelEvent.DOM_DELTA_PAGE)
return delta * PAGE_TO_PIXEL_APPROX;
return delta;
}
/** Minimum |wheelDelta| / |wheelDeltaY| for legacy mouse-wheel detection (Chromium ≈ 120). */
const LEGACY_MOUSE_WHEEL_DELTA_MIN = 100;
/** Mac Chrome/YaBrowser trackpad: wheelDeltaY ≈ ±3 × deltaY (not fixed ±120 mouse notch). */
const MAC_CHROME_TRACKPAD_WHEEL_DELTA_RATIO = 3;
const MAC_CHROME_TRACKPAD_RATIO_TOLERANCE = 0.35;
/**
* True when wheelDeltaY looks like a fixed ±120 mechanical mouse notch (Chromium/Windows).
*
* Mac Chrome trackpad also populates wheelDelta, but as a linear ±3 × deltaY scale — that must
* not be treated as a mouse wheel or fast trackpad swipes fall through to I4 zoom.
*/
function isMacChromeLinearWheelDelta(event, wheelDeltaY) {
const { deltaY, deltaMode } = event;
if (deltaMode !== WheelEvent.DOM_DELTA_PIXEL || deltaY === 0 || !Number.isInteger(deltaY)) {
return false;
}
const ratio = Math.abs(wheelDeltaY / deltaY);
return (ratio > MAC_CHROME_TRACKPAD_WHEEL_DELTA_RATIO - MAC_CHROME_TRACKPAD_RATIO_TOLERANCE &&
ratio < MAC_CHROME_TRACKPAD_WHEEL_DELTA_RATIO + MAC_CHROME_TRACKPAD_RATIO_TOLERANCE);
}
/**
* Chromium still sets deprecated wheelDelta(Y) on mechanical mouse wheels (typically ±120).
* Trackpad scroll on Mac Chrome uses wheelDelta ≈ ±3 × deltaY instead.
*/
function hasLegacyMouseWheelDelta(event) {
const legacy = event;
const axisDelta = legacy.wheelDeltaY ?? legacy.wheelDelta;
if (axisDelta === undefined) {
return false;
}
if (Math.abs(axisDelta) < LEGACY_MOUSE_WHEEL_DELTA_MIN) {
return false;
}
if (isMacChromeLinearWheelDelta(event, axisDelta)) {
return false;
}
return true;
}
function createWheelContext(event) {
const normX = normalizeWheelDelta(event.deltaX, event.deltaMode);
const normY = normalizeWheelDelta(event.deltaY, event.deltaMode);
const absX = Math.abs(normX);
const absY = Math.abs(normY);
const isPixelDeltaMode = event.deltaMode === WheelEvent.DOM_DELTA_PIXEL;
const hasFractionalDelta = isPixelDeltaMode && (!Number.isInteger(event.deltaY) || !Number.isInteger(event.deltaX));
return {
event,
normX,
normY,
absX,
absY,
hasFractionalDelta,
isPixelDeltaMode,
isSmallDelta: absX < SMALL_DELTA_THRESHOLD && absY < SMALL_DELTA_THRESHOLD,
isVerticalOnly: absX < MIN_HORIZONTAL_SCROLL_ABS,
hasLegacyMouseWheelDelta: hasLegacyMouseWheelDelta(event),
};
}
/**
* Vertical-only wheel step — physical mouse (LINE/PAGE, or PIXEL delta ≥ discrete minimum).
*
* Small integer PIXEL deltas (< {@link MOUSE_WHEEL_DISCRETE_MIN_PX}) are trackpad ticks — excluded here.
*/
function isClassicMouseWheelStep(ctx) {
const { event, absY, isPixelDeltaMode, hasFractionalDelta } = ctx;
if (Math.abs(event.deltaX) >= 0.5) {
return false;
}
if (event.deltaMode === WheelEvent.DOM_DELTA_LINE || event.deltaMode === WheelEvent.DOM_DELTA_PAGE) {
return true;
}
if (absY < MOUSE_WHEEL_DISCRETE_MIN_PX) {
return false;
}
if (isPixelDeltaMode && !hasFractionalDelta) {
// Chromium/Windows: integer PIXEL + legacy wheelDelta ≈ ±120.
return ctx.hasLegacyMouseWheelDelta;
}
return true;
}
/** Trackpad scroll in PIXEL mode: integer `deltaX` and `deltaY`. */
function isIntegerPixelTrackpadScroll(ctx, isRapidStream) {
if (!ctx.isPixelDeltaMode || ctx.hasFractionalDelta) {
return false;
}
if (ctx.hasLegacyMouseWheelDelta) {
return false;
}
const peak = Math.max(ctx.absX, ctx.absY);
// Small integer ticks: always trackpad (slow scroll, inertia, per-frame deltas).
if (peak < MOUSE_WHEEL_DISCRETE_MIN_PX) {
return true;
}
// Large integer steps: trackpad only inside a continuous rapid stream.
// Isolated large integer PIXEL steps are mouse wheels on Chromium (Windows/macOS).
return isRapidStream;
}
/** Rapid PIXEL-mode stream with small deltas — fractional trackpad inertia on some drivers. */
function isTrackpadLikeRapidSmall(ctx, isRapidStream) {
if (!isRapidStream || !ctx.isPixelDeltaMode) {
return false;
}
return ctx.absY < TRACKPAD_I3_MAX_PX && ctx.absX < TRACKPAD_I3_MAX_PX;
}
/** Smooth-scroll mouse: slow vertical-only **fractional** PIXEL delta in the notch band. */
function isSlowFractionalMouseWheelStep(ctx, isRapidStream, inMouseWheelBurst) {
if (isRapidStream ||
inMouseWheelBurst ||
!ctx.hasFractionalDelta ||
!ctx.isVerticalOnly ||
!ctx.isSmallDelta ||
!ctx.isPixelDeltaMode) {
return false;
}
return ctx.absY >= MOUSE_WHEEL_NOTCH_MIN_PX && ctx.absY <= MOUSE_WHEEL_NOTCH_MAX_PX;
}
/** One axis dominates (large step), the other ~0 — classic mouse wheel click. */
function isDominantAxisLargeWheel(ctx) {
const { event, absX, absY } = ctx;
return ((absX >= SMALL_DELTA_THRESHOLD && Math.abs(event.deltaY) < 0.5) ||
(absY >= SMALL_DELTA_THRESHOLD && Math.abs(event.deltaX) < 0.5));
}
function isPinchZoomWheelEvent(ctx) {
const { event, isPixelDeltaMode } = ctx;
// Trackpads always emit PIXEL deltas. A held modifier means zoom on every platform:
// Mac Cmd (metaKey), pinch (ctrlKey), Windows/Linux Ctrl+scroll (ctrlKey).
return isPixelDeltaMode && (event.ctrlKey || event.metaKey);
}
/**
* Returns true when a trackpad modifier-zoom gesture is active (PIXEL mode + ctrl/meta).
* Used by Camera for {@link PINCH_ZOOM_SPEED}; mechanical wheels (LINE/PAGE) are excluded.
*/
export function isPinchZoomGesture(event) {
return isPinchZoomWheelEvent(createWheelContext(event));
}
/** Horizontal movement dominates — ignores deltaX noise on predominantly vertical wheel ticks. */
function isPredominantHorizontalScroll(ctx) {
return ctx.absX >= MIN_HORIZONTAL_SCROLL_ABS && ctx.absX > ctx.absY;
}
function isDiagonalScroll(ctx) {
const { event, absX, absY } = ctx;
if (event.shiftKey || absX <= DIAGONAL_MIN_ABS || absY <= DIAGONAL_MIN_ABS) {
return false;
}
const minAxis = Math.min(absX, absY);
const maxAxis = Math.max(absX, absY);
return minAxis / maxAxis >= DIAGONAL_AXIS_MIN_RATIO;
}
function buildWheelSignals(ctx) {
return {
isPinchZoom: isPinchZoomWheelEvent(ctx),
isDiagonalScroll: isDiagonalScroll(ctx),
isPredominantHorizontalScroll: isPredominantHorizontalScroll(ctx),
isClassicMouseWheelStep: isClassicMouseWheelStep(ctx),
isDominantAxisLargeWheel: isDominantAxisLargeWheel(ctx),
isVerticalOnly: ctx.isVerticalOnly,
hasFractionalDelta: ctx.hasFractionalDelta,
isSmallDelta: ctx.isSmallDelta,
isPixelDeltaMode: ctx.isPixelDeltaMode,
hasLegacyMouseWheelDelta: ctx.hasLegacyMouseWheelDelta,
};
}
function intentFromMouseWheelBehavior(mouseWheelBehavior) {
return mouseWheelBehavior === "scroll" ? EWheelIntent.Pan : EWheelIntent.Zoom;
}
/** I1/I2 always win; during rapid stream keep prior vertical intent to avoid mid-gesture flips. */
function applyRapidStreamStickyIntent(intent, rule, signals, isRapidStream, lastIntentBefore, lastRuleBefore) {
if (!isRapidStream ||
signals.isPinchZoom ||
signals.isDiagonalScroll ||
signals.isPredominantHorizontalScroll ||
intent === lastIntentBefore ||
lastRuleBefore === WHEEL_INTENT_RULE.I5_LAST_INTENT) {
return { intent, rule };
}
return { intent: lastIntentBefore, rule: WHEEL_INTENT_RULE.I5_STICKY_STREAM };
}
function emitDebugEntry(ctx, mouseWheelBehavior, inputDevice, timeSinceLastWheel, isRapidStream, isInMouseWheelBurst, mouseWheelBurstRemainingMs, lastIntentBefore, signals, rule, result) {
const debugLogger = getWheelIntentDebugLogger();
if (debugLogger === null) {
return;
}
const { event, normX, normY } = ctx;
debugLogger({
mouseWheelBehavior,
inputDevice,
input: {
deltaX: event.deltaX,
deltaY: event.deltaY,
deltaMode: event.deltaMode,
deltaModeLabel: deltaModeLabel(event.deltaMode),
ctrlKey: event.ctrlKey,
metaKey: event.metaKey,
shiftKey: event.shiftKey,
altKey: event.altKey,
},
normalized: {
deltaX: normX,
deltaY: normY,
diagonalAxisRatio: formatDiagonalAxisRatio(Math.abs(normX), Math.abs(normY)),
},
session: {
timeSinceLastMs: timeSinceLastWheel === Number.POSITIVE_INFINITY ? -1 : timeSinceLastWheel,
isRapidStream,
isInMouseWheelBurst,
mouseWheelBurstRemainingMs,
lastIntentBefore,
},
signals,
rule,
result,
});
}
/**
* Creates the default wheel intent resolver (`TGraphSettingsConfig.resolveWheelIntent`).
*
* Classifies **intent** from gesture shape — not from inferred device type:
*
* | Signal | Intent |
* |---------------------------------------|-----------------|
* | ctrlKey / metaKey + PIXEL scroll | Zoom (I1) |
* | Horizontal or diagonal movement | Pan (I2) |
* | Integer PIXEL delta (trackpad, auto) | Pan (I3) |
* | wheelInputDevice `"trackpad"` | Pan (I3:input-device-trackpad) |
* | wheelInputDevice `"mouse"` | Zoom/Pan (I4:* per behavior) |
* | Large isolated integer PIXEL step | Zoom/Pan (I4)* |
* | Classic mouse wheel step (fractional) | Zoom/Pan (I4)* |
* | Rapid stream + small delta (fractional)| Pan (I3) |
* | Anything else | Last intent (I5)|
* | Rapid stream + confident-rule flip | Sticky prior (I5:sticky-stream) |
*
* *I4 respects `mouseWheelBehavior`: `"scroll"` → Pan, `"zoom"` → Zoom.
*
* Trackpad: small integer PIXEL ticks, or large integer PIXEL inside a rapid stream → pan (I3).
* Isolated large integer PIXEL (Chromium mouse on Windows) → I4 per `mouseWheelBehavior`.
* LINE/PAGE mode (`deltaMode !== 0`) is never trackpad — always mouse (I4).
* See `docs/system/wheel-intent.md` for rationale.
*
* Pass `wheelInputDevice` at resolve time (camera constant `WHEEL_INPUT_DEVICE`) when the app
* knows the primary wheel device and Mac Chrome/YaBrowser heuristics are ambiguous.
*/
export function createWheelIntentResolver() {
let lastIntent = EWheelIntent.Zoom;
let lastRule = WHEEL_INTENT_RULE.I5_LAST_INTENT;
let lastTimestamp = null;
let mouseWheelBurstUntil = null;
const markMouseWheelBurst = (now) => {
mouseWheelBurstUntil = now + MOUSE_WHEEL_BURST_MS;
};
const isInMouseWheelBurst = (now) => mouseWheelBurstUntil !== null && now <= mouseWheelBurstUntil;
const resolveExplicitMouseIntent = (ctx, signals, mouseWheelBehavior, isRapidStream, inMouseWheelBurst, now) => {
if (signals.isDominantAxisLargeWheel || signals.isClassicMouseWheelStep || signals.hasLegacyMouseWheelDelta) {
markMouseWheelBurst(now);
return {
intent: intentFromMouseWheelBehavior(mouseWheelBehavior),
rule: signals.isClassicMouseWheelStep ? WHEEL_INTENT_RULE.I4_MOUSE_WHEEL_STEP : WHEEL_INTENT_RULE.I4_LARGE_STEP,
};
}
if (isSlowFractionalMouseWheelStep(ctx, isRapidStream, inMouseWheelBurst)) {
markMouseWheelBurst(now);
return {
intent: intentFromMouseWheelBehavior(mouseWheelBehavior),
rule: WHEEL_INTENT_RULE.I4_FRACTIONAL_MOUSE,
};
}
if (inMouseWheelBurst && signals.isVerticalOnly && isTrackpadLikeRapidSmall(ctx, isRapidStream)) {
markMouseWheelBurst(now);
return {
intent: intentFromMouseWheelBehavior(mouseWheelBehavior),
rule: WHEEL_INTENT_RULE.I4_BURST_SMOOTHING,
};
}
markMouseWheelBurst(now);
return {
intent: intentFromMouseWheelBehavior(mouseWheelBehavior),
rule: WHEEL_INTENT_RULE.I4_INPUT_DEVICE_MOUSE,
};
};
return (event, options) => {
const mouseWheelBehavior = options.mouseWheelBehavior;
const wheelInputDevice = options.wheelInputDevice ?? "auto";
const now = performance.now();
const timeSince = lastTimestamp !== null ? now - lastTimestamp : Number.POSITIVE_INFINITY;
lastTimestamp = now;
const isRapidStream = timeSince < RAPID_STREAM_MS;
const inMouseWheelBurst = isInMouseWheelBurst(now);
const mouseWheelBurstRemainingMs = mouseWheelBurstUntil !== null ? Math.max(0, mouseWheelBurstUntil - now) : null;
const ctx = createWheelContext(event);
const signals = buildWheelSignals(ctx);
const lastIntentBefore = lastIntent;
const lastRuleBefore = lastRule;
let intent;
let rule;
if (signals.isPinchZoom) {
intent = EWheelIntent.Zoom;
rule = WHEEL_INTENT_RULE.I1_PINCH;
}
else if (signals.isDiagonalScroll || signals.isPredominantHorizontalScroll) {
intent = EWheelIntent.Pan;
rule = WHEEL_INTENT_RULE.I2_HORIZONTAL_OR_DIAGONAL;
}
else if (wheelInputDevice === "trackpad") {
intent = EWheelIntent.Pan;
rule = WHEEL_INTENT_RULE.I3_INPUT_DEVICE_TRACKPAD;
}
else if (wheelInputDevice === "mouse") {
({ intent, rule } = resolveExplicitMouseIntent(ctx, signals, mouseWheelBehavior, isRapidStream, inMouseWheelBurst, now));
}
else if (isIntegerPixelTrackpadScroll(ctx, isRapidStream)) {
intent = EWheelIntent.Pan;
rule = isRapidStream ? WHEEL_INTENT_RULE.I3_INTEGER_TRACKPAD : WHEEL_INTENT_RULE.I3_INTEGER_TRACKPAD_SLOW;
}
else if (signals.isDominantAxisLargeWheel ||
signals.isClassicMouseWheelStep ||
signals.hasLegacyMouseWheelDelta) {
intent = intentFromMouseWheelBehavior(mouseWheelBehavior);
rule = signals.isClassicMouseWheelStep ? WHEEL_INTENT_RULE.I4_MOUSE_WHEEL_STEP : WHEEL_INTENT_RULE.I4_LARGE_STEP;
markMouseWheelBurst(now);
}
else if (isTrackpadLikeRapidSmall(ctx, isRapidStream)) {
if (inMouseWheelBurst && signals.isVerticalOnly) {
intent = intentFromMouseWheelBehavior(mouseWheelBehavior);
rule = WHEEL_INTENT_RULE.I4_BURST_SMOOTHING;
markMouseWheelBurst(now);
}
else {
intent = EWheelIntent.Pan;
rule = WHEEL_INTENT_RULE.I3_RAPID_SMALL;
}
}
else if (isSlowFractionalMouseWheelStep(ctx, isRapidStream, inMouseWheelBurst)) {
intent = intentFromMouseWheelBehavior(mouseWheelBehavior);
rule = WHEEL_INTENT_RULE.I4_FRACTIONAL_MOUSE;
markMouseWheelBurst(now);
}
else {
intent = lastIntent;
rule = WHEEL_INTENT_RULE.I5_LAST_INTENT;
}
({ intent, rule } = applyRapidStreamStickyIntent(intent, rule, signals, isRapidStream, lastIntentBefore, lastRuleBefore));
lastIntent = intent;
lastRule = rule;
if (getWheelIntentDebugLogger() !== null) {
emitDebugEntry(ctx, mouseWheelBehavior, wheelInputDevice, timeSince, isRapidStream, inMouseWheelBurst, mouseWheelBurstRemainingMs, lastIntentBefore, signals, rule, intent);
}
return intent;
};
}