gloop-sim 0.0.0-stage → 1.0.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.
Files changed (166) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +111 -2
  3. package/dist/assert.d.ts +1 -0
  4. package/dist/assert.js +6 -0
  5. package/dist/assert.js.map +1 -0
  6. package/dist/backend/cpu.d.ts +2 -0
  7. package/dist/backend/cpu.js +23 -0
  8. package/dist/backend/cpu.js.map +1 -0
  9. package/dist/backend/gpu.d.ts +2 -0
  10. package/dist/backend/gpu.js +145 -0
  11. package/dist/backend/gpu.js.map +1 -0
  12. package/dist/backend/index.d.ts +14 -0
  13. package/dist/backend/index.js +23 -0
  14. package/dist/backend/index.js.map +1 -0
  15. package/dist/device.d.ts +11 -0
  16. package/dist/device.js +66 -0
  17. package/dist/device.js.map +1 -0
  18. package/dist/index.d.ts +18 -0
  19. package/dist/index.js +18 -0
  20. package/dist/index.js.map +1 -0
  21. package/dist/initializer/fixed.d.ts +2 -0
  22. package/dist/initializer/fixed.js +12 -0
  23. package/dist/initializer/fixed.js.map +1 -0
  24. package/dist/initializer/index.d.ts +14 -0
  25. package/dist/initializer/index.js +23 -0
  26. package/dist/initializer/index.js.map +1 -0
  27. package/dist/initializer/random.d.ts +2 -0
  28. package/dist/initializer/random.js +16 -0
  29. package/dist/initializer/random.js.map +1 -0
  30. package/dist/integrator/euler.d.ts +2 -0
  31. package/dist/integrator/euler.js +34 -0
  32. package/dist/integrator/euler.js.map +1 -0
  33. package/dist/integrator/index.d.ts +14 -0
  34. package/dist/integrator/index.js +23 -0
  35. package/dist/integrator/index.js.map +1 -0
  36. package/dist/integrator/semi-implicit-euler.d.ts +2 -0
  37. package/dist/integrator/semi-implicit-euler.js +36 -0
  38. package/dist/integrator/semi-implicit-euler.js.map +1 -0
  39. package/dist/model/elastic-network/cpu.d.ts +2 -0
  40. package/dist/model/elastic-network/cpu.js +90 -0
  41. package/dist/model/elastic-network/cpu.js.map +1 -0
  42. package/dist/model/elastic-network/gpu.d.ts +2 -0
  43. package/dist/model/elastic-network/gpu.js +114 -0
  44. package/dist/model/elastic-network/gpu.js.map +1 -0
  45. package/dist/model/elastic-network/index.d.ts +2 -0
  46. package/dist/model/elastic-network/index.js +69 -0
  47. package/dist/model/elastic-network/index.js.map +1 -0
  48. package/dist/model/index.d.ts +15 -0
  49. package/dist/model/index.js +24 -0
  50. package/dist/model/index.js.map +1 -0
  51. package/dist/model/inertial.d.ts +2 -0
  52. package/dist/model/inertial.js +17 -0
  53. package/dist/model/inertial.js.map +1 -0
  54. package/dist/render/backend/channels.d.ts +17 -0
  55. package/dist/render/backend/channels.js +35 -0
  56. package/dist/render/backend/channels.js.map +1 -0
  57. package/dist/render/backend/cloud.d.ts +77 -0
  58. package/dist/render/backend/cloud.js +29 -0
  59. package/dist/render/backend/cloud.js.map +1 -0
  60. package/dist/render/backend/compile.d.ts +31 -0
  61. package/dist/render/backend/compile.js +73 -0
  62. package/dist/render/backend/compile.js.map +1 -0
  63. package/dist/render/backend/glyph.d.ts +8 -0
  64. package/dist/render/backend/glyph.js +38 -0
  65. package/dist/render/backend/glyph.js.map +1 -0
  66. package/dist/render/backend/glyphs.d.ts +6 -0
  67. package/dist/render/backend/glyphs.js +146 -0
  68. package/dist/render/backend/glyphs.js.map +1 -0
  69. package/dist/render/backend/gpu.d.ts +12 -0
  70. package/dist/render/backend/gpu.js +278 -0
  71. package/dist/render/backend/gpu.js.map +1 -0
  72. package/dist/render/backend/material.d.ts +30 -0
  73. package/dist/render/backend/material.js +57 -0
  74. package/dist/render/backend/material.js.map +1 -0
  75. package/dist/render/backend/particles.d.ts +5 -0
  76. package/dist/render/backend/particles.js +22 -0
  77. package/dist/render/backend/particles.js.map +1 -0
  78. package/dist/render/backend/post.d.ts +29 -0
  79. package/dist/render/backend/post.js +77 -0
  80. package/dist/render/backend/post.js.map +1 -0
  81. package/dist/render/backend/solids.d.ts +10 -0
  82. package/dist/render/backend/solids.js +134 -0
  83. package/dist/render/backend/solids.js.map +1 -0
  84. package/dist/render/camera.d.ts +73 -0
  85. package/dist/render/camera.js +32 -0
  86. package/dist/render/camera.js.map +1 -0
  87. package/dist/render/channel.d.ts +25 -0
  88. package/dist/render/channel.js +9 -0
  89. package/dist/render/channel.js.map +1 -0
  90. package/dist/render/color.d.ts +34 -0
  91. package/dist/render/color.js +56 -0
  92. package/dist/render/color.js.map +1 -0
  93. package/dist/render/effect.d.ts +81 -0
  94. package/dist/render/effect.js +30 -0
  95. package/dist/render/effect.js.map +1 -0
  96. package/dist/render/frame.d.ts +60 -0
  97. package/dist/render/frame.js +52 -0
  98. package/dist/render/frame.js.map +1 -0
  99. package/dist/render/index.d.ts +12 -0
  100. package/dist/render/index.js +2 -0
  101. package/dist/render/index.js.map +1 -0
  102. package/dist/render/layer.d.ts +90 -0
  103. package/dist/render/layer.js +54 -0
  104. package/dist/render/layer.js.map +1 -0
  105. package/dist/render/ramp.d.ts +93 -0
  106. package/dist/render/ramp.js +120 -0
  107. package/dist/render/ramp.js.map +1 -0
  108. package/dist/render/sampler.d.ts +34 -0
  109. package/dist/render/sampler.js +53 -0
  110. package/dist/render/sampler.js.map +1 -0
  111. package/dist/render/signal.d.ts +66 -0
  112. package/dist/render/signal.js +51 -0
  113. package/dist/render/signal.js.map +1 -0
  114. package/dist/render/sources.d.ts +33 -0
  115. package/dist/render/sources.js +46 -0
  116. package/dist/render/sources.js.map +1 -0
  117. package/dist/render/springs/anchors.d.ts +37 -0
  118. package/dist/render/springs/anchors.js +34 -0
  119. package/dist/render/springs/anchors.js.map +1 -0
  120. package/dist/render/springs/construction.d.ts +30 -0
  121. package/dist/render/springs/construction.js +66 -0
  122. package/dist/render/springs/construction.js.map +1 -0
  123. package/dist/render/springs/impulses.d.ts +45 -0
  124. package/dist/render/springs/impulses.js +144 -0
  125. package/dist/render/springs/impulses.js.map +1 -0
  126. package/dist/render/springs/index.d.ts +9 -0
  127. package/dist/render/springs/index.js +2 -0
  128. package/dist/render/springs/index.js.map +1 -0
  129. package/dist/render/springs/link.d.ts +51 -0
  130. package/dist/render/springs/link.js +42 -0
  131. package/dist/render/springs/link.js.map +1 -0
  132. package/dist/render/springs/links.d.ts +70 -0
  133. package/dist/render/springs/links.js +210 -0
  134. package/dist/render/springs/links.js.map +1 -0
  135. package/dist/render/springs/palette.d.ts +36 -0
  136. package/dist/render/springs/palette.js +22 -0
  137. package/dist/render/springs/palette.js.map +1 -0
  138. package/dist/render/springs/preset.d.ts +65 -0
  139. package/dist/render/springs/preset.js +114 -0
  140. package/dist/render/springs/preset.js.map +1 -0
  141. package/dist/render/springs/segment.d.ts +40 -0
  142. package/dist/render/springs/segment.js +81 -0
  143. package/dist/render/springs/segment.js.map +1 -0
  144. package/dist/render/springs/source.d.ts +35 -0
  145. package/dist/render/springs/source.js +70 -0
  146. package/dist/render/springs/source.js.map +1 -0
  147. package/dist/render/springs/springs.d.ts +40 -0
  148. package/dist/render/springs/springs.js +34 -0
  149. package/dist/render/springs/springs.js.map +1 -0
  150. package/dist/render/springs/strain.d.ts +21 -0
  151. package/dist/render/springs/strain.js +39 -0
  152. package/dist/render/springs/strain.js.map +1 -0
  153. package/dist/render/stage.d.ts +96 -0
  154. package/dist/render/stage.js +2 -0
  155. package/dist/render/stage.js.map +1 -0
  156. package/dist/render/view.d.ts +63 -0
  157. package/dist/render/view.js +63 -0
  158. package/dist/render/view.js.map +1 -0
  159. package/dist/swarm.d.ts +5 -0
  160. package/dist/swarm.js +24 -0
  161. package/dist/swarm.js.map +1 -0
  162. package/dist/types.d.ts +290 -0
  163. package/dist/types.js +2 -0
  164. package/dist/types.js.map +1 -0
  165. package/docs/assets/logo.svg +112 -0
  166. package/package.json +102 -3
@@ -0,0 +1 @@
1
+ {"version":3,"file":"signal.js","sourceRoot":"","sources":["../../src/render/signal.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAgB,OAAO,IAAI,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAmB7E;;GAEG;AACH,MAAM,YAAY,GAAiC,aAAa,CAAsB;IACpF,IAAI,EAAE,OAAO;IACb,MAAM,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,MAAM,EAAE;CACpC,CAAC,CAAC;AAEH;;GAEG;AACH,MAAM,YAAY,GAAiC,aAAa,CAAsB;IACpF,IAAI,EAAE,OAAO;IACb,MAAM,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,MAAM,EAAE;CACpC,CAAC,CAAC;AAuCH,MAAM,UAAU,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC,GAAG,EAAE,IAAI,CAAC,EAAE,KAAK,EAAkB,EAAE,GAAW;IACzF,MAAM,CAAC,GAAG,CAAC,GAAG,GAAG,GAAG,CAAC,GAAG,CAAC,IAAI,GAAG,GAAG,CAAC,CAAC;IACrC,OAAO,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACjD,CAAC;AAED;;GAEG;AACH,MAAM,KAAW,MAAM,CA2BtB;AA3BD,WAAiB,MAAM;IACrB;;OAEG;IACH,SAAgB,EAAE,CAAmB,OAAmB,EAAE,EAAE,MAAM,EAAE,KAAK,GAAG,IAAI,EAAiB;QAC/F,MAAM,CAAC,GAAG,EAAE,IAAI,CAAC,GAAG,MAAM,CAAC;QAC3B,MAAM,CACJ,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,EAC7C,GAAG,OAAO,CAAC,IAAI,0CAA0C,GAAG,KAAK,IAAI,GAAG,CACzE,CAAC;QACF,MAAM,CAAC,GAAG,KAAK,IAAI,EAAE,GAAG,OAAO,CAAC,IAAI,sDAAsD,GAAG,EAAE,CAAC,CAAC;QACjG,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC;IACpC,CAAC;IARe,SAAE,KAQjB,CAAA;IAED;;OAEG;IACH,SAAgB,KAAK,CAAC,OAAsB;QAC1C,OAAO,EAAE,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC;IACnC,CAAC;IAFe,YAAK,QAEpB,CAAA;IAED;;OAEG;IACH,SAAgB,KAAK,CAAC,OAAsB;QAC1C,OAAO,EAAE,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC;IACnC,CAAC;IAFe,YAAK,QAEpB,CAAA;AACH,CAAC,EA3BgB,MAAM,KAAN,MAAM,QA2BtB","sourcesContent":["import { assert } from \"@/assert.js\";\nimport { type Channel, channel as defineChannel } from \"@/render/channel.js\";\nimport type { Source } from \"@/render/sources.js\";\n\nexport interface ParticleSpeedSource extends Source {\n /**\n * Each particle's velocity magnitude for the current frame, in scene units\n * per second.\n */\n speeds(): Float32Array;\n}\n\nexport interface ParticleDepthSource extends Source {\n /**\n * Each particle's distance into the scene along the view axis for the current\n * frame, in scene units.\n */\n depths(): Float32Array;\n}\n\n/**\n * Each particle's velocity magnitude, in scene units per second.\n */\nconst speedChannel: Channel<ParticleSpeedSource> = defineChannel<ParticleSpeedSource>({\n name: \"speed\",\n values: (source) => source.speeds(),\n});\n\n/**\n * Each particle's distance into the scene along the view axis, in scene units.\n */\nconst depthChannel: Channel<ParticleDepthSource> = defineChannel<ParticleDepthSource>({\n name: \"depth\",\n values: (source) => source.depths(),\n});\n\n/**\n * A component that normalizes the values provided by a channel.\n */\nexport interface Signal<S extends Source> {\n /**\n * The channel this signal reads from.\n */\n readonly channel: Channel<S>;\n\n /**\n * The `[low, high]` range mapped onto `0..1`. The endpoints must be\n * finite and distinct. A reversed range is also valid and inverts the\n * mapping.\n */\n readonly domain: readonly [number, number];\n\n /**\n * Whether raw values outside `domain` are clamped to `0..1` or allowed to\n * extrapolate beyond it.\n */\n readonly clamp: boolean;\n}\n\nexport interface SignalOptions {\n /**\n * The `[low, high]` range mapped onto `0..1`. The endpoints must be\n * finite and distinct. A reversed range is also valid and inverts the\n * mapping.\n */\n readonly domain: readonly [number, number];\n\n /**\n * Whether raw values outside `domain` are clamped or extrapolated beyond it.\n */\n readonly clamp?: boolean;\n}\n\nexport function normalizeSignal({ domain: [low, high], clamp }: Signal<Source>, raw: number): number {\n const t = (raw - low) / (high - low);\n return clamp ? Math.max(0, Math.min(1, t)) : t;\n}\n\n/**\n * The signal factories exposed on `view.signal`.\n */\nexport namespace signal {\n /**\n * A signal over the specified channel.\n */\n export function of<S extends Source>(channel: Channel<S>, { domain, clamp = true }: SignalOptions): Signal<S> {\n const [low, high] = domain;\n assert(\n Number.isFinite(low) && Number.isFinite(high),\n `${channel.name} signal domain must be finite, but is [${low}, ${high}]`,\n );\n assert(low !== high, `${channel.name} signal domain endpoints must differ, but are both ${low}`);\n return { channel, domain, clamp };\n }\n\n /**\n * A particle's velocity magnitude, in scene units per second.\n */\n export function speed(options: SignalOptions): Signal<ParticleSpeedSource> {\n return of(speedChannel, options);\n }\n\n /**\n * A particle's distance into the scene along the view axis, in scene units.\n */\n export function depth(options: SignalOptions): Signal<ParticleDepthSource> {\n return of(depthChannel, options);\n }\n}\n"]}
@@ -0,0 +1,33 @@
1
+ import { type Frame } from "../render/frame.js";
2
+ import type { SwarmState } from "../types.js";
3
+ /**
4
+ * A source of data the renderer draws from.
5
+ */
6
+ export interface Source {
7
+ /**
8
+ * Updates the source to reflect the current state of the swarm.
9
+ */
10
+ update(state: SwarmState, frame: Frame): void;
11
+ }
12
+ export interface ParticleSource extends Source {
13
+ /**
14
+ * The number of particles in the current frame.
15
+ */
16
+ count(): number;
17
+ /**
18
+ * Each particle's velocity magnitude for the current frame, in scene units
19
+ * per second.
20
+ */
21
+ speeds(): Float32Array;
22
+ /**
23
+ * Each particle's distance into the scene along the view axis for the current
24
+ * frame, in scene units.
25
+ */
26
+ depths(): Float32Array;
27
+ }
28
+ export declare namespace sources {
29
+ /**
30
+ * A source providing data on the swarm's particles.
31
+ */
32
+ function particles(): ParticleSource;
33
+ }
@@ -0,0 +1,46 @@
1
+ import { assert } from "../assert.js";
2
+ import { PackedPositions } from "../render/frame.js";
3
+ class SimpleParticleSource {
4
+ frame = null;
5
+ depthBuffer = new Float32Array(0);
6
+ count() {
7
+ return this.current().particleCount;
8
+ }
9
+ speeds() {
10
+ return this.current().speeds;
11
+ }
12
+ depths() {
13
+ this.depthBuffer = this.copyZCoordinatesTo(this.depthBuffer);
14
+ return this.depthBuffer;
15
+ }
16
+ update(_state, frame) {
17
+ this.frame = frame;
18
+ }
19
+ current() {
20
+ assert(this.frame !== null, "particle source read before its first update");
21
+ return this.frame;
22
+ }
23
+ copyZCoordinatesTo(buffer) {
24
+ const frame = this.current();
25
+ const count = frame.particleCount;
26
+ if (buffer.length !== count) {
27
+ buffer = new Float32Array(count);
28
+ }
29
+ const points = PackedPositions.of(frame.positions);
30
+ for (let index = 0; index < count; index++) {
31
+ buffer[index] = points.z(index);
32
+ }
33
+ return buffer;
34
+ }
35
+ }
36
+ export var sources;
37
+ (function (sources) {
38
+ /**
39
+ * A source providing data on the swarm's particles.
40
+ */
41
+ function particles() {
42
+ return new SimpleParticleSource();
43
+ }
44
+ sources.particles = particles;
45
+ })(sources || (sources = {}));
46
+ //# sourceMappingURL=sources.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sources.js","sourceRoot":"","sources":["../../src/render/sources.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAc,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAgChE,MAAM,oBAAoB;IAChB,KAAK,GAAiB,IAAI,CAAC;IAC3B,WAAW,GAAiB,IAAI,YAAY,CAAC,CAAC,CAAC,CAAC;IAExD,KAAK;QACH,OAAO,IAAI,CAAC,OAAO,EAAE,CAAC,aAAa,CAAC;IACtC,CAAC;IAED,MAAM;QACJ,OAAO,IAAI,CAAC,OAAO,EAAE,CAAC,MAAM,CAAC;IAC/B,CAAC;IAED,MAAM;QACJ,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC,kBAAkB,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;QAC7D,OAAO,IAAI,CAAC,WAAW,CAAC;IAC1B,CAAC;IAED,MAAM,CAAC,MAAkB,EAAE,KAAY;QACrC,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;IACrB,CAAC;IAEO,OAAO;QACb,MAAM,CAAC,IAAI,CAAC,KAAK,KAAK,IAAI,EAAE,8CAA8C,CAAC,CAAC;QAC5E,OAAO,IAAI,CAAC,KAAK,CAAC;IACpB,CAAC;IAEO,kBAAkB,CAAC,MAAoB;QAC7C,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,EAAE,CAAC;QAC7B,MAAM,KAAK,GAAG,KAAK,CAAC,aAAa,CAAC;QAElC,IAAI,MAAM,CAAC,MAAM,KAAK,KAAK,EAAE,CAAC;YAC5B,MAAM,GAAG,IAAI,YAAY,CAAC,KAAK,CAAC,CAAC;QACnC,CAAC;QAED,MAAM,MAAM,GAAG,eAAe,CAAC,EAAE,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;QACnD,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,KAAK,EAAE,KAAK,EAAE,EAAE,CAAC;YAC3C,MAAM,CAAC,KAAK,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;QAClC,CAAC;QAED,OAAO,MAAM,CAAC;IAChB,CAAC;CACF;AAED,MAAM,KAAW,OAAO,CAOvB;AAPD,WAAiB,OAAO;IACtB;;OAEG;IACH,SAAgB,SAAS;QACvB,OAAO,IAAI,oBAAoB,EAAE,CAAC;IACpC,CAAC;IAFe,iBAAS,YAExB,CAAA;AACH,CAAC,EAPgB,OAAO,KAAP,OAAO,QAOvB","sourcesContent":["import { assert } from \"@/assert.js\";\nimport { type Frame, PackedPositions } from \"@/render/frame.js\";\nimport type { SwarmState } from \"@/types.js\";\n\n/**\n * A source of data the renderer draws from.\n */\nexport interface Source {\n /**\n * Updates the source to reflect the current state of the swarm.\n */\n update(state: SwarmState, frame: Frame): void;\n}\n\nexport interface ParticleSource extends Source {\n /**\n * The number of particles in the current frame.\n */\n count(): number;\n\n /**\n * Each particle's velocity magnitude for the current frame, in scene units\n * per second.\n */\n speeds(): Float32Array;\n\n /**\n * Each particle's distance into the scene along the view axis for the current\n * frame, in scene units.\n */\n depths(): Float32Array;\n}\n\nclass SimpleParticleSource implements ParticleSource {\n private frame: Frame | null = null;\n private depthBuffer: Float32Array = new Float32Array(0);\n\n count(): number {\n return this.current().particleCount;\n }\n\n speeds(): Float32Array {\n return this.current().speeds;\n }\n\n depths(): Float32Array {\n this.depthBuffer = this.copyZCoordinatesTo(this.depthBuffer);\n return this.depthBuffer;\n }\n\n update(_state: SwarmState, frame: Frame): void {\n this.frame = frame;\n }\n\n private current(): Frame {\n assert(this.frame !== null, \"particle source read before its first update\");\n return this.frame;\n }\n\n private copyZCoordinatesTo(buffer: Float32Array): Float32Array {\n const frame = this.current();\n const count = frame.particleCount;\n\n if (buffer.length !== count) {\n buffer = new Float32Array(count);\n }\n\n const points = PackedPositions.of(frame.positions);\n for (let index = 0; index < count; index++) {\n buffer[index] = points.z(index);\n }\n\n return buffer;\n }\n}\n\nexport namespace sources {\n /**\n * A source providing data on the swarm's particles.\n */\n export function particles(): ParticleSource {\n return new SimpleParticleSource();\n }\n}\n"]}
@@ -0,0 +1,37 @@
1
+ import type { Color } from "../../render/color.js";
2
+ import { type Layer, type ParticleGlyph } from "../../render/layer.js";
3
+ import { type ColorValue, type ScalarValue } from "../../render/ramp.js";
4
+ import type { Springs } from "../../render/springs/source.js";
5
+ /**
6
+ * A layer that draws the springs' anchor particles as a glyph overlay, with a
7
+ * constant color and size of its own.
8
+ */
9
+ export interface AnchorsLayer extends Layer<Springs> {
10
+ /**
11
+ * The glyph drawn at each anchor particle's position.
12
+ */
13
+ readonly shape: ParticleGlyph;
14
+ /**
15
+ * The constant color applied to every anchor glyph.
16
+ */
17
+ readonly color: ColorValue;
18
+ /**
19
+ * The glyph size; always positive.
20
+ */
21
+ readonly size: ScalarValue;
22
+ /**
23
+ * The glyph opacity, within 0..1.
24
+ */
25
+ readonly opacity: ScalarValue;
26
+ }
27
+ /**
28
+ * An anchors layer over the springs, drawing the glyph at each anchor's
29
+ * particle position.
30
+ */
31
+ export declare function anchors({ from, shape, color, size, opacity, }: {
32
+ from: Springs;
33
+ shape?: ParticleGlyph;
34
+ color: Color;
35
+ size?: number;
36
+ opacity?: number;
37
+ }): AnchorsLayer;
@@ -0,0 +1,34 @@
1
+ import { buildGlyphCloud } from "../../render/backend/glyphs.js";
2
+ import { verifyIsPositive, verifyIsUnit } from "../../render/layer.js";
3
+ import { colorValue, scalarValue } from "../../render/ramp.js";
4
+ /**
5
+ * An anchors layer over the springs, drawing the glyph at each anchor's
6
+ * particle position.
7
+ */
8
+ export function anchors({ from, shape = "gyroscope", color, size = 2, opacity = 1, }) {
9
+ verifyIsPositive(size, "anchors size");
10
+ verifyIsUnit(opacity, "anchors opacity");
11
+ const spec = {
12
+ source: from,
13
+ shape,
14
+ color: colorValue(color),
15
+ size: scalarValue(size),
16
+ opacity: scalarValue(opacity),
17
+ build: () => buildAnchors(spec),
18
+ };
19
+ return spec;
20
+ }
21
+ function buildAnchors(layer) {
22
+ const cloud = buildGlyphCloud({ shape: layer.shape, style: layer, sizeScale: 1 });
23
+ const indices = [...layer.source.anchors];
24
+ return {
25
+ object: cloud.mesh,
26
+ update(frame) {
27
+ cloud.update(frame, indices);
28
+ },
29
+ dispose() {
30
+ cloud.dispose();
31
+ },
32
+ };
33
+ }
34
+ //# sourceMappingURL=anchors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"anchors.js","sourceRoot":"","sources":["../../../src/render/springs/anchors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAG7D,OAAO,EAAoD,gBAAgB,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACrH,OAAO,EAAmB,UAAU,EAAoB,WAAW,EAAE,MAAM,kBAAkB,CAAC;AA6B9F;;;GAGG;AACH,MAAM,UAAU,OAAO,CAAC,EACtB,IAAI,EACJ,KAAK,GAAG,WAAW,EACnB,KAAK,EACL,IAAI,GAAG,CAAC,EACR,OAAO,GAAG,CAAC,GAOZ;IACC,gBAAgB,CAAC,IAAI,EAAE,cAAc,CAAC,CAAC;IACvC,YAAY,CAAC,OAAO,EAAE,iBAAiB,CAAC,CAAC;IACzC,MAAM,IAAI,GAAiB;QACzB,MAAM,EAAE,IAAI;QACZ,KAAK;QACL,KAAK,EAAE,UAAU,CAAC,KAAK,CAAC;QACxB,IAAI,EAAE,WAAW,CAAC,IAAI,CAAC;QACvB,OAAO,EAAE,WAAW,CAAC,OAAO,CAAC;QAC7B,KAAK,EAAE,GAAG,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC;KAChC,CAAC;IACF,OAAO,IAAI,CAAC;AACd,CAAC;AAED,SAAS,YAAY,CAAC,KAAmB;IACvC,MAAM,KAAK,GAAG,eAAe,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,EAAE,CAAC,CAAC;IAClF,MAAM,OAAO,GAAG,CAAC,GAAG,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IAE1C,OAAO;QACL,MAAM,EAAE,KAAK,CAAC,IAAI;QAClB,MAAM,CAAC,KAAY;YACjB,KAAK,CAAC,MAAM,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;QAC/B,CAAC;QACD,OAAO;YACL,KAAK,CAAC,OAAO,EAAE,CAAC;QAClB,CAAC;KACF,CAAC;AACJ,CAAC","sourcesContent":["import { buildGlyphCloud } from \"@/render/backend/glyphs.js\";\nimport type { Color } from \"@/render/color.js\";\nimport type { Frame } from \"@/render/frame.js\";\nimport { type Layer, type LayerObject, type ParticleGlyph, verifyIsPositive, verifyIsUnit } from \"@/render/layer.js\";\nimport { type ColorValue, colorValue, type ScalarValue, scalarValue } from \"@/render/ramp.js\";\nimport type { Springs } from \"@/render/springs/source.js\";\n\n/**\n * A layer that draws the springs' anchor particles as a glyph overlay, with a\n * constant color and size of its own.\n */\nexport interface AnchorsLayer extends Layer<Springs> {\n /**\n * The glyph drawn at each anchor particle's position.\n */\n readonly shape: ParticleGlyph;\n\n /**\n * The constant color applied to every anchor glyph.\n */\n readonly color: ColorValue;\n\n /**\n * The glyph size; always positive.\n */\n readonly size: ScalarValue;\n\n /**\n * The glyph opacity, within 0..1.\n */\n readonly opacity: ScalarValue;\n}\n\n/**\n * An anchors layer over the springs, drawing the glyph at each anchor's\n * particle position.\n */\nexport function anchors({\n from,\n shape = \"gyroscope\",\n color,\n size = 2,\n opacity = 1,\n}: {\n from: Springs;\n shape?: ParticleGlyph;\n color: Color;\n size?: number;\n opacity?: number;\n}): AnchorsLayer {\n verifyIsPositive(size, \"anchors size\");\n verifyIsUnit(opacity, \"anchors opacity\");\n const spec: AnchorsLayer = {\n source: from,\n shape,\n color: colorValue(color),\n size: scalarValue(size),\n opacity: scalarValue(opacity),\n build: () => buildAnchors(spec),\n };\n return spec;\n}\n\nfunction buildAnchors(layer: AnchorsLayer): LayerObject {\n const cloud = buildGlyphCloud({ shape: layer.shape, style: layer, sizeScale: 1 });\n const indices = [...layer.source.anchors];\n\n return {\n object: cloud.mesh,\n update(frame: Frame): void {\n cloud.update(frame, indices);\n },\n dispose(): void {\n cloud.dispose();\n },\n };\n}\n"]}
@@ -0,0 +1,30 @@
1
+ import type { Color } from "../../render/color.js";
2
+ import type { Layer } from "../../render/layer.js";
3
+ import type { ParticleSource } from "../../render/sources.js";
4
+ /**
5
+ * A layer of static scaffolding geometry drawn behind a scene: concentric
6
+ * rings, the two axes, and a fan of radial rays.
7
+ */
8
+ export interface ConstructionLayer extends Layer<ParticleSource> {
9
+ /**
10
+ * The color of the scaffolding lines.
11
+ */
12
+ readonly color: Color;
13
+ /**
14
+ * The line opacity, within 0..1.
15
+ */
16
+ readonly opacity: number;
17
+ /**
18
+ * The number of concentric rings drawn.
19
+ */
20
+ readonly rings: number;
21
+ }
22
+ /**
23
+ * A construction-geometry layer, drawing static scaffolding behind the swarm.
24
+ */
25
+ export declare function construction({ from, color, opacity, rings, }: {
26
+ from: ParticleSource;
27
+ color: Color;
28
+ opacity?: number;
29
+ rings?: number;
30
+ }): ConstructionLayer;
@@ -0,0 +1,66 @@
1
+ import { BufferAttribute, BufferGeometry, LineSegments } from "three/webgpu";
2
+ import { assert } from "../../assert.js";
3
+ import { material as materialFactory } from "../../render/backend/material.js";
4
+ // The depth the underdrawing sits at, behind the particles and links it must
5
+ // not occlude.
6
+ const CONSTRUCTION_DEPTH = -0.2;
7
+ const MAX_RADIUS = 1.15;
8
+ const CIRCLE_SEGMENTS = 96;
9
+ const RAY_COUNT = 12;
10
+ /**
11
+ * A construction-geometry layer, drawing static scaffolding behind the swarm.
12
+ */
13
+ export function construction({ from, color, opacity = 0.12, rings = 5, }) {
14
+ assert(opacity >= 0 && opacity <= 1, `construction opacity must be within 0..1, but is ${opacity}`);
15
+ assert(Number.isInteger(rings) && rings >= 0, `construction rings must be a non-negative integer, but is ${rings}`);
16
+ const spec = {
17
+ source: from,
18
+ color,
19
+ opacity,
20
+ rings,
21
+ build: () => buildConstruction(spec),
22
+ };
23
+ return spec;
24
+ }
25
+ function pushCircle(points, radius) {
26
+ for (let segment = 0; segment < CIRCLE_SEGMENTS; segment++) {
27
+ const a = (segment / CIRCLE_SEGMENTS) * Math.PI * 2;
28
+ const b = ((segment + 1) / CIRCLE_SEGMENTS) * Math.PI * 2;
29
+ points.push(Math.cos(a) * radius, Math.sin(a) * radius, CONSTRUCTION_DEPTH, Math.cos(b) * radius, Math.sin(b) * radius, CONSTRUCTION_DEPTH);
30
+ }
31
+ }
32
+ function pushSegment(points, ax, ay, bx, by) {
33
+ points.push(ax, ay, CONSTRUCTION_DEPTH, bx, by, CONSTRUCTION_DEPTH);
34
+ }
35
+ function buildGeometryPoints(rings) {
36
+ const points = [];
37
+ for (let ring = 1; ring <= rings; ring++) {
38
+ pushCircle(points, (ring / Math.max(rings, 1)) * MAX_RADIUS);
39
+ }
40
+ pushSegment(points, -MAX_RADIUS, 0, MAX_RADIUS, 0);
41
+ pushSegment(points, 0, -MAX_RADIUS, 0, MAX_RADIUS);
42
+ for (let ray = 0; ray < RAY_COUNT; ray++) {
43
+ const angle = (ray / RAY_COUNT) * Math.PI * 2;
44
+ pushSegment(points, 0, 0, Math.cos(angle) * MAX_RADIUS, Math.sin(angle) * MAX_RADIUS);
45
+ }
46
+ return points;
47
+ }
48
+ /**
49
+ * Builds the construction-geometry layer: concentric rings, axes, and radial
50
+ * rays. The geometry is static, so the layer has no per-frame work.
51
+ */
52
+ function buildConstruction(layer) {
53
+ const points = buildGeometryPoints(layer.rings);
54
+ const geometry = new BufferGeometry();
55
+ geometry.setAttribute("position", new BufferAttribute(new Float32Array(points), 3));
56
+ const material = materialFactory.line({ color: layer.color, opacity: layer.opacity });
57
+ return {
58
+ object: new LineSegments(geometry, material),
59
+ update() { },
60
+ dispose() {
61
+ geometry.dispose();
62
+ material.dispose();
63
+ },
64
+ };
65
+ }
66
+ //# sourceMappingURL=construction.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"construction.js","sourceRoot":"","sources":["../../../src/render/springs/construction.ts"],"names":[],"mappings":"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","sourcesContent":["import { BufferAttribute, BufferGeometry, LineSegments } from \"three/webgpu\";\nimport { assert } from \"@/assert.js\";\nimport { material as materialFactory } from \"@/render/backend/material.js\";\nimport type { Color } from \"@/render/color.js\";\nimport type { Layer, LayerObject } from \"@/render/layer.js\";\nimport type { ParticleSource } from \"@/render/sources.js\";\n\n/**\n * A layer of static scaffolding geometry drawn behind a scene: concentric\n * rings, the two axes, and a fan of radial rays.\n */\nexport interface ConstructionLayer extends Layer<ParticleSource> {\n /**\n * The color of the scaffolding lines.\n */\n readonly color: Color;\n\n /**\n * The line opacity, within 0..1.\n */\n readonly opacity: number;\n\n /**\n * The number of concentric rings drawn.\n */\n readonly rings: number;\n}\n\n// The depth the underdrawing sits at, behind the particles and links it must\n// not occlude.\nconst CONSTRUCTION_DEPTH = -0.2;\nconst MAX_RADIUS = 1.15;\nconst CIRCLE_SEGMENTS = 96;\nconst RAY_COUNT = 12;\n\n/**\n * A construction-geometry layer, drawing static scaffolding behind the swarm.\n */\nexport function construction({\n from,\n color,\n opacity = 0.12,\n rings = 5,\n}: {\n from: ParticleSource;\n color: Color;\n opacity?: number;\n rings?: number;\n}): ConstructionLayer {\n assert(opacity >= 0 && opacity <= 1, `construction opacity must be within 0..1, but is ${opacity}`);\n assert(Number.isInteger(rings) && rings >= 0, `construction rings must be a non-negative integer, but is ${rings}`);\n const spec: ConstructionLayer = {\n source: from,\n color,\n opacity,\n rings,\n build: () => buildConstruction(spec),\n };\n return spec;\n}\n\nfunction pushCircle(points: number[], radius: number): void {\n for (let segment = 0; segment < CIRCLE_SEGMENTS; segment++) {\n const a = (segment / CIRCLE_SEGMENTS) * Math.PI * 2;\n const b = ((segment + 1) / CIRCLE_SEGMENTS) * Math.PI * 2;\n points.push(\n Math.cos(a) * radius,\n Math.sin(a) * radius,\n CONSTRUCTION_DEPTH,\n Math.cos(b) * radius,\n Math.sin(b) * radius,\n CONSTRUCTION_DEPTH,\n );\n }\n}\n\nfunction pushSegment(points: number[], ax: number, ay: number, bx: number, by: number): void {\n points.push(ax, ay, CONSTRUCTION_DEPTH, bx, by, CONSTRUCTION_DEPTH);\n}\n\nfunction buildGeometryPoints(rings: number): number[] {\n const points: number[] = [];\n for (let ring = 1; ring <= rings; ring++) {\n pushCircle(points, (ring / Math.max(rings, 1)) * MAX_RADIUS);\n }\n pushSegment(points, -MAX_RADIUS, 0, MAX_RADIUS, 0);\n pushSegment(points, 0, -MAX_RADIUS, 0, MAX_RADIUS);\n for (let ray = 0; ray < RAY_COUNT; ray++) {\n const angle = (ray / RAY_COUNT) * Math.PI * 2;\n pushSegment(points, 0, 0, Math.cos(angle) * MAX_RADIUS, Math.sin(angle) * MAX_RADIUS);\n }\n return points;\n}\n\n/**\n * Builds the construction-geometry layer: concentric rings, axes, and radial\n * rays. The geometry is static, so the layer has no per-frame work.\n */\nfunction buildConstruction(layer: ConstructionLayer): LayerObject {\n const points = buildGeometryPoints(layer.rings);\n const geometry = new BufferGeometry();\n geometry.setAttribute(\"position\", new BufferAttribute(new Float32Array(points), 3));\n const material = materialFactory.line({ color: layer.color, opacity: layer.opacity });\n\n return {\n object: new LineSegments(geometry, material),\n update(): void {},\n dispose(): void {\n geometry.dispose();\n material.dispose();\n },\n };\n}\n"]}
@@ -0,0 +1,45 @@
1
+ import { type Color } from "../../render/color.js";
2
+ import type { Layer } from "../../render/layer.js";
3
+ import type { Springs } from "../../render/springs/source.js";
4
+ /**
5
+ * A layer of short-lived impulses that travel along stressed links.
6
+ */
7
+ export interface ImpulsesLayer extends Layer<Springs> {
8
+ /**
9
+ * The strain, as a fraction of rest length, a link must exceed before it
10
+ * carries impulses.
11
+ */
12
+ readonly stress: number;
13
+ /**
14
+ * The color at the impulse head, its leading and brightest point.
15
+ */
16
+ readonly color: Color;
17
+ /**
18
+ * The color at the impulse tail, drawn trailing the head at reduced alpha.
19
+ */
20
+ readonly tailColor: Color;
21
+ /**
22
+ * The mean rate, per second, at which a qualifying link emits an impulse.
23
+ */
24
+ readonly frequency: number;
25
+ /**
26
+ * How fast an impulse travels along its link, in link-lengths per second.
27
+ */
28
+ readonly speed: number;
29
+ /**
30
+ * The impulse head-to-tail length, as a fraction of the link length.
31
+ */
32
+ readonly length: number;
33
+ }
34
+ /**
35
+ * An impulses layer traveling along the springs' stressed links.
36
+ */
37
+ export declare function impulses({ from, stress, color: headColor, tailColor, frequency, speed, length, }: {
38
+ from: Springs;
39
+ stress: number;
40
+ color: Color;
41
+ tailColor: Color;
42
+ frequency?: number;
43
+ speed?: number;
44
+ length?: number;
45
+ }): ImpulsesLayer;
@@ -0,0 +1,144 @@
1
+ import { attribute } from "three/tsl";
2
+ import { assert } from "../../assert.js";
3
+ import { material as materialFactory } from "../../render/backend/material.js";
4
+ import { color } from "../../render/color.js";
5
+ import { PackedPositions } from "../../render/frame.js";
6
+ import { segmentAttribute, segmentGeometry, segmentMesh, VERTICES_PER_SEGMENT, writeSegment, } from "../../render/springs/segment.js";
7
+ const IMPULSE_WIDTH_WORLD = 0.006;
8
+ const GOLDEN = 0.618033988749895;
9
+ const TAIL_ALPHA = 0.25;
10
+ const COLOR_ATTRIBUTE = "aColor";
11
+ const FLOATS_PER_COLOR = 4;
12
+ const ALPHA_OFFSET = 3;
13
+ // Which impulse endpoint each of the six segment vertices sits at: 0 is the
14
+ // tail, 1 is the head. Matches the corner order in writeSegment.
15
+ const VERTEX_END = [0, 1, 1, 0, 1, 0];
16
+ /**
17
+ * An impulses layer traveling along the springs' stressed links.
18
+ */
19
+ export function impulses({ from, stress, color: headColor, tailColor, frequency = 0.12, speed = 1.5, length = 0.08, }) {
20
+ assert(stress >= 0 && stress <= 1, `impulses stress must be within 0..1, but is ${stress}`);
21
+ assert(frequency >= 0, `impulses frequency must be non-negative, but is ${frequency}`);
22
+ assert(speed > 0, `impulses speed must be positive, but is ${speed}`);
23
+ assert(length > 0 && length <= 1, `impulses length must be within 0..1, but is ${length}`);
24
+ const spec = {
25
+ source: from,
26
+ stress,
27
+ color: headColor,
28
+ tailColor,
29
+ frequency,
30
+ speed,
31
+ length,
32
+ build: () => buildImpulses(spec),
33
+ };
34
+ return spec;
35
+ }
36
+ function hash(index) {
37
+ return (index * GOLDEN) % 1;
38
+ }
39
+ /**
40
+ * Builds the impulses layer: a short additive highlight travels along each
41
+ * stressed link, seeded by link index so it is deterministic. A link with
42
+ * strain below the stress threshold emits nothing.
43
+ */
44
+ function buildImpulses(layer) {
45
+ const links = layer.source.links;
46
+ const buffers = createImpulseBuffers(links.length);
47
+ const material = impulseMaterial();
48
+ const mesh = segmentMesh(buffers.geometry, material);
49
+ const colors = impulseColors(layer);
50
+ return {
51
+ object: mesh,
52
+ update(frame) {
53
+ const strains = layer.source.strains();
54
+ const points = PackedPositions.of(frame.positions);
55
+ for (const [index, link] of links.entries()) {
56
+ const span = impulseSpan({ layer, time: frame.time, index, strain: strainAt(strains, index) });
57
+ writeImpulse({ buffers, colors, index, link, points, span });
58
+ }
59
+ buffers.flush();
60
+ },
61
+ dispose() {
62
+ buffers.geometry.dispose();
63
+ material.dispose();
64
+ },
65
+ };
66
+ }
67
+ function createImpulseBuffers(count) {
68
+ const { geometry, positions } = segmentGeometry(count);
69
+ const colors = segmentAttribute(count, FLOATS_PER_COLOR);
70
+ geometry.setAttribute(COLOR_ATTRIBUTE, colors);
71
+ return {
72
+ geometry,
73
+ positions: positions.array,
74
+ colors: colors.array,
75
+ flush() {
76
+ positions.needsUpdate = true;
77
+ colors.needsUpdate = true;
78
+ },
79
+ };
80
+ }
81
+ function impulseColors(layer) {
82
+ return {
83
+ head: rgbaOf(color.parse(layer.color)),
84
+ tail: rgbaOf(color.parse(layer.tailColor), TAIL_ALPHA),
85
+ };
86
+ }
87
+ function strainAt(strains, index) {
88
+ const strain = strains[index];
89
+ assert(strain !== undefined, `link ${index} has no strain sample`);
90
+ return strain;
91
+ }
92
+ function colorOffset({ index, vertex }) {
93
+ return (index * VERTICES_PER_SEGMENT + vertex) * FLOATS_PER_COLOR;
94
+ }
95
+ function impulseMaterial() {
96
+ const vertexColor = attribute(COLOR_ATTRIBUTE, "vec4");
97
+ return materialFactory.flat({ color: vertexColor.xyz, opacity: vertexColor.w, additive: true });
98
+ }
99
+ function rgbaOf({ r, g, b, a }, alphaScale = 1) {
100
+ return [r, g, b, a * alphaScale];
101
+ }
102
+ function impulseSpan({ layer, time, index, strain, }) {
103
+ if (strain <= layer.stress || layer.frequency <= 0) {
104
+ return { visible: false, tail: 0, head: 0 };
105
+ }
106
+ const head = timeSinceSpawn({ time, frequency: layer.frequency, index }) * layer.speed;
107
+ const tail = head - layer.length;
108
+ if (tail >= 1 || head <= 0) {
109
+ return { visible: false, tail: 0, head: 0 };
110
+ }
111
+ return { visible: true, tail: Math.max(0, tail), head: Math.min(1, head) };
112
+ }
113
+ function timeSinceSpawn({ time, frequency, index }) {
114
+ return ((time * frequency + hash(index)) % 1) / frequency;
115
+ }
116
+ function writeImpulse({ buffers, colors, index, link, points, span, }) {
117
+ if (!span.visible) {
118
+ hideImpulse(buffers, index);
119
+ return;
120
+ }
121
+ const a = points.at(link.between[0]);
122
+ const b = points.at(link.between[1]);
123
+ writeSegment({
124
+ target: buffers.positions,
125
+ segment: index,
126
+ a: pointAlong({ a, b, t: span.tail }),
127
+ b: pointAlong({ a, b, t: span.head }),
128
+ halfWidth: IMPULSE_WIDTH_WORLD / 2,
129
+ });
130
+ for (let vertex = 0; vertex < VERTICES_PER_SEGMENT; vertex++) {
131
+ const rgba = VERTEX_END[vertex] === 1 ? colors.head : colors.tail;
132
+ buffers.colors.set(rgba, colorOffset({ index, vertex }));
133
+ }
134
+ }
135
+ function hideImpulse(buffers, index) {
136
+ writeSegment({ target: buffers.positions, segment: index, a: [0, 0, 0], b: [0, 0, 0], halfWidth: 0 });
137
+ for (let vertex = 0; vertex < VERTICES_PER_SEGMENT; vertex++) {
138
+ buffers.colors[colorOffset({ index, vertex }) + ALPHA_OFFSET] = 0;
139
+ }
140
+ }
141
+ function pointAlong({ a, b, t, }) {
142
+ return [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t, a[2] + (b[2] - a[2]) * t];
143
+ }
144
+ //# sourceMappingURL=impulses.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"impulses.js","sourceRoot":"","sources":["../../../src/render/springs/impulses.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAEtC,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,QAAQ,IAAI,eAAe,EAAE,MAAM,8BAA8B,CAAC;AAC3E,OAAO,EAAc,KAAK,EAAa,MAAM,mBAAmB,CAAC;AACjE,OAAO,EAAc,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAGhE,OAAO,EACL,gBAAgB,EAChB,eAAe,EACf,WAAW,EACX,oBAAoB,EACpB,YAAY,GACb,MAAM,6BAA6B,CAAC;AAGrC,MAAM,mBAAmB,GAAG,KAAK,CAAC;AAClC,MAAM,MAAM,GAAG,iBAAiB,CAAC;AACjC,MAAM,UAAU,GAAG,IAAI,CAAC;AAExB,MAAM,eAAe,GAAG,QAAQ,CAAC;AACjC,MAAM,gBAAgB,GAAG,CAAC,CAAC;AAC3B,MAAM,YAAY,GAAG,CAAC,CAAC;AAEvB,4EAA4E;AAC5E,iEAAiE;AACjE,MAAM,UAAU,GAAsB,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;AAsCzD;;GAEG;AACH,MAAM,UAAU,QAAQ,CAAC,EACvB,IAAI,EACJ,MAAM,EACN,KAAK,EAAE,SAAS,EAChB,SAAS,EACT,SAAS,GAAG,IAAI,EAChB,KAAK,GAAG,GAAG,EACX,MAAM,GAAG,IAAI,GASd;IACC,MAAM,CAAC,MAAM,IAAI,CAAC,IAAI,MAAM,IAAI,CAAC,EAAE,+CAA+C,MAAM,EAAE,CAAC,CAAC;IAC5F,MAAM,CAAC,SAAS,IAAI,CAAC,EAAE,mDAAmD,SAAS,EAAE,CAAC,CAAC;IACvF,MAAM,CAAC,KAAK,GAAG,CAAC,EAAE,2CAA2C,KAAK,EAAE,CAAC,CAAC;IACtE,MAAM,CAAC,MAAM,GAAG,CAAC,IAAI,MAAM,IAAI,CAAC,EAAE,+CAA+C,MAAM,EAAE,CAAC,CAAC;IAC3F,MAAM,IAAI,GAAkB;QAC1B,MAAM,EAAE,IAAI;QACZ,MAAM;QACN,KAAK,EAAE,SAAS;QAChB,SAAS;QACT,SAAS;QACT,KAAK;QACL,MAAM;QACN,KAAK,EAAE,GAAG,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC;KACjC,CAAC;IACF,OAAO,IAAI,CAAC;AACd,CAAC;AAED,SAAS,IAAI,CAAC,KAAa;IACzB,OAAO,CAAC,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;AAC9B,CAAC;AAED;;;;GAIG;AACH,SAAS,aAAa,CAAC,KAAoB;IACzC,MAAM,KAAK,GAAG,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC;IACjC,MAAM,OAAO,GAAG,oBAAoB,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IACnD,MAAM,QAAQ,GAAG,eAAe,EAAE,CAAC;IACnC,MAAM,IAAI,GAAG,WAAW,CAAC,OAAO,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;IACrD,MAAM,MAAM,GAAG,aAAa,CAAC,KAAK,CAAC,CAAC;IAEpC,OAAO;QACL,MAAM,EAAE,IAAI;QACZ,MAAM,CAAC,KAAY;YACjB,MAAM,OAAO,GAAG,KAAK,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;YACvC,MAAM,MAAM,GAAG,eAAe,CAAC,EAAE,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;YACnD,KAAK,MAAM,CAAC,KAAK,EAAE,IAAI,CAAC,IAAI,KAAK,CAAC,OAAO,EAAE,EAAE,CAAC;gBAC5C,MAAM,IAAI,GAAG,WAAW,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,CAAC,OAAO,EAAE,KAAK,CAAC,EAAE,CAAC,CAAC;gBAC/F,YAAY,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;YAC/D,CAAC;YACD,OAAO,CAAC,KAAK,EAAE,CAAC;QAClB,CAAC;QACD,OAAO;YACL,OAAO,CAAC,QAAQ,CAAC,OAAO,EAAE,CAAC;YAC3B,QAAQ,CAAC,OAAO,EAAE,CAAC;QACrB,CAAC;KACF,CAAC;AACJ,CAAC;AASD,SAAS,oBAAoB,CAAC,KAAa;IACzC,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,GAAG,eAAe,CAAC,KAAK,CAAC,CAAC;IACvD,MAAM,MAAM,GAAG,gBAAgB,CAAC,KAAK,EAAE,gBAAgB,CAAC,CAAC;IACzD,QAAQ,CAAC,YAAY,CAAC,eAAe,EAAE,MAAM,CAAC,CAAC;IAC/C,OAAO;QACL,QAAQ;QACR,SAAS,EAAE,SAAS,CAAC,KAAqB;QAC1C,MAAM,EAAE,MAAM,CAAC,KAAqB;QACpC,KAAK;YACH,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC;YAC7B,MAAM,CAAC,WAAW,GAAG,IAAI,CAAC;QAC5B,CAAC;KACF,CAAC;AACJ,CAAC;AAOD,SAAS,aAAa,CAAC,KAAoB;IACzC,OAAO;QACL,IAAI,EAAE,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QACtC,IAAI,EAAE,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,SAAS,CAAC,EAAE,UAAU,CAAC;KACvD,CAAC;AACJ,CAAC;AAED,SAAS,QAAQ,CAAC,OAAqB,EAAE,KAAa;IACpD,MAAM,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC;IAC9B,MAAM,CAAC,MAAM,KAAK,SAAS,EAAE,QAAQ,KAAK,uBAAuB,CAAC,CAAC;IACnE,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,SAAS,WAAW,CAAC,EAAE,KAAK,EAAE,MAAM,EAAqC;IACvE,OAAO,CAAC,KAAK,GAAG,oBAAoB,GAAG,MAAM,CAAC,GAAG,gBAAgB,CAAC;AACpE,CAAC;AAED,SAAS,eAAe;IACtB,MAAM,WAAW,GAAG,SAAS,CAAS,eAAe,EAAE,MAAM,CAAC,CAAC;IAC/D,OAAO,eAAe,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,WAAW,CAAC,GAAG,EAAE,OAAO,EAAE,WAAW,CAAC,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;AAClG,CAAC;AAID,SAAS,MAAM,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAQ,EAAE,UAAU,GAAG,CAAC;IAClD,OAAO,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,GAAG,UAAU,CAAC,CAAC;AACnC,CAAC;AAQD,SAAS,WAAW,CAAC,EACnB,KAAK,EACL,IAAI,EACJ,KAAK,EACL,MAAM,GAMP;IACC,IAAI,MAAM,IAAI,KAAK,CAAC,MAAM,IAAI,KAAK,CAAC,SAAS,IAAI,CAAC,EAAE,CAAC;QACnD,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;IAC9C,CAAC;IACD,MAAM,IAAI,GAAG,cAAc,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,CAAC,SAAS,EAAE,KAAK,EAAE,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC;IACvF,MAAM,IAAI,GAAG,IAAI,GAAG,KAAK,CAAC,MAAM,CAAC;IACjC,IAAI,IAAI,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,EAAE,CAAC;QAC3B,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;IAC9C,CAAC;IACD,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,EAAE,CAAC;AAC7E,CAAC;AAED,SAAS,cAAc,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAsD;IACpG,OAAO,CAAC,CAAC,IAAI,GAAG,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,SAAS,CAAC;AAC5D,CAAC;AAED,SAAS,YAAY,CAAC,EACpB,OAAO,EACP,MAAM,EACN,KAAK,EACL,IAAI,EACJ,MAAM,EACN,IAAI,GAQL;IACC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC;QAClB,WAAW,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;QAC5B,OAAO;IACT,CAAC;IAED,MAAM,CAAC,GAAG,MAAM,CAAC,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;IACrC,MAAM,CAAC,GAAG,MAAM,CAAC,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;IACrC,YAAY,CAAC;QACX,MAAM,EAAE,OAAO,CAAC,SAAS;QACzB,OAAO,EAAE,KAAK;QACd,CAAC,EAAE,UAAU,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC;QACrC,CAAC,EAAE,UAAU,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC;QACrC,SAAS,EAAE,mBAAmB,GAAG,CAAC;KACnC,CAAC,CAAC;IAEH,KAAK,IAAI,MAAM,GAAG,CAAC,EAAE,MAAM,GAAG,oBAAoB,EAAE,MAAM,EAAE,EAAE,CAAC;QAC7D,MAAM,IAAI,GAAG,UAAU,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC;QAClE,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE,WAAW,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC;IAC3D,CAAC;AACH,CAAC;AAED,SAAS,WAAW,CAAC,OAAuB,EAAE,KAAa;IACzD,YAAY,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,SAAS,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,CAAC,CAAC;IACtG,KAAK,IAAI,MAAM,GAAG,CAAC,EAAE,MAAM,GAAG,oBAAoB,EAAE,MAAM,EAAE,EAAE,CAAC;QAC7D,OAAO,CAAC,MAAM,CAAC,WAAW,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,GAAG,YAAY,CAAC,GAAG,CAAC,CAAC;IACpE,CAAC;AACH,CAAC;AAED,SAAS,UAAU,CAAC,EAClB,CAAC,EACD,CAAC,EACD,CAAC,GAKF;IACC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;AACxF,CAAC","sourcesContent":["import { attribute } from \"three/tsl\";\nimport type { BufferGeometry, MeshBasicNodeMaterial } from \"three/webgpu\";\nimport { assert } from \"@/assert.js\";\nimport { material as materialFactory } from \"@/render/backend/material.js\";\nimport { type Color, color, type Rgba } from \"@/render/color.js\";\nimport { type Frame, PackedPositions } from \"@/render/frame.js\";\nimport type { Layer, LayerObject } from \"@/render/layer.js\";\nimport type { Link } from \"@/render/springs/link.js\";\nimport {\n segmentAttribute,\n segmentGeometry,\n segmentMesh,\n VERTICES_PER_SEGMENT,\n writeSegment,\n} from \"@/render/springs/segment.js\";\nimport type { Springs } from \"@/render/springs/source.js\";\n\nconst IMPULSE_WIDTH_WORLD = 0.006;\nconst GOLDEN = 0.618033988749895;\nconst TAIL_ALPHA = 0.25;\n\nconst COLOR_ATTRIBUTE = \"aColor\";\nconst FLOATS_PER_COLOR = 4;\nconst ALPHA_OFFSET = 3;\n\n// Which impulse endpoint each of the six segment vertices sits at: 0 is the\n// tail, 1 is the head. Matches the corner order in writeSegment.\nconst VERTEX_END: readonly number[] = [0, 1, 1, 0, 1, 0];\n\n/**\n * A layer of short-lived impulses that travel along stressed links.\n */\nexport interface ImpulsesLayer extends Layer<Springs> {\n /**\n * The strain, as a fraction of rest length, a link must exceed before it\n * carries impulses.\n */\n readonly stress: number;\n\n /**\n * The color at the impulse head, its leading and brightest point.\n */\n readonly color: Color;\n\n /**\n * The color at the impulse tail, drawn trailing the head at reduced alpha.\n */\n readonly tailColor: Color;\n\n /**\n * The mean rate, per second, at which a qualifying link emits an impulse.\n */\n readonly frequency: number;\n\n /**\n * How fast an impulse travels along its link, in link-lengths per second.\n */\n readonly speed: number;\n\n /**\n * The impulse head-to-tail length, as a fraction of the link length.\n */\n readonly length: number;\n}\n\n/**\n * An impulses layer traveling along the springs' stressed links.\n */\nexport function impulses({\n from,\n stress,\n color: headColor,\n tailColor,\n frequency = 0.12,\n speed = 1.5,\n length = 0.08,\n}: {\n from: Springs;\n stress: number;\n color: Color;\n tailColor: Color;\n frequency?: number;\n speed?: number;\n length?: number;\n}): ImpulsesLayer {\n assert(stress >= 0 && stress <= 1, `impulses stress must be within 0..1, but is ${stress}`);\n assert(frequency >= 0, `impulses frequency must be non-negative, but is ${frequency}`);\n assert(speed > 0, `impulses speed must be positive, but is ${speed}`);\n assert(length > 0 && length <= 1, `impulses length must be within 0..1, but is ${length}`);\n const spec: ImpulsesLayer = {\n source: from,\n stress,\n color: headColor,\n tailColor,\n frequency,\n speed,\n length,\n build: () => buildImpulses(spec),\n };\n return spec;\n}\n\nfunction hash(index: number): number {\n return (index * GOLDEN) % 1;\n}\n\n/**\n * Builds the impulses layer: a short additive highlight travels along each\n * stressed link, seeded by link index so it is deterministic. A link with\n * strain below the stress threshold emits nothing.\n */\nfunction buildImpulses(layer: ImpulsesLayer): LayerObject {\n const links = layer.source.links;\n const buffers = createImpulseBuffers(links.length);\n const material = impulseMaterial();\n const mesh = segmentMesh(buffers.geometry, material);\n const colors = impulseColors(layer);\n\n return {\n object: mesh,\n update(frame: Frame): void {\n const strains = layer.source.strains();\n const points = PackedPositions.of(frame.positions);\n for (const [index, link] of links.entries()) {\n const span = impulseSpan({ layer, time: frame.time, index, strain: strainAt(strains, index) });\n writeImpulse({ buffers, colors, index, link, points, span });\n }\n buffers.flush();\n },\n dispose(): void {\n buffers.geometry.dispose();\n material.dispose();\n },\n };\n}\n\ninterface ImpulseBuffers {\n readonly geometry: BufferGeometry;\n readonly positions: Float32Array;\n readonly colors: Float32Array;\n flush(): void;\n}\n\nfunction createImpulseBuffers(count: number): ImpulseBuffers {\n const { geometry, positions } = segmentGeometry(count);\n const colors = segmentAttribute(count, FLOATS_PER_COLOR);\n geometry.setAttribute(COLOR_ATTRIBUTE, colors);\n return {\n geometry,\n positions: positions.array as Float32Array,\n colors: colors.array as Float32Array,\n flush(): void {\n positions.needsUpdate = true;\n colors.needsUpdate = true;\n },\n };\n}\n\ninterface ImpulseColors {\n readonly head: Quad;\n readonly tail: Quad;\n}\n\nfunction impulseColors(layer: ImpulsesLayer): ImpulseColors {\n return {\n head: rgbaOf(color.parse(layer.color)),\n tail: rgbaOf(color.parse(layer.tailColor), TAIL_ALPHA),\n };\n}\n\nfunction strainAt(strains: Float32Array, index: number): number {\n const strain = strains[index];\n assert(strain !== undefined, `link ${index} has no strain sample`);\n return strain;\n}\n\nfunction colorOffset({ index, vertex }: { index: number; vertex: number }): number {\n return (index * VERTICES_PER_SEGMENT + vertex) * FLOATS_PER_COLOR;\n}\n\nfunction impulseMaterial(): MeshBasicNodeMaterial {\n const vertexColor = attribute<\"vec4\">(COLOR_ATTRIBUTE, \"vec4\");\n return materialFactory.flat({ color: vertexColor.xyz, opacity: vertexColor.w, additive: true });\n}\n\ntype Quad = readonly [number, number, number, number];\n\nfunction rgbaOf({ r, g, b, a }: Rgba, alphaScale = 1): Quad {\n return [r, g, b, a * alphaScale];\n}\n\ninterface Span {\n readonly visible: boolean;\n readonly tail: number;\n readonly head: number;\n}\n\nfunction impulseSpan({\n layer,\n time,\n index,\n strain,\n}: {\n layer: ImpulsesLayer;\n time: number;\n index: number;\n strain: number;\n}): Span {\n if (strain <= layer.stress || layer.frequency <= 0) {\n return { visible: false, tail: 0, head: 0 };\n }\n const head = timeSinceSpawn({ time, frequency: layer.frequency, index }) * layer.speed;\n const tail = head - layer.length;\n if (tail >= 1 || head <= 0) {\n return { visible: false, tail: 0, head: 0 };\n }\n return { visible: true, tail: Math.max(0, tail), head: Math.min(1, head) };\n}\n\nfunction timeSinceSpawn({ time, frequency, index }: { time: number; frequency: number; index: number }): number {\n return ((time * frequency + hash(index)) % 1) / frequency;\n}\n\nfunction writeImpulse({\n buffers,\n colors,\n index,\n link,\n points,\n span,\n}: {\n buffers: ImpulseBuffers;\n colors: ImpulseColors;\n index: number;\n link: Link;\n points: PackedPositions;\n span: Span;\n}): void {\n if (!span.visible) {\n hideImpulse(buffers, index);\n return;\n }\n\n const a = points.at(link.between[0]);\n const b = points.at(link.between[1]);\n writeSegment({\n target: buffers.positions,\n segment: index,\n a: pointAlong({ a, b, t: span.tail }),\n b: pointAlong({ a, b, t: span.head }),\n halfWidth: IMPULSE_WIDTH_WORLD / 2,\n });\n\n for (let vertex = 0; vertex < VERTICES_PER_SEGMENT; vertex++) {\n const rgba = VERTEX_END[vertex] === 1 ? colors.head : colors.tail;\n buffers.colors.set(rgba, colorOffset({ index, vertex }));\n }\n}\n\nfunction hideImpulse(buffers: ImpulseBuffers, index: number): void {\n writeSegment({ target: buffers.positions, segment: index, a: [0, 0, 0], b: [0, 0, 0], halfWidth: 0 });\n for (let vertex = 0; vertex < VERTICES_PER_SEGMENT; vertex++) {\n buffers.colors[colorOffset({ index, vertex }) + ALPHA_OFFSET] = 0;\n }\n}\n\nfunction pointAlong({\n a,\n b,\n t,\n}: {\n a: readonly [number, number, number];\n b: readonly [number, number, number];\n t: number;\n}): readonly [number, number, number] {\n return [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t, a[2] + (b[2] - a[2]) * t];\n}\n"]}
@@ -0,0 +1,9 @@
1
+ export type { AnchorsLayer } from "../../render/springs/anchors.js";
2
+ export type { ConstructionLayer } from "../../render/springs/construction.js";
3
+ export type { ImpulsesLayer } from "../../render/springs/impulses.js";
4
+ export type { Link, LinkGroup, LinkSpecification } from "../../render/springs/link.js";
5
+ export type { FractureOptions, LinksLayer } from "../../render/springs/links.js";
6
+ export type { Palette } from "../../render/springs/palette.js";
7
+ export type { FractureMode, Preset, PresetOptions } from "../../render/springs/preset.js";
8
+ export type { Springs } from "../../render/springs/source.js";
9
+ export { springs } from "../../render/springs/springs.js";
@@ -0,0 +1,2 @@
1
+ export { springs } from "../../render/springs/springs.js";
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/render/springs/index.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,OAAO,EAAE,MAAM,6BAA6B,CAAC","sourcesContent":["export type { AnchorsLayer } from \"@/render/springs/anchors.js\";\nexport type { ConstructionLayer } from \"@/render/springs/construction.js\";\nexport type { ImpulsesLayer } from \"@/render/springs/impulses.js\";\nexport type { Link, LinkGroup, LinkSpecification } from \"@/render/springs/link.js\";\nexport type { FractureOptions, LinksLayer } from \"@/render/springs/links.js\";\nexport type { Palette } from \"@/render/springs/palette.js\";\nexport type { FractureMode, Preset, PresetOptions } from \"@/render/springs/preset.js\";\nexport type { Springs } from \"@/render/springs/source.js\";\nexport { springs } from \"@/render/springs/springs.js\";\n"]}
@@ -0,0 +1,51 @@
1
+ /**
2
+ * A link's classification, `pinned` if it touches an anchor and `free`
3
+ * otherwise.
4
+ */
5
+ export type LinkGroup = "pinned" | "free";
6
+ /**
7
+ * A single link to draw between two particles, before `springs.of` classifies
8
+ * it into a group and stores it as a {@link Link}.
9
+ */
10
+ export interface LinkSpecification {
11
+ /**
12
+ * The pair of particle indices the link joins. Order does not affect
13
+ * rendering.
14
+ */
15
+ readonly between: readonly [number, number];
16
+ /**
17
+ * The length at which the link is unstrained. When omitted, the renderer
18
+ * treats the link's first observed length as its rest length.
19
+ */
20
+ readonly restLength?: number;
21
+ /**
22
+ * Forces this link's group rather than deriving it from the anchors. When
23
+ * omitted, `springs.of` classifies the link.
24
+ */
25
+ readonly group?: LinkGroup;
26
+ }
27
+ /**
28
+ * A link after `springs.of` has classified it: a specification whose group is
29
+ * resolved.
30
+ */
31
+ export interface Link extends LinkSpecification {
32
+ /**
33
+ * The group the link resolved to, always present after classification.
34
+ */
35
+ readonly group: LinkGroup;
36
+ }
37
+ /**
38
+ * Checks that every link joins two distinct non-negative particle indices at a
39
+ * positive rest length.
40
+ */
41
+ export declare function validateLinks(links: readonly LinkSpecification[]): void;
42
+ /**
43
+ * Collects anchor particle indices into a set, checking each one.
44
+ */
45
+ export declare function anchorsFrom(particles: readonly number[] | undefined): ReadonlySet<number>;
46
+ /**
47
+ * Assigns a group to every link that does not already carry one: a link
48
+ * incident to an anchor becomes `pinned`, and every other link `free`. Links
49
+ * with an explicit group are returned unchanged.
50
+ */
51
+ export declare function classifyLinks(links: readonly LinkSpecification[], anchors: ReadonlySet<number>): readonly Link[];
@@ -0,0 +1,42 @@
1
+ import { assert } from "../../assert.js";
2
+ /**
3
+ * Checks that every link joins two distinct non-negative particle indices at a
4
+ * positive rest length.
5
+ */
6
+ export function validateLinks(links) {
7
+ for (const [index, { between, restLength }] of links.entries()) {
8
+ const [i, j] = between;
9
+ assert(Number.isInteger(i) && i >= 0, `link ${index} endpoint ${i} must be a non-negative integer`);
10
+ assert(Number.isInteger(j) && j >= 0, `link ${index} endpoint ${j} must be a non-negative integer`);
11
+ assert(i !== j, `link ${index} joins particle ${i} to itself`);
12
+ assert(restLength === undefined || (Number.isFinite(restLength) && restLength > 0), `link ${index} rest length must be positive and finite, but is ${restLength}`);
13
+ }
14
+ }
15
+ /**
16
+ * Collects anchor particle indices into a set, checking each one.
17
+ */
18
+ export function anchorsFrom(particles) {
19
+ if (particles === undefined) {
20
+ return new Set();
21
+ }
22
+ for (const [index, particle] of particles.entries()) {
23
+ assert(Number.isInteger(particle) && particle >= 0, `anchor ${index} must be a non-negative integer, but is ${particle}`);
24
+ }
25
+ return new Set(particles);
26
+ }
27
+ /**
28
+ * Assigns a group to every link that does not already carry one: a link
29
+ * incident to an anchor becomes `pinned`, and every other link `free`. Links
30
+ * with an explicit group are returned unchanged.
31
+ */
32
+ export function classifyLinks(links, anchors) {
33
+ return links.map((link) => {
34
+ if (link.group !== undefined) {
35
+ return { ...link, group: link.group };
36
+ }
37
+ const [i, j] = link.between;
38
+ const group = anchors.has(i) || anchors.has(j) ? "pinned" : "free";
39
+ return { ...link, group };
40
+ });
41
+ }
42
+ //# sourceMappingURL=link.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"link.js","sourceRoot":"","sources":["../../../src/render/springs/link.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAyCrC;;;GAGG;AACH,MAAM,UAAU,aAAa,CAAC,KAAmC;IAC/D,KAAK,MAAM,CAAC,KAAK,EAAE,EAAE,OAAO,EAAE,UAAU,EAAE,CAAC,IAAI,KAAK,CAAC,OAAO,EAAE,EAAE,CAAC;QAC/D,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,OAAO,CAAC;QACvB,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,QAAQ,KAAK,aAAa,CAAC,iCAAiC,CAAC,CAAC;QACpG,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,QAAQ,KAAK,aAAa,CAAC,iCAAiC,CAAC,CAAC;QACpG,MAAM,CAAC,CAAC,KAAK,CAAC,EAAE,QAAQ,KAAK,mBAAmB,CAAC,YAAY,CAAC,CAAC;QAC/D,MAAM,CACJ,UAAU,KAAK,SAAS,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,UAAU,CAAC,IAAI,UAAU,GAAG,CAAC,CAAC,EAC3E,QAAQ,KAAK,oDAAoD,UAAU,EAAE,CAC9E,CAAC;IACJ,CAAC;AACH,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,WAAW,CAAC,SAAwC;IAClE,IAAI,SAAS,KAAK,SAAS,EAAE,CAAC;QAC5B,OAAO,IAAI,GAAG,EAAE,CAAC;IACnB,CAAC;IACD,KAAK,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,IAAI,SAAS,CAAC,OAAO,EAAE,EAAE,CAAC;QACpD,MAAM,CACJ,MAAM,CAAC,SAAS,CAAC,QAAQ,CAAC,IAAI,QAAQ,IAAI,CAAC,EAC3C,UAAU,KAAK,2CAA2C,QAAQ,EAAE,CACrE,CAAC;IACJ,CAAC;IACD,OAAO,IAAI,GAAG,CAAC,SAAS,CAAC,CAAC;AAC5B,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAAC,KAAmC,EAAE,OAA4B;IAC7F,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE;QACxB,IAAI,IAAI,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;YAC7B,OAAO,EAAE,GAAG,IAAI,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,CAAC;QACxC,CAAC;QACD,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC;QAC5B,MAAM,KAAK,GAAc,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC;QAC9E,OAAO,EAAE,GAAG,IAAI,EAAE,KAAK,EAAE,CAAC;IAC5B,CAAC,CAAC,CAAC;AACL,CAAC","sourcesContent":["import { assert } from \"@/assert.js\";\n\n/**\n * A link's classification, `pinned` if it touches an anchor and `free`\n * otherwise.\n */\nexport type LinkGroup = \"pinned\" | \"free\";\n\n/**\n * A single link to draw between two particles, before `springs.of` classifies\n * it into a group and stores it as a {@link Link}.\n */\nexport interface LinkSpecification {\n /**\n * The pair of particle indices the link joins. Order does not affect\n * rendering.\n */\n readonly between: readonly [number, number];\n /**\n * The length at which the link is unstrained. When omitted, the renderer\n * treats the link's first observed length as its rest length.\n */\n readonly restLength?: number;\n /**\n * Forces this link's group rather than deriving it from the anchors. When\n * omitted, `springs.of` classifies the link.\n */\n readonly group?: LinkGroup;\n}\n\n/**\n * A link after `springs.of` has classified it: a specification whose group is\n * resolved.\n */\nexport interface Link extends LinkSpecification {\n /**\n * The group the link resolved to, always present after classification.\n */\n readonly group: LinkGroup;\n}\n\n/**\n * Checks that every link joins two distinct non-negative particle indices at a\n * positive rest length.\n */\nexport function validateLinks(links: readonly LinkSpecification[]): void {\n for (const [index, { between, restLength }] of links.entries()) {\n const [i, j] = between;\n assert(Number.isInteger(i) && i >= 0, `link ${index} endpoint ${i} must be a non-negative integer`);\n assert(Number.isInteger(j) && j >= 0, `link ${index} endpoint ${j} must be a non-negative integer`);\n assert(i !== j, `link ${index} joins particle ${i} to itself`);\n assert(\n restLength === undefined || (Number.isFinite(restLength) && restLength > 0),\n `link ${index} rest length must be positive and finite, but is ${restLength}`,\n );\n }\n}\n\n/**\n * Collects anchor particle indices into a set, checking each one.\n */\nexport function anchorsFrom(particles: readonly number[] | undefined): ReadonlySet<number> {\n if (particles === undefined) {\n return new Set();\n }\n for (const [index, particle] of particles.entries()) {\n assert(\n Number.isInteger(particle) && particle >= 0,\n `anchor ${index} must be a non-negative integer, but is ${particle}`,\n );\n }\n return new Set(particles);\n}\n\n/**\n * Assigns a group to every link that does not already carry one: a link\n * incident to an anchor becomes `pinned`, and every other link `free`. Links\n * with an explicit group are returned unchanged.\n */\nexport function classifyLinks(links: readonly LinkSpecification[], anchors: ReadonlySet<number>): readonly Link[] {\n return links.map((link) => {\n if (link.group !== undefined) {\n return { ...link, group: link.group };\n }\n const [i, j] = link.between;\n const group: LinkGroup = anchors.has(i) || anchors.has(j) ? \"pinned\" : \"free\";\n return { ...link, group };\n });\n}\n"]}