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
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Relates a scan to the tracker bar it was read with.
|
|
3
|
+
*
|
|
4
|
+
* Scanning software numbers the hole columns it finds by their position
|
|
5
|
+
* in the image, so its numbering is offset by however far the roll lay
|
|
6
|
+
* from the edge of the scanner bed. Calibrating means finding that offset,
|
|
7
|
+
* usually from a landmark such as the rewind perforation.
|
|
8
|
+
*
|
|
9
|
+
* column = offset + scannerTrack * separation
|
|
10
|
+
* trackerBar = scannerTrack + shift
|
|
11
|
+
*/
|
|
12
|
+
export interface TrackCalibration {
|
|
13
|
+
unit: 'px';
|
|
14
|
+
/** Image column of the scanning software's track 0. */
|
|
15
|
+
offset: number;
|
|
16
|
+
/** Distance between the centres of adjacent tracks. */
|
|
17
|
+
separation: number;
|
|
18
|
+
/** Added to the scanning software's numbering to reach the tracker bar. */
|
|
19
|
+
shift: number;
|
|
20
|
+
}
|
|
21
|
+
/** Image column at the centre of a tracker bar track. */
|
|
22
|
+
export declare const columnOf: (track: number, calibration: TrackCalibration) => number;
|
|
23
|
+
/** Tracker bar track covering an image column, unrounded. */
|
|
24
|
+
export declare const trackAt: (column: number, calibration: TrackCalibration) => number;
|
|
25
|
+
/**
|
|
26
|
+
* The columns covered by a run of tracks, from the outer edge of the
|
|
27
|
+
* first to the outer edge of the last.
|
|
28
|
+
*/
|
|
29
|
+
export declare const columnsOf: (from: number, to: number, calibration: TrackCalibration) => {
|
|
30
|
+
from: number;
|
|
31
|
+
to: number;
|
|
32
|
+
width: number;
|
|
33
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Image column at the centre of a tracker bar track. */
|
|
2
|
+
export const columnOf = (track, calibration) => calibration.offset + (track - calibration.shift) * calibration.separation;
|
|
3
|
+
/** Tracker bar track covering an image column, unrounded. */
|
|
4
|
+
export const trackAt = (column, calibration) => (column - calibration.offset) / calibration.separation + calibration.shift;
|
|
5
|
+
/**
|
|
6
|
+
* The columns covered by a run of tracks, from the outer edge of the
|
|
7
|
+
* first to the outer edge of the last.
|
|
8
|
+
*/
|
|
9
|
+
export const columnsOf = (from, to, calibration) => {
|
|
10
|
+
const [lower, upper] = from <= to ? [from, to] : [to, from];
|
|
11
|
+
const start = columnOf(lower, calibration) - calibration.separation / 2;
|
|
12
|
+
const end = columnOf(upper, calibration) + calibration.separation / 2;
|
|
13
|
+
return { from: start, to: end, width: end - start };
|
|
14
|
+
};
|
package/lib/TrackerBar.d.ts
CHANGED
|
@@ -1,8 +1,55 @@
|
|
|
1
1
|
import { Expression, Note } from "./Symbol";
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
/**
|
|
3
|
+
* What a tracker bar position does: sound a note, or operate one of
|
|
4
|
+
* the expression valves on the bass or the treble side.
|
|
5
|
+
*/
|
|
6
|
+
export type TrackRole = 'bass-expression' | 'note' | 'treble-expression';
|
|
7
|
+
/**
|
|
8
|
+
* A contiguous block of tracker bar positions serving one role.
|
|
9
|
+
* Both bounds are inclusive.
|
|
10
|
+
*/
|
|
11
|
+
export interface TrackArea {
|
|
12
|
+
readonly role: TrackRole;
|
|
13
|
+
readonly from: number;
|
|
14
|
+
readonly to: number;
|
|
4
15
|
}
|
|
5
|
-
export
|
|
6
|
-
|
|
7
|
-
|
|
16
|
+
export type NoteMeaning = Pick<Note, 'type' | 'pitch'>;
|
|
17
|
+
export type ExpressionMeaning = Pick<Expression, 'type' | 'expressionType' | 'scope'>;
|
|
18
|
+
export type TrackMeaning = NoteMeaning | ExpressionMeaning;
|
|
19
|
+
/**
|
|
20
|
+
* Describes a tracker bar. Track numbers are 1-based and count from
|
|
21
|
+
* the bass edge of the roll, which is the numbering used throughout
|
|
22
|
+
* the edition: a feature's `vertical.from` is a track in this sense.
|
|
23
|
+
*
|
|
24
|
+
* This is the only place where track numbers are given a meaning.
|
|
25
|
+
* Anything that needs to know where the note block ends, which side
|
|
26
|
+
* an expression belongs to, or how a track maps to a pitch, should
|
|
27
|
+
* ask the tracker bar rather than repeat the boundaries.
|
|
28
|
+
*/
|
|
29
|
+
export interface TrackerBar {
|
|
30
|
+
readonly name: string;
|
|
31
|
+
/** Width of the roll the bar reads, in mm. */
|
|
32
|
+
readonly width: number;
|
|
33
|
+
/** Number of positions on the bar. Tracks run from 1 to this. */
|
|
34
|
+
readonly trackCount: number;
|
|
35
|
+
/** The blocks of positions, from the bass edge upwards. */
|
|
36
|
+
readonly areas: readonly TrackArea[];
|
|
37
|
+
/**
|
|
38
|
+
* The position carrying the rewind perforation, which runs at
|
|
39
|
+
* the very end of a roll and is the usual landmark for calibrating
|
|
40
|
+
* a scan against the bar.
|
|
41
|
+
*/
|
|
42
|
+
readonly rewindTrack: number;
|
|
43
|
+
/** `undefined` for a position the bar does not read. */
|
|
44
|
+
meaningOf(track: number): TrackMeaning | undefined;
|
|
45
|
+
/** `undefined` for a position the bar does not read. */
|
|
46
|
+
roleOf(track: number): TrackRole | undefined;
|
|
8
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* Welte-Mignon T-100 ("red Welte"), cf. Hagmann, pp. 75 and 178.
|
|
50
|
+
*
|
|
51
|
+
* The note block spans 80 positions from C1 to g⁴, i.e. MIDI 24 to 103.
|
|
52
|
+
* The expression valves are duplicated, bass below the note block and
|
|
53
|
+
* treble above it, in mirrored order.
|
|
54
|
+
*/
|
|
55
|
+
export declare const welteT100: TrackerBar;
|
package/lib/TrackerBar.js
CHANGED
|
@@ -1,53 +1,74 @@
|
|
|
1
|
-
|
|
2
|
-
}
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
}
|
|
16
|
-
// Cf. Hagmann, p. 178
|
|
17
|
-
const expressionMap = new Map([
|
|
18
|
-
[1, 'MezzoforteOff'],
|
|
19
|
-
[2, 'MezzoforteOn'],
|
|
20
|
-
[3, 'SlowCrescendoOff'],
|
|
21
|
-
[4, 'SlowCrescendoOn'],
|
|
22
|
-
[5, 'ForzandoOff'],
|
|
23
|
-
[6, 'ForzandoOn'],
|
|
24
|
-
[7, 'SoftPedalOff'],
|
|
25
|
-
[8, 'SoftPedalOn'],
|
|
26
|
-
[9, 'MotorOff'],
|
|
27
|
-
[10, 'MotorOn'],
|
|
28
|
-
[91, 'Rewind'],
|
|
29
|
-
[92, 'ElectricCutOff'],
|
|
30
|
-
[93, 'SustainPedalOn'],
|
|
31
|
-
[94, 'SustainPedalOff'],
|
|
32
|
-
[95, 'ForzandoOn'],
|
|
33
|
-
[96, 'ForzandoOff'],
|
|
34
|
-
[97, 'SlowCrescendoOn'],
|
|
35
|
-
[98, 'SlowCrescendoOff'],
|
|
36
|
-
[99, 'MezzoforteOn'],
|
|
37
|
-
[100, 'MezzoforteOff']
|
|
38
|
-
]);
|
|
39
|
-
if (track <= 10 || track >= 91) {
|
|
40
|
-
const scope = track <= 10 ? 'bass' : 'treble';
|
|
1
|
+
const areasOf = ({ notes, trackCount }) => [
|
|
2
|
+
{ role: 'bass-expression', from: 1, to: notes.from - 1 },
|
|
3
|
+
{ role: 'note', from: notes.from, to: notes.to },
|
|
4
|
+
{ role: 'treble-expression', from: notes.to + 1, to: trackCount }
|
|
5
|
+
];
|
|
6
|
+
const scopeOf = (role) => role === 'bass-expression' ? 'bass' : 'treble';
|
|
7
|
+
const describe = (spec) => {
|
|
8
|
+
const areas = areasOf(spec);
|
|
9
|
+
const roleOf = (track) => areas.find(area => track >= area.from && track <= area.to)?.role;
|
|
10
|
+
const meaningOf = (track) => {
|
|
11
|
+
const role = roleOf(track);
|
|
12
|
+
if (!role)
|
|
13
|
+
return undefined;
|
|
14
|
+
if (role === 'note') {
|
|
41
15
|
return {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
type: 'expression'
|
|
16
|
+
type: 'note',
|
|
17
|
+
pitch: track - spec.notes.from + spec.notes.lowestPitch
|
|
45
18
|
};
|
|
46
19
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
20
|
+
const expressionType = spec.expressions.get(track);
|
|
21
|
+
if (!expressionType)
|
|
22
|
+
return undefined;
|
|
23
|
+
return { type: 'expression', expressionType, scope: scopeOf(role) };
|
|
24
|
+
};
|
|
25
|
+
const rewindTrack = [...spec.expressions]
|
|
26
|
+
.find(([, type]) => type === 'Rewind')?.[0];
|
|
27
|
+
if (rewindTrack === undefined) {
|
|
28
|
+
throw new Error(`${spec.name} declares no rewind track`);
|
|
52
29
|
}
|
|
53
|
-
|
|
30
|
+
return {
|
|
31
|
+
name: spec.name,
|
|
32
|
+
width: spec.width,
|
|
33
|
+
trackCount: spec.trackCount,
|
|
34
|
+
areas,
|
|
35
|
+
rewindTrack,
|
|
36
|
+
meaningOf,
|
|
37
|
+
roleOf
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* Welte-Mignon T-100 ("red Welte"), cf. Hagmann, pp. 75 and 178.
|
|
42
|
+
*
|
|
43
|
+
* The note block spans 80 positions from C1 to g⁴, i.e. MIDI 24 to 103.
|
|
44
|
+
* The expression valves are duplicated, bass below the note block and
|
|
45
|
+
* treble above it, in mirrored order.
|
|
46
|
+
*/
|
|
47
|
+
export const welteT100 = describe({
|
|
48
|
+
name: 'Welte-Mignon T100',
|
|
49
|
+
width: 328,
|
|
50
|
+
trackCount: 100,
|
|
51
|
+
notes: { from: 11, to: 90, lowestPitch: 24 },
|
|
52
|
+
expressions: new Map([
|
|
53
|
+
[1, 'MezzoforteOff'],
|
|
54
|
+
[2, 'MezzoforteOn'],
|
|
55
|
+
[3, 'SlowCrescendoOff'],
|
|
56
|
+
[4, 'SlowCrescendoOn'],
|
|
57
|
+
[5, 'ForzandoOff'],
|
|
58
|
+
[6, 'ForzandoOn'],
|
|
59
|
+
[7, 'SoftPedalOff'],
|
|
60
|
+
[8, 'SoftPedalOn'],
|
|
61
|
+
[9, 'MotorOff'],
|
|
62
|
+
[10, 'MotorOn'],
|
|
63
|
+
[91, 'Rewind'],
|
|
64
|
+
[92, 'ElectricCutOff'],
|
|
65
|
+
[93, 'SustainPedalOn'],
|
|
66
|
+
[94, 'SustainPedalOff'],
|
|
67
|
+
[95, 'ForzandoOn'],
|
|
68
|
+
[96, 'ForzandoOff'],
|
|
69
|
+
[97, 'SlowCrescendoOn'],
|
|
70
|
+
[98, 'SlowCrescendoOff'],
|
|
71
|
+
[99, 'MezzoforteOn'],
|
|
72
|
+
[100, 'MezzoforteOff']
|
|
73
|
+
])
|
|
74
|
+
});
|
package/lib/Version.d.ts
CHANGED
|
@@ -1,42 +1,41 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
import { Edit } from "./Edit";
|
|
2
|
+
import { ReferenceAssumption } from "./Assumption";
|
|
3
|
+
import { WithId, WithNote, WithType } from "./utils";
|
|
4
|
+
export declare const versionTypes: readonly ["edition", "unicum"];
|
|
5
|
+
/**
|
|
6
|
+
* The type of a version. An 'edition' version may serve as the
|
|
7
|
+
* master for several roll copies; a 'unicum' version exists only
|
|
8
|
+
* on one specific copy.
|
|
9
|
+
*/
|
|
7
10
|
export type VersionType = typeof versionTypes[number];
|
|
8
11
|
/**
|
|
9
|
-
*
|
|
12
|
+
* A motivation provides a reason or rationale for an editorial change.
|
|
13
|
+
* Motivations are defined at the version level and referenced by edits.
|
|
14
|
+
* @see crm:E73 Information Object
|
|
15
|
+
*/
|
|
16
|
+
export type Motivation = WithType<'motivation'> & WithId & WithNote;
|
|
17
|
+
/**
|
|
18
|
+
* A version is defined by the sum of edits applied
|
|
19
|
+
* to the version it is based on. For simple identification,
|
|
20
|
+
* a siglum is given to each version.
|
|
10
21
|
*/
|
|
11
22
|
export interface Version {
|
|
12
23
|
id: string;
|
|
24
|
+
/**
|
|
25
|
+
* A short siglum to identify the version, e.g. "A", "B1", "B2_rev", etc.
|
|
26
|
+
*/
|
|
13
27
|
siglum: string;
|
|
14
|
-
|
|
15
|
-
|
|
28
|
+
/**
|
|
29
|
+
* If no derivation is defined, it is assumed that this version represents the mother roll.
|
|
30
|
+
*/
|
|
31
|
+
basedOn?: ReferenceAssumption;
|
|
32
|
+
/**
|
|
33
|
+
* The list of edits that, applied to the base version, produce this version.
|
|
34
|
+
*/
|
|
16
35
|
edits: Edit[];
|
|
17
|
-
|
|
36
|
+
/**
|
|
37
|
+
* A collection of motivations used in this version's edits.
|
|
38
|
+
*/
|
|
39
|
+
motivations: Motivation[];
|
|
18
40
|
type: VersionType;
|
|
19
41
|
}
|
|
20
|
-
export declare const traverseVersions: (version: Version, callback: (version: Version) => void) => void;
|
|
21
|
-
export declare const getSnapshot: (version: Version) => AnySymbol[];
|
|
22
|
-
/**
|
|
23
|
-
* Walks through the versions. If it finds a symbol that is still
|
|
24
|
-
* part of the tradition (i.e. it is included in the current snapshot)
|
|
25
|
-
* and is equivalent with the given symbol, it will add the feature
|
|
26
|
-
* carrying the symbol to the collated symbol. Otherwise, the
|
|
27
|
-
* given symbol will be added to the current version's insertions.
|
|
28
|
-
*
|
|
29
|
-
* All symbols of the tradition that are not included in the given
|
|
30
|
-
* symbols are considered to be deleted.
|
|
31
|
-
*
|
|
32
|
-
* @param creation
|
|
33
|
-
* @param symbols
|
|
34
|
-
* @returns
|
|
35
|
-
*/
|
|
36
|
-
export declare function fillEdits(currentVersion: Version, symbols: AnySymbol[], tolerance?: CollationTolerance): void;
|
|
37
|
-
/**
|
|
38
|
-
* Assigns a generation (depth) to every node.
|
|
39
|
-
*/
|
|
40
|
-
export declare function assignGenerations(versions: Version[]): Array<Version & {
|
|
41
|
-
generation: number;
|
|
42
|
-
}>;
|
package/lib/Version.js
CHANGED
|
@@ -1,217 +1,11 @@
|
|
|
1
|
-
import { v4 } from "uuid";
|
|
2
|
-
import { flat } from "./EditorialAssumption";
|
|
3
|
-
import { dimensionOf } from "./Symbol";
|
|
4
1
|
export const versionTypes = [
|
|
5
2
|
/**
|
|
6
|
-
* The roll is in a state where it is used as
|
|
3
|
+
* The roll is in a state where it is (possibly) used as
|
|
7
4
|
* the master roll for several new reproductions.
|
|
8
5
|
*/
|
|
9
6
|
'edition',
|
|
10
7
|
/**
|
|
11
|
-
*
|
|
12
|
-
* Welte-Mignon piano rolls, where rolls inteded to
|
|
13
|
-
* be pulished are revised by a controller first. These
|
|
14
|
-
* rolls typically carry a "controlliert" stamp. The
|
|
15
|
-
* revision is done on the same date as the perforation
|
|
16
|
-
* and the date is written on the roll towards its end.
|
|
8
|
+
* A version that exists only on one specific copy of a roll.
|
|
17
9
|
*/
|
|
18
|
-
'
|
|
19
|
-
/**
|
|
20
|
-
* Unauthorised revisions are those, which cannot be linked
|
|
21
|
-
* to a specific controller and are likely done by
|
|
22
|
-
* a later, anonymous hand.
|
|
23
|
-
*/
|
|
24
|
-
'unauthorised-revision',
|
|
25
|
-
/**
|
|
26
|
-
* In the case of Welte Mignon rolls, glosses are
|
|
27
|
-
* typically comments about the roll's condition, added
|
|
28
|
-
* e.g. by the collector.
|
|
29
|
-
*/
|
|
30
|
-
'gloss'
|
|
10
|
+
'unicum'
|
|
31
11
|
];
|
|
32
|
-
export const traverseVersions = (version, callback) => {
|
|
33
|
-
callback(version);
|
|
34
|
-
if (version.basedOn) {
|
|
35
|
-
traverseVersions(flat(version.basedOn), callback);
|
|
36
|
-
}
|
|
37
|
-
};
|
|
38
|
-
export const getSnapshot = (version) => {
|
|
39
|
-
const snapshot = [];
|
|
40
|
-
const toDelete = [];
|
|
41
|
-
traverseVersions(version, s => {
|
|
42
|
-
snapshot.push(...s.edits.flatMap(edit => edit.insert || []));
|
|
43
|
-
// as we travel further up, remove symbols that are
|
|
44
|
-
// deleted in the versions further down
|
|
45
|
-
const deleted = [];
|
|
46
|
-
for (const toRemove of toDelete) {
|
|
47
|
-
const index = snapshot.findIndex(s => s === toRemove);
|
|
48
|
-
if (index !== -1) {
|
|
49
|
-
snapshot.splice(index, 1);
|
|
50
|
-
deleted.push(toRemove);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
for (const del of deleted) {
|
|
54
|
-
toDelete.splice(toDelete.indexOf(del), 1);
|
|
55
|
-
}
|
|
56
|
-
// collect symbols that are deleted in the current version
|
|
57
|
-
toDelete.push(...s.edits.flatMap(edit => edit.delete || []));
|
|
58
|
-
});
|
|
59
|
-
return snapshot.sort((a, b) => {
|
|
60
|
-
const aDimension = dimensionOf(a);
|
|
61
|
-
const bDimension = dimensionOf(b);
|
|
62
|
-
return aDimension.horizontal.from - bDimension.horizontal.from;
|
|
63
|
-
});
|
|
64
|
-
};
|
|
65
|
-
const isCollatable = (symbolA, symbolB, tolerance = {
|
|
66
|
-
toleranceEnd: 5,
|
|
67
|
-
toleranceStart: 5
|
|
68
|
-
}) => {
|
|
69
|
-
// two symbols are collatible if they share the same
|
|
70
|
-
// symbol characteristics (pitch, expression type etc.)
|
|
71
|
-
// and occur in the same horizontal position.
|
|
72
|
-
if (symbolA.type === 'note' && symbolB.type === 'note') {
|
|
73
|
-
if (symbolA.pitch !== symbolB.pitch)
|
|
74
|
-
return false;
|
|
75
|
-
}
|
|
76
|
-
else if (symbolA.type === 'expression' && symbolB.type === 'expression') {
|
|
77
|
-
if (symbolA.expressionType !== symbolB.expressionType)
|
|
78
|
-
return false;
|
|
79
|
-
if (symbolA.scope !== symbolB.scope)
|
|
80
|
-
return false;
|
|
81
|
-
}
|
|
82
|
-
const dimensionA = dimensionOf(symbolA);
|
|
83
|
-
const dimensionB = dimensionOf(symbolB);
|
|
84
|
-
const distanceStart = Math.abs(dimensionA.horizontal.from - dimensionB.horizontal.from);
|
|
85
|
-
const distanceEnd = Math.abs(dimensionA.horizontal.to - dimensionB.horizontal.to);
|
|
86
|
-
if (distanceStart > tolerance.toleranceStart
|
|
87
|
-
|| distanceEnd > tolerance.toleranceEnd) {
|
|
88
|
-
// the symbols are too far apart to be collated
|
|
89
|
-
return false;
|
|
90
|
-
}
|
|
91
|
-
return true;
|
|
92
|
-
};
|
|
93
|
-
const overlaps = (a, b) => {
|
|
94
|
-
const overlapsDimension = (a, b) => (a.from ?? 0) < (b.to ?? Infinity) && (b.from ?? 0) < (a.to ?? Infinity);
|
|
95
|
-
return overlapsDimension(a.horizontal, b.horizontal) && overlapsDimension(a.vertical, b.vertical);
|
|
96
|
-
};
|
|
97
|
-
/**
|
|
98
|
-
* Walks through the versions. If it finds a symbol that is still
|
|
99
|
-
* part of the tradition (i.e. it is included in the current snapshot)
|
|
100
|
-
* and is equivalent with the given symbol, it will add the feature
|
|
101
|
-
* carrying the symbol to the collated symbol. Otherwise, the
|
|
102
|
-
* given symbol will be added to the current version's insertions.
|
|
103
|
-
*
|
|
104
|
-
* All symbols of the tradition that are not included in the given
|
|
105
|
-
* symbols are considered to be deleted.
|
|
106
|
-
*
|
|
107
|
-
* @param creation
|
|
108
|
-
* @param symbols
|
|
109
|
-
* @returns
|
|
110
|
-
*/
|
|
111
|
-
export function fillEdits(currentVersion, symbols, tolerance = {
|
|
112
|
-
toleranceEnd: 5,
|
|
113
|
-
toleranceStart: 5
|
|
114
|
-
}) {
|
|
115
|
-
const snapshot = getSnapshot(currentVersion);
|
|
116
|
-
const treatedSymbols = [];
|
|
117
|
-
// can it be collated with any of the symbols of
|
|
118
|
-
// included in the current snapshot?
|
|
119
|
-
const insertions = [...symbols];
|
|
120
|
-
for (const symbol of symbols) {
|
|
121
|
-
snapshot
|
|
122
|
-
.filter(toCompare => isCollatable(symbol, toCompare, tolerance))
|
|
123
|
-
.forEach(corresp => {
|
|
124
|
-
corresp.carriers.push(...symbol.carriers);
|
|
125
|
-
insertions.splice(insertions.indexOf(symbol), 1);
|
|
126
|
-
treatedSymbols.push(corresp);
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
// special treatment for covers
|
|
130
|
-
const covers = insertions.filter(symbol => symbol.type === 'cover');
|
|
131
|
-
for (const cover of covers) {
|
|
132
|
-
// find perforations in the snapshot that
|
|
133
|
-
// overlap with the cover
|
|
134
|
-
snapshot
|
|
135
|
-
.filter(symbol => symbol.type === 'note' || symbol.type === 'expression')
|
|
136
|
-
.map(dimensionOf)
|
|
137
|
-
.filter(dimension => overlaps(dimension, dimensionOf(cover)))
|
|
138
|
-
.forEach(dimension => {
|
|
139
|
-
const coverDimension = dimensionOf(cover);
|
|
140
|
-
// check if the cover partially covers the beginning
|
|
141
|
-
if (dimension.horizontal.from >= coverDimension.horizontal.from &&
|
|
142
|
-
dimension.horizontal.from <= coverDimension.horizontal.to) {
|
|
143
|
-
// the note starts where the cover ends
|
|
144
|
-
dimension.horizontal.from = coverDimension.horizontal.to;
|
|
145
|
-
}
|
|
146
|
-
// check if the cover partially covers the ending
|
|
147
|
-
if (dimension.horizontal.to >= coverDimension.horizontal.from &&
|
|
148
|
-
dimension.horizontal.to <= coverDimension.horizontal.to) {
|
|
149
|
-
// the note ends where the cover starts
|
|
150
|
-
dimension.horizontal.to = coverDimension.horizontal.from;
|
|
151
|
-
}
|
|
152
|
-
});
|
|
153
|
-
insertions.splice(insertions.indexOf(cover), 1);
|
|
154
|
-
}
|
|
155
|
-
currentVersion.edits.push(...insertions.map((symbol) => {
|
|
156
|
-
return {
|
|
157
|
-
insert: [symbol],
|
|
158
|
-
delete: [],
|
|
159
|
-
id: v4(),
|
|
160
|
-
};
|
|
161
|
-
}));
|
|
162
|
-
const deletions = snapshot.filter(sym => {
|
|
163
|
-
return !treatedSymbols.includes(sym);
|
|
164
|
-
});
|
|
165
|
-
for (const symbol of deletions) {
|
|
166
|
-
currentVersion.edits.push({
|
|
167
|
-
insert: [],
|
|
168
|
-
delete: [symbol],
|
|
169
|
-
id: v4(),
|
|
170
|
-
});
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
/**
|
|
174
|
-
* Assigns a generation (depth) to every node.
|
|
175
|
-
*/
|
|
176
|
-
export function assignGenerations(versions) {
|
|
177
|
-
const byId = new Map();
|
|
178
|
-
for (const n of versions)
|
|
179
|
-
byId.set(n.id, n);
|
|
180
|
-
const memo = new Map(); // id -> generation
|
|
181
|
-
const inStack = new Set(); // for cycle detection
|
|
182
|
-
const computeGen = (id) => {
|
|
183
|
-
if (memo.has(id))
|
|
184
|
-
return memo.get(id);
|
|
185
|
-
if (inStack.has(id)) {
|
|
186
|
-
throw new Error(`Cycle detected involving node '${id}'. Check parentId links.`);
|
|
187
|
-
}
|
|
188
|
-
const node = byId.get(id);
|
|
189
|
-
if (!node) {
|
|
190
|
-
// If you ever call computeGen on a missing id, treat as root.
|
|
191
|
-
memo.set(id, 0);
|
|
192
|
-
return 0;
|
|
193
|
-
}
|
|
194
|
-
inStack.add(id);
|
|
195
|
-
let gen;
|
|
196
|
-
const p = node.basedOn?.assigned.id;
|
|
197
|
-
if (p === undefined) {
|
|
198
|
-
gen = 0; // root
|
|
199
|
-
}
|
|
200
|
-
else if (!byId.has(p)) {
|
|
201
|
-
// Orphaned parent reference — treat boundary as root
|
|
202
|
-
gen = 0;
|
|
203
|
-
}
|
|
204
|
-
else {
|
|
205
|
-
gen = 1 + computeGen(p);
|
|
206
|
-
}
|
|
207
|
-
inStack.delete(id);
|
|
208
|
-
memo.set(id, gen);
|
|
209
|
-
return gen;
|
|
210
|
-
};
|
|
211
|
-
// Compute for all nodes (order doesn’t matter)
|
|
212
|
-
const withGen = versions.map(n => ({
|
|
213
|
-
...n,
|
|
214
|
-
generation: computeGen(n.id),
|
|
215
|
-
}));
|
|
216
|
-
return withGen;
|
|
217
|
-
}
|
package/lib/alignFeatures.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { AnyFeature } from "./Feature";
|
|
2
|
+
import { TrackerBar } from "./TrackerBar";
|
|
2
3
|
type AlignmentResult = {
|
|
3
4
|
shift: number;
|
|
4
5
|
stretch: number;
|
|
@@ -7,5 +8,5 @@ type AlignmentResult = {
|
|
|
7
8
|
* Align two rolls by computing independent linear fits of each roll's note-onset positions
|
|
8
9
|
* using only the first and last segments, then deriving a transform x2 = (x1 + shift) * stretch.
|
|
9
10
|
*/
|
|
10
|
-
export declare function alignFeatures(rollA:
|
|
11
|
+
export declare function alignFeatures(rollA: AnyFeature[], rollB: AnyFeature[], bar?: TrackerBar): AlignmentResult;
|
|
11
12
|
export {};
|
package/lib/alignFeatures.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
const
|
|
3
|
-
return
|
|
1
|
+
import { welteT100 } from "./TrackerBar";
|
|
2
|
+
const isNoteOn = (bar) => (feature) => {
|
|
3
|
+
return feature.type === 'Hole' && bar.meaningOf(feature.vertical.from)?.type === 'note';
|
|
4
4
|
};
|
|
5
5
|
/**
|
|
6
6
|
* Fit a line: position = alpha * index + beta via least squares.
|
|
@@ -33,8 +33,9 @@ function selectEnds(arr, count) {
|
|
|
33
33
|
* Align two rolls by computing independent linear fits of each roll's note-onset positions
|
|
34
34
|
* using only the first and last segments, then deriving a transform x2 = (x1 + shift) * stretch.
|
|
35
35
|
*/
|
|
36
|
-
export function alignFeatures(rollA, rollB) {
|
|
36
|
+
export function alignFeatures(rollA, rollB, bar = welteT100) {
|
|
37
37
|
// 1. Extract note-onset positions
|
|
38
|
+
const isNote = isNoteOn(bar);
|
|
38
39
|
const allXA = rollA.filter(isNote).map(f => f.horizontal.from);
|
|
39
40
|
const allXB = rollB.filter(isNote).map(f => f.horizontal.from);
|
|
40
41
|
// 2. Determine segment size (e.g. 10% of notes, min 5)
|
package/lib/asJsonLd.d.ts
CHANGED
package/lib/asJsonLd.js
CHANGED
|
@@ -1,13 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
'delete',
|
|
7
|
-
'comprehends'
|
|
8
|
-
];
|
|
9
|
-
const asIDArray = (arr) => {
|
|
10
|
-
return arr.map(e => e.id);
|
|
1
|
+
export const exportDate = (date) => {
|
|
2
|
+
const year = date.getFullYear();
|
|
3
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
4
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
5
|
+
return `${year}-${month}-${day}`;
|
|
11
6
|
};
|
|
12
7
|
const asJsonLdEntity = (obj) => {
|
|
13
8
|
if (obj instanceof Date) {
|
|
@@ -21,19 +16,6 @@ const asJsonLdEntity = (obj) => {
|
|
|
21
16
|
if (typeof value === 'function' || typeof value === 'undefined') {
|
|
22
17
|
// ignore
|
|
23
18
|
}
|
|
24
|
-
else if (referenceTypes.includes(key)) {
|
|
25
|
-
if (!Array.isArray(value)) {
|
|
26
|
-
console.error(`Expected array for key ${key}, got ${value}`);
|
|
27
|
-
}
|
|
28
|
-
else {
|
|
29
|
-
result[key] = asIDArray(value);
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
else if (key === 'assigned'
|
|
33
|
-
&& (result['@type'] === 'derivation'
|
|
34
|
-
|| result['@type'] === 'carrierAssignment')) {
|
|
35
|
-
result['assigned'] = value.id;
|
|
36
|
-
}
|
|
37
19
|
else if (key === 'type') {
|
|
38
20
|
result['@type'] = value;
|
|
39
21
|
}
|
|
@@ -53,7 +35,8 @@ const asJsonLdEntity = (obj) => {
|
|
|
53
35
|
return result;
|
|
54
36
|
};
|
|
55
37
|
export const asJsonLd = (edition) => {
|
|
56
|
-
const
|
|
38
|
+
const { base, copies, ...rest } = asJsonLdEntity(edition);
|
|
39
|
+
return {
|
|
57
40
|
'@context': [
|
|
58
41
|
'https://linked-rolls.org/rollo/1.0/edition.jsonld',
|
|
59
42
|
{
|
|
@@ -61,7 +44,11 @@ export const asJsonLd = (edition) => {
|
|
|
61
44
|
}
|
|
62
45
|
],
|
|
63
46
|
'@type': "Edition",
|
|
64
|
-
|
|
47
|
+
'@id': edition.base,
|
|
48
|
+
...rest,
|
|
49
|
+
copies: copies?.map(copy => ({
|
|
50
|
+
...copy,
|
|
51
|
+
'@id': `copy/${copy['@id']}`
|
|
52
|
+
}))
|
|
65
53
|
};
|
|
66
|
-
return result;
|
|
67
54
|
};
|
package/lib/importJsonLd.d.ts
CHANGED