linked-rolls 0.16.0 → 0.17.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/lib/Feature.d.ts CHANGED
@@ -173,7 +173,7 @@ export interface GluedOn extends RollFeature<'GluedOn', typeof conditions.GluedO
173
173
  * Nested features do not need to be positioned explicitly.
174
174
  * @see crm:P56 bears feature
175
175
  */
176
- features?: PartialBy<AnyFeature, 'horizontal' | 'vertical'>[];
176
+ features?: NestedFeature[];
177
177
  }
178
178
  /**
179
179
  * The union of all physical feature types that can appear on a roll:
@@ -181,4 +181,14 @@ export interface GluedOn extends RollFeature<'GluedOn', typeof conditions.GluedO
181
181
  * and glued-on patches (paper, tape).
182
182
  */
183
183
  export type AnyFeature = Hole | Writing | Mark | GluedOn;
184
+ /**
185
+ * A feature borne by another feature. It states no place of its own,
186
+ * the feature bearing it standing in one.
187
+ */
188
+ export type NestedFeature = PartialBy<AnyFeature, 'horizontal' | 'vertical'>;
184
189
  export declare const isRollFeature: (obj: object) => obj is AnyFeature;
190
+ export declare const isGluedOn: <T extends NestedFeature>(feature: T) => feature is T & GluedOn;
191
+ /** The features a feature bears: a patch those stated as parts of it, any other feature none. */
192
+ export declare const featuresBorneBy: (feature: NestedFeature) => NestedFeature[];
193
+ /** The feature together with everything it bears, as deep as a patch on a patch goes. */
194
+ export declare const withBorneFeatures: (feature: NestedFeature) => NestedFeature[];
package/lib/Feature.js CHANGED
@@ -9,3 +9,8 @@ export const writingMethods = ['Print', 'Handwriting', 'Stamp'];
9
9
  export const isRollFeature = (obj) => {
10
10
  return 'type' in obj && featureTypes.includes(obj.type);
11
11
  };
12
+ export const isGluedOn = (feature) => feature.type === 'GluedOn';
13
+ /** The features a feature bears: a patch those stated as parts of it, any other feature none. */
14
+ export const featuresBorneBy = (feature) => isGluedOn(feature) ? feature.features ?? [] : [];
15
+ /** The feature together with everything it bears, as deep as a patch on a patch goes. */
16
+ export const withBorneFeatures = (feature) => [feature, ...featuresBorneBy(feature).flatMap(withBorneFeatures)];
@@ -50,7 +50,12 @@ export declare const addGeneralCondition: (copyId: string, condition: ObjectAssu
50
50
  export declare const stateFeatureCondition: (copyId: string, featureId: string, condition: FeatureConditionAssignment) => EditionOp;
51
51
  /** The symbols of the versions that no other copy carries. */
52
52
  export declare const symbolsCarriedOnlyBy: (edition: Edition, copyId: string) => AnySymbol[];
53
- /** Takes the features off the copy, and out of the versions with what only they carried. */
53
+ /**
54
+ * Takes the features off the copy, and out of the versions with what
55
+ * only they carried. A feature may be named wherever the copy bears it:
56
+ * a patch goes with everything glued onto it, and a feature of a patch
57
+ * may be taken back on its own, the patch staying where it is.
58
+ */
54
59
  export declare const removeFeatures: (copyId: string, featureIds: readonly string[]) => EditionOp;
55
60
  /**
56
61
  * Takes the copy out of the edition together with the symbols only it
@@ -78,6 +83,11 @@ export declare const mergeObstacle: (features: readonly AnyFeature[]) => MergeOb
78
83
  * whatever else named them, a modification or a comprehension, names
79
84
  * the merged one in their stead.
80
85
  *
86
+ * Only the features the copy bears itself are merged. A feature of a
87
+ * patch states no place along the roll of its own, so there is nothing
88
+ * for a merge to span, and an id naming one is passed over as an id the
89
+ * copy does not bear is.
90
+ *
81
91
  * Throws where the features cannot stand for one; `mergeObstacle`
82
92
  * says beforehand whether they can.
83
93
  */
package/lib/editionOps.js CHANGED
@@ -7,7 +7,7 @@ import { collationToleranceOf, insertedBy } from "./Version";
7
7
  import { asSymbols } from "./RollCopy";
8
8
  import { applyShift, applyScale, revertShift, revertScale } from "./alignment";
9
9
  import { assignReference, idOf } from "./Assumption";
10
- import { conditions as conditionsAllowed } from "./Feature";
10
+ import { conditions as conditionsAllowed, featuresBorneBy, isGluedOn, withBorneFeatures } from "./Feature";
11
11
  import { distance, mm, subtract } from "./Quantity";
12
12
  const noChange = () => undefined;
13
13
  const onCopy = (copyId, op) => draft => {
@@ -147,7 +147,8 @@ export const stateFeatureCondition = (copyId, featureId, condition) => onFeature
147
147
  }
148
148
  feature.condition = condition;
149
149
  });
150
- const featureIdsOf = (copy) => new Set(copy.features.map(feature => feature.id));
150
+ /** The ids of every feature the copy bears, those a patch bears among them. */
151
+ const featureIdsOf = (copy) => new Set(copy.features.flatMap(withBorneFeatures).map(feature => feature.id));
151
152
  /**
152
153
  * A symbol every carrier of which lies among the features loses its
153
154
  * evidence with them. A symbol without carriers, such as a label,
@@ -282,11 +283,33 @@ const forgetFeatures = (draft, features) => {
282
283
  version.edits = edits[i];
283
284
  });
284
285
  };
285
- /** Takes the features off the copy, and out of the versions with what only they carried. */
286
+ /**
287
+ * The ids of the features named and of everything they bear: a feature
288
+ * of a patch stands nowhere once the patch is gone.
289
+ */
290
+ const goneWith = (features, named) => features.flatMap(feature => named.has(feature.id)
291
+ ? withBorneFeatures(feature).map(borne => borne.id)
292
+ : goneWith(featuresBorneBy(feature), named));
293
+ /** The feature with the named ones gone from what it bears, or the very same one where it bears none of them. */
294
+ const withoutBorne = (feature, named) => {
295
+ if (!isGluedOn(feature) || !feature.features)
296
+ return feature;
297
+ const borne = withoutFeatures(feature.features, named);
298
+ return borne === feature.features ? feature : { ...feature, features: borne };
299
+ };
300
+ /** The features without those named, wherever they lie, and without whatever those bore. */
301
+ const withoutFeatures = (features, named) => mapped(without(features, feature => named.has(feature.id)), feature => withoutBorne(feature, named));
302
+ /**
303
+ * Takes the features off the copy, and out of the versions with what
304
+ * only they carried. A feature may be named wherever the copy bears it:
305
+ * a patch goes with everything glued onto it, and a feature of a patch
306
+ * may be taken back on its own, the patch staying where it is.
307
+ */
286
308
  export const removeFeatures = (copyId, featureIds) => onCopy(copyId, (copy, draft) => {
287
- const features = new Set(featureIds);
288
- copy.features = without(copy.features, feature => features.has(feature.id));
289
- forgetFeatures(draft, features);
309
+ const named = new Set(featureIds);
310
+ const features = stateOf(copy.features);
311
+ copy.features = withoutFeatures(features, named);
312
+ forgetFeatures(draft, new Set(goneWith(features, named)));
290
313
  });
291
314
  /**
292
315
  * Takes the copy out of the edition together with the symbols only it
@@ -311,10 +334,17 @@ const sayTheSame = (a, b) => {
311
334
  keys.delete('id');
312
335
  return [...keys].every(key => sayTheSame(a[key], b[key]));
313
336
  };
314
- /** What a feature states beyond its identity, its place along the roll, its depiction and its condition. */
337
+ /**
338
+ * What a feature states beyond its identity, its place along the roll,
339
+ * its depiction and its condition. What a patch bears counts by
340
+ * identity: two patches bear the same only where they bear the very
341
+ * same features, so that no merge takes a feature of a patch away.
342
+ */
315
343
  const nature = (feature) => {
316
344
  const { id, horizontal, depiction, condition, ...rest } = feature;
317
- return rest;
345
+ return isGluedOn(feature) && feature.features
346
+ ? { ...rest, features: feature.features.map(borne => borne.id) }
347
+ : rest;
318
348
  };
319
349
  const conditionsOf = (features) => features.flatMap(feature => feature.condition ? [feature.condition] : []);
320
350
  /**
@@ -393,20 +423,25 @@ const carryOver = (draft, replaced, mergedId) => {
393
423
  * whatever else named them, a modification or a comprehension, names
394
424
  * the merged one in their stead.
395
425
  *
426
+ * Only the features the copy bears itself are merged. A feature of a
427
+ * patch states no place along the roll of its own, so there is nothing
428
+ * for a merge to span, and an id naming one is passed over as an id the
429
+ * copy does not bear is.
430
+ *
396
431
  * Throws where the features cannot stand for one; `mergeObstacle`
397
432
  * says beforehand whether they can.
398
433
  */
399
434
  export const mergeFeatures = (copyId, featureIds) => onCopy(copyId, (copy, draft) => {
400
- const replaced = new Set(featureIds);
401
- const isReplaced = (feature) => replaced.has(feature.id);
435
+ const named = new Set(featureIds);
402
436
  const features = stateOf(copy.features);
403
- const toMerge = features.filter(isReplaced);
437
+ const toMerge = features.filter(feature => named.has(feature.id));
404
438
  const obstacle = mergeObstacle(toMerge);
405
439
  if (obstacle) {
406
440
  throw new Error(`The features of copy ${copyId} cannot be merged: ${obstacle}`);
407
441
  }
442
+ const replaced = new Set(toMerge.map(feature => feature.id));
408
443
  const merged = mergedFrom(toMerge);
409
- copy.features = standingFor(features, isReplaced, merged);
444
+ copy.features = standingFor(features, feature => replaced.has(feature.id), merged);
410
445
  carryOver(draft, replaced, merged.id);
411
446
  renameReferences(draft, ids => standingFor(ids, id => replaced.has(id), merged.id));
412
447
  });
package/lib/schema.json CHANGED
@@ -626,7 +626,7 @@
626
626
  "features": {
627
627
  "description": "A glued-on feature itself may carry other features. Nested features do not need to be positioned explicitly. [ontology: crm:P56 bears feature]",
628
628
  "items": {
629
- "$ref": "#/definitions/PartialBy%3CAnyFeature%2C(%22horizontal%22%7C%22vertical%22)%3E"
629
+ "$ref": "#/definitions/NestedFeature"
630
630
  },
631
631
  "type": "array"
632
632
  },
@@ -1049,6 +1049,10 @@
1049
1049
  ],
1050
1050
  "type": "object"
1051
1051
  },
1052
+ "NestedFeature": {
1053
+ "$ref": "#/definitions/PartialBy%3CAnyFeature%2C(%22horizontal%22%7C%22vertical%22)%3E",
1054
+ "description": "A feature borne by another feature. It states no place of its own, the feature bearing it standing in one."
1055
+ },
1052
1056
  "Note": {
1053
1057
  "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]",
1054
1058
  "properties": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "linked-rolls",
3
- "version": "0.16.0",
3
+ "version": "0.17.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": {