linked-rolls 0.21.0 → 0.22.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/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/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