@3driseai/3drise-core 1.0.18 → 1.0.20
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/dist/animations/index.d.ts +2 -0
- package/dist/animations/index.d.ts.map +1 -0
- package/dist/animations/index.js +1 -0
- package/dist/animations/trackGenerators.d.ts +140 -0
- package/dist/animations/trackGenerators.d.ts.map +1 -0
- package/dist/animations/trackGenerators.js +443 -0
- package/dist/autoAnimate/bounds.d.ts +7 -0
- package/dist/autoAnimate/bounds.d.ts.map +1 -0
- package/dist/autoAnimate/bounds.js +23 -0
- package/dist/autoAnimate/composer.d.ts +30 -0
- package/dist/autoAnimate/composer.d.ts.map +1 -0
- package/dist/autoAnimate/composer.js +251 -0
- package/dist/autoAnimate/describeScene.d.ts +105 -0
- package/dist/autoAnimate/describeScene.d.ts.map +1 -0
- package/dist/autoAnimate/describeScene.js +165 -0
- package/dist/autoAnimate/draft.d.ts +59 -0
- package/dist/autoAnimate/draft.d.ts.map +1 -0
- package/dist/autoAnimate/draft.js +2 -0
- package/dist/autoAnimate/index.d.ts +14 -0
- package/dist/autoAnimate/index.d.ts.map +1 -0
- package/dist/autoAnimate/index.js +9 -0
- package/dist/autoAnimate/recipes.d.ts +87 -0
- package/dist/autoAnimate/recipes.d.ts.map +1 -0
- package/dist/autoAnimate/recipes.js +224 -0
- package/dist/autoAnimate/rng.d.ts +10 -0
- package/dist/autoAnimate/rng.d.ts.map +1 -0
- package/dist/autoAnimate/rng.js +16 -0
- package/dist/autoAnimate/sceneAnalyzer.d.ts +7 -0
- package/dist/autoAnimate/sceneAnalyzer.d.ts.map +1 -0
- package/dist/autoAnimate/sceneAnalyzer.js +291 -0
- package/dist/autoAnimate/types.d.ts +124 -0
- package/dist/autoAnimate/types.d.ts.map +1 -0
- package/dist/autoAnimate/types.js +1 -0
- package/dist/autoAnimate/validator.d.ts +10 -0
- package/dist/autoAnimate/validator.d.ts.map +1 -0
- package/dist/autoAnimate/validator.js +59 -0
- package/dist/data/effectsDefaults.d.ts +4 -0
- package/dist/data/effectsDefaults.d.ts.map +1 -1
- package/dist/data/effectsDefaults.js +393 -2
- package/dist/data/gridsDefaults.d.ts +5 -0
- package/dist/data/gridsDefaults.d.ts.map +1 -0
- package/dist/data/gridsDefaults.js +290 -0
- package/dist/data/index.d.ts +1 -0
- package/dist/data/index.d.ts.map +1 -1
- package/dist/data/index.js +1 -0
- package/dist/data/particlesDefaults.d.ts +3 -1
- package/dist/data/particlesDefaults.d.ts.map +1 -1
- package/dist/data/particlesDefaults.js +78 -0
- package/dist/environment/cloudConfigs.d.ts +313 -0
- package/dist/environment/cloudConfigs.d.ts.map +1 -0
- package/dist/environment/cloudConfigs.js +342 -0
- package/dist/environment/index.d.ts +5 -0
- package/dist/environment/index.d.ts.map +1 -0
- package/dist/environment/index.js +6 -0
- package/dist/environment/landscapeConfigs.d.ts +220 -0
- package/dist/environment/landscapeConfigs.d.ts.map +1 -0
- package/dist/environment/landscapeConfigs.js +256 -0
- package/dist/environment/oceanConfigs.d.ts +293 -0
- package/dist/environment/oceanConfigs.d.ts.map +1 -0
- package/dist/environment/oceanConfigs.js +380 -0
- package/dist/environment/terrainConfigs.d.ts +254 -0
- package/dist/environment/terrainConfigs.d.ts.map +1 -0
- package/dist/environment/terrainConfigs.js +292 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/properties/index.d.ts +4 -0
- package/dist/properties/index.d.ts.map +1 -0
- package/dist/properties/index.js +3 -0
- package/dist/properties/registry.d.ts +44 -0
- package/dist/properties/registry.d.ts.map +1 -0
- package/dist/properties/registry.js +343 -0
- package/dist/properties/tables/effectsProperties.d.ts +40 -0
- package/dist/properties/tables/effectsProperties.d.ts.map +1 -0
- package/dist/properties/tables/effectsProperties.js +512 -0
- package/dist/properties/tables/geometryProperties.d.ts +38 -0
- package/dist/properties/tables/geometryProperties.d.ts.map +1 -0
- package/dist/properties/tables/geometryProperties.js +91 -0
- package/dist/properties/tables/getPropertyConfig.d.ts +3 -0
- package/dist/properties/tables/getPropertyConfig.d.ts.map +1 -0
- package/dist/properties/tables/getPropertyConfig.js +16 -0
- package/dist/properties/tables/gridProperties.d.ts +6 -0
- package/dist/properties/tables/gridProperties.d.ts.map +1 -0
- package/dist/properties/tables/gridProperties.js +121 -0
- package/dist/properties/tables/index.d.ts +9 -0
- package/dist/properties/tables/index.d.ts.map +1 -0
- package/dist/properties/tables/index.js +8 -0
- package/dist/properties/tables/lightProperties.d.ts +14 -0
- package/dist/properties/tables/lightProperties.d.ts.map +1 -0
- package/dist/properties/tables/lightProperties.js +263 -0
- package/dist/properties/tables/materialBounds.d.ts +33 -0
- package/dist/properties/tables/materialBounds.d.ts.map +1 -0
- package/dist/properties/tables/materialBounds.js +180 -0
- package/dist/properties/tables/optionalProperties.d.ts +83 -0
- package/dist/properties/tables/optionalProperties.d.ts.map +1 -0
- package/dist/properties/tables/optionalProperties.js +710 -0
- package/dist/properties/tables/particlesProperties.d.ts +17 -0
- package/dist/properties/tables/particlesProperties.d.ts.map +1 -0
- package/dist/properties/tables/particlesProperties.js +334 -0
- package/dist/properties/types.d.ts +43 -0
- package/dist/properties/types.d.ts.map +1 -0
- package/dist/properties/types.js +12 -0
- package/dist/services/AnimationsManager.d.ts +6 -106
- package/dist/services/AnimationsManager.d.ts.map +1 -1
- package/dist/services/AnimationsManager.js +5 -391
- package/dist/services/SceneActions.d.ts +17 -3
- package/dist/services/SceneActions.d.ts.map +1 -1
- package/dist/services/SceneActions.js +12 -8
- package/dist/services/ShadersLibrary.d.ts +1 -1
- package/dist/services/ShadersLibrary.d.ts.map +1 -1
- package/dist/services/index.d.ts +1 -2
- package/dist/services/index.d.ts.map +1 -1
- package/dist/shaders/WallpaperGridMaterial.d.ts.map +1 -1
- package/dist/shaders/WallpaperGridMaterial.js +6 -1
- package/dist/three/GalleryLayoutV.d.ts.map +1 -1
- package/dist/three/GalleryLayoutV.js +0 -10
- package/dist/three/SceneBuilderV.d.ts.map +1 -1
- package/dist/three/SceneBuilderV.js +1 -7
- package/dist/types/actions.d.ts +8 -1
- package/dist/types/actions.d.ts.map +1 -1
- package/dist/types/actions.js +0 -4
- package/dist/types/animations.d.ts +0 -51
- package/dist/types/animations.d.ts.map +1 -1
- package/dist/types/generativeEffects.d.ts +0 -4
- package/dist/types/generativeEffects.d.ts.map +1 -1
- package/dist/types/generativeParticles.d.ts +175 -2
- package/dist/types/generativeParticles.d.ts.map +1 -1
- package/dist/types/objectSettings.d.ts +96 -17
- package/dist/types/objectSettings.d.ts.map +1 -1
- package/dist/types/scene3d.d.ts +3 -3
- package/dist/types/scene3d.d.ts.map +1 -1
- package/dist/types/types.d.ts +3 -2
- package/dist/types/types.d.ts.map +1 -1
- package/dist/utils/generativeAnimatable.d.ts +1 -1
- package/dist/utils/generativeAnimatable.d.ts.map +1 -1
- package/dist/utils/generativeAnimatable.js +7 -1
- package/dist/utils/getAnimatableProperties.d.ts +12 -1
- package/dist/utils/getAnimatableProperties.d.ts.map +1 -1
- package/dist/utils/getAnimatableProperties.js +35 -138
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +1 -0
- package/dist/utils/objectEffects.d.ts +6 -0
- package/dist/utils/objectEffects.d.ts.map +1 -0
- package/dist/utils/objectEffects.js +5 -0
- package/package.json +1 -1
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { DraftEase, DraftTrack, DraftValue } from './draft';
|
|
2
|
+
import type { Rng } from './rng';
|
|
3
|
+
import type { ObjectProfile, SceneProfile, V3 } from './types';
|
|
4
|
+
export type StyleName = 'calm' | 'energetic' | 'cinematic';
|
|
5
|
+
export interface StyleSpec {
|
|
6
|
+
name: StyleName;
|
|
7
|
+
durationMul: number;
|
|
8
|
+
amplitudeMul: number;
|
|
9
|
+
/** Entrance recipes to pick from, in preference order. */
|
|
10
|
+
enters: EnterName[];
|
|
11
|
+
enterEase: DraftEase;
|
|
12
|
+
popEase: DraftEase;
|
|
13
|
+
dropEase: DraftEase;
|
|
14
|
+
ambientEase: DraftEase;
|
|
15
|
+
/** Camera orbit swing, degrees. */
|
|
16
|
+
orbitDeg: number;
|
|
17
|
+
}
|
|
18
|
+
export declare const STYLES: Record<StyleName, StyleSpec>;
|
|
19
|
+
export declare class Ledger {
|
|
20
|
+
private authored;
|
|
21
|
+
private values;
|
|
22
|
+
constructor(authored: (state: string, path: string) => DraftValue | undefined);
|
|
23
|
+
get(state: string, path: string): DraftValue | undefined;
|
|
24
|
+
set(state: string, path: string, value: DraftValue): void;
|
|
25
|
+
/** Record every track's last value (after placing a clip). */
|
|
26
|
+
commit(tracks: DraftTrack[]): void;
|
|
27
|
+
}
|
|
28
|
+
export interface RecipeContext {
|
|
29
|
+
profile: SceneProfile;
|
|
30
|
+
rng: Rng;
|
|
31
|
+
style: StyleSpec;
|
|
32
|
+
ledger: Ledger;
|
|
33
|
+
makeId: (prefix: string) => string;
|
|
34
|
+
/** Wheel / mouse driven: no overshoot eases. */
|
|
35
|
+
driven: boolean;
|
|
36
|
+
}
|
|
37
|
+
/** One property's move for an entrance: from `away` to `rest`. */
|
|
38
|
+
export interface EnterMove {
|
|
39
|
+
state: string;
|
|
40
|
+
path: string;
|
|
41
|
+
away: DraftValue;
|
|
42
|
+
rest: DraftValue;
|
|
43
|
+
ease: DraftEase;
|
|
44
|
+
}
|
|
45
|
+
export type EnterName = 'rise' | 'drop' | 'pop' | 'spin' | 'fade';
|
|
46
|
+
export declare const round: (n: number, d?: number) => number;
|
|
47
|
+
/**
|
|
48
|
+
* Plan an entrance. Returns null when it can't work here (a rise with no room above
|
|
49
|
+
* the ground, a fade on an opaque material) — the caller tries the next one.
|
|
50
|
+
*/
|
|
51
|
+
export declare function planEnter(name: EnterName, p: ObjectProfile, amp: number, ctx: RecipeContext): EnterMove[] | null;
|
|
52
|
+
/** The first entrance of the style that works for this object (pop always does). */
|
|
53
|
+
export declare function chooseEnter(p: ObjectProfile, amp: number, ctx: RecipeContext, prefer?: EnterName): {
|
|
54
|
+
name: EnterName;
|
|
55
|
+
moves: EnterMove[];
|
|
56
|
+
};
|
|
57
|
+
/** A light "switches on": intensity from ~0 to its authored value. */
|
|
58
|
+
export declare function planLightEnter(p: ObjectProfile, ctx: RecipeContext): EnterMove[];
|
|
59
|
+
export type AmbientName = 'float' | 'sway' | 'breathe' | 'glow' | 'swell';
|
|
60
|
+
/** Keys (relative to t0, over T) of an out-and-back move; null when not applicable. */
|
|
61
|
+
export declare function planAmbient(name: AmbientName, p: ObjectProfile, amp: number, ctx: RecipeContext): {
|
|
62
|
+
path: string;
|
|
63
|
+
points: Array<[number, DraftValue]>;
|
|
64
|
+
} | null;
|
|
65
|
+
/** Pull back from further away into the authored pose. */
|
|
66
|
+
export declare function planCameraDolly(ctx: RecipeContext, amp: number): {
|
|
67
|
+
path: string;
|
|
68
|
+
away: DraftValue;
|
|
69
|
+
rest: DraftValue;
|
|
70
|
+
} | null;
|
|
71
|
+
/**
|
|
72
|
+
* Swing around a centre and back (0 → ±A → 0 on a sine, so it starts and ends at
|
|
73
|
+
* rest and its speed is continuous). The swing is limited so the peak angular
|
|
74
|
+
* speed stays under MAX_CAMERA_DEG_PER_S. Returns sampled position keys (a chord
|
|
75
|
+
* between two far keys would cut inside the arc) and, when the camera looks
|
|
76
|
+
* elsewhere, a target track that turns to the centre and back.
|
|
77
|
+
*/
|
|
78
|
+
export declare function planCameraOrbit(ctx: RecipeContext, center: V3, duration: number, direction: 1 | -1): DraftTrack[];
|
|
79
|
+
/** The slow layer: sky elevation and cloud coverage, when those are on. */
|
|
80
|
+
export declare function environmentPaths(ctx: RecipeContext): Array<{
|
|
81
|
+
state: string;
|
|
82
|
+
path: string;
|
|
83
|
+
delta: number;
|
|
84
|
+
min: number;
|
|
85
|
+
max: number;
|
|
86
|
+
}>;
|
|
87
|
+
//# sourceMappingURL=recipes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recipes.d.ts","sourceRoot":"","sources":["../../src/autoAnimate/recipes.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
export const STYLES = {
|
|
2
|
+
calm: {
|
|
3
|
+
name: 'calm', durationMul: 1.35, amplitudeMul: 0.7, enters: ['rise', 'fade', 'pop'],
|
|
4
|
+
enterEase: 'easeOutCubic', popEase: 'easeOutCubic', dropEase: 'easeOutCubic', ambientEase: 'easeInOutSine', orbitDeg: 18,
|
|
5
|
+
},
|
|
6
|
+
energetic: {
|
|
7
|
+
name: 'energetic', durationMul: 0.75, amplitudeMul: 1.2, enters: ['pop', 'drop', 'rise'],
|
|
8
|
+
enterEase: 'easeOutQuart', popEase: 'easeOutBack', dropEase: 'easeOutBounce', ambientEase: 'easeInOutQuad', orbitDeg: 30,
|
|
9
|
+
},
|
|
10
|
+
cinematic: {
|
|
11
|
+
name: 'cinematic', durationMul: 1.2, amplitudeMul: 1, enters: ['spin', 'rise', 'fade'],
|
|
12
|
+
enterEase: 'easeOutExpo', popEase: 'easeOutCubic', dropEase: 'easeOutCubic', ambientEase: 'easeInOutSine', orbitDeg: 35,
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
// ---------------------------------------------------------------------------------------
|
|
16
|
+
// Ledger — the current value of every (state, path): authored until a track moves it.
|
|
17
|
+
// ---------------------------------------------------------------------------------------
|
|
18
|
+
export class Ledger {
|
|
19
|
+
authored;
|
|
20
|
+
values = new Map();
|
|
21
|
+
constructor(authored) {
|
|
22
|
+
this.authored = authored;
|
|
23
|
+
}
|
|
24
|
+
get(state, path) {
|
|
25
|
+
const k = `${state}|${path}`;
|
|
26
|
+
return this.values.has(k) ? this.values.get(k) : this.authored(state, path);
|
|
27
|
+
}
|
|
28
|
+
set(state, path, value) {
|
|
29
|
+
this.values.set(`${state}|${path}`, value);
|
|
30
|
+
}
|
|
31
|
+
/** Record every track's last value (after placing a clip). */
|
|
32
|
+
commit(tracks) {
|
|
33
|
+
for (const t of tracks) {
|
|
34
|
+
const last = t.keys[t.keys.length - 1];
|
|
35
|
+
if (last)
|
|
36
|
+
this.set(t.state, t.path, last.value);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const asV3 = (v, fallback) => Array.isArray(v) && v.length >= 3 ? [Number(v[0]), Number(v[1]), Number(v[2])] : fallback;
|
|
41
|
+
const r3 = (v) => [round(v[0]), round(v[1]), round(v[2])];
|
|
42
|
+
export const round = (n, d = 4) => Math.round(n * 10 ** d) / 10 ** d;
|
|
43
|
+
const safeEase = (ctx, ease, fallback = 'easeOutCubic') => ctx.driven && ['easeInBack', 'easeOutBack', 'easeInOutBack', 'easeOutElastic', 'easeOutBounce'].includes(ease) ? fallback : ease;
|
|
44
|
+
const height = (p) => Math.max(p.bounds.size[1], p.bounds.radius * 0.5, 1e-3);
|
|
45
|
+
// ---------------------------------------------------------------------------------------
|
|
46
|
+
// Entrances (end at the authored values)
|
|
47
|
+
// ---------------------------------------------------------------------------------------
|
|
48
|
+
/**
|
|
49
|
+
* Plan an entrance. Returns null when it can't work here (a rise with no room above
|
|
50
|
+
* the ground, a fade on an opaque material) — the caller tries the next one.
|
|
51
|
+
*/
|
|
52
|
+
export function planEnter(name, p, amp, ctx) {
|
|
53
|
+
const id = p.id;
|
|
54
|
+
const pos = asV3(ctx.ledger.get(id, 'meshSettings.position'), p.position);
|
|
55
|
+
const rot = asV3(ctx.ledger.get(id, 'meshSettings.rotation'), p.rotation);
|
|
56
|
+
const scl = asV3(ctx.ledger.get(id, 'meshSettings.scale'), p.scale);
|
|
57
|
+
const s = ctx.style;
|
|
58
|
+
const moves = [];
|
|
59
|
+
const fade = () => {
|
|
60
|
+
if (!p.capabilities.fadeable)
|
|
61
|
+
return null;
|
|
62
|
+
const rest = Number(ctx.ledger.get(id, 'materialSettings.opacity') ?? 1);
|
|
63
|
+
return { state: id, path: 'materialSettings.opacity', away: 0, rest, ease: 'easeOutQuad' };
|
|
64
|
+
};
|
|
65
|
+
switch (name) {
|
|
66
|
+
case 'rise': {
|
|
67
|
+
// From below, but never under the ground.
|
|
68
|
+
let d = 0.35 * height(p) * amp;
|
|
69
|
+
const room = p.bounds.min[1] - ctx.profile.ground;
|
|
70
|
+
if (!p.parentId)
|
|
71
|
+
d = Math.min(d, Math.max(0, room));
|
|
72
|
+
if (d < 0.1 * height(p) * amp)
|
|
73
|
+
return null;
|
|
74
|
+
const local = d / p.parentScale[1];
|
|
75
|
+
moves.push({ state: id, path: 'meshSettings.position', away: r3([pos[0], pos[1] - local, pos[2]]), rest: pos, ease: s.enterEase });
|
|
76
|
+
const f = fade();
|
|
77
|
+
if (f)
|
|
78
|
+
moves.push(f);
|
|
79
|
+
return moves;
|
|
80
|
+
}
|
|
81
|
+
case 'drop': {
|
|
82
|
+
const local = (0.6 * height(p) * amp) / p.parentScale[1];
|
|
83
|
+
moves.push({ state: id, path: 'meshSettings.position', away: r3([pos[0], pos[1] + local, pos[2]]), rest: pos, ease: safeEase(ctx, s.dropEase) });
|
|
84
|
+
return moves;
|
|
85
|
+
}
|
|
86
|
+
case 'pop': {
|
|
87
|
+
moves.push({ state: id, path: 'meshSettings.scale', away: r3([scl[0] * 0.001, scl[1] * 0.001, scl[2] * 0.001]), rest: scl, ease: safeEase(ctx, s.popEase) });
|
|
88
|
+
return moves;
|
|
89
|
+
}
|
|
90
|
+
case 'spin': {
|
|
91
|
+
// A quarter turn into place, growing from 60%.
|
|
92
|
+
moves.push({ state: id, path: 'meshSettings.rotation', away: r3([rot[0], rot[1] - (Math.PI / 2) * Math.min(1.2, amp), rot[2]]), rest: rot, ease: s.enterEase });
|
|
93
|
+
moves.push({ state: id, path: 'meshSettings.scale', away: r3([scl[0] * 0.6, scl[1] * 0.6, scl[2] * 0.6]), rest: scl, ease: s.enterEase });
|
|
94
|
+
return moves;
|
|
95
|
+
}
|
|
96
|
+
case 'fade': {
|
|
97
|
+
const f = fade();
|
|
98
|
+
return f ? [f] : null;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
/** The first entrance of the style that works for this object (pop always does). */
|
|
104
|
+
export function chooseEnter(p, amp, ctx, prefer) {
|
|
105
|
+
const order = [...(prefer ? [prefer] : []), ...ctx.style.enters, 'pop'];
|
|
106
|
+
for (const name of order) {
|
|
107
|
+
if (p.type === 'light')
|
|
108
|
+
break;
|
|
109
|
+
const moves = planEnter(name, p, amp, ctx);
|
|
110
|
+
if (moves?.length)
|
|
111
|
+
return { name, moves };
|
|
112
|
+
}
|
|
113
|
+
return { name: 'pop', moves: planEnter('pop', p, amp, ctx) ?? [] };
|
|
114
|
+
}
|
|
115
|
+
/** A light "switches on": intensity from ~0 to its authored value. */
|
|
116
|
+
export function planLightEnter(p, ctx) {
|
|
117
|
+
if (!p.capabilities.config.includes('intensity'))
|
|
118
|
+
return [];
|
|
119
|
+
const rest = Number(ctx.ledger.get(p.id, 'config.intensity') ?? 1);
|
|
120
|
+
return [{ state: p.id, path: 'config.intensity', away: round(rest * 0.05), rest, ease: 'easeInOutSine' }];
|
|
121
|
+
}
|
|
122
|
+
/** Keys (relative to t0, over T) of an out-and-back move; null when not applicable. */
|
|
123
|
+
export function planAmbient(name, p, amp, ctx) {
|
|
124
|
+
const id = p.id;
|
|
125
|
+
const e = amp * ctx.style.amplitudeMul;
|
|
126
|
+
switch (name) {
|
|
127
|
+
case 'float': {
|
|
128
|
+
const pos = asV3(ctx.ledger.get(id, 'meshSettings.position'), p.position);
|
|
129
|
+
const up = (0.06 * height(p) * e) / p.parentScale[1];
|
|
130
|
+
return { path: 'meshSettings.position', points: [[0, pos], [0.5, r3([pos[0], pos[1] + up, pos[2]])], [1, pos]] };
|
|
131
|
+
}
|
|
132
|
+
case 'sway': {
|
|
133
|
+
const rot = asV3(ctx.ledger.get(id, 'meshSettings.rotation'), p.rotation);
|
|
134
|
+
const a = 0.06 * e;
|
|
135
|
+
return {
|
|
136
|
+
path: 'meshSettings.rotation',
|
|
137
|
+
points: [[0, rot], [0.25, r3([rot[0], rot[1], rot[2] + a])], [0.5, rot], [0.75, r3([rot[0], rot[1], rot[2] - a])], [1, rot]],
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
case 'breathe': {
|
|
141
|
+
const scl = asV3(ctx.ledger.get(id, 'meshSettings.scale'), p.scale);
|
|
142
|
+
const k = 1 + 0.04 * e;
|
|
143
|
+
return { path: 'meshSettings.scale', points: [[0, scl], [0.5, r3([scl[0] * k, scl[1] * k, scl[2] * k])], [1, scl]] };
|
|
144
|
+
}
|
|
145
|
+
case 'glow': {
|
|
146
|
+
if (!p.capabilities.emissive)
|
|
147
|
+
return null;
|
|
148
|
+
const rest = Number(ctx.ledger.get(id, 'materialSettings.emissiveIntensity') ?? 0);
|
|
149
|
+
return { path: 'materialSettings.emissiveIntensity', points: [[0, rest], [0.5, round(rest + Math.max(0.6, rest * 0.8) * e)], [1, rest]] };
|
|
150
|
+
}
|
|
151
|
+
case 'swell': {
|
|
152
|
+
if (!p.capabilities.config.includes('intensity'))
|
|
153
|
+
return null;
|
|
154
|
+
const rest = Number(ctx.ledger.get(id, 'config.intensity') ?? 1);
|
|
155
|
+
return { path: 'config.intensity', points: [[0, rest], [0.5, round(rest * (1 + 0.25 * e))], [1, rest]] };
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
// ---------------------------------------------------------------------------------------
|
|
161
|
+
// Camera
|
|
162
|
+
// ---------------------------------------------------------------------------------------
|
|
163
|
+
const MAX_CAMERA_DEG_PER_S = 60;
|
|
164
|
+
/** Pull back from further away into the authored pose. */
|
|
165
|
+
export function planCameraDolly(ctx, amp) {
|
|
166
|
+
const pos = asV3(ctx.ledger.get('camera', 'position'), ctx.profile.camera.position);
|
|
167
|
+
const target = asV3(ctx.ledger.get('camera', 'controls.target'), ctx.profile.camera.target);
|
|
168
|
+
const k = 1 + 0.35 * amp * ctx.style.amplitudeMul;
|
|
169
|
+
const away = r3([target[0] + (pos[0] - target[0]) * k, target[1] + (pos[1] - target[1]) * k, target[2] + (pos[2] - target[2]) * k]);
|
|
170
|
+
return { path: 'position', away, rest: pos };
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Swing around a centre and back (0 → ±A → 0 on a sine, so it starts and ends at
|
|
174
|
+
* rest and its speed is continuous). The swing is limited so the peak angular
|
|
175
|
+
* speed stays under MAX_CAMERA_DEG_PER_S. Returns sampled position keys (a chord
|
|
176
|
+
* between two far keys would cut inside the arc) and, when the camera looks
|
|
177
|
+
* elsewhere, a target track that turns to the centre and back.
|
|
178
|
+
*/
|
|
179
|
+
export function planCameraOrbit(ctx, center, duration, direction) {
|
|
180
|
+
const pos = asV3(ctx.ledger.get('camera', 'position'), ctx.profile.camera.position);
|
|
181
|
+
const target = asV3(ctx.ledger.get('camera', 'controls.target'), ctx.profile.camera.target);
|
|
182
|
+
const dx = pos[0] - center[0];
|
|
183
|
+
const dz = pos[2] - center[2];
|
|
184
|
+
const radius = Math.hypot(dx, dz);
|
|
185
|
+
if (radius < 1e-3 || duration <= 0)
|
|
186
|
+
return [];
|
|
187
|
+
const a0 = Math.atan2(dz, dx);
|
|
188
|
+
const maxDeg = (MAX_CAMERA_DEG_PER_S * duration) / Math.PI; // peak of A·π/D
|
|
189
|
+
const A = (Math.min(ctx.style.orbitDeg, maxDeg) * Math.PI) / 180 * direction;
|
|
190
|
+
const n = Math.max(8, Math.ceil(duration / 0.25));
|
|
191
|
+
const keys = Array.from({ length: n + 1 }, (_, i) => {
|
|
192
|
+
const t = (duration * i) / n;
|
|
193
|
+
const a = a0 + A * Math.sin((Math.PI * t) / duration);
|
|
194
|
+
return { id: ctx.makeId('k'), t: round(t), value: r3([center[0] + radius * Math.cos(a), pos[1], center[2] + radius * Math.sin(a)]), ease: 'linear' };
|
|
195
|
+
});
|
|
196
|
+
const tracks = [{ id: ctx.makeId('tr'), state: 'camera', path: 'position', keys }];
|
|
197
|
+
const off = Math.hypot(target[0] - center[0], target[1] - center[1], target[2] - center[2]);
|
|
198
|
+
if (off > ctx.profile.scale * 0.05) {
|
|
199
|
+
tracks.push({
|
|
200
|
+
id: ctx.makeId('tr'), state: 'camera', path: 'controls.target',
|
|
201
|
+
keys: [
|
|
202
|
+
{ id: ctx.makeId('k'), t: 0, value: target, ease: 'easeInOutCubic' },
|
|
203
|
+
{ id: ctx.makeId('k'), t: round(duration * 0.25), value: r3(center), ease: 'linear' },
|
|
204
|
+
{ id: ctx.makeId('k'), t: round(duration * 0.75), value: r3(center), ease: 'easeInOutCubic' },
|
|
205
|
+
{ id: ctx.makeId('k'), t: round(duration), value: target },
|
|
206
|
+
],
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
return tracks;
|
|
210
|
+
}
|
|
211
|
+
// ---------------------------------------------------------------------------------------
|
|
212
|
+
// Environment
|
|
213
|
+
// ---------------------------------------------------------------------------------------
|
|
214
|
+
/** The slow layer: sky elevation and cloud coverage, when those are on. */
|
|
215
|
+
export function environmentPaths(ctx) {
|
|
216
|
+
const env = ctx.profile.environment;
|
|
217
|
+
const out = [];
|
|
218
|
+
const a = ctx.style.amplitudeMul;
|
|
219
|
+
if (env.sky && typeof ctx.ledger.get('sky', 'elevation') === 'number')
|
|
220
|
+
out.push({ state: 'sky', path: 'elevation', delta: 6 * a, min: -5, max: 90 });
|
|
221
|
+
if (env.clouds && typeof ctx.ledger.get('clouds', 'config.coverage') === 'number')
|
|
222
|
+
out.push({ state: 'clouds', path: 'config.coverage', delta: 0.1 * a, min: 0, max: 1 });
|
|
223
|
+
return out;
|
|
224
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Seeded PRNG (mulberry32): the same seed gives the same animation (Shuffle = a new seed). */
|
|
2
|
+
export interface Rng {
|
|
3
|
+
next(): number;
|
|
4
|
+
range(min: number, max: number): number;
|
|
5
|
+
/** 1 ± amount — delicate per-member variation. */
|
|
6
|
+
jitter(amount: number): number;
|
|
7
|
+
pick<T>(list: readonly T[]): T;
|
|
8
|
+
}
|
|
9
|
+
export declare function createRng(seed: number): Rng;
|
|
10
|
+
//# sourceMappingURL=rng.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rng.d.ts","sourceRoot":"","sources":["../../src/autoAnimate/rng.ts"],"names":[],"mappings":"AAAA,+FAA+F;AAC/F,MAAM,WAAW,GAAG;IAChB,IAAI,IAAI,MAAM,CAAC;IACf,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC;IACxC,kDAAkD;IAClD,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC;IAC/B,IAAI,CAAC,CAAC,EAAE,IAAI,EAAE,SAAS,CAAC,EAAE,GAAG,CAAC,CAAC;CAClC;AAED,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,GAAG,CAe3C"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export function createRng(seed) {
|
|
2
|
+
let a = (seed >>> 0) || 0x9e3779b9;
|
|
3
|
+
const next = () => {
|
|
4
|
+
a = (a + 0x6d2b79f5) >>> 0;
|
|
5
|
+
let t = a;
|
|
6
|
+
t = Math.imul(t ^ (t >>> 15), t | 1);
|
|
7
|
+
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
|
8
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
9
|
+
};
|
|
10
|
+
return {
|
|
11
|
+
next,
|
|
12
|
+
range: (min, max) => min + (max - min) * next(),
|
|
13
|
+
jitter: (amount) => 1 + (next() * 2 - 1) * amount,
|
|
14
|
+
pick: (list) => list[Math.floor(next() * list.length) % list.length],
|
|
15
|
+
};
|
|
16
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { makeBounds } from './bounds';
|
|
2
|
+
import type { AnalyzeOptions, SceneInput, SceneProfile } from './types';
|
|
3
|
+
/** "Chair 2", "chair_copy", "Chair (3)" → "chair". */
|
|
4
|
+
export declare const baseName: (name: string) => string;
|
|
5
|
+
export declare function analyzeScene(input: SceneInput, options?: AnalyzeOptions): SceneProfile;
|
|
6
|
+
export { makeBounds };
|
|
7
|
+
//# sourceMappingURL=sceneAnalyzer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sceneAnalyzer.d.ts","sourceRoot":"","sources":["../../src/autoAnimate/sceneAnalyzer.ts"],"names":[],"mappings":"AAuBA,OAAO,EAAwC,UAAU,EAAe,MAAM,UAAU,CAAC;AACzF,OAAO,KAAK,EACR,cAAc,EACa,UAAU,EAAE,YAAY,EACtD,MAAM,SAAS,CAAC;AAsBjB,sDAAsD;AACtD,eAAO,MAAM,QAAQ,GAAI,MAAM,MAAM,KAAG,MAMzB,CAAC;AAgHhB,wBAAgB,YAAY,CAAC,KAAK,EAAE,UAAU,EAAE,OAAO,GAAE,cAAmB,GAAG,YAAY,CA4I1F;AAED,OAAO,EAAE,UAAU,EAAE,CAAC"}
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// analyzeScene — what's in the scene, from an animator's point of view.
|
|
3
|
+
// =============================================================================
|
|
4
|
+
//
|
|
5
|
+
// Pure: reads plain settings (objects, camera, environment). Scene access is
|
|
6
|
+
// injected — `measure(id)` gives real world bounds from the live scene (viewer);
|
|
7
|
+
// without it bounds are estimated from the authored transform (a unit box scaled),
|
|
8
|
+
// which is enough for layout and roles, and works server-side (aiAgent).
|
|
9
|
+
//
|
|
10
|
+
// Produces a SceneProfile:
|
|
11
|
+
// environment which of sky / clouds / ocean / terrain are on, sea level
|
|
12
|
+
// camera pose, target, distance
|
|
13
|
+
// objects type, hierarchy, world bounds, role, group, capabilities
|
|
14
|
+
// groups siblings of a group object; clusters of alike top-level
|
|
15
|
+
// objects (same base name, or same type + size + close together),
|
|
16
|
+
// with cascade orders (along the main axis, and radial)
|
|
17
|
+
// hero the object the scene is about
|
|
18
|
+
// bounds/ground/scale the content's extent — every magnitude is scaled by `scale`
|
|
19
|
+
//
|
|
20
|
+
import { Euler, Matrix4, Quaternion, Vector3 } from 'three';
|
|
21
|
+
import { getAnimatableProperties } from '../utils/getAnimatableProperties.js';
|
|
22
|
+
import { isUuid } from '../utils/idUtils.js';
|
|
23
|
+
import { boundsAround, distance, EMPTY_BOUNDS, makeBounds, unionBounds } from './bounds.js';
|
|
24
|
+
/** Never the hero, never grouped: they are the stage, not the actors. */
|
|
25
|
+
const BACKGROUND_TYPES = new Set(['grid', 'space', 'environment', 'effect', 'path']);
|
|
26
|
+
/** Can be the hero. */
|
|
27
|
+
const HERO_TYPES = new Set(['mesh', 'model', 'text', 'custom_primitive', 'gallery', 'group']);
|
|
28
|
+
/** Estimated half-extent of an unmeasured object of scale 1. */
|
|
29
|
+
const UNIT_HALF = { light: 0.15, text: 0.5 };
|
|
30
|
+
const MATERIAL_SKIP = new Set(['wireframe', 'transparent', 'flatShading', 'materialName', 'name']);
|
|
31
|
+
const vec3 = (v, fallback) => Array.isArray(v) && v.length >= 3 && v.every((n) => Number.isFinite(Number(n)))
|
|
32
|
+
? [Number(v[0]), Number(v[1]), Number(v[2])]
|
|
33
|
+
: fallback;
|
|
34
|
+
const isKeyable = (v) => typeof v === 'number' ||
|
|
35
|
+
typeof v === 'boolean' ||
|
|
36
|
+
(typeof v === 'string' && /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(v)) ||
|
|
37
|
+
(Array.isArray(v) && v.length === 3 && v.every(n => typeof n === 'number'));
|
|
38
|
+
/** "Chair 2", "chair_copy", "Chair (3)" → "chair". */
|
|
39
|
+
export const baseName = (name) => (name || '').toLowerCase()
|
|
40
|
+
.replace(/\(\d+\)/g, '')
|
|
41
|
+
.replace(/\b(copy|clone)\b/g, '')
|
|
42
|
+
.replace(/[\s_\-.#]*\d+\s*$/g, '')
|
|
43
|
+
.replace(/[\s_\-.#]+$/g, '')
|
|
44
|
+
.trim();
|
|
45
|
+
function capabilitiesOf(obj, nodes) {
|
|
46
|
+
const type = obj.type;
|
|
47
|
+
const material = type === 'light' || type === 'group'
|
|
48
|
+
? []
|
|
49
|
+
: getAnimatableProperties(obj, 'material')
|
|
50
|
+
.map(p => p.value)
|
|
51
|
+
.filter(k => !MATERIAL_SKIP.has(k) && isKeyable(obj.materialSettings?.[k]));
|
|
52
|
+
const edgesOn = obj.edgesSettings && obj.edgesSettings.enabled !== false && obj.edgesSettings.materialSettings;
|
|
53
|
+
const edges = edgesOn
|
|
54
|
+
? getAnimatableProperties(obj, 'edges')
|
|
55
|
+
.map(p => p.value)
|
|
56
|
+
.filter(k => k !== 'tubeRadius' && k !== 'cubeSize' && isKeyable(obj.edgesSettings.materialSettings[k]))
|
|
57
|
+
: [];
|
|
58
|
+
const config = [
|
|
59
|
+
...getAnimatableProperties(obj, 'light'),
|
|
60
|
+
...getAnimatableProperties(obj, 'particles'),
|
|
61
|
+
].map(p => p.value);
|
|
62
|
+
return {
|
|
63
|
+
transform: type !== 'environment',
|
|
64
|
+
material,
|
|
65
|
+
edges,
|
|
66
|
+
config,
|
|
67
|
+
nodes,
|
|
68
|
+
fadeable: material.includes('opacity') && obj.materialSettings?.transparent === true,
|
|
69
|
+
emissive: material.includes('emissiveIntensity'),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/** World matrix of an object from the authored transforms up its parent chain. */
|
|
73
|
+
function worldMatrix(id, byId, cache, guard = 0) {
|
|
74
|
+
const hit = cache.get(id);
|
|
75
|
+
if (hit)
|
|
76
|
+
return hit;
|
|
77
|
+
const obj = byId.get(id);
|
|
78
|
+
const m = new Matrix4();
|
|
79
|
+
if (obj) {
|
|
80
|
+
const ms = obj.meshSettings ?? {};
|
|
81
|
+
const r = vec3(ms.rotation, [0, 0, 0]);
|
|
82
|
+
m.compose(new Vector3(...vec3(ms.position, [0, 0, 0])), new Quaternion().setFromEuler(new Euler(r[0], r[1], r[2])), new Vector3(...vec3(ms.scale, [1, 1, 1])));
|
|
83
|
+
if (obj.parentId && byId.has(obj.parentId) && guard < 64) {
|
|
84
|
+
m.premultiply(worldMatrix(obj.parentId, byId, cache, guard + 1));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
cache.set(id, m);
|
|
88
|
+
return m;
|
|
89
|
+
}
|
|
90
|
+
function worldScale(id, byId, cache) {
|
|
91
|
+
const s = new Vector3();
|
|
92
|
+
worldMatrix(id, byId, cache).decompose(new Vector3(), new Quaternion(), s);
|
|
93
|
+
return [Math.abs(s.x) || 1, Math.abs(s.y) || 1, Math.abs(s.z) || 1];
|
|
94
|
+
}
|
|
95
|
+
function estimateBounds(obj, world) {
|
|
96
|
+
const pos = new Vector3();
|
|
97
|
+
const quat = new Quaternion();
|
|
98
|
+
const scl = new Vector3();
|
|
99
|
+
world.decompose(pos, quat, scl);
|
|
100
|
+
const h = UNIT_HALF[obj.type] ?? 0.5;
|
|
101
|
+
return boundsAround([pos.x, pos.y, pos.z], [Math.abs(scl.x) * h, Math.abs(scl.y) * h, Math.abs(scl.z) * h]);
|
|
102
|
+
}
|
|
103
|
+
function environmentOf(input) {
|
|
104
|
+
const ocean = !!input.ocean?.config?.enabled;
|
|
105
|
+
const seaLevel = Number(input.ocean?.config?.seaLevel);
|
|
106
|
+
return {
|
|
107
|
+
sky: !!input.sky?.visible,
|
|
108
|
+
clouds: !!(input.clouds?.config?.enabled || input.clouds?.cirrus?.enabled),
|
|
109
|
+
ocean,
|
|
110
|
+
terrain: !!input.terrain?.config?.enabled,
|
|
111
|
+
seaLevel: ocean && Number.isFinite(seaLevel) ? seaLevel : null,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
function cameraOf(input) {
|
|
115
|
+
const position = vec3(input.camera?.position, [0, 2, 8]);
|
|
116
|
+
const target = vec3(input.camera?.controls?.target, [0, 0, 0]);
|
|
117
|
+
const fov = Number(input.camera?.fov) || 50;
|
|
118
|
+
return { position, target, fov, distance: distance(position, target) };
|
|
119
|
+
}
|
|
120
|
+
/** Axis with the largest spread of the given centres. */
|
|
121
|
+
function mainAxis(centers) {
|
|
122
|
+
const spread = [0, 1, 2].map(i => Math.max(...centers.map(c => c[i])) - Math.min(...centers.map(c => c[i])));
|
|
123
|
+
const i = spread.indexOf(Math.max(...spread));
|
|
124
|
+
return ['x', 'y', 'z'][i];
|
|
125
|
+
}
|
|
126
|
+
function makeGroup(id, kind, parentId, members) {
|
|
127
|
+
const bounds = unionBounds(members.map(m => m.bounds)) ?? EMPTY_BOUNDS;
|
|
128
|
+
const axis = mainAxis(members.map(m => m.bounds.center));
|
|
129
|
+
const ai = { x: 0, y: 1, z: 2 }[axis];
|
|
130
|
+
return {
|
|
131
|
+
id,
|
|
132
|
+
kind,
|
|
133
|
+
parentId,
|
|
134
|
+
memberIds: members.map(m => m.id),
|
|
135
|
+
axis,
|
|
136
|
+
order: [...members].sort((a, b) => a.bounds.center[ai] - b.bounds.center[ai]).map(m => m.id),
|
|
137
|
+
radialOrder: [...members]
|
|
138
|
+
.sort((a, b) => distance(a.bounds.center, bounds.center) - distance(b.bounds.center, bounds.center))
|
|
139
|
+
.map(m => m.id),
|
|
140
|
+
bounds,
|
|
141
|
+
uniformType: members.every(m => m.type === members[0].type),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
export function analyzeScene(input, options = {}) {
|
|
145
|
+
const raw = (input.objects ?? []).filter((o) => o && typeof o.id === 'string');
|
|
146
|
+
const byId = new Map(raw.map((o) => [o.id, o]));
|
|
147
|
+
const matrices = new Map();
|
|
148
|
+
const environment = environmentOf(input);
|
|
149
|
+
const camera = cameraOf(input);
|
|
150
|
+
// --- objects (bounds, hierarchy, capabilities) ------------------------------------
|
|
151
|
+
const children = new Map();
|
|
152
|
+
raw.forEach((o) => {
|
|
153
|
+
if (o.parentId && byId.has(o.parentId)) {
|
|
154
|
+
const list = children.get(o.parentId) ?? [];
|
|
155
|
+
list.push(o.id);
|
|
156
|
+
children.set(o.parentId, list);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
const depthOf = (id, guard = 0) => {
|
|
160
|
+
const p = byId.get(id)?.parentId;
|
|
161
|
+
return p && byId.has(p) && guard < 64 ? 1 + depthOf(p, guard + 1) : 0;
|
|
162
|
+
};
|
|
163
|
+
const objects = raw.map((o) => {
|
|
164
|
+
const measured = options.measure?.(o.id) ?? null;
|
|
165
|
+
const ms = o.meshSettings ?? {};
|
|
166
|
+
return {
|
|
167
|
+
id: o.id,
|
|
168
|
+
name: ms.name || o.name || o.type,
|
|
169
|
+
type: o.type,
|
|
170
|
+
parentId: o.parentId && byId.has(o.parentId) ? o.parentId : null,
|
|
171
|
+
childIds: children.get(o.id) ?? [],
|
|
172
|
+
depth: depthOf(o.id),
|
|
173
|
+
visible: ms.visible !== false,
|
|
174
|
+
saved: isUuid(o.id),
|
|
175
|
+
position: vec3(ms.position, [0, 0, 0]),
|
|
176
|
+
rotation: vec3(ms.rotation, [0, 0, 0]),
|
|
177
|
+
scale: vec3(ms.scale, [1, 1, 1]),
|
|
178
|
+
parentScale: o.parentId && byId.has(o.parentId) ? worldScale(o.parentId, byId, matrices) : [1, 1, 1],
|
|
179
|
+
bounds: measured ?? estimateBounds(o, worldMatrix(o.id, byId, matrices)),
|
|
180
|
+
measured: !!measured,
|
|
181
|
+
role: 'standalone',
|
|
182
|
+
groupId: null,
|
|
183
|
+
capabilities: capabilitiesOf(o, o.type === 'model' ? options.nodesOf?.(o.id) ?? [] : []),
|
|
184
|
+
heroScore: 0,
|
|
185
|
+
};
|
|
186
|
+
});
|
|
187
|
+
const profileById = new Map(objects.map(p => [p.id, p]));
|
|
188
|
+
// An unmeasured container is as big as what it holds.
|
|
189
|
+
for (const p of [...objects].sort((a, b) => b.depth - a.depth)) {
|
|
190
|
+
if (p.measured || !p.childIds.length)
|
|
191
|
+
continue;
|
|
192
|
+
const kids = unionBounds(p.childIds.map(id => profileById.get(id).bounds));
|
|
193
|
+
if (kids)
|
|
194
|
+
p.bounds = kids;
|
|
195
|
+
}
|
|
196
|
+
// --- content extent -------------------------------------------------------------------
|
|
197
|
+
const actorsFirstPass = objects.filter(p => p.visible && p.type !== 'light' && !BACKGROUND_TYPES.has(p.type));
|
|
198
|
+
const roughContent = unionBounds(actorsFirstPass.map(p => p.bounds)) ?? boundsAround(camera.target, [1, 1, 1]);
|
|
199
|
+
// Huge flat things (floors, backdrops) are background too.
|
|
200
|
+
const isBackdrop = (p) => {
|
|
201
|
+
const sorted = [...p.bounds.size].sort((a, b) => a - b);
|
|
202
|
+
const flat = sorted[0] < sorted[2] * 0.05;
|
|
203
|
+
return flat && p.bounds.radius > roughContent.radius * 0.6 && !p.childIds.length;
|
|
204
|
+
};
|
|
205
|
+
// --- roles (light / background / container) ------------------------------------------
|
|
206
|
+
for (const p of objects) {
|
|
207
|
+
if (p.type === 'light')
|
|
208
|
+
p.role = 'light';
|
|
209
|
+
else if (BACKGROUND_TYPES.has(p.type) || !p.visible || (actorsFirstPass.length > 1 && isBackdrop(p)))
|
|
210
|
+
p.role = 'background';
|
|
211
|
+
else if (p.childIds.length)
|
|
212
|
+
p.role = 'container';
|
|
213
|
+
}
|
|
214
|
+
const actors = objects.filter(p => p.role !== 'light' && p.role !== 'background');
|
|
215
|
+
const bounds = unionBounds(actors.map(p => p.bounds)) ?? roughContent;
|
|
216
|
+
const scale = Math.max(0.5, bounds.radius);
|
|
217
|
+
const ground = Math.min(bounds.min[1], environment.seaLevel ?? Infinity);
|
|
218
|
+
// --- groups ---------------------------------------------------------------------------
|
|
219
|
+
const groups = [];
|
|
220
|
+
// 1. children of one group object
|
|
221
|
+
for (const p of objects) {
|
|
222
|
+
const kids = p.childIds.map(id => profileById.get(id)).filter(k => k.role !== 'light' && k.role !== 'background');
|
|
223
|
+
if (kids.length < 2)
|
|
224
|
+
continue;
|
|
225
|
+
const g = makeGroup(`parent:${p.id}`, 'parent', p.id, kids);
|
|
226
|
+
groups.push(g);
|
|
227
|
+
kids.forEach(k => { k.groupId = g.id; });
|
|
228
|
+
}
|
|
229
|
+
// 2. clusters of alike top-level objects: same base name, or same type + size + close
|
|
230
|
+
const loose = actors.filter(p => !p.parentId && !p.groupId && p.role !== 'container');
|
|
231
|
+
const parent = new Map(loose.map(p => [p.id, p.id]));
|
|
232
|
+
const find = (id) => (parent.get(id) === id ? id : find(parent.get(id)));
|
|
233
|
+
const join = (a, b) => { parent.set(find(a), find(b)); };
|
|
234
|
+
for (let i = 0; i < loose.length; i++) {
|
|
235
|
+
for (let j = i + 1; j < loose.length; j++) {
|
|
236
|
+
const a = loose[i];
|
|
237
|
+
const b = loose[j];
|
|
238
|
+
if (a.type !== b.type)
|
|
239
|
+
continue;
|
|
240
|
+
const sameName = baseName(a.name) !== '' && baseName(a.name) === baseName(b.name);
|
|
241
|
+
const ra = Math.max(a.bounds.radius, 1e-3);
|
|
242
|
+
const rb = Math.max(b.bounds.radius, 1e-3);
|
|
243
|
+
const alikeSize = Math.max(ra, rb) / Math.min(ra, rb) <= 1.6;
|
|
244
|
+
const close = distance(a.bounds.center, b.bounds.center) <= 3 * Math.max(ra, rb);
|
|
245
|
+
if (sameName || (alikeSize && close))
|
|
246
|
+
join(a.id, b.id);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
const clusters = new Map();
|
|
250
|
+
loose.forEach(p => {
|
|
251
|
+
const root = find(p.id);
|
|
252
|
+
clusters.set(root, [...(clusters.get(root) ?? []), p]);
|
|
253
|
+
});
|
|
254
|
+
for (const [root, members] of clusters) {
|
|
255
|
+
if (members.length < 2)
|
|
256
|
+
continue;
|
|
257
|
+
const g = makeGroup(`cluster:${root}`, 'cluster', null, members);
|
|
258
|
+
groups.push(g);
|
|
259
|
+
members.forEach(m => { m.groupId = g.id; });
|
|
260
|
+
}
|
|
261
|
+
for (const p of objects)
|
|
262
|
+
if (p.groupId && p.role === 'standalone')
|
|
263
|
+
p.role = 'member';
|
|
264
|
+
// --- hero -------------------------------------------------------------------------------
|
|
265
|
+
const forward = new Vector3(...camera.target).sub(new Vector3(...camera.position)).normalize();
|
|
266
|
+
const maxRadius = Math.max(...actors.map(p => p.bounds.radius), 1e-3);
|
|
267
|
+
let heroId = null;
|
|
268
|
+
let best = 0;
|
|
269
|
+
for (const p of actors) {
|
|
270
|
+
// Top-level things only: a part of a group object is not "the scene".
|
|
271
|
+
if (!HERO_TYPES.has(p.type) || p.parentId)
|
|
272
|
+
continue;
|
|
273
|
+
const size = p.bounds.radius / maxRadius;
|
|
274
|
+
const centrality = 1 - Math.min(1, distance(p.bounds.center, camera.target) / Math.max(scale, 1e-3));
|
|
275
|
+
const toObj = new Vector3(...p.bounds.center).sub(new Vector3(...camera.position)).normalize();
|
|
276
|
+
const facing = Math.max(0, forward.dot(toObj));
|
|
277
|
+
const typeBonus = p.type === 'model' ? 0.1 : p.type === 'group' ? 0.05 : 0;
|
|
278
|
+
const inCluster = p.groupId?.startsWith('cluster:') ? 0.6 : 1; // one chair among six isn't the hero
|
|
279
|
+
p.heroScore = (0.45 * size + 0.3 * centrality + 0.25 * facing + typeBonus) * inCluster;
|
|
280
|
+
if (p.heroScore > best) {
|
|
281
|
+
best = p.heroScore;
|
|
282
|
+
heroId = p.id;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
if (heroId)
|
|
286
|
+
profileById.get(heroId).role = 'hero';
|
|
287
|
+
const countsByType = {};
|
|
288
|
+
objects.forEach(p => { countsByType[p.type] = (countsByType[p.type] ?? 0) + 1; });
|
|
289
|
+
return { objects, groups, heroId, environment, camera, bounds, ground, scale, countsByType };
|
|
290
|
+
}
|
|
291
|
+
export { makeBounds };
|