@field/cascade 0.10.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +25 -5
- package/ARCHITECTURE.md +5 -4
- package/CHANGELOG.md +382 -0
- package/README.md +1 -2
- package/dist/assets/AgentPanel-1JugFjE4.css +1 -0
- package/dist/assets/AgentPanel-BDL5MddA.js +5 -0
- package/dist/assets/{CodeEditor-DSZBpzdy.js → CodeEditor-C-xYG-Wn.js} +120 -120
- package/dist/assets/CodePanel-CtXd50m6.js +2 -0
- package/dist/assets/CookInfoPanel-B3NMhKJF.css +1 -0
- package/dist/assets/CookInfoPanel-DG4_TRsX.js +2 -0
- package/dist/assets/CustomNodeDialog-CHfZUs-r.js +1 -0
- package/dist/assets/DefinitionNode-CaRTXQ_b.js +1 -0
- package/dist/assets/DefinitionPanel-C3CxZY9r.js +1 -0
- package/dist/assets/DefinitionPanel-VNLnAiQD.css +1 -0
- package/dist/assets/OpenGraphDialog-BLdr6k_q.js +1 -0
- package/dist/assets/ProjectSettings-ZnvmIAir.js +2 -0
- package/dist/assets/{SettingsDialog-p25h3tj0.js → SettingsDialog-CBX3Wa87.js} +1 -1
- package/dist/assets/TimelinePanel-CDeR0arY.css +1 -0
- package/dist/assets/TimelinePanel-CWgnFH1e.js +1 -0
- package/dist/assets/{VersionHistoryDialog-DzFLUotB.js → VersionHistoryDialog-BOSoW1R-.js} +1 -1
- package/dist/assets/assert-BO2_erPD.js +1 -0
- package/dist/assets/assert-hvykGCnP.js +1 -0
- package/dist/assets/builder-BmBKcDix.js +1 -0
- package/dist/assets/camera-DFF7YA3F.js +1 -0
- package/dist/assets/checks-DAvyO9Gf.js +1 -0
- package/dist/assets/contactSheet-N1NcaWxw.js +2 -0
- package/dist/assets/{core-BICrEt0y.js → core-DB-MkGjj.js} +3 -4
- package/dist/assets/coreTypes-DtnDATZ7.js +1 -0
- package/dist/assets/{cssMode-D7TXw57I.js → cssMode-DPVckSOc.js} +1 -1
- package/dist/assets/definitions-B227e7f_.js +1 -0
- package/dist/assets/definitions-D0WOUJd7.js +1 -0
- package/dist/assets/{editor.api-DvkCO3ko.js → editor.api-Dkutg_Z1.js} +1 -1
- package/dist/assets/expressions-X-Z-GrgW.js +5 -0
- package/dist/assets/file-DAL6IfjV.js +1 -0
- package/dist/assets/filters-D3k1LRMa.js +1 -0
- package/dist/assets/{freemarker2-BA2lxsF1.js → freemarker2-CLiLKk3B.js} +1 -1
- package/dist/assets/generators-BhDxRAFC.js +1 -0
- package/dist/assets/geo-BsMImJIJ.js +1 -0
- package/dist/assets/geo-C4YngDob.js +2 -0
- package/dist/assets/{handlebars-BcDiyPd8.js → handlebars-CcD3aMeL.js} +1 -1
- package/dist/assets/{html-C7O4TyhX.js → html-DvZWGfqb.js} +1 -1
- package/dist/assets/{htmlMode-WaDGF3Mj.js → htmlMode-DUD3VaoN.js} +1 -1
- package/dist/assets/image-0SfH-snh.js +1 -0
- package/dist/assets/image-Ck-RdGU4.js +2 -0
- package/dist/assets/index-BWnrmIB9.js +137 -0
- package/dist/assets/index-DIaxEONI.css +1 -0
- package/dist/assets/io-BGmt2g0D.js +1 -0
- package/dist/assets/{javascript-CpIEK7tz.js → javascript-Cap2ahgh.js} +1 -1
- package/dist/assets/{jsonMode-B2FltW0f.js → jsonMode-B0uLjp9C.js} +1 -1
- package/dist/assets/{liquid-CN6FFHZE.js → liquid-CnxwhfLn.js} +1 -1
- package/dist/assets/{lspLanguageFeatures-QhV9XtLp.js → lspLanguageFeatures-Dn9hlKX6.js} +1 -1
- package/dist/assets/math-GeULS-sk.js +1 -0
- package/dist/assets/{mdx-DE89pafV.js → mdx-BV3jbrVa.js} +1 -1
- package/dist/assets/pop-KMF_qloi.js +1 -0
- package/dist/assets/preload-helper-Czpn1I53.js +1 -0
- package/dist/assets/{python-ByTllTB9.js → python-CGyFk4XK.js} +1 -1
- package/dist/assets/{razor-USYlRRCE.js → razor-CQfzX3-N.js} +1 -1
- package/dist/assets/remap-5SjXQU-q.js +1 -0
- package/dist/assets/renderMarkdown-ork0Oir7.js +67 -0
- package/dist/assets/studioImages-CAyH-0Pl.js +1 -0
- package/dist/assets/text-ZpX7qNkx.js +2 -0
- package/dist/assets/time-P_OiBhcf.js +1 -0
- package/dist/assets/{tsMode-BXpN8iUz.js → tsMode-BOeXzO9H.js} +1 -1
- package/dist/assets/{typescript-DInBjGQQ.js → typescript-J1SnE_Tu.js} +1 -1
- package/dist/assets/{workers-C7Ye5B7D.js → workers-CYwwTiVF.js} +1 -1
- package/dist/assets/{xml-U0Uocqyo.js → xml-DnPRNzK9.js} +1 -1
- package/dist/assets/{yaml-CBw_PVYf.js → yaml-BmmYBNa9.js} +1 -1
- package/dist/cli/DefinitionNode-P7AA4TAU.js +15 -0
- package/dist/cli/Graph-OB6EWSGL.js +2975 -0
- package/dist/cli/assert-GROGQNCQ.js +25 -0
- package/dist/cli/checks-3WB44XBL.js +189 -0
- package/dist/cli/chunk-2JEU3MGB.js +1125 -0
- package/dist/cli/chunk-2KXB47FP.js +115 -0
- package/dist/cli/chunk-3RXL5GHB.js +816 -0
- package/dist/cli/chunk-53LGFPWX.js +138 -0
- package/dist/cli/chunk-5ORUQ6ES.js +267 -0
- package/dist/cli/chunk-73S4JZXK.js +336 -0
- package/dist/cli/chunk-7QE2ZNSN.js +121 -0
- package/dist/cli/chunk-BP7UKF36.js +376 -0
- package/dist/cli/chunk-BPODRH52.js +201 -0
- package/dist/cli/chunk-BZ63XQ3I.js +1301 -0
- package/dist/cli/chunk-CKS2FQWR.js +89 -0
- package/dist/cli/chunk-CO22XHNG.js +593 -0
- package/dist/cli/chunk-CTPLRSWF.js +2197 -0
- package/dist/cli/chunk-DEQSEFFH.js +153 -0
- package/dist/cli/chunk-EU4ZFZ7Y.js +122 -0
- package/dist/cli/chunk-EZRWJ4G2.js +104 -0
- package/dist/cli/chunk-FWMSGKEH.js +71 -0
- package/dist/cli/chunk-G23A2BZZ.js +705 -0
- package/dist/cli/chunk-GRIN2DDP.js +1465 -0
- package/dist/cli/chunk-HFHWYL55.js +139 -0
- package/dist/cli/chunk-HSNOBIVT.js +263 -0
- package/dist/cli/chunk-ICH7N5PN.js +26 -0
- package/dist/cli/chunk-J7VK7MNF.js +418 -0
- package/dist/cli/chunk-JVOCK5W4.js +83 -0
- package/dist/cli/chunk-KBGUO27Q.js +15 -0
- package/dist/cli/chunk-KH4MUBKF.js +1551 -0
- package/dist/cli/chunk-L6B6FUQP.js +330 -0
- package/dist/cli/chunk-LTXWBXOM.js +438 -0
- package/dist/cli/chunk-MI5JLQYA.js +220 -0
- package/dist/cli/chunk-MP653A5O.js +3766 -0
- package/dist/cli/chunk-NJFMDXHR.js +320 -0
- package/dist/cli/chunk-OM6BEME6.js +267 -0
- package/dist/cli/chunk-OWWBYLHC.js +136 -0
- package/dist/cli/chunk-QQPA2ZHH.js +273 -0
- package/dist/cli/chunk-RUU3YWXU.js +99 -0
- package/dist/cli/chunk-RXDIMXN5.js +98 -0
- package/dist/cli/chunk-ST6LROUG.js +267 -0
- package/dist/cli/chunk-TJW67QSI.js +225 -0
- package/dist/cli/chunk-WD62JIP6.js +118 -0
- package/dist/cli/chunk-WGKPJZDY.js +364 -0
- package/dist/cli/chunk-X4WKAOQP.js +634 -0
- package/dist/cli/chunk-X566FWBV.js +156 -0
- package/dist/cli/chunk-YH637PDL.js +317 -0
- package/dist/cli/chunk-ZL4JTG5Y.js +37 -0
- package/dist/cli/chunk-ZSCUZE6I.js +112 -0
- package/dist/cli/cliCommands-JJN5Z3ZC.js +3470 -0
- package/dist/cli/commands-PHYFYA3F.js +1374 -0
- package/dist/cli/contactSheet-2PQVRUFW.js +11 -0
- package/dist/cli/definitions-BPOPW52L.js +32 -0
- package/dist/cli/doctorCommand-ORWQRNYP.js +106 -0
- package/dist/cli/edit-S5KEQ5RA.js +28 -0
- package/dist/cli/editorCommands-NFR32JOY.js +41 -0
- package/dist/cli/encode-ICPEPMH5.js +17 -0
- package/dist/cli/file-3HTQHPYQ.js +41 -0
- package/dist/cli/filters-6ODFWYK4.js +379 -0
- package/dist/cli/frames-LSAUBZ7P.js +16 -0
- package/dist/cli/generators-YU7GB4MD.js +210 -0
- package/dist/cli/geo-6ZPGKZRU.js +43 -0
- package/dist/cli/graphCommands-EOUOBOIJ.js +184 -0
- package/dist/cli/graphFile-YTGJB5F5.js +292 -0
- package/dist/cli/headlessCanvas-ENRHC7GS.js +16 -0
- package/dist/cli/helpCommand-PULH6ZYB.js +157 -0
- package/dist/cli/image-TXELZZK5.js +55 -0
- package/dist/cli/index.js +100 -30756
- package/dist/cli/math-RSYRQ3C6.js +16 -0
- package/dist/cli/mcp-JVZFQOEX.js +164 -0
- package/dist/cli/parity-X6DEH2DV.js +334 -0
- package/dist/cli/pop-B7SIIYKN.js +39 -0
- package/dist/cli/project-VYGIA7JX.js +12 -0
- package/dist/cli/projectDefinition-F3D7UIHJ.js +27 -0
- package/dist/cli/projectRuntime-2Q4JKEH3.js +55 -0
- package/dist/cli/registerStandardNodes-BXJCB6E4.js +744 -0
- package/dist/cli/render-PT4MWI6U.js +438 -0
- package/dist/cli/runCommand-HNPQVZMS.js +198 -0
- package/dist/cli/runner-5XPXVW7J.js +43 -0
- package/dist/cli/runtime/image.ts +302 -0
- package/dist/cli/runtime/io.ts +78 -0
- package/dist/cli/runtime/stage.ts +4 -1
- package/dist/cli/share-MHF6TNRK.js +166 -0
- package/dist/cli/stageCache-TY2BFZJR.js +15 -0
- package/dist/cli/stageRunner-KY2EJP7X.js +10 -0
- package/dist/cli/text-MB3EKR4H.js +126 -0
- package/dist/cli/verbs-PI2JH5FI.js +19 -0
- package/dist/cli/webBuild-WPPMGGW7.js +44 -0
- package/dist/contracts/capabilities.d.ts +18 -1
- package/dist/contracts/color.d.ts +8 -0
- package/dist/contracts/color.js +23 -0
- package/dist/contracts/definition.d.ts +10 -0
- package/dist/contracts/documentFormat.d.ts +18 -0
- package/dist/contracts/documentFormat.js +195 -0
- package/dist/contracts/editor.d.ts +17 -0
- package/dist/contracts/index.d.ts +1 -0
- package/dist/contracts/index.js +1 -0
- package/dist/contracts/schema.d.ts +1 -1
- package/dist/contracts/schema.js +1 -0
- package/dist/contracts/validate.js +11 -1
- package/dist/contracts/values.d.ts +24 -0
- package/dist/image.d.ts +156 -0
- package/dist/image.js +161 -0
- package/dist/index.html +8 -2
- package/dist/io.d.ts +29 -0
- package/dist/io.js +40 -0
- package/dist/player/app.js +3 -2
- package/dist/player/editor.js +35 -9
- package/dist/player/editorEngine.js +3 -2
- package/dist/runtime/builtins/assert/checks.d.ts +18 -0
- package/dist/runtime/builtins/assert/checks.js +207 -0
- package/dist/runtime/builtins/assert/definitions.d.ts +130 -0
- package/dist/runtime/builtins/assert/definitions.js +54 -0
- package/dist/runtime/builtins/assert/index.d.ts +152 -0
- package/dist/runtime/builtins/assert/index.js +36 -0
- package/dist/runtime/builtins/image/contactSheet.d.ts +64 -0
- package/dist/runtime/builtins/image/contactSheet.js +150 -0
- package/dist/runtime/builtins/image/definitions.d.ts +950 -0
- package/dist/runtime/builtins/image/definitions.js +343 -0
- package/dist/runtime/builtins/image/file.d.ts +8 -0
- package/dist/runtime/builtins/image/file.js +37 -0
- package/dist/runtime/builtins/image/filters.d.ts +8 -0
- package/dist/runtime/builtins/image/filters.js +394 -0
- package/dist/runtime/builtins/image/generators.d.ts +7 -0
- package/dist/runtime/builtins/image/generators.js +144 -0
- package/dist/runtime/builtins/image/index.d.ts +952 -0
- package/dist/runtime/builtins/image/index.js +44 -0
- package/dist/runtime/builtins/image/io.d.ts +100 -0
- package/dist/runtime/builtins/image/io.js +142 -0
- package/dist/runtime/builtins/image/planar.d.ts +55 -0
- package/dist/runtime/builtins/image/planar.js +274 -0
- package/dist/runtime/builtins/image/simplex.d.ts +22 -0
- package/dist/runtime/builtins/image/simplex.js +92 -0
- package/dist/runtime/builtins/image/text.d.ts +4 -0
- package/dist/runtime/builtins/image/text.js +141 -0
- package/dist/runtime/builtins/index.d.ts +3 -0
- package/dist/runtime/builtins/index.js +7 -0
- package/dist/runtime/gpu.d.ts +25 -0
- package/dist/runtime/gpu.js +40 -0
- package/dist/runtime/runtime.d.ts +2 -0
- package/dist/runtime/runtime.js +268 -44
- package/dist/runtime/types.d.ts +30 -0
- package/dist/stage.js +4 -1
- package/doc/CLI_REFERENCE.md +91 -2
- package/doc/EDITOR_EXPORT.md +37 -2
- package/doc/MCP.md +18 -1
- package/doc/NODE_REFERENCE.md +424 -10
- package/doc/PROJECT_AUTHORING.md +222 -7
- package/package.json +19 -4
- package/dist/assets/CodePanel-sY6sZa7B.js +0 -2
- package/dist/assets/CustomNodeDialog-BRKnqZhp.js +0 -1
- package/dist/assets/ExportDialog-C7dVGua4.js +0 -358
- package/dist/assets/ExportDialog-DOq0flMV.css +0 -1
- package/dist/assets/OpenGraphDialog-ZsgL-iwr.js +0 -1
- package/dist/assets/ProjectSettings-Cw17nNe3.js +0 -2
- package/dist/assets/definitions-DUww2Rt-.js +0 -1
- package/dist/assets/geo-C2RHOVHC.js +0 -1
- package/dist/assets/image-DR4oeyJB.js +0 -2062
- package/dist/assets/index-7vae6YFC.css +0 -1
- package/dist/assets/index-DyvDC6pS.js +0 -219
- package/dist/assets/math-DaDEgWKt.js +0 -1
- package/dist/assets/pop-pLH7L0PW.js +0 -1
|
@@ -0,0 +1,1551 @@
|
|
|
1
|
+
import {
|
|
2
|
+
GeometryBuilder,
|
|
3
|
+
createAttribute
|
|
4
|
+
} from "./chunk-HSNOBIVT.js";
|
|
5
|
+
import {
|
|
6
|
+
POSITION_ATTRIBUTE
|
|
7
|
+
} from "./chunk-J7VK7MNF.js";
|
|
8
|
+
|
|
9
|
+
// packages/contracts/src/particles.ts
|
|
10
|
+
var P = {
|
|
11
|
+
name: POSITION_ATTRIBUTE,
|
|
12
|
+
level: "point",
|
|
13
|
+
storage: "f64",
|
|
14
|
+
size: [2, 3],
|
|
15
|
+
required: true,
|
|
16
|
+
note: "Position. Shared with every other geometry node, not a particle attribute."
|
|
17
|
+
};
|
|
18
|
+
var PARTICLE_ATTRIBUTES = Object.freeze([
|
|
19
|
+
P,
|
|
20
|
+
{
|
|
21
|
+
name: "v",
|
|
22
|
+
level: "point",
|
|
23
|
+
storage: "f32",
|
|
24
|
+
size: [2, 3],
|
|
25
|
+
required: true,
|
|
26
|
+
note: "Velocity, in units per second. f32 rather than f64: a velocity is not a coordinate, so the precision argument for P does not carry over."
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
name: "age",
|
|
30
|
+
level: "point",
|
|
31
|
+
storage: "f32",
|
|
32
|
+
size: 1,
|
|
33
|
+
required: true,
|
|
34
|
+
units: "seconds",
|
|
35
|
+
// The one place this departs from the sketch it is modelled on.
|
|
36
|
+
note: "Seconds, not frames. A frame count changes meaning with fps, and a graph whose particles die at a different point when rendered at 60 is not deterministic in any useful sense."
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
name: "life",
|
|
40
|
+
level: "point",
|
|
41
|
+
storage: "f32",
|
|
42
|
+
size: 1,
|
|
43
|
+
required: true,
|
|
44
|
+
units: "seconds",
|
|
45
|
+
note: "Life expectancy in seconds. A particle dies when age >= life."
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: "id",
|
|
49
|
+
level: "point",
|
|
50
|
+
storage: "i32",
|
|
51
|
+
size: 1,
|
|
52
|
+
required: true,
|
|
53
|
+
note: "Stable across frames and across kills. Mandatory: a particle array reorders on every kill, so anything durable and per-particle must key on this rather than on an index."
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
name: "nextid",
|
|
57
|
+
level: "detail",
|
|
58
|
+
storage: "i32",
|
|
59
|
+
size: 1,
|
|
60
|
+
required: true,
|
|
61
|
+
note: "The next id to hand out. Houdini's own mechanism for keeping id monotonic, and what stops an id being reused after a kill."
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
name: "Cd",
|
|
65
|
+
level: "point",
|
|
66
|
+
storage: "f32",
|
|
67
|
+
size: 4,
|
|
68
|
+
required: false,
|
|
69
|
+
note: "Colour. Owned by the style vocabulary in PLAN geometry, which refuses a Cd that is a string or numeric with any size but four."
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
name: "pscale",
|
|
73
|
+
level: "point",
|
|
74
|
+
storage: "f32",
|
|
75
|
+
size: 1,
|
|
76
|
+
required: false,
|
|
77
|
+
note: "Not a POP attribute in Houdini's sense. Writing it is what makes CopyToPoints scale its instances, which is how oriented marks get drawn without a new node."
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
name: "N",
|
|
81
|
+
level: "point",
|
|
82
|
+
storage: "f32",
|
|
83
|
+
size: [2, 3],
|
|
84
|
+
required: false,
|
|
85
|
+
note: "Orientation, read by CopyToPoints alongside pscale."
|
|
86
|
+
}
|
|
87
|
+
]);
|
|
88
|
+
var BY_NAME = new Map(
|
|
89
|
+
PARTICLE_ATTRIBUTES.map((spec) => [spec.name, spec])
|
|
90
|
+
);
|
|
91
|
+
function sizeMatches(spec, size) {
|
|
92
|
+
return typeof spec.size === "number" ? size === spec.size : size >= spec.size[0] && size <= spec.size[1];
|
|
93
|
+
}
|
|
94
|
+
function describeSize(spec) {
|
|
95
|
+
return typeof spec.size === "number" ? String(spec.size) : `${spec.size[0]} or ${spec.size[1]}`;
|
|
96
|
+
}
|
|
97
|
+
function checkParticleContract(geometry) {
|
|
98
|
+
const violations = [];
|
|
99
|
+
for (const spec of PARTICLE_ATTRIBUTES) {
|
|
100
|
+
if (spec.level === "detail") {
|
|
101
|
+
const value = geometry.detail[spec.name];
|
|
102
|
+
if (value === void 0) {
|
|
103
|
+
if (spec.required) {
|
|
104
|
+
violations.push({
|
|
105
|
+
attribute: spec.name,
|
|
106
|
+
reason: `missing detail attribute ${spec.name} \u2014 ${spec.note}`
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (typeof value !== "number") {
|
|
112
|
+
violations.push({
|
|
113
|
+
attribute: spec.name,
|
|
114
|
+
reason: `detail ${spec.name} must be a number, not ${typeof value}`
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
const attribute = geometry.point[spec.name];
|
|
120
|
+
if (attribute === void 0) {
|
|
121
|
+
if (spec.required) {
|
|
122
|
+
violations.push({
|
|
123
|
+
attribute: spec.name,
|
|
124
|
+
reason: `missing point attribute ${spec.name} \u2014 ${spec.note}`
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (attribute.storage === "string") {
|
|
130
|
+
violations.push({
|
|
131
|
+
attribute: spec.name,
|
|
132
|
+
reason: `${spec.name} is a string attribute; a solver reads it as ${spec.storage}`
|
|
133
|
+
});
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
if (!sizeMatches(spec, attribute.size)) {
|
|
137
|
+
violations.push({
|
|
138
|
+
attribute: spec.name,
|
|
139
|
+
reason: `${spec.name} has size ${attribute.size}; expected ${describeSize(spec)}`
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return violations;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// packages/runtime/src/pop/state.ts
|
|
147
|
+
var ParticleState = class _ParticleState {
|
|
148
|
+
/**
|
|
149
|
+
* Internal, and taking the arrays by reference rather than copying.
|
|
150
|
+
*
|
|
151
|
+
* `@internal` rather than `private` because `step` and `select` are module
|
|
152
|
+
* functions rather than methods — they are the arithmetic, and putting them
|
|
153
|
+
* on the class would make a state object that can step itself, which is one
|
|
154
|
+
* short hop from a state object that holds a clock.
|
|
155
|
+
*
|
|
156
|
+
* @internal
|
|
157
|
+
*/
|
|
158
|
+
static of(fields) {
|
|
159
|
+
return new _ParticleState(fields);
|
|
160
|
+
}
|
|
161
|
+
constructor(fields) {
|
|
162
|
+
this.count = fields.count;
|
|
163
|
+
this.size = fields.size;
|
|
164
|
+
this.position = fields.position;
|
|
165
|
+
this.velocity = fields.velocity;
|
|
166
|
+
this.age = fields.age;
|
|
167
|
+
this.life = fields.life;
|
|
168
|
+
this.id = fields.id;
|
|
169
|
+
this.colour = fields.colour ?? null;
|
|
170
|
+
this.nextId = fields.nextId;
|
|
171
|
+
}
|
|
172
|
+
/** An empty system. `nextId` starts at 0 and only ever increases. */
|
|
173
|
+
static empty(size = 2) {
|
|
174
|
+
return new _ParticleState({
|
|
175
|
+
count: 0,
|
|
176
|
+
size,
|
|
177
|
+
position: new Float64Array(0),
|
|
178
|
+
velocity: new Float32Array(0),
|
|
179
|
+
age: new Float32Array(0),
|
|
180
|
+
life: new Float32Array(0),
|
|
181
|
+
id: new Int32Array(0),
|
|
182
|
+
colour: null,
|
|
183
|
+
nextId: 0
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Read a state out of a geometry.
|
|
188
|
+
*
|
|
189
|
+
* Refuses rather than repairs. `checkParticleContract` returns every
|
|
190
|
+
* violation, and a solver handed a geometry without `id` cannot invent one:
|
|
191
|
+
* inventing ids per frame is precisely the fault the mandatory `id` exists to
|
|
192
|
+
* prevent, because it would silently make every particle new every step.
|
|
193
|
+
*/
|
|
194
|
+
static fromGeometry(geometry) {
|
|
195
|
+
const violations = checkParticleContract(geometry);
|
|
196
|
+
if (violations.length > 0) {
|
|
197
|
+
throw new Error(
|
|
198
|
+
`not a particle geometry: ${violations.map((v) => v.reason).join("; ")}`
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
const position = geometry.point[POSITION_ATTRIBUTE];
|
|
202
|
+
const velocity = geometry.point.v;
|
|
203
|
+
return new _ParticleState({
|
|
204
|
+
count: geometry.pointCount,
|
|
205
|
+
size: position.size,
|
|
206
|
+
position: Float64Array.from(position.data),
|
|
207
|
+
velocity: Float32Array.from(velocity.data),
|
|
208
|
+
age: Float32Array.from(geometry.point.age.data),
|
|
209
|
+
life: Float32Array.from(geometry.point.life.data),
|
|
210
|
+
id: Int32Array.from(geometry.point.id.data),
|
|
211
|
+
colour: geometry.point.Cd && geometry.point.Cd.storage !== "string" ? Float32Array.from(geometry.point.Cd.data) : null,
|
|
212
|
+
nextId: Number(geometry.detail.nextid ?? geometry.pointCount)
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
/** Write the state back out as geometry, one point per particle. */
|
|
216
|
+
toGeometry() {
|
|
217
|
+
const builder = new GeometryBuilder({ positionSize: this.size });
|
|
218
|
+
for (let index = 0; index < this.count; index += 1) {
|
|
219
|
+
builder.addPoint(
|
|
220
|
+
this.position[index * this.size],
|
|
221
|
+
this.position[index * this.size + 1],
|
|
222
|
+
this.size === 3 ? this.position[index * this.size + 2] : 0
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
builder.setAttribute("point", "v", { storage: "f32", size: this.size, data: this.velocity });
|
|
226
|
+
builder.setAttribute("point", "age", { storage: "f32", size: 1, data: this.age });
|
|
227
|
+
builder.setAttribute("point", "life", { storage: "f32", size: 1, data: this.life });
|
|
228
|
+
builder.setAttribute("point", "id", { storage: "i32", size: 1, data: this.id });
|
|
229
|
+
if (this.colour) {
|
|
230
|
+
builder.setAttribute("point", "Cd", { storage: "f32", size: 4, data: this.colour });
|
|
231
|
+
}
|
|
232
|
+
builder.setDetail("nextid", this.nextId);
|
|
233
|
+
return builder.build();
|
|
234
|
+
}
|
|
235
|
+
/** Add particles, handing each a fresh id. Returns the new state; the
|
|
236
|
+
* receiver is not mutated, so a source can be applied to a cached frame. */
|
|
237
|
+
born(points) {
|
|
238
|
+
const added = points.life.length;
|
|
239
|
+
if (added === 0) return this;
|
|
240
|
+
const count = this.count + added;
|
|
241
|
+
const position = new Float64Array(count * this.size);
|
|
242
|
+
const velocity = new Float32Array(count * this.size);
|
|
243
|
+
const age = new Float32Array(count);
|
|
244
|
+
const life = new Float32Array(count);
|
|
245
|
+
const id = new Int32Array(count);
|
|
246
|
+
const wantsColour = this.colour !== null || points.colour !== void 0;
|
|
247
|
+
const colour = wantsColour ? new Float32Array(count * 4) : null;
|
|
248
|
+
if (colour) {
|
|
249
|
+
colour.fill(1, 0, this.count * 4);
|
|
250
|
+
if (this.colour) colour.set(this.colour);
|
|
251
|
+
}
|
|
252
|
+
position.set(this.position);
|
|
253
|
+
velocity.set(this.velocity);
|
|
254
|
+
age.set(this.age);
|
|
255
|
+
life.set(this.life);
|
|
256
|
+
id.set(this.id);
|
|
257
|
+
for (let index = 0; index < added; index += 1) {
|
|
258
|
+
const at = this.count + index;
|
|
259
|
+
for (let component = 0; component < this.size; component += 1) {
|
|
260
|
+
position[at * this.size + component] = points.position[index * this.size + component] ?? 0;
|
|
261
|
+
velocity[at * this.size + component] = points.velocity?.[index * this.size + component] ?? 0;
|
|
262
|
+
}
|
|
263
|
+
life[at] = points.life[index];
|
|
264
|
+
if (colour) {
|
|
265
|
+
for (let channel = 0; channel < 4; channel += 1) {
|
|
266
|
+
colour[at * 4 + channel] = points.colour ? Number(points.colour[index * 4 + channel] ?? 1) : 1;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
id[at] = this.nextId + index;
|
|
270
|
+
}
|
|
271
|
+
return _ParticleState.of({
|
|
272
|
+
count,
|
|
273
|
+
size: this.size,
|
|
274
|
+
position,
|
|
275
|
+
velocity,
|
|
276
|
+
age,
|
|
277
|
+
life,
|
|
278
|
+
id,
|
|
279
|
+
colour,
|
|
280
|
+
nextId: this.nextId + added
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
function step(state, forces, options) {
|
|
285
|
+
const { timestep, maxSpeed = 0, wrap } = options;
|
|
286
|
+
const { count, size } = state;
|
|
287
|
+
if (count === 0) return state;
|
|
288
|
+
const acceleration = new Float32Array(count * size);
|
|
289
|
+
for (const force of forces) {
|
|
290
|
+
const contribution = force.accelerate(state, size);
|
|
291
|
+
if (contribution.length !== count * size) {
|
|
292
|
+
throw new Error(
|
|
293
|
+
`force ${force.label} returned ${contribution.length} values for ${count} particles of size ${size}`
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
for (let index = 0; index < acceleration.length; index += 1) {
|
|
297
|
+
acceleration[index] = acceleration[index] + contribution[index];
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
const position = Float64Array.from(state.position);
|
|
301
|
+
const velocity = Float32Array.from(state.velocity);
|
|
302
|
+
const age = Float32Array.from(state.age);
|
|
303
|
+
for (let index = 0; index < count; index += 1) {
|
|
304
|
+
let speedSquared = 0;
|
|
305
|
+
for (let component = 0; component < size; component += 1) {
|
|
306
|
+
const at = index * size + component;
|
|
307
|
+
velocity[at] = velocity[at] + acceleration[at] * timestep;
|
|
308
|
+
speedSquared += velocity[at] * velocity[at];
|
|
309
|
+
}
|
|
310
|
+
if (maxSpeed > 0 && speedSquared > maxSpeed * maxSpeed) {
|
|
311
|
+
const scale = maxSpeed / Math.sqrt(speedSquared);
|
|
312
|
+
for (let component = 0; component < size; component += 1) {
|
|
313
|
+
velocity[index * size + component] = velocity[index * size + component] * scale;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
for (let component = 0; component < size; component += 1) {
|
|
317
|
+
const at = index * size + component;
|
|
318
|
+
position[at] = position[at] + velocity[at] * timestep;
|
|
319
|
+
}
|
|
320
|
+
if (wrap) {
|
|
321
|
+
position[index * size] = wrapInto(position[index * size], wrap[0], wrap[2]);
|
|
322
|
+
position[index * size + 1] = wrapInto(position[index * size + 1], wrap[1], wrap[3]);
|
|
323
|
+
}
|
|
324
|
+
age[index] = age[index] + timestep;
|
|
325
|
+
}
|
|
326
|
+
return kill(ParticleState.of({
|
|
327
|
+
count,
|
|
328
|
+
size: state.size,
|
|
329
|
+
position,
|
|
330
|
+
velocity,
|
|
331
|
+
age,
|
|
332
|
+
life: Float32Array.from(state.life),
|
|
333
|
+
id: Int32Array.from(state.id),
|
|
334
|
+
colour: state.colour ? Float32Array.from(state.colour) : null,
|
|
335
|
+
nextId: state.nextId
|
|
336
|
+
}));
|
|
337
|
+
}
|
|
338
|
+
function wrapInto(value, low, high) {
|
|
339
|
+
const span = high - low;
|
|
340
|
+
if (span <= 0) return value;
|
|
341
|
+
return low + ((value - low) % span + span) % span;
|
|
342
|
+
}
|
|
343
|
+
function kill(state) {
|
|
344
|
+
const keep = [];
|
|
345
|
+
for (let index = 0; index < state.count; index += 1) {
|
|
346
|
+
const life = state.life[index];
|
|
347
|
+
if (life <= 0 || state.age[index] < life) keep.push(index);
|
|
348
|
+
}
|
|
349
|
+
if (keep.length === state.count) return state;
|
|
350
|
+
return select(state, keep);
|
|
351
|
+
}
|
|
352
|
+
function select(state, indices) {
|
|
353
|
+
const count = indices.length;
|
|
354
|
+
const { size } = state;
|
|
355
|
+
const position = new Float64Array(count * size);
|
|
356
|
+
const velocity = new Float32Array(count * size);
|
|
357
|
+
const age = new Float32Array(count);
|
|
358
|
+
const life = new Float32Array(count);
|
|
359
|
+
const id = new Int32Array(count);
|
|
360
|
+
const colour = state.colour ? new Float32Array(count * 4) : null;
|
|
361
|
+
indices.forEach((from, to) => {
|
|
362
|
+
for (let component = 0; component < size; component += 1) {
|
|
363
|
+
position[to * size + component] = state.position[from * size + component];
|
|
364
|
+
velocity[to * size + component] = state.velocity[from * size + component];
|
|
365
|
+
}
|
|
366
|
+
age[to] = state.age[from];
|
|
367
|
+
life[to] = state.life[from];
|
|
368
|
+
id[to] = state.id[from];
|
|
369
|
+
if (colour && state.colour) {
|
|
370
|
+
for (let channel = 0; channel < 4; channel += 1) {
|
|
371
|
+
colour[to * 4 + channel] = state.colour[from * 4 + channel];
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
return ParticleState.of({
|
|
376
|
+
count,
|
|
377
|
+
size,
|
|
378
|
+
position,
|
|
379
|
+
velocity,
|
|
380
|
+
age,
|
|
381
|
+
life,
|
|
382
|
+
id,
|
|
383
|
+
colour,
|
|
384
|
+
nextId: state.nextId
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
function emptyParticleGeometry(size = 2) {
|
|
388
|
+
const builder = new GeometryBuilder({ positionSize: size });
|
|
389
|
+
builder.setAttribute("point", "v", createAttribute(0, size, "f32"));
|
|
390
|
+
builder.setAttribute("point", "age", createAttribute(0, 1, "f32"));
|
|
391
|
+
builder.setAttribute("point", "life", createAttribute(0, 1, "f32"));
|
|
392
|
+
builder.setAttribute("point", "id", createAttribute(0, 1, "i32"));
|
|
393
|
+
builder.setDetail("nextid", 0);
|
|
394
|
+
return builder.build();
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// packages/runtime/src/pop/cache.ts
|
|
398
|
+
var DEFAULT_STRIDE = 8;
|
|
399
|
+
var MAX_CHECKPOINTS = 64;
|
|
400
|
+
var entries = /* @__PURE__ */ new Map();
|
|
401
|
+
var clock = 0;
|
|
402
|
+
function cloneState(state) {
|
|
403
|
+
return ParticleState.of({
|
|
404
|
+
count: state.count,
|
|
405
|
+
size: state.size,
|
|
406
|
+
position: Float64Array.from(state.position),
|
|
407
|
+
velocity: Float32Array.from(state.velocity),
|
|
408
|
+
age: Float32Array.from(state.age),
|
|
409
|
+
life: Float32Array.from(state.life),
|
|
410
|
+
id: Int32Array.from(state.id),
|
|
411
|
+
...state.colour ? { colour: Float32Array.from(state.colour) } : {},
|
|
412
|
+
nextId: state.nextId
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
function checkpointAtOrBefore(key, fingerprint, frame) {
|
|
416
|
+
const entry = entries.get(key);
|
|
417
|
+
if (!entry) return null;
|
|
418
|
+
if (entry.fingerprint !== fingerprint) {
|
|
419
|
+
entries.delete(key);
|
|
420
|
+
return null;
|
|
421
|
+
}
|
|
422
|
+
entry.touched = clock += 1;
|
|
423
|
+
let best = null;
|
|
424
|
+
for (const checkpoint of entry.checkpoints) {
|
|
425
|
+
if (checkpoint.frame > frame) break;
|
|
426
|
+
best = checkpoint;
|
|
427
|
+
}
|
|
428
|
+
return best ? { frame: best.frame, state: cloneState(best.state) } : null;
|
|
429
|
+
}
|
|
430
|
+
function offerCheckpoint(key, fingerprint, frame, state, stride = DEFAULT_STRIDE) {
|
|
431
|
+
if (frame % stride !== 0) return;
|
|
432
|
+
let entry = entries.get(key);
|
|
433
|
+
if (!entry || entry.fingerprint !== fingerprint) {
|
|
434
|
+
entry = { fingerprint, checkpoints: [], touched: clock += 1 };
|
|
435
|
+
entries.set(key, entry);
|
|
436
|
+
}
|
|
437
|
+
if (entry.checkpoints.some((checkpoint) => checkpoint.frame === frame)) return;
|
|
438
|
+
entry.checkpoints.push({ frame, state: cloneState(state) });
|
|
439
|
+
entry.checkpoints.sort((a, b) => a.frame - b.frame);
|
|
440
|
+
entry.touched = clock += 1;
|
|
441
|
+
evict();
|
|
442
|
+
}
|
|
443
|
+
function evict() {
|
|
444
|
+
let total = 0;
|
|
445
|
+
for (const entry of entries.values()) total += entry.checkpoints.length;
|
|
446
|
+
while (total > MAX_CHECKPOINTS && entries.size > 1) {
|
|
447
|
+
let oldest = null;
|
|
448
|
+
let oldestTouched = Infinity;
|
|
449
|
+
for (const [key, entry] of entries) {
|
|
450
|
+
if (entry.touched < oldestTouched) {
|
|
451
|
+
oldestTouched = entry.touched;
|
|
452
|
+
oldest = key;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
if (oldest === null) return;
|
|
456
|
+
total -= entries.get(oldest).checkpoints.length;
|
|
457
|
+
entries.delete(oldest);
|
|
458
|
+
}
|
|
459
|
+
if (total > MAX_CHECKPOINTS) {
|
|
460
|
+
const remaining = entries.values().next().value;
|
|
461
|
+
if (!remaining) return;
|
|
462
|
+
remaining.checkpoints.splice(0, total - MAX_CHECKPOINTS);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// packages/runtime/src/pop/forces.ts
|
|
467
|
+
function gravity(force) {
|
|
468
|
+
return {
|
|
469
|
+
label: "gravity",
|
|
470
|
+
accelerate(state, size) {
|
|
471
|
+
const out = new Float32Array(state.count * size);
|
|
472
|
+
for (let index = 0; index < state.count; index += 1) {
|
|
473
|
+
for (let component = 0; component < size; component += 1) {
|
|
474
|
+
out[index * size + component] = force[component] ?? 0;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
return out;
|
|
478
|
+
}
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
function drag(airresist) {
|
|
482
|
+
return {
|
|
483
|
+
label: "drag",
|
|
484
|
+
accelerate(state, size) {
|
|
485
|
+
const out = new Float32Array(state.count * size);
|
|
486
|
+
for (let index = 0; index < out.length; index += 1) {
|
|
487
|
+
out[index] = -state.velocity[index] * airresist;
|
|
488
|
+
}
|
|
489
|
+
return out;
|
|
490
|
+
}
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
function noiseField(options) {
|
|
494
|
+
const { seed, frequency, amplitude, evolve = 0 } = options;
|
|
495
|
+
return {
|
|
496
|
+
label: "noiseField",
|
|
497
|
+
accelerate(state, size) {
|
|
498
|
+
const out = new Float32Array(state.count * size);
|
|
499
|
+
for (let index = 0; index < state.count; index += 1) {
|
|
500
|
+
const x = state.position[index * size] * frequency;
|
|
501
|
+
const y = state.position[index * size + 1] * frequency;
|
|
502
|
+
const heading = valueNoise(seed, x, y, evolve) * Math.PI * 2;
|
|
503
|
+
out[index * size] = Math.cos(heading) * amplitude;
|
|
504
|
+
out[index * size + 1] = Math.sin(heading) * amplitude;
|
|
505
|
+
}
|
|
506
|
+
return out;
|
|
507
|
+
}
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
function separation(options) {
|
|
511
|
+
const { radius, strength } = options;
|
|
512
|
+
return {
|
|
513
|
+
label: "separation",
|
|
514
|
+
accelerate(state, size) {
|
|
515
|
+
const out = new Float32Array(state.count * size);
|
|
516
|
+
if (radius <= 0 || strength === 0) return out;
|
|
517
|
+
const cell = radius;
|
|
518
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
519
|
+
const key = (x, y) => `${Math.floor(x / cell)}:${Math.floor(y / cell)}`;
|
|
520
|
+
for (let index = 0; index < state.count; index += 1) {
|
|
521
|
+
const k = key(state.position[index * size], state.position[index * size + 1]);
|
|
522
|
+
const bucket = buckets.get(k);
|
|
523
|
+
if (bucket) bucket.push(index);
|
|
524
|
+
else buckets.set(k, [index]);
|
|
525
|
+
}
|
|
526
|
+
for (let index = 0; index < state.count; index += 1) {
|
|
527
|
+
const px = state.position[index * size];
|
|
528
|
+
const py = state.position[index * size + 1];
|
|
529
|
+
const cx = Math.floor(px / cell);
|
|
530
|
+
const cy = Math.floor(py / cell);
|
|
531
|
+
let ax = 0;
|
|
532
|
+
let ay = 0;
|
|
533
|
+
for (let ox = -1; ox <= 1; ox += 1) {
|
|
534
|
+
for (let oy = -1; oy <= 1; oy += 1) {
|
|
535
|
+
const bucket = buckets.get(`${cx + ox}:${cy + oy}`);
|
|
536
|
+
if (!bucket) continue;
|
|
537
|
+
for (const other of bucket) {
|
|
538
|
+
if (other === index) continue;
|
|
539
|
+
const dx = px - state.position[other * size];
|
|
540
|
+
const dy = py - state.position[other * size + 1];
|
|
541
|
+
const distanceSquared = dx * dx + dy * dy;
|
|
542
|
+
if (distanceSquared === 0 || distanceSquared > radius * radius) continue;
|
|
543
|
+
const distance = Math.sqrt(distanceSquared);
|
|
544
|
+
const falloff = (1 - distance / radius) / distance;
|
|
545
|
+
ax += dx * falloff;
|
|
546
|
+
ay += dy * falloff;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
out[index * size] = ax * strength;
|
|
551
|
+
out[index * size + 1] = ay * strength;
|
|
552
|
+
}
|
|
553
|
+
return out;
|
|
554
|
+
}
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
function valueNoise(seed, x, y, z = 0) {
|
|
558
|
+
const x0 = Math.floor(x);
|
|
559
|
+
const y0 = Math.floor(y);
|
|
560
|
+
const z0 = Math.floor(z);
|
|
561
|
+
const fx = smooth(x - x0);
|
|
562
|
+
const fy = smooth(y - y0);
|
|
563
|
+
const fz = smooth(z - z0);
|
|
564
|
+
let value = 0;
|
|
565
|
+
for (let dz = 0; dz <= 1; dz += 1) {
|
|
566
|
+
for (let dy = 0; dy <= 1; dy += 1) {
|
|
567
|
+
for (let dx = 0; dx <= 1; dx += 1) {
|
|
568
|
+
const weight = (dx === 0 ? 1 - fx : fx) * (dy === 0 ? 1 - fy : fy) * (dz === 0 ? 1 - fz : fz);
|
|
569
|
+
value += hash(seed, x0 + dx, y0 + dy, z0 + dz) * weight;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
return value;
|
|
574
|
+
}
|
|
575
|
+
function smooth(t) {
|
|
576
|
+
return t * t * (3 - 2 * t);
|
|
577
|
+
}
|
|
578
|
+
function hash(seed, x, y, z) {
|
|
579
|
+
const n = Math.sin(seed * 12.9898 + x * 78.233 + y * 37.719 + z * 19.371) * 43758.5453;
|
|
580
|
+
return n - Math.floor(n);
|
|
581
|
+
}
|
|
582
|
+
function attract(options) {
|
|
583
|
+
const { targets, targetSize, targetCount, amplitude, radius } = options;
|
|
584
|
+
const cell = radius > 0 ? radius : 0;
|
|
585
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
586
|
+
if (cell > 0) {
|
|
587
|
+
for (let target = 0; target < targetCount; target += 1) {
|
|
588
|
+
const key = bucketKey(
|
|
589
|
+
Number(targets[target * targetSize]),
|
|
590
|
+
Number(targets[target * targetSize + 1]),
|
|
591
|
+
cell
|
|
592
|
+
);
|
|
593
|
+
const bucket = buckets.get(key);
|
|
594
|
+
if (bucket) bucket.push(target);
|
|
595
|
+
else buckets.set(key, [target]);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
return {
|
|
599
|
+
label: "attract",
|
|
600
|
+
accelerate(state, size) {
|
|
601
|
+
const out = new Float32Array(state.count * size);
|
|
602
|
+
if (amplitude === 0 || targetCount === 0) return out;
|
|
603
|
+
const limit = radius > 0 ? radius * radius : Infinity;
|
|
604
|
+
for (let index = 0; index < state.count; index += 1) {
|
|
605
|
+
const px = state.position[index * size];
|
|
606
|
+
const py = state.position[index * size + 1];
|
|
607
|
+
let ax = 0;
|
|
608
|
+
let ay = 0;
|
|
609
|
+
let pulls = 0;
|
|
610
|
+
const consider = (target) => {
|
|
611
|
+
const dx = Number(targets[target * targetSize]) - px;
|
|
612
|
+
const dy = Number(targets[target * targetSize + 1]) - py;
|
|
613
|
+
const distanceSquared = dx * dx + dy * dy;
|
|
614
|
+
if (distanceSquared === 0 || distanceSquared > limit) return;
|
|
615
|
+
const distance = Math.sqrt(distanceSquared);
|
|
616
|
+
const falloff = radius > 0 ? (1 - distance / radius) / distance : 1 / distance;
|
|
617
|
+
ax += dx * falloff;
|
|
618
|
+
ay += dy * falloff;
|
|
619
|
+
pulls += 1;
|
|
620
|
+
};
|
|
621
|
+
if (cell > 0) {
|
|
622
|
+
const cx = Math.floor(px / cell);
|
|
623
|
+
const cy = Math.floor(py / cell);
|
|
624
|
+
for (let ox = -1; ox <= 1; ox += 1) {
|
|
625
|
+
for (let oy = -1; oy <= 1; oy += 1) {
|
|
626
|
+
const bucket = buckets.get(`${cx + ox}:${cy + oy}`);
|
|
627
|
+
if (!bucket) continue;
|
|
628
|
+
for (const target of bucket) consider(target);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
} else {
|
|
632
|
+
for (let target = 0; target < targetCount; target += 1) consider(target);
|
|
633
|
+
}
|
|
634
|
+
if (pulls === 0) continue;
|
|
635
|
+
out[index * size] = ax / pulls * amplitude;
|
|
636
|
+
out[index * size + 1] = ay / pulls * amplitude;
|
|
637
|
+
}
|
|
638
|
+
return out;
|
|
639
|
+
}
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
function bucketKey(x, y, cell) {
|
|
643
|
+
return `${Math.floor(x / cell)}:${Math.floor(y / cell)}`;
|
|
644
|
+
}
|
|
645
|
+
function flowField(options) {
|
|
646
|
+
const {
|
|
647
|
+
positions,
|
|
648
|
+
positionSize,
|
|
649
|
+
directions,
|
|
650
|
+
levels,
|
|
651
|
+
count,
|
|
652
|
+
normal,
|
|
653
|
+
tangential,
|
|
654
|
+
radius,
|
|
655
|
+
level = 0,
|
|
656
|
+
hold = 0
|
|
657
|
+
} = options;
|
|
658
|
+
const cell = radius > 0 ? radius : 1;
|
|
659
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
660
|
+
for (let sample = 0; sample < count; sample += 1) {
|
|
661
|
+
const key = bucketKey(
|
|
662
|
+
Number(positions[sample * positionSize]),
|
|
663
|
+
Number(positions[sample * positionSize + 1]),
|
|
664
|
+
cell
|
|
665
|
+
);
|
|
666
|
+
const bucket = buckets.get(key);
|
|
667
|
+
if (bucket) bucket.push(sample);
|
|
668
|
+
else buckets.set(key, [sample]);
|
|
669
|
+
}
|
|
670
|
+
return {
|
|
671
|
+
label: "flowField",
|
|
672
|
+
accelerate(state, size) {
|
|
673
|
+
const out = new Float32Array(state.count * size);
|
|
674
|
+
if (count === 0 || normal === 0 && tangential === 0) return out;
|
|
675
|
+
const limit = radius * radius;
|
|
676
|
+
for (let index = 0; index < state.count; index += 1) {
|
|
677
|
+
const px = state.position[index * size];
|
|
678
|
+
const py = state.position[index * size + 1];
|
|
679
|
+
const cx = Math.floor(px / cell);
|
|
680
|
+
const cy = Math.floor(py / cell);
|
|
681
|
+
let gx = 0;
|
|
682
|
+
let gy = 0;
|
|
683
|
+
let levelSum = 0;
|
|
684
|
+
let weightSum = 0;
|
|
685
|
+
for (let ox = -1; ox <= 1; ox += 1) {
|
|
686
|
+
for (let oy = -1; oy <= 1; oy += 1) {
|
|
687
|
+
const bucket = buckets.get(`${cx + ox}:${cy + oy}`);
|
|
688
|
+
if (!bucket) continue;
|
|
689
|
+
for (const sample of bucket) {
|
|
690
|
+
const dx = Number(positions[sample * positionSize]) - px;
|
|
691
|
+
const dy = Number(positions[sample * positionSize + 1]) - py;
|
|
692
|
+
const distanceSquared = dx * dx + dy * dy;
|
|
693
|
+
if (distanceSquared > limit) continue;
|
|
694
|
+
const weight = 1 - Math.sqrt(distanceSquared) / radius;
|
|
695
|
+
if (weight <= 0) continue;
|
|
696
|
+
gx += Number(directions[sample * 2]) * weight;
|
|
697
|
+
gy += Number(directions[sample * 2 + 1]) * weight;
|
|
698
|
+
if (levels) levelSum += Number(levels[sample]) * weight;
|
|
699
|
+
weightSum += weight;
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
if (weightSum === 0) continue;
|
|
704
|
+
gx /= weightSum;
|
|
705
|
+
gy /= weightSum;
|
|
706
|
+
const climb = levels && hold !== 0 ? normal + (level - levelSum / weightSum) * hold : normal;
|
|
707
|
+
out[index * size] = gx * climb - gy * tangential;
|
|
708
|
+
out[index * size + 1] = gy * climb + gx * tangential;
|
|
709
|
+
}
|
|
710
|
+
return out;
|
|
711
|
+
}
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
function accumulateForce(geometry, contribution, size) {
|
|
715
|
+
const count = geometry.pointCount;
|
|
716
|
+
if (contribution.length !== count * size)
|
|
717
|
+
throw new Error(
|
|
718
|
+
`force contribution has ${contribution.length} values for ${count} particles of size ${size}`
|
|
719
|
+
);
|
|
720
|
+
const existing = geometry.point.force;
|
|
721
|
+
const total = new Float32Array(count * size);
|
|
722
|
+
if (existing && existing.storage !== "string" && existing.size === size)
|
|
723
|
+
for (let index = 0; index < total.length; index += 1)
|
|
724
|
+
total[index] = Number(existing.data[index] ?? 0);
|
|
725
|
+
for (let index = 0; index < total.length; index += 1)
|
|
726
|
+
total[index] = total[index] + contribution[index];
|
|
727
|
+
return {
|
|
728
|
+
...geometry,
|
|
729
|
+
point: {
|
|
730
|
+
...geometry.point,
|
|
731
|
+
force: { storage: "f32", size, data: total }
|
|
732
|
+
}
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
function forceAttribute(geometry) {
|
|
736
|
+
const stored = geometry.point.force;
|
|
737
|
+
const data = stored && stored.storage !== "string" ? stored.data : void 0;
|
|
738
|
+
const storedSize = stored?.size ?? 0;
|
|
739
|
+
return {
|
|
740
|
+
label: "force",
|
|
741
|
+
accelerate: (state, size) => {
|
|
742
|
+
const out = new Float32Array(state.count * size);
|
|
743
|
+
if (!data) return out;
|
|
744
|
+
const shared = Math.min(size, storedSize);
|
|
745
|
+
for (let point = 0; point < state.count; point += 1)
|
|
746
|
+
for (let axis = 0; axis < shared; axis += 1)
|
|
747
|
+
out[point * size + axis] = Number(data[point * storedSize + axis] ?? 0);
|
|
748
|
+
return out;
|
|
749
|
+
}
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
function clearForce(geometry) {
|
|
753
|
+
if (!geometry.point.force) return geometry;
|
|
754
|
+
const { force: _dropped, ...rest } = geometry.point;
|
|
755
|
+
return { ...geometry, point: rest };
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// packages/runtime/src/pop/trail.ts
|
|
759
|
+
function trailFrame(geometry) {
|
|
760
|
+
const position = geometry.point[POSITION_ATTRIBUTE];
|
|
761
|
+
return {
|
|
762
|
+
id: Int32Array.from(geometry.point.id?.data ?? new Int32Array(0)),
|
|
763
|
+
position: Float64Array.from(position.data),
|
|
764
|
+
size: position.size
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
function buildTrails(history, current, options = {}) {
|
|
768
|
+
const { result = "polylines", length = 12, increment = 1, velocityscale = 1 } = options;
|
|
769
|
+
if (result === "preserve") return current;
|
|
770
|
+
const frames = sampled(history, length, increment);
|
|
771
|
+
const size = frames[0]?.size ?? 2;
|
|
772
|
+
const paths = /* @__PURE__ */ new Map();
|
|
773
|
+
for (const frame of frames) {
|
|
774
|
+
for (let index = 0; index < frame.id.length; index += 1) {
|
|
775
|
+
const id = frame.id[index];
|
|
776
|
+
const path = paths.get(id) ?? [];
|
|
777
|
+
path.push(
|
|
778
|
+
frame.position[index * frame.size],
|
|
779
|
+
frame.position[index * frame.size + 1]
|
|
780
|
+
);
|
|
781
|
+
paths.set(id, path);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
if (result === "velocity") {
|
|
785
|
+
return withDerivedVelocity(current, paths, velocityscale);
|
|
786
|
+
}
|
|
787
|
+
const builder = new GeometryBuilder({ positionSize: 2 });
|
|
788
|
+
const trailIds = [];
|
|
789
|
+
for (const [id, path] of paths) {
|
|
790
|
+
if (path.length < 4) continue;
|
|
791
|
+
const points = [];
|
|
792
|
+
for (let at = 0; at < path.length; at += 2) {
|
|
793
|
+
points.push(builder.addPoint(path[at], path[at + 1]));
|
|
794
|
+
}
|
|
795
|
+
builder.addPrimitive(points, { closed: false, kind: "poly" });
|
|
796
|
+
trailIds.push(id);
|
|
797
|
+
}
|
|
798
|
+
if (trailIds.length > 0) {
|
|
799
|
+
builder.setNumericAttribute("primitive", "id", Int32Array.from(trailIds), 1, "i32");
|
|
800
|
+
const currentColour = current.point.Cd;
|
|
801
|
+
if (currentColour && currentColour.storage !== "string") {
|
|
802
|
+
const byId = /* @__PURE__ */ new Map();
|
|
803
|
+
const ids = current.point.id?.data;
|
|
804
|
+
if (ids) {
|
|
805
|
+
for (let index = 0; index < current.pointCount; index += 1) {
|
|
806
|
+
byId.set(Number(ids[index]), index);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
const source = currentColour.data;
|
|
810
|
+
const colours = new Float32Array(trailIds.length * 4);
|
|
811
|
+
colours.fill(1);
|
|
812
|
+
trailIds.forEach((id, primitive) => {
|
|
813
|
+
const at = byId.get(id);
|
|
814
|
+
if (at === void 0) return;
|
|
815
|
+
for (let channel = 0; channel < 4; channel += 1) {
|
|
816
|
+
colours[primitive * 4 + channel] = Number(source[at * 4 + channel] ?? 1);
|
|
817
|
+
}
|
|
818
|
+
});
|
|
819
|
+
builder.setNumericAttribute("primitive", "Cd", colours, 4, "f32");
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
void size;
|
|
823
|
+
return builder.build();
|
|
824
|
+
}
|
|
825
|
+
function sampled(history, length, increment) {
|
|
826
|
+
const step2 = Math.max(1, Math.floor(increment));
|
|
827
|
+
const window = history.slice(Math.max(0, history.length - length * step2));
|
|
828
|
+
if (step2 === 1) return window;
|
|
829
|
+
const picked = [];
|
|
830
|
+
for (let index = window.length - 1; index >= 0; index -= step2) picked.unshift(window[index]);
|
|
831
|
+
return picked;
|
|
832
|
+
}
|
|
833
|
+
function withDerivedVelocity(current, paths, velocityscale) {
|
|
834
|
+
const size = current.point[POSITION_ATTRIBUTE].size;
|
|
835
|
+
const ids = current.point.id?.data;
|
|
836
|
+
const velocity = new Float32Array(current.pointCount * size);
|
|
837
|
+
for (let index = 0; index < current.pointCount; index += 1) {
|
|
838
|
+
const path = ids ? paths.get(Number(ids[index])) : void 0;
|
|
839
|
+
if (!path || path.length < 4) continue;
|
|
840
|
+
const last = path.length;
|
|
841
|
+
velocity[index * size] = (path[last - 2] - path[last - 4]) * velocityscale;
|
|
842
|
+
velocity[index * size + 1] = (path[last - 1] - path[last - 3]) * velocityscale;
|
|
843
|
+
}
|
|
844
|
+
return {
|
|
845
|
+
...current,
|
|
846
|
+
point: { ...current.point, v: { storage: "f32", size, data: velocity } }
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
// packages/runtime/src/builtins/pop/definitions.ts
|
|
851
|
+
var sourceDefinition = {
|
|
852
|
+
apiVersion: 1,
|
|
853
|
+
label: "POP Source",
|
|
854
|
+
description: "Birth particles from a geometry's points.",
|
|
855
|
+
icon: "Sparkles",
|
|
856
|
+
runsOn: "portable",
|
|
857
|
+
inputs: {
|
|
858
|
+
geometry: { kind: "data", type: "geometry" },
|
|
859
|
+
particles: { kind: "data", type: "geometry" }
|
|
860
|
+
},
|
|
861
|
+
outputs: { geometry: { kind: "data", type: "geometry" } },
|
|
862
|
+
props: {
|
|
863
|
+
impulse: { type: "int", default: 8, min: 0, max: 1e5 },
|
|
864
|
+
life: { type: "float", default: 3, min: 0, max: 3600, step: 0.1 },
|
|
865
|
+
lifevar: { type: "float", default: 0.3, min: 0, max: 1, step: 0.01 },
|
|
866
|
+
velocity: { type: "vec2", default: [0, 0] },
|
|
867
|
+
seed: { type: "int", default: 7, min: 0, max: 999999 }
|
|
868
|
+
}
|
|
869
|
+
};
|
|
870
|
+
function hsvToRgb(h, s, v) {
|
|
871
|
+
const turn = (h % 1 + 1) % 1;
|
|
872
|
+
const sector = turn * 6;
|
|
873
|
+
const c = v * s;
|
|
874
|
+
const x = c * (1 - Math.abs(sector % 2 - 1));
|
|
875
|
+
const m = v - c;
|
|
876
|
+
const rgb = sector < 1 ? [c, x, 0] : sector < 2 ? [x, c, 0] : sector < 3 ? [0, c, x] : sector < 4 ? [0, x, c] : sector < 5 ? [x, 0, c] : [c, 0, x];
|
|
877
|
+
return [rgb[0] + m, rgb[1] + m, rgb[2] + m];
|
|
878
|
+
}
|
|
879
|
+
function birthRandom(seed, sample) {
|
|
880
|
+
const n = Math.sin(seed * 12.9898 + sample * 78.233) * 43758.5453;
|
|
881
|
+
return n - Math.floor(n);
|
|
882
|
+
}
|
|
883
|
+
function executeSource(context) {
|
|
884
|
+
const props = context.props;
|
|
885
|
+
const incoming = context.inputs.particles;
|
|
886
|
+
const state = incoming ? ParticleState.fromGeometry(incoming) : ParticleState.fromGeometry(emptyParticleGeometry(2));
|
|
887
|
+
const from = context.inputs.geometry;
|
|
888
|
+
const available = from ? from.pointCount : 0;
|
|
889
|
+
const born = available === 0 ? 0 : Math.min(props.impulse, available);
|
|
890
|
+
if (born === 0) {
|
|
891
|
+
context.outputs.geometry.set(state.toGeometry());
|
|
892
|
+
return;
|
|
893
|
+
}
|
|
894
|
+
const size = state.size;
|
|
895
|
+
const position = new Float64Array(born * size);
|
|
896
|
+
const velocity = new Float32Array(born * size);
|
|
897
|
+
const life = new Float32Array(born);
|
|
898
|
+
const source = from.point.P;
|
|
899
|
+
for (let index = 0; index < born; index += 1) {
|
|
900
|
+
const pick = available === born ? index : Math.floor(index / born * available);
|
|
901
|
+
for (let component = 0; component < size; component += 1) {
|
|
902
|
+
position[index * size + component] = Number(source.data[pick * source.size + component] ?? 0);
|
|
903
|
+
velocity[index * size + component] = props.velocity[component] ?? 0;
|
|
904
|
+
}
|
|
905
|
+
const jitter = birthRandom(props.seed, state.nextId + index);
|
|
906
|
+
life[index] = props.life * (1 - props.lifevar * jitter);
|
|
907
|
+
}
|
|
908
|
+
context.outputs.geometry.set(state.born({ position, velocity, life }).toGeometry());
|
|
909
|
+
}
|
|
910
|
+
var solverDefinition = {
|
|
911
|
+
apiVersion: 1,
|
|
912
|
+
label: "POP Solver",
|
|
913
|
+
description: "Advance a particle system by one timestep.",
|
|
914
|
+
icon: "Play",
|
|
915
|
+
runsOn: "portable",
|
|
916
|
+
inputs: { particles: { kind: "data", type: "geometry" } },
|
|
917
|
+
outputs: { geometry: { kind: "data", type: "geometry" } },
|
|
918
|
+
props: {
|
|
919
|
+
/**
|
|
920
|
+
* Where the timestep comes from. Marcus's ruling, 2026-09-09: *"Let the
|
|
921
|
+
* user choose nodes fixed or variable."*
|
|
922
|
+
*
|
|
923
|
+
* `fixed` is the default on the ordinary grounds that the safer of two
|
|
924
|
+
* behaviours is the one you get without asking - a simulation whose result
|
|
925
|
+
* depends on a playback setting is not reproducible from the document
|
|
926
|
+
* alone. With `fps` the render rate becomes part of what determines the
|
|
927
|
+
* output, which is correct for a mode called variable and must not be
|
|
928
|
+
* silent: it belongs in the cache key, and `cascade check` should say so.
|
|
929
|
+
*/
|
|
930
|
+
timestep_mode: {
|
|
931
|
+
type: "string",
|
|
932
|
+
default: "fixed",
|
|
933
|
+
control: "select",
|
|
934
|
+
options: [
|
|
935
|
+
{ value: "fixed", label: "Fixed - reproducible from the document" },
|
|
936
|
+
{ value: "fps", label: "Variable - derived from the render rate" }
|
|
937
|
+
]
|
|
938
|
+
},
|
|
939
|
+
timestep: { type: "float", default: 1 / 24, min: 1 / 240, max: 1, step: 1 / 240 },
|
|
940
|
+
maxspeed: { type: "float", default: 0, min: 0, max: 1e4, step: 0.5 },
|
|
941
|
+
force: { type: "vec2", default: [0, 0] },
|
|
942
|
+
airresist: { type: "float", default: 0, min: 0, max: 20, step: 0.05 }
|
|
943
|
+
}
|
|
944
|
+
};
|
|
945
|
+
function executeSolver(context) {
|
|
946
|
+
const props = context.props;
|
|
947
|
+
const incoming = context.inputs.particles;
|
|
948
|
+
if (!incoming) {
|
|
949
|
+
context.outputs.geometry.set(emptyParticleGeometry(2));
|
|
950
|
+
return;
|
|
951
|
+
}
|
|
952
|
+
const state = ParticleState.fromGeometry(incoming);
|
|
953
|
+
const forces = [];
|
|
954
|
+
forces.push(forceAttribute(incoming));
|
|
955
|
+
if (props.force[0] !== 0 || props.force[1] !== 0) forces.push(gravity(props.force));
|
|
956
|
+
if (props.airresist > 0) forces.push(drag(props.airresist));
|
|
957
|
+
context.outputs.geometry.set(
|
|
958
|
+
clearForce(
|
|
959
|
+
step(state, forces, {
|
|
960
|
+
timestep: props.timestep,
|
|
961
|
+
...props.maxspeed > 0 ? { maxSpeed: props.maxspeed } : {}
|
|
962
|
+
}).toGeometry()
|
|
963
|
+
)
|
|
964
|
+
);
|
|
965
|
+
}
|
|
966
|
+
var noiseForceDefinition = {
|
|
967
|
+
apiVersion: 1,
|
|
968
|
+
label: "POP Noise Force",
|
|
969
|
+
description: "Push particles along a heading read from a noise field.",
|
|
970
|
+
icon: "Wind",
|
|
971
|
+
runsOn: "portable",
|
|
972
|
+
inputs: { particles: { kind: "data", type: "geometry" } },
|
|
973
|
+
outputs: { geometry: { kind: "data", type: "geometry" } },
|
|
974
|
+
props: {
|
|
975
|
+
amplitude: { type: "float", default: 6, min: 0, max: 500, step: 0.5 },
|
|
976
|
+
frequency: { type: "float", default: 0.3, min: 1e-3, max: 10, step: 0.01 },
|
|
977
|
+
evolve: { type: "float", default: 0, min: -1e3, max: 1e3, step: 0.01 },
|
|
978
|
+
timestep: { type: "float", default: 1 / 24, min: 1 / 240, max: 1, step: 1 / 240 },
|
|
979
|
+
seed: { type: "int", default: 7, min: 0, max: 999999 }
|
|
980
|
+
}
|
|
981
|
+
};
|
|
982
|
+
function executeNoiseForce(context) {
|
|
983
|
+
const incoming = context.inputs.particles;
|
|
984
|
+
if (!incoming) {
|
|
985
|
+
context.outputs.geometry.set(emptyParticleGeometry(2));
|
|
986
|
+
return;
|
|
987
|
+
}
|
|
988
|
+
const props = context.props;
|
|
989
|
+
const state = ParticleState.fromGeometry(incoming);
|
|
990
|
+
context.outputs.geometry.set(
|
|
991
|
+
accumulateForce(
|
|
992
|
+
incoming,
|
|
993
|
+
noiseField({
|
|
994
|
+
seed: props.seed,
|
|
995
|
+
frequency: props.frequency,
|
|
996
|
+
amplitude: props.amplitude,
|
|
997
|
+
evolve: props.evolve
|
|
998
|
+
}).accelerate(state, state.size),
|
|
999
|
+
state.size
|
|
1000
|
+
)
|
|
1001
|
+
);
|
|
1002
|
+
}
|
|
1003
|
+
var separateDefinition = {
|
|
1004
|
+
apiVersion: 1,
|
|
1005
|
+
label: "POP Separate",
|
|
1006
|
+
description: "Push neighbours apart within a radius.",
|
|
1007
|
+
icon: "Move3d",
|
|
1008
|
+
runsOn: "portable",
|
|
1009
|
+
inputs: { particles: { kind: "data", type: "geometry" } },
|
|
1010
|
+
outputs: { geometry: { kind: "data", type: "geometry" } },
|
|
1011
|
+
props: {
|
|
1012
|
+
radius: { type: "float", default: 0.5, min: 0, max: 100, step: 0.01 },
|
|
1013
|
+
strength: { type: "float", default: 4, min: 0, max: 500, step: 0.1 },
|
|
1014
|
+
timestep: { type: "float", default: 1 / 24, min: 1 / 240, max: 1, step: 1 / 240 }
|
|
1015
|
+
}
|
|
1016
|
+
};
|
|
1017
|
+
function executeSeparate(context) {
|
|
1018
|
+
const incoming = context.inputs.particles;
|
|
1019
|
+
if (!incoming) {
|
|
1020
|
+
context.outputs.geometry.set(emptyParticleGeometry(2));
|
|
1021
|
+
return;
|
|
1022
|
+
}
|
|
1023
|
+
const props = context.props;
|
|
1024
|
+
const state = ParticleState.fromGeometry(incoming);
|
|
1025
|
+
context.outputs.geometry.set(
|
|
1026
|
+
accumulateForce(
|
|
1027
|
+
incoming,
|
|
1028
|
+
separation({ radius: props.radius, strength: props.strength }).accelerate(
|
|
1029
|
+
state,
|
|
1030
|
+
state.size
|
|
1031
|
+
),
|
|
1032
|
+
state.size
|
|
1033
|
+
)
|
|
1034
|
+
);
|
|
1035
|
+
}
|
|
1036
|
+
var fieldForceDefinition = {
|
|
1037
|
+
apiVersion: 1,
|
|
1038
|
+
label: "POP Field Force",
|
|
1039
|
+
description: "Pull particles along a field's gradient and around its contours. The calligraphy force.",
|
|
1040
|
+
icon: "Waves",
|
|
1041
|
+
runsOn: "portable",
|
|
1042
|
+
inputs: {
|
|
1043
|
+
particles: { kind: "data", type: "geometry" },
|
|
1044
|
+
/** The field: points carrying `N` (the gradient) and optionally `level`. */
|
|
1045
|
+
field: { kind: "data", type: "geometry" }
|
|
1046
|
+
},
|
|
1047
|
+
outputs: { geometry: { kind: "data", type: "geometry" } },
|
|
1048
|
+
props: {
|
|
1049
|
+
normal: {
|
|
1050
|
+
type: "float",
|
|
1051
|
+
default: 0,
|
|
1052
|
+
min: -1e5,
|
|
1053
|
+
max: 1e5,
|
|
1054
|
+
label: "Normal",
|
|
1055
|
+
description: "Along N, toward the spine. Negative pushes away."
|
|
1056
|
+
},
|
|
1057
|
+
tangential: {
|
|
1058
|
+
type: "float",
|
|
1059
|
+
default: 2600,
|
|
1060
|
+
min: -1e5,
|
|
1061
|
+
max: 1e5,
|
|
1062
|
+
label: "Tangential",
|
|
1063
|
+
description: "Perpendicular to N, around the outline. Sign picks the direction."
|
|
1064
|
+
},
|
|
1065
|
+
radius: {
|
|
1066
|
+
type: "float",
|
|
1067
|
+
default: 20,
|
|
1068
|
+
min: 0,
|
|
1069
|
+
max: 1e4,
|
|
1070
|
+
label: "Radius",
|
|
1071
|
+
description: "How far a field sample reaches."
|
|
1072
|
+
},
|
|
1073
|
+
level: {
|
|
1074
|
+
type: "float",
|
|
1075
|
+
default: 0.28,
|
|
1076
|
+
min: 0,
|
|
1077
|
+
max: 1,
|
|
1078
|
+
step: 0.01,
|
|
1079
|
+
label: "Level",
|
|
1080
|
+
description: "The contour to hold, rather than climbing to the peak."
|
|
1081
|
+
},
|
|
1082
|
+
hold: {
|
|
1083
|
+
type: "float",
|
|
1084
|
+
default: 5200,
|
|
1085
|
+
min: 0,
|
|
1086
|
+
max: 1e5,
|
|
1087
|
+
label: "Hold",
|
|
1088
|
+
description: "How hard to hold that contour."
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
};
|
|
1092
|
+
function executeFieldForce(context) {
|
|
1093
|
+
const incoming = context.inputs.particles;
|
|
1094
|
+
if (!incoming) {
|
|
1095
|
+
context.outputs.geometry.set(emptyParticleGeometry(2));
|
|
1096
|
+
return;
|
|
1097
|
+
}
|
|
1098
|
+
const flow = context.inputs.field;
|
|
1099
|
+
const props = context.props;
|
|
1100
|
+
if (!flow || flow.pointCount === 0 || props.normal === 0 && props.tangential === 0) {
|
|
1101
|
+
context.outputs.geometry.set(incoming);
|
|
1102
|
+
return;
|
|
1103
|
+
}
|
|
1104
|
+
if (!flow.point.N)
|
|
1105
|
+
throw new Error(
|
|
1106
|
+
`${context.nodeId}.field requires point attribute N when the field contains points`
|
|
1107
|
+
);
|
|
1108
|
+
const state = ParticleState.fromGeometry(incoming);
|
|
1109
|
+
context.outputs.geometry.set(
|
|
1110
|
+
accumulateForce(
|
|
1111
|
+
incoming,
|
|
1112
|
+
flowField({
|
|
1113
|
+
positions: flow.point.P.data,
|
|
1114
|
+
positionSize: flow.point.P.size,
|
|
1115
|
+
directions: flow.point.N.data,
|
|
1116
|
+
...flow.point.level && flow.point.level.storage !== "string" ? { levels: flow.point.level.data } : {},
|
|
1117
|
+
count: flow.pointCount,
|
|
1118
|
+
normal: props.normal,
|
|
1119
|
+
tangential: props.tangential,
|
|
1120
|
+
radius: props.radius,
|
|
1121
|
+
level: props.level,
|
|
1122
|
+
hold: props.hold
|
|
1123
|
+
}).accelerate(state, state.size),
|
|
1124
|
+
state.size
|
|
1125
|
+
)
|
|
1126
|
+
);
|
|
1127
|
+
}
|
|
1128
|
+
var trailDefinition = {
|
|
1129
|
+
apiVersion: 1,
|
|
1130
|
+
label: "POP Trail",
|
|
1131
|
+
description: "Build trails from a Feedback container's history.",
|
|
1132
|
+
icon: "Activity",
|
|
1133
|
+
runsOn: "portable",
|
|
1134
|
+
inputs: {
|
|
1135
|
+
/** The container's `history` output: every kept step, oldest first. */
|
|
1136
|
+
history: { kind: "data", type: "array" },
|
|
1137
|
+
/** The current state, for `preserve` and for the newest point of each trail. */
|
|
1138
|
+
particles: { kind: "data", type: "geometry" }
|
|
1139
|
+
},
|
|
1140
|
+
outputs: { geometry: { kind: "data", type: "geometry" } },
|
|
1141
|
+
props: {
|
|
1142
|
+
result: {
|
|
1143
|
+
type: "string",
|
|
1144
|
+
default: "polylines",
|
|
1145
|
+
control: "select",
|
|
1146
|
+
options: [
|
|
1147
|
+
{ value: "polylines", label: "Polylines - one line per particle" },
|
|
1148
|
+
{ value: "preserve", label: "Preserve - pass the points through" }
|
|
1149
|
+
],
|
|
1150
|
+
label: "Result"
|
|
1151
|
+
},
|
|
1152
|
+
length: { type: "int", default: 12, min: 1, max: 1e5, label: "Length" },
|
|
1153
|
+
increment: { type: "int", default: 1, min: 1, max: 1e3, label: "Increment" },
|
|
1154
|
+
velocityscale: { type: "float", default: 1, min: 0, max: 100, step: 0.01, label: "Velocity Scale" }
|
|
1155
|
+
}
|
|
1156
|
+
};
|
|
1157
|
+
function executeTrail(context) {
|
|
1158
|
+
const current = context.inputs.particles;
|
|
1159
|
+
if (!current) {
|
|
1160
|
+
context.outputs.geometry.set(emptyParticleGeometry(2));
|
|
1161
|
+
return;
|
|
1162
|
+
}
|
|
1163
|
+
const history = context.inputs.history ?? [];
|
|
1164
|
+
const frames = [];
|
|
1165
|
+
for (const [index, entry] of history.entries()) {
|
|
1166
|
+
if (typeof entry !== "object" || entry === null || entry.kind !== "geometry")
|
|
1167
|
+
throw new Error(`${context.nodeId}.history[${index}] must be particle geometry`);
|
|
1168
|
+
const violations = checkParticleContract(entry);
|
|
1169
|
+
if (violations.length > 0)
|
|
1170
|
+
throw new Error(
|
|
1171
|
+
`${context.nodeId}.history[${index}] is not particle geometry: ${violations.map((violation) => violation.reason).join("; ")}`
|
|
1172
|
+
);
|
|
1173
|
+
frames.push(trailFrame(entry));
|
|
1174
|
+
}
|
|
1175
|
+
const props = context.props;
|
|
1176
|
+
context.outputs.geometry.set(
|
|
1177
|
+
buildTrails(frames, current, {
|
|
1178
|
+
result: props.result === "preserve" ? "preserve" : "polylines",
|
|
1179
|
+
length: props.length,
|
|
1180
|
+
increment: props.increment,
|
|
1181
|
+
velocityscale: props.velocityscale
|
|
1182
|
+
})
|
|
1183
|
+
);
|
|
1184
|
+
}
|
|
1185
|
+
var simulateDefinition = {
|
|
1186
|
+
apiVersion: 1,
|
|
1187
|
+
label: "POP Simulate",
|
|
1188
|
+
description: "Re-simulate a particle system from frame zero to this frame.",
|
|
1189
|
+
icon: "Rewind",
|
|
1190
|
+
runsOn: "portable",
|
|
1191
|
+
inputs: {
|
|
1192
|
+
/** Where particles are born. Its points are the birth positions. */
|
|
1193
|
+
geometry: { kind: "data", type: "geometry" },
|
|
1194
|
+
/** The frame to simulate up to. Drive it with `$F`. An input rather than
|
|
1195
|
+
* a prop because the whole node is a function of it, and because a node
|
|
1196
|
+
* that read the clock itself would be the thing `AGENTS.md` forbids. */
|
|
1197
|
+
frame: { kind: "data", type: "float", default: 1 },
|
|
1198
|
+
/**
|
|
1199
|
+
* Points particles are drawn toward. Houdini's POP Attract.
|
|
1200
|
+
*
|
|
1201
|
+
* **Geometry rather than an image, and that was a correction.** The
|
|
1202
|
+
* original piece renders its word to a canvas, blurs it, reads the pixels
|
|
1203
|
+
* back and takes central differences — so its target really is a raster
|
|
1204
|
+
* field, and an `image` input was the first thing tried here. It was wrong
|
|
1205
|
+
* for this node: decoding an image needs a capability, the media
|
|
1206
|
+
* capability hands back a host-specific lease rather than a portable
|
|
1207
|
+
* raster, and a POP node doing IO stops being the pure arithmetic that
|
|
1208
|
+
* makes it cook identically in both hosts.
|
|
1209
|
+
*
|
|
1210
|
+
* So the rasterising belongs upstream, in whatever node knows about text
|
|
1211
|
+
* or photographs, and what reaches the solver is a set of target points.
|
|
1212
|
+
* `fieldForce` in `pop/forces.ts` still takes a sampled raster for a node
|
|
1213
|
+
* that has one; this input is the composable half.
|
|
1214
|
+
*/
|
|
1215
|
+
attract: { kind: "data", type: "geometry" },
|
|
1216
|
+
/**
|
|
1217
|
+
* A flow field, as points carrying `N`.
|
|
1218
|
+
*
|
|
1219
|
+
* Marcus's description of the original: *"a delicate balance between a
|
|
1220
|
+
* force pulling the particles towards the centre spine of the type and
|
|
1221
|
+
* tangentially around their outlines."* This is that field — the gradient
|
|
1222
|
+
* of a BLURRED letterform, whose ridge runs down each stroke's middle, so
|
|
1223
|
+
* the gradient points at the spine and its perpendicular runs along the
|
|
1224
|
+
* outline. `field_normal` and `field_tangential` are the two halves of the
|
|
1225
|
+
* balance.
|
|
1226
|
+
*/
|
|
1227
|
+
field: { kind: "data", type: "geometry" }
|
|
1228
|
+
},
|
|
1229
|
+
outputs: {
|
|
1230
|
+
/** The particles, one point each. */
|
|
1231
|
+
geometry: { kind: "data", type: "geometry" },
|
|
1232
|
+
/**
|
|
1233
|
+
* The trails, one open polyline per particle.
|
|
1234
|
+
*
|
|
1235
|
+
* Emitted here rather than by a standalone `Trail` node, and that is a
|
|
1236
|
+
* consequence of re-simulation rather than a shortcut. Houdini's Trail SOP
|
|
1237
|
+
* reads a rolling window the cook already holds, because Houdini's cook is
|
|
1238
|
+
* stateful; Cascade's node is a pure function of its inputs, so a
|
|
1239
|
+
* standalone `Trail` can only see the frame on its port — one frame, which
|
|
1240
|
+
* draws nothing. This node replays every frame from zero, so it has the
|
|
1241
|
+
* whole history for free, and a node that draws nothing would be worse than
|
|
1242
|
+
* no node.
|
|
1243
|
+
*
|
|
1244
|
+
* When `CookScheduler` owns the rolling window the plan describes,
|
|
1245
|
+
* `cascade.pop.Trail` becomes a real node and this output stays as the
|
|
1246
|
+
* cheap path. The parameters are Houdini's either way.
|
|
1247
|
+
*/
|
|
1248
|
+
trails: { kind: "data", type: "geometry" }
|
|
1249
|
+
},
|
|
1250
|
+
props: {
|
|
1251
|
+
/** Frames of history in the trails. Houdini's Trail `length`. */
|
|
1252
|
+
trail_length: { type: "int", default: 14, min: 1, max: 500 },
|
|
1253
|
+
/** Use every nth frame. Houdini's `increment`. */
|
|
1254
|
+
trail_increment: { type: "int", default: 1, min: 1, max: 32 },
|
|
1255
|
+
/**
|
|
1256
|
+
* Re-simulation, and why this is the first implementation rather than a
|
|
1257
|
+
* placeholder for a cached one.
|
|
1258
|
+
*
|
|
1259
|
+
* `PLAN particles.md`: re-simulation from the start frame is the
|
|
1260
|
+
* *definition* of correctness, and a cache is an accelerator holding
|
|
1261
|
+
* nothing re-simulation would not have produced. So this node is the
|
|
1262
|
+
* reference: it reads a frame, replays every step from zero, and depends on
|
|
1263
|
+
* no state whatsoever. Scrubbing is O(frame) and that is the honest cost —
|
|
1264
|
+
* a checkpoint cache makes it fast later without changing a single output,
|
|
1265
|
+
* which is exactly the property a cache should have.
|
|
1266
|
+
*/
|
|
1267
|
+
substeps: { type: "int", default: 1, min: 1, max: 32 },
|
|
1268
|
+
timestep: { type: "float", default: 1 / 24, min: 1 / 240, max: 1, step: 1 / 240 },
|
|
1269
|
+
/** Per frame, as on Source. */
|
|
1270
|
+
impulse: { type: "int", default: 6, min: 0, max: 1e5 },
|
|
1271
|
+
life: { type: "float", default: 4, min: 0, max: 3600, step: 0.1 },
|
|
1272
|
+
lifevar: { type: "float", default: 0.4, min: 0, max: 1, step: 0.01 },
|
|
1273
|
+
force: { type: "vec2", default: [0, 0] },
|
|
1274
|
+
airresist: { type: "float", default: 0.4, min: 0, max: 20, step: 0.05 },
|
|
1275
|
+
noise_amplitude: { type: "float", default: 14, min: 0, max: 500, step: 0.5 },
|
|
1276
|
+
noise_frequency: { type: "float", default: 0.02, min: 1e-3, max: 10, step: 1e-3 },
|
|
1277
|
+
/** Turns per second of field evolution. */
|
|
1278
|
+
noise_evolve: { type: "float", default: 0.08, min: -10, max: 10, step: 0.01 },
|
|
1279
|
+
/** Toward the spine, up the field's gradient. */
|
|
1280
|
+
field_normal: { type: "float", default: 0, min: -2e4, max: 2e4, step: 1 },
|
|
1281
|
+
/** Around the outline, perpendicular to the gradient. Sign picks which way. */
|
|
1282
|
+
field_tangential: { type: "float", default: 0, min: -2e4, max: 2e4, step: 1 },
|
|
1283
|
+
/** Field samples beyond this are ignored, and it is the grid's cell size. */
|
|
1284
|
+
field_radius: { type: "float", default: 24, min: 0.5, max: 2e3, step: 0.5 },
|
|
1285
|
+
/**
|
|
1286
|
+
* The contour to hold, as a field level 0..1, with `field_hold` as how
|
|
1287
|
+
* firmly.
|
|
1288
|
+
*
|
|
1289
|
+
* Marcus: *"the lines flow tangentially around the letters in a bundle …
|
|
1290
|
+
* loosely describing the type."* A bundle is a contour, so holding a level
|
|
1291
|
+
* and letting the tangent carry particles round it is the mechanism —
|
|
1292
|
+
* pulling them to the spine instead collapses each letter to a point, and
|
|
1293
|
+
* pure tangential flow lets every particle converge onto one level set.
|
|
1294
|
+
*/
|
|
1295
|
+
field_level: { type: "float", default: 0.35, min: 0, max: 1, step: 0.01 },
|
|
1296
|
+
field_hold: { type: "float", default: 0, min: 0, max: 4e4, step: 10 },
|
|
1297
|
+
/**
|
|
1298
|
+
* Scatter births across this box instead of on the source geometry's
|
|
1299
|
+
* points, `[minX, minY, maxX, maxY]`. All zero uses the points.
|
|
1300
|
+
*
|
|
1301
|
+
* Houdini's POP Source has the same choice — *Scatter onto Surface* against
|
|
1302
|
+
* *Points* — and Marcus's note is that the original spawns *around* the
|
|
1303
|
+
* type rather than on it: *"The particles should spawn in the area around
|
|
1304
|
+
* the type … and then the forces should drag them around."* Birthing on
|
|
1305
|
+
* the letterforms puts every particle where it is already going, so
|
|
1306
|
+
* nothing travels and the field has nothing to reveal.
|
|
1307
|
+
*/
|
|
1308
|
+
birth_area: { type: "vec4", default: [0, 0, 0, 0] },
|
|
1309
|
+
/**
|
|
1310
|
+
* A colour per particle, drawn at birth and fixed for its life.
|
|
1311
|
+
*
|
|
1312
|
+
* Hashed on the id, so the same particle gets the same colour however many
|
|
1313
|
+
* times the frame is re-simulated — a colour drawn from a stream would
|
|
1314
|
+
* change on every scrub, which reads as the render being unstable.
|
|
1315
|
+
*
|
|
1316
|
+
* `[from, to]` in turns around the wheel. Equal values give one hue; a
|
|
1317
|
+
* narrow span is usually what a piece wants, because a full-spectrum
|
|
1318
|
+
* random set reads as a test pattern rather than as a palette.
|
|
1319
|
+
*/
|
|
1320
|
+
birth_hue: { type: "vec2", default: [0, 0], min: 0, max: 1, step: 0.01 },
|
|
1321
|
+
birth_saturation: { type: "float", default: 0.6, min: 0, max: 1, step: 0.01 },
|
|
1322
|
+
birth_value: { type: "float", default: 1, min: 0, max: 1, step: 0.01 },
|
|
1323
|
+
/** How hard the targets pull. Houdini's POP Attract strength. */
|
|
1324
|
+
attract_amplitude: { type: "float", default: 0, min: 0, max: 2e4, step: 1 },
|
|
1325
|
+
/** Beyond this distance a target is ignored, which is what keeps the pull
|
|
1326
|
+
* local and the cost bounded. 0 means every target pulls every particle. */
|
|
1327
|
+
attract_radius: { type: "float", default: 60, min: 0, max: 5e3, step: 1 },
|
|
1328
|
+
separate_radius: { type: "float", default: 0, min: 0, max: 500, step: 0.5 },
|
|
1329
|
+
separate_strength: { type: "float", default: 12, min: 0, max: 500, step: 0.5 },
|
|
1330
|
+
maxspeed: { type: "float", default: 60, min: 0, max: 1e4, step: 1 },
|
|
1331
|
+
/** `[minX, minY, maxX, maxY]`, or all zero for open space. The original
|
|
1332
|
+
* piece is toroidal. */
|
|
1333
|
+
wrap: { type: "vec4", default: [0, 0, 0, 0] },
|
|
1334
|
+
seed: { type: "int", default: 7, min: 0, max: 999999 }
|
|
1335
|
+
}
|
|
1336
|
+
};
|
|
1337
|
+
function simulationFingerprint(props, geometries) {
|
|
1338
|
+
const parts = [];
|
|
1339
|
+
for (const [name, value] of Object.entries(props)) {
|
|
1340
|
+
if (name === "trail_length" || name === "trail_increment") continue;
|
|
1341
|
+
parts.push(`${name}=${JSON.stringify(value)}`);
|
|
1342
|
+
}
|
|
1343
|
+
for (const geometry of geometries) {
|
|
1344
|
+
parts.push(geometry ? geometryFingerprint(geometry) : "none");
|
|
1345
|
+
}
|
|
1346
|
+
return parts.join("|");
|
|
1347
|
+
}
|
|
1348
|
+
function geometryFingerprint(geometry) {
|
|
1349
|
+
let hash2 = 2166136261;
|
|
1350
|
+
const fold = (value) => {
|
|
1351
|
+
hash2 ^= value | 0;
|
|
1352
|
+
hash2 = Math.imul(hash2, 16777619);
|
|
1353
|
+
};
|
|
1354
|
+
fold(geometry.pointCount);
|
|
1355
|
+
fold(geometry.primitiveCount);
|
|
1356
|
+
for (const [name, attribute] of Object.entries(geometry.point)) {
|
|
1357
|
+
for (let index = 0; index < name.length; index += 1) fold(name.charCodeAt(index));
|
|
1358
|
+
if (attribute.storage === "string") continue;
|
|
1359
|
+
const data = attribute.data;
|
|
1360
|
+
for (let index = 0; index < data.length; index += 1) fold(Math.round(Number(data[index]) * 4096));
|
|
1361
|
+
}
|
|
1362
|
+
return (hash2 >>> 0).toString(36);
|
|
1363
|
+
}
|
|
1364
|
+
function executeSimulate(context) {
|
|
1365
|
+
const props = context.props;
|
|
1366
|
+
const from = context.inputs.geometry;
|
|
1367
|
+
const frames = Math.max(0, Math.floor(context.inputs.frame));
|
|
1368
|
+
let state = ParticleState.fromGeometry(emptyParticleGeometry(2));
|
|
1369
|
+
const history = [];
|
|
1370
|
+
let startFrame = 1;
|
|
1371
|
+
const available = from ? from.pointCount : 0;
|
|
1372
|
+
const source = from?.point.P;
|
|
1373
|
+
const targets = context.inputs.attract;
|
|
1374
|
+
const flow = context.inputs.field;
|
|
1375
|
+
const scatter = props.birth_area.some((value) => value !== 0) ? props.birth_area : null;
|
|
1376
|
+
const canBirth = scatter !== null || available > 0 && source !== void 0;
|
|
1377
|
+
const wrap = props.wrap.some((value) => value !== 0) ? [props.wrap[0], props.wrap[1], props.wrap[2], props.wrap[3]] : void 0;
|
|
1378
|
+
const fingerprint = simulationFingerprint(props, [from, targets, flow]);
|
|
1379
|
+
const keep = props.trail_length * props.trail_increment;
|
|
1380
|
+
const resumed = checkpointAtOrBefore(
|
|
1381
|
+
context.nodeId,
|
|
1382
|
+
fingerprint,
|
|
1383
|
+
Math.max(0, frames - keep)
|
|
1384
|
+
);
|
|
1385
|
+
if (resumed) {
|
|
1386
|
+
state = resumed.state;
|
|
1387
|
+
startFrame = resumed.frame + 1;
|
|
1388
|
+
}
|
|
1389
|
+
for (let frame = startFrame; frame <= frames; frame += 1) {
|
|
1390
|
+
if (canBirth && props.impulse > 0) {
|
|
1391
|
+
const born = scatter ? props.impulse : Math.min(props.impulse, available);
|
|
1392
|
+
const position = new Float64Array(born * 2);
|
|
1393
|
+
const life = new Float32Array(born);
|
|
1394
|
+
const colours = props.birth_hue[0] !== props.birth_hue[1] || props.birth_saturation !== 0 ? new Float32Array(born * 4) : null;
|
|
1395
|
+
for (let index = 0; index < born; index += 1) {
|
|
1396
|
+
const id = state.nextId + index;
|
|
1397
|
+
if (scatter) {
|
|
1398
|
+
position[index * 2] = scatter[0] + birthRandom(props.seed, id * 2) * (scatter[2] - scatter[0]);
|
|
1399
|
+
position[index * 2 + 1] = scatter[1] + birthRandom(props.seed, id * 2 + 1) * (scatter[3] - scatter[1]);
|
|
1400
|
+
} else {
|
|
1401
|
+
const pick = (frame * born + index) % available;
|
|
1402
|
+
const points = source.data;
|
|
1403
|
+
position[index * 2] = Number(points[pick * source.size] ?? 0);
|
|
1404
|
+
position[index * 2 + 1] = Number(points[pick * source.size + 1] ?? 0);
|
|
1405
|
+
}
|
|
1406
|
+
const jitter = birthRandom(props.seed, id);
|
|
1407
|
+
life[index] = props.life * (1 - props.lifevar * jitter);
|
|
1408
|
+
if (colours) {
|
|
1409
|
+
const hue = props.birth_hue[0] + birthRandom(props.seed, id * 4 + 3) * (props.birth_hue[1] - props.birth_hue[0]);
|
|
1410
|
+
const [r, g, b] = hsvToRgb(hue, props.birth_saturation, props.birth_value);
|
|
1411
|
+
colours[index * 4] = r;
|
|
1412
|
+
colours[index * 4 + 1] = g;
|
|
1413
|
+
colours[index * 4 + 2] = b;
|
|
1414
|
+
colours[index * 4 + 3] = 1;
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
state = state.born({ position, life, ...colours ? { colour: colours } : {} });
|
|
1418
|
+
}
|
|
1419
|
+
const forces = [];
|
|
1420
|
+
if (props.force[0] !== 0 || props.force[1] !== 0) forces.push(gravity(props.force));
|
|
1421
|
+
if (props.airresist > 0) forces.push(drag(props.airresist));
|
|
1422
|
+
if (props.noise_amplitude > 0) {
|
|
1423
|
+
forces.push(noiseField({
|
|
1424
|
+
seed: props.seed,
|
|
1425
|
+
frequency: props.noise_frequency,
|
|
1426
|
+
amplitude: props.noise_amplitude,
|
|
1427
|
+
// Derived from the frame the caller stated, not from a clock — which
|
|
1428
|
+
// is what keeps the whole node a pure function of `frame`.
|
|
1429
|
+
evolve: frame * props.timestep * props.noise_evolve
|
|
1430
|
+
}));
|
|
1431
|
+
}
|
|
1432
|
+
if (flow && flow.pointCount > 0 && flow.point.N && (props.field_normal !== 0 || props.field_tangential !== 0)) {
|
|
1433
|
+
forces.push(flowField({
|
|
1434
|
+
positions: flow.point.P.data,
|
|
1435
|
+
positionSize: flow.point.P.size,
|
|
1436
|
+
directions: flow.point.N.data,
|
|
1437
|
+
...flow.point.level && flow.point.level.storage !== "string" ? { levels: flow.point.level.data } : {},
|
|
1438
|
+
count: flow.pointCount,
|
|
1439
|
+
normal: props.field_normal,
|
|
1440
|
+
tangential: props.field_tangential,
|
|
1441
|
+
radius: props.field_radius,
|
|
1442
|
+
level: props.field_level,
|
|
1443
|
+
hold: props.field_hold
|
|
1444
|
+
}));
|
|
1445
|
+
}
|
|
1446
|
+
if (targets && targets.pointCount > 0 && props.attract_amplitude > 0) {
|
|
1447
|
+
forces.push(attract({
|
|
1448
|
+
targets: targets.point.P.data,
|
|
1449
|
+
targetSize: targets.point.P.size,
|
|
1450
|
+
targetCount: targets.pointCount,
|
|
1451
|
+
amplitude: props.attract_amplitude,
|
|
1452
|
+
radius: props.attract_radius
|
|
1453
|
+
}));
|
|
1454
|
+
}
|
|
1455
|
+
if (props.separate_radius > 0) {
|
|
1456
|
+
forces.push(separation({
|
|
1457
|
+
radius: props.separate_radius,
|
|
1458
|
+
strength: props.separate_strength
|
|
1459
|
+
}));
|
|
1460
|
+
}
|
|
1461
|
+
for (let substep = 0; substep < props.substeps; substep += 1) {
|
|
1462
|
+
state = step(state, forces, {
|
|
1463
|
+
timestep: props.timestep / props.substeps,
|
|
1464
|
+
...props.maxspeed > 0 ? { maxSpeed: props.maxspeed } : {},
|
|
1465
|
+
...wrap ? { wrap } : {}
|
|
1466
|
+
});
|
|
1467
|
+
}
|
|
1468
|
+
history.push({
|
|
1469
|
+
id: Int32Array.from(state.id),
|
|
1470
|
+
position: Float64Array.from(state.position),
|
|
1471
|
+
size: state.size
|
|
1472
|
+
});
|
|
1473
|
+
if (history.length > keep) history.splice(0, history.length - keep);
|
|
1474
|
+
offerCheckpoint(context.nodeId, fingerprint, frame, state);
|
|
1475
|
+
}
|
|
1476
|
+
const geometry = state.toGeometry();
|
|
1477
|
+
context.outputs.geometry.set(geometry);
|
|
1478
|
+
context.outputs.trails.set(
|
|
1479
|
+
buildTrails(history, geometry, {
|
|
1480
|
+
result: "polylines",
|
|
1481
|
+
length: props.trail_length,
|
|
1482
|
+
increment: props.trail_increment
|
|
1483
|
+
})
|
|
1484
|
+
);
|
|
1485
|
+
}
|
|
1486
|
+
var simulateRegistration = {
|
|
1487
|
+
kind: "definition-v1",
|
|
1488
|
+
moduleId: "cascade.pop.Simulate",
|
|
1489
|
+
definition: simulateDefinition,
|
|
1490
|
+
loadExecute: async () => executeSimulate
|
|
1491
|
+
};
|
|
1492
|
+
var sourceRegistration = {
|
|
1493
|
+
kind: "definition-v1",
|
|
1494
|
+
moduleId: "cascade.pop.Source",
|
|
1495
|
+
definition: sourceDefinition,
|
|
1496
|
+
loadExecute: async () => executeSource
|
|
1497
|
+
};
|
|
1498
|
+
var solverRegistration = {
|
|
1499
|
+
kind: "definition-v1",
|
|
1500
|
+
moduleId: "cascade.pop.Solver",
|
|
1501
|
+
definition: solverDefinition,
|
|
1502
|
+
loadExecute: async () => executeSolver
|
|
1503
|
+
};
|
|
1504
|
+
var noiseForceRegistration = {
|
|
1505
|
+
kind: "definition-v1",
|
|
1506
|
+
moduleId: "cascade.pop.NoiseForce",
|
|
1507
|
+
definition: noiseForceDefinition,
|
|
1508
|
+
loadExecute: async () => executeNoiseForce
|
|
1509
|
+
};
|
|
1510
|
+
var separateRegistration = {
|
|
1511
|
+
kind: "definition-v1",
|
|
1512
|
+
moduleId: "cascade.pop.Separate",
|
|
1513
|
+
definition: separateDefinition,
|
|
1514
|
+
loadExecute: async () => executeSeparate
|
|
1515
|
+
};
|
|
1516
|
+
var fieldForceRegistration = {
|
|
1517
|
+
kind: "definition-v1",
|
|
1518
|
+
moduleId: "cascade.pop.FieldForce",
|
|
1519
|
+
definition: fieldForceDefinition,
|
|
1520
|
+
loadExecute: async () => executeFieldForce
|
|
1521
|
+
};
|
|
1522
|
+
var trailRegistration = {
|
|
1523
|
+
kind: "definition-v1",
|
|
1524
|
+
moduleId: "cascade.pop.Trail",
|
|
1525
|
+
definition: trailDefinition,
|
|
1526
|
+
loadExecute: async () => executeTrail
|
|
1527
|
+
};
|
|
1528
|
+
var popNodeRegistrations = Object.freeze([
|
|
1529
|
+
simulateRegistration,
|
|
1530
|
+
sourceRegistration,
|
|
1531
|
+
solverRegistration,
|
|
1532
|
+
noiseForceRegistration,
|
|
1533
|
+
separateRegistration,
|
|
1534
|
+
fieldForceRegistration,
|
|
1535
|
+
trailRegistration
|
|
1536
|
+
]);
|
|
1537
|
+
var popNodeDefinitions = Object.freeze([
|
|
1538
|
+
["cascade.pop.Simulate", simulateDefinition],
|
|
1539
|
+
["cascade.pop.Source", sourceDefinition],
|
|
1540
|
+
["cascade.pop.Solver", solverDefinition],
|
|
1541
|
+
["cascade.pop.NoiseForce", noiseForceDefinition],
|
|
1542
|
+
["cascade.pop.Separate", separateDefinition],
|
|
1543
|
+
["cascade.pop.FieldForce", fieldForceDefinition],
|
|
1544
|
+
["cascade.pop.Trail", trailDefinition]
|
|
1545
|
+
]);
|
|
1546
|
+
|
|
1547
|
+
export {
|
|
1548
|
+
popNodeRegistrations,
|
|
1549
|
+
popNodeDefinitions
|
|
1550
|
+
};
|
|
1551
|
+
//# sourceMappingURL=chunk-KH4MUBKF.js.map
|