@octanejs/three 0.1.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/LICENSE +49 -0
- package/README.md +215 -0
- package/UPSTREAM.md +58 -0
- package/package.json +77 -0
- package/src/config.ts +50 -0
- package/src/core/attach.ts +117 -0
- package/src/core/catalogue.ts +245 -0
- package/src/core/driver.ts +1609 -0
- package/src/core/events.ts +539 -0
- package/src/core/hooks.ts +203 -0
- package/src/core/index.ts +86 -0
- package/src/core/loader.ts +210 -0
- package/src/core/loop.ts +210 -0
- package/src/core/portal.ts +236 -0
- package/src/core/props.ts +645 -0
- package/src/core/root.ts +652 -0
- package/src/core/store.ts +741 -0
- package/src/index.ts +86 -0
- package/src/intrinsics.ts +18 -0
- package/src/renderer.ts +12 -0
- package/src/testing.ts +259 -0
- package/src/web/Canvas.tsrx +295 -0
- package/src/web/Canvas.tsrx.d.ts +23 -0
- package/src/web/events.ts +79 -0
- package/src/web/measure.ts +96 -0
|
@@ -0,0 +1,645 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Renderer-neutral Three property resolution, diffing, application, and disposal.
|
|
3
|
+
*
|
|
4
|
+
* Adapted from React Three Fiber v9.6.1's property utilities:
|
|
5
|
+
* https://github.com/pmndrs/react-three-fiber/blob/2a528745e9aa7c9e6cca41e404b59d45cf0d0cc7/packages/fiber/src/core/utils.tsx#L204-L216
|
|
6
|
+
* https://github.com/pmndrs/react-three-fiber/blob/2a528745e9aa7c9e6cca41e404b59d45cf0d0cc7/packages/fiber/src/core/utils.tsx#L256-L284
|
|
7
|
+
* https://github.com/pmndrs/react-three-fiber/blob/2a528745e9aa7c9e6cca41e404b59d45cf0d0cc7/packages/fiber/src/core/utils.tsx#L321-L389
|
|
8
|
+
* https://github.com/pmndrs/react-three-fiber/blob/2a528745e9aa7c9e6cca41e404b59d45cf0d0cc7/packages/fiber/src/core/utils.tsx#L392-L534
|
|
9
|
+
*
|
|
10
|
+
* Octane owns events, lifecycle callbacks, attachment, and disposal policy in
|
|
11
|
+
* the universal driver. Consequently this module applies ordinary Three
|
|
12
|
+
* properties only; renderer-reserved callbacks never become object fields.
|
|
13
|
+
*/
|
|
14
|
+
import * as THREE from 'three';
|
|
15
|
+
|
|
16
|
+
export type ThreePropBag = Readonly<Record<string, unknown>>;
|
|
17
|
+
|
|
18
|
+
export interface ResolvedProperty {
|
|
19
|
+
readonly root: any;
|
|
20
|
+
readonly key: string;
|
|
21
|
+
readonly target: any;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface Disposable {
|
|
25
|
+
type?: string;
|
|
26
|
+
dispose?: () => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface ApplyThreePropsOptions {
|
|
30
|
+
/** Apply R3F's managed-root sRGB texture conversion. */
|
|
31
|
+
colorSpace?: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
type PropertyApplication =
|
|
35
|
+
| 'skip'
|
|
36
|
+
| 'layers'
|
|
37
|
+
| 'color'
|
|
38
|
+
| 'copy'
|
|
39
|
+
| 'array'
|
|
40
|
+
| 'number'
|
|
41
|
+
| 'uniforms'
|
|
42
|
+
| 'assign';
|
|
43
|
+
|
|
44
|
+
interface PropertyBehaviorReader {
|
|
45
|
+
readonly target: (key: 'set' | 'copy' | 'constructor' | 'fromArray' | 'setScalar') => unknown;
|
|
46
|
+
readonly value: (key: 'constructor') => unknown;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export const THREE_RESERVED_PROPS = Object.freeze([
|
|
50
|
+
'children',
|
|
51
|
+
'key',
|
|
52
|
+
'ref',
|
|
53
|
+
'args',
|
|
54
|
+
'dispose',
|
|
55
|
+
'attach',
|
|
56
|
+
'object',
|
|
57
|
+
'onUpdate',
|
|
58
|
+
] as const);
|
|
59
|
+
|
|
60
|
+
const EVENT_PROP = /^on(Pointer|Click|DoubleClick|ContextMenu|Wheel)/;
|
|
61
|
+
const COLOR_MAPS = new Set(['map', 'emissiveMap', 'sheenColorMap', 'specularColorMap', 'envMap']);
|
|
62
|
+
const MEMOIZED_PROTOTYPES = new Map<Function, any>();
|
|
63
|
+
const THREE_CONSTRUCTORS = new Set<Function>(
|
|
64
|
+
Object.values(THREE as unknown as Record<string, unknown>).filter(
|
|
65
|
+
(value) => typeof value === 'function',
|
|
66
|
+
) as Function[],
|
|
67
|
+
);
|
|
68
|
+
const UNKNOWN_SHAPE = Symbol('unknown Three property shape');
|
|
69
|
+
|
|
70
|
+
function isObject(value: unknown): value is Record<string, any> {
|
|
71
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function isPropertyRoot(value: unknown): value is Record<string, any> {
|
|
75
|
+
return value !== null && (typeof value === 'object' || typeof value === 'function');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function isReservedProp(name: string): boolean {
|
|
79
|
+
return (THREE_RESERVED_PROPS as readonly string[]).includes(name) || EVENT_PROP.test(name);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Compare ordinary prop values with R3F's defaults: objects by identity and
|
|
84
|
+
* arrays shallowly. The latter keeps newly allocated vector arrays from
|
|
85
|
+
* producing property work when their entries are unchanged.
|
|
86
|
+
*/
|
|
87
|
+
function propsEqual(previous: unknown, next: unknown): boolean {
|
|
88
|
+
if (previous === next) return true;
|
|
89
|
+
if (typeof previous !== typeof next || Boolean(previous) !== Boolean(next)) return false;
|
|
90
|
+
if (
|
|
91
|
+
typeof previous === 'string' ||
|
|
92
|
+
typeof previous === 'number' ||
|
|
93
|
+
typeof previous === 'boolean'
|
|
94
|
+
) {
|
|
95
|
+
return previous === next;
|
|
96
|
+
}
|
|
97
|
+
if (!Array.isArray(previous) || !Array.isArray(next)) return false;
|
|
98
|
+
|
|
99
|
+
let key: string | undefined;
|
|
100
|
+
for (key in previous) {
|
|
101
|
+
if (!(key in next) || previous[key] !== next[key]) return false;
|
|
102
|
+
}
|
|
103
|
+
for (key in next) {
|
|
104
|
+
if (!(key in previous) || previous[key] !== next[key]) return false;
|
|
105
|
+
}
|
|
106
|
+
if (key === undefined) return previous.length === 0 && next.length === 0;
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function getMemoizedPrototype(root: any): any {
|
|
111
|
+
const Constructor = root?.constructor as Function | undefined;
|
|
112
|
+
if (Constructor === undefined) return undefined;
|
|
113
|
+
let prototype = MEMOIZED_PROTOTYPES.get(Constructor);
|
|
114
|
+
try {
|
|
115
|
+
if (prototype === undefined) {
|
|
116
|
+
prototype = new (Constructor as new () => any)();
|
|
117
|
+
MEMOIZED_PROTOTYPES.set(Constructor, prototype);
|
|
118
|
+
}
|
|
119
|
+
} catch {
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
122
|
+
return prototype;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Resolve direct and dash-pierced properties. A real dashed property wins over
|
|
127
|
+
* piercing, including when its current value is null or undefined.
|
|
128
|
+
*/
|
|
129
|
+
export function resolveProperty(root: any, key: string): ResolvedProperty {
|
|
130
|
+
if (!key.includes('-')) return { root, key, target: root?.[key] };
|
|
131
|
+
if (isPropertyRoot(root) && key in root) return { root, key, target: root[key] };
|
|
132
|
+
|
|
133
|
+
let target = root;
|
|
134
|
+
const parts = key.split('-');
|
|
135
|
+
for (let index = 0; index < parts.length; index++) {
|
|
136
|
+
const part = parts[index];
|
|
137
|
+
if (!isPropertyRoot(target)) {
|
|
138
|
+
return {
|
|
139
|
+
root: target,
|
|
140
|
+
key: parts.slice(index).join('-'),
|
|
141
|
+
target: undefined,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
key = part;
|
|
145
|
+
root = target;
|
|
146
|
+
target = target[key];
|
|
147
|
+
}
|
|
148
|
+
return { root, key, target };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Produce the ordinary Three property patch from two complete host snapshots.
|
|
153
|
+
* Removed value-class properties reset from a memoized zero-argument instance,
|
|
154
|
+
* matching R3F's Fast Refresh/default restoration behavior.
|
|
155
|
+
*/
|
|
156
|
+
export function diffThreeProps(
|
|
157
|
+
object: unknown,
|
|
158
|
+
next: ThreePropBag,
|
|
159
|
+
previous: ThreePropBag = {},
|
|
160
|
+
): Record<string, unknown> {
|
|
161
|
+
const changed: Record<string, unknown> = {};
|
|
162
|
+
|
|
163
|
+
for (const prop in next) {
|
|
164
|
+
if (isReservedProp(prop)) continue;
|
|
165
|
+
if (propsEqual(next[prop], previous[prop])) continue;
|
|
166
|
+
changed[prop] = next[prop];
|
|
167
|
+
|
|
168
|
+
// A replaced root invalidates every explicitly pierced descendant, even
|
|
169
|
+
// when the descendant's authored value is referentially unchanged.
|
|
170
|
+
for (const other in next) {
|
|
171
|
+
if (other.startsWith(`${prop}-`)) changed[other] = next[other];
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
for (const prop in previous) {
|
|
176
|
+
if (isReservedProp(prop) || Object.prototype.hasOwnProperty.call(next, prop)) continue;
|
|
177
|
+
const { root, key } = resolveProperty(object, prop);
|
|
178
|
+
const Constructor = root?.constructor as Function | undefined;
|
|
179
|
+
if (Constructor !== undefined && Constructor.length === 0) {
|
|
180
|
+
const prototype = getMemoizedPrototype(root);
|
|
181
|
+
if (prototype !== undefined) changed[prop] = prototype[key];
|
|
182
|
+
} else {
|
|
183
|
+
changed[prop] = 0;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return changed;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function isColorRepresentation(value: unknown): value is THREE.ColorRepresentation {
|
|
191
|
+
return (
|
|
192
|
+
value !== null &&
|
|
193
|
+
(typeof value === 'string' ||
|
|
194
|
+
typeof value === 'number' ||
|
|
195
|
+
(value as THREE.Color).isColor === true)
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function applyColorSpace(root: Record<string, any>, key: string): void {
|
|
200
|
+
if (!COLOR_MAPS.has(key)) return;
|
|
201
|
+
const texture = root[key] as THREE.Texture | undefined;
|
|
202
|
+
if (
|
|
203
|
+
texture?.isTexture === true &&
|
|
204
|
+
texture.format === THREE.RGBAFormat &&
|
|
205
|
+
texture.type === THREE.UnsignedByteType
|
|
206
|
+
) {
|
|
207
|
+
texture.colorSpace = THREE.SRGBColorSpace;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function getPropertyApplication(
|
|
212
|
+
{ root, key, target }: ResolvedProperty,
|
|
213
|
+
value: unknown,
|
|
214
|
+
behavior?: PropertyBehaviorReader,
|
|
215
|
+
): PropertyApplication {
|
|
216
|
+
const readTarget = behavior?.target ?? ((name) => target?.[name]);
|
|
217
|
+
const readValue = behavior?.value ?? ((name) => (value as any)?.[name]);
|
|
218
|
+
if (value === undefined) return 'skip';
|
|
219
|
+
if (target instanceof THREE.Layers && value instanceof THREE.Layers) return 'layers';
|
|
220
|
+
if (target instanceof THREE.Color && isColorRepresentation(value)) return 'color';
|
|
221
|
+
if (
|
|
222
|
+
target !== null &&
|
|
223
|
+
typeof target === 'object' &&
|
|
224
|
+
typeof readTarget('set') === 'function' &&
|
|
225
|
+
typeof readTarget('copy') === 'function' &&
|
|
226
|
+
readValue('constructor') !== undefined &&
|
|
227
|
+
readTarget('constructor') === readValue('constructor')
|
|
228
|
+
) {
|
|
229
|
+
return 'copy';
|
|
230
|
+
}
|
|
231
|
+
if (
|
|
232
|
+
target !== null &&
|
|
233
|
+
typeof target === 'object' &&
|
|
234
|
+
typeof readTarget('set') === 'function' &&
|
|
235
|
+
Array.isArray(value)
|
|
236
|
+
) {
|
|
237
|
+
return 'array';
|
|
238
|
+
}
|
|
239
|
+
if (
|
|
240
|
+
target !== null &&
|
|
241
|
+
typeof target === 'object' &&
|
|
242
|
+
typeof readTarget('set') === 'function' &&
|
|
243
|
+
typeof value === 'number'
|
|
244
|
+
) {
|
|
245
|
+
return 'number';
|
|
246
|
+
}
|
|
247
|
+
if (root instanceof THREE.ShaderMaterial && key === 'uniforms' && isObject(value)) {
|
|
248
|
+
return 'uniforms';
|
|
249
|
+
}
|
|
250
|
+
return 'assign';
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
type AppliedPathStatus = 'valid' | 'invalid' | 'uncertain';
|
|
254
|
+
type ShadowValue = PropertyShadow | typeof UNKNOWN_SHAPE | unknown;
|
|
255
|
+
|
|
256
|
+
class PropertyShadow {
|
|
257
|
+
readonly overrides = new Map<string, ShadowValue>();
|
|
258
|
+
uncertain = false;
|
|
259
|
+
unsafeToAssign = false;
|
|
260
|
+
|
|
261
|
+
constructor(readonly base: Record<string, any>) {}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
interface ShadowState {
|
|
265
|
+
readonly root: PropertyShadow;
|
|
266
|
+
readonly cache: WeakMap<object, PropertyShadow>;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
interface ResolvedShadowProperty {
|
|
270
|
+
readonly root: ShadowValue;
|
|
271
|
+
readonly key: string;
|
|
272
|
+
readonly target: ShadowValue;
|
|
273
|
+
readonly status: Exclude<AppliedPathStatus, 'valid'> | null;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function toShadow(value: unknown, cache: WeakMap<object, PropertyShadow>): ShadowValue {
|
|
277
|
+
if (!isPropertyRoot(value)) return value;
|
|
278
|
+
let shadow = cache.get(value);
|
|
279
|
+
if (shadow === undefined) {
|
|
280
|
+
shadow = new PropertyShadow(value);
|
|
281
|
+
cache.set(value, shadow);
|
|
282
|
+
}
|
|
283
|
+
return shadow;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function fromShadow(value: ShadowValue): unknown {
|
|
287
|
+
return value instanceof PropertyShadow ? value.base : value;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function getShadowValue(target: PropertyShadow, key: string, state: ShadowState): ShadowValue {
|
|
291
|
+
if (target.overrides.has(key)) return target.overrides.get(key);
|
|
292
|
+
if (target.uncertain) return UNKNOWN_SHAPE;
|
|
293
|
+
return toShadow(target.base[key], state.cache);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function hasShadowValue(target: PropertyShadow, key: string): boolean | null {
|
|
297
|
+
if (target.overrides.has(key)) return true;
|
|
298
|
+
if (target.uncertain) return null;
|
|
299
|
+
return key in target.base;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function readShadowMember(target: ShadowValue, key: string, state: ShadowState): ShadowValue {
|
|
303
|
+
if (target === UNKNOWN_SHAPE) return UNKNOWN_SHAPE;
|
|
304
|
+
if (target instanceof PropertyShadow) return getShadowValue(target, key, state);
|
|
305
|
+
return toShadow((target as any)?.[key], state.cache);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function getShadowAssignmentStatus(target: PropertyShadow, key: string): AppliedPathStatus {
|
|
309
|
+
if (target.overrides.has(key)) {
|
|
310
|
+
return target.overrides.get(key) === UNKNOWN_SHAPE ? 'uncertain' : 'valid';
|
|
311
|
+
}
|
|
312
|
+
if (target.uncertain && target.unsafeToAssign) return 'uncertain';
|
|
313
|
+
|
|
314
|
+
try {
|
|
315
|
+
let owner: object | null = target.base;
|
|
316
|
+
let own = true;
|
|
317
|
+
while (owner !== null) {
|
|
318
|
+
const descriptor = Object.getOwnPropertyDescriptor(owner, key);
|
|
319
|
+
if (descriptor !== undefined) {
|
|
320
|
+
if ('value' in descriptor) {
|
|
321
|
+
if (descriptor.writable !== true) return 'invalid';
|
|
322
|
+
return own || Object.isExtensible(target.base) ? 'valid' : 'invalid';
|
|
323
|
+
}
|
|
324
|
+
return descriptor.set === undefined ? 'invalid' : 'uncertain';
|
|
325
|
+
}
|
|
326
|
+
owner = Object.getPrototypeOf(owner);
|
|
327
|
+
own = false;
|
|
328
|
+
}
|
|
329
|
+
return Object.isExtensible(target.base) ? 'valid' : 'invalid';
|
|
330
|
+
} catch {
|
|
331
|
+
return 'uncertain';
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function resolveShadowProperty(state: ShadowState, key: string): ResolvedShadowProperty {
|
|
336
|
+
if (!key.includes('-')) {
|
|
337
|
+
return {
|
|
338
|
+
root: state.root,
|
|
339
|
+
key,
|
|
340
|
+
target: getShadowValue(state.root, key, state),
|
|
341
|
+
status: null,
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const direct = hasShadowValue(state.root, key);
|
|
346
|
+
if (direct === null) {
|
|
347
|
+
return { root: UNKNOWN_SHAPE, key, target: UNKNOWN_SHAPE, status: 'uncertain' };
|
|
348
|
+
}
|
|
349
|
+
if (direct) {
|
|
350
|
+
return {
|
|
351
|
+
root: state.root,
|
|
352
|
+
key,
|
|
353
|
+
target: getShadowValue(state.root, key, state),
|
|
354
|
+
status: null,
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
let root: ShadowValue = state.root;
|
|
359
|
+
let target: ShadowValue = state.root;
|
|
360
|
+
const parts = key.split('-');
|
|
361
|
+
for (let index = 0; index < parts.length; index++) {
|
|
362
|
+
const part = parts[index];
|
|
363
|
+
if (target === UNKNOWN_SHAPE) {
|
|
364
|
+
return { root: target, key: part, target, status: 'uncertain' };
|
|
365
|
+
}
|
|
366
|
+
if (!(target instanceof PropertyShadow)) {
|
|
367
|
+
return {
|
|
368
|
+
root: target,
|
|
369
|
+
key: parts.slice(index).join('-'),
|
|
370
|
+
target: undefined,
|
|
371
|
+
status: null,
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
key = part;
|
|
375
|
+
root = target;
|
|
376
|
+
target = getShadowValue(target, key, state);
|
|
377
|
+
}
|
|
378
|
+
return { root, key, target, status: null };
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function isKnownThreeValue(value: unknown): boolean {
|
|
382
|
+
const Constructor = (value as { constructor?: unknown } | null)?.constructor;
|
|
383
|
+
return typeof Constructor === 'function' && THREE_CONSTRUCTORS.has(Constructor);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function isCanonicalThreeMethod(value: unknown, key: string, method: unknown): boolean {
|
|
387
|
+
if (!isKnownThreeValue(value)) return false;
|
|
388
|
+
const Constructor = (value as { constructor: { prototype?: Record<string, unknown> } })
|
|
389
|
+
.constructor;
|
|
390
|
+
return Constructor.prototype?.[key] === method;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function markUnknownMutation(target: PropertyShadow, unsafeToAssign: boolean): void {
|
|
394
|
+
target.overrides.clear();
|
|
395
|
+
target.uncertain = true;
|
|
396
|
+
target.unsafeToAssign ||= unsafeToAssign;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function replayUniforms(
|
|
400
|
+
state: ShadowState,
|
|
401
|
+
property: ResolvedShadowProperty,
|
|
402
|
+
value: Record<string, any>,
|
|
403
|
+
): AppliedPathStatus {
|
|
404
|
+
if (!(property.root instanceof PropertyShadow)) return 'invalid';
|
|
405
|
+
let uniforms = property.target;
|
|
406
|
+
if (uniforms === UNKNOWN_SHAPE) return 'uncertain';
|
|
407
|
+
if (!isObject(fromShadow(uniforms))) {
|
|
408
|
+
const assignment = getShadowAssignmentStatus(property.root, property.key);
|
|
409
|
+
if (assignment !== 'valid') {
|
|
410
|
+
if (assignment === 'uncertain') markUnknownMutation(property.root, true);
|
|
411
|
+
return assignment;
|
|
412
|
+
}
|
|
413
|
+
uniforms = toShadow({}, state.cache);
|
|
414
|
+
property.root.overrides.set(property.key, uniforms);
|
|
415
|
+
}
|
|
416
|
+
if (!(uniforms instanceof PropertyShadow)) return 'invalid';
|
|
417
|
+
|
|
418
|
+
for (const name in value) {
|
|
419
|
+
const uniform = value[name];
|
|
420
|
+
const current = getShadowValue(uniforms, name, state);
|
|
421
|
+
if (current === UNKNOWN_SHAPE) return 'uncertain';
|
|
422
|
+
if (fromShadow(current) === undefined) {
|
|
423
|
+
const assignment = getShadowAssignmentStatus(uniforms, name);
|
|
424
|
+
if (assignment !== 'valid') {
|
|
425
|
+
if (assignment === 'uncertain') markUnknownMutation(uniforms, true);
|
|
426
|
+
return assignment;
|
|
427
|
+
}
|
|
428
|
+
uniforms.overrides.set(name, toShadow(Object.assign({}, uniform), state.cache));
|
|
429
|
+
} else if (current instanceof PropertyShadow) {
|
|
430
|
+
for (const key of Object.keys(Object(uniform))) {
|
|
431
|
+
const assignment = getShadowAssignmentStatus(current, key);
|
|
432
|
+
if (assignment !== 'valid') {
|
|
433
|
+
if (assignment === 'uncertain') markUnknownMutation(current, true);
|
|
434
|
+
return assignment;
|
|
435
|
+
}
|
|
436
|
+
current.overrides.set(key, toShadow(Object(uniform)[key], state.cache));
|
|
437
|
+
}
|
|
438
|
+
} else if (current === null) {
|
|
439
|
+
return 'invalid';
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
return 'valid';
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function replayThreeProps(object: object, props: ThreePropBag): ShadowState | AppliedPathStatus {
|
|
446
|
+
const cache = new WeakMap<object, PropertyShadow>();
|
|
447
|
+
const root = toShadow(object, cache) as PropertyShadow;
|
|
448
|
+
const state = { root, cache };
|
|
449
|
+
|
|
450
|
+
for (const prop in props) {
|
|
451
|
+
if (isReservedProp(prop)) continue;
|
|
452
|
+
const value = props[prop];
|
|
453
|
+
const property = resolveShadowProperty(state, prop);
|
|
454
|
+
if (property.status !== null) return property.status;
|
|
455
|
+
const resolved = {
|
|
456
|
+
root: fromShadow(property.root),
|
|
457
|
+
key: property.key,
|
|
458
|
+
target: fromShadow(property.target),
|
|
459
|
+
};
|
|
460
|
+
if (resolved.target === undefined && !isPropertyRoot(resolved.root)) return 'invalid';
|
|
461
|
+
|
|
462
|
+
const targetMembers = new Map<string, unknown>();
|
|
463
|
+
const valueMembers = new Map<string, unknown>();
|
|
464
|
+
const readMember = (
|
|
465
|
+
shadow: ShadowValue,
|
|
466
|
+
members: Map<string, unknown>,
|
|
467
|
+
key: string,
|
|
468
|
+
): unknown => {
|
|
469
|
+
if (members.has(key)) return members.get(key);
|
|
470
|
+
const member = readShadowMember(shadow, key, state);
|
|
471
|
+
if (member === UNKNOWN_SHAPE) throw UNKNOWN_SHAPE;
|
|
472
|
+
const result = fromShadow(member);
|
|
473
|
+
members.set(key, result);
|
|
474
|
+
return result;
|
|
475
|
+
};
|
|
476
|
+
const valueShadow = toShadow(value, cache);
|
|
477
|
+
const behavior: PropertyBehaviorReader = {
|
|
478
|
+
target: (key) => readMember(property.target, targetMembers, key),
|
|
479
|
+
value: (key) => readMember(valueShadow, valueMembers, key),
|
|
480
|
+
};
|
|
481
|
+
let application: PropertyApplication;
|
|
482
|
+
try {
|
|
483
|
+
application = getPropertyApplication(resolved, value, behavior);
|
|
484
|
+
} catch {
|
|
485
|
+
return 'uncertain';
|
|
486
|
+
}
|
|
487
|
+
if (application === 'skip') continue;
|
|
488
|
+
if (application === 'uniforms') {
|
|
489
|
+
const status = replayUniforms(state, property, value as Record<string, any>);
|
|
490
|
+
if (status !== 'valid') return status;
|
|
491
|
+
continue;
|
|
492
|
+
}
|
|
493
|
+
if (application === 'assign') {
|
|
494
|
+
if (!(property.root instanceof PropertyShadow)) return 'invalid';
|
|
495
|
+
const assignment = getShadowAssignmentStatus(property.root, property.key);
|
|
496
|
+
if (assignment === 'invalid') return assignment;
|
|
497
|
+
if (assignment === 'uncertain') {
|
|
498
|
+
markUnknownMutation(property.root, true);
|
|
499
|
+
continue;
|
|
500
|
+
}
|
|
501
|
+
property.root.overrides.set(property.key, toShadow(value, cache));
|
|
502
|
+
continue;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
if (!(property.target instanceof PropertyShadow)) return 'invalid';
|
|
506
|
+
let safeMutation = false;
|
|
507
|
+
try {
|
|
508
|
+
if (application === 'layers') {
|
|
509
|
+
const assignment = getShadowAssignmentStatus(property.target, 'mask');
|
|
510
|
+
if (assignment === 'invalid') return assignment;
|
|
511
|
+
safeMutation = assignment === 'valid' && resolved.target?.constructor === THREE.Layers;
|
|
512
|
+
} else if (application === 'color') {
|
|
513
|
+
const set = behavior.target('set');
|
|
514
|
+
if (typeof set !== 'function') return 'invalid';
|
|
515
|
+
safeMutation =
|
|
516
|
+
resolved.target?.constructor === THREE.Color &&
|
|
517
|
+
isCanonicalThreeMethod(resolved.target, 'set', set);
|
|
518
|
+
} else if (application === 'copy') {
|
|
519
|
+
safeMutation = isCanonicalThreeMethod(resolved.target, 'copy', behavior.target('copy'));
|
|
520
|
+
} else if (application === 'array') {
|
|
521
|
+
const fromArray = behavior.target('fromArray');
|
|
522
|
+
const method = typeof fromArray === 'function' ? fromArray : behavior.target('set');
|
|
523
|
+
safeMutation = isCanonicalThreeMethod(
|
|
524
|
+
resolved.target,
|
|
525
|
+
typeof fromArray === 'function' ? 'fromArray' : 'set',
|
|
526
|
+
method,
|
|
527
|
+
);
|
|
528
|
+
} else {
|
|
529
|
+
const setScalar = behavior.target('setScalar');
|
|
530
|
+
const method = typeof setScalar === 'function' ? setScalar : behavior.target('set');
|
|
531
|
+
safeMutation = isCanonicalThreeMethod(
|
|
532
|
+
resolved.target,
|
|
533
|
+
typeof setScalar === 'function' ? 'setScalar' : 'set',
|
|
534
|
+
method,
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
} catch {
|
|
538
|
+
return 'uncertain';
|
|
539
|
+
}
|
|
540
|
+
markUnknownMutation(property.target, !safeMutation);
|
|
541
|
+
}
|
|
542
|
+
return state;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Inspect whether an attachment's leaf parent will still be object-like after
|
|
547
|
+
* applying an ordered prop patch. This replays direct dashed-key precedence,
|
|
548
|
+
* root replacement order, and known Three value-class setters without touching
|
|
549
|
+
* the committed object. Unknown custom setter effects are reported separately.
|
|
550
|
+
*/
|
|
551
|
+
export function inspectAppliedThreePropsPath(
|
|
552
|
+
object: object,
|
|
553
|
+
path: string,
|
|
554
|
+
props: ThreePropBag,
|
|
555
|
+
): AppliedPathStatus {
|
|
556
|
+
const replay = replayThreeProps(object, props);
|
|
557
|
+
if (!(typeof replay === 'object')) return replay;
|
|
558
|
+
|
|
559
|
+
const direct = hasShadowValue(replay.root, path);
|
|
560
|
+
if (direct === null) return 'uncertain';
|
|
561
|
+
if (direct) return 'valid';
|
|
562
|
+
|
|
563
|
+
let target: ShadowValue = replay.root;
|
|
564
|
+
const parts = path.split('-');
|
|
565
|
+
for (let index = 0; index < parts.length - 1; index++) {
|
|
566
|
+
if (target === UNKNOWN_SHAPE) return 'uncertain';
|
|
567
|
+
if (!(target instanceof PropertyShadow)) return 'invalid';
|
|
568
|
+
target = getShadowValue(target, parts[index], replay);
|
|
569
|
+
}
|
|
570
|
+
if (target === UNKNOWN_SHAPE) return 'uncertain';
|
|
571
|
+
return target instanceof PropertyShadow
|
|
572
|
+
? target.unsafeToAssign
|
|
573
|
+
? 'uncertain'
|
|
574
|
+
: 'valid'
|
|
575
|
+
: 'invalid';
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Apply ordinary Three properties. Passing `previous` makes this perform the
|
|
580
|
+
* R3F-compatible diff first; omitting it applies the complete initial snapshot.
|
|
581
|
+
*/
|
|
582
|
+
export function applyThreeProps<T extends object>(
|
|
583
|
+
object: T,
|
|
584
|
+
next: ThreePropBag,
|
|
585
|
+
previous?: ThreePropBag,
|
|
586
|
+
options: ApplyThreePropsOptions = {},
|
|
587
|
+
): T {
|
|
588
|
+
const props = previous === undefined ? next : diffThreeProps(object, next, previous);
|
|
589
|
+
|
|
590
|
+
for (const prop in props) {
|
|
591
|
+
if (isReservedProp(prop)) continue;
|
|
592
|
+
const value = props[prop];
|
|
593
|
+
|
|
594
|
+
const property = resolveProperty(object, prop);
|
|
595
|
+
const { root, key, target } = property;
|
|
596
|
+
if (target === undefined && !isPropertyRoot(root)) {
|
|
597
|
+
throw new Error(
|
|
598
|
+
`@octanejs/three: Cannot set ${JSON.stringify(prop)}. Ensure the parent is an object before setting ${JSON.stringify(key)}.`,
|
|
599
|
+
);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
const application = getPropertyApplication(property, value);
|
|
603
|
+
if (application === 'skip') {
|
|
604
|
+
continue;
|
|
605
|
+
} else if (application === 'layers') {
|
|
606
|
+
target.mask = (value as THREE.Layers).mask;
|
|
607
|
+
} else if (application === 'color') {
|
|
608
|
+
target.set(value);
|
|
609
|
+
} else if (application === 'copy') {
|
|
610
|
+
target.copy(value);
|
|
611
|
+
} else if (application === 'array') {
|
|
612
|
+
if (typeof target.fromArray === 'function') target.fromArray(value);
|
|
613
|
+
else target.set(...(value as unknown[]));
|
|
614
|
+
} else if (application === 'number') {
|
|
615
|
+
if (typeof target.setScalar === 'function') target.setScalar(value);
|
|
616
|
+
else target.set(value);
|
|
617
|
+
} else if (application === 'uniforms') {
|
|
618
|
+
if (!isObject(root.uniforms)) root.uniforms = {};
|
|
619
|
+
const uniforms = value as Record<string, any>;
|
|
620
|
+
for (const name in uniforms) {
|
|
621
|
+
const uniform = uniforms[name];
|
|
622
|
+
const current = root.uniforms[name];
|
|
623
|
+
if (current !== undefined) Object.assign(current, uniform);
|
|
624
|
+
else root.uniforms[name] = { ...uniform };
|
|
625
|
+
}
|
|
626
|
+
} else {
|
|
627
|
+
if (!isPropertyRoot(root)) {
|
|
628
|
+
throw new Error(`@octanejs/three: Cannot assign ${JSON.stringify(prop)}.`);
|
|
629
|
+
}
|
|
630
|
+
root[key] = value;
|
|
631
|
+
if (options.colorSpace === true) applyColorSpace(root, key);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
return object;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/** Dispose an object and its directly-owned disposable properties. */
|
|
639
|
+
export function dispose<T extends Disposable>(object: T): void {
|
|
640
|
+
if (object.type !== 'Scene') object.dispose?.();
|
|
641
|
+
for (const name in object) {
|
|
642
|
+
const value = object[name] as Disposable | undefined;
|
|
643
|
+
if (value?.type !== 'Scene') value?.dispose?.();
|
|
644
|
+
}
|
|
645
|
+
}
|