@call-me-sensei/toonlab 0.4.19 → 0.4.21

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.
Files changed (123) hide show
  1. package/NPM-LIBRARY.md +230 -0
  2. package/README.md +78 -36
  3. package/agents/PROMPTS.md +91 -0
  4. package/agents/README.md +9 -14
  5. package/agents/claude/CLAUDE.md +8 -0
  6. package/agents/codex/AGENTS.md +161 -0
  7. package/agents/cursor/toonlab.mdc +37 -0
  8. package/agents/references/mcp-asset-discovery.md +12 -9
  9. package/agents/references/runtime-entry-points.md +5 -4
  10. package/agents/skills/claude/asset-sourcing/SKILL.md +9 -6
  11. package/agents/skills/claude/rock-ground-shaders/SKILL.md +16 -0
  12. package/agents/skills/claude/vegetation-sky/SKILL.md +7 -4
  13. package/agents/skills/codex/asset-sourcing/SKILL.md +9 -6
  14. package/agents/skills/codex/rock-ground-shaders/SKILL.md +16 -0
  15. package/agents/skills/codex/vegetation-sky/SKILL.md +7 -4
  16. package/cli/toonlab.mjs +3 -2
  17. package/database/fal-patina.mjs +131 -0
  18. package/database/generation-service.mjs +108 -7
  19. package/database/migrations/0007_creation_revisions.sql +80 -0
  20. package/database/migrations/0008_creation_revision_hardening.sql +98 -0
  21. package/database/migrations/0009_creation_revision_delete_integrity.sql +11 -0
  22. package/database/migrations/0010_external_catalog_assets.sql +28 -0
  23. package/database/providers.mjs +195 -16
  24. package/database/repository.mjs +518 -70
  25. package/database/seeds/catalog/0003_2026-08-open-assets.sql +159086 -0
  26. package/database/seeds/catalog/0004_2026-08-c7-rocks.sql +39405 -0
  27. package/mcp/public-catalog.mjs +4 -52
  28. package/mcp/server.mjs +175 -12
  29. package/mcp/vite-plugin.mjs +78 -2
  30. package/package.json +32 -12
  31. package/scripts/generate-catalog-seed.mjs +34 -7
  32. package/src/asset-policy/catalogLicenses.js +64 -6
  33. package/src/assetlib/loadImported.js +15 -1
  34. package/src/catalog/officialCatalogAssetRuntime.js +13 -2
  35. package/src/catalog/officialCatalogLod.js +34 -1
  36. package/src/catalog/officialCatalogPlacement.js +29 -14
  37. package/src/character/animationRetarget.js +2 -0
  38. package/src/character/characterRig.js +6 -1
  39. package/src/character/characterRuntime.js +175 -7
  40. package/src/environment/environmentMaterialAdapter.js +30 -0
  41. package/src/environment/environmentPresets.js +90 -0
  42. package/src/environment/environmentSunShadowPass.js +42 -0
  43. package/src/environment/toonLabSurfaceLighting.js +108 -10
  44. package/src/environment/urbanPropMaterial.js +104 -2
  45. package/src/lighting/lightingSystem.js +65 -10
  46. package/src/renderer/index.js +7 -0
  47. package/src/renderer/styleComparison.js +985 -0
  48. package/src/rock-shader/index.js +5 -0
  49. package/src/rock-shader/rockGeometryDetail.js +701 -0
  50. package/src/rock-shader/rockMaterial.js +782 -39
  51. package/src/rock-shader/rockRegionRuntime.js +185 -0
  52. package/src/rock-shader/rockSemanticMaterialRuntime.js +337 -0
  53. package/src/rock-shader/rockShaderRuntime.js +354 -67
  54. package/src/rock-shader/rockShaderSettings.js +246 -16
  55. package/src/rock-shader/rockTangentIntegrity.js +136 -0
  56. package/src/rock-shader/rockTextureIntegrity.js +115 -0
  57. package/src/rockgen/index.js +1 -0
  58. package/src/rockgen/rockDocument.js +27 -3
  59. package/src/rockgen/surface/c7GeologySurface.js +372 -0
  60. package/src/shaders-tsl/water.js +8 -0
  61. package/src/sky/cloudShadow.js +25 -1
  62. package/src/sky/skySystem.js +9 -0
  63. package/src/styles/index.js +23 -0
  64. package/src/styles/neutralStylePresets.js +712 -0
  65. package/src/styles/sceneStyleRuntime.js +51 -4
  66. package/src/styles/styleAdapters.js +13 -2
  67. package/src/styles/styleBundle.js +1 -1
  68. package/src/texgen/evaluateTexture.js +30 -3
  69. package/src/texgen/textureGenerators.js +63 -31
  70. package/src/texgen/textureSettings.js +25 -8
  71. package/src/toon/toonSettings.js +96 -2
  72. package/src/vegetation/branchTree.js +327 -42
  73. package/src/vegetation/index.js +24 -2
  74. package/src/vegetation/scatter.js +372 -1
  75. package/src/vegetation/stylizedTree.js +312 -126
  76. package/src/vegetation/stylizedTreeFoliage.js +229 -7
  77. package/src/vegetation/treeSurfaceTextures.js +328 -0
  78. package/src/version.js +1 -1
  79. package/src/water/waterSettings.js +15 -7
  80. package/types/asset-policy/catalogLicenses.d.ts +2 -0
  81. package/types/asset-policy/index.d.ts +2 -0
  82. package/types/assetlib/loadImported.d.ts +3 -1
  83. package/types/catalog/officialCatalogLod.d.ts +63 -5
  84. package/types/character/characterRig.d.ts +59 -0
  85. package/types/character/characterRuntime.d.ts +1 -0
  86. package/types/environment/environmentMaterialAdapter.d.ts +3 -1
  87. package/types/environment/toonLabSurfaceLighting.d.ts +20 -3
  88. package/types/index.d.ts +89 -7
  89. package/types/lighting/lightingSystem.d.ts +7 -0
  90. package/types/react/index.d.ts +20 -0
  91. package/types/renderer/index.d.ts +1 -0
  92. package/types/renderer/styleComparison.d.ts +409 -0
  93. package/types/rock-shader/index.d.ts +5 -0
  94. package/types/rock-shader/rockGeometryDetail.d.ts +300 -0
  95. package/types/rock-shader/rockMaterial.d.ts +3 -0
  96. package/types/rock-shader/rockRegionRuntime.d.ts +86 -0
  97. package/types/rock-shader/rockSemanticMaterialRuntime.d.ts +29 -0
  98. package/types/rock-shader/rockShaderRuntime.d.ts +2 -0
  99. package/types/rock-shader/rockShaderSettings.d.ts +19 -5
  100. package/types/rock-shader/rockTangentIntegrity.d.ts +51 -0
  101. package/types/rock-shader/rockTextureIntegrity.d.ts +61 -0
  102. package/types/rockgen/index.d.ts +1 -0
  103. package/types/rockgen/rockDocument.d.ts +29 -1
  104. package/types/rockgen/surface/c7GeologySurface.d.ts +8 -0
  105. package/types/sky/cloudShadow.d.ts +1 -0
  106. package/types/styles/index.d.ts +1 -0
  107. package/types/styles/neutralStylePresets.d.ts +944 -0
  108. package/types/styles/sceneStyleRuntime.d.ts +20 -0
  109. package/types/vegetation/branchTree.d.ts +4 -0
  110. package/types/vegetation/index.d.ts +9 -7
  111. package/types/vegetation/scatter.d.ts +1 -0
  112. package/types/vegetation/stylizedTree.d.ts +1 -2
  113. package/types/vegetation/stylizedTreeFoliage.d.ts +1 -0
  114. package/types/version.d.ts +1 -1
  115. package/agents/references/geology-playbook.md +0 -118
  116. package/agents/skills/claude/karst-cliff-construction/SKILL.md +0 -258
  117. package/agents/skills/claude/outdoor-world/SKILL.md +0 -492
  118. package/agents/skills/claude/outdoor-world/references/coastal-landform-formation.md +0 -100
  119. package/agents/skills/codex/karst-cliff-construction/SKILL.md +0 -258
  120. package/agents/skills/codex/outdoor-world/SKILL.md +0 -492
  121. package/agents/skills/codex/outdoor-world/references/coastal-landform-formation.md +0 -100
  122. package/src/vegetation/legacyTreePresets.js +0 -120
  123. package/types/vegetation/legacyTreePresets.d.ts +0 -22
@@ -247,7 +247,12 @@ export function scatterGrassAround({
247
247
  * straight to StylizedTree for silhouette variation:
248
248
  *
249
249
  * for (const p of scatterForest({ center, radius: 80, spacing: 9, heightAt, mask })) {
250
- * const tree = new StylizedTree({ preset: 'call_me_sensei', seed: p.seed, size: 3.2 });
250
+ * const tree = new StylizedTree({
251
+ * ...galleryTreeRecipe.options,
252
+ * seed: p.seed,
253
+ * size: 3.2,
254
+ * vegetationShader: { preset: 'call_me_sensei' },
255
+ * });
251
256
  * tree.position.set(p.x, p.y, p.z);
252
257
  * scene.add(tree);
253
258
  * }
@@ -614,3 +619,369 @@ export function scatterOnSurface({
614
619
 
615
620
  return placements;
616
621
  }
622
+
623
+ // ---------------------------------------------------------------------------
624
+ // Curve-relative placement
625
+ // ---------------------------------------------------------------------------
626
+
627
+ function curvePoint(spine, t) {
628
+ const point = spine(t);
629
+ if (Array.isArray(point)) return { x: Number(point[0]), z: Number(point[1]) };
630
+ return { x: Number(point?.x), z: Number(point?.z) };
631
+ }
632
+
633
+ /**
634
+ * A placement frame defined by an authored curve instead of by world axes.
635
+ *
636
+ * Every boundary a natural scene actually has — a shoreline, a garden path, a
637
+ * pond margin, a wall, a hedge line, a tree line — is a curve. `scatterInRect`
638
+ * can only *reject* candidates against such a boundary through its boolean
639
+ * mask; it can never *distribute along* one, so a host either accepts a
640
+ * rectangular distribution with a curved hole punched in it, or hand-authors
641
+ * world coordinates that silently drift away from the curve they were meant to
642
+ * follow.
643
+ *
644
+ * A curve frame closes that gap. Positions are authored as `(along, offset)`:
645
+ * `along` is the spine's own parameter and `offset` is signed distance across
646
+ * it, positive toward the spine's left normal (the tangent rotated +90° about
647
+ * +Y). Both hand-placed tables and seeded scatter resolve through the same
648
+ * frame, so a scene authors its spine once and everything that references it
649
+ * stays parallel to it for the curve's whole length.
650
+ *
651
+ * ```js
652
+ * const margin = createCurveFrame({
653
+ * spine: (theta) => pondMarginPoint(theta),
654
+ * domain: [0, Math.PI * 2],
655
+ * });
656
+ * // hand placement: eight set stones just outside the water's edge
657
+ * const stones = SET_STONES.map(([along, offset]) => margin.toWorld({ along, offset }));
658
+ * // seeded scatter: pond-edge planting in a 0.4–1.8 m band behind them
659
+ * const reeds = margin.scatterAlong({ count: 140, offsetRange: [0.4, 1.8], seed: 91 });
660
+ * ```
661
+ *
662
+ * @param {Object} options
663
+ * @param {(t: number) => ({x: number, z: number}|[number, number])} options.spine
664
+ * The curve, sampled in its own parameter.
665
+ * @param {[number, number]} [options.domain] Parameter range, `[min, max]`.
666
+ * @param {number} [options.samples] Arc-length table resolution.
667
+ * @param {null|'x'|'z'} [options.offsetAxis] `null` (default) offsets along the
668
+ * curve normal, which is what keeps a band a constant width. Set `'z'` (or
669
+ * `'x'`) for a graph-style spine authored as `z = f(x)`, where the intent is
670
+ * a plain axis offset — the frame then reproduces `f(along) + offset`
671
+ * exactly rather than approximately.
672
+ * @param {boolean} [options.closed] Wrap the parameter instead of clamping.
673
+ * @returns {Object} frozen frame API
674
+ */
675
+ export function createCurveFrame({
676
+ spine,
677
+ domain = [0, 1],
678
+ samples = 256,
679
+ offsetAxis = null,
680
+ closed = false,
681
+ } = {}) {
682
+ if (typeof spine !== 'function') {
683
+ throw new TypeError('createCurveFrame requires spine(t) => { x, z }.');
684
+ }
685
+ const start = Number(domain?.[0] ?? 0);
686
+ const end = Number(domain?.[1] ?? 1);
687
+ if (!Number.isFinite(start) || !Number.isFinite(end) || end === start) {
688
+ throw new RangeError('createCurveFrame requires a finite, non-empty domain.');
689
+ }
690
+ const span = end - start;
691
+ const steps = Math.max(Math.trunc(samples) || 0, 8);
692
+ const epsilon = Math.abs(span) / steps * 0.5;
693
+
694
+ const wrap = (t) => {
695
+ if (!closed) return Math.min(Math.max(t, Math.min(start, end)), Math.max(start, end));
696
+ const shifted = (t - start) % span;
697
+ return start + (shifted < 0 ? shifted + span : shifted);
698
+ };
699
+
700
+ // Arc-length table. Sampled once at construction so both `atArcLength` and
701
+ // arc-uniform scatter are O(log n) afterwards.
702
+ const table = new Array(steps + 1);
703
+ let total = 0;
704
+ let previous = curvePoint(spine, start);
705
+ table[0] = { s: 0, t: start, x: previous.x, z: previous.z };
706
+ for (let index = 1; index <= steps; index += 1) {
707
+ const t = start + span * (index / steps);
708
+ const point = curvePoint(spine, t);
709
+ total += Math.hypot(point.x - previous.x, point.z - previous.z);
710
+ table[index] = { s: total, t, x: point.x, z: point.z };
711
+ previous = point;
712
+ }
713
+
714
+ function pointAt(t) {
715
+ return curvePoint(spine, wrap(t));
716
+ }
717
+
718
+ function tangentAt(t) {
719
+ const at = wrap(t);
720
+ const back = curvePoint(spine, closed ? at - epsilon : Math.max(at - epsilon, Math.min(start, end)));
721
+ const forward = curvePoint(spine, closed ? at + epsilon : Math.min(at + epsilon, Math.max(start, end)));
722
+ const dx = forward.x - back.x;
723
+ const dz = forward.z - back.z;
724
+ const length = Math.hypot(dx, dz);
725
+ if (length === 0) return { x: 1, z: 0 };
726
+ return { x: dx / length, z: dz / length };
727
+ }
728
+
729
+ // Left normal: the tangent rotated +90° about +Y, i.e. (x, z) -> (-z, x).
730
+ // For a spine authored as z = f(x) with x increasing, this points toward +Z,
731
+ // which is what makes `offset` read the same way as a hand-authored
732
+ // "distance inland" column.
733
+ function normalAt(t) {
734
+ const tangent = tangentAt(t);
735
+ return { x: -tangent.z, z: tangent.x };
736
+ }
737
+
738
+ /** Yaw in radians that turns an object's +Z along the curve tangent. */
739
+ function headingAt(t) {
740
+ const tangent = tangentAt(t);
741
+ return Math.atan2(tangent.x, tangent.z);
742
+ }
743
+
744
+ function toWorld({ along = start, offset = 0 } = {}) {
745
+ const t = wrap(Number(along) || 0);
746
+ const point = curvePoint(spine, t);
747
+ const distance = Number(offset) || 0;
748
+ if (offsetAxis === 'z') return { x: point.x, z: point.z + distance };
749
+ if (offsetAxis === 'x') return { x: point.x + distance, z: point.z };
750
+ const normal = normalAt(t);
751
+ return { x: point.x + normal.x * distance, z: point.z + normal.z * distance };
752
+ }
753
+
754
+ function arcLengthAt(t) {
755
+ // Clamped, never wrapped: on a closed curve `wrap(end)` folds back to the
756
+ // domain start, which would collapse a full-loop range to zero length.
757
+ const target = Math.min(Math.max(Number(t) || 0, Math.min(start, end)), Math.max(start, end));
758
+ const fraction = (target - start) / span;
759
+ const position = Math.min(Math.max(fraction, 0), 1) * steps;
760
+ const low = Math.min(Math.floor(position), steps - 1);
761
+ const blend = position - low;
762
+ return table[low].s + (table[low + 1].s - table[low].s) * blend;
763
+ }
764
+
765
+ /** Parameter value at arc length `s` metres from the domain start. */
766
+ function atArcLength(s) {
767
+ const target = Math.min(Math.max(Number(s) || 0, 0), total);
768
+ let low = 0;
769
+ let high = steps;
770
+ while (high - low > 1) {
771
+ const mid = (low + high) >> 1;
772
+ if (table[mid].s <= target) low = mid;
773
+ else high = mid;
774
+ }
775
+ const segment = table[low + 1].s - table[low].s;
776
+ const blend = segment > 0 ? (target - table[low].s) / segment : 0;
777
+ return table[low].t + (table[low + 1].t - table[low].t) * blend;
778
+ }
779
+
780
+ /**
781
+ * Nearest point on the spine. Coarse table search, then a bounded ternary
782
+ * refinement on squared distance, so the result is stable and allocation-free
783
+ * for the callers that use it as a mask.
784
+ */
785
+ function fromWorld(x, z) {
786
+ const px = Number(x);
787
+ const pz = Number(z);
788
+ let bestIndex = 0;
789
+ let best = Infinity;
790
+ for (let index = 0; index <= steps; index += 1) {
791
+ const dx = table[index].x - px;
792
+ const dz = table[index].z - pz;
793
+ const squared = dx * dx + dz * dz;
794
+ if (squared < best) {
795
+ best = squared;
796
+ bestIndex = index;
797
+ }
798
+ }
799
+ let low = table[Math.max(bestIndex - 1, 0)].t;
800
+ let high = table[Math.min(bestIndex + 1, steps)].t;
801
+ const distanceSquared = (t) => {
802
+ const point = curvePoint(spine, t);
803
+ const dx = point.x - px;
804
+ const dz = point.z - pz;
805
+ return dx * dx + dz * dz;
806
+ };
807
+ for (let iteration = 0; iteration < 24; iteration += 1) {
808
+ const a = low + (high - low) / 3;
809
+ const b = high - (high - low) / 3;
810
+ if (distanceSquared(a) < distanceSquared(b)) high = b;
811
+ else low = a;
812
+ }
813
+ const along = (low + high) * 0.5;
814
+ const point = curvePoint(spine, along);
815
+ const normal = normalAt(along);
816
+ const offset = (px - point.x) * normal.x + (pz - point.z) * normal.z;
817
+ return { along, distance: Math.hypot(px - point.x, pz - point.z), offset };
818
+ }
819
+
820
+ /**
821
+ * `(x, z) => boolean` keep filter for a band of signed offsets. Pass it to
822
+ * `scatterInRect`, `surface.createGrassField`, or `combineMasks` so a
823
+ * rectangular scatter respects a curved boundary without the host
824
+ * re-deriving the distance by hand.
825
+ */
826
+ function bandMask({ min = -Infinity, max = Infinity, alongRange = null } = {}) {
827
+ return (x, z) => {
828
+ const { along, offset } = fromWorld(x, z);
829
+ if (offset < min || offset > max) return false;
830
+ if (!alongRange) return true;
831
+ return along >= Math.min(alongRange[0], alongRange[1])
832
+ && along <= Math.max(alongRange[0], alongRange[1]);
833
+ };
834
+ }
835
+
836
+ function resolveAlongRange(alongRange) {
837
+ if (!alongRange) return [Math.min(start, end), Math.max(start, end)];
838
+ return [Math.min(alongRange[0], alongRange[1]), Math.max(alongRange[0], alongRange[1])];
839
+ }
840
+
841
+ function makeSpacingFilter(minSpacing) {
842
+ const spacing = Math.max(Number(minSpacing) || 0, 0);
843
+ if (spacing <= 0) return { accept: () => true, commit: () => {} };
844
+ const cell = spacing;
845
+ const spacingSq = spacing * spacing;
846
+ const occupied = new Map();
847
+ return {
848
+ accept(x, z) {
849
+ const cx = Math.floor(x / cell);
850
+ const cz = Math.floor(z / cell);
851
+ for (let ix = cx - 1; ix <= cx + 1; ix += 1) {
852
+ for (let iz = cz - 1; iz <= cz + 1; iz += 1) {
853
+ const bucket = occupied.get(`${ix},${iz}`);
854
+ if (!bucket) continue;
855
+ for (const p of bucket) {
856
+ const dx = p.x - x;
857
+ const dz = p.z - z;
858
+ if (dx * dx + dz * dz < spacingSq) return false;
859
+ }
860
+ }
861
+ }
862
+ return true;
863
+ },
864
+ commit(placement) {
865
+ const key = `${Math.floor(placement.x / cell)},${Math.floor(placement.z / cell)}`;
866
+ const bucket = occupied.get(key);
867
+ if (bucket) bucket.push(placement);
868
+ else occupied.set(key, [placement]);
869
+ },
870
+ };
871
+ }
872
+
873
+ /**
874
+ * Seeded scatter distributed along the curve and across an offset band.
875
+ * `along` is uniform in **arc length**, not in the spine's parameter, so a
876
+ * curve whose parameter bunches (a radial pond margin, a heavily curved
877
+ * path) still receives even spacing on the ground.
878
+ *
879
+ * @returns {Array<{x, y, z, along, offset, heading}>}
880
+ */
881
+ function scatterAlong({
882
+ count = 100,
883
+ offsetRange = [0, 1],
884
+ alongRange = null,
885
+ minSpacing = 0,
886
+ seed = 1,
887
+ mask = null,
888
+ heightAt = null,
889
+ } = {}) {
890
+ const random = mulberry32(seed);
891
+ const [alongMin, alongMax] = resolveAlongRange(alongRange);
892
+ const arcMin = arcLengthAt(alongMin);
893
+ const arcMax = arcLengthAt(alongMax);
894
+ const offsetMin = Number(offsetRange?.[0] ?? 0);
895
+ const offsetMax = Number(offsetRange?.[1] ?? 0);
896
+ const spacing = makeSpacingFilter(minSpacing);
897
+ const target = Math.max(Math.trunc(count) || 0, 0);
898
+ const attempts = target * (mask ? 16 : minSpacing > 0 ? 8 : 1);
899
+ const placements = [];
900
+
901
+ for (let index = 0; index < attempts && placements.length < target; index += 1) {
902
+ const along = atArcLength(arcMin + (arcMax - arcMin) * random());
903
+ const offset = offsetMin + (offsetMax - offsetMin) * random();
904
+ const { x, z } = toWorld({ along, offset });
905
+ if (!passesMask(mask, x, z) || !spacing.accept(x, z)) continue;
906
+ const placement = {
907
+ along,
908
+ heading: headingAt(along),
909
+ offset,
910
+ x,
911
+ y: placementY(heightAt, x, z),
912
+ z,
913
+ };
914
+ placements.push(placement);
915
+ spacing.commit(placement);
916
+ }
917
+ return placements;
918
+ }
919
+
920
+ /**
921
+ * Evenly spaced placements measured in arc length — stepping stones, path
922
+ * edging, fence posts, wall bays, a bamboo screen. Deterministic and
923
+ * gap-free where `scatterAlong` would clump.
924
+ *
925
+ * @returns {Array<{x, y, z, along, offset, heading}>}
926
+ */
927
+ function stepAlong({
928
+ spacing = 1,
929
+ count = null,
930
+ offset = 0,
931
+ alongRange = null,
932
+ phase = 0,
933
+ jitterAlong = 0,
934
+ jitterOffset = 0,
935
+ seed = 1,
936
+ mask = null,
937
+ heightAt = null,
938
+ } = {}) {
939
+ const step = Math.max(Number(spacing) || 0, 1e-4);
940
+ const random = mulberry32(seed);
941
+ const [alongMin, alongMax] = resolveAlongRange(alongRange);
942
+ const arcMin = arcLengthAt(alongMin);
943
+ const arcMax = arcLengthAt(alongMax);
944
+ const available = Math.max(arcMax - arcMin, 0);
945
+ const limit = Number.isFinite(Number(count)) && Number(count) > 0
946
+ ? Math.trunc(Number(count))
947
+ : Math.floor(available / step) + 1;
948
+ const placements = [];
949
+
950
+ for (let index = 0; index < limit; index += 1) {
951
+ const jitteredAlongMetres = arcMin + Number(phase) * step + index * step
952
+ + (random() - 0.5) * 2 * Number(jitterAlong || 0);
953
+ if (jitteredAlongMetres > arcMax + 1e-6 || jitteredAlongMetres < arcMin - 1e-6) continue;
954
+ const along = atArcLength(jitteredAlongMetres);
955
+ const across = Number(offset) + (random() - 0.5) * 2 * Number(jitterOffset || 0);
956
+ const { x, z } = toWorld({ along, offset: across });
957
+ if (!passesMask(mask, x, z)) continue;
958
+ placements.push({
959
+ along,
960
+ heading: headingAt(along),
961
+ offset: across,
962
+ x,
963
+ y: placementY(heightAt, x, z),
964
+ z,
965
+ });
966
+ }
967
+ return placements;
968
+ }
969
+
970
+ return Object.freeze({
971
+ arcLengthAt,
972
+ atArcLength,
973
+ bandMask,
974
+ closed,
975
+ domain: Object.freeze([start, end]),
976
+ fromWorld,
977
+ headingAt,
978
+ length: total,
979
+ normalAt,
980
+ offsetAxis,
981
+ pointAt,
982
+ scatterAlong,
983
+ stepAlong,
984
+ tangentAt,
985
+ toWorld,
986
+ });
987
+ }