linked-rolls 0.3.0 → 0.4.1

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 CHANGED
@@ -69,20 +69,28 @@ reads editions does not need it. For development on both at once, a
69
69
  checkout of [welte-t100](https://github.com/pfefferniels/welte-t100)
70
70
  beside this repository, declared as `file:../welte-t100`, works too.
71
71
 
72
- ### Alignment and pairing
72
+ ### Alignment, order and pairing
73
73
 
74
- A perforation takes its place from the holes that carry it. Two fields
74
+ A perforation takes its place from the holes that carry it. Four fields
75
75
  on a perforation let an editor state where the measurement should give
76
76
  way. `alignedWith` names another perforation whose onset this one takes
77
77
  in the performance, as a "crescendo off" is meant to fall on the note
78
- it belongs to. `pairedWith` names a partner whose distance to this one
79
- is fixed, as a "forzando on" belongs with its "forzando off": whatever
80
- displaces the one displaces the other. Any two perforations may be
81
- paired, the relation is symmetric, and it is stated on one side only.
82
- Both are applied when a version is emulated. `constraintProblems` lists,
83
- version by version, the cases in which the statements cannot hold: a
84
- reference or partner that is absent, a perforation claimed by several
85
- pairs, or a pair whose members are both aligned.
78
+ it belongs to. `before` and `after` name a perforation whose onset this
79
+ one precedes or follows, without saying by how much: where the copies
80
+ disagree on which side of a note an expression falls, the statement
81
+ settles the order. A perforation the measurement already has on the
82
+ stated side keeps its place. One it does not is put on that side, as
83
+ far from the reference as the copies that agree with the statement put
84
+ it, and a punch diameter away where none does. A perforation makes one
85
+ of these three statements at most. `pairedWith` names a partner whose
86
+ distance to this one is fixed, as a "forzando on" belongs with its
87
+ "forzando off": whatever displaces the one displaces the other. Any two
88
+ perforations may be paired, the relation is symmetric, and it is stated
89
+ on one side only. All are applied when a version is emulated.
90
+ `constraintProblems` lists, version by version, the cases in which the
91
+ statements cannot hold: a reference or partner that is absent, a
92
+ perforation placed relative to itself or in several ways at once, one
93
+ claimed by several pairs, or a pair whose members are both placed.
86
94
 
87
95
  ## Building
88
96
  ```
@@ -22,7 +22,13 @@ export declare class Emulation<Options extends object> {
22
22
  curves: readonly EmulatedCurve[];
23
23
  source?: string;
24
24
  constructor(system: ReproducingSystem<Options>, options?: Options);
25
- applyConstraints(): void;
25
+ /**
26
+ * Moves the negotiated events to where their statements put them.
27
+ * The view supplies the copies, whose measurements decide how far
28
+ * before or after its reference a perforation goes, and a punch
29
+ * diameter, or a millimetre, where no copy agrees with a statement.
30
+ */
31
+ applyConstraints(view: EditionView): void;
26
32
  emulateVersion(version: Version, view: EditionView, { range, skipToFirstNote }?: EmulationScope): AnyPerformedRollFeature[];
27
33
  findEventsPerforming(id: string): AnyPerformedRollFeature[];
28
34
  asMIDI(): MidiFile;
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 } from "./Symbol";
3
+ import { pairsAmong, placementsOf } from "./Symbol";
4
4
  /** The punch diameter the edition's copies report, where any of them does. */
5
5
  const punchDiameterOf = (view) => {
6
6
  const measured = view.edition.copies
@@ -14,24 +14,70 @@ const propertiesOf = (view) => ({
14
14
  punchDiameter: punchDiameterOf(view),
15
15
  tempo: view.edition.tempoAdjustment
16
16
  });
17
+ const mean = (values) => values.reduce((sum, value) => sum + value, 0) / values.length;
18
+ /** The onset a symbol has on each copy carrying it, by the copy's index, as the mean of its holes there. */
19
+ const onsetsByCopy = (view, symbolId) => {
20
+ const symbol = view.get(symbolId);
21
+ if (!symbol)
22
+ return new Map();
23
+ const onsets = view.carriersOf(symbol).flatMap((carrier) => {
24
+ const copy = view.getPath(carrier.id)?.[1];
25
+ return typeof copy === 'number' ? [[copy, carrier.horizontal.from]] : [];
26
+ });
27
+ const copies = new Set(onsets.map(([copy]) => copy));
28
+ return new Map([...copies].map(copy => [copy, mean(onsets.filter(([at]) => at === copy).map(([, onset]) => onset))]));
29
+ };
30
+ /**
31
+ * The measured distance from the onset of the reference to the onset of
32
+ * the follower on each copy carrying both, negative where the follower
33
+ * comes first there.
34
+ */
35
+ const offsetsBetween = (view, followerId, referenceId) => {
36
+ const references = onsetsByCopy(view, referenceId);
37
+ return [...onsetsByCopy(view, followerId)].flatMap(([copy, onset]) => {
38
+ const reference = references.get(copy);
39
+ return reference === undefined ? [] : [onset - reference];
40
+ });
41
+ };
17
42
  /**
18
- * How far each event has to move, in mm, for the alignments to hold:
19
- * an aligned event takes the onset of its reference, and a paired
20
- * event follows its partner so that the distance between the two
21
- * is kept. Events that stay where they are do not appear.
43
+ * How far before or after its reference a follower is put when the
44
+ * measurement does not already have it there: as far as the copies
45
+ * that agree with the statement put it, and one gap where none does.
22
46
  */
23
- const displacementsOf = (events) => {
47
+ const distanceOnSide = (side, offsets, gap) => {
48
+ const agreeing = offsets
49
+ .filter(offset => side === 'before' ? offset < 0 : offset > 0)
50
+ .map(Math.abs);
51
+ return agreeing.length > 0 ? mean(agreeing) : gap;
52
+ };
53
+ /**
54
+ * How far each event has to move, in mm, for the placements to hold:
55
+ * an aligned event takes the onset of its reference, one placed before
56
+ * or after its reference keeps its place where the measurement has it
57
+ * on that side and is moved there where it does not, and a paired
58
+ * event follows its partner so that the distance between the two is
59
+ * kept. Events that stay where they are do not appear.
60
+ */
61
+ const displacementsOf = (events, offsetsBetween, gap) => {
24
62
  const byId = new Map(events.map(event => [event.id, event]));
25
- const referenceOf = (event) => event.alignedWith && byId.get(idOf(event.alignedWith));
26
- const alignedOnsetOf = (event, visited = new Set()) => {
27
- const reference = referenceOf(event);
28
- if (!reference || visited.has(event.id))
29
- return event.horizontal.from;
30
- return alignedOnsetOf(reference, visited.add(event.id));
63
+ /** Where an event comes to lie once its statement and those of its references hold. */
64
+ const placedOnsetOf = (event, visited = new Set()) => {
65
+ const measured = event.horizontal.from;
66
+ const placement = placementsOf(event)[0];
67
+ const reference = placement && byId.get(idOf(placement.reference));
68
+ if (!placement || !reference || reference.id === event.id || visited.has(event.id))
69
+ return measured;
70
+ const referenceOnset = placedOnsetOf(reference, new Set([...visited, event.id]));
71
+ const distance = (side) => distanceOnSide(side, offsetsBetween(event.id, reference.id), gap);
72
+ switch (placement.relation) {
73
+ case 'alignedWith': return referenceOnset;
74
+ case 'before': return measured < referenceOnset ? measured : referenceOnset - distance('before');
75
+ case 'after': return measured > referenceOnset ? measured : referenceOnset + distance('after');
76
+ }
31
77
  };
32
78
  const displacements = new Map(events
33
- .filter(event => referenceOf(event) !== undefined)
34
- .map((event) => [event, alignedOnsetOf(event) - event.horizontal.from]));
79
+ .map((event) => [event, placedOnsetOf(event) - event.horizontal.from])
80
+ .filter(([, distance]) => distance !== 0));
35
81
  pairsAmong(events).forEach(([one, other]) => {
36
82
  const shared = displacements.get(one) ?? displacements.get(other);
37
83
  if (shared === undefined)
@@ -89,8 +135,16 @@ export class Emulation {
89
135
  this.system = system;
90
136
  this.options = options;
91
137
  }
92
- applyConstraints() {
93
- displacementsOf(this.negotiatedEvents).forEach((distance, event) => {
138
+ /**
139
+ * Moves the negotiated events to where their statements put them.
140
+ * The view supplies the copies, whose measurements decide how far
141
+ * before or after its reference a perforation goes, and a punch
142
+ * diameter, or a millimetre, where no copy agrees with a statement.
143
+ */
144
+ applyConstraints(view) {
145
+ const gap = punchDiameterOf(view) ?? 1;
146
+ const offsets = (follower, reference) => offsetsBetween(view, follower, reference);
147
+ displacementsOf(this.negotiatedEvents, offsets, gap).forEach((distance, event) => {
94
148
  event.horizontal.from += distance;
95
149
  event.horizontal.to += distance;
96
150
  });
@@ -118,7 +172,7 @@ export class Emulation {
118
172
  this.curves = [];
119
173
  return this.midiEvents;
120
174
  }
121
- this.applyConstraints();
175
+ this.applyConstraints(view);
122
176
  const performance = this.system.perform(this.negotiatedEvents, this.options, propertiesOf(view));
123
177
  this.curves = performance.curves;
124
178
  const onsets = performance.events.filter(event => event.type === 'noteOn').map(event => event.at);
package/lib/Plan.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { Draft } from "immer";
2
2
  import { EditionView } from "./EditionView";
3
3
  import { Edition } from "./Edition";
4
+ import { AnySymbol } from "./Symbol";
4
5
  import { CollationTolerance } from "./Collation";
5
6
  import { Edit } from "./Edit";
6
7
  import { RollCopy } from "./RollCopy";
@@ -175,6 +176,13 @@ export declare class RemoveFeature extends BasePlan {
175
176
  constructor(copyID: string, featureIDs: string[]);
176
177
  build(): EditionOp[];
177
178
  }
179
+ /** The symbols of the versions that no other copy carries. */
180
+ export declare const symbolsCarriedOnlyBy: (edition: Edition, copyId: string) => AnySymbol[];
181
+ /**
182
+ * Takes the copy out of the edition together with the symbols only it
183
+ * carries, and with every reference the versions made to those symbols.
184
+ */
185
+ export declare const removeCopy: (copyId: string) => EditionOp;
178
186
  export declare class MergeEdits extends BasePlan {
179
187
  private versionId;
180
188
  private toMerge;
package/lib/Plan.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { getAt } from "./EditionView";
2
+ import { isPerforation, placementRelations } from "./Symbol";
2
3
  import { v4 } from "uuid";
3
4
  import { asSymbols } from "./RollCopy";
4
5
  import { assignReference, idOf } from "./Assumption";
@@ -378,6 +379,60 @@ export class RemoveFeature extends BasePlan {
378
379
  ];
379
380
  }
380
381
  }
382
+ const featureIdsOf = (copy) => new Set(copy.features.map(feature => feature.id));
383
+ const insertedIn = (versions) => versions.flatMap(version => version.edits).flatMap(edit => edit.insert ?? []);
384
+ /**
385
+ * A symbol every carrier of which lies on the copy loses its evidence
386
+ * with the copy. A symbol without carriers, such as a label, stands
387
+ * on its own.
388
+ */
389
+ const carriedOnlyOn = (features) => (symbol) => symbol.carriers.length > 0 && symbol.carriers.every(carrier => features.has(idOf(carrier)));
390
+ /** The symbols of the versions that no other copy carries. */
391
+ export const symbolsCarriedOnlyBy = (edition, copyId) => {
392
+ const copy = edition.copies.find(c => c.id === copyId);
393
+ return copy ? insertedIn(edition.versions).filter(carriedOnlyOn(featureIdsOf(copy))) : [];
394
+ };
395
+ const references = [...placementRelations, 'pairedWith'];
396
+ const forgetPerforations = (perforation, dropped) => references
397
+ .filter(relation => {
398
+ const reference = perforation[relation];
399
+ return reference && dropped.has(idOf(reference));
400
+ })
401
+ .forEach(relation => { delete perforation[relation]; });
402
+ const forgetFeatures = (symbol, features, dropped) => {
403
+ symbol.carriers = symbol.carriers.filter(carrier => !features.has(idOf(carrier)));
404
+ if (isPerforation(symbol))
405
+ forgetPerforations(symbol, dropped);
406
+ };
407
+ const forgetCopy = (edit, features, dropped) => {
408
+ if (edit.insert) {
409
+ edit.insert = edit.insert.filter(symbol => !dropped.has(symbol.id));
410
+ edit.insert.forEach(symbol => forgetFeatures(symbol, features, dropped));
411
+ }
412
+ if (edit.delete) {
413
+ edit.delete = edit.delete.filter(id => !dropped.has(id));
414
+ }
415
+ };
416
+ const isEmpty = (edit) => !edit.insert?.length && !edit.delete?.length;
417
+ /** Drops the edits the removal has emptied and leaves those that were empty before alone. */
418
+ const forgetCopyIn = (version, features, dropped) => {
419
+ const emptyBefore = new Set(version.edits.filter(isEmpty).map(edit => edit.id));
420
+ version.edits.forEach(edit => forgetCopy(edit, features, dropped));
421
+ version.edits = version.edits.filter(edit => !isEmpty(edit) || emptyBefore.has(edit.id));
422
+ };
423
+ /**
424
+ * Takes the copy out of the edition together with the symbols only it
425
+ * carries, and with every reference the versions made to those symbols.
426
+ */
427
+ export const removeCopy = (copyId) => draft => {
428
+ const copy = draft.copies.find(c => c.id === copyId);
429
+ if (!copy)
430
+ return;
431
+ const features = featureIdsOf(copy);
432
+ const dropped = new Set(insertedIn(draft.versions).filter(carriedOnlyOn(features)).map(symbol => symbol.id));
433
+ draft.copies = draft.copies.filter(c => c.id !== copyId);
434
+ draft.versions.forEach(version => forgetCopyIn(version, features, dropped));
435
+ };
381
436
  export class MergeEdits extends BasePlan {
382
437
  constructor(versionId, toMerge) {
383
438
  super();
package/lib/Symbol.d.ts CHANGED
@@ -19,6 +19,7 @@ export interface Symbol<T extends string> extends WithId {
19
19
  carriers: ReferenceAssumption[];
20
20
  }
21
21
  export declare const isSymbol: (object: any) => object is AnySymbol;
22
+ export declare const isPerforation: (symbol: AnySymbol) => symbol is Note | Expression;
22
23
  /**
23
24
  * A perforation is a symbol that is typically encoded as a single punched
24
25
  * hole or a group of punched holes in the physical carrier.
@@ -34,6 +35,24 @@ export interface Perforation<T extends string> extends Symbol<T> {
34
35
  * @see reo:alignedWith
35
36
  */
36
37
  alignedWith?: ReferenceAssumption;
38
+ /**
39
+ * A perforation whose onset this one precedes when the roll is
40
+ * performed, without saying by how much: a "crescendo off" ends
41
+ * before the note it leads to begins, even where the copies
42
+ * disagree on it. Where the measurement has it so, nothing moves;
43
+ * where it does not, this one is placed before the reference as
44
+ * far as the copies that agree put it. This points to the
45
+ * perforation by its `@id`.
46
+ * @see reo:before
47
+ */
48
+ before?: ReferenceAssumption;
49
+ /**
50
+ * A perforation whose onset this one follows when the roll is
51
+ * performed, the counterpart of `before`. This points to the
52
+ * perforation by its `@id`.
53
+ * @see reo:after
54
+ */
55
+ after?: ReferenceAssumption;
37
56
  /**
38
57
  * The perforation this one forms a pair with, e.g. a "forzando on"
39
58
  * with its "forzando off". Any two perforations may be paired.
@@ -44,6 +63,20 @@ export interface Perforation<T extends string> extends Symbol<T> {
44
63
  */
45
64
  pairedWith?: ReferenceAssumption;
46
65
  }
66
+ export declare const placementRelations: readonly ["alignedWith", "before", "after"];
67
+ /** The ways a perforation may be placed relative to another. */
68
+ export type PlacementRelation = typeof placementRelations[number];
69
+ type Placeable = Partial<Record<PlacementRelation, ReferenceAssumption>>;
70
+ export type Placement = {
71
+ relation: PlacementRelation;
72
+ reference: ReferenceAssumption;
73
+ };
74
+ /**
75
+ * The statements placing a perforation relative to others, alignment
76
+ * first. A perforation is meant to make one at most; the first is the
77
+ * one a performance applies.
78
+ */
79
+ export declare const placementsOf: (perforation: Placeable) => Placement[];
47
80
  type Pairable = WithId & {
48
81
  pairedWith?: ReferenceAssumption;
49
82
  };
package/lib/Symbol.js CHANGED
@@ -3,6 +3,17 @@ export const isSymbol = (object) => {
3
3
  return ('type' in object
4
4
  && (object.type === 'note' || object.type === 'expression' || object.type === 'text'));
5
5
  };
6
+ export const isPerforation = (symbol) => symbol.type !== 'text';
7
+ export const placementRelations = ['alignedWith', 'before', 'after'];
8
+ /**
9
+ * The statements placing a perforation relative to others, alignment
10
+ * first. A perforation is meant to make one at most; the first is the
11
+ * one a performance applies.
12
+ */
13
+ export const placementsOf = (perforation) => placementRelations.flatMap(relation => {
14
+ const reference = perforation[relation];
15
+ return reference ? [{ relation, reference }] : [];
16
+ });
6
17
  /**
7
18
  * The pairs among the given perforations, each once and in the order
8
19
  * the pairing is stated. A pair whose partner is absent is left out.
package/lib/schema.json CHANGED
@@ -425,10 +425,18 @@
425
425
  "Expression": {
426
426
  "description": "An expression symbol, representing a perforation on the roll that governs dynamics, pedaling, or mechanical functions of the reproducing piano. Each expression has a scope (bass or treble) and a specific expression type. [ontology: reo:Expression]",
427
427
  "properties": {
428
+ "after": {
429
+ "$ref": "#/definitions/ReferenceAssumption",
430
+ "description": "A perforation whose onset this one follows when the roll is performed, the counterpart of `before`. This points to the perforation by its `@id`. [ontology: reo:after]"
431
+ },
428
432
  "alignedWith": {
429
433
  "$ref": "#/definitions/ReferenceAssumption",
430
434
  "description": "In piano rolls, perforations are often aligned with other perforations, e.g. a \"crescendo off\" might be logically aligned to the start of a note perforation. This points to the perforation by its `@id`. [ontology: reo:alignedWith]"
431
435
  },
436
+ "before": {
437
+ "$ref": "#/definitions/ReferenceAssumption",
438
+ "description": "A perforation whose onset this one precedes when the roll is performed, without saying by how much: a \"crescendo off\" ends before the note it leads to begins, even where the copies disagree on it. Where the measurement has it so, nothing moves; where it does not, this one is placed before the reference as far as the copies that agree put it. This points to the perforation by its `@id`. [ontology: reo:before]"
439
+ },
432
440
  "carriers": {
433
441
  "description": "References to the physical features (e.g. holes) on the roll copies that carry this symbol. Since the association might be debatable, it can be annotated. There might be e.g. doubts about the meaning and correct transcription of a feature on a physical roll. This points to a feature by its `@id`. [ontology: crm:P128i is carried by]",
434
442
  "items": {
@@ -813,10 +821,18 @@
813
821
  "Note": {
814
822
  "description": "A note symbol, representing a single pitched musical event on the roll. The pitch is encoded via the tracker bar position (track number). [ontology: reo:Note]",
815
823
  "properties": {
824
+ "after": {
825
+ "$ref": "#/definitions/ReferenceAssumption",
826
+ "description": "A perforation whose onset this one follows when the roll is performed, the counterpart of `before`. This points to the perforation by its `@id`. [ontology: reo:after]"
827
+ },
816
828
  "alignedWith": {
817
829
  "$ref": "#/definitions/ReferenceAssumption",
818
830
  "description": "In piano rolls, perforations are often aligned with other perforations, e.g. a \"crescendo off\" might be logically aligned to the start of a note perforation. This points to the perforation by its `@id`. [ontology: reo:alignedWith]"
819
831
  },
832
+ "before": {
833
+ "$ref": "#/definitions/ReferenceAssumption",
834
+ "description": "A perforation whose onset this one precedes when the roll is performed, without saying by how much: a \"crescendo off\" ends before the note it leads to begins, even where the copies disagree on it. Where the measurement has it so, nothing moves; where it does not, this one is placed before the reference as far as the copies that agree put it. This points to the perforation by its `@id`. [ontology: reo:before]"
835
+ },
820
836
  "carriers": {
821
837
  "description": "References to the physical features (e.g. holes) on the roll copies that carry this symbol. Since the association might be debatable, it can be annotated. There might be e.g. doubts about the meaning and correct transcription of a feature on a physical roll. This points to a feature by its `@id`. [ontology: crm:P128i is carried by]",
822
838
  "items": {
@@ -1173,7 +1189,7 @@
1173
1189
  ],
1174
1190
  "type": "object"
1175
1191
  },
1176
- "ObjectAssumption<alias-731470504-73739-73887-731470504-0-217694<def-interface-src_Symbol.ts-4034-4398-src_Symbol.ts-0-4606400334313,\"carriers\">>": {
1192
+ "ObjectAssumption<alias-731470504-73739-73887-731470504-0-217694<def-interface-src_Symbol.ts-5670-6034-src_Symbol.ts-0-6242400334313,\"carriers\">>": {
1177
1193
  "description": "An object assumption wraps a complex object with an optional annotation. Used for structured values (e.g. persons, conditions) whose properties may be uncertain.",
1178
1194
  "properties": {
1179
1195
  "@annotation": {
@@ -1744,7 +1760,7 @@
1744
1760
  "description": "The method by which this writing was produced (e.g. through print, handwriting, or stamping). [ontology: crm:P2 has type]"
1745
1761
  },
1746
1762
  "transcription": {
1747
- "$ref": "#/definitions/ObjectAssumption%3Calias-731470504-73739-73887-731470504-0-217694%3Cdef-interface-src_Symbol.ts-4034-4398-src_Symbol.ts-0-4606400334313%2C%22carriers%22%3E%3E",
1763
+ "$ref": "#/definitions/ObjectAssumption%3Calias-731470504-73739-73887-731470504-0-217694%3Cdef-interface-src_Symbol.ts-5670-6034-src_Symbol.ts-0-6242400334313%2C%22carriers%22%3E%3E",
1748
1764
  "description": "A transcription of the text content of the writing. This is an object assumption so that the transcription can be annotated with a belief about its correctness. [ontology: crm:P128 carries]"
1749
1765
  },
1750
1766
  "vertical": {
@@ -208,6 +208,8 @@
208
208
  "motivations": "@included",
209
209
  "carriers": "crm:P128i_is_carried_by",
210
210
  "alignedWith": "reo:alignedWith",
211
+ "before": "reo:before",
212
+ "after": "reo:after",
211
213
  "pairedWith": "reo:pairedWith",
212
214
  "pitch": {
213
215
  "@id": "reo:pitch",
package/lib/validate.d.ts CHANGED
@@ -6,14 +6,15 @@ export { validate };
6
6
  export type ConstraintProblem = {
7
7
  version: string;
8
8
  symbol: string;
9
- problem: 'alignment-reference-missing' | 'partner-missing' | 'paired-with-itself' | 'in-several-pairs' | 'pair-aligned-on-both-sides';
9
+ problem: 'alignment-reference-missing' | 'before-reference-missing' | 'after-reference-missing' | 'placed-relative-to-itself' | 'placed-several-ways' | 'partner-missing' | 'paired-with-itself' | 'in-several-pairs' | 'pair-placed-on-both-sides';
10
10
  };
11
11
  /**
12
- * Where the alignments and pairings of the edition cannot hold as
12
+ * Where the placements and pairings of the edition cannot hold as
13
13
  * stated, version by version: a reference or partner absent from the
14
- * version, a perforation claimed by several pairs, or a pair whose
15
- * members are both aligned and so cannot keep their distance and
16
- * follow their references at once.
14
+ * version, a perforation placed relative to itself or in several ways
15
+ * at once, one claimed by several pairs, or a pair whose members are
16
+ * both placed and so cannot keep their distance and follow their
17
+ * references at once.
17
18
  */
18
19
  export declare const constraintProblems: (view: EditionView) => ConstraintProblem[];
19
20
  export type UnknownExpressionType = {
package/lib/validate.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import Ajv from "ajv";
2
2
  import * as schema from "./schema.json";
3
3
  import { idOf } from "./Assumption";
4
- import { pairsAmong } from "./Symbol";
4
+ import { isPerforation, pairsAmong, placementsOf } from "./Symbol";
5
5
  const ajv = new Ajv({
6
6
  strict: false,
7
7
  formats: {
@@ -10,13 +10,24 @@ const ajv = new Ajv({
10
10
  });
11
11
  const validate = ajv.compile(schema);
12
12
  export { validate };
13
- const isPerforation = (symbol) => symbol.type !== 'text';
13
+ const missingReference = {
14
+ alignedWith: 'alignment-reference-missing',
15
+ before: 'before-reference-missing',
16
+ after: 'after-reference-missing'
17
+ };
14
18
  const problemsIn = (version, perforations) => {
15
19
  const ids = new Set(perforations.map(p => p.id));
16
20
  const report = (symbol, problem) => ({ version, symbol, problem });
17
21
  const missingReferences = perforations
18
- .filter(p => p.alignedWith && !ids.has(idOf(p.alignedWith)))
19
- .map(p => report(p.id, 'alignment-reference-missing'));
22
+ .flatMap(p => placementsOf(p)
23
+ .filter(({ reference }) => !ids.has(idOf(reference)))
24
+ .map(({ relation }) => report(p.id, missingReference[relation])));
25
+ const selfPlaced = perforations
26
+ .filter(p => placementsOf(p).some(({ reference }) => idOf(reference) === p.id))
27
+ .map(p => report(p.id, 'placed-relative-to-itself'));
28
+ const placedSeveralWays = perforations
29
+ .filter(p => placementsOf(p).length > 1)
30
+ .map(p => report(p.id, 'placed-several-ways'));
20
31
  const missingPartners = perforations
21
32
  .filter(p => p.pairedWith && !ids.has(idOf(p.pairedWith)))
22
33
  .map(p => report(p.id, 'partner-missing'));
@@ -28,17 +39,21 @@ const problemsIn = (version, perforations) => {
28
39
  const inSeveralPairs = perforations
29
40
  .filter(p => pairsOf(p).length > 1)
30
41
  .map(p => report(p.id, 'in-several-pairs'));
31
- const alignedOnBothSides = pairs
32
- .filter(([one, other]) => one.alignedWith && other.alignedWith)
33
- .flatMap(pair => pair.map(p => report(p.id, 'pair-aligned-on-both-sides')));
34
- return [...missingReferences, ...missingPartners, ...selfPaired, ...inSeveralPairs, ...alignedOnBothSides];
42
+ const placedOnBothSides = pairs
43
+ .filter(([one, other]) => placementsOf(one).length > 0 && placementsOf(other).length > 0)
44
+ .flatMap(pair => pair.map(p => report(p.id, 'pair-placed-on-both-sides')));
45
+ return [
46
+ ...missingReferences, ...selfPlaced, ...placedSeveralWays,
47
+ ...missingPartners, ...selfPaired, ...inSeveralPairs, ...placedOnBothSides
48
+ ];
35
49
  };
36
50
  /**
37
- * Where the alignments and pairings of the edition cannot hold as
51
+ * Where the placements and pairings of the edition cannot hold as
38
52
  * stated, version by version: a reference or partner absent from the
39
- * version, a perforation claimed by several pairs, or a pair whose
40
- * members are both aligned and so cannot keep their distance and
41
- * follow their references at once.
53
+ * version, a perforation placed relative to itself or in several ways
54
+ * at once, one claimed by several pairs, or a pair whose members are
55
+ * both placed and so cannot keep their distance and follow their
56
+ * references at once.
42
57
  */
43
58
  export const constraintProblems = (view) => view.edition.versions.flatMap(version => problemsIn(version.id, view.snapshot(version.id).filter(isPerforation)));
44
59
  const isExpression = (symbol) => symbol.type === 'expression';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "linked-rolls",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
4
4
  "description": "Digital editions of piano rolls: import, collation, editorial assumptions, JSON-LD export, and emulation through a reproducing system",
5
5
  "license": "MIT",
6
6
  "repository": {