This commit is contained in:
2026-09-09 17:35:23 +08:00
parent 2a98ae7556
commit 161af68fe3
1271 changed files with 58251 additions and 98639 deletions
@@ -0,0 +1,347 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Numeric Input | HiAPI-C# 2025 </title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="title" content="Numeric Input | HiAPI-C# 2025 ">
<link rel="icon" href="../../img/HiAPI.favicon.ico">
<link rel="stylesheet" href="../../public/docfx.min.css">
<link rel="stylesheet" href="../../public/main.css">
<meta name="docfx:navrel" content="../../toc.html">
<meta name="docfx:tocrel" content="toc.html">
<meta name="docfx:rel" content="../../">
<meta name="loc:inThisArticle" content="In this article">
<meta name="loc:searchResultsCount" content="{count} results for &quot;{query}&quot;">
<meta name="loc:searchNoResults" content="No results for &quot;{query}&quot;">
<meta name="loc:tocFilter" content="Filter by title">
<meta name="loc:nextArticle" content="Next">
<meta name="loc:prevArticle" content="Previous">
<meta name="loc:themeLight" content="Light">
<meta name="loc:themeDark" content="Dark">
<meta name="loc:themeAuto" content="Auto">
<meta name="loc:changeTheme" content="Change theme">
<meta name="loc:copy" content="Copy">
<meta name="loc:downloadPdf" content="Download PDF">
<script type="module" src="./../../public/docfx.min.js"></script>
<script>
const theme = localStorage.getItem('theme') || 'auto'
document.documentElement.setAttribute('data-bs-theme', theme === 'auto' ? (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light') : theme)
</script>
</head>
<body class="tex2jax_ignore" data-layout="" data-yaml-mime="">
<header class="bg-body border-bottom">
<nav id="autocollapse" class="navbar navbar-expand-md" role="navigation">
<div class="container-xxl flex-nowrap">
<a class="navbar-brand" href="../../index.html">
<img id="logo" class="svg" src="../../img/HiAPI.logo.png" alt="">
</a>
<button class="btn btn-lg d-md-none border-0" type="button" data-bs-toggle="collapse" data-bs-target="#navpanel" aria-controls="navpanel" aria-expanded="false" aria-label="Toggle navigation">
<i class="bi bi-three-dots"></i>
</button>
<div class="collapse navbar-collapse" id="navpanel">
<div id="navbar">
<form class="search" role="search" id="search">
<i class="bi bi-search"></i>
<input class="form-control" id="search-query" type="search" disabled placeholder="Search" autocomplete="off" aria-label="Search">
</form>
</div>
</div>
</div>
</nav>
</header>
<main class="container-xxl">
<div class="toc-offcanvas">
<div class="offcanvas-md offcanvas-start" tabindex="-1" id="tocOffcanvas" aria-labelledby="tocOffcanvasLabel">
<div class="offcanvas-header">
<h5 class="offcanvas-title" id="tocOffcanvasLabel">Table of Contents</h5>
<button type="button" class="btn-close" data-bs-dismiss="offcanvas" data-bs-target="#tocOffcanvas" aria-label="Close"></button>
</div>
<div class="offcanvas-body">
<nav class="toc" id="toc"></nav>
</div>
</div>
</div>
<div class="content">
<div class="actionbar">
<button class="btn btn-lg border-0 d-md-none" type="button" data-bs-toggle="offcanvas" data-bs-target="#tocOffcanvas" aria-controls="tocOffcanvas" aria-expanded="false" aria-label="Show table of contents">
<i class="bi bi-list"></i>
</button>
<nav id="breadcrumb"></nav>
</div>
<article data-uid="Anatomy-NumericInput">
<h1 id="numeric-input">Numeric Input</h1>
<p>NumericInput is the single-value numeric field the rest of the web application embeds. It has no
route and no Control-Tree node of its own: it is reached only through the editors that host it,
and those span the equipment and mission trees, the Tool House tool editors, the Legacy Controller
tabs, the transformer and geometry editors and the Preference menu. The rule for carrying <code>NaN</code> and
the infinities across the JSON boundary is stated once in
<a class="xref" href="../conventions/numeric-io-utilities.html">Numeric Input/Output</a>; this page is the widget that implements
the client end of it.</p>
<h2 id="the-binding-contract">The Binding Contract</h2>
<p>The widget owns no value. It holds the raw text the user is typing and nothing else, and every
committed number is handed straight back to the host, which decides what to persist and when.</p>
<table>
<thead>
<tr>
<th>Prop</th>
<th>Meaning</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>modelValue</code></td>
<td><code>number</code>, <code>null</code> or <code>undefined</code> — the value to show</td>
</tr>
<tr>
<td><code>label</code></td>
<td>the field's label, inside the outline; floats to the top of it once the field has content</td>
</tr>
<tr>
<td><code>hint</code></td>
<td>text below the field</td>
</tr>
<tr>
<td><code>unit</code></td>
<td>a display-only suffix inside the field</td>
</tr>
<tr>
<td><code>readonly</code> / <code>disable</code></td>
<td>passed through to the underlying field</td>
</tr>
<tr>
<td><code>min</code> / <code>max</code></td>
<td>inclusive bounds, checked on commit</td>
</tr>
<tr>
<td><code>allowEmpty</code></td>
<td>whether a blank field commits <code>null</code>; defaults to on</td>
</tr>
<tr>
<td><code>hideBottomSpace</code></td>
<td>stop holding space below the field; a hint or message still renders when there is one</td>
</tr>
<tr>
<td><code>rules</code></td>
<td>extra validation functions over the raw <strong>text</strong></td>
</tr>
</tbody>
</table>
<p>Two events leave the widget.</p>
<ul>
<li><code>update:modelValue</code> carries a number or <code>null</code>. <code>null</code> is a real outcome rather than an error
signal, so a host that must not receive one either turns <code>allowEmpty</code> off or filters what it
gets: the cutter section panel returns early on <code>null</code> so that clearing a field cannot write a
zero into the profile, and the graphic-cache menu rejects <code>null</code> and every non-finite value
before it calls the server.</li>
<li><code>parseError</code> carries the raw text that failed to parse. No shipped host listens for it, so a
parse failure is visible only as the message under the field.</li>
</ul>
<h2 id="commit-semantics">Commit Semantics</h2>
<p>Typing changes nothing but the text in the box. The commit runs on blur and on Enter, and on
nothing else — there is no per-keystroke emit, no debounce and no timer. The underlying control is
a plain text field rather than a browser number input, so there is no spinner, no step and no
keystroke-level filtering: any text at all may sit in the box until the field is committed.</p>
<p>A commit parses the text once and then takes one of three exits. On success the error strip is
cleared, <code>update:modelValue</code> is emitted, and the box is rewritten from the parsed number. On text
that does not parse, <code>parseError</code> is emitted and a message appears. On a number outside the bounds,
a message appears. Neither failing exit emits <code>update:modelValue</code>, and neither rewrites the box.</p>
<p>Enter commits without moving focus, so the value is committed again when the field is finally left.
The widget carries no equality guard — it emits whenever the text parses and passes the bounds,
whether or not the result differs from <code>modelValue</code> — so that second commit reaches the host as a
second, identical write. Here the widget parts company with the vector and matrix editors, both of
which compare against the model before emitting.</p>
<h2 id="bounds-and-special-values">Bounds and Special Values</h2>
<p><code>min</code> and <code>max</code> are inclusive and are enforced inside the widget, before anything is emitted: a
number below <code>min</code> or above <code>max</code> is refused and the host never sees it.</p>
<p><strong>The bounds apply to finite numbers only.</strong> <code>Infinity</code>, <code>-Infinity</code>, <code>NaN</code> and the <code>null</code> a blank
field produces all skip the range check, so a field declared with a <code>min</code> of 0 still commits
<code>-Infinity</code> and <code>NaN</code>. Where that matters, the host filters what it receives.</p>
<table>
<thead>
<tr>
<th>Typed</th>
<th>Committed</th>
<th>Shown afterwards</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>Infinity</code> in any case, or <code>∞</code></td>
<td><code>Infinity</code></td>
<td><code>Infinity</code></td>
</tr>
<tr>
<td><code>-Infinity</code> in any case, or <code>-∞</code></td>
<td><code>-Infinity</code></td>
<td><code>-Infinity</code></td>
</tr>
<tr>
<td><code>NaN</code> in any case</td>
<td><code>NaN</code></td>
<td><em>(empty)</em></td>
</tr>
<tr>
<td>blank, <code>allowEmpty</code> on</td>
<td><code>null</code></td>
<td><em>(empty)</em></td>
</tr>
<tr>
<td>blank, <code>allowEmpty</code> off</td>
<td>nothing — parse error</td>
<td>the blank stays</td>
</tr>
<tr>
<td>anything JavaScript's <code>Number()</code> reads as a finite number</td>
<td>that number</td>
<td>its default string form</td>
</tr>
<tr>
<td>anything else</td>
<td>nothing — parse error</td>
<td>the rejected text stays</td>
</tr>
</tbody>
</table>
<p><code>Number()</code> is the whole numeric parser, so exponent notation and the <code>0x</code>, <code>0b</code> and <code>0o</code> integer
literals are accepted alongside ordinary decimals, while a thousands separator is not. Its result
must also come back finite to be accepted, so a literal that overflows the double — <code>1e400</code> — is a
parse error rather than a second route to <code>Infinity</code>. The two spellings at the head of the table
are the only way to reach one.</p>
<p>Two consequences follow from <code>NaN</code> and <code>null</code> sharing the empty box. A <code>NaN</code> arriving from the
server is indistinguishable from a blank field; and because a blank field commits <code>null</code> under the
default, merely focusing such a field and leaving it replaces the <code>NaN</code> with <code>null</code>. With
<code>allowEmpty</code> turned off the same field instead fails to parse on every blur until something is
typed into it.</p>
<p>The infinity spellings the widget writes and reads are the same strings the API layer puts on the
wire for a non-finite number — see <a class="xref" href="../conventions/numeric-io-utilities.html">Numeric Input/Output</a>. The
widget itself never touches the wire: it emits a JavaScript number, and converting a non-finite one
into its string form belongs to the host's API module, as <code>wwwroot-src/src/api/mission.ts</code> does for
the mission commands. That conversion is not symmetric for <code>NaN</code>: the mission module writes all
three strings but recognises only the two infinity spellings when reading, so a <code>NaN</code> returned by
the endpoint becomes the caller's supplied default instead.</p>
<h2 id="unit-suffix-and-precision">Unit Suffix and Precision</h2>
<p><code>unit</code> is rendered as a suffix inside the field, to the right of the text. It is decoration only:
it is not part of the editable text, it is not parsed, and no unit conversion happens anywhere in
the widget — the number committed is in whatever unit the host's model already uses. Hosts pass
plain unit text, <code>mm</code> and <code>deg</code> being the common ones, alongside <code>mm/min</code>, <code>rpm</code>, <code>N</code>, <code>°C</code> and
<code>MB</code>.</p>
<p>There is no precision, decimals or step prop. A value is displayed through JavaScript's default
number-to-string conversion, so it shows at full precision and switches to exponent notation at the
magnitudes where that conversion does. Rounding, where a host wants it, is the host's own: the
graphic-cache current-size field rounds in its commit handler, after the widget has handed the
number over, while the two limit fields in the same menu send what they were given.</p>
<h2 id="validation-messages">Validation Messages</h2>
<p>Three messages can appear under the field, and all three are built inside the component in English:
<code>Invalid number: &quot;…&quot;</code> quoting the rejected text, <code>Must be ≥ …</code> and <code>Must be ≤ …</code> quoting the bound.
They are not keys and appear in no i18n bundle, so they do not follow the application's language
setting — unlike <code>label</code> and <code>hint</code>, which hosts pass in already translated.</p>
<p><code>rules</code> is a separate mechanism and differs in three ways: its functions receive the raw display
<strong>text</strong> rather than the parsed number, they run on the underlying field's own validation pass —
as the text changes, and again when focus leaves — rather than inside the commit, and <strong>they do
not gate the commit</strong>: a value failing a caller rule is still parsed, still bounds-checked and
still emitted. While the widget's own message is showing, it also masks the rule's.</p>
<p>No shipped host passes <code>rules</code>, and none fills the <code>append</code> slot, so both are available rather
than exercised. <code>hideBottomSpace</code> is the opposite — it is among the most-passed props on the
widget, set on nearly every field the SoftNc controller panels, the spindle contour editor and the
fluting span list drop into a table cell.</p>
<h2 id="when-the-box-and-the-model-disagree">When the Box and the Model Disagree</h2>
<p>A rejected commit leaves the typed text in the box. That text is replaced only when <code>modelValue</code>
changes to something whose formatted form differs from what is showing; the widget does not revert
on its own. That is the second thing the vector and matrix editors do that this one does not: both
reinstate the last valid value the moment a cell fails to parse. Two situations follow, and both
put a number on screen that was never stored:</p>
<ul>
<li>A number outside the bounds, or text that does not parse, stays in the box under its error
message. Nothing restores it until the host pushes a different value.</li>
<li>A commit the host accepts and the server then refuses leaves the box showing the refused number
unless the host had already applied it locally, because assigning the unchanged value back to
<code>modelValue</code> is not a change and moves nothing.</li>
</ul>
<p>The message is equally persistent: it is cleared only by the next successful commit, so it can
outlive the value that caused it and sit under a field the host has since repopulated.</p>
<h2 id="layout">Layout</h2>
<ul>
<li>Numeric Input Field
<ul>
<li>Label — the host's <code>label</code>, positioned inside the field's outline: on the input line while the
field is empty and unfocused, floated to the top of the outline once it has a value or the
focus; absent when none is given</li>
<li>Text Box — one line, free text until committed</li>
<li>Unit Suffix — the host's <code>unit</code>, inside the field at the right; display only</li>
<li>Append Slot — an optional trailing slot for a host-supplied control</li>
<li>Bottom Strip — the host's <code>hint</code>, replaced by the validation message while the field is in
error. <code>hideBottomSpace</code> stops space being held below the field but does not suppress the
strip: a hint or a validation message still renders, and with nothing reserved for it, it
grows the field instead of filling a gap already left below.</li>
</ul>
</li>
</ul>
<h2 id="source-code-path">Source Code Path</h2>
<p>See <a class="xref" href="../index.html">HiNC App Anatomy</a> for git repository links.</p>
<h3 id="web-application">Web Application</h3>
<p>HiNC-2025-webservice (Quasar CLI SPA):</p>
<ul>
<li><code>wwwroot-src/src/components/widgets/NumericInput.vue</code> — the widget: the raw-text buffer, the
local format and parse pair, the blur-and-Enter commit, the inclusive bounds check that skips
non-finite values, and the three English validation strings.</li>
<li><code>wwwroot-src/src/components/preference/GraphicCacheMenu.vue</code> — a compact host of three instances,
one of them bounded by the values of the other two; every handler rejects <code>null</code> and non-finite
numbers, and the current-size handler is the one that rounds before sending.</li>
<li><code>wwwroot-src/src/components/controlTree/toolhouse/CutterSectionPanel.vue</code> — a host that returns
early on the <code>null</code> a cleared field emits, so blanking a cutter dimension cannot write a zero.</li>
<li><code>wwwroot-src/src/components/spindle/SpindleContourEditor.vue</code> — a host that turns <code>allowEmpty</code>
off and keeps its own draft copy of the contour points around the blur commit.</li>
<li><code>wwwroot-src/src/api/mission.ts</code> — the module that turns a committed non-finite number into the
<code>Infinity</code>, <code>-Infinity</code> or <code>NaN</code> string the endpoint takes, and reads the two infinity spellings
back; a <code>NaN</code> arriving from the endpoint falls through to the caller's default instead.</li>
</ul>
<h2 id="see-also">See Also</h2>
<ul>
<li><a class="xref" href="../conventions/numeric-io-utilities.html">Numeric Input/Output</a> — the cross-boundary rule this widget implements the client half of, and how the three numeric inputs differ from one another</li>
<li><a class="xref" href="index.html">Widgets</a> — the other controls that pages embed rather than own</li>
<li><a class="xref" href="../general-setup/controller/editing-contract.html">Editing Contract</a> — the branch that embeds this field most heavily, and the
commit rules its panels inherit from it</li>
</ul>
</article>
<div class="contribution d-print-none">
</div>
<div class="next-article d-print-none border-top" id="nextArticle"></div>
</div>
<div class="affix">
<nav id="affix"></nav>
</div>
</main>
<div class="container-xxl search-results" id="search-results"></div>
<footer class="border-top text-secondary">
<div class="container-xxl">
<div class="flex-fill">
<span> Copyright © 2025 <a href='https://superhightech.com.tw'>Tech Coordinate</a>. All rights reserved. <a href='https://superhightech.com.tw'>超級高科技股份有限公司</a> © 2025 版權所有 </span>
</div>
</div>
</footer>
</body>
</html>