@toolpath/tool-support 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,161 @@
1
+ /**
2
+ * How far a tool stands out of whatever holds it — the one answer.
3
+ *
4
+ * **There used to be four.** The same question was worked out in four
5
+ * unconnected places, and they disagreed by a factor of two on an ordinary
6
+ * tool:
7
+ *
8
+ * | where | what it computed | on a ⌀1 in end mill, `OAL` 5, `LCF` 1.25, `SFDM` 1 |
9
+ * | --------------------------------- | ----------------------------- | -------------------------------------------------- |
10
+ * | the clamping rule → `geometry.LBH` | `OAL − 3×SFDM` | 2.000 in |
11
+ * | the holding module's `max` | `OAL − OAL×heldShare` | 3.333 in |
12
+ * | its `default` → the drawing | flutes, floored and stepped | 1.250 in |
13
+ * | the hole-mode reach check | read `geometry.LBH` as a ceiling | 2.000 in |
14
+ *
15
+ * The details table printed the first and the drawing beside it drew the third,
16
+ * so a dimension line for `LBH` ran up past the holder nose and into the holder
17
+ * body. Neither of the first two consulted the other, and the two knobs behind
18
+ * them — a minimum clamping length (a length of **shank**) and a good hold (a
19
+ * share of the **overall length**) — were combined nowhere.
20
+ *
21
+ * **That bug is the single strongest argument for this package existing.** It
22
+ * was fixed inside one application, which left the next consumer of a tool
23
+ * catalog and a tool drawing to reproduce it from scratch: the quantity is a
24
+ * pure function of the tool, the collet and a shop's policy, and it had no home
25
+ * until this one.
26
+ *
27
+ * So this module owns the quantity outright and every other number is this same
28
+ * function with more arguments:
29
+ *
30
+ * ```
31
+ * geometry.LBH ≡ stickoutRange(tool).setup — no holder, no feature
32
+ * Assembly.stickout ≡ stickoutRange(tool, { grip, required }).setup
33
+ * the ceiling ≡ stickoutRange(tool, …).max
34
+ * ```
35
+ *
36
+ * `min ≤ setup ≤ max` holds by construction, so a drawn stickout can never
37
+ * exceed the length a table prints beside it. That invariant is a test rather
38
+ * than this sentence.
39
+ *
40
+ * **`LBH` is the setup length, not the ceiling.** The "below holder" column
41
+ * answers what a machinist would set the tool up at; the most it *could* stand
42
+ * out is {@link StickoutRange.max}, which is checked and reported but is not
43
+ * the column. What makes that reading workable is that the floor and the step
44
+ * reach it — {@link DEFAULT_STICKOUT_POLICY} carries `least` and `step`, and an
45
+ * earlier default carried zero for both and so produced the bare flute length.
46
+ */
47
+ import { DEFAULT_CLAMPING, clampWanted } from './clamping.js';
48
+ import { hasNeck } from './tool.js';
49
+ /**
50
+ * The share of a tool's overall length a holder must always have hold of.
51
+ *
52
+ * A third. **A shop's figure, not a vendor's** — no vendor in the scraped
53
+ * catalog publishes a minimum engagement — which is why it is named here and
54
+ * every control that shows it should say whose it is. Deliberately a share of
55
+ * the length and not a multiple of the shank diameter: how much of a tool a
56
+ * collet needs is about the tool's leverage, not its shank. That other reading
57
+ * is {@link ClampingRule}, and both are honoured — see {@link StickoutLimit}.
58
+ */
59
+ export const HELD_SHARE = 1 / 3;
60
+ /** What a dataset is built with, and what a page starts at. */
61
+ export const DEFAULT_STICKOUT_POLICY = {
62
+ heldShare: HELD_SHARE,
63
+ least: 12.7,
64
+ step: { inches: 3.175, millimeters: 3 },
65
+ };
66
+ const round = (value) => Math.round(value * 100) / 100;
67
+ /** A hair, so a rounded stickout a femtometre under what is needed is not stepped up. */
68
+ const STICKOUT_TOLERANCE = 1e-6;
69
+ /**
70
+ * The least a tool can stand out: its flutes, or its neck where it has one.
71
+ *
72
+ * The collet face sits at the end of the flutes, and a stated neck — which a
73
+ * collet must not close on — pushes it back to the shoulder. A tool that states
74
+ * no flute length has no known head, so it has no known stickout at all and
75
+ * this answers `null`; it carries no `LBH` either, rather than one derived from
76
+ * `OAL` and `SFDM` alone.
77
+ */
78
+ export const minStickout = (tool) => {
79
+ const { LCF } = tool.geometry;
80
+ if (LCF === undefined) {
81
+ return null;
82
+ }
83
+ const shoulder = tool.geometry['shoulder-length'];
84
+ return hasNeck(tool) && shoulder !== undefined ? shoulder : LCF;
85
+ };
86
+ /**
87
+ * The setup length before the ceiling: what is needed, no shorter than the
88
+ * policy's least, on the policy's step for this tool — the nearest step, or the
89
+ * one above it where the nearest falls short of what is needed.
90
+ */
91
+ const steppedTo = (tool, needed, policy) => {
92
+ const step = policy.step[tool.unitSystem];
93
+ const preferred = Math.max(needed, policy.least);
94
+ if (step <= 0) {
95
+ return preferred;
96
+ }
97
+ const nearest = Math.round(preferred / step) * step;
98
+ return nearest + STICKOUT_TOLERANCE < needed ? nearest + step : nearest;
99
+ };
100
+ /**
101
+ * Every stickout this tool has, in one answer.
102
+ *
103
+ * `null` only when the tool states no flute length, because then nothing about
104
+ * where it stands out of a holder can be worked out at all.
105
+ */
106
+ export const stickoutRange = (tool, request = {}) => {
107
+ const min = minStickout(tool);
108
+ if (min === null) {
109
+ return null;
110
+ }
111
+ const { grip = null, required = null, rule = DEFAULT_CLAMPING, policy = DEFAULT_STICKOUT_POLICY, } = request;
112
+ const { OAL } = tool.geometry;
113
+ /**
114
+ * The three ways of saying "this much stays in the holder", as three caps on
115
+ * one number. The tightest wins and says its name — which is the whole point
116
+ * of the module: a shop's sheet carries a minimum clamping length and a good
117
+ * hold as separate knobs, and before this they capped separate numbers in
118
+ * separate files and nothing ever compared them.
119
+ */
120
+ const caps = [];
121
+ if (OAL !== undefined) {
122
+ const clamp = clampWanted(tool.geometry, rule);
123
+ if (clamp !== null) {
124
+ caps.push({ by: 'clamp', at: OAL - clamp });
125
+ }
126
+ if (policy.heldShare > 0) {
127
+ caps.push({ by: 'hold', at: OAL * (1 - policy.heldShare) });
128
+ }
129
+ if (grip !== null) {
130
+ caps.push({ by: 'collet', at: OAL - grip });
131
+ }
132
+ }
133
+ const tightest = caps.reduce((best, cap) => (best === null || cap.at < best.at ? cap : best), null);
134
+ const gripShort = tightest !== null && tightest.at < min;
135
+ const max = tightest === null ? null : round(gripShort ? min : tightest.at);
136
+ const wanted = steppedTo(tool, Math.max(min, required ?? min), policy);
137
+ return {
138
+ min: round(min),
139
+ setup: round(max === null ? wanted : Math.min(wanted, max)),
140
+ max,
141
+ limitedBy: tightest?.by ?? null,
142
+ grip: OAL === undefined ? null : round(Math.max(0, OAL - min)),
143
+ wantedGrip: OAL === undefined || tightest === null ? null : round(OAL - tightest.at),
144
+ gripShort,
145
+ };
146
+ };
147
+ /**
148
+ * What this tool would be set up at on its own: no holder chosen and no feature
149
+ * to reach. This is `geometry.LBH`, and a build writes it with exactly this
150
+ * call.
151
+ */
152
+ export const setupStickout = (tool, rule = DEFAULT_CLAMPING, policy = DEFAULT_STICKOUT_POLICY) => stickoutRange(tool, { rule, policy })?.setup ?? null;
153
+ /**
154
+ * The furthest this tool can ever stand out of a holder, mm.
155
+ *
156
+ * **A reach check's number, not `LBH`.** A tap that will not reach the bottom of
157
+ * a hole at its setup length may reach it pulled further out, and asking `LBH` —
158
+ * which is the setup — would refuse it. Anything asking "could this tool get
159
+ * down there at all" asks this.
160
+ */
161
+ export const stickoutCeiling = (tool, rule = DEFAULT_CLAMPING, policy = DEFAULT_STICKOUT_POLICY) => stickoutRange(tool, { rule, policy })?.max ?? null;
package/dist/tool.d.ts ADDED
@@ -0,0 +1,80 @@
1
+ /**
2
+ * A cutting tool, as everything downstream of a scrape needs one.
3
+ *
4
+ * ## Deliberately not a catalog's record
5
+ *
6
+ * A catalog's tool carries identity and commerce — a guid, a brand, a catalog
7
+ * number, a product link, which material groups a vendor rates it for. None of
8
+ * that is arithmetic: nothing that draws a tool, fits it to a feature or works
9
+ * out how far it stands out of a holder reads a single one of those fields.
10
+ *
11
+ * So this is the *domain* shape and a catalog's record **extends** it, rather
12
+ * than this being a projection of a record. That direction matters: a record
13
+ * simply _is_ a {@link Tool}, with no adapter, which is what lets one number be
14
+ * computed once and drawn, printed and checked against the same value.
15
+ *
16
+ * `geometry` keeps the scraper's own field names — `DC`, `SFDM`, `OAL`, `LCF`,
17
+ * `RE`, `SIG`, `NOF`, `shoulder-diameter`, `shoulder-length`. See
18
+ * `geometry.ts` for why they are not renamed here.
19
+ *
20
+ * All lengths are in millimetres and all angles in degrees. `UnitSystem` is a
21
+ * fact about the vendor's sheet and is not carried here: it decides how a
22
+ * number is rounded and shown rather than what it is, and the arithmetic that
23
+ * needs it takes it as its own argument.
24
+ */
25
+ import type { Geometry } from './geometry.js';
26
+ import type { ProvenanceMap } from './provenance.js';
27
+ export interface Tool {
28
+ /** The CAM-library name for what the tool is: `flat end mill`, `drill`, `slot mill`. */
29
+ readonly form: string;
30
+ /** What a machinist calls this one tool — a catalog number, usually. */
31
+ readonly label?: string;
32
+ readonly geometry: Geometry;
33
+ readonly provenance?: ProvenanceMap;
34
+ }
35
+ /**
36
+ * Whether the section between the flutes and the shank is a neck: a stated
37
+ * shoulder past the flutes, narrower than the shank.
38
+ *
39
+ * A collet cannot close on a neck, so the tool stands out to its shoulder at
40
+ * least, and a sweep meets the wall with the neck at its own radius. A shoulder
41
+ * as wide as the shank is still a relief worth drawing, but it is plain shank.
42
+ *
43
+ * **This had a twin, and the twin is why this package exists.** One copy drew
44
+ * the picture and the other decided the verdict, and the note beside the second
45
+ * said it outright: *"If the rule ever changes, it changes in both places or the
46
+ * picture and the verdict disagree about the same tool."* Nothing was watching
47
+ * that. Now there is one rule, and a drawing and a clearance check cannot read a
48
+ * different tool out of the same numbers.
49
+ *
50
+ * Takes the geometry structurally rather than a whole {@link Tool}, so a
51
+ * catalog record with a narrower `Record<string, number>` satisfies it with no
52
+ * adapter.
53
+ */
54
+ export declare const hasNeck: (tool: {
55
+ readonly geometry: Geometry;
56
+ }) => boolean;
57
+ /** Whether the shank behind the flutes is reduced, or the full cutting diameter. */
58
+ export type Shank = 'reduced' | 'full';
59
+ /**
60
+ * Whether the shank behind the flutes is reduced.
61
+ *
62
+ * A **real relief**: a section immediately above the flutes that is a smaller
63
+ * diameter than the flute diameter *and has a length*. The distinction matters
64
+ * because vendors state a shoulder two different ways — 74 end mills in the
65
+ * scraped corpus have a genuine reduced shank, while 171 state a shoulder
66
+ * narrower than the cut whose shoulder length equals the flute length, which is
67
+ * no section to draw or to sweep. The second kind is not called reduced.
68
+ *
69
+ * `null` where no shoulder is stated at all, which is not a claim that the
70
+ * shank is full: it is nobody having said.
71
+ *
72
+ * Distinct from {@link hasNeck}, and both are needed. `hasNeck` asks whether
73
+ * there is a narrower section to draw and sweep *below the shank*; this asks
74
+ * whether the shank itself is reduced against the *cut*. A tool can have a
75
+ * relief wider than its cutting diameter — 860 end mills do, 245 of them under
76
+ * the cut — which is a neck to draw and not a reduced shank.
77
+ */
78
+ export declare const shankOf: (tool: {
79
+ readonly geometry: Geometry;
80
+ }) => Shank | null;
package/dist/tool.js ADDED
@@ -0,0 +1,89 @@
1
+ /**
2
+ * A cutting tool, as everything downstream of a scrape needs one.
3
+ *
4
+ * ## Deliberately not a catalog's record
5
+ *
6
+ * A catalog's tool carries identity and commerce — a guid, a brand, a catalog
7
+ * number, a product link, which material groups a vendor rates it for. None of
8
+ * that is arithmetic: nothing that draws a tool, fits it to a feature or works
9
+ * out how far it stands out of a holder reads a single one of those fields.
10
+ *
11
+ * So this is the *domain* shape and a catalog's record **extends** it, rather
12
+ * than this being a projection of a record. That direction matters: a record
13
+ * simply _is_ a {@link Tool}, with no adapter, which is what lets one number be
14
+ * computed once and drawn, printed and checked against the same value.
15
+ *
16
+ * `geometry` keeps the scraper's own field names — `DC`, `SFDM`, `OAL`, `LCF`,
17
+ * `RE`, `SIG`, `NOF`, `shoulder-diameter`, `shoulder-length`. See
18
+ * `geometry.ts` for why they are not renamed here.
19
+ *
20
+ * All lengths are in millimetres and all angles in degrees. `UnitSystem` is a
21
+ * fact about the vendor's sheet and is not carried here: it decides how a
22
+ * number is rounded and shown rather than what it is, and the arithmetic that
23
+ * needs it takes it as its own argument.
24
+ */
25
+ /**
26
+ * A hair, because a shoulder and a shank that are the same nominal size are the
27
+ * same float only until one of them has been through a unit conversion: 3/8" is
28
+ * 9.525 on the collet's sheet and 9.524999999999999 on the tool's.
29
+ */
30
+ const EPSILON = 1e-6;
31
+ /**
32
+ * Whether the section between the flutes and the shank is a neck: a stated
33
+ * shoulder past the flutes, narrower than the shank.
34
+ *
35
+ * A collet cannot close on a neck, so the tool stands out to its shoulder at
36
+ * least, and a sweep meets the wall with the neck at its own radius. A shoulder
37
+ * as wide as the shank is still a relief worth drawing, but it is plain shank.
38
+ *
39
+ * **This had a twin, and the twin is why this package exists.** One copy drew
40
+ * the picture and the other decided the verdict, and the note beside the second
41
+ * said it outright: *"If the rule ever changes, it changes in both places or the
42
+ * picture and the verdict disagree about the same tool."* Nothing was watching
43
+ * that. Now there is one rule, and a drawing and a clearance check cannot read a
44
+ * different tool out of the same numbers.
45
+ *
46
+ * Takes the geometry structurally rather than a whole {@link Tool}, so a
47
+ * catalog record with a narrower `Record<string, number>` satisfies it with no
48
+ * adapter.
49
+ */
50
+ export const hasNeck = (tool) => {
51
+ const { LCF, SFDM, DC } = tool.geometry;
52
+ const shoulder = tool.geometry['shoulder-length'];
53
+ const relief = tool.geometry['shoulder-diameter'];
54
+ if (shoulder === undefined || relief === undefined || LCF === undefined || shoulder <= LCF) {
55
+ return false;
56
+ }
57
+ const shank = SFDM ?? DC;
58
+ return shank === undefined ? true : relief < shank - EPSILON;
59
+ };
60
+ /**
61
+ * Whether the shank behind the flutes is reduced.
62
+ *
63
+ * A **real relief**: a section immediately above the flutes that is a smaller
64
+ * diameter than the flute diameter *and has a length*. The distinction matters
65
+ * because vendors state a shoulder two different ways — 74 end mills in the
66
+ * scraped corpus have a genuine reduced shank, while 171 state a shoulder
67
+ * narrower than the cut whose shoulder length equals the flute length, which is
68
+ * no section to draw or to sweep. The second kind is not called reduced.
69
+ *
70
+ * `null` where no shoulder is stated at all, which is not a claim that the
71
+ * shank is full: it is nobody having said.
72
+ *
73
+ * Distinct from {@link hasNeck}, and both are needed. `hasNeck` asks whether
74
+ * there is a narrower section to draw and sweep *below the shank*; this asks
75
+ * whether the shank itself is reduced against the *cut*. A tool can have a
76
+ * relief wider than its cutting diameter — 860 end mills do, 245 of them under
77
+ * the cut — which is a neck to draw and not a reduced shank.
78
+ */
79
+ export const shankOf = (tool) => {
80
+ const { DC, LCF } = tool.geometry;
81
+ const shoulder = tool.geometry['shoulder-diameter'];
82
+ const length = tool.geometry['shoulder-length'];
83
+ if (DC === undefined || shoulder === undefined) {
84
+ return null;
85
+ }
86
+ const narrower = shoulder < DC - EPSILON;
87
+ const real = length !== undefined && LCF !== undefined && length > LCF + EPSILON;
88
+ return narrower && real ? 'reduced' : 'full';
89
+ };
@@ -0,0 +1,73 @@
1
+ /**
2
+ * The one constant between the two measuring systems, and how a family states
3
+ * which it was published in.
4
+ *
5
+ * Three names for two values stood in this tree before this module: the
6
+ * scraper's `'millimeters' | 'inches'`, the catalog's `'metric' | 'inch'`, and
7
+ * a display unit spelled `'mm' | 'in'` — reconciled by a lookup table on
8
+ * ingest. A lookup table between two spellings of one axis is where a metric
9
+ * family quietly becomes an inch one, and nothing was watching it.
10
+ *
11
+ * The scraper's spelling wins because the scraper originates the fact: a vendor
12
+ * publishes a family in one system or the other, and every name downstream is a
13
+ * rename of what it said.
14
+ *
15
+ * ## The unit a number is *stored* in is not on this axis
16
+ *
17
+ * Every length this domain states is in millimetres and every angle in
18
+ * degrees, whatever system the vendor published — that is what lets an inch
19
+ * tool and a metric tool compare at all. {@link UnitSystem} is a fact *about
20
+ * the tool*: which system its vendor's sheet was written in, which is what
21
+ * decides how a number is rounded, stepped and shown. It is never the unit a
22
+ * stored value is in.
23
+ */
24
+ /** How a vendor publishes a family: not the unit a number is stored in. */
25
+ export type UnitSystem = 'millimeters' | 'inches';
26
+ /** Every {@link UnitSystem}, for a control that offers them or a message that lists them. */
27
+ export declare const UNIT_SYSTEMS: readonly UnitSystem[];
28
+ /** Exact by definition: the inch has been 25.4 mm since 1959. */
29
+ export declare const MM_PER_INCH = 25.4;
30
+ /**
31
+ * The short form, as a machinist writes it beside a number.
32
+ *
33
+ * **This constant is the third vocabulary, made into a map.** `'mm'` and
34
+ * `'in'` are not a second unit axis — they are how {@link UnitSystem} is
35
+ * spelled on a drawing — so they live here as a projection of it rather than
36
+ * as a type of their own that something has to convert onto.
37
+ */
38
+ export declare const UNIT_ABBREVIATION: Readonly<Record<UnitSystem, 'mm' | 'in'>>;
39
+ /** `value`, converted from `from` to `to`. A no-op when they agree. */
40
+ export declare const convertLength: (value: number, from: UnitSystem, to: UnitSystem) => number;
41
+ /**
42
+ * Decimals worth showing at a given size.
43
+ *
44
+ * A thousandth of an inch and a hundredth of a millimetre are about the same
45
+ * distance, and both are near the limit of what a mill holds — so each system
46
+ * gets the precision a machinist actually reads, rather than a fixed number of
47
+ * decimals that is either noise in one or useless in the other.
48
+ *
49
+ * The rounding is domain, not presentation: two consumers that round the same
50
+ * dimension differently print two different numbers for one tool.
51
+ */
52
+ export declare const decimalsFor: (system: UnitSystem) => number;
53
+ /**
54
+ * The same conversion, applied to an area.
55
+ *
56
+ * Areas scale with the *square* of the length conversion: 1 in² is 645.16 mm²,
57
+ * not 25.4. An area put through {@link convertLength} is wrong by a factor of
58
+ * an inch, which is large enough to read as a different pocket and small
59
+ * enough that nobody checks it.
60
+ */
61
+ export declare const convertArea: (value: number, from: UnitSystem, to: UnitSystem) => number;
62
+ /**
63
+ * A stored length, written the way a machinist reads it in `system`.
64
+ *
65
+ * The value is in millimetres, because every length this domain states is —
66
+ * see the note at the top of this module. What `system` decides is the unit it
67
+ * is *shown* in, the decimals it is rounded to, and the abbreviation beside it,
68
+ * which are three parts of one answer and belong together: two consumers that
69
+ * round the same dimension differently print two different numbers for one tool.
70
+ */
71
+ export declare const formatLength: (value: number, system: UnitSystem) => string;
72
+ /** {@link formatLength} for an area, squared units and all. */
73
+ export declare const formatArea: (value: number, system: UnitSystem) => string;
package/dist/units.js ADDED
@@ -0,0 +1,82 @@
1
+ /**
2
+ * The one constant between the two measuring systems, and how a family states
3
+ * which it was published in.
4
+ *
5
+ * Three names for two values stood in this tree before this module: the
6
+ * scraper's `'millimeters' | 'inches'`, the catalog's `'metric' | 'inch'`, and
7
+ * a display unit spelled `'mm' | 'in'` — reconciled by a lookup table on
8
+ * ingest. A lookup table between two spellings of one axis is where a metric
9
+ * family quietly becomes an inch one, and nothing was watching it.
10
+ *
11
+ * The scraper's spelling wins because the scraper originates the fact: a vendor
12
+ * publishes a family in one system or the other, and every name downstream is a
13
+ * rename of what it said.
14
+ *
15
+ * ## The unit a number is *stored* in is not on this axis
16
+ *
17
+ * Every length this domain states is in millimetres and every angle in
18
+ * degrees, whatever system the vendor published — that is what lets an inch
19
+ * tool and a metric tool compare at all. {@link UnitSystem} is a fact *about
20
+ * the tool*: which system its vendor's sheet was written in, which is what
21
+ * decides how a number is rounded, stepped and shown. It is never the unit a
22
+ * stored value is in.
23
+ */
24
+ /** Every {@link UnitSystem}, for a control that offers them or a message that lists them. */
25
+ export const UNIT_SYSTEMS = ['millimeters', 'inches'];
26
+ /** Exact by definition: the inch has been 25.4 mm since 1959. */
27
+ export const MM_PER_INCH = 25.4;
28
+ /**
29
+ * The short form, as a machinist writes it beside a number.
30
+ *
31
+ * **This constant is the third vocabulary, made into a map.** `'mm'` and
32
+ * `'in'` are not a second unit axis — they are how {@link UnitSystem} is
33
+ * spelled on a drawing — so they live here as a projection of it rather than
34
+ * as a type of their own that something has to convert onto.
35
+ */
36
+ export const UNIT_ABBREVIATION = {
37
+ millimeters: 'mm',
38
+ inches: 'in',
39
+ };
40
+ /** `value`, converted from `from` to `to`. A no-op when they agree. */
41
+ export const convertLength = (value, from, to) => {
42
+ if (from === to)
43
+ return value;
44
+ return to === 'inches' ? value / MM_PER_INCH : value * MM_PER_INCH;
45
+ };
46
+ /**
47
+ * Decimals worth showing at a given size.
48
+ *
49
+ * A thousandth of an inch and a hundredth of a millimetre are about the same
50
+ * distance, and both are near the limit of what a mill holds — so each system
51
+ * gets the precision a machinist actually reads, rather than a fixed number of
52
+ * decimals that is either noise in one or useless in the other.
53
+ *
54
+ * The rounding is domain, not presentation: two consumers that round the same
55
+ * dimension differently print two different numbers for one tool.
56
+ */
57
+ export const decimalsFor = (system) => (system === 'inches' ? 3 : 2);
58
+ /**
59
+ * The same conversion, applied to an area.
60
+ *
61
+ * Areas scale with the *square* of the length conversion: 1 in² is 645.16 mm²,
62
+ * not 25.4. An area put through {@link convertLength} is wrong by a factor of
63
+ * an inch, which is large enough to read as a different pocket and small
64
+ * enough that nobody checks it.
65
+ */
66
+ export const convertArea = (value, from, to) => {
67
+ if (from === to)
68
+ return value;
69
+ return to === 'inches' ? value / MM_PER_INCH ** 2 : value * MM_PER_INCH ** 2;
70
+ };
71
+ /**
72
+ * A stored length, written the way a machinist reads it in `system`.
73
+ *
74
+ * The value is in millimetres, because every length this domain states is —
75
+ * see the note at the top of this module. What `system` decides is the unit it
76
+ * is *shown* in, the decimals it is rounded to, and the abbreviation beside it,
77
+ * which are three parts of one answer and belong together: two consumers that
78
+ * round the same dimension differently print two different numbers for one tool.
79
+ */
80
+ export const formatLength = (value, system) => `${convertLength(value, 'millimeters', system).toFixed(decimalsFor(system))} ${UNIT_ABBREVIATION[system]}`;
81
+ /** {@link formatLength} for an area, squared units and all. */
82
+ export const formatArea = (value, system) => `${convertArea(value, 'millimeters', system).toFixed(decimalsFor(system))} ${UNIT_ABBREVIATION[system]}²`;
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@toolpath/tool-support",
3
+ "version": "0.1.0",
4
+ "description": "The cutting-tool domain: what a tool, holder, collet and assembly are",
5
+ "license": "MIT",
6
+ "engines": {
7
+ "node": ">=20"
8
+ },
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "https://github.com/toolpath/ui-packages.git",
12
+ "directory": "packages/tool-support"
13
+ },
14
+ "homepage": "https://developers.toolpath.com",
15
+ "bugs": {
16
+ "url": "https://github.com/toolpath/ui-packages/issues"
17
+ },
18
+ "publishConfig": {
19
+ "access": "public",
20
+ "registry": "https://registry.npmjs.org"
21
+ },
22
+ "type": "module",
23
+ "main": "./dist/index.js",
24
+ "types": "./dist/index.d.ts",
25
+ "exports": {
26
+ ".": {
27
+ "types": "./dist/index.d.ts",
28
+ "import": "./dist/index.js"
29
+ },
30
+ "./package.json": "./package.json"
31
+ },
32
+ "files": [
33
+ "dist",
34
+ "LICENSE",
35
+ "README.md"
36
+ ],
37
+ "sideEffects": false,
38
+ "scripts": {
39
+ "build": "tsc -p tsconfig.build.json",
40
+ "check-types": "tsc --noEmit",
41
+ "test": "vitest run"
42
+ },
43
+ "devDependencies": {
44
+ "@types/node": "24.10.1",
45
+ "typescript": "5.9.3",
46
+ "vitest": "4.1.10"
47
+ }
48
+ }