@idle-screens/schema 3.9.0 → 3.9.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/dist/index.d.ts CHANGED
@@ -843,6 +843,13 @@ interface IdleSequence {
843
843
  }
844
844
 
845
845
  declare function validateSpec(spec: unknown): ValidationResult;
846
+ /**
847
+ * Dot-paths (numeric layer indices, as `steerablePaths` spells them) of every
848
+ * property the validator flags as unknown or misplaced — i.e. fields the
849
+ * renderer never reads, so steering them does nothing. Pure: unlike
850
+ * `validateSpec` it never normalises colours into the caller's spec.
851
+ */
852
+ declare function ignoredPropertyPaths(spec: unknown): string[];
846
853
  /** Narrowing helper: validate + cast. Throws with a joined message when invalid. */
847
854
  declare function assertValidSpec(spec: unknown): SaverSpec;
848
855
  /**
@@ -1235,6 +1242,12 @@ declare function morphNothingMorphable(a: SaverSpec, b: SaverSpec, opts?: {
1235
1242
  * (id, label, schemaVersion, seed, units, kind, type, key) are excluded —
1236
1243
  * they describe structure, not tuneable values.
1237
1244
  *
1245
+ * Properties the validator flags as unknown or misplaced (e.g. `angle` on a
1246
+ * gradient background, `blend` inside a sprite) are excluded too: the
1247
+ * renderer never reads them, so advertising them invites a steer that
1248
+ * silently does nothing. Same source of truth as validateSpec's
1249
+ * "will be ignored" warnings (`ignoredPropertyPaths`).
1250
+ *
1238
1251
  * Layer keys are NOT substituted: paths always use numeric indices.
1239
1252
  * Consumers can map to key-based paths via resolveSpecPath if needed.
1240
1253
  */
@@ -1919,10 +1932,14 @@ declare function sequenceWantsFinish(seq: IdleSequence): boolean;
1919
1932
  * when present;
1920
1933
  * - the same top-level `seq.seed`, compared normalized (like the mount-time
1921
1934
  * seed itself) so a change of an exact multiple of 2^32 isn't flagged —
1922
- * even when every segment and the bed resolve their own seed (so
1923
- * `segmentRenderSeed`/`bedRenderSeed` are unaffected), a sequence-level
1924
- * `finish` grain resolves against the instance's mount-time seed
1925
- * (`frameFinishSeed`), which a swap cannot update;
1935
+ * but ONLY when either side has a sequence-level `finish`: that's the one
1936
+ * place `seq.seed` reaches rendering on its own (`frameFinishSeed` uses the
1937
+ * instance's mount-time seed for a sequence-level finish's grain, which a
1938
+ * swap cannot update). Without a sequence-level `finish`, every consumer of
1939
+ * `seq.seed` is a `segmentRenderSeed`/`bedRenderSeed` fallback, and those
1940
+ * are already compared per segment and for the bed below — so when every
1941
+ * segment and the bed carry their own seed, `seq.seed` is dead weight and
1942
+ * changing it is free;
1926
1943
  * - the same finish presence (`sequenceWantsFinish`) — the presentation
1927
1944
  * pass is allocated once at construction, so a swap that adds or removes
1928
1945
  * every `finish` in the sequence cannot (de)allocate it;
@@ -2263,8 +2280,12 @@ declare const MURMUR_SPEC: SaverSpec;
2263
2280
  * hex value glides over 2500 ms, and because the words differ and the
2264
2281
  * transition declares `text: 'crossfade'`, the outgoing caption fades under
2265
2282
  * the incoming one instead of switching whole-frame. Without the crossfade
2266
- * declaration a morph steps strings on its first frame — it reads as a cut
2267
- * and the validator says `morph-nothing-morphable`.
2283
+ * declaration the morph would still glide — colour alone gives it a
2284
+ * `hasGlide` — just with the caption stepping on the first frame instead of
2285
+ * fading; the validator only says `morph-nothing-morphable` for a pair
2286
+ * whose differences are all values morph cannot glide — strings
2287
+ * (`textBlock.text` above all), booleans, mismatched arrays — which this
2288
+ * one isn't.
2268
2289
  * - **fade.** Segments 2 → 3 have nothing structurally in common, so a morph
2269
2290
  * is impossible and a `fade` is the honest transition: the outgoing scene
2270
2291
  * keeps animating on its own canvas for 3000 ms while the incoming one
@@ -2313,4 +2334,4 @@ declare const EXAMPLE_SPECS: SaverSpec[];
2313
2334
  /** Lookup by saver id. */
2314
2335
  declare const EXAMPLE_BY_ID: Readonly<Record<string, SaverSpec>>;
2315
2336
 
2316
- export { AQUARIUM_SPEC, AURORA_SPEC, type BackgroundDrift, type BackgroundSpec, type BandSpec, COHESION_T, COMETS_SPEC, CONSTELLATION_SPEC, type CapabilityTier, type CohesionRead, type CycleSpec, DASHBOARD_SPEC, DITHER_ALPHA, DITHER_TILE, type DominanceEntry, EXAMPLE_BY_ID, EXAMPLE_SEQUENCES, EXAMPLE_SPECS, type Entity, FACETS_SPEC, FIELD_BUCKET_MS, FIELD_DRIFT_DEFAULT_AMOUNT, FIELD_RASTER_SHORT_SIDE, FIELD_RASTER_SHORT_SIDE_LOW, FINISH_ANIMATE_HZ, type FieldBackground, type FieldDrift, type FinishPass, type FinishSpec, GRAIN_ALPHA, GRAIN_TILE, type GradientStop, HAIKU_SPEC, type IdleSequence, LANTERNS_SPEC, LIMITS, LOBBY_TALK_SPEC, type LayerCohesion, type LayerMotionStats, type LayerSnapshot, type LayerSpec, type LinkEdge, type LuminanceGrid, type LuminanceGridOptions, MATRIX_RAIN_SPEC, MURMUR_SPEC, type MotionEase, type MotionSpec, NOSTALGHIA_CANDLE_SPEC, ORRERY_SPEC, PHASE_DUET_SPEC, PINGS_SPEC, POLYGONS_SPEC, PROCESSION_SPEC, type PerceiveOptions, type PerceiveSequenceOptions, type Placed, RAIN_SPEC, RELAY_BOARD_SPEC, type ResolveOptions, type ResolvedSegment, type RevealState, SAKURA_SPEC, SCHEMA_EXAMPLES, SCHEMA_VERSION, SHARD_FALL_SPEC, SIGNAL_BOARD_SPEC, SNOWFALL_SPEC, SPARSE_NIGHT_SPEC, type SaverSpec, type SceneDescription, type SceneDiff, type ScenePerception, type SceneSnapshot, type SchemaExample, type ScreenOp, type SequenceExample, type SequenceFramePerception, type SequenceMountContext, type SequenceSaverInstance, type SequenceSaverPlugin, type SequenceSegment, type SequenceTransition, type SpecError, type SpecMountContext, type SpecWarning, type SpriteSpec, type SteerDelta, THERMAL_FIELD_SPEC, THREE_MOVEMENTS_SEQUENCE, TYPED_CUE_SPEC, type TextBlockAnchor, type TextBlockLine, type TextMetricsClass, type TextRevealInput, type TextRevealSpec, type TextRole, type TextSpriteInfo, type ValidationResult, WARP_MAX_SCALE, WARP_NEAR, WARP_TUNNEL_SPEC, WEB_WORK_SPEC, type WanderOsc, type WarningBox, adviseSequence, adviseSpec, alphaAt, applyDeltasToSpec, assertValidSequence, assertValidSpec, bayerMatrix, bayerTilePixels, bedRenderSeed, breakTextBlock, buildEntities, canMorph, cohesionOf, compileSaver, compileSequence, createFinishPass, createSceneCanvas, describeScene, diffScenes, dominanceRanking, easeSmooth, emitEnvelope, emitWindow, fieldAt, fieldOctaves, fieldRgb, fieldRgbAt, fieldSampleTime, finishStrength, grainOffset, grainTilePixels, graphemeClusters, hasHotSwapSequence, headingAt, hexToRgb255, latticeHash, layerCohesion, lerpSpec, lifeAlphaAt, linkEdges, linkPairs, luminanceGrid, manifestFor, morphChainRoot, morphNothingMorphable, motionStats, normalizeSeed, perceiveScene, perceiveSequenceFrame, pickScreenOp, positionAt, presentWithFinish, renderBrailleMap, renderDensityMap, resolveSegment, resolveSpecPath, revealState, rgb255Luma, rotationAt, seamCauseOf, seamsWorthWarning, segmentRenderSeed, segmentStart, sequenceSteerablePaths, sequenceSwapCompatible, sequenceWantsFinish, sizeAt, spriteIndexAt, spriteVariants, steerablePaths, structuralSignature, textBlockAnchorOffset, textMetricsClassFor, textSprites, textStringsDiffer, textStringsOf, textWidthEm, validateSequence, validateSpec };
2337
+ export { AQUARIUM_SPEC, AURORA_SPEC, type BackgroundDrift, type BackgroundSpec, type BandSpec, COHESION_T, COMETS_SPEC, CONSTELLATION_SPEC, type CapabilityTier, type CohesionRead, type CycleSpec, DASHBOARD_SPEC, DITHER_ALPHA, DITHER_TILE, type DominanceEntry, EXAMPLE_BY_ID, EXAMPLE_SEQUENCES, EXAMPLE_SPECS, type Entity, FACETS_SPEC, FIELD_BUCKET_MS, FIELD_DRIFT_DEFAULT_AMOUNT, FIELD_RASTER_SHORT_SIDE, FIELD_RASTER_SHORT_SIDE_LOW, FINISH_ANIMATE_HZ, type FieldBackground, type FieldDrift, type FinishPass, type FinishSpec, GRAIN_ALPHA, GRAIN_TILE, type GradientStop, HAIKU_SPEC, type IdleSequence, LANTERNS_SPEC, LIMITS, LOBBY_TALK_SPEC, type LayerCohesion, type LayerMotionStats, type LayerSnapshot, type LayerSpec, type LinkEdge, type LuminanceGrid, type LuminanceGridOptions, MATRIX_RAIN_SPEC, MURMUR_SPEC, type MotionEase, type MotionSpec, NOSTALGHIA_CANDLE_SPEC, ORRERY_SPEC, PHASE_DUET_SPEC, PINGS_SPEC, POLYGONS_SPEC, PROCESSION_SPEC, type PerceiveOptions, type PerceiveSequenceOptions, type Placed, RAIN_SPEC, RELAY_BOARD_SPEC, type ResolveOptions, type ResolvedSegment, type RevealState, SAKURA_SPEC, SCHEMA_EXAMPLES, SCHEMA_VERSION, SHARD_FALL_SPEC, SIGNAL_BOARD_SPEC, SNOWFALL_SPEC, SPARSE_NIGHT_SPEC, type SaverSpec, type SceneDescription, type SceneDiff, type ScenePerception, type SceneSnapshot, type SchemaExample, type ScreenOp, type SequenceExample, type SequenceFramePerception, type SequenceMountContext, type SequenceSaverInstance, type SequenceSaverPlugin, type SequenceSegment, type SequenceTransition, type SpecError, type SpecMountContext, type SpecWarning, type SpriteSpec, type SteerDelta, THERMAL_FIELD_SPEC, THREE_MOVEMENTS_SEQUENCE, TYPED_CUE_SPEC, type TextBlockAnchor, type TextBlockLine, type TextMetricsClass, type TextRevealInput, type TextRevealSpec, type TextRole, type TextSpriteInfo, type ValidationResult, WARP_MAX_SCALE, WARP_NEAR, WARP_TUNNEL_SPEC, WEB_WORK_SPEC, type WanderOsc, type WarningBox, adviseSequence, adviseSpec, alphaAt, applyDeltasToSpec, assertValidSequence, assertValidSpec, bayerMatrix, bayerTilePixels, bedRenderSeed, breakTextBlock, buildEntities, canMorph, cohesionOf, compileSaver, compileSequence, createFinishPass, createSceneCanvas, describeScene, diffScenes, dominanceRanking, easeSmooth, emitEnvelope, emitWindow, fieldAt, fieldOctaves, fieldRgb, fieldRgbAt, fieldSampleTime, finishStrength, grainOffset, grainTilePixels, graphemeClusters, hasHotSwapSequence, headingAt, hexToRgb255, ignoredPropertyPaths, latticeHash, layerCohesion, lerpSpec, lifeAlphaAt, linkEdges, linkPairs, luminanceGrid, manifestFor, morphChainRoot, morphNothingMorphable, motionStats, normalizeSeed, perceiveScene, perceiveSequenceFrame, pickScreenOp, positionAt, presentWithFinish, renderBrailleMap, renderDensityMap, resolveSegment, resolveSpecPath, revealState, rgb255Luma, rotationAt, seamCauseOf, seamsWorthWarning, segmentRenderSeed, segmentStart, sequenceSteerablePaths, sequenceSwapCompatible, sequenceWantsFinish, sizeAt, spriteIndexAt, spriteVariants, steerablePaths, structuralSignature, textBlockAnchorOffset, textMetricsClassFor, textSprites, textStringsDiffer, textStringsOf, textWidthEm, validateSequence, validateSpec };
package/dist/index.js CHANGED
@@ -246,7 +246,9 @@ function steerablePaths(spec) {
246
246
  if (prefix) out.push(prefix);
247
247
  };
248
248
  walk(spec, "", "");
249
- return out;
249
+ const ignored = ignoredPropertyPaths(spec);
250
+ if (ignored.length === 0) return out;
251
+ return out.filter((p) => !ignored.some((ig) => p === ig || p.startsWith(`${ig}.`)));
250
252
  }
251
253
  function sequenceSteerablePaths(seq) {
252
254
  const out = /* @__PURE__ */ new Set(["sequence.segment"]);
@@ -361,6 +363,16 @@ var LAYER_PROPS_ON_SPRITE = /* @__PURE__ */ new Set(["blend", "trail", "alpha",
361
363
  function unknownKeys(obj, known) {
362
364
  return Object.keys(obj).filter((k) => !known.has(k));
363
365
  }
366
+ function dimUnit(spec) {
367
+ return isObj(spec) && spec.units === "px" ? "px" : "fractions of min(width, height)";
368
+ }
369
+ function rangeMsg(spec, note) {
370
+ return `must be a [min,max] range of positive ${dimUnit(spec)}${note ? ` (${note})` : ""}`;
371
+ }
372
+ function positiveScalar(v, path, err, msg = "must be > 0") {
373
+ if (Array.isArray(v)) err(path, "must be a single number > 0, not a [min,max] range");
374
+ else if (!isNum(v) || v <= 0) err(path, msg);
375
+ }
364
376
  function normalizeColors(spec) {
365
377
  if (!isObj(spec) || !Array.isArray(spec.layers)) return;
366
378
  for (const layer of spec.layers) {
@@ -375,6 +387,13 @@ function normalizeColors(spec) {
375
387
  }
376
388
  function validateSpec(spec) {
377
389
  normalizeColors(spec);
390
+ return validateSpecCore(spec);
391
+ }
392
+ function ignoredPropertyPaths(spec) {
393
+ if (!isObj(spec)) return [];
394
+ return (validateSpecCore(spec).warnings ?? []).filter((w) => w.code === "unknown-property" || w.code === "misplaced-property").map((w) => w.path.replace(/\[(\d+)\]/g, ".$1"));
395
+ }
396
+ function validateSpecCore(spec) {
378
397
  const errors = [];
379
398
  const warnings = [];
380
399
  const err = (path, message) => void errors.push({ path, message });
@@ -471,7 +490,7 @@ function validateBackground(bg, err, warn) {
471
490
  if (!isObj(bg.band)) err("background.band", "must be an object");
472
491
  else {
473
492
  color(bg.band.color, "background.band.color", err);
474
- if (!isNum(bg.band.height) || bg.band.height <= 0) err("background.band.height", "must be > 0");
493
+ positiveScalar(bg.band.height, "background.band.height", err);
475
494
  }
476
495
  }
477
496
  for (const k of unknownKeys(bg, KNOWN_BG_GRADIENT)) {
@@ -537,7 +556,7 @@ function validateLayer(layer, path, err, warn, spec) {
537
556
  err(`${path}.count`, `at most ${LIMITS.maxPerLayer} per layer`);
538
557
  }
539
558
  if (layer.size !== void 0 && (!isRange(layer.size) || layer.size[0] <= 0)) {
540
- err(`${path}.size`, "must be a [min,max] range of positive px");
559
+ err(`${path}.size`, rangeMsg(spec));
541
560
  }
542
561
  if (layer.wrap !== void 0 && typeof layer.wrap !== "boolean") err(`${path}.wrap`, "must be a boolean");
543
562
  if (layer.flip !== void 0 && typeof layer.flip !== "boolean") err(`${path}.flip`, "must be a boolean");
@@ -634,16 +653,12 @@ function validateLayer(layer, path, err, warn, spec) {
634
653
  if (!isNum(layer.links.k) || !Number.isInteger(layer.links.k) || layer.links.k < 1 || layer.links.k > LIMITS.maxLinksK) {
635
654
  err(`${path}.links.k`, `must be an integer 1..${LIMITS.maxLinksK}`);
636
655
  }
637
- if (!isNum(layer.links.maxDist) || layer.links.maxDist <= 0) {
638
- err(`${path}.links.maxDist`, "must be > 0");
639
- }
656
+ positiveScalar(layer.links.maxDist, `${path}.links.maxDist`, err);
640
657
  if (layer.links.color !== void 0) color(layer.links.color, `${path}.links.color`, err);
641
658
  if (layer.links.alpha !== void 0 && (!isNum(layer.links.alpha) || layer.links.alpha < 0 || layer.links.alpha > 1)) {
642
659
  err(`${path}.links.alpha`, "must be 0..1");
643
660
  }
644
- if (layer.links.width !== void 0 && (!isNum(layer.links.width) || layer.links.width <= 0)) {
645
- err(`${path}.links.width`, "must be > 0");
646
- }
661
+ if (layer.links.width !== void 0) positiveScalar(layer.links.width, `${path}.links.width`, err);
647
662
  if (layer.links.mode !== void 0 && !["nearest", "chain", "random"].includes(layer.links.mode)) {
648
663
  err(`${path}.links.mode`, "must be 'nearest' | 'chain' | 'random'");
649
664
  }
@@ -797,10 +812,10 @@ function validateLayer(layer, path, err, warn, spec) {
797
812
  }
798
813
  }
799
814
  }
800
- validateSprite(layer.sprite, `${path}.sprite`, err, warn);
815
+ validateSprite(layer.sprite, `${path}.sprite`, err, warn, spec);
801
816
  validateMotion(layer.motion, `${path}.motion`, err, warn, spec);
802
817
  }
803
- function validateSprite(sprite, path, err, warn) {
818
+ function validateSprite(sprite, path, err, warn, spec) {
804
819
  if (!isObj(sprite)) return err(path, "must be an object");
805
820
  let knownSet;
806
821
  if (sprite.kind === "emoji") {
@@ -824,32 +839,30 @@ function validateSprite(sprite, path, err, warn) {
824
839
  if (sprite.baseline !== void 0 && !["top", "middle", "bottom"].includes(sprite.baseline)) {
825
840
  err(`${path}.baseline`, "must be top | middle | bottom");
826
841
  }
827
- if (sprite.maxWidth !== void 0 && (!isNum(sprite.maxWidth) || sprite.maxWidth <= 0)) {
828
- err(`${path}.maxWidth`, "must be a positive number");
829
- }
842
+ if (sprite.maxWidth !== void 0) positiveScalar(sprite.maxWidth, `${path}.maxWidth`, err, "must be a positive number");
830
843
  validateTextRole(sprite, path, err);
831
844
  validateCycle(sprite, path, err);
832
845
  } else if (sprite.kind === "circle") {
833
846
  knownSet = KNOWN_CIRCLE;
834
- if (!isRange(sprite.radius) || sprite.radius[0] <= 0) err(`${path}.radius`, "must be a [min,max] range of positive px");
847
+ if (!isRange(sprite.radius) || sprite.radius[0] <= 0) err(`${path}.radius`, rangeMsg(spec));
835
848
  color(sprite.color, `${path}.color`, err);
836
849
  if (sprite.soft !== void 0 && typeof sprite.soft !== "boolean") err(`${path}.soft`, "must be a boolean");
837
850
  validatePalette(sprite, path, err);
838
851
  } else if (sprite.kind === "ring") {
839
852
  knownSet = KNOWN_RING;
840
- if (!isRange(sprite.radius) || sprite.radius[0] <= 0) err(`${path}.radius`, "must be a [min,max] range of positive px");
853
+ if (!isRange(sprite.radius) || sprite.radius[0] <= 0) err(`${path}.radius`, rangeMsg(spec));
841
854
  color(sprite.color, `${path}.color`, err);
842
- if (sprite.width !== void 0 && (!isNum(sprite.width) || sprite.width <= 0)) err(`${path}.width`, "must be > 0");
855
+ if (sprite.width !== void 0) positiveScalar(sprite.width, `${path}.width`, err);
843
856
  validatePalette(sprite, path, err);
844
857
  } else if (sprite.kind === "streak") {
845
858
  knownSet = KNOWN_STREAK;
846
- if (!isRange(sprite.length) || sprite.length[0] <= 0) err(`${path}.length`, "must be a [min,max] range of positive px");
859
+ if (!isRange(sprite.length) || sprite.length[0] <= 0) err(`${path}.length`, rangeMsg(spec));
847
860
  color(sprite.color, `${path}.color`, err);
848
- if (sprite.width !== void 0 && (!isNum(sprite.width) || sprite.width <= 0)) err(`${path}.width`, "must be > 0");
861
+ if (sprite.width !== void 0) positiveScalar(sprite.width, `${path}.width`, err);
849
862
  validatePalette(sprite, path, err);
850
863
  } else if (sprite.kind === "rect") {
851
864
  knownSet = KNOWN_RECT;
852
- if (!isRange(sprite.width) || sprite.width[0] <= 0) err(`${path}.width`, "must be a [min,max] range of positive px");
865
+ if (!isRange(sprite.width) || sprite.width[0] <= 0) err(`${path}.width`, rangeMsg(spec));
853
866
  if (sprite.aspect !== void 0 && (!isRange(sprite.aspect) || sprite.aspect[0] <= 0)) {
854
867
  err(`${path}.aspect`, "must be a [min,max] range of positive height/width ratios");
855
868
  }
@@ -863,9 +876,9 @@ function validateSprite(sprite, path, err, warn) {
863
876
  if (!Array.isArray(sprite.values) || sprite.values.length === 0 || sprite.values.length > LIMITS.maxPerLayer || !sprite.values.every((v) => isNum(v) && v >= 0)) {
864
877
  err(`${path}.values`, `must be 1..${LIMITS.maxPerLayer} numbers >= 0`);
865
878
  }
866
- if (!isNum(sprite.length) || sprite.length <= 0) err(`${path}.length`, "must be > 0 (full-scale bar length)");
867
- if (!isNum(sprite.thickness) || sprite.thickness <= 0) err(`${path}.thickness`, "must be > 0");
868
- if (sprite.max !== void 0 && (!isNum(sprite.max) || sprite.max <= 0)) err(`${path}.max`, "must be > 0");
879
+ positiveScalar(sprite.length, `${path}.length`, err, "must be > 0 (full-scale bar length)");
880
+ positiveScalar(sprite.thickness, `${path}.thickness`, err);
881
+ if (sprite.max !== void 0) positiveScalar(sprite.max, `${path}.max`, err);
869
882
  if (sprite.direction !== void 0 && !["right", "left", "up", "down"].includes(sprite.direction)) {
870
883
  err(`${path}.direction`, "must be 'right' | 'left' | 'up' | 'down'");
871
884
  }
@@ -873,7 +886,7 @@ function validateSprite(sprite, path, err, warn) {
873
886
  validatePalette(sprite, path, err);
874
887
  } else if (sprite.kind === "polygon") {
875
888
  knownSet = KNOWN_POLYGON;
876
- if (!isRange(sprite.radius) || sprite.radius[0] <= 0) err(`${path}.radius`, "must be a [min,max] range of positive px (circumradius)");
889
+ if (!isRange(sprite.radius) || sprite.radius[0] <= 0) err(`${path}.radius`, rangeMsg(spec, "circumradius"));
877
890
  if (sprite.sides !== void 0 && sprite.points !== void 0) {
878
891
  err(`${path}.sides`, "use sides (a regular polygon) or points (a custom one), not both");
879
892
  }
@@ -886,9 +899,9 @@ function validateSprite(sprite, path, err, warn) {
886
899
  validatePalette(sprite, path, err);
887
900
  } else if (sprite.kind === "stroke") {
888
901
  knownSet = KNOWN_STROKE;
889
- if (!isRange(sprite.length) || sprite.length[0] <= 0) err(`${path}.length`, "must be a [min,max] range of positive px (the mark's bounding size)");
902
+ if (!isRange(sprite.length) || sprite.length[0] <= 0) err(`${path}.length`, rangeMsg(spec, "the mark's bounding size"));
890
903
  validateShapePoints(sprite.points, `${path}.points`, 2, err);
891
- if (sprite.width !== void 0 && (!isNum(sprite.width) || sprite.width <= 0)) err(`${path}.width`, "must be > 0");
904
+ if (sprite.width !== void 0) positiveScalar(sprite.width, `${path}.width`, err);
892
905
  if (sprite.curve !== void 0 && sprite.curve !== "smooth" && sprite.curve !== "linear") err(`${path}.curve`, "must be 'smooth' | 'linear'");
893
906
  if (sprite.taper !== void 0 && typeof sprite.taper !== "boolean") err(`${path}.taper`, "must be a boolean");
894
907
  if (sprite.orient !== void 0 && typeof sprite.orient !== "boolean") err(`${path}.orient`, "must be a boolean");
@@ -901,10 +914,14 @@ function validateSprite(sprite, path, err, warn) {
901
914
  } else if (sprite.text.length > LIMITS.maxTextBlockLength) {
902
915
  err(`${path}.text`, `must be at most ${LIMITS.maxTextBlockLength} characters`);
903
916
  }
904
- if (!isNum(sprite.maxWidth) || sprite.maxWidth <= 0 || sprite.maxWidth > LIMITS.maxTextBlockMaxWidth) {
917
+ if (Array.isArray(sprite.maxWidth)) {
918
+ err(`${path}.maxWidth`, "must be a single number (viewport fraction), not a [min,max] range");
919
+ } else if (!isNum(sprite.maxWidth) || sprite.maxWidth <= 0 || sprite.maxWidth > LIMITS.maxTextBlockMaxWidth) {
905
920
  err(`${path}.maxWidth`, `must be a positive number up to ${LIMITS.maxTextBlockMaxWidth} (viewport fraction)`);
906
921
  }
907
- if (!isNum(sprite.fontSize) || sprite.fontSize < LIMITS.minTextBlockFontSize || sprite.fontSize > LIMITS.maxTextBlockFontSize) {
922
+ if (Array.isArray(sprite.fontSize)) {
923
+ err(`${path}.fontSize`, "must be a single number (viewport fraction), not a [min,max] range");
924
+ } else if (!isNum(sprite.fontSize) || sprite.fontSize < LIMITS.minTextBlockFontSize || sprite.fontSize > LIMITS.maxTextBlockFontSize) {
908
925
  err(`${path}.fontSize`, `must be between ${LIMITS.minTextBlockFontSize} and ${LIMITS.maxTextBlockFontSize} (viewport fraction)`);
909
926
  }
910
927
  if (sprite.lineHeight !== void 0 && (!isNum(sprite.lineHeight) || sprite.lineHeight < 0.5 || sprite.lineHeight > 4)) {
@@ -1052,7 +1069,7 @@ function validateMotion(motion, path, err, warn, spec) {
1052
1069
  warn(`${path}.speed`, "near-zero-speed", `orbit speed is near zero \u2014 entities will appear frozen. Typical range: 5\u201360 deg/sec`);
1053
1070
  }
1054
1071
  }
1055
- if (!isRange(motion.radius) || motion.radius[0] <= 0) err(`${path}.radius`, "must be a [min,max] range of positive px");
1072
+ if (!isRange(motion.radius) || motion.radius[0] <= 0) err(`${path}.radius`, rangeMsg(spec));
1056
1073
  if (motion.center !== void 0) {
1057
1074
  if (!isObj(motion.center)) {
1058
1075
  err(`${path}.center`, "must be {x, y} (0..1) or { layer: key }");
@@ -2480,8 +2497,10 @@ function sequenceWantsFinish(seq) {
2480
2497
  }
2481
2498
  function sequenceSwapCompatible(prev, next) {
2482
2499
  if (prev.segments.length !== next.segments.length) return false;
2483
- if (prev.seed === void 0 !== (next.seed === void 0)) return false;
2484
- if (prev.seed !== void 0 && next.seed !== void 0 && normalizeSeed(prev.seed) !== normalizeSeed(next.seed)) return false;
2500
+ if (prev.finish !== void 0 || next.finish !== void 0) {
2501
+ if (prev.seed === void 0 !== (next.seed === void 0)) return false;
2502
+ if (prev.seed !== void 0 && next.seed !== void 0 && normalizeSeed(prev.seed) !== normalizeSeed(next.seed)) return false;
2503
+ }
2485
2504
  if (sequenceWantsFinish(prev) !== sequenceWantsFinish(next)) return false;
2486
2505
  if (prev.loop !== next.loop) return false;
2487
2506
  if ((prev.sync ?? "mount") !== (next.sync ?? "mount")) return false;
@@ -4768,7 +4787,7 @@ var LINE_SALIENCE = 3.5;
4768
4787
  var GHOST_TAPS = 12;
4769
4788
  function buildScene(spec, opts) {
4770
4789
  const { width: w, height: h } = opts.viewport ?? { width: 1920, height: 1080 };
4771
- const seed = opts.seed ?? spec.seed ?? 42;
4790
+ const seed = normalizeSeed(opts.seed ?? spec.seed ?? 42);
4772
4791
  const scale = spec.units === "px" ? 1 : Math.min(w, h);
4773
4792
  const refVp = spec.referenceViewport ?? LIMITS.referenceViewport;
4774
4793
  let countScale = scale > 1 ? Math.min(w, h) / refVp : 1;
@@ -4780,7 +4799,7 @@ function buildScene(spec, opts) {
4780
4799
  const layers = spec.layers.map((layer) => ({ layer, entities: buildEntities(layer, rng, w, h, scale, countScale) }));
4781
4800
  const byKey = /* @__PURE__ */ new Map();
4782
4801
  for (const { layer, entities } of layers) if (layer.key) byKey.set(layer.key, entities);
4783
- return { w, h, scale, layers, byKey };
4802
+ return { w, h, scale, seed, layers, byKey };
4784
4803
  }
4785
4804
  function posOf(scene, e, t) {
4786
4805
  const p = positionAt(e, t, scene.w, scene.h);
@@ -4860,7 +4879,7 @@ function luminanceGrid(spec, opts = {}) {
4860
4879
  } else if (bg.type === "field") {
4861
4880
  bgCells = new Array(cols * rows);
4862
4881
  const short = Math.max(1, Math.min(w, h));
4863
- const seed = normalizeSeed(opts.seed ?? spec.seed ?? 42);
4882
+ const seed = scene.seed;
4864
4883
  const ft = fieldSampleTime(bg, t);
4865
4884
  for (let r = 0; r < rows; r++) {
4866
4885
  let rowSum = 0;
@@ -6268,7 +6287,10 @@ var SIGNAL_BOARD_SPEC = {
6268
6287
  kind: "bar",
6269
6288
  values: [62, 38, 84, 21, 55, 91, 44, 70, 29],
6270
6289
  max: 100,
6271
- length: 0.24,
6290
+ // Cell pitch is the table's authored `gap` (0.07), so a full-value
6291
+ // bar must stay under that or neighboring cells' bars overlap.
6292
+ // 0.06 leaves a hairline margin.
6293
+ length: 0.06,
6272
6294
  thickness: 0.013,
6273
6295
  color: "#ffb000",
6274
6296
  direction: "right"
@@ -7976,6 +7998,7 @@ export {
7976
7998
  hasHotSwapSequence,
7977
7999
  headingAt,
7978
8000
  hexToRgb255,
8001
+ ignoredPropertyPaths,
7979
8002
  latticeHash,
7980
8003
  layerCohesion,
7981
8004
  lerpSpec,