linked-rolls 0.21.0 → 0.23.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.
- package/lib/Quantity.d.ts +18 -7
- package/lib/Quantity.js +11 -2
- package/lib/RollCopy.d.ts +3 -1
- package/lib/RollCopy.js +13 -16
- package/lib/TrackerBar.d.ts +25 -2
- package/lib/TrackerBar.js +26 -6
- package/lib/alignment.js +3 -2
- package/lib/constraints.js +4 -2
- package/package.json +1 -1
package/lib/Quantity.d.ts
CHANGED
|
@@ -7,8 +7,12 @@
|
|
|
7
7
|
* The unit names are the ones the records state in their `unit` field.
|
|
8
8
|
*/
|
|
9
9
|
declare const unit: unique symbol;
|
|
10
|
+
/**
|
|
11
|
+
* The units a record may state. A `Measure` is limited to these, since
|
|
12
|
+
* they are what the `unit` field is allowed to say.
|
|
13
|
+
*/
|
|
10
14
|
export type Unit = 'mm' | 'cm' | 'px' | 'track' | 's' | 'ms' | 'ft/min' | 'm/min' | 'px/in';
|
|
11
|
-
export type Quantity<U extends
|
|
15
|
+
export type Quantity<U extends string> = number & {
|
|
12
16
|
readonly [unit]: U;
|
|
13
17
|
};
|
|
14
18
|
/** A place along the roll, or any length on the paper. */
|
|
@@ -24,6 +28,8 @@ export type FeetPerMinute = Quantity<'ft/min'>;
|
|
|
24
28
|
export type MetersPerMinute = Quantity<'m/min'>;
|
|
25
29
|
/** How finely a scan was read: pixels of the image per inch of paper. */
|
|
26
30
|
export type Resolution = Quantity<'px/in'>;
|
|
31
|
+
/** Names a number in a unit. Partially apply it to make a constructor. */
|
|
32
|
+
export declare const quantity: <U extends string>(value: number) => Quantity<U>;
|
|
27
33
|
export declare const mm: (value: number) => Quantity<"mm">;
|
|
28
34
|
export declare const cm: (value: number) => Quantity<"cm">;
|
|
29
35
|
export declare const px: (value: number) => Quantity<"px">;
|
|
@@ -50,19 +56,24 @@ export interface Measure<U extends Unit> {
|
|
|
50
56
|
*/
|
|
51
57
|
unit: U;
|
|
52
58
|
}
|
|
53
|
-
export declare const add: <U extends
|
|
54
|
-
export declare const subtract: <U extends
|
|
55
|
-
export declare const distance: <U extends
|
|
59
|
+
export declare const add: <U extends string>(a: Quantity<U>, b: Quantity<NoInfer<U>>) => Quantity<U>;
|
|
60
|
+
export declare const subtract: <U extends string>(a: Quantity<U>, b: Quantity<NoInfer<U>>) => Quantity<U>;
|
|
61
|
+
export declare const distance: <U extends string>(a: Quantity<U>, b: Quantity<NoInfer<U>>) => Quantity<U>;
|
|
56
62
|
/** A quantity times a plain factor, such as a stretch. */
|
|
57
|
-
export declare const scale: <U extends
|
|
58
|
-
export declare const
|
|
59
|
-
export declare const
|
|
63
|
+
export declare const scale: <U extends string>(a: Quantity<U>, factor: number) => Quantity<U>;
|
|
64
|
+
export declare const min: <U extends string>(a: Quantity<U>, b: Quantity<NoInfer<U>>) => Quantity<U>;
|
|
65
|
+
export declare const max: <U extends string>(a: Quantity<U>, b: Quantity<NoInfer<U>>) => Quantity<U>;
|
|
66
|
+
/** The value brought inside the bounds, which must not be the wrong way round. */
|
|
67
|
+
export declare const clamp: <U extends string>(value: Quantity<U>, low: Quantity<NoInfer<U>>, high: Quantity<NoInfer<U>>) => Quantity<U>;
|
|
68
|
+
export declare const sum: <U extends string>(values: readonly Quantity<U>[]) => Quantity<U>;
|
|
69
|
+
export declare const mean: <U extends string>(values: readonly Quantity<U>[]) => Quantity<U>;
|
|
60
70
|
/** A place in a scan taken at `dpi` dots per inch, on the paper. */
|
|
61
71
|
export declare const inMillimeters: (place: Pixels, dpi: number) => Millimeters;
|
|
62
72
|
/** A place on the paper, in a scan taken at `dpi` dots per inch. */
|
|
63
73
|
export declare const inPixels: (place: Millimeters, dpi: number) => Pixels;
|
|
64
74
|
export declare const inCentimeters: (length: Millimeters) => Centimeters;
|
|
65
75
|
export declare const inSeconds: (time: Milliseconds) => Seconds;
|
|
76
|
+
export declare const inMilliseconds: (time: Seconds) => Milliseconds;
|
|
66
77
|
/** A speed as a record states it, in feet or metres per minute. */
|
|
67
78
|
export type SpeedMeasure = Measure<'ft/min'> | Measure<'m/min'>;
|
|
68
79
|
/** A speed in metres per minute, whichever unit it was stated in. */
|
package/lib/Quantity.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
/** Names a number in a unit. Partially apply it to make a constructor. */
|
|
2
|
+
export const quantity = (value) => value;
|
|
2
3
|
export const mm = quantity;
|
|
3
4
|
export const cm = quantity;
|
|
4
5
|
export const px = quantity;
|
|
@@ -15,6 +16,12 @@ export const subtract = (a, b) => quantity(a - b);
|
|
|
15
16
|
export const distance = (a, b) => quantity(Math.abs(a - b));
|
|
16
17
|
/** A quantity times a plain factor, such as a stretch. */
|
|
17
18
|
export const scale = (a, factor) => quantity(a * factor);
|
|
19
|
+
// Math.min and Math.max are declared over plain numbers, so a quantity
|
|
20
|
+
// passed through them comes back without its unit.
|
|
21
|
+
export const min = (a, b) => quantity(Math.min(a, b));
|
|
22
|
+
export const max = (a, b) => quantity(Math.max(a, b));
|
|
23
|
+
/** The value brought inside the bounds, which must not be the wrong way round. */
|
|
24
|
+
export const clamp = (value, low, high) => quantity(Math.min(high, Math.max(low, value)));
|
|
18
25
|
// A list is taken at its element type, so one that mixes units yields a
|
|
19
26
|
// union unit rather than an error; only the binary operations reject a mix.
|
|
20
27
|
export const sum = (values) => quantity(values.reduce((total, value) => total + value, 0));
|
|
@@ -25,7 +32,9 @@ export const inMillimeters = (place, dpi) => mm(place / dpi * MM_PER_INCH);
|
|
|
25
32
|
/** A place on the paper, in a scan taken at `dpi` dots per inch. */
|
|
26
33
|
export const inPixels = (place, dpi) => px(place / MM_PER_INCH * dpi);
|
|
27
34
|
export const inCentimeters = (length) => cm(length / 10);
|
|
28
|
-
|
|
35
|
+
const MS_PER_SECOND = 1000;
|
|
36
|
+
export const inSeconds = (time) => seconds(time / MS_PER_SECOND);
|
|
37
|
+
export const inMilliseconds = (time) => milliseconds(time * MS_PER_SECOND);
|
|
29
38
|
const METERS_PER_FOOT = 0.3048;
|
|
30
39
|
/** A speed in metres per minute, whichever unit it was stated in. */
|
|
31
40
|
export const inMetersPerMinute = (speed) => speed.unit === 'm/min' ? speed.value : metersPerMinute(speed.value * METERS_PER_FOOT);
|
package/lib/RollCopy.d.ts
CHANGED
|
@@ -332,7 +332,9 @@ export declare const barOf: (copy: Pick<RollCopy, 'production'>) => TrackerBar;
|
|
|
332
332
|
export declare function asSymbols(features: AnyFeature[], bar: TrackerBar): AnySymbol[];
|
|
333
333
|
/**
|
|
334
334
|
* The tracks a copy carries holes on that the tracker bar does not read.
|
|
335
|
-
* A non-empty result usually means the scan is calibrated wrongly.
|
|
335
|
+
* A non-empty result usually means the scan is calibrated wrongly. A hole
|
|
336
|
+
* lying across several positions is counted against each one the bar
|
|
337
|
+
* cannot read, and not at all where it reads them all.
|
|
336
338
|
*/
|
|
337
339
|
export declare function unreadTracks(features: AnyFeature[], bar: TrackerBar): Map<Track, number>;
|
|
338
340
|
/**
|
package/lib/RollCopy.js
CHANGED
|
@@ -40,30 +40,27 @@ export function asSymbols(features, bar) {
|
|
|
40
40
|
const end = bar.endsAt(holes);
|
|
41
41
|
return holes
|
|
42
42
|
.filter(feature => end === undefined || feature.horizontal.from <= end.at)
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
carriers: [assignReference(feature.id)]
|
|
51
|
-
}];
|
|
52
|
-
});
|
|
43
|
+
// An opening across two positions uncovers both bar holes and so
|
|
44
|
+
// reads as both commands; one on a single position gives the one.
|
|
45
|
+
.flatMap((feature) => bar.meaningsOf(feature.vertical).map(meaning => ({
|
|
46
|
+
id: `symbol_${v4()}`,
|
|
47
|
+
...meaning,
|
|
48
|
+
carriers: [assignReference(feature.id)]
|
|
49
|
+
})));
|
|
53
50
|
}
|
|
54
51
|
/**
|
|
55
52
|
* The tracks a copy carries holes on that the tracker bar does not read.
|
|
56
|
-
* A non-empty result usually means the scan is calibrated wrongly.
|
|
53
|
+
* A non-empty result usually means the scan is calibrated wrongly. A hole
|
|
54
|
+
* lying across several positions is counted against each one the bar
|
|
55
|
+
* cannot read, and not at all where it reads them all.
|
|
57
56
|
*/
|
|
58
57
|
export function unreadTracks(features, bar) {
|
|
59
58
|
const counts = new Map();
|
|
60
59
|
features
|
|
61
60
|
.filter(feature => feature.type === 'Hole')
|
|
62
|
-
.
|
|
63
|
-
.
|
|
64
|
-
|
|
65
|
-
counts.set(position, (counts.get(position) || 0) + 1);
|
|
66
|
-
});
|
|
61
|
+
.flatMap(feature => bar.positionsIn(feature.vertical))
|
|
62
|
+
.filter(position => !bar.meaningOf(position))
|
|
63
|
+
.forEach(position => counts.set(position, (counts.get(position) || 0) + 1));
|
|
67
64
|
return counts;
|
|
68
65
|
}
|
|
69
66
|
/**
|
package/lib/TrackerBar.d.ts
CHANGED
|
@@ -67,8 +67,20 @@ export interface TrackerBar {
|
|
|
67
67
|
* the symbols afterwards.
|
|
68
68
|
*/
|
|
69
69
|
endsAt(features: readonly PlacedOnBar[]): RollEnd | undefined;
|
|
70
|
-
/**
|
|
70
|
+
/**
|
|
71
|
+
* `undefined` for a position the bar does not read. A measured place
|
|
72
|
+
* is snapped to the nearest position: perforations sit on the grid and
|
|
73
|
+
* measurements of them scatter around it.
|
|
74
|
+
*/
|
|
71
75
|
meaningOf(position: Track): TrackMeaning | undefined;
|
|
76
|
+
/**
|
|
77
|
+
* What the bar reads off a feature, which may lie across more than one
|
|
78
|
+
* position. A perforation lifts every valve whose bar hole it uncovers,
|
|
79
|
+
* so an opening across two positions reads as two commands; one on a
|
|
80
|
+
* single position reads as the one `meaningOf` gives, and one on
|
|
81
|
+
* positions the bar does not read as none at all.
|
|
82
|
+
*/
|
|
83
|
+
meaningsOf(span: OnBar): readonly TrackMeaning[];
|
|
72
84
|
/**
|
|
73
85
|
* `meaningOf` inverted: the position this bar reads the meaning on,
|
|
74
86
|
* or `undefined` where it does not read it at all. A symbol's track
|
|
@@ -76,8 +88,10 @@ export interface TrackerBar {
|
|
|
76
88
|
* sits on exactly one position of a given bar.
|
|
77
89
|
*/
|
|
78
90
|
positionOf(meaning: TrackMeaning): Track | undefined;
|
|
79
|
-
/** `undefined` for a position the bar does not read. */
|
|
91
|
+
/** `undefined` for a position the bar does not read; snapped as `meaningOf` is. */
|
|
80
92
|
roleOf(position: Track): TrackRole | undefined;
|
|
93
|
+
/** The positions a feature lies across, snapped to the grid. */
|
|
94
|
+
positionsIn(span: OnBar): readonly Track[];
|
|
81
95
|
}
|
|
82
96
|
/**
|
|
83
97
|
* What a position says, as a key. Two bars read the same thing exactly
|
|
@@ -118,6 +132,15 @@ export type PlacedOnBar = {
|
|
|
118
132
|
readonly from: Track;
|
|
119
133
|
};
|
|
120
134
|
};
|
|
135
|
+
/**
|
|
136
|
+
* Where a feature lies across the bar: one place, or a run of them where
|
|
137
|
+
* `to` is given. Structural, so a feature's `vertical` passes as it is and
|
|
138
|
+
* this module need know nothing about features.
|
|
139
|
+
*/
|
|
140
|
+
export type OnBar = {
|
|
141
|
+
readonly from: Track;
|
|
142
|
+
readonly to?: Track;
|
|
143
|
+
};
|
|
121
144
|
export interface TrackerBarSpec {
|
|
122
145
|
id: string;
|
|
123
146
|
name: string;
|
package/lib/TrackerBar.js
CHANGED
|
@@ -20,24 +20,42 @@ const areasOf = ({ notes, trackCount }) => [
|
|
|
20
20
|
{ role: 'treble-expression', from: track(notes.to + 1), to: track(trackCount) }
|
|
21
21
|
];
|
|
22
22
|
const scopeOf = (role) => role === 'bass-expression' ? 'bass' : 'treble';
|
|
23
|
+
/**
|
|
24
|
+
* The bar position a measured place falls on. Places are measured off a
|
|
25
|
+
* scan and scatter around the grid, while the bar has holes only at whole
|
|
26
|
+
* positions, so the nearest one is the one uncovered.
|
|
27
|
+
*/
|
|
28
|
+
const snap = (place) => track(Math.round(place));
|
|
29
|
+
/** The positions a span reaches, both ends snapped and the run between them. */
|
|
30
|
+
const positionsBetween = (span) => {
|
|
31
|
+
const ends = [snap(span.from), snap(span.to ?? span.from)];
|
|
32
|
+
const [first, last] = [Math.min(...ends), Math.max(...ends)];
|
|
33
|
+
return Array.from({ length: last - first + 1 }, (_, step) => track(first + step));
|
|
34
|
+
};
|
|
23
35
|
export const describeTrackerBar = (spec) => {
|
|
24
36
|
const areas = areasOf(spec);
|
|
25
|
-
const
|
|
37
|
+
const areaAt = (position) => areas.find(area => position >= area.from && position <= area.to)?.role;
|
|
38
|
+
const roleOf = (position) => areaAt(snap(position));
|
|
26
39
|
const meaningOf = (position) => {
|
|
27
|
-
const
|
|
40
|
+
const place = snap(position);
|
|
41
|
+
const role = areaAt(place);
|
|
28
42
|
if (!role)
|
|
29
43
|
return undefined;
|
|
30
44
|
if (role === 'note') {
|
|
31
45
|
return {
|
|
32
46
|
type: 'note',
|
|
33
|
-
pitch:
|
|
47
|
+
pitch: place - spec.notes.from + spec.notes.lowestPitch
|
|
34
48
|
};
|
|
35
49
|
}
|
|
36
|
-
const expressionType = spec.expressions.get(
|
|
50
|
+
const expressionType = spec.expressions.get(place);
|
|
37
51
|
if (!expressionType)
|
|
38
52
|
return undefined;
|
|
39
53
|
return { type: 'expression', expressionType, scope: scopeOf(role) };
|
|
40
54
|
};
|
|
55
|
+
const meaningsOf = (span) => positionsBetween(span).flatMap(position => {
|
|
56
|
+
const meaning = meaningOf(position);
|
|
57
|
+
return meaning ? [meaning] : [];
|
|
58
|
+
});
|
|
41
59
|
const positions = new Map(Array.from({ length: spec.trackCount }, (_, i) => track(i + 1))
|
|
42
60
|
.flatMap(position => {
|
|
43
61
|
const meaning = meaningOf(position);
|
|
@@ -54,7 +72,7 @@ export const describeTrackerBar = (spec) => {
|
|
|
54
72
|
const endsAt = (features) => {
|
|
55
73
|
const hold = spec.rewindHold ?? mm(0);
|
|
56
74
|
const candidates = features
|
|
57
|
-
.filter(feature => feature.vertical.from === rewind)
|
|
75
|
+
.filter(feature => snap(feature.vertical.from) === rewind)
|
|
58
76
|
.filter(feature => !shared || feature.horizontal.to - feature.horizontal.from >= hold)
|
|
59
77
|
.map(feature => feature.horizontal.from);
|
|
60
78
|
return candidates.length ? { at: mm(Math.min(...candidates)), because: 'rewind' } : undefined;
|
|
@@ -70,8 +88,10 @@ export const describeTrackerBar = (spec) => {
|
|
|
70
88
|
rewindTrack: track(rewind),
|
|
71
89
|
...(spec.paperSpeed && { paperSpeed: spec.paperSpeed }),
|
|
72
90
|
meaningOf,
|
|
91
|
+
meaningsOf,
|
|
73
92
|
positionOf: meaning => positions.get(keyOf(meaning)),
|
|
74
|
-
roleOf
|
|
93
|
+
roleOf,
|
|
94
|
+
positionsIn: positionsBetween
|
|
75
95
|
};
|
|
76
96
|
};
|
|
77
97
|
/**
|
package/lib/alignment.js
CHANGED
|
@@ -59,8 +59,9 @@ const noteOnsets = (features, bar) => features
|
|
|
59
59
|
.flatMap((feature) => {
|
|
60
60
|
if (feature.type !== 'Hole')
|
|
61
61
|
return [];
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
return bar.meaningsOf(feature.vertical)
|
|
63
|
+
.filter(meaning => meaning.type === 'note')
|
|
64
|
+
.map(meaning => ({ pitch: meaning.pitch, at: feature.horizontal.from }));
|
|
64
65
|
})
|
|
65
66
|
.sort(byPlace);
|
|
66
67
|
const median = (values) => {
|
package/lib/constraints.js
CHANGED
|
@@ -79,8 +79,10 @@ const carriersOffTheirMeaning = (view, version, perforations) => {
|
|
|
79
79
|
const copy = view.copyOf(carrier.id);
|
|
80
80
|
if (!copy)
|
|
81
81
|
return false;
|
|
82
|
-
|
|
83
|
-
|
|
82
|
+
// A carrier lying across several positions reads as several commands,
|
|
83
|
+
// and carries the symbol as long as one of them is the symbol's.
|
|
84
|
+
return !barOf(copy).meaningsOf(carrier.vertical)
|
|
85
|
+
.some(meaning => keyOf(meaning) === keyOf(symbol));
|
|
84
86
|
};
|
|
85
87
|
return perforations
|
|
86
88
|
.filter(symbol => view.carriersOf(symbol).some(carrier => misread(carrier, symbol)))
|
package/package.json
CHANGED