@aethervtt/shard-scatter 0.0.0-stage → 0.2.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.
@@ -0,0 +1,10 @@
1
+ import { type World } from '@aethervtt/shard-core';
2
+ import type { AppMethod } from '@aethervtt/shard-runtime';
3
+ import { type SurfaceScatter } from './runtime.js';
4
+ /** A scattered surface (a planet or a ScatterSurface entity), by id or scene path; the only one when omitted. */
5
+ export declare function resolveSurface(world: World, ref: unknown): SurfaceScatter;
6
+ /** Placements of prop rules within `radius` of a surface-frame point, nearest first. */
7
+ export declare function samplePlacements(ss: SurfaceScatter, x: number, y: number, z: number, radius: number, frame: number, removed?: {
8
+ props: string[];
9
+ }): Record<string, unknown>[];
10
+ export declare const scatterMethods: AppMethod[];
@@ -0,0 +1,341 @@
1
+ import {
2
+ defineSchema,
3
+ findComponent,
4
+ ShardError,
5
+ t
6
+ } from "@aethervtt/shard-core";
7
+ import { GlobalTransform, worldPosition64 } from "@aethervtt/shard-transform";
8
+ import { isRemoved, Removed, removedKey, ScatterBudget } from "./components.js";
9
+ import { FOLIAGE } from "./rules.js";
10
+ import { chunkState, finalPlacements, Scatter } from "./runtime.js";
11
+ import {
12
+ P_CELL,
13
+ P_ITEM,
14
+ P_SCALE,
15
+ P_VARIANT,
16
+ P_X,
17
+ PLACEMENT_STRIDE
18
+ } from "./surface.js";
19
+ function scenePath(world, entity) {
20
+ const member = findComponent("scene/SceneMember");
21
+ if (!member || !world.isAlive(entity)) return null;
22
+ const v = world.tryGet(entity, member);
23
+ return v?.path || null;
24
+ }
25
+ function resolveSurface(world, ref) {
26
+ const surfaces = world.tryResource(Scatter)?.surfaces;
27
+ if (!surfaces || surfaces.size === 0) {
28
+ throw new ShardError("scatter/no-surface", "Nothing in the world scatters", {
29
+ hint: "Give a Planet (or a Biome) a scatter set, or add scatter/ScatterSurface to a mesh entity; and add the scatter plugin."
30
+ });
31
+ }
32
+ if (ref === void 0 || ref === null || ref === "") {
33
+ if (surfaces.size > 1) {
34
+ throw new ShardError("scatter/which-surface", `${surfaces.size} surfaces scatter; name one`, {
35
+ hint: "Pass surface: an entity id or a scene path (scatter.describe lists them)."
36
+ });
37
+ }
38
+ return [...surfaces.values()][0];
39
+ }
40
+ for (const ss of surfaces.values()) {
41
+ if (ss.surface.entity === ref) return ss;
42
+ const path = scenePath(world, ss.surface.entity);
43
+ if (typeof ref === "string" && path && (path === ref || path.endsWith(`/${ref}`))) return ss;
44
+ }
45
+ throw new ShardError(
46
+ "scatter/not-a-surface",
47
+ `No scattered surface matches ${JSON.stringify(ref)}`,
48
+ {
49
+ hint: "scatter.describe lists every surface with its entity and scene path."
50
+ }
51
+ );
52
+ }
53
+ const round = (v, digits = 3) => {
54
+ const k = 10 ** digits;
55
+ return Math.round(v * k) / k;
56
+ };
57
+ function describeSurface(world, ss) {
58
+ const surface = ss.surface;
59
+ const rules = surface.rules.map((rule) => {
60
+ const s = ss.stats[rule.index];
61
+ const out = {
62
+ name: rule.name,
63
+ id: rule.id,
64
+ kind: rule.kind === FOLIAGE ? "foliage" : "prop",
65
+ biome: rule.biome,
66
+ range: rule.range,
67
+ cellsPerChunk: surface.cells[rule.index]
68
+ };
69
+ if (rule.kind === FOLIAGE) {
70
+ const f = ss.foliage.get(rule.index);
71
+ let drawn = 0;
72
+ let shadows = 0;
73
+ if (f) {
74
+ for (const view of f.layer.views.keys()) {
75
+ const v = f.layer.visible(view);
76
+ drawn += v.drawn;
77
+ shadows += v.shadows;
78
+ }
79
+ }
80
+ out.foliage = f ? {
81
+ chunks: f.layer.chunkCount,
82
+ capacity: f.layer.capacity,
83
+ visible: drawn,
84
+ shadowCasters: shadows,
85
+ meshes: f.layer.options.meshes.length
86
+ } : {
87
+ chunks: 0,
88
+ capacity: 0,
89
+ visible: 0,
90
+ shadowCasters: 0,
91
+ meshes: 0,
92
+ note: "No renderer: foliage is GPU-only."
93
+ };
94
+ } else {
95
+ out.chunksInRange = s?.chunks ?? 0;
96
+ out.placed = s?.placed ?? 0;
97
+ out.spawned = s?.spawned ?? 0;
98
+ out.candidates = s?.candidates ?? 0;
99
+ }
100
+ return out;
101
+ });
102
+ return {
103
+ entity: surface.entity,
104
+ path: scenePath(world, surface.entity),
105
+ kind: surface.kind,
106
+ ready: ss.ready,
107
+ waiting: ss.waiting,
108
+ problem: ss.problem ? { code: ss.problem.code, message: ss.problem.message, path: ss.problem.path } : null,
109
+ rules,
110
+ chunksCached: ss.chunks.size,
111
+ spawnedLastFrame: ss.spawnedLast,
112
+ despawnedLastFrame: ss.despawnedLast,
113
+ msLastFrame: round(ss.ms)
114
+ };
115
+ }
116
+ const surfaceField = t.json({
117
+ description: "The surface: a planet or ScatterSurface entity id or scene path (default: the only one)."
118
+ });
119
+ const probe = new Float64Array(3);
120
+ function samplePlacements(ss, x, y, z, radius, frame, removed) {
121
+ const surface = ss.surface;
122
+ const out = [];
123
+ for (const rule of surface.rules) {
124
+ if (rule.kind === FOLIAGE) continue;
125
+ const seen = /* @__PURE__ */ new Map();
126
+ surface.upAt(x, y, z, probe);
127
+ const steps = 4;
128
+ for (let j = -steps; j <= steps; j++) {
129
+ for (let i = -steps; i <= steps; i++) {
130
+ const s = radius / steps;
131
+ const ux = probe[0];
132
+ const uy = probe[1];
133
+ const uz = probe[2];
134
+ const ax = Math.abs(uy) < 0.9 ? 0 : 1;
135
+ const ay = Math.abs(uy) < 0.9 ? 1 : 0;
136
+ let e1x = ay * uz;
137
+ let e1y = -ax * uz;
138
+ let e1z = ax * uy - ay * ux;
139
+ const l = Math.sqrt(e1x * e1x + e1y * e1y + e1z * e1z) || 1;
140
+ e1x /= l;
141
+ e1y /= l;
142
+ e1z /= l;
143
+ const e2x = uy * e1z - uz * e1y;
144
+ const e2y = uz * e1x - ux * e1z;
145
+ const e2z = ux * e1y - uy * e1x;
146
+ const c = surface.chunkAt(
147
+ rule,
148
+ x + (e1x * i + e2x * j) * s,
149
+ y + (e1y * i + e2y * j) * s,
150
+ z + (e1z * i + e2z * j) * s
151
+ );
152
+ seen.set(c.key, c);
153
+ }
154
+ }
155
+ for (const chunk of seen.values()) {
156
+ const id = `${rule.index}|${chunk.key}`;
157
+ let c = ss.chunks.get(id);
158
+ if (!c) {
159
+ c = chunkState(id, chunk, frame);
160
+ ss.chunks.set(id, c);
161
+ }
162
+ const p = finalPlacements(ss, c, frame);
163
+ for (let k = 0; k < p.count; k++) {
164
+ const o = k * PLACEMENT_STRIDE;
165
+ const px = chunk.center[0] + p.data[o + P_X];
166
+ const py = chunk.center[1] + p.data[o + P_X + 1];
167
+ const pz = chunk.center[2] + p.data[o + P_X + 2];
168
+ const d = Math.hypot(px - x, py - y, pz - z);
169
+ if (d > radius) continue;
170
+ const item = rule.items[p.data[o + P_ITEM]];
171
+ const index = p.data[o + P_CELL];
172
+ out.push({
173
+ d,
174
+ v: {
175
+ rule: rule.name,
176
+ set: rule.id.slice(0, rule.id.lastIndexOf(":")),
177
+ item: item.generator || item.prefab?.path || "",
178
+ variant: p.data[o + P_VARIANT],
179
+ position: [round(px), round(py), round(pz)],
180
+ distance: round(d),
181
+ scale: round(p.data[o + P_SCALE]),
182
+ chunk: chunk.key,
183
+ index,
184
+ removed: removed ? isRemoved(removed, removedKey(rule.id, chunk.key, index)) : false
185
+ }
186
+ });
187
+ }
188
+ }
189
+ }
190
+ out.sort((a, b) => a.d - b.d);
191
+ return out.map((o) => o.v);
192
+ }
193
+ function pointOf(world, ss, p) {
194
+ if (Array.isArray(p.position)) return p.position.slice(0, 3);
195
+ if (p.entity !== void 0 && p.entity !== null) {
196
+ let e;
197
+ if (typeof p.entity === "number") e = p.entity;
198
+ else {
199
+ const member = findComponent("scene/SceneMember");
200
+ for (const table of world.allTables()) {
201
+ if (!member || !table.has(member)) continue;
202
+ for (let row = 0; row < table.count; row++) {
203
+ const ent = table.entities[row];
204
+ const path = world.get(ent, member).path;
205
+ if (path === p.entity || path?.endsWith(`/${p.entity}`)) e = ent;
206
+ }
207
+ }
208
+ }
209
+ if (e === void 0 || !world.isAlive(e)) {
210
+ throw new ShardError("scatter/bad-point", `No entity ${JSON.stringify(p.entity)}`, {
211
+ hint: "Pass an entity id or a scene path."
212
+ });
213
+ }
214
+ if (ss.surface.kind === "planet" || ss.surface.kind === "heightfield") {
215
+ return Array.from(worldPosition64(world, e, new Float64Array(3), ss.surface.entity));
216
+ }
217
+ const g = world.get(e, GlobalTransform).matrix;
218
+ const s = world.get(ss.surface.entity, GlobalTransform).matrix;
219
+ const w = [g[3], g[7], g[11]];
220
+ const inv = invertAffine(s);
221
+ const scale = ss.surface.scale;
222
+ return [0, 1, 2].map(
223
+ (r) => (inv[r * 4] * w[0] + inv[r * 4 + 1] * w[1] + inv[r * 4 + 2] * w[2] + inv[r * 4 + 3]) * scale
224
+ );
225
+ }
226
+ if (Array.isArray(p.latlon) && ss.surface.kind === "planet") {
227
+ const [lat, lon] = p.latlon;
228
+ const a = lat * Math.PI / 180;
229
+ const b = lon * Math.PI / 180;
230
+ const r = ss.surface.rt.settings.radius;
231
+ return [Math.cos(a) * Math.sin(b) * r, Math.sin(a) * r, Math.cos(a) * Math.cos(b) * r];
232
+ }
233
+ throw new ShardError("scatter/bad-point", "Say where: position, entity, or latlon (planets)", {
234
+ hint: 'e.g. { "entity": "landing-pad", "radius": 10 }.'
235
+ });
236
+ }
237
+ function invertAffine(m) {
238
+ const a = m[0];
239
+ const b = m[1];
240
+ const c = m[2];
241
+ const d = m[4];
242
+ const e = m[5];
243
+ const f = m[6];
244
+ const g = m[8];
245
+ const h = m[9];
246
+ const i = m[10];
247
+ const det = a * (e * i - f * h) - b * (d * i - f * g) + c * (d * h - e * g) || 1;
248
+ const r = [
249
+ (e * i - f * h) / det,
250
+ (c * h - b * i) / det,
251
+ (b * f - c * e) / det,
252
+ (f * g - d * i) / det,
253
+ (a * i - c * g) / det,
254
+ (c * d - a * f) / det,
255
+ (d * h - e * g) / det,
256
+ (b * g - a * h) / det,
257
+ (a * e - b * d) / det
258
+ ];
259
+ const tx = m[3];
260
+ const ty = m[7];
261
+ const tz = m[11];
262
+ return [
263
+ r[0],
264
+ r[1],
265
+ r[2],
266
+ -(r[0] * tx + r[1] * ty + r[2] * tz),
267
+ r[3],
268
+ r[4],
269
+ r[5],
270
+ -(r[3] * tx + r[4] * ty + r[5] * tz),
271
+ r[6],
272
+ r[7],
273
+ r[8],
274
+ -(r[6] * tx + r[7] * ty + r[8] * tz)
275
+ ];
276
+ }
277
+ const scatterMethods = [
278
+ {
279
+ name: "scatter.describe",
280
+ description: "Scatter as data: per surface (a planet, or a ScatterSurface mesh), whether it is ready (or what it waits for, or its problem), and per rule its kind and range; props: chunks in range, placements, spawned entities, lattice candidates; foliage: GPU chunks, instance capacity, and instances visible and casting shadows (last read back). Also props spawned and despawned last frame and the main-thread milliseconds it took.",
281
+ params: defineSchema("scatter/DescribeParams", { surface: surfaceField }),
282
+ handler: ({ world }, p) => {
283
+ const surfaces = world.tryResource(Scatter)?.surfaces;
284
+ if (!surfaces) return { surfaces: [] };
285
+ const removed = world.tryResource(Removed)?.props.length ?? 0;
286
+ const budget = world.tryResource(ScatterBudget);
287
+ const list = p.surface !== void 0 && p.surface !== null && p.surface !== "" ? [resolveSurface(world, p.surface)] : [...surfaces.values()];
288
+ return { surfaces: list.map((ss) => describeSurface(world, ss)), removed, budget };
289
+ }
290
+ },
291
+ {
292
+ name: "scatter.sample",
293
+ description: 'Prop placements near a point, from the CPU placement (headless-safe, whether or not they are spawned): rule, set, item, variant, position (surface frame), distance, scale, chunk and index, and whether the game removed it. Ask "what is within 10 m of the landing pad" and move the pad. Foliage is GPU-only and not listed.',
294
+ params: defineSchema("scatter/SampleParams", {
295
+ surface: surfaceField,
296
+ position: t.json({
297
+ description: "A point in the surface\u2019s frame: [x, y, z] (planet frame, or the mesh\u2019s local metres)."
298
+ }),
299
+ entity: t.json({
300
+ description: "Or an entity (id or scene path) whose position is the point."
301
+ }),
302
+ latlon: t.json({ description: "Or, on a planet, [lat, lon] in degrees." }),
303
+ radius: t.f32({ default: 10, min: 0.1, max: 500, unit: "m" })
304
+ }),
305
+ handler: ({ world }, p) => {
306
+ const ss = resolveSurface(world, p.surface);
307
+ if (ss.problem) throw ss.problem;
308
+ if (!ss.ready || !ss.surface.ready) {
309
+ throw new ShardError(
310
+ "scatter/not-ready",
311
+ `The surface is waiting for ${ss.waiting ?? "its sets"}`,
312
+ {
313
+ hint: "Sets and item meshes load asynchronously; step a frame and ask again."
314
+ }
315
+ );
316
+ }
317
+ const [x, y, z] = pointOf(world, ss, p);
318
+ const frame = world.resource(Scatter).frame;
319
+ const placements = samplePlacements(
320
+ ss,
321
+ x,
322
+ y,
323
+ z,
324
+ p.radius,
325
+ frame,
326
+ world.tryResource(Removed)
327
+ );
328
+ return {
329
+ point: [round(x), round(y), round(z)],
330
+ radius: p.radius,
331
+ count: placements.length,
332
+ placements
333
+ };
334
+ }
335
+ }
336
+ ];
337
+ export {
338
+ resolveSurface,
339
+ samplePlacements,
340
+ scatterMethods
341
+ };
@@ -0,0 +1,4 @@
1
+ /** Spawned props as dots (a short upright tick per item), colored by rule. */
2
+ export declare const scatterOverlay: import("@aethervtt/shard-render").OverlayDef;
3
+ /** Foliage chunks' borders on the ground (green when placed on the GPU, amber while building). */
4
+ export declare const foliageChunksOverlay: import("@aethervtt/shard-render").OverlayDef;
@@ -0,0 +1,84 @@
1
+ import { hash32 } from "@aethervtt/shard-core";
2
+ import { defineOverlay } from "@aethervtt/shard-render";
3
+ import { Scatter } from "./runtime.js";
4
+ import { P_SCALE, P_X, PLACEMENT_STRIDE } from "./surface.js";
5
+ const rows = new Float32Array(12);
6
+ const a = new Float32Array(3);
7
+ const b = new Float32Array(3);
8
+ const colors = /* @__PURE__ */ new Map();
9
+ function ruleColor(id) {
10
+ let c = colors.get(id);
11
+ if (c) return c;
12
+ let h = 0;
13
+ for (let i = 0; i < id.length; i++) h = hash32(h, id.charCodeAt(i));
14
+ const hue = h % 360 / 60;
15
+ const x = 1 - Math.abs(hue % 2 - 1);
16
+ const rgb = hue < 1 ? [1, x, 0] : hue < 2 ? [x, 1, 0] : hue < 3 ? [0, 1, x] : hue < 4 ? [0, x, 1] : hue < 5 ? [x, 0, 1] : [1, 0, x];
17
+ c = [...rgb, 1];
18
+ colors.set(id, c);
19
+ return c;
20
+ }
21
+ function toWorld(x, y, z, out) {
22
+ out[0] = rows[0] * x + rows[1] * y + rows[2] * z + rows[3];
23
+ out[1] = rows[4] * x + rows[5] * y + rows[6] * z + rows[7];
24
+ out[2] = rows[8] * x + rows[9] * y + rows[10] * z + rows[11];
25
+ return out;
26
+ }
27
+ const scatterOverlay = defineOverlay({
28
+ name: "scatter",
29
+ description: "Scatter placements: a tick at every spawned prop, colored by rule (each rule keeps its color), to see density, spacing and masks at a glance.",
30
+ draw: (world, g) => {
31
+ const state = world.tryResource(Scatter);
32
+ if (!state) return;
33
+ for (const ss of state.surfaces.values()) {
34
+ for (const c of ss.chunks.values()) {
35
+ if (c.root < 0 || !c.placements) continue;
36
+ const rule = ss.surface.rules[c.chunk.rule];
37
+ const color = ruleColor(rule.id);
38
+ ss.surface.chunkTransform(world, c.chunk, rows);
39
+ const p = c.placements;
40
+ ss.surface.upAt(c.chunk.center[0], c.chunk.center[1], c.chunk.center[2], up);
41
+ for (let i = 0; i < p.count; i++) {
42
+ const o = i * PLACEMENT_STRIDE;
43
+ const x = p.data[o + P_X];
44
+ const y = p.data[o + P_X + 1];
45
+ const z = p.data[o + P_X + 2];
46
+ const s = 0.6 * p.data[o + P_SCALE];
47
+ toWorld(x, y, z, a);
48
+ toWorld(x + up[0] * s, y + up[1] * s, z + up[2] * s, b);
49
+ g.line(a, b, color);
50
+ }
51
+ }
52
+ }
53
+ }
54
+ });
55
+ const up = new Float64Array(3);
56
+ const LIVE = [0.3, 1, 0.5, 1];
57
+ const WAITING = [1, 0.75, 0.2, 1];
58
+ const foliageChunksOverlay = defineOverlay({
59
+ name: "foliage-chunks",
60
+ description: "GPU foliage chunks: each one\u2019s border on the ground, green once placed on the GPU and amber while its ground patch is still being built.",
61
+ draw: (world, g) => {
62
+ const state = world.tryResource(Scatter);
63
+ if (!state) return;
64
+ for (const ss of state.surfaces.values()) {
65
+ for (const f of ss.foliage.values()) {
66
+ for (const c of f.chunks.values()) {
67
+ if (!c.corners) continue;
68
+ ss.surface.chunkTransform(world, c.chunk, rows);
69
+ const color = c.slot >= 0 ? LIVE : WAITING;
70
+ for (let k = 0; k < 4; k++) {
71
+ const n = (k + 1) % 4;
72
+ toWorld(c.corners[k * 3], c.corners[k * 3 + 1], c.corners[k * 3 + 2], a);
73
+ toWorld(c.corners[n * 3], c.corners[n * 3 + 1], c.corners[n * 3 + 2], b);
74
+ g.line(a, b, color);
75
+ }
76
+ }
77
+ }
78
+ }
79
+ }
80
+ });
81
+ export {
82
+ foliageChunksOverlay,
83
+ scatterOverlay
84
+ };
@@ -0,0 +1,74 @@
1
+ import type { Entity, Query, World } from '@aethervtt/shard-core';
2
+ import { type NoiseGraph } from '@aethervtt/shard-noise';
3
+ import type { Workers } from '@aethervtt/shard-platform';
4
+ import { type PlanetRuntime } from '@aethervtt/shard-terrain';
5
+ import { type CompiledRule } from './rules.js';
6
+ import { type ChunkPlacements, type FoliagePatch, type PatchJob, type PlacementJob, type Surface, type SurfaceChunk } from './surface.js';
7
+ /** Metres along a face edge per node at `depth` (a quarter circle over 2^depth nodes). */
8
+ export declare const nodeMetres: (radius: number, depth: number) => number;
9
+ /**
10
+ * The quadtree depth a rule places at: props aim for about 256 items a chunk (64–1 024); foliage
11
+ * uses the collider depth (about 1 m between the chunk's 33 vertices, the ground it's drawn on),
12
+ * deeper if a chunk would hold more than one dispatch can place.
13
+ */
14
+ export declare function ruleDepth(rule: CompiledRule, radius: number, colliderDepth: number): number;
15
+ /** Lattice cells per chunk side for a rule at a depth: cells the rule's size, aligned to chunks. */
16
+ export declare function cellsPerChunk(rule: CompiledRule, radius: number, depth: number): number;
17
+ /** A planet's scatter surface: its rules on the cube-sphere quadtree, sampled from its graphs. */
18
+ export declare class PlanetSurface implements Surface {
19
+ readonly kind = "planet";
20
+ readonly entity: Entity;
21
+ rules: CompiledRule[];
22
+ version: number;
23
+ /** Per rule: chunk depth and cells per chunk side. */
24
+ depths: number[];
25
+ cells: number[];
26
+ /** Graph values of each rule's noise mask, by rule index (undefined: no mask). */
27
+ masks: (NoiseGraph | undefined)[];
28
+ readonly rt: PlanetRuntime;
29
+ private readonly cameras;
30
+ constructor(rt: PlanetRuntime, cameras: Query);
31
+ get ready(): boolean;
32
+ /** Rebuilds per-rule depths after the rules or the planet changed. */
33
+ configure(rules: CompiledRule[], masks: (NoiseGraph | undefined)[], version: number): void;
34
+ viewers(world: World, out: {
35
+ points: Float64Array;
36
+ cameras: number;
37
+ anchors: number;
38
+ }): void;
39
+ chunksNear(rule: CompiledRule, viewers: Float64Array, first: number, count: number, range: number, visit: (chunk: SurfaceChunk, distance: number) => void): void;
40
+ /** A chunk of `rule` (its center and extent filled in). */
41
+ chunk(rule: CompiledRule, face: number, depth: number, x: number, y: number): SurfaceChunk;
42
+ chunkAt(rule: CompiledRule, x: number, y: number, z: number): SurfaceChunk;
43
+ upAt(x: number, y: number, z: number, out: Float64Array): Float64Array;
44
+ placeChunkRoot(world: World, root: Entity, chunk: SurfaceChunk): void;
45
+ startPlacement(chunk: SurfaceChunk, due: number, workers: Workers | undefined): PlacementJob;
46
+ finishPlacement(job: PlacementJob): ChunkPlacements;
47
+ /**
48
+ * A chunk's lattice: cells per side, and the chance and jitter of each cell from the chunk's real
49
+ * size (its edges measured across the middle; cells aren't square on a cube-sphere).
50
+ */
51
+ lattice(rule: CompiledRule, chunk: SurfaceChunk): {
52
+ k: number;
53
+ accept: number;
54
+ jitter: number;
55
+ };
56
+ /** The chunk's lattice cells that pass the acceptance roll, as directions and sample points. */
57
+ private candidates;
58
+ startPatch(chunk: SurfaceChunk, workers: Workers | undefined): PatchJob;
59
+ /**
60
+ * The chunk's ground as terrain builds it (the collider chunk's own vertices at the collider
61
+ * depth, so grass stands on the drawn ground), with the rule's density at each vertex: its
62
+ * height, slope and noise masks, and its biome.
63
+ */
64
+ finishPatch(job: PatchJob): FoliagePatch;
65
+ chunkTransform(_world: World, chunk: SurfaceChunk, out: Float32Array): void;
66
+ private sampleInline;
67
+ /** Samples into placements: masks, biome, then each item's transform from its cell's hashes. */
68
+ private assemble;
69
+ }
70
+ /**
71
+ * The rotation taking +Y to `up`, after a yaw about +Y, as a quaternion (xyzw) into `out`. Trig
72
+ * through `sinCos`, so the bits match on every host.
73
+ */
74
+ export declare function writeRotation(ux: number, uy: number, uz: number, yaw: number, out: Float32Array, o: number): void;