Log inGet started
▣
module · drop-in viewer
asset⌬ modulemoduleprimary: init.luau·part ofmodule zinput.module·originates fromworld 07158574-5…

utils

Pure helpers for the `Zin` input library. Mouse-button name↔index conversion, key-code normalization, held-modifier matching against a frame snapshot, and the shaping a reading passes through on its way to a consumer. No FFI; safe to call from any context. Stateless.

byzero-proxy @ DESKTOP-DB3UJOJ·posted 2mo ago
What it does

utils

Pure helpers for the Zin input library. Mouse-button name↔index conversion, key-code normalization, held-modifier matching against a frame snapshot, and the shaping a reading passes through on its way to a consumer. No FFI; safe to call from any context. Stateless.

Exports

  • M.buttonName(idx: number) -> string? — convert a 0-based mouse button index to its name.
  • M.buttonIndex(name: string) -> number? — convert a mouse button name to its 0-based index.
  • M.normalizeKey(code: string) -> string — canonicalize a key code: a single ASCII letter becomes its Key<L> code, a single digit its Digit<N> code; multi-character codes pass through.
  • M.matchModifiers(snapshot: Snapshot, mods: Modifiers) -> boolean — test whether the snapshot's held keys match the requested modifier set.
  • M.applyCurve(curve, x: number) -> number — apply a response curve to a reading.
  • M.applyDeadzoneScalar(x: number, deadzone: number?) -> number — 0 below the threshold, the reading at or above it.
  • M.applyDeadzoneVector(v: Vector2, deadzone: number?) -> Vector2 — the same, measured radially on the magnitude of the pair.
  • M.shapeScalar(x: number, deadzone: number?, curve, invert: boolean?) -> number — deadzone, then curve, then inversion.
  • M.shapeVector(v: Vector2, deadzone: number?, curve, invert: boolean?) -> Vector2 — the same for a pair.
  • M.smoothToward(current: number, target: number, dt: number, tau: number) -> number — one step of an exponential approach with time constant tau.

A curve is "linear", "quadratic", "cubic", a function of the reading, or nil for linear.

Types:

  • Snapshot = { [string]: any } — the raw frame-snapshot table returned by Zin.state.get().
  • Modifiers = { ctrl: boolean?, shift: boolean?, alt: boolean? } — modifier subset to require in matchModifiers.
  • Vector2 = { x: number, y: number } — a two-component reading.

Shaping

The order is deadzone, curve, invert, then the approach toward the shaped target. A scalar takes its deadzone per value; a pair takes it radially, so a diagonal is not clipped into a cross by two independent thresholds.

"quadratic" squares while keeping the sign, so a half-pushed stick reads a quarter in the direction it was pushed. A curve function that raises, or answers with anything other than a number, leaves the reading as it was.

This is the one implementation of that shaping: Zin.axes reads it for a registered axis and Zin.scheme for an axis1 / axis2 control, so an axis and a control declaring the same numbers read the same.

Usage

local Utils = require("@builtin::modules.zinput.utils")

local name = Utils.buttonName(0)            -- "left"
local idx  = Utils.buttonIndex("right")     -- 1
local key  = Utils.normalizeKey("w")        -- "KeyW"
local hit  = Utils.matchModifiers(snap, { ctrl = true, shift = false })

Notes

  • Pure functions — no state, no side effects.
  • Mouse-button indexing is 0-based and matches Zin.state.mouseButtonDown(idx) (0=left, 1=right, 2=middle).
  • matchModifiers ignores fields set to nil in mods — only true/false are constraints.
  • normalizeKey rejects any other single character: no bound key code is one character long, so "?" raises rather than resolving to a phantom key nothing consumes.

Interface

What this asset declares: the schema it conforms to, what it exposes, and the rendered structured payload.

conforms to

zero/source-extract/v2

ZinputUtils Module Pure helpers for input: mouse-button name↔index conversion, key-code normalization, held-modifier matching against a frame snapshot, and the shaping a reading passes through on its way to a consumer. No FFI; safe to call from any context. Backs `Zin.utils`. The index convention matches `Zin.state.mouseButtonDown(idx)`: 0=left, 1=right, 2=middle. The named string form ("left" / "right" / "middle") matches the `button` field on `Zin.events.frame()` mouse.* records. Consumers: local Utils = require("modules.zinput.utils") Utils.buttonName(0) -- → "left" Utils.buttonIndex("right") -- → 1

buttonName(idx: number) → string

Convert a 0-based mouse button index to its name.

argtypedescription
idxnumberMouse button index (0=left, 1=right, 2=middle).

examples

Utils.buttonName(0)  -- → "left"

buttonIndex(name: string) → number

Convert a mouse button name to its 0-based index.

argtypedescription
namestringMouse button name (`"left"`/`"right"`/`"middle"`).

examples

Utils.buttonIndex("right")  -- → 1

resolveButtonIndex(button: (number | string) →

Resolve a mouse button given as a 0-based index OR a case-insensitive name (`"left"`/`"right"`/`"middle"`, the same names `Zin.bindings.mouse` takes) to its 0-based index. `nil` resolves to left (0). Raises on an unknown name so a typo is loud, not a silent left-click — the single coercion every input surface that accepts a button uses so index and name mean the same thing everywhere.

argtypedescription
button(number | stringButton index, name, or nil.

examples

Utils.resolveButtonIndex("Right")  -- → 1
Utils.resolveButtonIndex(2)         -- → 2

normalizeKey(code: string) → string

Normalize a key code to the engine's canonical web-style form — the `KeyboardEvent.code` vocabulary the input map and `Zin.state.get().keys` use (`"KeyW"`, `"Space"`, `"ArrowUp"`, `"ShiftLeft"`, …). A single ASCII letter is promoted to its `Key<L>` code and a single digit to its `Digit<N>` code, so the common shorthand `"W"` resolves to the `"KeyW"` the default map binds instead of a phantom key nothing consumes. Any other single character is rejected — no bound key code is one character long. Multi-character codes pass through unchanged.

argtypedescription
codestringThe raw key code (`"KeyW"`) or a single-letter/digit shorthand (`"w"`).

examples

Utils.normalizeKey("w")     -- → "KeyW"
Utils.normalizeKey("KeyW")  -- → "KeyW"

keyFields(surface: string?) → KeyFields

The snapshot fields that carry a surface's keys: `held`, `pressed` and `released`. `surface` is `"scene"` (the default), the keys pressed while the scene was the active input context, or `"editor"`, every key no focused widget took.

argtypedescription
surfacestring?`"scene"` or `"editor"`; nil reads as `"scene"`.

examples

local f = Utils.keyFields("editor"); local held = snap[f.held]

matchModifiers(snapshot: any, mods: Modifiers, surface: string?) → boolean

Check whether the current frame's snapshot has the given modifiers held. Pass any subset of `{ ctrl, shift, alt }`; unspecified keys are not checked. Returns `false` if `snapshot` is not a table — callers can forward `Zin.state.get()` directly without nil-checking first. non-table value (treated as "no modifiers held").

argtypedescription
snapshotanyThe frame snapshot table (from `Zin.state.get()`), or any
modsModifiersSubset of `{ ctrl, shift, alt }` booleans to require.
surfacestring?Whose keys to read: `"scene"` (the default) or `"editor"`.

examples

Utils.matchModifiers(snap, { ctrl = true })

curveOf(curve: (string | (number) → void

What conditions a reading between the device and the consumer: a deadzone that drops the play around centre, a response curve, an inversion, and an exponential approach that carries the value toward a target over time rather than stepping onto it. The order is deadzone, curve, invert, then the approach toward the shaped target. A scalar takes its deadzone per value; a vector takes it radially, on the magnitude of the pair, so a diagonal is not clipped into a cross by two independent thresholds. These are the one implementation of that shaping. `Zin.axes` reads them for a registered axis and `Zin.scheme` for an `axis1` / `axis2` control, so an axis and a control declaring the same numbers read the same. Internal: the curve maths, reached by both the entry point below and the shaping pipelines, so a curve is applied one way.

argtypedescription
curve(string | (number

deadzoneScalarOf(x: number, deadzone: number?) → number

Internal: scalar deadzone — magnitudes below the threshold snap to 0.

argtypedescription
xnumber
deadzonenumber?

deadzoneVectorOf(v: Vector2, deadzone: number?) → Vector2

Internal: radial deadzone — a magnitude under the threshold zeroes both components.

argtypedescription
vVector2
deadzonenumber?

applyCurve(curve: (string | (number, x: ?) →

Apply a response curve to a reading. `"linear"` (and no curve at all) is the identity, `"quadratic"` squares while keeping the sign, `"cubic"` cubes, and a function is called with the reading. A function that raises, or answers with anything other than a number, leaves the reading as it was.

argtypedescription
curve(string | (number`"linear"` | `"quadratic"` | `"cubic"` | a function of the reading.
x?The reading to shape.

examples

Utils.applyCurve("quadratic", -0.5)  -- → -0.25

applyDeadzoneScalar(x: number, deadzone: number?) → number

Apply a scalar deadzone: a magnitude below the threshold reads 0, anything at or above it passes through untouched.

argtypedescription
xnumberThe reading.
deadzonenumber?The threshold; `nil` applies none.

examples

Utils.applyDeadzoneScalar(0.04, 0.1)  -- → 0

applyDeadzoneVector(v: Vector2, deadzone: number?) → Vector2

Apply a radial deadzone to a pair: the MAGNITUDE of the pair is what the threshold is measured against, so a diagonal held past it keeps both components and a stick resting inside it reads `{0, 0}`.

argtypedescription
vVector2The reading, as `{ x, y }`.
deadzonenumber?The threshold; `nil` applies none.

examples

Utils.applyDeadzoneVector({ x = 0.05, y = 0.05 }, 0.2)  -- → { x = 0, y = 0 }

smoothToward(current: number, target: number, dt: number, tau: number) → number

One step of an exponential approach toward a target: `tau` is the time constant in seconds, and the step covers `dt` of it. A `tau` of 0 or less arrives immediately; a `dt` of 0 or less stays put.

argtypedescription
currentnumberWhere the value is now.
targetnumberWhere it is heading.
dtnumberSeconds this step covers.
taunumberThe time constant, in seconds.

examples

Utils.smoothToward(0, 1, 0.05, 0.2)  -- → ~0.221
⌬ Types
Snapshot = { [string]: any }Modifiers = { ctrl: boolean?, shift: boolean?, alt: boolean? }Vector2 = { x: number, y: number }KeyFields = { held: string, pressed: string, released: string }

Sub-parts

Everything contained inside this part. Assets are composite children (clickable cards). Files are leaf payloads. Expand any row to view its source.

3items
·
other · born here
▤file
▲ 0↑ born
backing path · modules/zinput.module/utils.module

Problems

Everything affecting this asset right now: its own problems, anything wrong inside it, and problems on its direct dependencies.

0problems
No problems reported. This asset, its contents, and its direct deps are clean as of the latest commit.
⌬ZeroMind agent review · awaiting first pass
Findings
Reviewer findings (handle · model · tag · quoted note) appear here once the per-pass review log lands. Today only the rolled-up agent_score is exposed.
usability—
did it work as advertised
quality—
authoring polish + cohesion
performance—
frame & memory budget held
agent review score
—
/ 100
awaiting first pass
usability × 0.40
+ quality × 0.35
+ performance × 0.25
± compat factor

Usability ratings

Did the part work as advertised when consumers tried to drop it in. Separate from upvotes: those are taste; this is "did it function".

—%no reports yet
Sign in to report whether this part worked for you.
Discussion

Scoped to this part · feeds back into the world's score.

0comments
Sign in to post.sign in
No comments yet. Be the first.