New drop-menu for picking a standard bit size.

This commit is contained in:
David Carley
2022-08-18 20:30:53 +00:00
parent 1bd096e55c
commit c1b93270e7
12 changed files with 278 additions and 189 deletions

View File

@@ -1,91 +0,0 @@
<script lang="ts">
import TextField from "@smui/textfield";
import Select, { Option } from "@smui/select";
import type { MenuComponentDev } from "@smui/menu";
import Menu from "@smui/menu";
import List, { Item, Text } from "@smui/list";
import { onMount } from "svelte";
import { set_input_value } from "svelte/internal";
let menu: MenuComponentDev;
type Option = {
value: number;
label: string;
metric: boolean;
};
export let label: string;
export let value: number;
export let metric: boolean;
export let options: Option[];
let textValue = "";
$: if (textValue) {
value = Number.parseFloat(textValue) || null;
}
onMount(() => {
textValue = value?.toString() || "";
});
function onOptionSelected(option: Option) {
textValue = option.value.toString();
metric = option.metric;
}
function filterKeys(event) {
const input = event.target;
if (input.value.match(/[^0-9.]/)) {
input.value = input.value.replace(/[^0-9.]/g, "");
}
if (input.value.match(/\.[^.]*\./)) {
input.value = input.value.replace(/(\.[^.]*)\./g, "$1");
}
}
</script>
<div>
<div class="value-and-unit">
<TextField
{label}
on:keypress={filterKeys}
on:keyup={filterKeys}
bind:value={textValue}
on:click={() => menu.setOpen(true)}
/>
<Select bind:value={metric}>
<Option value={true}>mm</Option>
<Option value={false}>in</Option>
</Select>
</div>
<Menu bind:this={menu} anchorCorner="BOTTOM_LEFT">
<List>
{#each options as option}
<Item on:SMUI:action={() => onOptionSelected(option)}>
<Text>{option.label}</Text>
</Item>
{/each}
</List>
</Menu>
</div>
<style lang="scss">
:global {
.value-and-unit {
display: flex;
column-gap: 10px;
.mdc-select {
max-width: 60px;
}
.smui-select--standard .mdc-select__dropdown-icon {
margin-left: 0;
}
}
}
</style>

View File

@@ -0,0 +1,90 @@
<script lang="ts">
import TextField from "@smui/textfield";
import Icon from "@smui/textfield/icon";
import HelperText from "@smui/textfield/helper-text";
import MenuSurface, {
type MenuSurfaceComponentDev,
} from "@smui/menu-surface";
import List, { Item, Text } from "@smui/list";
import { virtualKeyboardChange } from "$lib/CustomActions";
import { onDestroy } from "svelte";
let menuSurface: MenuSurfaceComponentDev;
let menuTimeout;
let optionSelected: boolean = false;
export let value: string;
export let options: string[][];
export let valid: boolean;
export let helperText: string;
onDestroy(() => {
if (menuTimeout) {
clearTimeout(menuTimeout);
menuTimeout = undefined;
}
});
function showMenu(show: boolean) {
if (show && optionSelected) {
return;
}
optionSelected = false;
if (menuTimeout) {
clearTimeout(menuTimeout);
}
// Use a timeout to "debounce" the display of the menu.
menuTimeout = setTimeout(() => menuSurface.setOpen(show), 100);
}
</script>
<div class="textfield-with-options">
<TextField
bind:value
on:focusin={() => showMenu(true)}
on:focusout={() => showMenu(false)}
use={[[virtualKeyboardChange, (newValue) => (value = newValue)]]}
{...$$restProps}
>
<div slot="trailingIcon">
{#if valid}
<Icon class="fa fa-check-circle-o" style="color: green;" />
{/if}
</div>
<HelperText persistent slot="helper">{helperText}</HelperText>
</TextField>
<MenuSurface bind:this={menuSurface} anchorCorner="BOTTOM_LEFT">
<div style="display: flex; flex-direction: row;">
{#each options as group}
<List>
{#each group as option}
<Item
on:SMUI:action={() => {
value = option;
showMenu(false);
optionSelected = true;
}}
>
<Text>{option}</Text>
</Item>
{/each}
</List>
{/each}
</div>
</MenuSurface>
</div>
<style lang="scss">
:global {
.textfield-with-options {
.mdc-deprecated-list-item {
height: 35px;
}
}
}
</style>

View File

@@ -4,7 +4,7 @@
import TextField from "@smui/textfield";
import MessageDialog from "$dialogs/MessageDialog.svelte";
import * as api from "$lib/api";
import { virtualKeyboardChangeHelper } from "$lib/customActions";
import { virtualKeyboardChange } from "$lib/CustomActions";
// https://man7.org/linux/man-pages/man7/hostname.7.html
//
@@ -73,7 +73,7 @@
<Content id="change-hostname-dialog-content">
<TextField
bind:value={hostname}
use={[[virtualKeyboardChangeHelper, (newValue) => (hostname = newValue)]]}
use={[[virtualKeyboardChange, (newValue) => (hostname = newValue)]]}
label="New Hostname"
spellcheck="false"
variant="filled"

View File

@@ -3,7 +3,7 @@
import TextField from "@smui/textfield";
import Button, { Label } from "@smui/button";
import { ControllerMethods } from "$lib/RegisterControllerMethods";
import { virtualKeyboardChangeHelper } from "$lib/customActions";
import { virtualKeyboardChange } from "$lib/CustomActions";
export let open: boolean;
export let axis = "";
@@ -31,7 +31,7 @@
label="Absolute"
type="number"
bind:value
use={[[virtualKeyboardChangeHelper, (newValue) => (value = newValue)]]}
use={[[virtualKeyboardChange, (newValue) => (value = newValue)]]}
variant="filled"
style="width: 100%;"
/>

View File

@@ -1,5 +1,4 @@
<script type="ts">
import DimensionInput from "$components/DimensionInput.svelte";
import Dialog, { Title, Content, Actions } from "@smui/dialog";
import Button, { Label } from "@smui/button";
import { waitForChange } from "$lib/StoreHelpers";
@@ -13,14 +12,23 @@
probingFailed,
probingStarted,
} from "$lib/ControllerState";
import { numberWithUnit } from "$lib/RegexHelpers";
import TextFieldWithOptions from "$components/TextFieldWithOptions.svelte";
type Step =
| "None"
| "CheckProbe"
| "BitDimensions"
| "PlaceProbeBlock"
| "Probe"
| "Done";
const ValidSteps = [
"None",
"CheckProbe",
"BitDimensions",
"PlaceProbeBlock",
"Probe",
"Done",
] as const;
type Step = typeof ValidSteps[number];
function isStep(str): str is Step {
return ValidSteps.includes(str);
}
const stepLabels: Record<Step, string> = {
None: "",
@@ -34,21 +42,16 @@
const cancelled = writable(false);
const userAcknowledged = writable(false);
const cutterDiameterOptions = [
{ value: 0.5, label: '1/2 "', metric: false },
{ value: 0.25, label: '1/4 "', metric: false },
{ value: 0.125, label: '1/8 "', metric: false },
{ value: 10, label: "10 mm", metric: true },
{ value: 6, label: "6 mm", metric: true },
{ value: 3, label: "10 mm", metric: true },
];
const imperialBits = ["1/2 in", "1/4 in", "1/8 in"];
const metricBits = ["10 mm", "8 mm", "6 mm", "3 mm"];
export let open;
export let probeType: "xyz" | "z";
let currentStep: Step = "None";
let cutterDiameter: number;
let cutterDiameterString: string = "";
let cutterDiameterMetric: number;
let showCancelButton = true;
let steps: Array<Step> = [];
let steps: Step[] = [];
let nextButton = {
label: "Next",
disabled: false,
@@ -56,10 +59,12 @@
};
$: metric = $Config.settings?.units === "METRIC";
$: cutterDiameterMetric = numberWithUnit
.parse(cutterDiameterString)
?.toMetric();
$: if (open) {
cutterDiameter =
Number.parseFloat(localStorage.getItem("cutterDiameter")) || null;
cutterDiameterString = localStorage.getItem("cutterDiameter") ?? "";
// Svelte appears not to like it when you invoke
// an async function from a reactive statement, so we
@@ -67,32 +72,31 @@
requestAnimationFrame(begin);
}
$: if (cutterDiameter) {
$: if (cutterDiameterString) {
updateButtons();
}
function removeSkippedSteps(steps: Step[]): Step[] {
return steps.filter((x) => x);
}
async function begin() {
try {
$probingActive = true;
assertValidProbeType();
steps = removeSkippedSteps([
steps = [
"CheckProbe",
probeType === "xyz" ? "BitDimensions" : undefined,
"PlaceProbeBlock",
"Probe",
"Done",
]);
].filter<Step>(isStep);
await stepCompleted("CheckProbe", probeContacted);
if (probeType === "xyz") {
await stepCompleted("BitDimensions", userAcknowledged);
localStorage.setItem("cutterDiameter", cutterDiameter.toString());
localStorage.setItem(
"cutterDiameter",
numberWithUnit.normalize(cutterDiameterString)
);
}
await stepCompleted("PlaceProbeBlock", userAcknowledged);
@@ -177,11 +181,7 @@
break;
case "BitDimensions":
nextButton.disabled = !(
cutterDiameter !== null &&
cutterDiameter !== 0 &&
isFinite(cutterDiameter)
);
nextButton.disabled = !isFinite(cutterDiameterMetric);
break;
case "Done":
@@ -204,8 +204,8 @@
const cutterLength = 12.7;
const zLift = 1;
const xOffset = probeBlockWidth + cutterDiameter / 2.0;
const yOffset = probeBlockLength + cutterDiameter / 2.0;
const xOffset = probeBlockWidth + cutterDiameterMetric / 2.0;
const yOffset = probeBlockLength + cutterDiameterMetric / 2.0;
const zOffset = probeBlockHeight;
if (probeType === "z") {
@@ -269,11 +269,11 @@
scrimClickAction=""
aria-labelledby="probe-dialog-title"
aria-describedby="probe-dialog-content"
surface$style="width: 700px; height: 400px; max-width: calc(100vw - 32px); overflow: visible;"
surface$style="width: 700px; max-width: calc(100vw - 32px);"
>
<Title id="probe-dialog-title">Probing {probeType?.toUpperCase()}</Title>
<Content id="probe-dialog-content" style="overflow: visible;">
<Content id="probe-dialog-content">
<div class="steps">
<p><b>Step {steps.indexOf(currentStep) + 1} of {steps.length}</b></p>
<ul>
@@ -282,48 +282,49 @@
{/each}
</ul>
</div>
<div class="current-step">
<p>
{#if currentStep === "CheckProbe"}
Attach the probe magnet to the collet, then touch the probe block to
the bit.
{:else if currentStep === "BitDimensions"}
<DimensionInput
label="Cutter diameter"
options={cutterDiameterOptions}
bind:value={cutterDiameter}
{metric}
/>
{:else if currentStep === "PlaceProbeBlock"}
<p>
{#if currentStep === "CheckProbe"}
Attach the probe magnet to the collet, then touch the probe block to the
bit.
{:else if currentStep === "BitDimensions"}
<TextFieldWithOptions
label="Cutter diameter"
variant="filled"
spellcheck="false"
style="width: 100%;"
bind:value={cutterDiameterString}
options={[imperialBits, metricBits]}
valid={isFinite(cutterDiameterMetric)}
helperText={`Examples: 1/2", 10 mm, 0.25 in`}
/>
{:else if currentStep === "PlaceProbeBlock"}
{#if probeType === "xyz"}
Place the probe block face up, on the lower-left corner of your
workpiece.
{:else}
Place the probe block face down, with the bit above the recess.
{/if}
{:else if currentStep === "Probe"}
Probing in progress...
{:else if currentStep === "Done"}
{#if $probingFailed}
Could not find the probe block during probing!
<p>
Make sure the tip of the bit is less than {metric ? "25mm" : "1 in"}
above the probe block, and try again.
</p>
{:else}
Don't forget to put away the probe!
{#if probeType === "xyz"}
Place the probe block face up, on the lower-left corner of your
workpiece.
{:else}
Place the probe block face down, with the bit above the recess.
{/if}
{:else if currentStep === "Probe"}
Probing in progress...
{:else if currentStep === "Done"}
{#if $probingFailed}
Could not find the probe block during probing!
<p>The machine will now move to the XY origin.</p>
<p>
Make sure the tip of the bit is less than {metric
? "25mm"
: "1 in"} above the probe block, and try again.
</p>
{:else}
Don't forget to put away the probe!
{#if probeType === "xyz"}
<p>The machine will now move to the XY origin.</p>
<p>Watch your hands!</p>
{/if}
<p>Watch your hands!</p>
{/if}
{/if}
</p>
</div>
{/if}
</p>
</Content>
<Actions>
@@ -358,12 +359,9 @@
flex-direction: row;
}
.current-step {
flex-grow: 1;
.mdc-text-field {
margin-bottom: 20px;
}
.bit-dimensions {
display: flex;
flex-direction: column;
}
.steps {

View File

@@ -3,7 +3,7 @@
import TextField from "@smui/textfield";
import Button, { Label } from "@smui/button";
import { ControllerMethods } from "$lib/RegisterControllerMethods";
import { virtualKeyboardChangeHelper } from "$lib/customActions";
import { virtualKeyboardChange } from "$lib/CustomActions";
export let open: boolean;
export let axis = "";
@@ -43,7 +43,7 @@
label="Position"
type="number"
bind:value
use={[[virtualKeyboardChangeHelper, (newValue) => (value = newValue)]]}
use={[[virtualKeyboardChange, (newValue) => (value = newValue)]]}
spellcheck="false"
variant="filled"
style="width: 100%;"

View File

@@ -5,7 +5,7 @@
import CircularProgress from "@smui/circular-progress";
import VirtualList from "svelte-tiny-virtual-list";
import * as api from "$lib/api";
import { virtualKeyboardChangeHelper } from "$lib/customActions";
import { virtualKeyboardChange } from "$lib/CustomActions";
const itemHeight = 35;
@@ -154,9 +154,7 @@
<Label>Date & Time</Label>
<TextField
bind:value
use={[
[virtualKeyboardChangeHelper, (newValue) => (value = newValue)],
]}
use={[[virtualKeyboardChange, (newValue) => (value = newValue)]]}
label="Time"
type="datetime-local"
variant="filled"

View File

@@ -7,7 +7,7 @@
import MessageDialog from "$dialogs/MessageDialog.svelte";
import type { WifiNetwork } from "$lib/NetworkInfo";
import * as api from "$lib/api";
import { virtualKeyboardChangeHelper } from "$lib/customActions";
import { virtualKeyboardChange } from "$lib/CustomActions";
export let open = false;
export let network: WifiNetwork;
@@ -57,7 +57,7 @@
{#if needPassword}
<TextField
bind:value={password}
use={[[virtualKeyboardChangeHelper, (newValue) => (password = newValue)]]}
use={[[virtualKeyboardChange, (newValue) => (password = newValue)]]}
label="Password"
spellcheck="false"
variant="filled"

View File

@@ -0,0 +1,9 @@
export function virtualKeyboardChange(node: HTMLElement, cb: (value: any) => void) {
const input = node.querySelector("input");
if (!input) {
console.error("Could not find the textfield's <input>:", node);
throw new Error("Could not find the textfield's <input>");
}
input.addEventListener("keyup", () => cb(input.value));
}

View File

@@ -0,0 +1,90 @@
type NumberWithUnit = {
value: number,
metric: boolean,
toMetric: () => number;
}
function numberWithUnitToMetric() {
return this.metric ? this.value : this.value * 25.4;
}
function isPojo(value) {
if (value === null || typeof value !== "object") {
return false;
}
return Object.getPrototypeOf(value) === Object.prototype;
}
const fractions = [
{ value: 0.75, formatted: "3/4" },
{ value: 0.625, formatted: "5/8" },
{ value: 0.5, formatted: "1/2" },
{ value: 0.375, formatted: "3/8" },
{ value: 0.25, formatted: "1/4" },
{ value: 0.1875, formatted: "3/16" },
{ value: 0.125, formatted: "1/8" },
{ value: 0.09375, formatted: "3/32" },
{ value: 0.0625, formatted: "1/16" },
{ value: 0.03125, formatted: "1/32" },
];
function formatFraction(value: number) {
const fraction = fractions.find(f => f.value === value);
return fraction ? fraction.formatted : value.toString();
}
export const numberWithUnit = {
regex: /^\s*(?:(\d+)\s*\/\s*(\d+)|(\d*\.\d+)|(\d+(?:\.\d+)?))\s*("|in|inch|inches|mm|millimeters)\s*$/,
parse: function (str: string) {
let [, numerator, denominator, decimal1, decimal2, unit]: any = str?.match(numberWithUnit.regex) ?? [];
numerator = Number.parseFloat(numerator)
denominator = Number.parseFloat(denominator)
decimal1 = Number.parseFloat(decimal1)
decimal2 = Number.parseFloat(decimal2)
const metric = (unit ?? "").includes("m");
switch (true) {
case isFinite(numerator) && isFinite(denominator):
return {
value: numerator / denominator,
metric,
toMetric: numberWithUnitToMetric
};
case isFinite(decimal1) && decimal1 !== 0:
return {
value: decimal1,
metric,
toMetric: numberWithUnitToMetric
};
case isFinite(decimal2) && decimal2 !== 0:
return {
value: decimal2,
metric,
toMetric: numberWithUnitToMetric
};
default:
return undefined;
}
},
normalize: function (str) {
const value = this.parse(str);
switch (true) {
case !isPojo(value):
return "";
case value.metric:
return `${value.value} mm`
default:
return `${formatFraction(value.value)} in`
}
}
}

View File

@@ -1,4 +0,0 @@
export function virtualKeyboardChangeHelper(node: HTMLElement, cb: (value: any) => void) {
const input = node.querySelector("input");
input.addEventListener("keyup", () => cb(input.value));
}

View File

@@ -15,7 +15,6 @@ export default defineConfig({
}
},
build: {
minify: false,
target: "chrome60",
lib: {
entry: resolve(__dirname, 'src/main.ts'),