linked-rolls 0.10.1 → 0.12.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.
Files changed (42) hide show
  1. package/lib/Collation.d.ts +1 -1
  2. package/lib/Collation.js +53 -14
  3. package/lib/EditionView.d.ts +11 -0
  4. package/lib/EditionView.js +78 -82
  5. package/lib/Emulation.js +10 -16
  6. package/lib/Quantity.d.ts +4 -0
  7. package/lib/Quantity.js +3 -0
  8. package/lib/RollCopy.d.ts +43 -1
  9. package/lib/TrackerBar.d.ts +16 -1
  10. package/lib/TrackerBar.js +21 -0
  11. package/lib/Version.d.ts +5 -0
  12. package/lib/Version.js +4 -0
  13. package/lib/alignment.d.ts +8 -8
  14. package/lib/alignment.js +15 -15
  15. package/lib/constraints.js +4 -2
  16. package/lib/editionOps.d.ts +13 -5
  17. package/lib/editionOps.js +87 -37
  18. package/lib/importJsonLd.d.ts +3 -1
  19. package/lib/importJsonLd.js +22 -42
  20. package/lib/index.d.ts +3 -1
  21. package/lib/index.js +3 -1
  22. package/lib/migrate.js +34 -6
  23. package/lib/readers/spencerBar.d.ts +34 -0
  24. package/lib/readers/spencerBar.js +141 -0
  25. package/lib/readers/stanfordAton.d.ts +9 -1
  26. package/lib/readers/stanfordAton.js +30 -24
  27. package/lib/schema.json +90 -1
  28. package/lib/sorted.d.ts +6 -0
  29. package/lib/sorted.js +17 -0
  30. package/lib/spec/context.json +11 -1
  31. package/lib/systems/index.d.ts +6 -0
  32. package/lib/systems/index.js +10 -0
  33. package/lib/systems/welteLicensee/bar.d.ts +11 -0
  34. package/lib/systems/welteLicensee/bar.js +38 -0
  35. package/lib/systems/welteT100/bar.d.ts +2 -1
  36. package/lib/systems/welteT100/bar.js +4 -2
  37. package/lib/systems/welteT100/system.js +5 -4
  38. package/package.json +1 -1
  39. package/lib/readers/midiSpans.d.ts +0 -25
  40. package/lib/readers/midiSpans.js +0 -112
  41. package/lib/readers/spencerMidi.d.ts +0 -27
  42. package/lib/readers/spencerMidi.js +0 -60
@@ -26,5 +26,5 @@ export type Collation = {
26
26
  symbol: Readonly<AnySymbol>;
27
27
  counterpart: Readonly<AnySymbol>;
28
28
  };
29
- /** Each of the own symbols with every inherited symbol it collates with. */
29
+ /** Each of the own symbols with every inherited symbol it collates with, both in the order given. */
30
30
  export declare const collationsOf: (own: readonly Readonly<AnySymbol>[], inherited: readonly Readonly<AnySymbol>[], locate: Locate, tolerance?: CollationTolerance) => Collation[];
package/lib/Collation.js CHANGED
@@ -1,26 +1,65 @@
1
1
  import { distance, mm } from "./Quantity";
2
+ import { partitionPoint } from "./sorted";
2
3
  export const defaultCollationTolerance = { toleranceStart: mm(5), toleranceEnd: mm(5) };
4
+ /**
5
+ * What a symbol says, as a key: the pitch of a note, the type and scope
6
+ * of an expression. Symbols collate within one key only.
7
+ */
8
+ const kindOf = (symbol) => {
9
+ switch (symbol.type) {
10
+ case 'note': return `note ${symbol.pitch}`;
11
+ case 'expression': return `expression ${symbol.scope} ${symbol.expressionType}`;
12
+ case 'text': return 'text';
13
+ }
14
+ };
15
+ const nearby = (here, there, tolerance) => distance(here.from, there.from) <= tolerance.toleranceStart
16
+ && distance(here.to, there.to) <= tolerance.toleranceEnd;
3
17
  /**
4
18
  * Two symbols collate when they are of one kind, say the same thing
5
19
  * (pitch, or expression type and scope), and lie at about the same
6
20
  * place along the roll.
7
21
  */
8
22
  export const isCollatable = (a, b, locate, tolerance = defaultCollationTolerance) => {
9
- if (a.type !== b.type)
10
- return false;
11
- if (a.type === 'note' && b.type === 'note' && a.pitch !== b.pitch)
12
- return false;
13
- if (a.type === 'expression' && b.type === 'expression'
14
- && (a.expressionType !== b.expressionType || a.scope !== b.scope))
23
+ if (kindOf(a) !== kindOf(b))
15
24
  return false;
16
25
  const here = locate(a)?.horizontal;
17
26
  const there = locate(b)?.horizontal;
18
- if (!here || !there)
19
- return false;
20
- return distance(here.from, there.from) <= tolerance.toleranceStart
21
- && distance(here.to, there.to) <= tolerance.toleranceEnd;
27
+ return here !== undefined && there !== undefined && nearby(here, there, tolerance);
28
+ };
29
+ const placed = (symbols, locate) => symbols.flatMap((symbol, index) => {
30
+ const horizontal = locate(symbol)?.horizontal;
31
+ return horizontal ? [{ symbol, index, horizontal }] : [];
32
+ });
33
+ const groupBy = (items, keyOf) => items.reduce((groups, item) => {
34
+ const key = keyOf(item);
35
+ const group = groups.get(key);
36
+ if (group)
37
+ group.push(item);
38
+ else
39
+ groups.set(key, [item]);
40
+ return groups;
41
+ }, new Map());
42
+ /** The placed symbols by kind, each kind in order of onset. */
43
+ const byKindInOrderOfOnset = (symbols) => {
44
+ const groups = groupBy(symbols, ({ symbol }) => kindOf(symbol));
45
+ groups.forEach(group => group.sort((a, b) => a.horizontal.from - b.horizontal.from));
46
+ return groups;
47
+ };
48
+ /** Widens the onset window by a hair, so that rounding in its bounds cannot leave out what `nearby` accepts. */
49
+ const WINDOW_SLACK = 1e-9;
50
+ /** The symbols of a kind whose onset lies within the start tolerance of the span. */
51
+ const nearOnsetOf = (kind, span, tolerance) => {
52
+ const lowest = span.from - tolerance.toleranceStart - WINDOW_SLACK;
53
+ const highest = span.from + tolerance.toleranceStart + WINDOW_SLACK;
54
+ const first = partitionPoint(kind, candidate => candidate.horizontal.from < lowest);
55
+ const end = partitionPoint(kind, candidate => candidate.horizontal.from <= highest);
56
+ return kind.slice(first, end);
57
+ };
58
+ /** Each of the own symbols with every inherited symbol it collates with, both in the order given. */
59
+ export const collationsOf = (own, inherited, locate, tolerance = defaultCollationTolerance) => {
60
+ const kinds = byKindInOrderOfOnset(placed(inherited, locate));
61
+ return placed(own, locate).flatMap(({ symbol, horizontal }) => nearOnsetOf(kinds.get(kindOf(symbol)) ?? [], horizontal, tolerance)
62
+ .filter(candidate => nearby(horizontal, candidate.horizontal, tolerance))
63
+ .sort((a, b) => a.index - b.index)
64
+ .map(({ symbol: counterpart }) => ({ symbol, counterpart })));
22
65
  };
23
- /** Each of the own symbols with every inherited symbol it collates with. */
24
- export const collationsOf = (own, inherited, locate, tolerance = defaultCollationTolerance) => own.flatMap(symbol => inherited
25
- .filter(candidate => isCollatable(symbol, candidate, locate, tolerance))
26
- .map(counterpart => ({ symbol, counterpart })));
@@ -3,6 +3,7 @@ import { HorizontalSpan, VerticalSpan, AnyFeature } from "./Feature";
3
3
  import { AnySymbol, Expression, Note } from "./Symbol";
4
4
  import { Version } from "./Version";
5
5
  import { NegotiatedEvent } from "./ReproducingSystem";
6
+ import { Millimeters } from "./Quantity";
6
7
  export type Path = (string | number)[];
7
8
  export declare const getAt: <T>(path: Path, obj: unknown) => T | undefined;
8
9
  export declare class EditionView {
@@ -27,12 +28,22 @@ export declare class EditionView {
27
28
  getPath(anyId: string): Path | undefined;
28
29
  linksTo(anyId: string): Path[];
29
30
  travelUp(versionId: string, callback: (version: Readonly<Version>) => void): void;
31
+ /** The version and its ancestors, from the version up to the root. */
32
+ private lineageOf;
30
33
  carriersOf(symbol: AnySymbol): Readonly<AnyFeature>[];
31
34
  predecessorOf(versionId: string): Readonly<Version> | undefined;
32
35
  dimensionOf(symbol: AnySymbol): Readonly<{
33
36
  horizontal: HorizontalSpan;
34
37
  vertical: VerticalSpan;
35
38
  }> | undefined;
39
+ /** Where the symbol begins, as the mean onset of its carriers, or nothing for a symbol without a place. */
40
+ onsetOf(symbol: AnySymbol): Millimeters | undefined;
41
+ /** The symbols by onset, those without a place first; symbols at one place keep their order. */
42
+ private inOrderOfPlace;
43
+ /**
44
+ * The symbols the version shows: what it and its ancestors insert,
45
+ * each version's deletions striking what it or its ancestors inserted.
46
+ */
36
47
  snapshot(versionId: string): readonly Readonly<AnySymbol>[];
37
48
  /**
38
49
  * Assigns a generation (depth) to every node.
@@ -1,3 +1,4 @@
1
+ import { deletedBy, insertedBy } from "./Version";
1
2
  import { idOf, idsOf } from "./Assumption";
2
3
  import { mean } from "./Quantity";
3
4
  export const getAt = (path, obj) => {
@@ -9,11 +10,17 @@ export const getAt = (path, obj) => {
9
10
  }
10
11
  return node;
11
12
  };
12
- const referenceKeys = new Set([
13
- 'delete',
14
- 'comprehends',
15
- 'motivation'
16
- ]);
13
+ /** Keys under which an object names others by id, singly or in a list. */
14
+ const referenceKeys = ['delete', 'comprehends', 'motivation'];
15
+ const isObject = (v) => v !== null && typeof v === "object";
16
+ /** An object that names another by its id and says nothing else, save perhaps a belief about the reference. */
17
+ const isReferenceOnly = (keys) => keys.every(key => key === 'id' || key === '@annotation');
18
+ const pathOf = (trail) => {
19
+ const path = [];
20
+ for (let link = trail; link !== null; link = link.up)
21
+ path.push(link.key);
22
+ return path.reverse();
23
+ };
17
24
  export class EditionView {
18
25
  constructor(edition) {
19
26
  Object.defineProperty(this, "edition", {
@@ -58,63 +65,48 @@ export class EditionView {
58
65
  }
59
66
  indexObjects() {
60
67
  const visited = new WeakSet();
61
- const isObject = (v) => v !== null && typeof v === "object";
62
- const traverse = (node, path) => {
63
- if (!isObject(node))
68
+ const link = (id, trail) => {
69
+ const trails = this.links.get(id);
70
+ if (trails)
71
+ trails.push(trail);
72
+ else
73
+ this.links.set(id, [trail]);
74
+ };
75
+ const index = (record, keys, trail) => {
76
+ if (typeof record.id !== "string")
64
77
  return;
65
- if (visited.has(node))
78
+ if (isReferenceOnly(keys))
79
+ link(record.id, { key: 'id', up: trail });
80
+ else if (!this.byId.has(record.id)) {
81
+ this.byId.set(record.id, record);
82
+ this.paths.set(record.id, trail);
83
+ }
84
+ };
85
+ const linkReferences = (record, trail) => referenceKeys.forEach(key => {
86
+ const ref = record[key];
87
+ if (typeof ref === "string")
88
+ link(ref, { key, up: trail });
89
+ else if (Array.isArray(ref))
90
+ ref.forEach((r, i) => {
91
+ if (typeof r === "string")
92
+ link(r, { key: i, up: { key, up: trail } });
93
+ });
94
+ });
95
+ const traverse = (node, trail) => {
96
+ if (!isObject(node) || visited.has(node))
66
97
  return;
67
98
  visited.add(node);
68
- const anyNode = node;
69
- if (typeof anyNode.id === "string") {
70
- if (Object.keys(anyNode).filter(k => k !== '@annotation').length === 1) {
71
- // this is a reference-only object, store link
72
- if (!this.links.has(anyNode.id)) {
73
- this.links.set(anyNode.id, new Set());
74
- }
75
- this.links.get(anyNode.id).add([...path, 'id']);
76
- }
77
- else {
78
- if (!this.byId.has(anyNode.id)) {
79
- this.byId.set(anyNode.id, node);
80
- this.paths.set(anyNode.id, [...path]);
81
- }
82
- }
83
- }
84
- Object
85
- .keys(anyNode)
86
- .filter(k => referenceKeys.has(k))
87
- .forEach(key => {
88
- const ref = anyNode[key];
89
- if (typeof ref === "string") {
90
- if (!this.links.has(ref)) {
91
- this.links.set(ref, new Set());
92
- }
93
- this.links.get(ref).add([...path, key]);
94
- }
95
- else if (Array.isArray(ref)) {
96
- for (const r of ref) {
97
- if (typeof r === "string") {
98
- if (!this.links.has(anyNode.id)) {
99
- this.links.set(anyNode.id, new Set());
100
- }
101
- this.links.get(anyNode.id).add([...path, key]);
102
- }
103
- }
104
- }
105
- });
106
99
  if (Array.isArray(node)) {
107
- for (let i = 0; i < node.length; i++) {
108
- traverse(node[i], [...path, i]);
109
- }
110
- }
111
- else {
112
- for (const key of Object.keys(node)) {
113
- traverse(anyNode[key], [...path, key]);
114
- }
100
+ node.forEach((item, i) => traverse(item, { key: i, up: trail }));
101
+ return;
115
102
  }
103
+ const record = node;
104
+ const keys = Object.keys(record);
105
+ index(record, keys, trail);
106
+ linkReferences(record, trail);
107
+ keys.forEach(key => traverse(record[key], { key, up: trail }));
116
108
  };
117
- traverse(this.edition, []);
109
+ traverse(this.edition, null);
118
110
  }
119
111
  get(anyId) {
120
112
  return this.byId.get(anyId);
@@ -123,10 +115,11 @@ export class EditionView {
123
115
  return anyIds.map(id => this.get(id)).filter((v) => !!v);
124
116
  }
125
117
  getPath(anyId) {
126
- return this.paths.get(anyId);
118
+ const trail = this.paths.get(anyId);
119
+ return trail === undefined ? undefined : pathOf(trail);
127
120
  }
128
121
  linksTo(anyId) {
129
- return Array.from(this.links.get(anyId) || []);
122
+ return (this.links.get(anyId) ?? []).map(pathOf);
130
123
  }
131
124
  travelUp(versionId, callback) {
132
125
  const v = this.get(versionId);
@@ -137,6 +130,12 @@ export class EditionView {
137
130
  this.travelUp(idOf(v.basedOn), callback);
138
131
  }
139
132
  }
133
+ /** The version and its ancestors, from the version up to the root. */
134
+ lineageOf(versionId) {
135
+ const lineage = [];
136
+ this.travelUp(versionId, version => lineage.push(version));
137
+ return lineage;
138
+ }
140
139
  carriersOf(symbol) {
141
140
  return this.getAll(idsOf(symbol.carriers));
142
141
  }
@@ -165,32 +164,29 @@ export class EditionView {
165
164
  }
166
165
  };
167
166
  }
167
+ /** Where the symbol begins, as the mean onset of its carriers, or nothing for a symbol without a place. */
168
+ onsetOf(symbol) {
169
+ const carriers = this.carriersOf(symbol);
170
+ return carriers.length > 0 ? mean(carriers.map(carrier => carrier.horizontal.from)) : undefined;
171
+ }
172
+ /** The symbols by onset, those without a place first; symbols at one place keep their order. */
173
+ inOrderOfPlace(symbols) {
174
+ return symbols
175
+ .map(symbol => ({ symbol, at: this.onsetOf(symbol) || 0 }))
176
+ .sort((a, b) => a.at - b.at)
177
+ .map(({ symbol }) => symbol);
178
+ }
179
+ /**
180
+ * The symbols the version shows: what it and its ancestors insert,
181
+ * each version's deletions striking what it or its ancestors inserted.
182
+ */
168
183
  snapshot(versionId) {
169
- const snapshot = [];
170
- const toDelete = [];
171
- this.travelUp(versionId, s => {
172
- snapshot.push(...s.edits.flatMap(edit => edit.insert || []));
173
- // as we travel further up, remove symbols that are
174
- // deleted in the versions further down
175
- const deleted = [];
176
- for (const toRemove of toDelete) {
177
- const index = snapshot.findIndex(s => s.id === toRemove);
178
- if (index !== -1) {
179
- snapshot.splice(index, 1);
180
- deleted.push(toRemove);
181
- }
182
- }
183
- for (const del of deleted) {
184
- toDelete.splice(toDelete.indexOf(del), 1);
185
- }
186
- // collect symbols that are deleted in the current version
187
- toDelete.push(...s.edits.flatMap(edit => edit.delete || []));
188
- });
189
- return snapshot.sort((a, b) => {
190
- const aDimension = this.dimensionOf(a);
191
- const bDimension = this.dimensionOf(b);
192
- return (aDimension?.horizontal.from || 0) - (bDimension?.horizontal.from || 0);
184
+ const deleted = new Set();
185
+ const symbols = this.lineageOf(versionId).flatMap(version => {
186
+ deletedBy(version).forEach(id => deleted.add(id));
187
+ return insertedBy(version).filter(symbol => !deleted.has(symbol.id));
193
188
  });
189
+ return this.inOrderOfPlace(symbols);
194
190
  }
195
191
  /**
196
192
  * Assigns a generation (depth) to every node.
package/lib/Emulation.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { MIDIControlEvents } from "midifile-ts";
2
2
  import { idOf } from "./Assumption";
3
- import { pairsAmong, placementsOf } from "./Symbol";
3
+ import { isPerforation, pairsAmong, placementsOf } from "./Symbol";
4
4
  import { add, mean, mm, seconds, subtract } from "./Quantity";
5
5
  /** The punch diameter the edition's copies report, where any of them does. */
6
6
  const punchDiameterOf = (view) => {
@@ -83,6 +83,8 @@ const displacementsOf = (events, offsetsBetween, gap) => {
83
83
  });
84
84
  return displacements;
85
85
  };
86
+ /** The earliest of the times, or the beginning of the roll where there are none. */
87
+ const earliestOf = (times) => times.length === 0 ? seconds(0) : times.reduce((soonest, at) => at < soonest ? at : soonest);
86
88
  /**
87
89
  * A version of the edition, performed: the symbols are negotiated into
88
90
  * placed events, the reproducing system plays them, and the result goes
@@ -147,22 +149,14 @@ export class Emulation {
147
149
  }
148
150
  emulateVersion(version, view, { range, skipToFirstNote = false } = {}) {
149
151
  this.source = version.id;
152
+ /** A note plays only where its onset falls in the range; expressions play throughout. */
153
+ const inScope = (event) => !range || event.type !== 'note' || (event.horizontal.from > range[0] && event.horizontal.from < range[1]);
150
154
  this.negotiatedEvents =
151
155
  view.snapshot(version.id)
152
- .filter(s => s.type === 'note' || s.type === 'expression')
153
- .filter(s => {
154
- if (range && s.type === 'note') {
155
- const dimensions = view.dimensionOf(s);
156
- if (!dimensions)
157
- return true; // in case of doubt, include the note
158
- // check if the note onset is within the specified range
159
- const onset = dimensions.horizontal.from;
160
- return onset > range[0] && onset < range[1];
161
- }
162
- return true;
163
- })
164
- .map((e) => view.simplifySymbol(e))
165
- .filter(s => s !== null);
156
+ .filter(isPerforation)
157
+ .map(symbol => view.simplifySymbol(symbol))
158
+ .filter(event => event !== null)
159
+ .filter(inScope);
166
160
  if (this.negotiatedEvents.length === 0) {
167
161
  this.midiEvents = [];
168
162
  this.curves = [];
@@ -172,7 +166,7 @@ export class Emulation {
172
166
  const performance = this.system.perform(this.negotiatedEvents, this.options, propertiesOf(view));
173
167
  this.curves = performance.curves;
174
168
  const onsets = performance.events.filter(event => event.type === 'noteOn').map(event => event.at);
175
- const origin = seconds(skipToFirstNote && onsets.length > 0 ? Math.min(...onsets) : 0);
169
+ const origin = skipToFirstNote ? earliestOf(onsets) : seconds(0);
176
170
  this.midiEvents = performance.events
177
171
  .map(event => ({ ...event, at: subtract(event.at, origin) }))
178
172
  .filter(event => event.at >= 0)
package/lib/Quantity.d.ts CHANGED
@@ -58,4 +58,8 @@ export declare const mean: <U extends Unit>(values: readonly Quantity<U>[]) => Q
58
58
  export declare const inMillimeters: (place: Pixels, dpi: number) => Millimeters;
59
59
  export declare const inCentimeters: (length: Millimeters) => Centimeters;
60
60
  export declare const inSeconds: (time: Milliseconds) => Seconds;
61
+ /** A speed as a record states it, in feet or metres per minute. */
62
+ export type SpeedMeasure = Measure<'ft/min'> | Measure<'m/min'>;
63
+ /** A speed in metres per minute, whichever unit it was stated in. */
64
+ export declare const inMetersPerMinute: (speed: SpeedMeasure) => MetersPerMinute;
61
65
  export {};
package/lib/Quantity.js CHANGED
@@ -23,3 +23,6 @@ const MM_PER_INCH = 25.4;
23
23
  export const inMillimeters = (place, dpi) => mm(place / dpi * MM_PER_INCH);
24
24
  export const inCentimeters = (length) => cm(length / 10);
25
25
  export const inSeconds = (time) => seconds(time / 1000);
26
+ const METERS_PER_FOOT = 0.3048;
27
+ /** A speed in metres per minute, whichever unit it was stated in. */
28
+ export const inMetersPerMinute = (speed) => speed.unit === 'm/min' ? speed.value : metersPerMinute(speed.value * METERS_PER_FOOT);
package/lib/RollCopy.d.ts CHANGED
@@ -51,9 +51,28 @@ export interface Margins<U extends 'px' | 'mm'> {
51
51
  bass: Quantity<U>;
52
52
  unit: U;
53
53
  }
54
+ /**
55
+ * A paper speed, as a roll's label, its catalogue or its format
56
+ * states it.
57
+ * @see crm:E54 Dimension
58
+ */
59
+ export type PaperSpeed = Measure<'ft/min'> | Measure<'m/min'>;
60
+ /**
61
+ * What the scale an alignment found is put down to: the paper of the
62
+ * copy having stretched or shrunk, or the copy having been cut for
63
+ * another paper speed than the roll it is aligned with.
64
+ */
65
+ export type ScaleReading = {
66
+ cause: 'paper';
67
+ condition: ObjectAssumption<PaperStretch>;
68
+ } | {
69
+ cause: 'speed';
70
+ speed: ObjectAssumption<PaperSpeed>;
71
+ };
54
72
  /**
55
73
  * Describes the production of a roll copy: the manufacturer,
56
- * the paper used, and the date.
74
+ * the paper used, the date, and the system and paper speed the
75
+ * copy was cut for.
57
76
  * @see lrmoo:F32 Item Production Event
58
77
  */
59
78
  export interface ProductionEvent {
@@ -73,6 +92,22 @@ export interface ProductionEvent {
73
92
  * @see dcterms:date
74
93
  */
75
94
  date?: DateAssignment;
95
+ /**
96
+ * The reproducing system the copy was cut for. Left out, it is
97
+ * the roll's own; a Licensee re-cut of a T-100 roll names the
98
+ * Licensee here. A system the type vocabulary knows carries the
99
+ * IRI of its concept as `id`.
100
+ * @see crm:P32 used general technique
101
+ */
102
+ system?: Concept;
103
+ /**
104
+ * The paper speed the copy was cut for. A copy cut from the same
105
+ * master for another speed comes out longer or shorter than the
106
+ * roll it is aligned with by the ratio of the speeds, which is
107
+ * what the alignment then measures.
108
+ * @see reo:paperSpeed
109
+ */
110
+ speed?: ObjectAssumption<PaperSpeed>;
76
111
  }
77
112
  /**
78
113
  * This type denotes identifiable activities that modified
@@ -179,6 +214,13 @@ export interface RollCopy extends WithType<'RollCopy'>, WithId {
179
214
  * Not exported to RDF.
180
215
  */
181
216
  shift: Shift;
217
+ /**
218
+ * The factor this copy's features were scaled by to align them
219
+ * with the others. What it is put down to is stated apart: a
220
+ * paper-stretch condition, or the speed the copy was cut for.
221
+ * Not exported to RDF.
222
+ */
223
+ scale: number;
182
224
  /**
183
225
  * Relates this copy's scan to the tracker bar: how the scanning
184
226
  * software's hole numbering was shifted onto the bar, and where
@@ -1,6 +1,6 @@
1
1
  import type { Concept } from "./Agent";
2
2
  import { Expression, Note } from "./Symbol";
3
- import { Millimeters, Track } from "./Quantity";
3
+ import { Millimeters, SpeedMeasure, Track } from "./Quantity";
4
4
  /**
5
5
  * What a tracker bar position does: sound a note, or operate one of
6
6
  * the expression valves on the bass or the treble side.
@@ -50,6 +50,12 @@ export interface TrackerBar {
50
50
  * a scan against the bar.
51
51
  */
52
52
  readonly rewindTrack: Track;
53
+ /**
54
+ * The paper speed the system runs its rolls at, where the
55
+ * literature states one. A system whose rolls each carry a tempo
56
+ * of their own, as the Licensee's do, states none.
57
+ */
58
+ readonly paperSpeed?: SpeedMeasure;
53
59
  /** `undefined` for a position the bar does not read. */
54
60
  meaningOf(position: Track): TrackMeaning | undefined;
55
61
  /** `undefined` for a position the bar does not read. */
@@ -77,5 +83,14 @@ export interface TrackerBarSpec {
77
83
  };
78
84
  /** Every position outside the note block, keyed by track. */
79
85
  expressions: ReadonlyMap<number, string>;
86
+ /** The speed the system runs its rolls at, where the literature states one. */
87
+ paperSpeed?: SpeedMeasure;
80
88
  }
81
89
  export declare const describeTrackerBar: (spec: TrackerBarSpec) => TrackerBar;
90
+ /**
91
+ * Puts a position of one bar onto the position of another that reads
92
+ * the same thing, or nowhere when the other bar does not read it. This
93
+ * is how a copy cut for one system takes its place in an edition of
94
+ * another, as a Licensee re-cut does in an edition of a T-100 roll.
95
+ */
96
+ export declare const translationBetween: (from: TrackerBar, to: TrackerBar) => (position: Track) => Track | undefined;
package/lib/TrackerBar.js CHANGED
@@ -41,7 +41,28 @@ export const describeTrackerBar = (spec) => {
41
41
  areas,
42
42
  expressionTypes: [...new Set(spec.expressions.values())],
43
43
  rewindTrack: track(rewind),
44
+ ...(spec.paperSpeed && { paperSpeed: spec.paperSpeed }),
44
45
  meaningOf,
45
46
  roleOf
46
47
  };
47
48
  };
49
+ const keyOf = (meaning) => meaning.type === 'note' ? `note ${meaning.pitch}` : `${meaning.scope} ${meaning.expressionType}`;
50
+ const positionsOf = (bar) => Array.from({ length: bar.trackCount }, (_, i) => track(i + 1));
51
+ /**
52
+ * Puts a position of one bar onto the position of another that reads
53
+ * the same thing, or nowhere when the other bar does not read it. This
54
+ * is how a copy cut for one system takes its place in an edition of
55
+ * another, as a Licensee re-cut does in an edition of a T-100 roll.
56
+ */
57
+ export const translationBetween = (from, to) => {
58
+ const positions = new Map();
59
+ positionsOf(to).forEach(position => {
60
+ const meaning = to.meaningOf(position);
61
+ if (meaning)
62
+ positions.set(keyOf(meaning), position);
63
+ });
64
+ return position => {
65
+ const meaning = from.meaningOf(position);
66
+ return meaning ? positions.get(keyOf(meaning)) : undefined;
67
+ };
68
+ };
package/lib/Version.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { Edit } from "./Edit";
2
2
  import { ReferenceAssumption } from "./Assumption";
3
+ import { AnySymbol } from "./Symbol";
3
4
  import { WithId, WithNote, WithType } from "./utils";
4
5
  export declare const versionTypes: readonly [
5
6
  /**
@@ -56,3 +57,7 @@ export interface Version extends WithId, WithType<'Version'> {
56
57
  */
57
58
  motivations: Motivation[];
58
59
  }
60
+ /** The symbols the version's edits insert. */
61
+ export declare const insertedBy: (version: Readonly<Version>) => AnySymbol[];
62
+ /** The ids of the symbols the version's edits delete. */
63
+ export declare const deletedBy: (version: Readonly<Version>) => string[];
package/lib/Version.js CHANGED
@@ -9,3 +9,7 @@ export const versionTypes = [
9
9
  */
10
10
  'unicum'
11
11
  ];
12
+ /** The symbols the version's edits insert. */
13
+ export const insertedBy = (version) => version.edits.flatMap(edit => edit.insert ?? []);
14
+ /** The ids of the symbols the version's edits delete. */
15
+ export const deletedBy = (version) => version.edits.flatMap(edit => edit.delete ?? []);
@@ -1,22 +1,22 @@
1
- import { ObjectAssumption } from "./Assumption";
2
1
  import { AnyFeature } from "./Feature";
3
- import { PaperStretch, RollCopy, Shift } from "./RollCopy";
2
+ import { RollCopy, Shift } from "./RollCopy";
4
3
  import { TrackerBar } from "./TrackerBar";
5
4
  import { Millimeters } from "./Quantity";
6
5
  export declare const applyShift: (shift: Shift, copy: RollCopy) => void;
7
- export declare const applyStretch: (paperStretch: ObjectAssumption<PaperStretch>, copy: RollCopy) => void;
6
+ /** Scales the copy's features away from the beginning of the roll, and records the factor. */
7
+ export declare const applyScale: (factor: number, copy: RollCopy) => void;
8
8
  /** Takes the shift off the copy's features again, as far as one was applied. */
9
9
  export declare const revertShift: (copy: RollCopy) => void;
10
- /** Takes the stretch off the copy's features again, as far as one was applied. */
11
- export declare const revertStretch: (copy: RollCopy) => void;
10
+ /** Takes the scale off the copy's features again, as far as one was applied. */
11
+ export declare const revertScale: (copy: RollCopy) => void;
12
12
  type AlignmentResult = {
13
- /** Applied before the stretch. */
13
+ /** Applied before the scale. */
14
14
  shift: Millimeters;
15
- stretch: number;
15
+ scale: number;
16
16
  };
17
17
  /**
18
18
  * Align two rolls by computing independent linear fits of each roll's note-onset positions
19
- * using only the first and last segments, then deriving a transform x2 = (x1 + shift) * stretch.
19
+ * using only the first and last segments, then deriving a transform x2 = (x1 + shift) * scale.
20
20
  */
21
21
  export declare function alignFeatures(rollA: AnyFeature[], rollB: AnyFeature[], bar?: TrackerBar): AlignmentResult;
22
22
  export {};
package/lib/alignment.js CHANGED
@@ -23,12 +23,13 @@ export const applyShift = (shift, copy) => {
23
23
  copy.ops = [...copy.ops, 'shifted'];
24
24
  copy.measurements.shift = shift;
25
25
  };
26
- export const applyStretch = (paperStretch, copy) => {
26
+ /** Scales the copy's features away from the beginning of the roll, and records the factor. */
27
+ export const applyScale = (factor, copy) => {
27
28
  if (copy.ops.includes('stretched'))
28
29
  return;
29
- copy.features.forEach(feature => stretch(feature.horizontal, paperStretch.factor));
30
+ copy.features.forEach(feature => stretch(feature.horizontal, factor));
30
31
  copy.ops = [...copy.ops, 'stretched'];
31
- copy.conditions.push(paperStretch);
32
+ copy.measurements.scale = factor;
32
33
  };
33
34
  /** Takes the shift off the copy's features again, as far as one was applied. */
34
35
  export const revertShift = (copy) => {
@@ -43,15 +44,14 @@ export const revertShift = (copy) => {
43
44
  copy.ops = copy.ops.filter(op => op !== 'shifted');
44
45
  delete copy.measurements.shift;
45
46
  };
46
- const isPaperStretch = (condition) => condition.conditionType === 'paper-stretch';
47
- /** Takes the stretch off the copy's features again, as far as one was applied. */
48
- export const revertStretch = (copy) => {
49
- const applied = copy.conditions.find(isPaperStretch);
50
- if (!copy.ops.includes('stretched') || !applied)
47
+ /** Takes the scale off the copy's features again, as far as one was applied. */
48
+ export const revertScale = (copy) => {
49
+ const factor = copy.measurements.scale;
50
+ if (!copy.ops.includes('stretched') || factor === undefined)
51
51
  return;
52
- copy.features.forEach(feature => stretch(feature.horizontal, 1 / applied.factor));
52
+ copy.features.forEach(feature => stretch(feature.horizontal, 1 / factor));
53
53
  copy.ops = copy.ops.filter(op => op !== 'stretched');
54
- copy.conditions = copy.conditions.filter(condition => !isPaperStretch(condition));
54
+ delete copy.measurements.scale;
55
55
  };
56
56
  const isNoteOn = (bar) => (feature) => {
57
57
  return feature.type === 'Hole' && bar.meaningOf(feature.vertical.from)?.type === 'note';
@@ -85,7 +85,7 @@ function selectEnds(arr, count) {
85
85
  }
86
86
  /**
87
87
  * Align two rolls by computing independent linear fits of each roll's note-onset positions
88
- * using only the first and last segments, then deriving a transform x2 = (x1 + shift) * stretch.
88
+ * using only the first and last segments, then deriving a transform x2 = (x1 + shift) * scale.
89
89
  */
90
90
  export function alignFeatures(rollA, rollB, bar = welteT100) {
91
91
  // 1. Extract note-onset positions
@@ -102,8 +102,8 @@ export function alignFeatures(rollA, rollB, bar = welteT100) {
102
102
  // 4. Fit index->position for each roll on selected ends
103
103
  const { alpha: alphaA, beta: betaA } = fitIndexToPosition(idxA, XA);
104
104
  const { alpha: alphaB, beta: betaB } = fitIndexToPosition(idxB, XB);
105
- // 5. Derive stretch and shift such that x2 = (x1 + shift) * stretch
106
- const stretch = alphaB / alphaA;
107
- const shift = mm(betaB / stretch - betaA);
108
- return { stretch, shift };
105
+ // 5. Derive scale and shift such that x2 = (x1 + shift) * scale
106
+ const scale = alphaB / alphaA;
107
+ const shift = mm(betaB / scale - betaA);
108
+ return { scale, shift };
109
109
  }