1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
//! Surface attributes that survive destruction: paint that stays where it was
//! sprayed when the wall under it is carved away.
//!
//! Row 4 of `docs/research/2026-08-11-novel-gameplay-opportunities.md` — *"you
//! spray graffiti on a wall, then blow a hole through it, and the paint on the
//! remaining wall is still exactly where you sprayed it — not smeared, not
//! reset."*
//!
//! # Why there is no attribute *transfer* here
//!
//! The research prices that row on **common subdivision plus L²-nearest
//! attribute transfer** (Integer Coordinates §3.8, §4.4), on the reasoning that
//! the shared tetrahedral grid is the common refinement so the expensive half
//! comes free. That machinery exists to move attributes from an old mesh to a
//! new one, and it is needed exactly when the attributes live **on the mesh**.
//!
//! They do not have to. This crate's world is not stored voxels; it is a base
//! field plus an ordered log of edits (the [`brush`] module), which is what
//! lets [`chunk::dirty`](crate::chunk::dirty) express an edit as *two fields*
//! and what lets an editor treat undo as a re-fold of the log rather than a
//! snapshot. Put the paint in the same log and it is a function of world
//! position, so re-meshing cannot move it: the carve changes the *surface*, and
//! the paint is not on the surface, it is in the space the surface passes
//! through.
//!
//! So the transfer is not cheap here. It is **unnecessary**, and the result is
//! exact rather than L²-nearest. See M-137.
//!
//! # One log, both kinds of edit
//!
//! Sprays and carves interleave in a single [`Edit`] list, and that ordering is
//! load-bearing rather than tidy. Paint is confined to a shell around the
//! surface **as it stood when the spray happened** — that is what makes it
//! graffiti rather than dyed material, and it is what leaves the inside of a
//! fresh hole bare. Walking one log in order means the field value the walk is
//! already carrying *is* that surface, at no extra cost and with no index into
//! another list to get wrong.
//!
//! ```
//! use isomesh::brush::Brush;
//! use isomesh::fields::{BoxExact, Sphere};
//! use isomesh::paint::{Edit, PaintStack, Splat};
//! use isomesh::Sdf;
//!
//! // A wall.
//! let wall = BoxExact::<f64> { center: [0.0; 3], half_extents: [2.0, 2.0, 0.25] };
//!
//! let log = [
//! // Spray red on the front face...
//! Edit::Spray(Splat {
//! shape: Sphere { center: [0.0, 0.0, -0.25], radius: 0.75 },
//! color: [1.0, 0.0, 0.0, 1.0],
//! softness: 0.1,
//! depth: 0.05,
//! }),
//! // ...then blow a hole through it, well away from the paint.
//! Edit::Carve(Brush::subtract(Sphere { center: [1.2, 0.0, 0.0], radius: 0.5 })),
//! ];
//!
//! let world = PaintStack { base: wall, edits: &log, background: [0.5, 0.5, 0.5, 1.0] };
//!
//! // Still red where it was sprayed, after the carve.
//! assert!(world.color_at([0.0, 0.0, -0.25])[0] > 0.9);
//! // And the field is a field: extractors take this directly.
//! assert!(world.sample([1.2, 0.0, 0.0]) > 0.0);
//! ```
use Vec;
use crate;
use crate::;
/// One spray: a shape, a colour, and how far the paint reaches.
///
/// The shape is any [`Sdf`], so a spray can be a sphere, a capsule dragged
/// along a stroke, or a stencil — this type does not care.
///
/// # The two widths, which do different jobs
///
/// [`softness`](Self::softness) is measured **outward from the shape**, in
/// world units: coverage is full where the shape is solid and fades to nothing
/// `softness` beyond its boundary. It is the edge of the spray cone.
///
/// [`depth`](Self::depth) is measured **from the surface**, and it is the one
/// that makes this paint rather than dye. Coverage is full at the surface as it
/// stood when this splat was sprayed and falls to nothing `depth` away from it,
/// on both sides. Carve into painted material and the newly exposed interior is
/// bare, because it was never within `depth` of the old surface.
///
/// # The shape decides reach, and it is the *only* thing that does
///
/// There is no spray direction here, and its absence is a decision rather than
/// an omission. The shape is an arbitrary [`Sdf`], so it already carries every
/// bit of directionality a caller might want — a cone, a capsule swept along a
/// stroke, a stencil intersected with a half-space — and a `direction` field
/// would duplicate what the shape already expresses.
///
/// The consequence to know about: **a shape that passes through a thin wall
/// paints the far face too**, because both faces are surface and both are
/// inside the shape. A spray that must not reach through gets a shape that does
/// not reach through. There is a test for both halves of that.
/// One entry in the world's edit log: a carve or a spray.
///
/// The order is part of the value. [`Brush`] entries already have measured
/// reordering rules — see [`BrushOp::commutes_with`](crate::brush::BrushOp::commutes_with)
/// and M-36..M-38 — and [`Spray`](Self::Spray) entries are stricter still: a
/// spray reads the surface as it stands *at its position in the log*, so moving
/// one across a carve changes what it paints. None of them commute.
/// A base field with an ordered log of carves and sprays applied to it.
///
/// This is an [`Sdf`], so it goes straight into any extractor in the crate;
/// [`color_at`](Self::color_at) is the second output, evaluated at whatever
/// positions the extractor produced.
///
/// # Relationship to [`BrushStack`](crate::brush::BrushStack)
///
/// `BrushStack` is the unpainted path and stays the right choice when there are
/// no attributes in play. A `PaintStack` whose log contains no
/// [`Spray`](Edit::Spray) samples **bit-identically** to the `BrushStack` over
/// the same brushes in the same order — both fold through [`brush::apply`], so
/// they cannot drift, and there is a test that says so.
/// A linear ramp from 1 at `x <= 0` to 0 at `x >= width`.
///
/// A zero or negative `width` is the limit of that ramp — a hard step — rather
/// than a division by zero, on the same reasoning as
/// [`smooth_min`](crate::brush::smooth_min) treating `k <= 0` as an ordinary
/// `min`: the degenerate case has a correct answer, so it gets that answer
/// instead of a rejection.
/// Colour every position, into a caller-owned buffer.
///
/// The vertex-attribute counterpart to
/// [`normals::recompute`](crate::normals::recompute): hand it the positions an
/// extractor produced and it fills `out` with one RGBA per vertex, parallel to
/// them.
///
/// `out` is truncated and refilled without releasing its capacity, so the same
/// buffer serves every chunk of a re-mesh — rule 6, the same contract as
/// [`MeshBuffer::reset`](crate::MeshBuffer::reset).