telperion 0.0.1 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +532 -0
- package/dist/browser/core.d.ts +195 -0
- package/dist/browser/leaf.d.ts +39 -0
- package/dist/browser/presets.generated.d.ts +278 -0
- package/dist/browser/render.d.ts +165 -0
- package/dist/browser/specimen-wire.d.ts +22 -0
- package/dist/browser/specimen.d.ts +106 -0
- package/dist/field/index.d.ts +43 -0
- package/dist/field/voxelize.d.ts +44 -0
- package/dist/field.js +62 -0
- package/dist/index.d.ts +7 -0
- package/dist/telperion-field.wasm +0 -0
- package/dist/telperion-render.wasm +0 -0
- package/dist/telperion.js +3463 -0
- package/dist/telperion.wasm +0 -0
- package/dist/voxelize.js +98 -0
- package/dist/wasm-source.d.ts +1 -0
- package/dist/wasm-source.js +12 -0
- package/package.json +79 -8
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Daniel Killenberger
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,532 @@
|
|
|
1
|
+
# telperion
|
|
2
|
+
|
|
3
|
+
A procedural tree generator with an authored silhouette, seeded specimens and named species presets. One Rust core grows the structure; consumers choose surface meshes, instanced foliage, structural data or spatial fields.
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import { TreeEngine, TELPERION } from "telperion";
|
|
7
|
+
|
|
8
|
+
const engine = await TreeEngine.create();
|
|
9
|
+
const family = structuredClone(TELPERION);
|
|
10
|
+
family.skeleton.seed = 7;
|
|
11
|
+
const tree = engine.build(family, { surface: true, foliage: true });
|
|
12
|
+
// tree.surface: Float32 positions/normals, Uint32 indices, bounds.
|
|
13
|
+
// tree.foliage: one leaf mesh, twelve bytes a leaf - three Uint32 words with
|
|
14
|
+
// the reference box they decode against - and bounds. `leafTransform` rebuilds
|
|
15
|
+
// the sixteen column-major floats for a caller that wants them.
|
|
16
|
+
engine.release(); // returned arrays are owned copies and remain usable
|
|
17
|
+
engine.dispose();
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
`ORDINARY`, `TELPERION` and `LAURELIN` come from Rust preset metadata. Parameters define the family; the seed selects a specimen. `PRESETS` contains all five named templates; `TWO_TREES` contains Telperion and Laurelin. `createRenderer(canvas)` puts the Rust renderer on a canvas and draws the tree its own module generates; it is the package's only rendering path and it has no runtime dependencies. The native core uses pinned `libm` and `slotmap`; its optional `json` feature supplies the wire schema used by the Wasm binding.
|
|
21
|
+
|
|
22
|
+
For a block-based consumer, request occupancy without constructing a wood surface or transferring render buffers:
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
const engine = await TreeEngine.create();
|
|
26
|
+
const tree = engine.build(family, { field: true });
|
|
27
|
+
const flags = tree.field!.query(new Float64Array([
|
|
28
|
+
0, 1, 0, 0.5, // cell centre x/y/z and half extent; zero means a point
|
|
29
|
+
]));
|
|
30
|
+
// Each byte: wood = 1, foliage = 2; zero is a valid empty cell.
|
|
31
|
+
engine.dispose();
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Field handles expire on the next native build or release; copied query results remain owned. Field construction places retained foliage internally. `{ structure: true }` returns six f64 values per node (xyz, distal/proximal/base radius) and three u32 values (parent, branch, kind). The root parent is `0xffffffff`; kinds are structural 0, branch 1 and twig 2. An empty output selection still generates structure for diagnostics. Invalid inputs throw, and cap diagnostics distinguish incomplete growth from a finished tree.
|
|
35
|
+
|
|
36
|
+
The optional `tree.field!.snapshot()` exports an owned schema-1 CPU field snapshot
|
|
37
|
+
for experiments and other consumers. It contains f64 wood segments and BVH bounds,
|
|
38
|
+
u32 topology, revision, union bounds and extraction timings; the
|
|
39
|
+
[exact layout](scripts/benchmarks/generation.md#portable-snapshot-contract) defines
|
|
40
|
+
each array. Existing builds and queries make no snapshot copies. Extraction needs
|
|
41
|
+
a live field revision: release, rebuild (including a failed native rebuild) and
|
|
42
|
+
disposal invalidate the handle. Already copied arrays survive these operations,
|
|
43
|
+
and caller mutations cannot change the CPU field. Extraction errors throw without
|
|
44
|
+
a partial snapshot; temporary Wasm staging is released after copying or failure.
|
|
45
|
+
The giant snapshot alone is about 129 MB, so opt in only when needed.
|
|
46
|
+
|
|
47
|
+
## The field package
|
|
48
|
+
|
|
49
|
+
`npm install telperion` also ships the slim growth-and-field package: the
|
|
50
|
+
`telperion/field` export grows a species at a seed and answers occupancy
|
|
51
|
+
queries over it, and `telperion/field/voxelize` is an example reading of
|
|
52
|
+
those answers as cubes. Both are built by `npm run build` into `dist`, with
|
|
53
|
+
the slim Wasm beside `field.js`; the main entry `telperion.js` loads
|
|
54
|
+
`telperion.wasm` and `telperion-render.wasm` from beside itself the same
|
|
55
|
+
way, so a site serves the three Wasm files next to the JavaScript. Each
|
|
56
|
+
`new URL("./name.wasm", import.meta.url)` in `dist` is a literal, which
|
|
57
|
+
Vite, webpack and Rollup read as an asset and emit into a site's own build;
|
|
58
|
+
the files are also exported as `telperion/telperion.wasm`,
|
|
59
|
+
`telperion/telperion-render.wasm` and `telperion/telperion-field.wasm` for a
|
|
60
|
+
bundler's `?url` import, passed as `source`. In Node each entry reads its
|
|
61
|
+
file from disk. [docs/field-package.md](docs/field-package.md)
|
|
62
|
+
describes the entry point, the voxelizer's dials, and the Node smoke.
|
|
63
|
+
|
|
64
|
+
## Architecture
|
|
65
|
+
|
|
66
|
+
The retained `branching::Specimen` owns scaffold and local frontiers. Nodes
|
|
67
|
+
carry a monotone birth order and a generational key. The annual timeline stamps
|
|
68
|
+
shed nodes with their death year and keeps their slots and topology unchanged;
|
|
69
|
+
`Specimen::node(identity)` finds survivors and rejects dead identities. Packed
|
|
70
|
+
reads filter dead wood and order the structural segment before local nodes.
|
|
71
|
+
The legacy full-envelope builder retains its existing compaction path.
|
|
72
|
+
The core uses pinned pure-Rust `libm` for transcendental functions. Run
|
|
73
|
+
`npm run wasm:build && npx vitest run harness/parity.test.ts` to compare the
|
|
74
|
+
five preset node buffers byte for byte across native and wasm targets.
|
|
75
|
+
The native `Specimen::build(&family)` path starts at a seedling and grows to
|
|
76
|
+
`family.age`; `advance(years)` continues its retained frontiers. Age supports
|
|
77
|
+
0 through 1,000,000 years, rounded to one twelve-billionth of a year at each API call (the original
|
|
78
|
+
billionth-of-a-month resolution). Whole years run in order and the integer
|
|
79
|
+
sub-year remainder carries between calls;
|
|
80
|
+
zero pauses, and backward inspection filters the retained chronicle at the earlier age.
|
|
81
|
+
The same quantized elapsed time produces the same annual history. `growth.rate`
|
|
82
|
+
and `growth.shape` are numeric Chapman–Richards traits and blend with age.
|
|
83
|
+
Their current defaults are provisional, without species age calibration.
|
|
84
|
+
|
|
85
|
+
```rust
|
|
86
|
+
use telperion_core::{branching::Specimen, presets::Preset};
|
|
87
|
+
let mut family = Preset::OregonWhiteOak.parameters();
|
|
88
|
+
family.age = 10.0;
|
|
89
|
+
let mut tree = Specimen::build(&family)?;
|
|
90
|
+
let mut buffers = tree.buffers()?;
|
|
91
|
+
let changes = tree.advance(0.25)?;
|
|
92
|
+
changes.validate(&buffers, &tree.buffers()?)?; // optional reconciliation check
|
|
93
|
+
changes.apply(&mut buffers)?;
|
|
94
|
+
let skeleton = tree.tree();
|
|
95
|
+
let earlier = tree.read_at_age(5.0)?; // owned skeleton, envelope, placements and shed identities
|
|
96
|
+
# Ok::<(), telperion_core::Error>(())
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
`read()` returns an owned view at the frontier; `read_at_age(years)` filters the
|
|
100
|
+
chronicle at an earlier age without simulation. The view contains the packed
|
|
101
|
+
skeleton, envelope, placements and shed identities. Radii use the last keyframe
|
|
102
|
+
at that age, and copied shoot histories omit future observations. A read beyond
|
|
103
|
+
the frontier is refused with the frontier's age.
|
|
104
|
+
|
|
105
|
+
Each native advance returns born, resized and shed runs and born, moved and shed
|
|
106
|
+
leaf placements. `buffers()` reads those outputs in identity order. To check a
|
|
107
|
+
record, call `changes.validate(&previous_buffers, &tree.buffers()?)` before applying
|
|
108
|
+
it; a mismatch names the run's birth identity. Application needs no fresh read.
|
|
109
|
+
Run-buffer radii are exact canonical keyframe values; the family's resize
|
|
110
|
+
tolerance controls frame creation without a separate consumer rounding grid.
|
|
111
|
+
Packed placements reconcile bit for bit - the three words a record carries are
|
|
112
|
+
the three a fresh read carries - including movement caused by an adjacent
|
|
113
|
+
branch changing a surface-contact polygon. A clock-only advance
|
|
114
|
+
returns newly reached cohorts, deriving transforms only for their shoots.
|
|
115
|
+
Once those cohorts are full, clock-only advances derive no placements.
|
|
116
|
+
|
|
117
|
+
Negative/non-finite advances and invalid ages name the field and value. A node
|
|
118
|
+
cap rolls back the failed year, retains completed years, sets `node_capped`,
|
|
119
|
+
and refuses the next advance. `set_node_ceiling` can raise the resource limit
|
|
120
|
+
and resume the same frontier. The curve's final work quantum defines saturation;
|
|
121
|
+
advancing beyond it jumps directly to the requested age.
|
|
122
|
+
|
|
123
|
+
Annual scaffold stations release their lateral buds while the parent axis is
|
|
124
|
+
still extending. Boundary pauses leave the growth budget for eligible shoots;
|
|
125
|
+
local terminal and lateral buds retain separate allocation state. Local runs
|
|
126
|
+
plan against the authored room and wait at the current crown before each birth.
|
|
127
|
+
The mature annual populations still differ substantially from the envelope
|
|
128
|
+
builds; convergence and calibration remain unfinished.
|
|
129
|
+
|
|
130
|
+
Annual shoots retain birth/death years, terminal/lateral fate and year-stamped
|
|
131
|
+
crown-depth vigour observations. `shoot.vigour()` reads the latest observation;
|
|
132
|
+
tolerance counters are retained in the same append-only event sequence.
|
|
133
|
+
Attractor consumption likewise retains its first consumption year.
|
|
134
|
+
The existing habit `sheddingThreshold` is the annual vigour threshold;
|
|
135
|
+
`growth.sheddingTolerance` counts consecutive active years below it, in years.
|
|
136
|
+
Equality resets the clock. The shell contributes to vigour, and a lit descendant
|
|
137
|
+
supports its ancestors. Each slice snapshots decisions before growth, sheds at
|
|
138
|
+
most 32 subtrees in birth order, and protects the main structural leader.
|
|
139
|
+
`growth.apicalControlLoss` weakens terminal control with age and releases lateral
|
|
140
|
+
allocation. Its default is zero; the tolerance defaults to two years. These are
|
|
141
|
+
uncalibrated numeric traits. Oak and spruce still have threshold zero.
|
|
142
|
+
|
|
143
|
+
Surviving structural and local radii never decrease. Local allocations and taper
|
|
144
|
+
are re-derived from current parents without changing twig lengths. Dead shoots
|
|
145
|
+
leave the growth frontiers while their records remain. A cut invalidates only
|
|
146
|
+
surviving pipe ancestor paths; local widths propagate from changed parents.
|
|
147
|
+
Dead records keep canonical final widths independent of advance partitions.
|
|
148
|
+
The annual solve records radius keyframes only along changed paths. A frame is
|
|
149
|
+
appended when any radius exceeds the last frame by more than
|
|
150
|
+
`growth.resizeTolerance` (metres, range 0–1, default `1e-9`); births always get a
|
|
151
|
+
frame. Radii never decrease. One ten-year advance retains the same frames as ten
|
|
152
|
+
yearly advances. Output radii still materialize once per advance, from the latest
|
|
153
|
+
frames, and packing remains lazy.
|
|
154
|
+
|
|
155
|
+
Integration is incomplete: `branching::generate`, `Specimen::grow`, mesh builds
|
|
156
|
+
and the browser still use the existing full-envelope build. The JSON wire now
|
|
157
|
+
round-trips and validates `age` and `growth` (`rate`, `shape`, `sheddingTolerance`,
|
|
158
|
+
`apicalControlLoss`, `leafLifetime`, `resizeTolerance`); those fields
|
|
159
|
+
currently affect only `Specimen::build`, not the full-envelope entry points.
|
|
160
|
+
`Specimen::placements()` returns owned leaf transforms, each identified by its
|
|
161
|
+
shoot's generational identity and station ordinal, before optional canopy shell
|
|
162
|
+
culling. `growth.leafLifetime` is a numeric family trait: one year by default and
|
|
163
|
+
for oak, provisionally six for spruce; zero bears no leaves. Stations are spread
|
|
164
|
+
across `ceil(leafLifetime)` annual cohort offsets, beginning at birth. A one-year
|
|
165
|
+
lifetime fills immediately; a longer lifetime fills over its first years and
|
|
166
|
+
then holds the same station identities while the shoot lives. Wood above the
|
|
167
|
+
twig anatomy's bearing diameter carries no foliage. Station randomness is keyed
|
|
168
|
+
by shoot identity. Unchanged wood reuses its cached transforms; changes to radii
|
|
169
|
+
or neighboring contact polygons re-derive only the affected shoots. This timeline
|
|
170
|
+
foliage path is not yet used by production. Cohort persistence fixes the earlier
|
|
171
|
+
bare mature crowns; the structural convergence and visual judgment remain open.
|
|
172
|
+
`Specimen::changes_between(from, to)` filters birth/death years, radius frames
|
|
173
|
+
and cohort offsets in either direction. Growing advances use the same filter.
|
|
174
|
+
Records carry exact keyframe radii and selected station transforms, including
|
|
175
|
+
motion caused by neighboring contact paths; no whole-buffer diff is computed.
|
|
176
|
+
`ChangeRecord::apply` updates identity-keyed consumer buffers atomically.
|
|
177
|
+
`build_with_history_cap(family, years)` and `set_history_cap(years)` set retention;
|
|
178
|
+
the default is 10,000 years. Reads older than the retained window refuse with the
|
|
179
|
+
cap and earliest available age. Increasing the cap cannot restore discarded data.
|
|
180
|
+
Compaction drops old dead geometry, shoot histories, radius frames and placements,
|
|
181
|
+
retaining a compact death index for the cumulative shed set and reserving identity
|
|
182
|
+
slots. It preserves frontier bytes, later growth and node-ceiling behavior.
|
|
183
|
+
`TreeEngine.buildSpecimen(family, historyCap?)` returns a retained handle.
|
|
184
|
+
`read()` defaults to its frontier; `read(age)` and `changes(from, to)` inspect
|
|
185
|
+
retained ages. `advance(years)` returns the new frontier and its change record.
|
|
186
|
+
Reads, records and snapshots are owned JavaScript copies. Successful specimen
|
|
187
|
+
rebuild/import, handle release, engine release and disposal invalidate the old
|
|
188
|
+
handle. Failed rebuilds/imports preserve it. `setNodeCeiling(limit)` resumes a
|
|
189
|
+
capped specimen, and `setHistoryCap(years)` changes retention. Native consumers
|
|
190
|
+
have the same operations through `specimen::SpecimenStore` (snapshot operations
|
|
191
|
+
require the `json` feature), or use `branching::Specimen` directly.
|
|
192
|
+
|
|
193
|
+
```ts
|
|
194
|
+
const specimen = engine.buildSpecimen({ ...OREGON_WHITE_OAK, age: 10 }, 100);
|
|
195
|
+
const before = specimen.read();
|
|
196
|
+
const { changes } = specimen.advance(0.25);
|
|
197
|
+
const earlier = specimen.read(5);
|
|
198
|
+
const snapshot = specimen.snapshot(); // optional, never a mesh
|
|
199
|
+
const restored = engine.importSpecimen(snapshot); // invalidates specimen
|
|
200
|
+
restored.advance(1);
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
The schema-1 specimen snapshot carries the chronicle, retained frontiers,
|
|
204
|
+
integer clock, cap/floor, identity slots and writer state. It omits packed reads,
|
|
205
|
+
foliage contact caches, crown caches and meshes. The owned `Uint8Array` is the
|
|
206
|
+
same byte format as native `Specimen::snapshot()` / `from_snapshot(bytes)`:
|
|
207
|
+
`TLPS`, a little-endian u32 schema (1), fixed-integer little-endian bincode 1.3.3
|
|
208
|
+
state in the declared `Specimen` field order, then an eight-byte FNV-1a checksum
|
|
209
|
+
of the preceding bytes. Lengths and native indices encode as u64; unlimited
|
|
210
|
+
canopy counts encode as UINT64_MAX, compacted identity indices as UINT32_MAX.
|
|
211
|
+
The adapters restore native sentinels on import. Invalid size, schema, checksum
|
|
212
|
+
or payload refuses before replacement. The current staging limit is 512 MiB.
|
|
213
|
+
`harness/parity.test.ts` exchanges snapshots in both directions between native
|
|
214
|
+
and wasm, advances them, and compares earlier and frontier node/placement bytes
|
|
215
|
+
for every preset.
|
|
216
|
+
|
|
217
|
+
The harness's age number and slider inspect the one retained specimen; beyond
|
|
218
|
+
its frontier they advance it. Rebuild starts a new specimen at the chosen age.
|
|
219
|
+
Play uses the page's years-per-second setting and carries fractional years.
|
|
220
|
+
`SpecimenView` applies interval records to its identity buffers, sweeps wood
|
|
221
|
+
again when a year changes and submits the updated placement transforms. Camera
|
|
222
|
+
framing remains explicit. The ordinary production `mesh::build` and
|
|
223
|
+
`branching::generate` routes retain the envelope build until calibration.
|
|
224
|
+
Its pipe cache recomputes insertion/deletion ancestor paths. An ordered scale
|
|
225
|
+
index visits structural wood only when its historical width can be exceeded;
|
|
226
|
+
local width changes propagate to descendants in birth order. Crown exposure uses
|
|
227
|
+
an indexed profile and caches samples until that envelope or position changes.
|
|
228
|
+
With shedding disabled, only frontier shoots sample vigour; other nodes retain
|
|
229
|
+
their last sampled state. With shedding enabled, the slice-start survival pass
|
|
230
|
+
refreshes the live crown and propagates descendant support.
|
|
231
|
+
Structural births append without moving local storage inside a slice. Internal
|
|
232
|
+
frontiers and pipe reductions use node kinds. Consumer reads lazily pack a
|
|
233
|
+
structural-first view without moving the retained frontiers' storage.
|
|
234
|
+
Chronicle slots are never reused, so retention boundaries cannot change
|
|
235
|
+
future handles. Local seeding retains unallocated stations and structural child
|
|
236
|
+
counts.
|
|
237
|
+
Full-tree validation remains available to callers; annual mutations validate
|
|
238
|
+
new or resized nodes. Native cost measurements, including sparse and dense
|
|
239
|
+
changes on large trees, run with
|
|
240
|
+
`FN11_MEASURE=1 cargo test --release -p telperion-core --lib monthly_cost_report -- --nocapture`.
|
|
241
|
+
The command retains its historical name; it now measures annual slices. Widths
|
|
242
|
+
finalize once per advance from the annual radius keyframes;
|
|
243
|
+
consumer packing is lazy and timed separately. Fixed-geometry shoots sleep until
|
|
244
|
+
the crown can reach them. An unchanged queue keeps its identity order and an
|
|
245
|
+
empty local frontier makes no width queries. Radius-dependent failures still retry.
|
|
246
|
+
R10 selected annual slices after the mature monthly oak measured 5.906 seconds
|
|
247
|
+
(native three-build median), above the approximately half-second target. The
|
|
248
|
+
change-record timings are included in that command. For the mature oak's
|
|
249
|
+
optional snapshot export/import and fresh-build equivalence checks, run
|
|
250
|
+
`FN11_SNAPSHOT=1 cargo test --release -p telperion-core --lib monthly_cost_report -- --nocapture`.
|
|
251
|
+
The final cost report and calibration remain later work.
|
|
252
|
+
The early annual medians were 779 ms oak and 561 ms spruce. The annual oak also
|
|
253
|
+
misses the target; the closed-form design remains the owner's reserve. See the
|
|
254
|
+
[measurement and convergence figures](scripts/benchmarks/generation.md#annual-slice-choice-fn-11-native-2026-09-13).
|
|
255
|
+
|
|
256
|
+
The native entry is `branching::generate(&family.skeleton, family.radii)`. Its solved `Tree` can feed `surface::build`, foliage placement/culling, or `Field::new` independently. The Wasm binding assembles the requested stages; `src/browser` loads it and copies output arrays. There is no TypeScript generator and no TypeScript renderer.
|
|
257
|
+
|
|
258
|
+
| Owner in `crates/telperion-core/src` | Responsibility |
|
|
259
|
+
|---|---|
|
|
260
|
+
| `envelope`, `colonization`, `bias` | Authored crown, attractor points, directional fields and turn constraints |
|
|
261
|
+
| `branching/traits`, `branching/scaffold` | The numeric habit traits and the one builder that grows every axis from them |
|
|
262
|
+
| `branching/local`, `twigs`, `radius` | Radius-driven branch generations, fixed twig anatomy, local taper and fork conservation |
|
|
263
|
+
| `surface` | Continuous swept wood, transported frames, lobes, twist and sockets |
|
|
264
|
+
| `foliage` | Leaf outline and lean from numeric traits, anatomical stations, phyllotaxis and shell retention |
|
|
265
|
+
| `field` | Wood and foliage cell occupancy, independently of render meshes |
|
|
266
|
+
| `presets` | Named parameter sets and scale choices |
|
|
267
|
+
|
|
268
|
+
`crates/telperion-render` draws that output on wgpu, and is the only renderer in the repository. It compiles to two targets from one code path: a wasm module the page loads through `src/browser/render.ts`, which generates and uploads inside its own linear memory, and a native offscreen target whose `headless` example writes a PNG at the hero pose and, on request, a GPU timing record. The renderer owns the whole scene - ground, sky, one sun and the single depth map it casts its shadow through, the 1.8 m scale figure, camera and the whole, bare, single-leaf and clay views - and it never names a species: only the headless entry point resolves a preset id. Appearance is numbers, not textures: a material row on the family carries the bark and leaf colours, the bark's roughness, the hue and brightness ranges each leaf varies inside and how far the crown's interior is darkened, while a separate scene row carries the sun, the sky, the ground and the shadow coarsening and filtering controls; both reach the shaders as uniforms and no shader path branches on a species. Colour and depth are drawn at four samples a pixel where the adapter offers them and resolved into the single-sample target, and one filmic tone map closes every lit frame. The clay view draws the neutral room this renderer started as - no material, no sun, no tone map - so geometry can still be judged with nothing over it. The crown is not one draw: a compute pass reads every placement once per frame, projects the leaf element's extent to a pixel size and gives that leaf the coarsest level whose outline deviation projects under half a pixel, or the unseen bucket if it is outside the frustum; the renderer then issues one indexed indirect draw per level, over the instances that chose it. A level is a subset of the element's own sections, so a coarse leaf's vertices are a fine leaf's and no placement, count or bound changes with the level drawn.
|
|
269
|
+
|
|
270
|
+
The sun writes one 1,024-square depth map. Wood casters are a prefix of whole runs ordered by largest radius; foliage casters use a fixed placement stride at the coarsest level, scaling each retained surface by the square root of the stride about its centre. The connector stays fixed. The shared read averages a square of comparisons after offsetting the receiver along its normal; taps outside the map are lit. The caster sets do not depend on camera selection. Reordering placements changes the sampled subset, and changing run radii retunes the threshold; future vertex motion must also move the casters.
|
|
271
|
+
|
|
272
|
+
The crown envelope controls the silhouette. One scaffold builder grows every axis inside it from a table of numeric habit traits - apical dominance, whorl strength, station spacing, pitch, rise, crookedness and the rest - and every growth unit of every axis takes its heading from one sum of the rule heading, the pull of the envelope's attractors and the bias field. Attractor pull is a trait weight in that sum rather than a phase of its own, and no species has a builder or a field the others lack: an oak and a spruce are two rows of the same table, and any point between them is a tree. The leaf is a row too - lobe count, lobe depth and section roundness draw the outline, forward lean, lean rise and surface contact place it on its shoot - so there is no anatomy to switch between. Local branch laws continue from the scaffold down to leaf-bearing twigs and read the same traits. Forks conserve cross-sectional area with a tunable exponent. Branch resolution and lateral count are independent. Terminal twig anatomy is measured in metres; the giant presets retain fine twigs instead of uniformly enlarging them.
|
|
273
|
+
|
|
274
|
+
Botanical and output changes have separate owners. A representative development exercise and the retained test mapping are in [the migration guide](tests/migration/README.md#changing-a-rule-or-an-output). Native mesh-free examples are exercised in `crates/telperion-core/tests/field.rs`; browser binding ownership and failure recovery are checked in `tests/browser/bindings.mjs`, and the page on a real GPU in `tests/browser/render.mjs`.
|
|
275
|
+
|
|
276
|
+
## Build and develop
|
|
277
|
+
|
|
278
|
+
Install Rust through rustup, then:
|
|
279
|
+
|
|
280
|
+
```sh
|
|
281
|
+
rustup toolchain install 1.98.1 --profile minimal --component rustfmt --component clippy --target wasm32-unknown-unknown
|
|
282
|
+
cargo install wasm-bindgen-cli --version 0.2.128 # the version Cargo.lock pins
|
|
283
|
+
npm ci
|
|
284
|
+
npm run rust:build
|
|
285
|
+
npm run wasm:build
|
|
286
|
+
npm run render:build
|
|
287
|
+
npm run rust:test
|
|
288
|
+
cargo fmt --all -- --check
|
|
289
|
+
cargo clippy --workspace --all-targets -- -D warnings
|
|
290
|
+
npm test
|
|
291
|
+
npm run catalogue:check
|
|
292
|
+
npm run typecheck
|
|
293
|
+
npm run build
|
|
294
|
+
npx playwright install chromium
|
|
295
|
+
npm run rust:test:wasm
|
|
296
|
+
npm run test:render
|
|
297
|
+
npm run dev
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
The repository pins Rust in `rust-toolchain.toml`, and `render:build` checks the installed `wasm-bindgen` against the version the crate pins before it generates the glue - a mismatch fails with the command to run. `dev` and `build` regenerate both Wasm modules and the preset metadata. Build them before running the Node harness tests from a clean checkout. The package ships both binaries as their own files, `telperion.wasm` and `telperion-render.wasm`, fetched at run time from beside `dist/telperion.js`; consumers do not need Rust. The development viewer exposes every generator parameter, the material and scene rows through the same numeric controls, the five presets and the whole, bare, single-leaf and clay views, with a GPU timing session on the button beside them.
|
|
301
|
+
|
|
302
|
+
A still without a browser, from the same renderer:
|
|
303
|
+
|
|
304
|
+
```sh
|
|
305
|
+
cargo run --release -p telperion-render --example headless -- \
|
|
306
|
+
--preset norway-spruce --seed 7 --view whole --out /tmp/spruce.png \
|
|
307
|
+
[--timing /tmp/spruce.json] [--orbit] [--level <n>] [--scene <json>]
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
`--view` takes `whole`, `bare`, `leaf` or `clay`; the first three are lit and the last is the neutral room, which is how geometry is inspected with no material, sun or tone map over it. `--scene <json>` replaces the default scene row for the frame - the sun's azimuth, elevation and colour, the sky's zenith and horizon and the ground, plus the shadow controls - as the row's own JSON under the field names the panel shows (`{"sunElevation": 24, "skyHorizonBlue": 1.1}`, every field optional); an unknown field is refused with the list of the real ones and a value outside its range with the range it wanted. The shadow defaults are `casterTexels: 1` (range 0–8), `casterStride: 4` (1–64), `shadowFilterTexels: 1` (0–3) and `shadowNormalOffset: 1` (0–4). Threshold and offset use the fitted world size of a map texel; stride and kernel radius truncate to whole counts. Threshold zero keeps every wood run, stride one keeps every placement, and filter radius zero keeps a single hardware comparison. A threshold above every run radius or a stride above the placement count legally empties that caster set. The material row is the family's, so it arrives with the preset. `--level <n>` holds every leaf of the frame at one level instead of letting selection choose, which is how a single level's cost is measured on its own. `--orbit` turns the camera one full revolution about the subject at the hero pose's elevation and distance while the timing session runs; the still beside it is always the hero pose, because the orbit is what is measured and not what is judged.
|
|
311
|
+
|
|
312
|
+
The same target walks between two presets:
|
|
313
|
+
|
|
314
|
+
```sh
|
|
315
|
+
cargo run --release -p telperion-render --example headless -- \
|
|
316
|
+
--preset oregon-white-oak --to norway-spruce --seed 7 --frames 240 \
|
|
317
|
+
--out /tmp/walk/frame.png
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
The material row also controls procedural surface detail: `ridgeScale` and
|
|
321
|
+
`plateScale` (0–1 metres), `furrowStrength` and `roughnessDetail` (0–1),
|
|
322
|
+
`veinScale` (0–32 pairs per blade), `veinContrast` (0–1),
|
|
323
|
+
`transmissionStrength` (0–1), linear `transmissionRed/Green/Blue` (each 0–1),
|
|
324
|
+
and `thickness` (0–8 optical depth). Transmission attenuates by
|
|
325
|
+
`exp(-thickness)` and the existing shadow comparison. Round sections suppress
|
|
326
|
+
vein and margin tone. Ridge scale sets circumferential ridge spacing; plate
|
|
327
|
+
scale sets staggered scale height, bounded to 1.5–2 ridge widths so long
|
|
328
|
+
furrows still carry short scales.
|
|
329
|
+
Larger plate-to-ridge ratios deepen and lengthen the shouldered furrows.
|
|
330
|
+
Furrow strength independently narrows and shallows those gaps; zero keeps
|
|
331
|
+
the plates and flakes with narrow outlines. It defaults to one for older rows.
|
|
332
|
+
Flat faces have lifted lower edges and finer flakes derived from those same
|
|
333
|
+
two lengths. Zero ridge scale disables relief; zero plate scale leaves ridges
|
|
334
|
+
without cross-fissures. The young-wood fade spans diameters of two to five
|
|
335
|
+
ridge widths; relief continues strengthening with girth on mature runs.
|
|
336
|
+
Both axial distance and circumferential arc length supply pixel footprints.
|
|
337
|
+
Noise, plates and flakes fade to their means before becoming unresolved;
|
|
338
|
+
edge support expands with the footprint to suppress sharp normal harmonics.
|
|
339
|
+
The shading normal differentiates this filtered height at the fragment,
|
|
340
|
+
holding the footprint fixed, so changing the filter does not create relief.
|
|
341
|
+
Four subpixel shading evaluations integrate the normal's nonlinear lighting;
|
|
342
|
+
the geometry coverage and existing shadow lookup are unchanged.
|
|
343
|
+
These fields affect shading only.
|
|
344
|
+
|
|
345
|
+
Colour and cavity are numeric rows too. `fissureRed/Green/Blue` and
|
|
346
|
+
`crestRed/Green/Blue` (linear offsets, −1–1) tint the relief's lows and highs,
|
|
347
|
+
weighted by `fissureStrength` and `crestStrength` (0–1).
|
|
348
|
+
`barkMottleScale` (0–8 metres) and `barkMottleStrength` (0–1) vary colour
|
|
349
|
+
along the wood; the noise fades with its pixel footprint. `cavityStrength`
|
|
350
|
+
(0–1) cuts sun and ambient light in low relief and within one radius of the
|
|
351
|
+
ground, and in concave fork necks derived from the existing surface normals.
|
|
352
|
+
Disconnected crossing limbs do not gain a contact term. `bladeMottleScale` (0–32 noise cells per blade length) and
|
|
353
|
+
`bladeMottleStrength` (0–1) vary each blade with its placement's stable seed.
|
|
354
|
+
`marginWidth` (0–0.5 of the half-blade) adds `marginRed/Green/Blue`
|
|
355
|
+
(linear offsets, −1–1) at the sides and tip; `cuticleGloss` (0–1) narrows the
|
|
356
|
+
front face's one sun highlight, below. `skyOcclusionStrength` (0–1)
|
|
357
|
+
cuts the sky hemisphere by crown depth on wood and leaves, preserving ground
|
|
358
|
+
bounce; leaf interior darkening still multiplies the remaining ambient light.
|
|
359
|
+
All nineteen of those additions default to zero. Zero scale disables mottling;
|
|
360
|
+
zero width disables the margin. A needle stays matte and uniform through its row.
|
|
361
|
+
|
|
362
|
+
The highlight is physical on both materials. `barkReflectance` and
|
|
363
|
+
`leafReflectance` (0–1, default 0.04, a dielectric) are Schlick's reflectance
|
|
364
|
+
at normal incidence, the foot of one Blinn-Phong lobe normalised to the
|
|
365
|
+
hemisphere, whose width follows `barkRoughness` on wood and `cuticleGloss` on
|
|
366
|
+
the blade's front face; what the lobe mirrors is taken from the sun's diffuse
|
|
367
|
+
rather than added to it, so over every direction a surface returns at most its
|
|
368
|
+
Fresnel share of the sun. Below the relief, `barkGrainScale` (0–0.05 metres, a
|
|
369
|
+
cell) with `barkGrainStrength` (0–1), and `bladeGrainScale` (0–256 cells per
|
|
370
|
+
blade length) with `bladeGrainStrength` (0–1), vary the colour and tilt the
|
|
371
|
+
normal at the pixel and fade to their mean with the footprint like the mottle,
|
|
372
|
+
so distant wood and leaves converge to their smooth means. Zero grain leaves
|
|
373
|
+
both smooth.
|
|
374
|
+
|
|
375
|
+
Plates are the field's second primitive, and structure rather than tint.
|
|
376
|
+
`plateCellScale` (0-1 metres) is one plate's width across the run before girth
|
|
377
|
+
scales it; zero leaves the field the parallel ridges it was. `plateElongation`
|
|
378
|
+
(0-16) is how much longer a plate runs than it is wide, so an oak wears long
|
|
379
|
+
blocks and a spruce round scales off one row. A furrow is where two
|
|
380
|
+
sites stand equally near, walled at a seventh of a plate's width and cut a
|
|
381
|
+
twentieth of it deep. `plateFurrowWidth` (0-1) widens the flat floor of that
|
|
382
|
+
furrow, up to a fifth of a plate's width, so a bigger plate carries a wider
|
|
383
|
+
furrow off the same row; zero leaves the hairline the network cuts between
|
|
384
|
+
two faces. The floor is integrated against the footprint like every other
|
|
385
|
+
band, and the mean the far path returns falls away with it. `plateDome` (0-1) raises a face from its own edge
|
|
386
|
+
towards its middle and `plateEdgeLift` (0-1) stands its rim off the furrow
|
|
387
|
+
beside it. `plateIdentity` (0-1) is how much of its own a plate keeps: how
|
|
388
|
+
proud it stands, how far it leans across its run, and the value and cast it
|
|
389
|
+
holds against its neighbours. The network is footprint-faded on both axes,
|
|
390
|
+
converging to a mean the colour range carries, so a distant trunk neither
|
|
391
|
+
aliases nor steps.
|
|
392
|
+
|
|
393
|
+
`weatheringStrength` (0-1) greys and lightens a plate face against the fresher
|
|
394
|
+
wood a furrow keeps, tinted by `weatheringRed/Green/Blue` (linear offsets,
|
|
395
|
+
-1-1). `orientationStrength` (0-1) with `orientationRed/Green/Blue` colours
|
|
396
|
+
the side turned away from the sun and the foot of the trunk - what damp growth
|
|
397
|
+
would look like, not damp growth itself. `directionalOcclusion` (0-1) walks
|
|
398
|
+
the height field towards the sun and darkens a furrow floor its own crest
|
|
399
|
+
stands over, which leaves the sun side of the same furrow lit; it adds no
|
|
400
|
+
light and changes no shadow map. `depthStrength` (0-1) gives the relief depth
|
|
401
|
+
beyond the shaded normal. All fifteen default to zero, and with them at zero a
|
|
402
|
+
document renders exactly as it did before they existed.
|
|
403
|
+
|
|
404
|
+
Smooth bark is colour rather than relief. `lichenScale` (0-1 metres) is the
|
|
405
|
+
size of the cells lichen patches scatter over, and `lichenCoverage` (0-1) the
|
|
406
|
+
share of them holding one; a patch is a sphere cut by the bark's own surface,
|
|
407
|
+
so most show small and a few broad, over two octaves. `lichenStrength` (0-1)
|
|
408
|
+
covers the bark with `lichenRed/Green/Blue` (linear reflectance, 0-1).
|
|
409
|
+
`lenticelDensity` (0-400 rows a metre) and `lenticelLength` (0-0.5 metres,
|
|
410
|
+
the longest dash across the wood) lay short horizontal dashes;
|
|
411
|
+
`lenticelStrength` (0-1) is how far one shows and how deep a shallow groove it
|
|
412
|
+
cuts into the relief, and `lenticelTint` (-1-1) its value against the bark,
|
|
413
|
+
-1 black. `peelCurl` (0-1) turns the plate network into strips peeling across
|
|
414
|
+
the wood: they stretch up to four plates wide, lift at their lower edge, and
|
|
415
|
+
that share of them has peeled away to `peelRed/Green/Blue` (linear
|
|
416
|
+
reflectance, 0-1), the inner bark. The share rides on the relief's own
|
|
417
|
+
maturity, so the old wood at the base peels and the young stem does not. All
|
|
418
|
+
fourteen default to zero and are footprint-faded to their means, and at zero
|
|
419
|
+
a document renders exactly as it did before they existed.
|
|
420
|
+
|
|
421
|
+
Foliage selection compacts each level in placement-index order. Equal-depth
|
|
422
|
+
leaf samples therefore resolve consistently when the same frame is redrawn.
|
|
423
|
+
|
|
424
|
+
`--to <preset>` renders a numbered PNG sequence instead of one still: `--frames <n>` frames, 240 by default, each the blend of the two families at the one seed, all of them at the hero pose the first frame's bounds fixed. `--out` names the sequence, so `--out /tmp/walk/frame.png` writes `/tmp/walk/frame-0001.png` onward with `transition.json` beside them, naming both presets, the seed, the size, the frame count, the rate of 24 a second and what the encoder did. When `ffmpeg` is on the path the frames are assembled into `transition.mp4` at that rate; when it is not, the run says so in one line and keeps the sequence, which is the artefact either way.
|
|
425
|
+
|
|
426
|
+
The same walk stated in seconds, eased, with the camera between the two trees:
|
|
427
|
+
|
|
428
|
+
```sh
|
|
429
|
+
cargo run --release -p telperion-render --example headless -- \
|
|
430
|
+
--preset oregon-white-oak --to norway-spruce --seed 7 \
|
|
431
|
+
--walk 12 --hold 2 --sweep 35 --size 1920x1080 --out .flow/evidence/fn25/whole/frame.png
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
`--walk <seconds>` gives the blend a length in seconds at 24 frames a second instead of a frame count, and eases it with a smoothstep so the walk leaves and arrives at rest. `--hold <seconds>` holds that many seconds of frames at each end, at the near family before the walk and the far one after it. `--sweep <degrees>` turns the camera that many degrees of azimuth across the whole sequence, holds included, so the shot keeps drifting while the tree stands still. A walk also changes what the camera is: instead of the first frame's pose held throughout, it eases between the two ends' own hero poses - what it looks at, how far back it stands and how high - so neither tree is framed for the other. `--view leaf` walks the same way on the leaf of each end. `--walk` and `--frames` are two ways to say one length and refuse each other; a walk of zero seconds, a negative hold, or a sweep outside -360 to 360 degrees is refused by the name of the flag. `transition.json` carries the walk, the hold, the sweep and the ease beside the frames.
|
|
435
|
+
|
|
436
|
+
```sh
|
|
437
|
+
node scripts/scenic-cut.mjs --out .flow/evidence/fn25/scenic.mp4
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
`scripts/scenic-cut.mjs` cuts a whole-tree sequence and a leaf sequence, both already on disk, into the one clip: a half-second crossfade from the first into the second, a light grade of contrast, warmth and a soft vignette, and one ffmpeg encode to 1920 by 1080 H.264 at 24 frames a second. It writes `scenic.json` beside the clip naming every input, the grade and the exact invocation, and it re-reads the finished file rather than assuming it. It renders nothing itself, refuses a sequence it cannot find by naming the path, and on a machine with no `ffmpeg` says so in one line and leaves the sequences standing.
|
|
441
|
+
|
|
442
|
+
`npm run rust:test:wasm` holds the Wasm binding to its contract in a plain headless browser, which needs no adapter at all. `npm run test:render` drives the page on hardware WebGPU: it needs a display, and skips with the renderer's own words when the machine offers no hardware adapter. `npm run species:qa` renders the species stills through the headless target.
|
|
443
|
+
|
|
444
|
+
## Measurements and limits
|
|
445
|
+
|
|
446
|
+
Rendering is measured by the renderer's own GPU timing session - conditioning frames, warmup and measured samples, with a verdict that is `valid`, `unavailable`, `disjoint` or `contended`, and no millisecond figure at all unless the verdict is valid. A record carries `p50_ms` and `p95_ms` for the vegetation pass, `selection_p50_ms` and `selection_p95_ms` for the compute pass that chose the levels, `shadow_p50_ms` and `shadow_p95_ms` for the depth pass the sun writes its map in, `total_p50_ms` and `total_p95_ms` for the three added frame by frame and then ranked, `multisample`, the samples a pixel the frame was actually drawn at, `caster_triangles` and `caster_instances`, the wood triangles and foliage placements submitted to the sun, and `levels`, one entry per level with its tolerance in metres and the median instance count it drew, the last being the leaves no level drew. An orbit session adds `wall_p50_ms`, `wall_p95_ms`, `wall_max_ms` and `wall_frames`, the frame-to-frame wall clock of the loop that drew them. An invalid GPU record omits GPU percentiles and level medians; multisample and caster counts remain, and a valid wall series is reported independently. The adapter line identifies the comparison sampling mode (WebGPU reports the requested mode; native adapters are checked for linear filtering). The [FN22 report](.flow/evidence/fn22/REPORT.md) records the native and browser sessions for oak and spruce before level selection, the [FN23 report](.flow/evidence/fn23/REPORT.md) the same sessions after it, and the [FN14 report](.flow/evidence/fn14/REPORT.md) the same sessions again with the sun, its shadow map and four samples a pixel in the frame, each beside the stills they were measured on. The [FN27 report](.flow/evidence/fn27/REPORT.md) adds the coarse casters and filtered comparison: it closes fn-14's coarse-caster, browser-type and stale-view-documentation follow-ups, returns the native oak under 3.8 ms, and records the spruce with needle aggregation as its next performance step. Its visual verdicts remain with the owner. That is the rendering path, and it is unrelated to the rejected GPU query backend below, which was about generation and occupancy rather than drawing.
|
|
447
|
+
|
|
448
|
+
The [FN8 report](.flow/evidence/fn8/REPORT.md) owns the matched full-build measurements, binding costs, native observations, memory-domain limits and GPU results. CPU generation latency and GPU frame time are separate measurements. Wasm linear-memory capacity is a high-water allocation, not live heap or total browser memory; release allows allocator reuse and dispose allows host reclamation once references are gone. Scene replacement retains the previous tree until the new build succeeds, so transient coexistence matters.
|
|
449
|
+
|
|
450
|
+
The [FN12 generation report](.flow/evidence/fn12/REPORT.md) records a **rejected
|
|
451
|
+
production GPU query candidate**. All cold workloads were slower and f32 contact
|
|
452
|
+
results differed from the f64 CPU reference. A giant 64³ resident query improved
|
|
453
|
+
locally, but did not qualify a complete lifecycle or the general precision contract.
|
|
454
|
+
Skeleton and field-construction GPU performance remain inconclusive; only their
|
|
455
|
+
CPU stages and code dependencies were examined. That experiment introduced no
|
|
456
|
+
production GPU query entry point. The rejected WebGPU implementation, runner and dedicated tests have
|
|
457
|
+
been removed. The report retains final timing and correctness evidence from Linux
|
|
458
|
+
Chromium 151 and an RTX 3080. The snapshot API and
|
|
459
|
+
[CPU reproduction tools](scripts/benchmarks/generation.md) remain in use by the
|
|
460
|
+
field-generation follow-up's correctness checks and measurements.
|
|
461
|
+
|
|
462
|
+
The separate [fn-91 generation path](scripts/benchmarks/generation.md#qualified-gpu-positions)
|
|
463
|
+
expands compact foliage and qualified wood inputs into GPU-resident buffers shared
|
|
464
|
+
by native `Delivery::Resident` and browser `setTreeGpu`. CPU-owned output remains
|
|
465
|
+
available, and the default browser path remains synchronous CPU generation.
|
|
466
|
+
The [retained desktop measurements](.flow/evidence/fn-91-fast-tree-generation-as-a-core-engine/stations/REPORT.md)
|
|
467
|
+
record completed-frame delivery of 158.1–180.8 ms for mature oak and spruce seeds
|
|
468
|
+
1/7 with an initialized renderer. These results do not qualify cold page startup,
|
|
469
|
+
phone performance or complete process/GPU peak memory. This GPU expansion path
|
|
470
|
+
does not revive fn-12's rejected spatial-query implementation.
|
|
471
|
+
|
|
472
|
+
The archived [FN7 surface experiment](experiments/rust-surface-benchmark/REPORT.md) measured a narrower and older workload. Its numbers are historical, not a full-engine migration result. Further botanical realism and species visual QA remain future work. Bark and foliage are now judged lit - colour, sun, shadow and the crown's own depth, beside the reference photographs - and the clay view remains for judging geometry alone; procedural bark relief, leaf veins and two-sided leaf transmission are implemented, with owner judgments at the close-up scales still pending. Full lifecycle simulation is not implemented.
|
|
473
|
+
|
|
474
|
+
## License
|
|
475
|
+
|
|
476
|
+
MIT
|
|
477
|
+
|
|
478
|
+
Historical measurement payloads and the frozen FN7 implementation live in Git history. The reports link to their pinned archive. The remaining generation benchmark runners live in `scripts/benchmarks/` and write results outside the repository by default; the browser rendering runners retired with the Three.js stage. Retrieve the old evidence without changing this checkout:
|
|
479
|
+
|
|
480
|
+
```sh
|
|
481
|
+
mkdir -p /tmp/telperion-history
|
|
482
|
+
git archive 1922505a8a396d73b335974eabf6a9faf33ccd62 .flow/evidence experiments/rust-surface-benchmark | tar -x -C /tmp/telperion-history
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
## Species and reproducible specimens
|
|
486
|
+
|
|
487
|
+
The viewer's species selector exposes Oregon white oak (`oregon-white-oak`,
|
|
488
|
+
*Quercus garryana*) and Norway spruce (`norway-spruce`, *Picea abies*), alongside
|
|
489
|
+
Ordinary, Telperion and Laurelin. Choose species independently of the unsigned
|
|
490
|
+
32-bit specimen seed; changing species preserves the seed. Within a stated generator revision and backend, family
|
|
491
|
+
parameters and seed identify the specimen; generated output may change as the
|
|
492
|
+
engine improves. Cross-revision and CPU/GPU byte equality are not promised
|
|
493
|
+
(see STRATEGY.md, Attributability). Different seeds vary structure and
|
|
494
|
+
placement, not species identity. Whole, bare-branch and single-leaf views
|
|
495
|
+
support inspection; the leaf view isolates one placed unit at generated scale.
|
|
496
|
+
Open `/?species=norway-spruce&seed=1` to load a full-foliage specimen directly.
|
|
497
|
+
|
|
498
|
+
Browser consumers can use `presetById('oregon-white-oak')`, set
|
|
499
|
+
`family.skeleton.seed`, then pass the family to `TreeEngine.build`. Native
|
|
500
|
+
consumers use `Preset::OregonWhiteOak.parameters()` or
|
|
501
|
+
`Preset::NorwaySpruce.parameters()`. Unknown identities are rejected. Natural
|
|
502
|
+
presets disable the separate supernatural group; Telperion and Laurelin enable
|
|
503
|
+
it explicitly. Botanical lean and gravitropism remain independent.
|
|
504
|
+
|
|
505
|
+
Native needle placement can use `foliage::place_on_surface` with the family’s
|
|
506
|
+
`SurfaceParams` to attach to the rendered polygonal sweep and fork sockets without
|
|
507
|
+
building mesh indices or normals. The browser engine and species measurement
|
|
508
|
+
runner use this path. `foliage::place` retains the circular-radius placement API.
|
|
509
|
+
|
|
510
|
+
The [frozen botanical profiles](.flow/evidence/fn9/profiles.json) define mature
|
|
511
|
+
open-grown contexts, source-backed dimensional gates, contextual estimates and
|
|
512
|
+
unknown quantities. [References](.flow/evidence/fn9/REFERENCES.md) attribute the
|
|
513
|
+
photographs and research; [cross-seed QA](.flow/evidence/fn9/REPORT.md) records
|
|
514
|
+
remaining fidelity failures. A numeric pass alone is not botanical approval.
|
|
515
|
+
|
|
516
|
+
CPU-only measurement needs Rust, not a GPU. The stills additionally need a
|
|
517
|
+
hardware adapter, which the renderer requires and names when it is missing; no
|
|
518
|
+
browser, page or Playwright is in that path any more. Run
|
|
519
|
+
`npm run species:qa -- --output /tmp/species-run` for the whole protocol, or
|
|
520
|
+
`npm run species:measure -- --output /tmp/species-run` for the numbers alone. See
|
|
521
|
+
the [migration guide](tests/migration/README.md#species-evidence-and-replay) for
|
|
522
|
+
the full replay protocol and failure semantics.
|
|
523
|
+
|
|
524
|
+
The [comparative botanical benchmark](.flow/evidence/fn19/REPORT.md) freezes twelve
|
|
525
|
+
mature oak/spruce specimens, structural distributions and matched anatomy views.
|
|
526
|
+
Its [record](tests/migration/README.md#comparative-botanical-benchmark-fn19)
|
|
527
|
+
separates collection failures, engineering inspection and pending independent
|
|
528
|
+
botanical assessment; the rig itself measured the Three.js renderer and is
|
|
529
|
+
retired with that renderer. To expand the catalogue, use the
|
|
530
|
+
[species onboarding workflow](docs/species-onboarding.md),
|
|
531
|
+
[dispatch template](templates/species-profile.md) and
|
|
532
|
+
[independent example packets](.flow/evidence/fn19/onboarding-examples/README.md).
|