linked-rolls 0.0.1 → 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.
- package/README.md +48 -1
- package/lib/Assumption.d.ts +124 -0
- package/lib/Assumption.js +34 -0
- package/lib/Collation.d.ts +11 -0
- package/lib/ConditionState.d.ts +8 -3
- package/lib/ConditionState.js +0 -5
- package/lib/Edit.d.ts +44 -14
- package/lib/Edit.js +2 -95
- package/lib/Edition.d.ts +144 -4
- package/lib/EditionView.d.ts +51 -0
- package/lib/EditionView.js +294 -0
- package/lib/Emulation.d.ts +21 -68
- package/lib/Emulation.js +94 -386
- package/lib/Feature.d.ts +137 -16
- package/lib/Feature.js +9 -1
- package/lib/Plan.d.ts +190 -0
- package/lib/Plan.js +555 -0
- package/lib/ReproducingSystem.d.ts +80 -0
- package/lib/RollCopy.d.ts +259 -24
- package/lib/RollCopy.js +232 -215
- package/lib/Symbol.d.ts +70 -37
- package/lib/Symbol.js +2 -27
- package/lib/TrackCalibration.d.ts +33 -0
- package/lib/TrackCalibration.js +14 -0
- package/lib/TrackerBar.d.ts +52 -5
- package/lib/TrackerBar.js +70 -49
- package/lib/Version.d.ts +32 -33
- package/lib/Version.js +3 -209
- package/lib/alignFeatures.d.ts +3 -2
- package/lib/alignFeatures.js +5 -4
- package/lib/asJsonLd.d.ts +1 -1
- package/lib/asJsonLd.js +13 -26
- package/lib/importJsonLd.d.ts +0 -1
- package/lib/importJsonLd.js +9 -72
- package/lib/index.d.ts +7 -3
- package/lib/index.js +7 -3
- package/lib/schema.json +1739 -0
- package/lib/spec/context.json +125 -144
- package/lib/systems/welteT100.d.ts +55 -0
- package/lib/systems/welteT100.js +191 -0
- package/lib/utils.d.ts +29 -0
- package/lib/validate.d.ts +3 -0
- package/lib/validate.js +10 -0
- package/package.json +41 -8
- package/lib/Condition.d.ts +0 -10
- package/lib/EditorialAssumption.d.ts +0 -67
- package/lib/EditorialAssumption.js +0 -26
- package/lib/Measurement.d.ts +0 -9
- package/lib/PlaceTimeConversion.d.ts +0 -65
- package/lib/PlaceTimeConversion.js +0 -175
- package/lib/RollEvent.d.ts +0 -76
- package/lib/RollEvent.js +0 -3
- package/lib/Stage.d.ts +0 -37
- package/lib/Stage.js +0 -165
- package/lib/Transcription.d.ts +0 -7
- package/lib/Transcription.js +0 -9
- package/lib/WithId.d.ts +0 -3
- package/lib/WithId.js +0 -1
- package/lib/alignRolls.d.ts +0 -7
- package/lib/alignRolls.js +0 -49
- package/lib/alignSymbols.d.ts +0 -7
- package/lib/alignSymbols.js +0 -49
- package/lib/aton/AtonParser.test.d.ts +0 -1
- package/lib/aton/AtonParser.test.js +0 -16
- package/lib/build-schema.cjs +0 -113
- /package/lib/{Condition.js → ReproducingSystem.js} +0 -0
- /package/lib/{Measurement.js → utils.js} +0 -0
package/lib/Plan.js
ADDED
|
@@ -0,0 +1,555 @@
|
|
|
1
|
+
import { getAt } from "./EditionView";
|
|
2
|
+
import { v4 } from "uuid";
|
|
3
|
+
import { asSymbols } from "./RollCopy";
|
|
4
|
+
import { assignReference, idOf } from "./Assumption";
|
|
5
|
+
export const isPlan = (obj) => {
|
|
6
|
+
return obj && typeof obj.build === 'function' && typeof obj.setView === 'function';
|
|
7
|
+
};
|
|
8
|
+
export class BasePlan {
|
|
9
|
+
constructor() {
|
|
10
|
+
Object.defineProperty(this, "view", {
|
|
11
|
+
enumerable: true,
|
|
12
|
+
configurable: true,
|
|
13
|
+
writable: true,
|
|
14
|
+
value: null
|
|
15
|
+
});
|
|
16
|
+
Object.defineProperty(this, "setView", {
|
|
17
|
+
enumerable: true,
|
|
18
|
+
configurable: true,
|
|
19
|
+
writable: true,
|
|
20
|
+
value: (view) => {
|
|
21
|
+
this.view = view;
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Walks through the versions. If it finds a symbol that is still
|
|
28
|
+
* part of the tradition (i.e. it is included in the current snapshot)
|
|
29
|
+
* and is equivalent with the given symbol, it will add the feature
|
|
30
|
+
* carrying the symbol to the collated symbol. Otherwise, the
|
|
31
|
+
* given symbol will be added to the current version's insertions.
|
|
32
|
+
*
|
|
33
|
+
* All symbols of the tradition that are not included in the given
|
|
34
|
+
* symbols are considered to be deleted.
|
|
35
|
+
*
|
|
36
|
+
* @param creation
|
|
37
|
+
* @param symbols
|
|
38
|
+
* @returns
|
|
39
|
+
*/
|
|
40
|
+
export class ConnectVersions extends BasePlan {
|
|
41
|
+
constructor(childId, parentId, tolerance = {
|
|
42
|
+
toleranceEnd: 5,
|
|
43
|
+
toleranceStart: 5
|
|
44
|
+
}) {
|
|
45
|
+
super();
|
|
46
|
+
Object.defineProperty(this, "childId", {
|
|
47
|
+
enumerable: true,
|
|
48
|
+
configurable: true,
|
|
49
|
+
writable: true,
|
|
50
|
+
value: childId
|
|
51
|
+
});
|
|
52
|
+
Object.defineProperty(this, "parentId", {
|
|
53
|
+
enumerable: true,
|
|
54
|
+
configurable: true,
|
|
55
|
+
writable: true,
|
|
56
|
+
value: parentId
|
|
57
|
+
});
|
|
58
|
+
Object.defineProperty(this, "tolerance", {
|
|
59
|
+
enumerable: true,
|
|
60
|
+
configurable: true,
|
|
61
|
+
writable: true,
|
|
62
|
+
value: tolerance
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
build() {
|
|
66
|
+
if (!this.view)
|
|
67
|
+
return [];
|
|
68
|
+
const view = this.view;
|
|
69
|
+
const result = [];
|
|
70
|
+
const parentSnapshot = view.snapshot(this.parentId);
|
|
71
|
+
const childSnapshot = view.snapshot(this.childId);
|
|
72
|
+
const treatedSymbols = [];
|
|
73
|
+
// can the symbol be collated with any of the
|
|
74
|
+
// symbols of the tradition, i.e. the ones
|
|
75
|
+
// included in the current snapshot?
|
|
76
|
+
const insertions = [...childSnapshot];
|
|
77
|
+
for (const symbol of childSnapshot) {
|
|
78
|
+
parentSnapshot
|
|
79
|
+
.filter(toCompare => view.isCollatable(symbol, toCompare, this.tolerance))
|
|
80
|
+
.forEach(parentSymbol => {
|
|
81
|
+
result.push(draft => {
|
|
82
|
+
const symbolPath = view.getPath(parentSymbol.id);
|
|
83
|
+
if (!symbolPath)
|
|
84
|
+
return;
|
|
85
|
+
const correspSymbol = getAt(symbolPath, draft);
|
|
86
|
+
if (!correspSymbol)
|
|
87
|
+
return;
|
|
88
|
+
correspSymbol.carriers.push(...symbol.carriers);
|
|
89
|
+
});
|
|
90
|
+
insertions.splice(insertions.indexOf(symbol), 1);
|
|
91
|
+
treatedSymbols.push(parentSymbol);
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
const edits = insertions.map((symbol) => {
|
|
95
|
+
return {
|
|
96
|
+
type: 'edit',
|
|
97
|
+
id: v4(),
|
|
98
|
+
insert: [symbol],
|
|
99
|
+
delete: [],
|
|
100
|
+
};
|
|
101
|
+
});
|
|
102
|
+
const deletions = parentSnapshot.filter(sym => {
|
|
103
|
+
return !treatedSymbols.includes(sym);
|
|
104
|
+
});
|
|
105
|
+
for (const symbol of deletions) {
|
|
106
|
+
edits.push({
|
|
107
|
+
type: 'edit',
|
|
108
|
+
id: v4(),
|
|
109
|
+
insert: [],
|
|
110
|
+
delete: [symbol.id],
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
result.push(draft => {
|
|
114
|
+
const v = draft.versions.find(v => v.id === this.childId);
|
|
115
|
+
if (!v)
|
|
116
|
+
return;
|
|
117
|
+
v.edits = edits;
|
|
118
|
+
v.basedOn = assignReference(this.parentId);
|
|
119
|
+
});
|
|
120
|
+
return result;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Walks through the versions. If it finds a symbol that is still
|
|
125
|
+
* part of the tradition (i.e. it is included in the current snapshot)
|
|
126
|
+
* and is equivalent with the given symbol, it will add the feature
|
|
127
|
+
* carrying the symbol to the collated symbol. Otherwise, the
|
|
128
|
+
* given symbol will be added to the current version's insertions.
|
|
129
|
+
*
|
|
130
|
+
* All symbols of the tradition that are not included in the given
|
|
131
|
+
* symbols are considered to be deleted.
|
|
132
|
+
*
|
|
133
|
+
* @param creation
|
|
134
|
+
* @param symbols
|
|
135
|
+
* @returns
|
|
136
|
+
*/
|
|
137
|
+
export class CreateVersion extends BasePlan {
|
|
138
|
+
constructor(siglum, copy) {
|
|
139
|
+
super();
|
|
140
|
+
Object.defineProperty(this, "siglum", {
|
|
141
|
+
enumerable: true,
|
|
142
|
+
configurable: true,
|
|
143
|
+
writable: true,
|
|
144
|
+
value: siglum
|
|
145
|
+
});
|
|
146
|
+
Object.defineProperty(this, "copy", {
|
|
147
|
+
enumerable: true,
|
|
148
|
+
configurable: true,
|
|
149
|
+
writable: true,
|
|
150
|
+
value: copy
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
build() {
|
|
154
|
+
const view = this.view;
|
|
155
|
+
if (!view)
|
|
156
|
+
return [];
|
|
157
|
+
const result = [];
|
|
158
|
+
result.push(draft => {
|
|
159
|
+
draft.copies.push(this.copy);
|
|
160
|
+
const newVersion = {
|
|
161
|
+
siglum: this.siglum,
|
|
162
|
+
id: v4(),
|
|
163
|
+
edits: asSymbols(this.copy.features).map((symbol) => {
|
|
164
|
+
return {
|
|
165
|
+
type: 'edit',
|
|
166
|
+
id: v4(),
|
|
167
|
+
insert: [symbol],
|
|
168
|
+
delete: [],
|
|
169
|
+
};
|
|
170
|
+
}),
|
|
171
|
+
type: 'edition',
|
|
172
|
+
motivations: []
|
|
173
|
+
};
|
|
174
|
+
draft.versions.push(newVersion);
|
|
175
|
+
});
|
|
176
|
+
return result;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Creates a new version based on the given version
|
|
181
|
+
* calculating the effect of a cover on existing symbols.
|
|
182
|
+
|
|
183
|
+
export class CoverPerforation extends BasePlan {
|
|
184
|
+
constructor(
|
|
185
|
+
private copyId: string,
|
|
186
|
+
private versionId: string,
|
|
187
|
+
private coverDimension: { horizontal: HorizontalSpan, vertical: VerticalSpan },
|
|
188
|
+
private material: string | undefined = undefined,
|
|
189
|
+
) {
|
|
190
|
+
super()
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
build(): EditionOp[] {
|
|
194
|
+
const view = this.view
|
|
195
|
+
if (!view) return []
|
|
196
|
+
|
|
197
|
+
return [
|
|
198
|
+
(draft: Draft<Edition>): void => {
|
|
199
|
+
const version = draft.versions.find(v => v.id === this.versionId)
|
|
200
|
+
const copy = draft.copies.find(c => c.id === this.copyId)
|
|
201
|
+
if (!version || !copy) return
|
|
202
|
+
|
|
203
|
+
// find perforations in the snapshot that
|
|
204
|
+
// overlap with the cover
|
|
205
|
+
const overlappingSymbols =
|
|
206
|
+
view.snapshot(this.versionId)
|
|
207
|
+
.filter(symbol => {
|
|
208
|
+
const dimension = view.dimensionOf(symbol)
|
|
209
|
+
if (!dimension) return false
|
|
210
|
+
|
|
211
|
+
return (
|
|
212
|
+
(symbol.type === 'note' || symbol.type === 'expression') &&
|
|
213
|
+
overlaps(dimension, this.coverDimension)
|
|
214
|
+
)
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
if (!overlappingSymbols.length) return undefined
|
|
218
|
+
|
|
219
|
+
const deepClone = JSON.parse(JSON.stringify(overlappingSymbols)) as AnySymbol[]
|
|
220
|
+
|
|
221
|
+
for (const symbol of deepClone) {
|
|
222
|
+
//symbol.id = v4();
|
|
223
|
+
const dimension = structuredClone(view.dimensionOf(symbol))
|
|
224
|
+
if (!dimension) continue
|
|
225
|
+
|
|
226
|
+
// check if the cover partially covers the beginning
|
|
227
|
+
if (dimension.horizontal.from >= this.coverDimension.horizontal.from &&
|
|
228
|
+
dimension.horizontal.from <= this.coverDimension.horizontal.to
|
|
229
|
+
) {
|
|
230
|
+
dimension.horizontal.from = this.coverDimension.horizontal.to
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// check if the cover partially covers the ending
|
|
234
|
+
if (dimension.horizontal.to >= this.coverDimension.horizontal.from &&
|
|
235
|
+
dimension.horizontal.to <= this.coverDimension.horizontal.to
|
|
236
|
+
) {
|
|
237
|
+
dimension.horizontal.to = this.coverDimension.horizontal.from
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const newFeature: RollFeature = {
|
|
241
|
+
...dimension,
|
|
242
|
+
id: v4(),
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
copy.features.push(newFeature)
|
|
246
|
+
symbol.id = `symbol-${v4().slice(0, 8)}`
|
|
247
|
+
symbol.carriers = [assignReference(newFeature.id)]
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const coverId = `cover-${v4().slice(0, 8)}`
|
|
251
|
+
copy.features.push({
|
|
252
|
+
...this.coverDimension,
|
|
253
|
+
id: coverId,
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
const edit: Edit = {
|
|
257
|
+
type: 'edit',
|
|
258
|
+
id: v4(),
|
|
259
|
+
delete: overlappingSymbols.map(s => s.id),
|
|
260
|
+
insert: deepClone,
|
|
261
|
+
intentionOf: [{
|
|
262
|
+
type: 'cover',
|
|
263
|
+
id: v4(),
|
|
264
|
+
note: this.material,
|
|
265
|
+
carriers: [assignReference(coverId)],
|
|
266
|
+
}]
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
draft.versions.push({
|
|
270
|
+
edits: [edit],
|
|
271
|
+
id: v4(),
|
|
272
|
+
basedOn: assignReference(version.id),
|
|
273
|
+
siglum: version.siglum + ' rev',
|
|
274
|
+
type: 'authorised-revision',
|
|
275
|
+
motivations: [
|
|
276
|
+
{
|
|
277
|
+
type: 'motivation',
|
|
278
|
+
note: 'Stanzfehler korrigiert',
|
|
279
|
+
id: v4(),
|
|
280
|
+
}
|
|
281
|
+
],
|
|
282
|
+
})
|
|
283
|
+
}]
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
*/
|
|
287
|
+
export class DetachVersion extends BasePlan {
|
|
288
|
+
constructor(versionId) {
|
|
289
|
+
super();
|
|
290
|
+
Object.defineProperty(this, "versionId", {
|
|
291
|
+
enumerable: true,
|
|
292
|
+
configurable: true,
|
|
293
|
+
writable: true,
|
|
294
|
+
value: versionId
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
build() {
|
|
298
|
+
if (!this.view)
|
|
299
|
+
return [];
|
|
300
|
+
const snapshot = this.view.snapshot(this.versionId);
|
|
301
|
+
const newEdits = snapshot.map((symbol) => ({
|
|
302
|
+
type: 'edit',
|
|
303
|
+
id: v4(),
|
|
304
|
+
insert: [symbol],
|
|
305
|
+
delete: [],
|
|
306
|
+
}));
|
|
307
|
+
return [draft => {
|
|
308
|
+
const v = draft.versions.find(ver => ver.id === this.versionId);
|
|
309
|
+
if (!v)
|
|
310
|
+
return;
|
|
311
|
+
v.edits = newEdits;
|
|
312
|
+
delete v.basedOn;
|
|
313
|
+
v.motivations = [];
|
|
314
|
+
}];
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
export class RemoveFeature extends BasePlan {
|
|
318
|
+
constructor(copyID, featureIDs) {
|
|
319
|
+
super();
|
|
320
|
+
Object.defineProperty(this, "copyID", {
|
|
321
|
+
enumerable: true,
|
|
322
|
+
configurable: true,
|
|
323
|
+
writable: true,
|
|
324
|
+
value: copyID
|
|
325
|
+
});
|
|
326
|
+
Object.defineProperty(this, "featureIDs", {
|
|
327
|
+
enumerable: true,
|
|
328
|
+
configurable: true,
|
|
329
|
+
writable: true,
|
|
330
|
+
value: featureIDs
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
build() {
|
|
334
|
+
const view = this.view;
|
|
335
|
+
if (!view)
|
|
336
|
+
return [];
|
|
337
|
+
return [
|
|
338
|
+
(draft) => {
|
|
339
|
+
const copy = draft.copies.find(c => c.id === this.copyID);
|
|
340
|
+
if (!copy)
|
|
341
|
+
return;
|
|
342
|
+
copy.features = copy.features.filter(f => !this.featureIDs.includes(f.id));
|
|
343
|
+
this.featureIDs.forEach(featureId => {
|
|
344
|
+
// expect a path of format:
|
|
345
|
+
// ['versions', index, 'edits', index, 'insert', index, 'carriers', index, 'id']
|
|
346
|
+
const carrierPath = view.linksTo(featureId).at(0);
|
|
347
|
+
if (!carrierPath)
|
|
348
|
+
return;
|
|
349
|
+
// expect to find the parent symbol at:
|
|
350
|
+
// ['versions', index, 'edits', index, 'insert', index]
|
|
351
|
+
const symbol = getAt(carrierPath.slice(0, 6), draft);
|
|
352
|
+
if (!symbol)
|
|
353
|
+
return;
|
|
354
|
+
symbol.carriers = symbol.carriers.filter(c => idOf(c) !== featureId);
|
|
355
|
+
// if the symbol has no more carriers, remove it
|
|
356
|
+
if (symbol.carriers.length === 0) {
|
|
357
|
+
// expect to find the parent edit at:
|
|
358
|
+
// ['versions', index, 'edits', index]
|
|
359
|
+
const inserts = getAt(carrierPath.slice(0, 5), draft);
|
|
360
|
+
if (!inserts)
|
|
361
|
+
return;
|
|
362
|
+
inserts.splice(inserts.findIndex(s => s.id === symbol.id), 1);
|
|
363
|
+
if (inserts.length === 0) {
|
|
364
|
+
const edit = getAt(carrierPath.slice(0, 4), draft);
|
|
365
|
+
if (!edit)
|
|
366
|
+
return;
|
|
367
|
+
if (!edit.insert?.length && !edit.delete?.length) {
|
|
368
|
+
const edits = getAt(carrierPath.slice(0, 3), draft);
|
|
369
|
+
if (!edits)
|
|
370
|
+
return;
|
|
371
|
+
edits.splice(edits.findIndex(e => e.id === edit.id), 1);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
];
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
export class MergeEdits extends BasePlan {
|
|
381
|
+
constructor(versionId, toMerge) {
|
|
382
|
+
super();
|
|
383
|
+
Object.defineProperty(this, "versionId", {
|
|
384
|
+
enumerable: true,
|
|
385
|
+
configurable: true,
|
|
386
|
+
writable: true,
|
|
387
|
+
value: versionId
|
|
388
|
+
});
|
|
389
|
+
Object.defineProperty(this, "toMerge", {
|
|
390
|
+
enumerable: true,
|
|
391
|
+
configurable: true,
|
|
392
|
+
writable: true,
|
|
393
|
+
value: toMerge
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
guessMotivation(edit) {
|
|
397
|
+
const view = this.view;
|
|
398
|
+
if (!view)
|
|
399
|
+
return 'correct-error';
|
|
400
|
+
const inserts = (edit.insert || []);
|
|
401
|
+
const deletes = view.getAll(edit.delete ?? []);
|
|
402
|
+
const types = [
|
|
403
|
+
inserts.filter(e => e.type === 'expression').map(e => e.expressionType),
|
|
404
|
+
deletes.filter(e => e.type === 'expression').map(e => e.expressionType)
|
|
405
|
+
];
|
|
406
|
+
if (arraysIdentical(types, [['SlowCrescendoOn', 'SlowCrescendoOff'], []])) {
|
|
407
|
+
return 'additional-accent';
|
|
408
|
+
}
|
|
409
|
+
else if (arraysIdentical(types, [['ForzandoOn', 'ForzandoOff'], []])) {
|
|
410
|
+
// TODO: check if the inserts are very close
|
|
411
|
+
// and return 'short-dynamic-differentation'
|
|
412
|
+
return 'additional-accent';
|
|
413
|
+
}
|
|
414
|
+
else if (types.every(t => t.length > 1) && arraysIdentical(types[0], types[1])) {
|
|
415
|
+
return 'shift';
|
|
416
|
+
}
|
|
417
|
+
else if (types[0].length === 0 && types[1].length === 1) {
|
|
418
|
+
return 'remove-redundancy';
|
|
419
|
+
}
|
|
420
|
+
if (inserts.length === 1 && deletes.length === 1) {
|
|
421
|
+
const insertDim = view.dimensionOf(inserts[0])?.horizontal;
|
|
422
|
+
const deleteDim = view.dimensionOf(deletes[0])?.horizontal;
|
|
423
|
+
if (!insertDim || !deleteDim) {
|
|
424
|
+
return 'correct-error';
|
|
425
|
+
}
|
|
426
|
+
if (Math.abs(insertDim.from - deleteDim.from) < 5) {
|
|
427
|
+
const insertLength = Math.abs(insertDim.to - insertDim.from);
|
|
428
|
+
const deleteLength = Math.abs(deleteDim.to - deleteDim.from);
|
|
429
|
+
if (insertLength < deleteLength) {
|
|
430
|
+
return 'shorten';
|
|
431
|
+
}
|
|
432
|
+
else {
|
|
433
|
+
return 'prolong';
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
return 'correct-error';
|
|
438
|
+
}
|
|
439
|
+
build() {
|
|
440
|
+
const view = this.view;
|
|
441
|
+
if (!view)
|
|
442
|
+
return [];
|
|
443
|
+
const result = structuredClone(this.toMerge[0]);
|
|
444
|
+
this.toMerge
|
|
445
|
+
.slice(1)
|
|
446
|
+
.forEach(edit => {
|
|
447
|
+
if (result.insert) {
|
|
448
|
+
result.insert.push(...(edit.insert || []));
|
|
449
|
+
}
|
|
450
|
+
else {
|
|
451
|
+
result.insert = edit.insert;
|
|
452
|
+
}
|
|
453
|
+
if (result.delete) {
|
|
454
|
+
result.delete.push(...(edit.delete || []));
|
|
455
|
+
}
|
|
456
|
+
else {
|
|
457
|
+
result.delete = edit.delete;
|
|
458
|
+
}
|
|
459
|
+
});
|
|
460
|
+
const edit = {
|
|
461
|
+
...result,
|
|
462
|
+
id: v4(),
|
|
463
|
+
motivation: this.guessMotivation(result),
|
|
464
|
+
};
|
|
465
|
+
return ([draft => {
|
|
466
|
+
const version = draft.versions.find(v => v.id === this.versionId);
|
|
467
|
+
if (!version)
|
|
468
|
+
return;
|
|
469
|
+
// remove all edits that were merged
|
|
470
|
+
for (const e of this.toMerge) {
|
|
471
|
+
const index = version.edits.findIndex(ve => ve.id === e.id);
|
|
472
|
+
if (index !== -1)
|
|
473
|
+
version.edits.splice(index, 1);
|
|
474
|
+
}
|
|
475
|
+
version.edits.push(edit);
|
|
476
|
+
}]);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
export class SplitEdit extends BasePlan {
|
|
480
|
+
constructor(versionId, toSplit) {
|
|
481
|
+
super();
|
|
482
|
+
Object.defineProperty(this, "versionId", {
|
|
483
|
+
enumerable: true,
|
|
484
|
+
configurable: true,
|
|
485
|
+
writable: true,
|
|
486
|
+
value: versionId
|
|
487
|
+
});
|
|
488
|
+
Object.defineProperty(this, "toSplit", {
|
|
489
|
+
enumerable: true,
|
|
490
|
+
configurable: true,
|
|
491
|
+
writable: true,
|
|
492
|
+
value: toSplit
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
build() {
|
|
496
|
+
const view = this.view;
|
|
497
|
+
if (!view)
|
|
498
|
+
return [];
|
|
499
|
+
const result = [];
|
|
500
|
+
for (const insert of this.toSplit.insert ?? []) {
|
|
501
|
+
result.push({
|
|
502
|
+
type: 'edit',
|
|
503
|
+
id: v4(),
|
|
504
|
+
insert: [insert]
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
for (const remove of this.toSplit.delete ?? []) {
|
|
508
|
+
result.push({
|
|
509
|
+
type: 'edit',
|
|
510
|
+
id: v4(),
|
|
511
|
+
delete: [remove]
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
return ([draft => {
|
|
515
|
+
const version = draft.versions.find(v => v.id === this.versionId);
|
|
516
|
+
if (!version)
|
|
517
|
+
return;
|
|
518
|
+
// remove the edit that is split
|
|
519
|
+
const index = version.edits.findIndex(ve => ve.id === this.toSplit.id);
|
|
520
|
+
if (index !== -1)
|
|
521
|
+
version.edits.splice(index, 1);
|
|
522
|
+
version.edits.push(...result);
|
|
523
|
+
}]);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
/*
|
|
527
|
+
type LazyDimension = Partial<{ from: number, to: number }>;
|
|
528
|
+
|
|
529
|
+
type LazyArea = {
|
|
530
|
+
horizontal: LazyDimension;
|
|
531
|
+
vertical: LazyDimension;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
const overlaps = (a: LazyArea, b: LazyArea): boolean => {
|
|
535
|
+
const overlapsDimension = (a: LazyDimension, b: LazyDimension): boolean =>
|
|
536
|
+
(a.from ?? 0) < (b.to ?? Infinity) && (b.from ?? 0) < (a.to ?? Infinity);
|
|
537
|
+
|
|
538
|
+
// console.log('do the dimensions overlap?', a, b)
|
|
539
|
+
|
|
540
|
+
return overlapsDimension(a.horizontal, b.horizontal);
|
|
541
|
+
}
|
|
542
|
+
*/
|
|
543
|
+
const arraysIdentical = (a, b) => {
|
|
544
|
+
let i = a.length;
|
|
545
|
+
if (i != b.length)
|
|
546
|
+
return false;
|
|
547
|
+
while (i--) {
|
|
548
|
+
if (Array.isArray(a[i]) && Array.isArray(b[i])) {
|
|
549
|
+
return arraysIdentical(a[i], b[i]);
|
|
550
|
+
}
|
|
551
|
+
if (a[i] !== b[i])
|
|
552
|
+
return false;
|
|
553
|
+
}
|
|
554
|
+
return true;
|
|
555
|
+
};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { RollTempo } from "./Edition";
|
|
2
|
+
import { Hole } from "./Feature";
|
|
3
|
+
import { Expression, Note } from "./Symbol";
|
|
4
|
+
import { TrackerBar } from "./TrackerBar";
|
|
5
|
+
/**
|
|
6
|
+
* A note or expression of a version with the dimensions of its carriers
|
|
7
|
+
* averaged in and the editorial assumptions applied, which is all a
|
|
8
|
+
* performance needs to know of a symbol.
|
|
9
|
+
*/
|
|
10
|
+
export type NegotiatedEvent = Omit<Note | Expression, 'carriers'> & Pick<Hole, 'horizontal' | 'vertical'>;
|
|
11
|
+
interface PerformedRollFeature<T> {
|
|
12
|
+
type: T;
|
|
13
|
+
performs: NegotiatedEvent;
|
|
14
|
+
/** Seconds from the beginning of the roll. */
|
|
15
|
+
at: number;
|
|
16
|
+
}
|
|
17
|
+
interface PerformedNoteEvent<T> extends PerformedRollFeature<T> {
|
|
18
|
+
pitch: number;
|
|
19
|
+
velocity: number;
|
|
20
|
+
}
|
|
21
|
+
export interface PerformedNoteOnEvent extends PerformedNoteEvent<'noteOn'> {
|
|
22
|
+
}
|
|
23
|
+
export interface PerformedNoteOffEvent extends PerformedNoteEvent<'noteOff'> {
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* One step of a pedal. A pedal driven by a bellows takes time to travel,
|
|
27
|
+
* so a single perforation results in a run of these; `performs` is the
|
|
28
|
+
* perforation whose reading the step follows from.
|
|
29
|
+
*/
|
|
30
|
+
export interface PerformedPedalEvent extends PerformedRollFeature<'damper' | 'hammerRail'> {
|
|
31
|
+
/** 0 with the pedal up and 127 with it fully down. */
|
|
32
|
+
value: number;
|
|
33
|
+
}
|
|
34
|
+
export type AnyPerformedRollFeature = PerformedNoteOnEvent | PerformedNoteOffEvent | PerformedPedalEvent;
|
|
35
|
+
interface CurveSamples {
|
|
36
|
+
readonly name: string;
|
|
37
|
+
/** Paper position of each sample, in mm from the beginning of the roll. */
|
|
38
|
+
readonly place: Float64Array;
|
|
39
|
+
readonly seconds: Float64Array;
|
|
40
|
+
/**
|
|
41
|
+
* Position between the two ends of a travel: 0 with a bellows open and
|
|
42
|
+
* 1 with it closed, 0 with a pedal up and 1 with it down.
|
|
43
|
+
*/
|
|
44
|
+
readonly travel: Float64Array;
|
|
45
|
+
}
|
|
46
|
+
/** The dynamics of one part of the keyboard, with the velocity the travel maps onto. */
|
|
47
|
+
export type DynamicsCurve = CurveSamples & {
|
|
48
|
+
readonly kind: 'dynamics';
|
|
49
|
+
readonly velocity: Float64Array;
|
|
50
|
+
};
|
|
51
|
+
export type PedalCurve = CurveSamples & {
|
|
52
|
+
readonly kind: 'pedal';
|
|
53
|
+
};
|
|
54
|
+
export type EmulatedCurve = DynamicsCurve | PedalCurve;
|
|
55
|
+
/** What the edition records about the roll that a mechanism may want to know. */
|
|
56
|
+
export type RollProperties = {
|
|
57
|
+
/** Diameter of the punches in mm, where the copies record it. */
|
|
58
|
+
punchDiameter?: number;
|
|
59
|
+
/** The tempo the edition adjusts the roll to, where it states one. */
|
|
60
|
+
tempo?: RollTempo;
|
|
61
|
+
};
|
|
62
|
+
export type Performance = {
|
|
63
|
+
readonly events: readonly AnyPerformedRollFeature[];
|
|
64
|
+
readonly curves: readonly EmulatedCurve[];
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* A reproducing piano as the edition sees it, from two sides: the tracker
|
|
68
|
+
* bar reads the holes into symbols, and the mechanism plays the symbols.
|
|
69
|
+
* Implementations live outside the core of this library, so that it does
|
|
70
|
+
* not depend on any one instrument's model; `linked-rolls/welte-t100` is
|
|
71
|
+
* the first.
|
|
72
|
+
*/
|
|
73
|
+
export interface ReproducingSystem<Options extends object> {
|
|
74
|
+
readonly name: string;
|
|
75
|
+
readonly trackerBar: TrackerBar;
|
|
76
|
+
readonly defaultOptions: Options;
|
|
77
|
+
/** Performs the events of a version, given in order of place. */
|
|
78
|
+
perform(events: readonly NegotiatedEvent[], options: Options, roll: RollProperties): Performance;
|
|
79
|
+
}
|
|
80
|
+
export {};
|