linked-rolls 0.46.0 → 0.48.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 CHANGED
@@ -143,17 +143,50 @@ and the onset alone 39, so neither test is idle, but a difference at the
143
143
  end is as often a punch measured badly as a punch genuinely prolonged,
144
144
  and the two are not worth confusing.
145
145
 
146
+ ## What a reader adds to a hole
147
+
148
+ A copy read pneumatically rather than scanned reports how long a valve
149
+ stayed open, and a valve is held on past the perforation that opened it,
150
+ so its holes run longer than the punched slots while their onsets agree.
151
+ Left in, that is not a difference between copies: a collation comparing
152
+ such a copy's hole ends against a scanned copy's compares an on-time
153
+ with a slot, and reads the one as a lengthening of the other.
154
+
155
+ `shortenHoles` takes the extension off and records it on the copy,
156
+ `revertShortening` puts it back, and `readFromPhillipsEroll` will take
157
+ it off at import. Only holes are touched, what a writing spans being no
158
+ valve. The figure is measured against the other copies rather than
159
+ derived from the bar, since Phillips gives only the difference between
160
+ two bars and not the absolute.
161
+
162
+ On welte225.org that figure is about 1.6 mm, and its shape is simple. It
163
+ does not grow with the length of the perforation: the ratio of the read
164
+ length to the true one falls from 1.34 on the shortest punches to 1.01
165
+ on the longest, which is what a constant added to everything does. There
166
+ is no shortest on-time the valve cannot fall below. It shows no run
167
+ along the roll, though one roll's span of paper speed is too narrow to
168
+ tell a constant in millimetres from a constant in time. What it does
169
+ vary with is the port that read it, by about half a millimetre either
170
+ way and with no gradient across the bar, which is measured but too
171
+ coarse to model from a single roll.
172
+
146
173
  ## Collating a derivation a second time
147
174
 
148
175
  A collated symbol is one symbol carrying every copy that reads it, so a
149
176
  collation cannot simply be run again: there is no second symbol left to
150
- match. `separateReadings` is the inverse. It takes one copy's carriers
151
- back off the symbols it shares, gives them to new symbols of the
177
+ match. `separateReadings` is the inverse. It takes the carriers of one
178
+ side back off the symbols it shares, gives them to new symbols of the
152
179
  version's own, and states the exchange, leaving the edition in the state
153
180
  a collation would have produced had nothing joined. `connectVersions` at
154
181
  a new tolerance then re-collates, joining again whatever the new window
155
182
  admits.
156
183
 
184
+ It takes a side and not a copy, the same side `sidesOf` names and a
185
+ window is measured over. Naming one copy of a side that has several does
186
+ not separate that side: the rest of it stays behind on the other side's
187
+ symbol, and a collation then compares one copy against that mixture
188
+ rather than the two texts against each other.
189
+
157
190
  Nothing an editor established is lost by that. `connectVersions`
158
191
  rewrites only what a collation wrote (`isCollationsOwn`): a bare
159
192
  insertion or deletion, or an equivalence between two systems' spellings.
@@ -85,6 +85,27 @@ export type Collation = {
85
85
  };
86
86
  /** Each of the own symbols with every inherited symbol it collates with, both in the order given. */
87
87
  export declare const collationsOf: (own: readonly Readonly<AnySymbol>[], inherited: readonly Readonly<AnySymbol>[], locate: Locate, tolerance?: CollationTolerance) => Collation[];
88
+ /**
89
+ * The motivation a collation writes on what it makes, saying that
90
+ * nobody has read it yet.
91
+ *
92
+ * A collation produces its edits by a rule, and an edition that
93
+ * publishes them unmarked would present a machine's output as an
94
+ * editor's reading. Stating that they are unchecked is the honest
95
+ * middle: the readings are there to be seen and argued with, and a
96
+ * reader can tell which of them anybody has weighed.
97
+ *
98
+ * It does not make an edit the editor's. `isCollationsOwn` passes over
99
+ * this motivation exactly, so a collation rewrites what it marked
100
+ * before, and only what somebody has actually written stays.
101
+ */
102
+ export declare const unchecked = "unchecked";
103
+ /** The version's statement of what the unchecked motivation means, to stand in its `motivations`. */
104
+ export declare const uncheckedMotivation: {
105
+ readonly type: 'motivation';
106
+ readonly id: "unchecked";
107
+ readonly note: 'Made by collating the two texts and not yet read by an editor.';
108
+ };
88
109
  /**
89
110
  * Whether the edit is one a collation writes by itself: a bare
90
111
  * insertion or a bare deletion saying nothing further, or an
package/lib/Collation.js CHANGED
@@ -89,6 +89,27 @@ export const collationsOf = (own, inherited, locate, tolerance = defaultCollatio
89
89
  };
90
90
  /** The edit type a collation draws by itself, from the two systems' vocabularies rather than off the paper. */
91
91
  const drawnByCollation = 'replace-with-equivalent';
92
+ /**
93
+ * The motivation a collation writes on what it makes, saying that
94
+ * nobody has read it yet.
95
+ *
96
+ * A collation produces its edits by a rule, and an edition that
97
+ * publishes them unmarked would present a machine's output as an
98
+ * editor's reading. Stating that they are unchecked is the honest
99
+ * middle: the readings are there to be seen and argued with, and a
100
+ * reader can tell which of them anybody has weighed.
101
+ *
102
+ * It does not make an edit the editor's. `isCollationsOwn` passes over
103
+ * this motivation exactly, so a collation rewrites what it marked
104
+ * before, and only what somebody has actually written stays.
105
+ */
106
+ export const unchecked = 'unchecked';
107
+ /** The version's statement of what the unchecked motivation means, to stand in its `motivations`. */
108
+ export const uncheckedMotivation = {
109
+ type: 'motivation',
110
+ id: unchecked,
111
+ note: 'Made by collating the two texts and not yet read by an editor.'
112
+ };
92
113
  /**
93
114
  * Whether the edit is one a collation writes by itself: a bare
94
115
  * insertion or a bare deletion saying nothing further, or an
@@ -107,4 +128,6 @@ const drawnByCollation = 'replace-with-equivalent';
107
128
  * draws a second time over the very same symbols.
108
129
  */
109
130
  export const isCollationsOwn = (edit) => edit.editType === drawnByCollation
110
- || (edit.editType === undefined && edit.motivation === undefined && edit['@annotation'] === undefined);
131
+ || (edit.editType === undefined
132
+ && (edit.motivation === undefined || edit.motivation === unchecked)
133
+ && edit['@annotation'] === undefined);
package/lib/RollCopy.d.ts CHANGED
@@ -201,7 +201,7 @@ export interface RollCopy extends WithType<'RollCopy'>, WithId {
201
201
  * (e.g. 'shifted', 'stretched') to normalize measurements
202
202
  * for comparison with other copies. Not exported to RDF.
203
203
  */
204
- ops: Array<'shifted' | 'stretched'>;
204
+ ops: Array<'shifted' | 'stretched' | 'shortened'>;
205
205
  /**
206
206
  * Physical measurements of this roll copy, including
207
207
  * dimensions, punch diameter, hole separation, margins,
@@ -258,6 +258,17 @@ export interface RollCopy extends WithType<'RollCopy'>, WithId {
258
258
  * Not exported to RDF.
259
259
  */
260
260
  scale: number;
261
+ /**
262
+ * How much longer this copy's reading of a hole ran than the
263
+ * perforation that caused it, where that was taken off the ends
264
+ * again. A pneumatic reader reports how long a valve stayed
265
+ * open, which exceeds the perforation that opened it, so its
266
+ * holes are overlong by a constant while its onsets agree.
267
+ * Nothing for a copy read by other means, whose holes are the
268
+ * punched slots themselves.
269
+ * Not exported to RDF.
270
+ */
271
+ readerExtension: Millimeters;
261
272
  /**
262
273
  * The resolution this copy's scan was read at, along the roll.
263
274
  * It is what the measurements given in pixels are to be read
@@ -9,6 +9,45 @@ export declare const applyScale: (factor: number, copy: RollCopy) => void;
9
9
  export declare const revertShift: (copy: RollCopy) => void;
10
10
  /** Takes the scale off the copy's features again, as far as one was applied. */
11
11
  export declare const revertScale: (copy: RollCopy) => void;
12
+ /**
13
+ * Takes the extension a pneumatic reader adds off the ends of the
14
+ * copy's holes, and records how much was taken.
15
+ *
16
+ * Such a reader reports how long a valve stayed open, and a valve is
17
+ * held on past the perforation that opened it, so its holes run longer
18
+ * than the punched slots while their onsets agree. Left in, the
19
+ * difference is not a difference between copies at all: a collation
20
+ * comparing this copy's hole ends against a scanned copy's compares a
21
+ * pneumatic on-time with a punched slot, and reads the one as a
22
+ * lengthening of the other.
23
+ *
24
+ * The extension is a property of the reader rather than of the roll,
25
+ * and on the material measured so far it is a constant: it does not
26
+ * grow with the length of the perforation, there is no shortest
27
+ * on-time the valve cannot fall below, and it does not run along the
28
+ * roll with the paper speed as far as one roll can show. What it does
29
+ * vary with is the port that read it, by about half a millimetre either
30
+ * way and without any gradient across the bar, which is measured but
31
+ * too coarse to model from one roll.
32
+ *
33
+ * Only holes are touched. What a writing or a mark spans is no valve.
34
+ */
35
+ /**
36
+ * The holes the extension cannot be taken off, being no longer than it
37
+ * is: the reader reported them open for less time than it holds a valve
38
+ * on beyond the perforation, so the constant over-corrects them and
39
+ * would leave them ending before they begin.
40
+ *
41
+ * They are where the constant shows its limit rather than where the
42
+ * copy is wrong, the extension varying by about half a millimetre with
43
+ * the port. What to do with them is an editorial question — a condition
44
+ * on the feature, a reading of the hole, a smaller extension — and
45
+ * `shortenHoles` throws rather than answer it.
46
+ */
47
+ export declare const tooShortToShorten: (extension: Millimeters, copy: RollCopy) => Readonly<FeatureOrPatch>[];
48
+ export declare const shortenHoles: (extension: Millimeters, copy: RollCopy) => void;
49
+ /** Puts the reader's extension back on the copy's holes, as far as one was taken off. */
50
+ export declare const revertShortening: (copy: RollCopy) => void;
12
51
  /**
13
52
  * How a copy's places are carried onto another copy's:
14
53
  * `x_other = (x + shift) · scale`.
package/lib/alignment.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { featuresOf } from "./RollCopy.js";
2
2
  import { welteT100 } from "./systems/welteT100/bar.js";
3
- import { add, mm, scale } from "./Quantity.js";
3
+ import { add, mm, scale, subtract } from "./Quantity.js";
4
4
  /** Moves both ends of a span, the far end only where the span has one. */
5
5
  const move = (span, by) => {
6
6
  span.from = add(span.from, by);
@@ -54,6 +54,65 @@ export const revertScale = (copy) => {
54
54
  copy.ops = copy.ops.filter(op => op !== 'stretched');
55
55
  delete copy.measurements.scale;
56
56
  };
57
+ const holesOf = (copy) => featuresOf(copy).filter(feature => feature.type === 'Hole');
58
+ /**
59
+ * Takes the extension a pneumatic reader adds off the ends of the
60
+ * copy's holes, and records how much was taken.
61
+ *
62
+ * Such a reader reports how long a valve stayed open, and a valve is
63
+ * held on past the perforation that opened it, so its holes run longer
64
+ * than the punched slots while their onsets agree. Left in, the
65
+ * difference is not a difference between copies at all: a collation
66
+ * comparing this copy's hole ends against a scanned copy's compares a
67
+ * pneumatic on-time with a punched slot, and reads the one as a
68
+ * lengthening of the other.
69
+ *
70
+ * The extension is a property of the reader rather than of the roll,
71
+ * and on the material measured so far it is a constant: it does not
72
+ * grow with the length of the perforation, there is no shortest
73
+ * on-time the valve cannot fall below, and it does not run along the
74
+ * roll with the paper speed as far as one roll can show. What it does
75
+ * vary with is the port that read it, by about half a millimetre either
76
+ * way and without any gradient across the bar, which is measured but
77
+ * too coarse to model from one roll.
78
+ *
79
+ * Only holes are touched. What a writing or a mark spans is no valve.
80
+ */
81
+ /**
82
+ * The holes the extension cannot be taken off, being no longer than it
83
+ * is: the reader reported them open for less time than it holds a valve
84
+ * on beyond the perforation, so the constant over-corrects them and
85
+ * would leave them ending before they begin.
86
+ *
87
+ * They are where the constant shows its limit rather than where the
88
+ * copy is wrong, the extension varying by about half a millimetre with
89
+ * the port. What to do with them is an editorial question — a condition
90
+ * on the feature, a reading of the hole, a smaller extension — and
91
+ * `shortenHoles` throws rather than answer it.
92
+ */
93
+ export const tooShortToShorten = (extension, copy) => holesOf(copy).filter(hole => hole.horizontal.to - hole.horizontal.from <= extension);
94
+ export const shortenHoles = (extension, copy) => {
95
+ if (copy.ops.includes('shortened'))
96
+ return;
97
+ const tooShort = tooShortToShorten(extension, copy);
98
+ if (tooShort.length > 0) {
99
+ throw new Error(`The extension of ${extension} mm cannot be taken off ${tooShort.length} `
100
+ + `hole(s) of copy ${copy.id}, which are no longer than it: `
101
+ + `${tooShort.map(hole => hole.id).join(', ')}`);
102
+ }
103
+ holesOf(copy).forEach(hole => { hole.horizontal.to = subtract(hole.horizontal.to, extension); });
104
+ copy.ops = [...copy.ops, 'shortened'];
105
+ copy.measurements.readerExtension = extension;
106
+ };
107
+ /** Puts the reader's extension back on the copy's holes, as far as one was taken off. */
108
+ export const revertShortening = (copy) => {
109
+ const extension = copy.measurements.readerExtension;
110
+ if (!copy.ops.includes('shortened') || extension === undefined)
111
+ return;
112
+ holesOf(copy).forEach(hole => { hole.horizontal.to = add(hole.horizontal.to, extension); });
113
+ copy.ops = copy.ops.filter(op => op !== 'shortened');
114
+ delete copy.measurements.readerExtension;
115
+ };
57
116
  const byPlace = (x, y) => x.at - y.at || x.pitch - y.pitch;
58
117
  /** The notes the bar reads off the features, in the order they pass it. */
59
118
  const noteOnsets = (features, bar) => features
@@ -8,6 +8,7 @@ import { GeneralRollCondition, ModificationPurpose, RollCopy, ScaleReading, Shif
8
8
  import { FeatureSource } from "./FeatureSource.js";
9
9
  import { AnyArgumentation, Belief, Certainty, ObjectAssumption } from "./Assumption.js";
10
10
  import { FeatureConditionAssignment, FeatureOrPatch, NestedFeature } from "./Feature.js";
11
+ import { Millimeters } from "./Quantity.js";
11
12
  /**
12
13
  * A change to an edition, written onto an immer draft of it. One
13
14
  * operation is one undo step, so an operation that has to read the
@@ -40,6 +41,14 @@ export declare const alignCopy: (copyId: string, shift: Shift, scale: number, re
40
41
  * being a fact about the copy.
41
42
  */
42
43
  export declare const unalignCopy: (copyId: string) => EditionOp;
44
+ /**
45
+ * Takes the extension a pneumatic reader adds off the ends of the
46
+ * copy's holes, and records how much was taken, so that its lengths
47
+ * can be compared with a scanned copy's at all.
48
+ */
49
+ export declare const shortenCopy: (copyId: string, extension: Millimeters) => EditionOp;
50
+ /** Puts the reader's extension back on the copy's holes. */
51
+ export declare const unshortenCopy: (copyId: string) => EditionOp;
43
52
  /** States what the copy's features were read from, in place of any earlier statement. */
44
53
  export declare const stateSource: (copyId: string, source: FeatureSource) => EditionOp;
45
54
  /** Takes back the statement, leaving the copy silent about its source again. */
@@ -180,9 +189,9 @@ export declare const connectVersions: (view: EditionView, childId: string, paren
180
189
  */
181
190
  export declare const collateSymbols: (view: EditionView, versionId: string, symbolIds: readonly string[], tolerance?: CollationTolerance) => EditionOp;
182
191
  /**
183
- * Takes the copy's reading of a symbol back out of the symbol it was
184
- * collated into: the copy's carriers pass to a new symbol of the
185
- * version's own, the other copies keep the symbol they had, and the
192
+ * Takes the named copies' reading of a symbol back out of the symbol it
193
+ * was collated into: their carriers pass to a new symbol of the
194
+ * version's own, the remaining copies keep the symbol they had, and the
186
195
  * version states the exchange.
187
196
  *
188
197
  * This is the inverse of the hand-over a collation makes, and it is
@@ -191,12 +200,19 @@ export declare const collateSymbols: (view: EditionView, versionId: string, symb
191
200
  * takes the two readings apart again, and without that a tolerance
192
201
  * arrived at after the fact could never be applied.
193
202
  *
194
- * Symbols the copy alone carries are already the version's own and are
195
- * passed over, so a reading somebody has separated by hand keeps its
196
- * identifier and the edit that speaks for it. Named symbols narrow the
197
- * act to those; naming none separates the copy's whole reading.
203
+ * The copies named are one **side** of the derivation, the same side a
204
+ * window is measured over, and `sidesOf` is what says which they are.
205
+ * Naming one copy of a side that has several does not separate that
206
+ * side: what stays behind is the rest of it, mixed with the other
207
+ * side's copies, and a collation then compares one copy against that
208
+ * mixture rather than the two texts against each other.
209
+ *
210
+ * Symbols the named copies alone carry are already the version's own
211
+ * and are passed over, so a reading somebody has separated by hand
212
+ * keeps its identifier and the edit that speaks for it. Named symbols
213
+ * narrow the act to those; naming none separates the whole reading.
198
214
  */
199
- export declare const separateReadings: (view: EditionView, versionId: string, copyId: string, symbolIds?: readonly string[]) => EditionOp;
215
+ export declare const separateReadings: (view: EditionView, versionId: string, copies: ReadonlySet<string>, symbolIds?: readonly string[]) => EditionOp;
200
216
  /**
201
217
  * Makes the version stand on its own: what it inherited becomes its
202
218
  * own insertions, and its derivations go, the hypotheses among them,
package/lib/editionOps.js CHANGED
@@ -2,13 +2,13 @@ import { current, isDraft } from "immer";
2
2
  import { v4 } from "uuid";
3
3
  import { getAt } from "./EditionView.js";
4
4
  import { isCommand, placementRelations } from "./Symbol.js";
5
- import { collationsOf, defaultCollationTolerance, isCollationsOwn } from "./Collation.js";
5
+ import { collationsOf, defaultCollationTolerance, isCollationsOwn, unchecked, uncheckedMotivation } from "./Collation.js";
6
6
  import { collationToleranceOf, editsOf, insertedBy, principalDerivationOf } from "./Version.js";
7
7
  import { asSymbols, barOf, featuresByAct, featuresOf, isPaperStretch, statesNothing } from "./RollCopy.js";
8
8
  import { systemOf } from "./TrackerBar.js";
9
9
  import { substitutionsBetween } from "./substitution.js";
10
10
  import { trackerBarOf } from "./systems/index.js";
11
- import { applyShift, applyScale, revertShift, revertScale } from "./alignment.js";
11
+ import { applyShift, applyScale, revertShift, revertScale, revertShortening, shortenHoles } from "./alignment.js";
12
12
  import { assignReference, idOf } from "./Assumption.js";
13
13
  import { conditions as conditionsAllowed, featuresBorneBy, isGluedOn, withBorneFeatures } from "./Feature.js";
14
14
  import { distance, mm, subtract } from "./Quantity.js";
@@ -54,6 +54,17 @@ const droppedIds = (before, after) => {
54
54
  };
55
55
  const insertion = (symbol) => ({ type: 'edit', id: v4(), insert: [symbol] });
56
56
  const deletion = (symbolId) => ({ type: 'edit', id: v4(), delete: [symbolId] });
57
+ /** The edit as a collation leaves it: made by rule, and not yet read by anybody. */
58
+ const asUnchecked = (edit) => ({ ...edit, motivation: unchecked });
59
+ /**
60
+ * The motivations with the unchecked one declared, where any edit
61
+ * names it and the version does not state it yet. An edit naming a
62
+ * motivation the version leaves undeclared points at nothing.
63
+ */
64
+ const declaring = (motivations, edits) => edits.some(edit => edit.motivation === unchecked)
65
+ && !motivations.some(motivation => motivation.id === unchecked)
66
+ ? [...motivations, uncheckedMotivation]
67
+ : motivations;
57
68
  const isEmpty = (edit) => !edit.insert?.length && !edit.delete?.length;
58
69
  const insertedIn = (versions) => versions.flatMap(insertedBy);
59
70
  /** The edits with the change applied, less those it emptied; the very same array where it changed none. */
@@ -125,6 +136,14 @@ export const unalignCopy = (copyId) => onCopy(copyId, copy => {
125
136
  revertShift(copy);
126
137
  copy.conditions = without(copy.conditions, isPaperStretch);
127
138
  });
139
+ /**
140
+ * Takes the extension a pneumatic reader adds off the ends of the
141
+ * copy's holes, and records how much was taken, so that its lengths
142
+ * can be compared with a scanned copy's at all.
143
+ */
144
+ export const shortenCopy = (copyId, extension) => onCopy(copyId, copy => shortenHoles(extension, copy));
145
+ /** Puts the reader's extension back on the copy's holes. */
146
+ export const unshortenCopy = (copyId) => onCopy(copyId, copy => revertShortening(copy));
128
147
  /** States what the copy's features were read from, in place of any earlier statement. */
129
148
  export const stateSource = (copyId, source) => onCopy(copyId, copy => {
130
149
  copy.readFrom = source;
@@ -725,6 +744,7 @@ export const connectVersions = (view, childId, parentId, tolerance = defaultColl
725
744
  type: 'edit',
726
745
  id: v4(),
727
746
  editType: 'replace-with-equivalent',
747
+ motivation: unchecked,
728
748
  insert: [...by],
729
749
  delete: deleted
730
750
  };
@@ -732,14 +752,15 @@ export const connectVersions = (view, childId, parentId, tolerance = defaultColl
732
752
  const edits = [
733
753
  ...established,
734
754
  ...substituted.map(equivalence),
735
- ...own.filter(symbol => !collated.has(symbol.id) && !paired.has(symbol.id)).map(insertion),
755
+ ...own.filter(symbol => !collated.has(symbol.id) && !paired.has(symbol.id)).map(insertion).map(asUnchecked),
736
756
  ...inherited
737
757
  .filter(symbol => !matched.has(symbol.id) && !paired.has(symbol.id))
738
- .map(symbol => deletion(symbol.id))
758
+ .map(symbol => asUnchecked(deletion(symbol.id)))
739
759
  ];
740
760
  return onVersion(childId, (child, draft) => {
741
761
  handOverCarriers(view, draft, collations);
742
762
  child.edits = edits;
763
+ child.motivations = declaring(stateOf(child).motivations, edits);
743
764
  child.basedOn = [
744
765
  { ...assignReference(parentId), collationTolerance: tolerance },
745
766
  ...hypothesesBeside(stateOf(child), parentId)
@@ -778,16 +799,29 @@ const readingOf = (symbol, carriers) => {
778
799
  }
779
800
  };
780
801
  /** The symbol where the copy and at least one other carry it, with the carriers of each side. */
781
- const sharedWith = (view, symbol, copyId) => {
782
- const onCopy = (carrier) => view.copyOf(idOf(carrier))?.id === copyId;
783
- const mine = symbol.carriers.filter(onCopy);
784
- const theirs = symbol.carriers.filter(carrier => !onCopy(carrier));
802
+ /**
803
+ * The symbol where the named copies and at least one other carry it,
804
+ * with the carriers of each side.
805
+ *
806
+ * A symbol the named copies alone carry has no other side to be
807
+ * separated from and is passed over. That covers the insertions, and it
808
+ * covers a symbol an ancestor holds on one branch's witness alone,
809
+ * where the two versions do not disagree and there is nothing to take
810
+ * apart.
811
+ */
812
+ const sharedWith = (view, symbol, copies) => {
813
+ const onNamedCopy = (carrier) => {
814
+ const copy = view.copyOf(idOf(carrier));
815
+ return copy !== undefined && copies.has(copy.id);
816
+ };
817
+ const mine = symbol.carriers.filter(onNamedCopy);
818
+ const theirs = symbol.carriers.filter(carrier => !onNamedCopy(carrier));
785
819
  return mine.length > 0 && theirs.length > 0 ? [{ symbol, mine, theirs }] : [];
786
820
  };
787
821
  /**
788
- * Takes the copy's reading of a symbol back out of the symbol it was
789
- * collated into: the copy's carriers pass to a new symbol of the
790
- * version's own, the other copies keep the symbol they had, and the
822
+ * Takes the named copies' reading of a symbol back out of the symbol it
823
+ * was collated into: their carriers pass to a new symbol of the
824
+ * version's own, the remaining copies keep the symbol they had, and the
791
825
  * version states the exchange.
792
826
  *
793
827
  * This is the inverse of the hand-over a collation makes, and it is
@@ -796,19 +830,26 @@ const sharedWith = (view, symbol, copyId) => {
796
830
  * takes the two readings apart again, and without that a tolerance
797
831
  * arrived at after the fact could never be applied.
798
832
  *
799
- * Symbols the copy alone carries are already the version's own and are
800
- * passed over, so a reading somebody has separated by hand keeps its
801
- * identifier and the edit that speaks for it. Named symbols narrow the
802
- * act to those; naming none separates the copy's whole reading.
833
+ * The copies named are one **side** of the derivation, the same side a
834
+ * window is measured over, and `sidesOf` is what says which they are.
835
+ * Naming one copy of a side that has several does not separate that
836
+ * side: what stays behind is the rest of it, mixed with the other
837
+ * side's copies, and a collation then compares one copy against that
838
+ * mixture rather than the two texts against each other.
839
+ *
840
+ * Symbols the named copies alone carry are already the version's own
841
+ * and are passed over, so a reading somebody has separated by hand
842
+ * keeps its identifier and the edit that speaks for it. Named symbols
843
+ * narrow the act to those; naming none separates the whole reading.
803
844
  */
804
- export const separateReadings = (view, versionId, copyId, symbolIds) => {
845
+ export const separateReadings = (view, versionId, copies, symbolIds) => {
805
846
  const version = view.get(versionId);
806
847
  if (!version)
807
848
  return noChange;
808
849
  const chosen = symbolIds && new Set(symbolIds);
809
850
  const shared = view.snapshot(versionId)
810
851
  .filter(symbol => chosen === undefined || chosen.has(symbol.id))
811
- .flatMap(symbol => sharedWith(view, symbol, copyId));
852
+ .flatMap(symbol => sharedWith(view, symbol, copies));
812
853
  if (shared.length === 0)
813
854
  return noChange;
814
855
  const inserted = new Set(insertedBy(version).map(symbol => symbol.id));
@@ -20,16 +20,21 @@ import { Millimeters, Seconds } from "../Quantity.js";
20
20
  * perforation, the height of the tracker bar hole and the paper speed
21
21
  * (p. 180).
22
22
  *
23
- * That extension is not subtracted. Doing so would need the absolute
24
- * hole height of the Welte bar, where he gives only the 0.5 mm by which
25
- * it exceeds the Licensee's. So the end of a hole on this copy is a
26
- * pneumatic on-time where the end of a scanned copy's hole is a punched
27
- * slot, and the two are not the same quantity. It shows in a collation:
28
- * across welte225.org his notes run about 1.8 mm longer than the other
29
- * copies read them, while the onsets agree. Until it is corrected, a
30
- * comparison of hole ends against this copy measures the reader as much
31
- * as the roll, and `offsetEnd` on the derivation absorbs the systematic
32
- * part of it.
23
+ * Left in, that extension is not a difference between copies at all:
24
+ * the end of a hole here is a pneumatic on-time where the end of a
25
+ * scanned copy's hole is a punched slot, so a collation comparing the
26
+ * two reads the one as a lengthening of the other. `extension` takes it
27
+ * off at the end of every hole, and the copy records what was taken.
28
+ *
29
+ * How much to take off is measured rather than derived, since he gives
30
+ * only the 0.5 mm by which the Welte bar's holes exceed the Licensee's
31
+ * and not the absolute. Across welte225.org it comes to about 1.6 mm,
32
+ * and the shape of it is simple: a constant, not growing with the
33
+ * length of the perforation, with no shortest on-time the valve cannot
34
+ * fall below, and with no run along the roll that one roll's span of
35
+ * paper speed could show. The port that read it matters by about half a
36
+ * millimetre either way, without any gradient across the bar, which is
37
+ * measured but too coarse to model from one roll.
33
38
  */
34
39
  /**
35
40
  * He numbers his files by two rules at once, which his free samples
@@ -64,12 +69,25 @@ export interface PhillipsErollOptions {
64
69
  * account for it.
65
70
  */
66
71
  placeAt?: (elapsed: Seconds) => Millimeters;
72
+ /**
73
+ * How much longer a valve is held on than the perforation that
74
+ * opened it, taken off the end of every hole so that these lengths
75
+ * can be compared with a scanned copy's. Nothing leaves the holes
76
+ * as the reader reported them, which is what every copy read before
77
+ * this option existed states.
78
+ *
79
+ * The copy records what was taken, and `revertShortening` puts it
80
+ * back, so a figure revised later needs no second import. On
81
+ * welte225.org the extension measures about 1.6 mm, with no
82
+ * dependence on how long the perforation is.
83
+ */
84
+ extension?: Millimeters;
67
85
  }
68
86
  /**
69
87
  * Reads one of his e-roll files as a copy of the roll, in millimetres
70
88
  * of paper, on the bar it was cut for and in that bar's numbering. A
71
89
  * position the bar does not read is left out, as the bar would leave it.
72
90
  */
73
- export declare function readFromPhillipsEroll(buffer: ArrayBuffer, { system, controlOffset, placeAt }?: PhillipsErollOptions): RollCopy;
91
+ export declare function readFromPhillipsEroll(buffer: ArrayBuffer, { system, controlOffset, placeAt, extension }?: PhillipsErollOptions): RollCopy;
74
92
  /** The bars his files are known to be numbered by. */
75
93
  export declare const phillipsSystems: readonly TrackerBar[];
@@ -2,6 +2,7 @@ import { read } from "midifile-ts";
2
2
  import { v4 } from "uuid";
3
3
  import { assignObject } from "../Assumption.js";
4
4
  import { systemOf } from "../TrackerBar.js";
5
+ import { shortenHoles } from "../alignment.js";
5
6
  import { welteT100 } from "../systems/welteT100/bar.js";
6
7
  import { welteLicensee } from "../systems/welteLicensee/bar.js";
7
8
  import { inMetersPerMinute, mm, seconds, track } from "../Quantity.js";
@@ -24,16 +25,21 @@ import { inMetersPerMinute, mm, seconds, track } from "../Quantity.js";
24
25
  * perforation, the height of the tracker bar hole and the paper speed
25
26
  * (p. 180).
26
27
  *
27
- * That extension is not subtracted. Doing so would need the absolute
28
- * hole height of the Welte bar, where he gives only the 0.5 mm by which
29
- * it exceeds the Licensee's. So the end of a hole on this copy is a
30
- * pneumatic on-time where the end of a scanned copy's hole is a punched
31
- * slot, and the two are not the same quantity. It shows in a collation:
32
- * across welte225.org his notes run about 1.8 mm longer than the other
33
- * copies read them, while the onsets agree. Until it is corrected, a
34
- * comparison of hole ends against this copy measures the reader as much
35
- * as the roll, and `offsetEnd` on the derivation absorbs the systematic
36
- * part of it.
28
+ * Left in, that extension is not a difference between copies at all:
29
+ * the end of a hole here is a pneumatic on-time where the end of a
30
+ * scanned copy's hole is a punched slot, so a collation comparing the
31
+ * two reads the one as a lengthening of the other. `extension` takes it
32
+ * off at the end of every hole, and the copy records what was taken.
33
+ *
34
+ * How much to take off is measured rather than derived, since he gives
35
+ * only the 0.5 mm by which the Welte bar's holes exceed the Licensee's
36
+ * and not the absolute. Across welte225.org it comes to about 1.6 mm,
37
+ * and the shape of it is simple: a constant, not growing with the
38
+ * length of the perforation, with no shortest on-time the valve cannot
39
+ * fall below, and with no run along the roll that one roll's span of
40
+ * paper speed could show. The port that read it matters by about half a
41
+ * millimetre either way, without any gradient across the bar, which is
42
+ * measured but too coarse to model from one roll.
37
43
  */
38
44
  /**
39
45
  * He numbers his files by two rules at once, which his free samples
@@ -134,7 +140,7 @@ const positionsByPitch = (bar) => new Map(Array.from({ length: bar.trackCount },
134
140
  * of paper, on the bar it was cut for and in that bar's numbering. A
135
141
  * position the bar does not read is left out, as the bar would leave it.
136
142
  */
137
- export function readFromPhillipsEroll(buffer, { system = welteT100, controlOffset, placeAt } = {}) {
143
+ export function readFromPhillipsEroll(buffer, { system = welteT100, controlOffset, placeAt, extension } = {}) {
138
144
  const file = read(new Uint8Array(buffer));
139
145
  const timed = onOneClock(file.tracks);
140
146
  const elapsedAt = clockOf(temposIn(timed), file.header.ticksPerBeat);
@@ -157,7 +163,7 @@ export function readFromPhillipsEroll(buffer, { system = welteT100, controlOffse
157
163
  }
158
164
  }];
159
165
  });
160
- return {
166
+ const copy = {
161
167
  type: 'RollCopy',
162
168
  id: v4(),
163
169
  ops: [],
@@ -171,6 +177,9 @@ export function readFromPhillipsEroll(buffer, { system = welteT100, controlOffse
171
177
  note: 'Read on Phillips’s pneumatic roll reader. The places are elapsed time put back onto the paper, and a hole runs as long as its switch stayed open, which is longer than the perforation.'
172
178
  }
173
179
  };
180
+ if (extension !== undefined)
181
+ shortenHoles(extension, copy);
182
+ return copy;
174
183
  }
175
184
  /** The bars his files are known to be numbered by. */
176
185
  export const phillipsSystems = [welteT100, welteLicensee];
package/lib/schema.json CHANGED
@@ -2828,6 +2828,10 @@
2828
2828
  "description": "The average diameter of punched holes.",
2829
2829
  "ontology": "reo:punchDiameter"
2830
2830
  },
2831
+ "readerExtension": {
2832
+ "$ref": "#/definitions/Millimeters",
2833
+ "description": "How much longer this copy's reading of a hole ran than the perforation that caused it, where that was taken off the ends again. A pneumatic reader reports how long a valve stayed open, which exceeds the perforation that opened it, so its holes are overlong by a constant while its onsets agree. Nothing for a copy read by other means, whose holes are the punched slots themselves. Not exported to RDF."
2834
+ },
2831
2835
  "scale": {
2832
2836
  "description": "The factor this copy's features were scaled by to align them with the others. What it is put down to is stated apart: a paper-stretch condition, or the speed the copy was cut for. Not exported to RDF.",
2833
2837
  "type": "number"
@@ -2861,7 +2865,8 @@
2861
2865
  "items": {
2862
2866
  "enum": [
2863
2867
  "shifted",
2864
- "stretched"
2868
+ "stretched",
2869
+ "shortened"
2865
2870
  ],
2866
2871
  "type": "string"
2867
2872
  },
@@ -151,6 +151,7 @@
151
151
  "@context": {
152
152
  "shift": null,
153
153
  "scale": null,
154
+ "readerExtension": null,
154
155
  "trackCalibration": null,
155
156
  "margins": null,
156
157
  "dimensions": "reo:dimensions",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "linked-rolls",
3
- "version": "0.46.0",
3
+ "version": "0.48.0",
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": {