@3driseai/3drise-viewer 0.1.32 → 0.1.33
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/components/CameraController.d.ts.map +1 -1
- package/dist/components/CameraController.js +9 -0
- package/dist/components/ObjectsRenderer.d.ts.map +1 -1
- package/dist/components/ObjectsRenderer.js +26 -19
- package/dist/components/SceneBuilder.d.ts.map +1 -1
- package/dist/components/SceneBuilder.js +7 -4
- package/dist/components/land/LandPatch.d.ts.map +1 -1
- package/dist/components/land/LandPatch.js +15 -1
- package/dist/hooks/useContinuousEffects.js +1 -1
- package/dist/hooks/useOrbitTargetFollow.d.ts.map +1 -1
- package/dist/hooks/useOrbitTargetFollow.js +6 -4
- package/dist/operations/bake.d.ts +36 -0
- package/dist/operations/bake.d.ts.map +1 -0
- package/dist/operations/bake.js +113 -0
- package/dist/operations/catalogue/shaderLayers.js +2 -2
- package/dist/operations/catalogue/warmup.d.ts +23 -1
- package/dist/operations/catalogue/warmup.d.ts.map +1 -1
- package/dist/operations/catalogue/warmup.js +200 -12
- package/dist/operations/index.d.ts +2 -0
- package/dist/operations/index.d.ts.map +1 -1
- package/dist/operations/index.js +2 -0
- package/dist/operations/runtime.d.ts +17 -9
- package/dist/operations/runtime.d.ts.map +1 -1
- package/dist/operations/runtime.js +71 -18
- package/dist/triggers/readiness.d.ts +2 -0
- package/dist/triggers/readiness.d.ts.map +1 -1
- package/dist/triggers/readiness.js +28 -3
- package/dist/utils/stallLog.d.ts +4 -0
- package/dist/utils/stallLog.d.ts.map +1 -0
- package/dist/utils/stallLog.js +88 -0
- package/package.json +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CameraController.d.ts","sourceRoot":"","sources":["../../src/components/CameraController.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAQ7D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAGzD,UAAU,qBAAsB,SAAQ,gBAAgB;IACpD,QAAQ,EAAE,WAAW,CAAC;IACtB,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;;OAIG;IACH,eAAe,CAAC,EAAE,CAAC,QAAQ,EAAE,GAAG,KAAK,IAAI,CAAC;IAC1C,iBAAiB,CAAC,EAAE,CAAC,QAAQ,EAAE,GAAG,KAAK,IAAI,CAAC;CAC/C;AAGD,eAAO,MAAM,gBAAgB,EAAE,KAAK,CAAC,EAAE,CAAC,qBAAqB,
|
|
1
|
+
{"version":3,"file":"CameraController.d.ts","sourceRoot":"","sources":["../../src/components/CameraController.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAQ7D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAGzD,UAAU,qBAAsB,SAAQ,gBAAgB;IACpD,QAAQ,EAAE,WAAW,CAAC;IACtB,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;;OAIG;IACH,eAAe,CAAC,EAAE,CAAC,QAAQ,EAAE,GAAG,KAAK,IAAI,CAAC;IAC1C,iBAAiB,CAAC,EAAE,CAAC,QAAQ,EAAE,GAAG,KAAK,IAAI,CAAC;CAC/C;AAGD,eAAO,MAAM,gBAAgB,EAAE,KAAK,CAAC,EAAE,CAAC,qBAAqB,CAqI5D,CAAC;AACF,eAAe,gBAAgB,CAAC"}
|
|
@@ -94,6 +94,15 @@ export const CameraController = ({ enableAutoAnimation = true, animationDuration
|
|
|
94
94
|
orbitControls.maxDistance = controls.maxDistance;
|
|
95
95
|
// Saved limits may be null (once ±Infinity): the full range then.
|
|
96
96
|
Object.assign(orbitControls, orbitAngleLimits(controls));
|
|
97
|
+
// The authored limits, kept on the live instance: controllers drag them directly
|
|
98
|
+
// (setLive), the follow hook widens from them and clamps the height with them,
|
|
99
|
+
// and the limits helper draws them.
|
|
100
|
+
orbitControls.authoredLimits = {
|
|
101
|
+
minDistance: controls.minDistance, maxDistance: controls.maxDistance,
|
|
102
|
+
...orbitAngleLimits(controls),
|
|
103
|
+
minHeight: typeof controls.minHeight === 'number' ? controls.minHeight : null,
|
|
104
|
+
maxHeight: typeof controls.maxHeight === 'number' ? controls.maxHeight : null,
|
|
105
|
+
};
|
|
97
106
|
}, [controls]);
|
|
98
107
|
// Look at a scene object (orbit target) and / or travel with one (follow).
|
|
99
108
|
useOrbitTargetFollow(controlsRef, controls.targetObject, controls.followObject, () => isInAnimatedTransition.current || isAnimating() || isPathAnimating(), controls.enabled !== false, controls);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ObjectsRenderer.d.ts","sourceRoot":"","sources":["../../src/components/ObjectsRenderer.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,qBAAqB,EAAE,WAAW,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AACvH,OAAO,KAAK,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"ObjectsRenderer.d.ts","sourceRoot":"","sources":["../../src/components/ObjectsRenderer.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,qBAAqB,EAAE,WAAW,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AACvH,OAAO,KAAK,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAU9E,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAC7D,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AACjC,OAAO,EAAE,KAAK,UAAU,EAAE,MAAM,oBAAoB,CAAC;AACrD,UAAU,oBAAqB,SAAQ,gBAAgB;IACnD,cAAc,EAAE,WAAW,CAAC;IAC5B,qBAAqB,EAAE,qBAAqB,CAAC;IAC7C,cAAc,EAAE,qBAAqB,EAAE,CAAC;IACxC,cAAc,CAAC,EAAE,MAAM,IAAI,CAAC;IAC5B,iBAAiB,CAAC,EAAE,CAAC,MAAM,EAAE,qBAAqB,EAAE,GAAG,EAAE,QAAQ,EAAE,KAAK,EAAE,UAAU,CAAC,UAAU,CAAC,KAAK,IAAI,CAAC;IAC1G,mEAAmE;IACnE,SAAS,CAAC,EAAE,iBAAiB,EAAE,CAAC;IAChC,KAAK,CAAC,EAAE,aAAa,EAAE,CAAC;IACxB,eAAe,CAAC,EAAE,cAAc,EAAE,CAAC;CACtC;AAGD,QAAA,MAAM,eAAe,EAAE,KAAK,CAAC,EAAE,CAAC,oBAAoB,CAgGlD,CAAC;AAEH,eAAe,eAAe,CAAC"}
|
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { memo, useRef, useCallback, useMemo, useEffect } from 'react';
|
|
3
|
-
import { buildObjectTree } from '@3driseai/3drise-core';
|
|
2
|
+
import { memo, useRef, useCallback, useMemo, useEffect, useState } from 'react';
|
|
3
|
+
import { buildObjectTree, SceneStore } from '@3driseai/3drise-core';
|
|
4
4
|
import ObjectNode from './ObjectNode.js';
|
|
5
5
|
import useInteraction from '../hooks/useInteraction.js';
|
|
6
6
|
import { ObjectHoverContext, setEffectPrewarmer, setEffectPreviewer, setInteractionRunner } from '../interactions/runner.js';
|
|
7
7
|
import { prewarmEffect, previewEffect, previewEffects } from '../operations/runtime.js';
|
|
8
|
+
import { bakeProject, setBakeSource } from '../operations/bake.js';
|
|
9
|
+
import { installStallLog } from '../utils/stallLog.js';
|
|
8
10
|
import { findById } from '../utils/sceneQuery.js';
|
|
9
|
-
import {
|
|
11
|
+
import { commandsOf, sceneReadiness } from '../triggers/index.js';
|
|
10
12
|
import { useObjectReady } from '../hooks/useObjectReady.js';
|
|
11
13
|
const ObjectsRenderer = memo(({ cameraSettings, createdObjects, generalObjectSettings, onObjectsReady, onObjectReady, onObjectDispose, handleObjectClick, sequences = [], clips = [], actionSequences = [], }) => {
|
|
12
14
|
const groupRef = useRef(null);
|
|
13
15
|
const objectsTree = useMemo(() => buildObjectTree(createdObjects), [createdObjects]);
|
|
14
16
|
const readyObjectsCount = useRef(0);
|
|
17
|
+
const bumpBakeRef = useRef(() => { });
|
|
15
18
|
// Count for the aggregate onObjectsReady signal, then forward the event up.
|
|
16
19
|
// Previously this callback counted and dropped the (object, ref) pair, which
|
|
17
20
|
// is why nothing above ObjectsRenderer ever received an object ref.
|
|
@@ -21,6 +24,7 @@ const ObjectsRenderer = memo(({ cameraSettings, createdObjects, generalObjectSet
|
|
|
21
24
|
onObjectsReady && onObjectsReady();
|
|
22
25
|
}
|
|
23
26
|
onObjectReady && onObjectReady(object, ref);
|
|
27
|
+
bumpBakeRef.current();
|
|
24
28
|
}, [objectsTree.length, onObjectsReady, onObjectReady]);
|
|
25
29
|
const handleObjectDispose = useCallback((object, ref) => {
|
|
26
30
|
readyObjectsCount.current = Math.max(0, readyObjectsCount.current - 1);
|
|
@@ -72,24 +76,27 @@ const ObjectsRenderer = memo(({ cameraSettings, createdObjects, generalObjectSet
|
|
|
72
76
|
const ref = findById(objectId);
|
|
73
77
|
return ref ? prewarmEffect(effect, ref) : Promise.resolve();
|
|
74
78
|
}), []);
|
|
75
|
-
//
|
|
76
|
-
//
|
|
79
|
+
// Bake everything this project can play (operations/bake.ts): every effect of its interactions,
|
|
80
|
+
// actions, clip / sequence cues, effect clips and objects' own Effects tabs is compiled and
|
|
81
|
+
// prepared ahead of time, so no first play stalls. A published scene bakes inside its readiness,
|
|
82
|
+
// under the loading cover; here it re-bakes in the background when the lists change or an object
|
|
83
|
+
// reports ready (a model landed — only what is new is warmed).
|
|
84
|
+
const [bakeTick, setBakeTick] = useState(0);
|
|
77
85
|
useEffect(() => {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
continue;
|
|
83
|
-
for (const ix of (o.actions ?? []))
|
|
84
|
-
for (const e of effectsOf(ix))
|
|
85
|
-
void prewarmEffect(e, ref);
|
|
86
|
-
}
|
|
87
|
-
for (const a of actionSequences ?? [])
|
|
88
|
-
for (const e of actionEffectsOf(a))
|
|
89
|
-
void prewarmEffect(e, null);
|
|
90
|
-
}, 1500);
|
|
86
|
+
setBakeSource({ objects: createdObjects, actions: actionSequences, clips, sequences });
|
|
87
|
+
if (!sceneReadiness.isReady())
|
|
88
|
+
return; // the readiness bake does it
|
|
89
|
+
const t = setTimeout(() => { void bakeProject(); }, 400);
|
|
91
90
|
return () => clearTimeout(t);
|
|
92
|
-
}, [createdObjects, actionSequences]);
|
|
91
|
+
}, [createdObjects, actionSequences, clips, sequences, bakeTick]);
|
|
92
|
+
const bumpBake = useCallback(() => setBakeTick(n => n + 1), []);
|
|
93
|
+
bumpBakeRef.current = bumpBake;
|
|
94
|
+
// dev: log frames that stall and why (localStorage '3drise:stalls' = '1' — utils/stallLog)
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
const t = setTimeout(() => { const gl = SceneStore.gl; if (gl)
|
|
97
|
+
installStallLog(gl); }, 500);
|
|
98
|
+
return () => clearTimeout(t);
|
|
99
|
+
}, []);
|
|
93
100
|
const handleObjectClickHandler = (createdObject, ref, event) => {
|
|
94
101
|
const clickInteractions = filterInteractionsByEvent(createdObject.actions, "click");
|
|
95
102
|
clickInteractions.forEach((interaction) => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SceneBuilder.d.ts","sourceRoot":"","sources":["../../src/components/SceneBuilder.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"SceneBuilder.d.ts","sourceRoot":"","sources":["../../src/components/SceneBuilder.tsx"],"names":[],"mappings":"AAKA,OAAO,EAAqB,KAAK,WAAW,EAA0B,MAAM,oBAAoB,CAAC;AAGjG,OAAO,KAAK,EAAE,iBAAiB,EAAE,OAAO,EAAE,MAAM,uBAAuB,CAAC;AAWxE,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAM7D,MAAM,MAAM,uBAAuB,GAAG,iBAAiB,GAAG,gBAAgB,GAAG;IACzE;;;OAGG;IACH,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,IAAI,CAAC;IAC5C,mFAAmF;IACnF,gBAAgB,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IACnC,oEAAoE;IACpE,EAAE,CAAC,EAAE,OAAO,CAAC;IACb,qFAAqF;IACrF,QAAQ,CAAC,EAAE,OAAO,EAAE,CAAC;IACrB,2EAA2E;IAC3E,QAAQ,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,QAAA,MAAM,YAAY;IAfd;;;OAGG;mBACY,CAAC,KAAK,EAAE,WAAW,KAAK,IAAI;IAC3C,mFAAmF;uBAChE,KAAK,CAAC,SAAS;IAClC,oEAAoE;SAC/D,OAAO;IACZ,qFAAqF;eAC1E,OAAO,EAAE;IACpB,2EAA2E;eAChE,MAAM;qCA6LnB,CAAC;AAIH,eAAe,YAAY,CAAC"}
|
|
@@ -2,6 +2,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import AdaptiveResolution from './AdaptiveResolution.js';
|
|
3
3
|
import { forwardRef, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
4
4
|
import { EFFECT_CLIP_ASSET, effectClipFromAsset, setEffectClipTemplates } from "../operations/runtime.js";
|
|
5
|
+
import { compileScene, uploadTextures } from "../operations/catalogue/warmup.js";
|
|
6
|
+
import { bakeProject } from "../operations/bake.js";
|
|
5
7
|
import { createSceneHandle } from "../api/sceneHandle.js";
|
|
6
8
|
import { Canvas } from "@react-three/fiber";
|
|
7
9
|
import { LIGHTING_PRESETS, SceneStore } from "@3driseai/3drise-core";
|
|
@@ -81,10 +83,11 @@ const SceneBuilder = forwardRef(({ project, width = "100%", height = "100vh", st
|
|
|
81
83
|
const [started, setStarted] = useState(false);
|
|
82
84
|
useLayoutEffect(() => {
|
|
83
85
|
sceneReadiness.begin([CANVAS_READY, ...(modelKey ? modelKey.split(',') : [])], {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
86
|
+
// as the frame draws it (render target included — see warmup), then every texture uploaded
|
|
87
|
+
// (objects hidden at load would upload theirs in the frame they appear in)
|
|
88
|
+
compile: () => compileScene(threeRef.current).then(() => { uploadTextures(); }),
|
|
89
|
+
// every effect the project can play, compiled and prepared (operations/bake)
|
|
90
|
+
bake: () => bakeProject(),
|
|
88
91
|
});
|
|
89
92
|
if (threeRef.current)
|
|
90
93
|
sceneReadiness.report(CANVAS_READY); // remount with the canvas already there
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LandPatch.d.ts","sourceRoot":"","sources":["../../../src/components/land/LandPatch.tsx"],"names":[],"mappings":"AACA,OAAO,KAAmE,MAAM,OAAO,CAAC;AAExF,OAAO,EAGH,KAAK,gBAAgB,EACrB,KAAK,gBAAgB,EACrB,KAAK,eAAe,EACvB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;
|
|
1
|
+
{"version":3,"file":"LandPatch.d.ts","sourceRoot":"","sources":["../../../src/components/land/LandPatch.tsx"],"names":[],"mappings":"AACA,OAAO,KAAmE,MAAM,OAAO,CAAC;AAExF,OAAO,EAGH,KAAK,gBAAgB,EACrB,KAAK,gBAAgB,EACrB,KAAK,eAAe,EACvB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAoDhE,MAAM,WAAW,cAAe,SAAQ,gBAAgB;IACpD,MAAM,EAAE,gBAAgB,CAAC;IACzB,4DAA4D;IAC5D,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,mEAAmE;IACnE,SAAS,CAAC,EAAE,gBAAgB,EAAE,CAAC;IAC/B,8DAA8D;IAC9D,SAAS,CAAC,EAAE,eAAe,CAAC;CAC/B;AASD,QAAA,MAAM,SAAS,EAAE,KAAK,CAAC,EAAE,CAAC,cAAc,CAgJvC,CAAC;AAEF,eAAe,SAAS,CAAC"}
|
|
@@ -11,6 +11,13 @@ import { TerrainMaterial } from '../../shaders/terrain/TerrainMaterial.js';
|
|
|
11
11
|
import { applySunUniforms } from '../../utils/cloudUniforms.js';
|
|
12
12
|
import { applyTerrainAmbient, applyTerrainTextures, applyTerrainUniforms } from '../../utils/terrainUniforms.js';
|
|
13
13
|
import { withConfigDefaults } from '../../utils/configAnimations.js';
|
|
14
|
+
import { isMobileDevice } from '../../utils/deviceTier.js';
|
|
15
|
+
/**
|
|
16
|
+
* Phone / tablet tier, at render time only — the saved object keeps its numbers.
|
|
17
|
+
* The same caps as the environment terrain (TerrainSurface), plus the low grid:
|
|
18
|
+
* per-pixel cost (octaves, texture distance) is what dominates, measured on desktop.
|
|
19
|
+
*/
|
|
20
|
+
const MOBILE = { quality: 'low', maxOctaves: 5, maxTextureDistance: 400 };
|
|
14
21
|
import { resolveTerrainTextureSets, useTerrainTextureResolverVersion } from '../../utils/terrainTextureResolver.js';
|
|
15
22
|
import { LAND_SEGMENTS, applyLandPatchUniforms, landFrame, landHeightAt, landQuality, landToTerrainConfig, } from '../../utils/landPatch.js';
|
|
16
23
|
// =============================================================================
|
|
@@ -47,7 +54,8 @@ const LandPatch = ({ config, id, keyframes, mouseMove, onObjectReady, onObjectDi
|
|
|
47
54
|
const type = DEFAULT_LAND_OBJECTS[config.type] ? config.type : 'hills';
|
|
48
55
|
const base = useMemo(() => withConfigDefaults(DEFAULT_LAND_OBJECTS[type], config), [config, type]);
|
|
49
56
|
const allowed = useMemo(() => new Set(LAND_OBJECTS_ANIMATABLE[type].map((p) => p.value)), [type]);
|
|
50
|
-
const
|
|
57
|
+
const mobile = isMobileDevice();
|
|
58
|
+
const segments = LAND_SEGMENTS[mobile ? MOBILE.quality : base.quality] ?? 256;
|
|
51
59
|
const geometry = useMemo(() => {
|
|
52
60
|
const g = new THREE.PlaneGeometry(1, 1, segments, segments);
|
|
53
61
|
g.rotateX(-Math.PI / 2);
|
|
@@ -78,6 +86,12 @@ const LandPatch = ({ config, id, keyframes, mouseMove, onObjectReady, onObjectDi
|
|
|
78
86
|
if (!mesh)
|
|
79
87
|
return;
|
|
80
88
|
const cfg = { ...current.current, textures: current.current.textures ?? base.textures };
|
|
89
|
+
if (mobile) {
|
|
90
|
+
// quality must match the grid built above: the shader's cell size comes from it.
|
|
91
|
+
cfg.quality = MOBILE.quality;
|
|
92
|
+
cfg.octaves = Math.min(cfg.octaves, MOBILE.maxOctaves);
|
|
93
|
+
cfg.textureDistance = Math.min(cfg.textureDistance ?? 4000, MOBILE.maxTextureDistance);
|
|
94
|
+
}
|
|
81
95
|
const t = landToTerrainConfig(cfg);
|
|
82
96
|
const s = sun.update();
|
|
83
97
|
const m = material;
|
|
@@ -97,7 +97,7 @@ objectSettings, options = {}, objectSource) => {
|
|
|
97
97
|
object,
|
|
98
98
|
meshSettings: objectSettings.meshSettings || {},
|
|
99
99
|
pathIsActive,
|
|
100
|
-
}, activeTransformAnimations, currentTime, hovered);
|
|
100
|
+
}, activeTransformAnimations, currentTime, hovered, delta);
|
|
101
101
|
}
|
|
102
102
|
if (hasEdgesAnimations) {
|
|
103
103
|
handleEdgesAnimations({ object, startValuesCache: edgesStartValuesRef.current }, edgesAnimations, currentTime, hovered);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useOrbitTargetFollow.d.ts","sourceRoot":"","sources":["../../src/hooks/useOrbitTargetFollow.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"useOrbitTargetFollow.d.ts","sourceRoot":"","sources":["../../src/hooks/useOrbitTargetFollow.ts"],"names":[],"mappings":"AAgDA,OAAO,KAAK,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAsE/E,wBAAgB,oBAAoB,CAChC,WAAW,EAAE,KAAK,CAAC,gBAAgB,CAAC,GAAG,CAAC,EACxC,YAAY,EAAE,iBAAiB,GAAG,IAAI,GAAG,SAAS,EAClD,YAAY,EAAE,iBAAiB,GAAG,IAAI,GAAG,SAAS;AAClD,+EAA+E;AAC/E,YAAY,EAAE,MAAM,OAAO;AAC3B,4FAA4F;AAC5F,QAAQ,EAAE,OAAO;AACjB,gFAAgF;AAChF,QAAQ,EAAE,cAAc,QAsE3B;AAED,eAAe,oBAAoB,CAAC"}
|
|
@@ -34,7 +34,8 @@
|
|
|
34
34
|
// So while looking at an object, the limits are widened each frame just enough to hold
|
|
35
35
|
// where the camera is now: the object can take the view outside the authored range,
|
|
36
36
|
// the user still can't rotate / zoom further out of it, and moving back in tightens
|
|
37
|
-
// them to the authored values again. The authored values
|
|
37
|
+
// them to the authored values again. The authored values are `controls.authoredLimits` on
|
|
38
|
+
// the live instance (CameraController sets them, controllers drag them), else the config.
|
|
38
39
|
//
|
|
39
40
|
// Height limits (controls.minHeight / maxHeight) are world Y, not relative to the target,
|
|
40
41
|
// and are applied after everything else in the frame.
|
|
@@ -173,8 +174,9 @@ authored) {
|
|
|
173
174
|
}
|
|
174
175
|
// HEIGHT LIMITS (world Y): last, so nothing — user, target, follow, a clip — ends
|
|
175
176
|
// the frame under the floor or over the ceiling. The camera keeps facing the target.
|
|
176
|
-
const
|
|
177
|
-
const
|
|
177
|
+
const lim = controls?.authoredLimits ?? auth;
|
|
178
|
+
const minH = typeof lim.minHeight === 'number' ? lim.minHeight : -Infinity;
|
|
179
|
+
const maxH = typeof lim.maxHeight === 'number' ? lim.maxHeight : Infinity;
|
|
178
180
|
const y = Math.min(maxH, Math.max(minH, camera.position.y));
|
|
179
181
|
if (y !== camera.position.y) {
|
|
180
182
|
camera.position.y = y;
|
|
@@ -186,7 +188,7 @@ authored) {
|
|
|
186
188
|
camera.updateMatrixWorld();
|
|
187
189
|
// Limits hold the camera where the moving target left it (see the header).
|
|
188
190
|
if (tObj && controls && a.locked)
|
|
189
|
-
widenLimits(controls, camera.position, auth);
|
|
191
|
+
widenLimits(controls, camera.position, controls.authoredLimits ?? auth);
|
|
190
192
|
};
|
|
191
193
|
return () => { scene.onBeforeRender = prev; };
|
|
192
194
|
}, [scene, camera, controlsRef]);
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { ActionSequence, SceneCommand } from '@3driseai/3drise-core';
|
|
2
|
+
import type { OperationParams } from './types';
|
|
3
|
+
/** What a project holds (the lists SceneBuilder / the studio render with). */
|
|
4
|
+
export interface BakeSource {
|
|
5
|
+
objects?: ReadonlyArray<any>;
|
|
6
|
+
actions?: ReadonlyArray<ActionSequence>;
|
|
7
|
+
clips?: ReadonlyArray<{
|
|
8
|
+
cues?: ReadonlyArray<{
|
|
9
|
+
command?: SceneCommand | null;
|
|
10
|
+
}>;
|
|
11
|
+
}>;
|
|
12
|
+
sequences?: ReadonlyArray<{
|
|
13
|
+
cues?: ReadonlyArray<{
|
|
14
|
+
command?: SceneCommand | null;
|
|
15
|
+
}>;
|
|
16
|
+
}>;
|
|
17
|
+
}
|
|
18
|
+
/** One thing to warm: an operation with its parameters on target ids ('self' already resolved). */
|
|
19
|
+
interface BakeJob {
|
|
20
|
+
op: string;
|
|
21
|
+
params: OperationParams;
|
|
22
|
+
targets: string[];
|
|
23
|
+
meshNames?: Record<string, string[]>;
|
|
24
|
+
}
|
|
25
|
+
/** Every warm job of a project. */
|
|
26
|
+
export declare function collectBakeJobs(src: BakeSource): BakeJob[];
|
|
27
|
+
/** The project the scene renders (ObjectsRenderer keeps it current). */
|
|
28
|
+
export declare const setBakeSource: (src: BakeSource) => void;
|
|
29
|
+
export interface BakeReport {
|
|
30
|
+
jobs: number;
|
|
31
|
+
ms: number;
|
|
32
|
+
}
|
|
33
|
+
/** Bake the current project now (queued behind a bake that is running). Resolves when done. */
|
|
34
|
+
export declare function bakeProject(src?: BakeSource): Promise<BakeReport>;
|
|
35
|
+
export {};
|
|
36
|
+
//# sourceMappingURL=bake.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bake.d.ts","sourceRoot":"","sources":["../../src/operations/bake.ts"],"names":[],"mappings":"AA4BA,OAAO,KAAK,EAAE,cAAc,EAA6C,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAKrH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAE/C,8EAA8E;AAC9E,MAAM,WAAW,UAAU;IACvB,OAAO,CAAC,EAAE,aAAa,CAAC,GAAG,CAAC,CAAC;IAC7B,OAAO,CAAC,EAAE,aAAa,CAAC,cAAc,CAAC,CAAC;IACxC,KAAK,CAAC,EAAE,aAAa,CAAC;QAAE,IAAI,CAAC,EAAE,aAAa,CAAC;YAAE,OAAO,CAAC,EAAE,YAAY,GAAG,IAAI,CAAA;SAAE,CAAC,CAAA;KAAE,CAAC,CAAC;IACnF,SAAS,CAAC,EAAE,aAAa,CAAC;QAAE,IAAI,CAAC,EAAE,aAAa,CAAC;YAAE,OAAO,CAAC,EAAE,YAAY,GAAG,IAAI,CAAA;SAAE,CAAC,CAAA;KAAE,CAAC,CAAC;CAC1F;AAED,mGAAmG;AACnG,UAAU,OAAO;IACb,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,eAAe,CAAC;IACxB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;CACxC;AASD,mCAAmC;AACnC,wBAAgB,eAAe,CAAC,GAAG,EAAE,UAAU,GAAG,OAAO,EAAE,CAyC1D;AAiBD,wEAAwE;AACxE,eAAO,MAAM,aAAa,GAAI,KAAK,UAAU,SAAsB,CAAC;AAIpE,MAAM,WAAW,UAAU;IAAG,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE;AAExD,+FAA+F;AAC/F,wBAAgB,WAAW,CAAC,GAAG,GAAE,UAAmB,GAAG,OAAO,CAAC,UAAU,CAAC,CAkBzE"}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { findNodesByName, objectEffectClipOf } from '@3driseai/3drise-core';
|
|
2
|
+
import { findById } from '../utils/sceneQuery.js';
|
|
3
|
+
import { getOperation, resolveParams } from './registry.js';
|
|
4
|
+
import { getEffectClipTemplate, isWarmable, warmEffectOn } from './runtime.js';
|
|
5
|
+
import { objectEffectParams } from './objectEffects.js';
|
|
6
|
+
const commandsOfInteraction = (ix) => (Array.isArray(ix?.commands) ? ix.commands : []);
|
|
7
|
+
const commandsOfAction = (a) => (Array.isArray(a?.commands) ? a.commands : []);
|
|
8
|
+
/** The effects of an effect clip (template or an object's own): Base and every step. */
|
|
9
|
+
const clipEffects = (clip) => [...(clip?.effects ?? []), ...(clip?.steps ?? []).flatMap(s => s?.effects ?? [])].filter(e => e && typeof e.operation === 'string');
|
|
10
|
+
/** Every warm job of a project. */
|
|
11
|
+
export function collectBakeJobs(src) {
|
|
12
|
+
const jobs = new Map();
|
|
13
|
+
const add = (op, params, targets, meshNames) => {
|
|
14
|
+
if (!targets.length)
|
|
15
|
+
return;
|
|
16
|
+
const k = JSON.stringify([op, params, targets, meshNames ?? null]);
|
|
17
|
+
if (!jobs.has(k))
|
|
18
|
+
jobs.set(k, { op, params, targets, ...(meshNames ? { meshNames } : {}) });
|
|
19
|
+
};
|
|
20
|
+
const objectRow = (e, targets, meshNames) => {
|
|
21
|
+
const def = getOperation(e.operation);
|
|
22
|
+
if (!isWarmable(def))
|
|
23
|
+
return;
|
|
24
|
+
add(def.value, objectEffectParams(def, e), targets, meshNames);
|
|
25
|
+
};
|
|
26
|
+
const fromCommand = (c, selfId) => {
|
|
27
|
+
if (!c)
|
|
28
|
+
return;
|
|
29
|
+
const ids = (list) => (list ?? []).map(t => (t === 'self' ? selfId : t)).filter((t) => !!t);
|
|
30
|
+
if (c.kind === 'effect') {
|
|
31
|
+
const e = c.effect;
|
|
32
|
+
if (!e || e.enabled === false)
|
|
33
|
+
return;
|
|
34
|
+
const def = getOperation(e.operation);
|
|
35
|
+
if (!isWarmable(def))
|
|
36
|
+
return;
|
|
37
|
+
add(def.value, resolveParams(def, e.params), ids(e.targets), e.meshNames);
|
|
38
|
+
}
|
|
39
|
+
else if (c.kind === 'effectClip') {
|
|
40
|
+
const use = c.use;
|
|
41
|
+
const clip = use && getEffectClipTemplate(use.clip);
|
|
42
|
+
if (!clip)
|
|
43
|
+
return;
|
|
44
|
+
for (const e of clipEffects(clip))
|
|
45
|
+
objectRow(e, ids(use.targets), use.meshNames);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
for (const o of src.objects ?? []) {
|
|
49
|
+
if (!o?.id)
|
|
50
|
+
continue;
|
|
51
|
+
for (const ix of (Array.isArray(o.actions) ? o.actions : [])) {
|
|
52
|
+
if (ix?.enabled === false)
|
|
53
|
+
continue;
|
|
54
|
+
for (const c of commandsOfInteraction(ix))
|
|
55
|
+
fromCommand(c, o.id);
|
|
56
|
+
}
|
|
57
|
+
const own = objectEffectClipOf(o);
|
|
58
|
+
const parts = own.parts?.length ? { [o.id]: own.parts } : undefined;
|
|
59
|
+
for (const e of clipEffects(own))
|
|
60
|
+
objectRow(e, [o.id], parts);
|
|
61
|
+
}
|
|
62
|
+
for (const a of src.actions ?? [])
|
|
63
|
+
for (const c of commandsOfAction(a))
|
|
64
|
+
fromCommand(c, null);
|
|
65
|
+
for (const t of [...(src.clips ?? []), ...(src.sequences ?? [])])
|
|
66
|
+
for (const cue of t?.cues ?? [])
|
|
67
|
+
fromCommand(cue?.command, null);
|
|
68
|
+
return [...jobs.values()];
|
|
69
|
+
}
|
|
70
|
+
function targetsOf(job) {
|
|
71
|
+
const out = [];
|
|
72
|
+
for (const id of job.targets) {
|
|
73
|
+
const root = findById(id);
|
|
74
|
+
if (!root)
|
|
75
|
+
continue;
|
|
76
|
+
const names = job.meshNames?.[id];
|
|
77
|
+
if (names?.length)
|
|
78
|
+
for (const n of names)
|
|
79
|
+
out.push(...findNodesByName(root, n).filter((x) => x !== root));
|
|
80
|
+
else
|
|
81
|
+
out.push(root);
|
|
82
|
+
}
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
85
|
+
// ---------------------------------------------------------------- running it
|
|
86
|
+
let source = {};
|
|
87
|
+
/** The project the scene renders (ObjectsRenderer keeps it current). */
|
|
88
|
+
export const setBakeSource = (src) => { source = src; };
|
|
89
|
+
let running = null;
|
|
90
|
+
/** Bake the current project now (queued behind a bake that is running). Resolves when done. */
|
|
91
|
+
export function bakeProject(src = source) {
|
|
92
|
+
if (running)
|
|
93
|
+
return running.then(() => bakeProject(src));
|
|
94
|
+
const run = (async () => {
|
|
95
|
+
const t0 = performance.now();
|
|
96
|
+
const jobs = collectBakeJobs(src);
|
|
97
|
+
for (const job of jobs) {
|
|
98
|
+
const def = getOperation(job.op);
|
|
99
|
+
if (!def)
|
|
100
|
+
continue;
|
|
101
|
+
const targets = targetsOf(job);
|
|
102
|
+
if (targets.length)
|
|
103
|
+
await warmEffectOn(def, targets, job.params);
|
|
104
|
+
// let a frame through between jobs (the studio is live while it bakes)
|
|
105
|
+
await new Promise(r => setTimeout(r, 0));
|
|
106
|
+
}
|
|
107
|
+
return { jobs: jobs.length, ms: Math.round(performance.now() - t0) };
|
|
108
|
+
})();
|
|
109
|
+
running = run;
|
|
110
|
+
void run.finally(() => { if (running === run)
|
|
111
|
+
running = null; });
|
|
112
|
+
return run;
|
|
113
|
+
}
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
// three keeps covers every cached program.
|
|
35
35
|
//
|
|
36
36
|
import { Color, Group, InstancedMesh, Material as ThreeMaterial, Mesh, SkinnedMesh } from 'three';
|
|
37
|
-
import { compileInBackground, warmContext } from './warmup.js';
|
|
37
|
+
import { asInFrame, compileInBackground, warmContext } from './warmup.js';
|
|
38
38
|
/** material → uniform name → the live uniform object of the current layer */
|
|
39
39
|
const liveUniforms = new WeakMap();
|
|
40
40
|
/** material → uniform name → the stable proxy handed to the compiled program */
|
|
@@ -170,7 +170,7 @@ export async function commitPendingLayers(root) {
|
|
|
170
170
|
if (ctx) {
|
|
171
171
|
// let the live materials take the cached programs before the clones let go of them
|
|
172
172
|
try {
|
|
173
|
-
ctx.gl.compile(root, ctx.cam, ctx.scene);
|
|
173
|
+
asInFrame(ctx, () => ctx.gl.compile(root, ctx.cam, ctx.scene));
|
|
174
174
|
}
|
|
175
175
|
catch { /* next frame will */ }
|
|
176
176
|
setTimeout(() => clones.forEach(c => c.dispose()), 1500);
|
|
@@ -5,12 +5,34 @@ export interface WarmContext {
|
|
|
5
5
|
cam: any;
|
|
6
6
|
}
|
|
7
7
|
export declare function warmContext(): WarmContext | null;
|
|
8
|
+
/** Run a compile with the render target the scene is drawn into bound (see the header). */
|
|
9
|
+
export declare function asInFrame<T>(ctx: WarmContext, fn: () => T): T;
|
|
8
10
|
/** compileAsync with a safety timeout (a driver that never reports ready must not hang an effect). */
|
|
9
|
-
export declare function compileInBackground(root: Object3D, timeoutMs?: number): Promise<void>;
|
|
11
|
+
export declare function compileInBackground(root: Object3D, timeoutMs?: number, given?: WarmContext | null): Promise<void>;
|
|
12
|
+
/** The whole scene, as the frame draws it (the published scene's load: see triggers/readiness). */
|
|
13
|
+
export declare function compileScene(three?: {
|
|
14
|
+
gl: any;
|
|
15
|
+
scene: any;
|
|
16
|
+
camera: any;
|
|
17
|
+
} | null, timeoutMs?: number): Promise<void>;
|
|
18
|
+
/**
|
|
19
|
+
* Upload every texture of `root` to the GPU now. three uploads a texture the first time
|
|
20
|
+
* something using it is drawn — in that frame: an object hidden at load and shown later
|
|
21
|
+
* (a cue, an action, visibility) would stall the frame it appears in.
|
|
22
|
+
*/
|
|
23
|
+
export declare function uploadTextures(root?: Object3D): number;
|
|
10
24
|
/** Keep a freshly created helper off screen until its shaders are compiled (see warmHelpers). */
|
|
11
25
|
export declare function hideUntilWarm(obj: Object3D): void;
|
|
12
26
|
/** Compile every helper under `root` that is waiting, in the background, then show it. */
|
|
13
27
|
export declare function warmHelpers(root: Object3D): Promise<void>;
|
|
28
|
+
/**
|
|
29
|
+
* The shadow pass draws a shadow caster with three's own depth material — a program
|
|
30
|
+
* compileAsync never builds, so a new kind of caster (an instanced helper) compiled it
|
|
31
|
+
* in the first frame that drew its shadow. Build it here: a stand-in of the same kind
|
|
32
|
+
* with an equivalent depth material, compiled into a render target as the shadow map is.
|
|
33
|
+
* The stand-ins are kept (never drawn) so the programs stay cached.
|
|
34
|
+
*/
|
|
35
|
+
export declare function warmShadowPass(root: Object3D): Promise<void>;
|
|
14
36
|
/**
|
|
15
37
|
* Warm-only runs (pre-warming): while this is on, an effect's runProgress compiles
|
|
16
38
|
* everything it needs and then stops — nothing animates. See runtime.prewarmEffect.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"warmup.d.ts","sourceRoot":"","sources":["../../../src/operations/catalogue/warmup.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"warmup.d.ts","sourceRoot":"","sources":["../../../src/operations/catalogue/warmup.ts"],"names":[],"mappings":"AAsCA,OAAO,KAAK,EAAY,QAAQ,EAAW,MAAM,OAAO,CAAC;AAGzD,MAAM,WAAW,WAAW;IAAG,EAAE,EAAE,GAAG,CAAC;IAAC,KAAK,EAAE,GAAG,CAAC;IAAC,GAAG,EAAE,GAAG,CAAA;CAAE;AAE9D,wBAAgB,WAAW,IAAI,WAAW,GAAG,IAAI,CAIhD;AA4BD,2FAA2F;AAC3F,wBAAgB,SAAS,CAAC,CAAC,EAAE,GAAG,EAAE,WAAW,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,CAO7D;AAED,sGAAsG;AACtG,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,QAAQ,EAAE,SAAS,SAAO,EAAE,KAAK,CAAC,EAAE,WAAW,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAQ/G;AAED,mGAAmG;AACnG,wBAAgB,YAAY,CAAC,KAAK,CAAC,EAAE;IAAE,EAAE,EAAE,GAAG,CAAC;IAAC,KAAK,EAAE,GAAG,CAAC;IAAC,MAAM,EAAE,GAAG,CAAA;CAAE,GAAG,IAAI,EAAE,SAAS,SAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAOjH;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,IAAI,CAAC,EAAE,QAAQ,GAAG,MAAM,CAatD;AAWD,iGAAiG;AACjG,wBAAgB,aAAa,CAAC,GAAG,EAAE,QAAQ,QAI1C;AAED,0FAA0F;AAC1F,wBAAgB,WAAW,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAQzD;AAsBD;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAK5D;AAyCD;;;GAGG;AACH,eAAO,MAAM,QAAQ;;CAAgB,CAAC;AAStC,wBAAgB,cAAc,CAAC,IAAI,EAAE,QAAQ,QAqB5C"}
|
|
@@ -17,32 +17,125 @@
|
|
|
17
17
|
// and reuses it, no compile (see shaderLayers.commitPendingLayers);
|
|
18
18
|
// • effect helpers (particles, threads, rings, beams, instances): their
|
|
19
19
|
// materials start hidden (material.visible = false; three still compiles
|
|
20
|
-
// hidden materials) and are shown once their program is ready
|
|
20
|
+
// hidden materials) and are shown once their program is ready — together with
|
|
21
|
+
// their shadow-pass program when they cast shadows (compileAsync never
|
|
22
|
+
// compiles the shadow pass).
|
|
21
23
|
// Without a renderer / camera (SSR, tests) everything happens immediately.
|
|
22
24
|
//
|
|
25
|
+
// Every warm compile runs AS THE FRAME DRAWS THE SCENE (asInFrame): three bakes tone
|
|
26
|
+
// mapping and the output colour space into a program by the render target bound when
|
|
27
|
+
// it compiles — the screen (null) gets the renderer's tone mapping and sRGB, a render
|
|
28
|
+
// target gets neither. Volumetric clouds and post-processing draw the scene into a
|
|
29
|
+
// render target, so a warm-up compiled against the screen built the wrong program and
|
|
30
|
+
// the first real frame compiled again, in the frame (every effect froze). The target
|
|
31
|
+
// the scene is really drawn into is watched (scene.onBeforeRender) and bound for the
|
|
32
|
+
// compile. Measured in claude/prebake-audit.md.
|
|
33
|
+
//
|
|
23
34
|
import { SceneStore } from '@3driseai/3drise-core';
|
|
24
|
-
import { InstancedMesh, LineSegments, Mesh, Points } from 'three';
|
|
35
|
+
import { Color, InstancedMesh, LineSegments, Mesh, MeshDepthMaterial, MeshDistanceMaterial, Points, RGBADepthPacking, VSMShadowMap, WebGLRenderTarget, } from 'three';
|
|
25
36
|
import cameraManager from '../../utils/CameraSingleton.js';
|
|
26
37
|
export function warmContext() {
|
|
27
38
|
const gl = SceneStore.gl, scene = SceneStore.scene, cam = cameraManager.getCamera?.();
|
|
39
|
+
if (scene)
|
|
40
|
+
watchFrameTarget(scene);
|
|
28
41
|
return gl && scene && cam && typeof gl.compileAsync === 'function' ? { gl, scene, cam } : null;
|
|
29
42
|
}
|
|
43
|
+
// ---------------------------------------------------------------- the frame's render target
|
|
44
|
+
const FRAME_TARGET = '__fxFrameTarget';
|
|
45
|
+
const UNSEEN = 'unseen';
|
|
46
|
+
/** Note, every frame, the render target the scene is drawn into. Installed once per scene. */
|
|
47
|
+
function watchFrameTarget(scene) {
|
|
48
|
+
if (scene.userData[FRAME_TARGET] !== undefined)
|
|
49
|
+
return;
|
|
50
|
+
scene.userData[FRAME_TARGET] = UNSEEN;
|
|
51
|
+
const own = scene.onBeforeRender;
|
|
52
|
+
scene.onBeforeRender = function (renderer, sc, camera, target) {
|
|
53
|
+
scene.userData[FRAME_TARGET] = target ?? null;
|
|
54
|
+
return own?.call(this, renderer, sc, camera, target);
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/** Resolves once the scene has been drawn since the watch started (its target is known), or after a short wait. */
|
|
58
|
+
function frameSeen(ctx) {
|
|
59
|
+
if (ctx.scene.userData[FRAME_TARGET] !== UNSEEN || typeof requestAnimationFrame !== 'function')
|
|
60
|
+
return Promise.resolve();
|
|
61
|
+
return new Promise(resolve => {
|
|
62
|
+
const t0 = performance.now();
|
|
63
|
+
const check = () => (ctx.scene.userData[FRAME_TARGET] !== UNSEEN || performance.now() - t0 > 500 ? resolve() : requestAnimationFrame(check));
|
|
64
|
+
requestAnimationFrame(check);
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
/** Run a compile with the render target the scene is drawn into bound (see the header). */
|
|
68
|
+
export function asInFrame(ctx, fn) {
|
|
69
|
+
const seen = ctx.scene.userData?.[FRAME_TARGET];
|
|
70
|
+
const target = seen === UNSEEN || seen === undefined ? null : seen;
|
|
71
|
+
const prev = ctx.gl.getRenderTarget();
|
|
72
|
+
if (target === prev)
|
|
73
|
+
return fn();
|
|
74
|
+
ctx.gl.setRenderTarget(target);
|
|
75
|
+
try {
|
|
76
|
+
return fn();
|
|
77
|
+
}
|
|
78
|
+
finally {
|
|
79
|
+
ctx.gl.setRenderTarget(prev);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
30
82
|
/** compileAsync with a safety timeout (a driver that never reports ready must not hang an effect). */
|
|
31
|
-
export function compileInBackground(root, timeoutMs = 8000) {
|
|
32
|
-
const ctx = warmContext();
|
|
83
|
+
export function compileInBackground(root, timeoutMs = 8000, given) {
|
|
84
|
+
const ctx = given ?? warmContext();
|
|
33
85
|
if (!ctx)
|
|
34
86
|
return Promise.resolve();
|
|
35
87
|
let timer;
|
|
36
|
-
return Promise.race([
|
|
37
|
-
Promise.resolve(ctx.gl.compileAsync(root, ctx.cam, ctx.scene)).then(() => undefined, () => undefined),
|
|
88
|
+
return frameSeen(ctx).then(() => Promise.race([
|
|
89
|
+
Promise.resolve(asInFrame(ctx, () => ctx.gl.compileAsync(root, ctx.cam, ctx.scene))).then(() => undefined, () => undefined),
|
|
38
90
|
new Promise(r => { timer = setTimeout(r, timeoutMs); }),
|
|
39
|
-
]).finally(() => clearTimeout(timer));
|
|
91
|
+
])).finally(() => clearTimeout(timer));
|
|
92
|
+
}
|
|
93
|
+
/** The whole scene, as the frame draws it (the published scene's load: see triggers/readiness). */
|
|
94
|
+
export function compileScene(three, timeoutMs = 8000) {
|
|
95
|
+
let ctx = warmContext();
|
|
96
|
+
if (!ctx && three?.gl?.compileAsync && three.scene && three.camera) {
|
|
97
|
+
watchFrameTarget(three.scene);
|
|
98
|
+
ctx = { gl: three.gl, scene: three.scene, cam: three.camera };
|
|
99
|
+
}
|
|
100
|
+
return ctx ? compileInBackground(ctx.scene, timeoutMs, ctx) : Promise.resolve();
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Upload every texture of `root` to the GPU now. three uploads a texture the first time
|
|
104
|
+
* something using it is drawn — in that frame: an object hidden at load and shown later
|
|
105
|
+
* (a cue, an action, visibility) would stall the frame it appears in.
|
|
106
|
+
*/
|
|
107
|
+
export function uploadTextures(root) {
|
|
108
|
+
const ctx = warmContext();
|
|
109
|
+
if (!ctx || typeof ctx.gl.initTexture !== 'function')
|
|
110
|
+
return 0;
|
|
111
|
+
const seen = new Set();
|
|
112
|
+
(root ?? ctx.scene).traverse((c) => {
|
|
113
|
+
for (const m of materialsOf1(c)) {
|
|
114
|
+
for (const v of Object.values(m))
|
|
115
|
+
if (v?.isTexture && !v.isRenderTargetTexture)
|
|
116
|
+
seen.add(v);
|
|
117
|
+
const u = m.uniforms;
|
|
118
|
+
if (u)
|
|
119
|
+
for (const x of Object.values(u)) {
|
|
120
|
+
const t = x?.value;
|
|
121
|
+
if (t?.isTexture && !t.isRenderTargetTexture)
|
|
122
|
+
seen.add(t);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
for (const t of seen) {
|
|
127
|
+
try {
|
|
128
|
+
ctx.gl.initTexture(t);
|
|
129
|
+
}
|
|
130
|
+
catch { /* not uploadable yet (image still loading) */ }
|
|
131
|
+
}
|
|
132
|
+
return seen.size;
|
|
40
133
|
}
|
|
41
134
|
const FLAG = '__opFxWarm';
|
|
135
|
+
const materialsOf1 = (c) => (c.material ? (Array.isArray(c.material) ? c.material : [c.material]) : []);
|
|
42
136
|
const materialsOf = (obj) => {
|
|
43
137
|
const out = [];
|
|
44
|
-
obj.traverse((c) => {
|
|
45
|
-
out.push(...(Array.isArray(c.material) ? c.material : [c.material])); });
|
|
138
|
+
obj.traverse((c) => { out.push(...materialsOf1(c)); });
|
|
46
139
|
return out;
|
|
47
140
|
};
|
|
48
141
|
/** Keep a freshly created helper off screen until its shaders are compiled (see warmHelpers). */
|
|
@@ -58,13 +151,105 @@ export function warmHelpers(root) {
|
|
|
58
151
|
const waiting = [];
|
|
59
152
|
root.traverse((c) => { if (c.userData?.[FLAG])
|
|
60
153
|
waiting.push(c); });
|
|
61
|
-
return Promise.all(waiting.map(h => compileInBackground(h).then(() => {
|
|
154
|
+
return Promise.all(waiting.map(h => Promise.all([compileInBackground(h), warmShadowPass(h)]).then(() => {
|
|
62
155
|
for (const m of materialsOf(h))
|
|
63
156
|
m.visible = true;
|
|
64
157
|
delete h.userData[FLAG];
|
|
65
158
|
retainPrograms(h);
|
|
66
159
|
}))).then(() => undefined);
|
|
67
160
|
}
|
|
161
|
+
// ---------------------------------------------------------------- the shadow pass
|
|
162
|
+
let shadowTarget = null;
|
|
163
|
+
// three's shadow pass draws a caster with ITS depth material turned to the caster's shadow side
|
|
164
|
+
// (front faces → back, back → front, double stays) — the side is part of the program
|
|
165
|
+
const SHADOW_SIDE = { 0: 1, 1: 0, 2: 2 };
|
|
166
|
+
const depthMats = new Map();
|
|
167
|
+
function depthFor(kind, side) {
|
|
168
|
+
const key = `${kind}|${side}`;
|
|
169
|
+
let m = depthMats.get(key);
|
|
170
|
+
if (!m) {
|
|
171
|
+
m = kind === 'depth' ? new MeshDepthMaterial({ depthPacking: RGBADepthPacking }) : new MeshDistanceMaterial();
|
|
172
|
+
m.side = side;
|
|
173
|
+
depthMats.set(key, m);
|
|
174
|
+
}
|
|
175
|
+
return m;
|
|
176
|
+
}
|
|
177
|
+
const shadowKept = [];
|
|
178
|
+
const shadowKeys = new Set();
|
|
179
|
+
/**
|
|
180
|
+
* The shadow pass draws a shadow caster with three's own depth material — a program
|
|
181
|
+
* compileAsync never builds, so a new kind of caster (an instanced helper) compiled it
|
|
182
|
+
* in the first frame that drew its shadow. Build it here: a stand-in of the same kind
|
|
183
|
+
* with an equivalent depth material, compiled into a render target as the shadow map is.
|
|
184
|
+
* The stand-ins are kept (never drawn) so the programs stay cached.
|
|
185
|
+
*/
|
|
186
|
+
export function warmShadowPass(root) {
|
|
187
|
+
try {
|
|
188
|
+
return shadowPass(root);
|
|
189
|
+
}
|
|
190
|
+
catch (e) {
|
|
191
|
+
console.warn('[3drise] shadow warm-up failed', e);
|
|
192
|
+
return Promise.resolve();
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
function shadowPass(root) {
|
|
196
|
+
const ctx = warmContext();
|
|
197
|
+
if (!ctx || !ctx.gl.shadowMap?.enabled || ctx.gl.shadowMap.type === VSMShadowMap)
|
|
198
|
+
return Promise.resolve();
|
|
199
|
+
let dirOrSpot = false, point = false;
|
|
200
|
+
ctx.scene.traverseVisible((o) => {
|
|
201
|
+
if (!o.isLight || !o.castShadow)
|
|
202
|
+
return;
|
|
203
|
+
if (o.isPointLight)
|
|
204
|
+
point = true;
|
|
205
|
+
else
|
|
206
|
+
dirOrSpot = true;
|
|
207
|
+
});
|
|
208
|
+
if (!dirOrSpot && !point)
|
|
209
|
+
return Promise.resolve();
|
|
210
|
+
const stands = [];
|
|
211
|
+
root.traverse((c) => {
|
|
212
|
+
if (!c.castShadow || !(c.isMesh || c.isPoints || c.isLine))
|
|
213
|
+
return;
|
|
214
|
+
const own = materialsOf1(c)[0];
|
|
215
|
+
const side = own?.shadowSide ?? SHADOW_SIDE[own?.side ?? 0] ?? 1;
|
|
216
|
+
for (const [use, kind] of [[dirOrSpot, 'depth'], [point, 'distance']]) {
|
|
217
|
+
if (!use)
|
|
218
|
+
continue;
|
|
219
|
+
const mat = depthFor(kind, side);
|
|
220
|
+
const key = `${kind}|${side}|${c.isInstancedMesh ? `inst${c.instanceColor ? 'C' : ''}` : c.isSkinnedMesh ? 'skin' : c.type}|${Object.keys(c.geometry?.morphAttributes ?? {}).join(',')}`;
|
|
221
|
+
if (shadowKeys.has(key))
|
|
222
|
+
continue;
|
|
223
|
+
shadowKeys.add(key);
|
|
224
|
+
let s;
|
|
225
|
+
if (c.isInstancedMesh) {
|
|
226
|
+
s = new InstancedMesh(c.geometry, mat, 1);
|
|
227
|
+
if (c.instanceColor)
|
|
228
|
+
s.setColorAt(0, new Color(1, 1, 1));
|
|
229
|
+
}
|
|
230
|
+
else if (c.isMesh)
|
|
231
|
+
s = new Mesh(c.geometry, mat);
|
|
232
|
+
else
|
|
233
|
+
continue;
|
|
234
|
+
s.frustumCulled = false;
|
|
235
|
+
stands.push(s);
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
if (!stands.length)
|
|
239
|
+
return Promise.resolve();
|
|
240
|
+
shadowTarget ??= new WebGLRenderTarget(1, 1);
|
|
241
|
+
const prev = ctx.gl.getRenderTarget();
|
|
242
|
+
let res;
|
|
243
|
+
ctx.gl.setRenderTarget(shadowTarget);
|
|
244
|
+
try {
|
|
245
|
+
res = Promise.all(stands.map(s => Promise.resolve(ctx.gl.compileAsync(s, ctx.cam, ctx.scene)).catch(() => undefined)));
|
|
246
|
+
}
|
|
247
|
+
finally {
|
|
248
|
+
ctx.gl.setRenderTarget(prev);
|
|
249
|
+
}
|
|
250
|
+
shadowKept.push(...stands);
|
|
251
|
+
return Promise.race([res, new Promise(r => setTimeout(r, 8000))]).then(() => undefined);
|
|
252
|
+
}
|
|
68
253
|
/**
|
|
69
254
|
* Warm-only runs (pre-warming): while this is on, an effect's runProgress compiles
|
|
70
255
|
* everything it needs and then stops — nothing animates. See runtime.prewarmEffect.
|
|
@@ -84,7 +269,7 @@ export function retainPrograms(root) {
|
|
|
84
269
|
root.traverse((c) => {
|
|
85
270
|
const mats = c.material ? (Array.isArray(c.material) ? c.material : [c.material]) : [];
|
|
86
271
|
for (const m of mats) {
|
|
87
|
-
const key = `${c.type}|${m.type}|${m.vertexShader?.length ?? ''}|${m.fragmentShader?.length ?? ''}|${m.customProgramCacheKey?.() ?? ''}`;
|
|
272
|
+
const key = `${c.type}${c.instanceColor ? '+color' : ''}|${m.type}|${m.vertexShader?.length ?? ''}|${m.fragmentShader?.length ?? ''}|${m.customProgramCacheKey?.() ?? ''}`;
|
|
88
273
|
if (retainedKeys.has(key))
|
|
89
274
|
continue;
|
|
90
275
|
retainedKeys.add(key);
|
|
@@ -96,8 +281,11 @@ export function retainPrograms(root) {
|
|
|
96
281
|
: c.isLineSegments ? new LineSegments(c.geometry, keep) : c.isMesh ? new Mesh(c.geometry, keep) : null;
|
|
97
282
|
if (!stand)
|
|
98
283
|
continue;
|
|
284
|
+
// instance colours are part of the program: without them the kept program is another one
|
|
285
|
+
if (c.isInstancedMesh && c.instanceColor)
|
|
286
|
+
stand.setColorAt(0, new Color(1, 1, 1));
|
|
99
287
|
try {
|
|
100
|
-
ctx.gl.compile(stand, ctx.cam, ctx.scene);
|
|
288
|
+
asInFrame(ctx, () => ctx.gl.compile(stand, ctx.cam, ctx.scene));
|
|
101
289
|
retained.push(keep);
|
|
102
290
|
}
|
|
103
291
|
catch { /* ignore */ }
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export * from './types';
|
|
2
2
|
export { OPERATIONS, getOperation, operationsFor, resolveParams, channelOf } from './registry';
|
|
3
3
|
export { OPERATION_GROUPS, groupOperations, type OperationGroup } from './groups';
|
|
4
|
+
export { bakeProject, collectBakeJobs, setBakeSource, type BakeSource, type BakeReport } from './bake';
|
|
5
|
+
export { installStallLog, stallLogEnabled } from '../utils/stallLog';
|
|
4
6
|
export { runEffect, runEffectClip, setEffectClipTemplates, getEffectClipTemplate, effectClipFromAsset, EFFECT_CLIP_ASSET, applyOperation, revertOperation, runOf, isOperationOn, resolveEffectTargets, runActionEffects, previewEffect, previewEffects, isPreviewing, prewarmEffect, areActionEffectsOn, subscribeActionEffects, registerOperationTools, } from './runtime';
|
|
5
7
|
export { useOperationsRuntime } from './useOperationsRuntime';
|
|
6
8
|
export { ObjectEffectsController, runningObjectEffects, objectEffectParams, isObjectOperation, surfaceSignature, playObjectEffect, playbackOf, clipFollowsVisibility, setObjectEffectsVisible, currentEffectStep } from './objectEffects';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/operations/index.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAC;AACxB,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,aAAa,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAC/F,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,KAAK,cAAc,EAAE,MAAM,UAAU,CAAC;AAClF,OAAO,EACH,SAAS,EAAE,aAAa,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,cAAc,EAAE,eAAe,EAAE,KAAK,EAAE,aAAa,EAAE,oBAAoB,EAC5L,gBAAgB,EAAE,aAAa,EAAE,cAAc,EAAE,YAAY,EAAE,aAAa,EAAE,kBAAkB,EAAE,sBAAsB,EAAE,sBAAsB,GACnJ,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAC9D,OAAO,EAAE,uBAAuB,EAAE,oBAAoB,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,UAAU,EAAE,qBAAqB,EAAE,uBAAuB,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAC1O,OAAO,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AAChE,OAAO,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAClE,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/operations/index.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAC;AACxB,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,aAAa,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAC/F,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,KAAK,cAAc,EAAE,MAAM,UAAU,CAAC;AAClF,OAAO,EAAE,WAAW,EAAE,eAAe,EAAE,aAAa,EAAE,KAAK,UAAU,EAAE,KAAK,UAAU,EAAE,MAAM,QAAQ,CAAC;AACvG,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACrE,OAAO,EACH,SAAS,EAAE,aAAa,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,cAAc,EAAE,eAAe,EAAE,KAAK,EAAE,aAAa,EAAE,oBAAoB,EAC5L,gBAAgB,EAAE,aAAa,EAAE,cAAc,EAAE,YAAY,EAAE,aAAa,EAAE,kBAAkB,EAAE,sBAAsB,EAAE,sBAAsB,GACnJ,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAC9D,OAAO,EAAE,uBAAuB,EAAE,oBAAoB,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,UAAU,EAAE,qBAAqB,EAAE,uBAAuB,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAC1O,OAAO,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AAChE,OAAO,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAClE,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC"}
|
package/dist/operations/index.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export * from './types.js';
|
|
2
2
|
export { OPERATIONS, getOperation, operationsFor, resolveParams, channelOf } from './registry.js';
|
|
3
3
|
export { OPERATION_GROUPS, groupOperations } from './groups.js';
|
|
4
|
+
export { bakeProject, collectBakeJobs, setBakeSource } from './bake.js';
|
|
5
|
+
export { installStallLog, stallLogEnabled } from '../utils/stallLog.js';
|
|
4
6
|
export { runEffect, runEffectClip, setEffectClipTemplates, getEffectClipTemplate, effectClipFromAsset, EFFECT_CLIP_ASSET, applyOperation, revertOperation, runOf, isOperationOn, resolveEffectTargets, runActionEffects, previewEffect, previewEffects, isPreviewing, prewarmEffect, areActionEffectsOn, subscribeActionEffects, registerOperationTools, } from './runtime.js';
|
|
5
7
|
export { useOperationsRuntime } from './useOperationsRuntime.js';
|
|
6
8
|
export { ObjectEffectsController, runningObjectEffects, objectEffectParams, isObjectOperation, surfaceSignature, playObjectEffect, playbackOf, clipFollowsVisibility, setObjectEffectsVisible, currentEffectStep } from './objectEffects.js';
|
|
@@ -88,19 +88,26 @@ export declare function previewEffect(effect: OperationEffect, self?: Object3D |
|
|
|
88
88
|
/** Preview several effects together (an interaction's "Try it"). */
|
|
89
89
|
export declare function previewEffects(effects: OperationEffect[], self?: Object3D | null): Promise<boolean>;
|
|
90
90
|
export declare const isPreviewing: (effectId: string) => boolean;
|
|
91
|
+
/** Whether a pre-warm can do anything for an operation (visual, undoable, with shaders). */
|
|
92
|
+
export declare const isWarmable: (def: OperationDefinition | undefined) => def is OperationDefinition;
|
|
91
93
|
/**
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
94
|
+
* Warm an operation on these targets: build and compile everything its first real play
|
|
95
|
+
* needs — shaders (in the background), and the CPU work it caches (surface samples,
|
|
96
|
+
* geometry) — showing nothing, then take it off again. Queued, one at a time; each
|
|
97
|
+
* (operation, target, its meshes, parameters) once. Targets with no meshes yet (a model
|
|
98
|
+
* still loading) are skipped and warmed when it has landed.
|
|
97
99
|
*/
|
|
98
|
-
export declare function
|
|
100
|
+
export declare function warmEffectOn(def: OperationDefinition, targets: Object3D[], params: OperationParams, self?: Object3D | null): Promise<void>;
|
|
99
101
|
/**
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
102
|
+
* Compile an effect's shaders ahead of time, without showing anything, so its
|
|
103
|
+
* first real play starts instantly (see warmEffectOn). Used when an effect is picked
|
|
104
|
+
* in an editor; the project bake (bake.ts) warms everything a scene can play.
|
|
103
105
|
*/
|
|
106
|
+
export declare function prewarmEffect(effect: OperationEffect, self?: Object3D | null): Promise<void>;
|
|
107
|
+
/** Main-thread work an effect did, for the stall log (utils/stallLog). Cheap when it is off. */
|
|
108
|
+
type WorkListener = (label: string, ms: number) => void;
|
|
109
|
+
export declare const setWorkListener: (fn: WorkListener | null) => void;
|
|
110
|
+
export declare function noteWork(label: string, ms: number): void;
|
|
104
111
|
export declare function warmOperation(def: OperationDefinition, target: Object3D, params: OperationParams, self?: Object3D | null, opts?: {
|
|
105
112
|
bare?: boolean;
|
|
106
113
|
}): Promise<void>;
|
|
@@ -112,4 +119,5 @@ export declare function runActionEffects(action: ActionSequence, mode: 'apply' |
|
|
|
112
119
|
export declare const areActionEffectsOn: (actionId: string) => boolean;
|
|
113
120
|
/** Subscribe to action-effects on/off changes (studio badges). */
|
|
114
121
|
export declare function subscribeActionEffects(fn: () => void): () => void;
|
|
122
|
+
export {};
|
|
115
123
|
//# sourceMappingURL=runtime.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../../src/operations/runtime.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAEtC,OAAO,KAAK,EAAE,cAAc,EAAE,UAAU,EAAE,aAAa,EAAE,eAAe,EAAE,aAAa,EAAgB,MAAM,uBAAuB,CAAC;AAIrI,OAAO,KAAK,EAAoB,mBAAmB,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAGtG,OAAO,2BAA2B,CAAC;AAwCnC,kEAAkE;AAClE,wBAAgB,sBAAsB,CAAC,CAAC,EAAE,cAAc,GAAG,MAAM,IAAI,CAQpE;AAED,wBAAgB,aAAa,CAAC,EAAE,EAAE,aAAa,EAAE,MAAM,EAAE,QAAQ,GAAG,OAAO,CAE1E;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,mBAAmB,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,eAAe,EAAE,IAAI,CAAC,EAAE,QAAQ,GAAG,IAAI,EAAE,IAAI,CAAC,EAAE;IAAE,IAAI,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,OAAO,
|
|
1
|
+
{"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../../src/operations/runtime.ts"],"names":[],"mappings":"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"}
|
|
@@ -75,7 +75,10 @@ export function applyOperation(def, target, params, self, opts) {
|
|
|
75
75
|
}
|
|
76
76
|
slots.set(def.exclusive.group, key);
|
|
77
77
|
}
|
|
78
|
-
|
|
78
|
+
const t0 = performance.now();
|
|
79
|
+
const res = def.apply(target, params, ctx);
|
|
80
|
+
noteWork(def.value, performance.now() - t0);
|
|
81
|
+
runs.set(key, finished(res, params));
|
|
79
82
|
if (def.revert)
|
|
80
83
|
active.set(key, { op: def.value, target, params });
|
|
81
84
|
return true;
|
|
@@ -348,30 +351,69 @@ export const isPreviewing = (effectId) => previews.has(effectId);
|
|
|
348
351
|
// ---------------------------------------------------------------- pre-warm
|
|
349
352
|
/** Visual operations with no shaders of their own — nothing to warm (and a warm-only run would show). */
|
|
350
353
|
const NO_SHADERS = new Set(['glow', 'fade', 'visibility', 'displayImage']);
|
|
354
|
+
/** Whether a pre-warm can do anything for an operation (visual, undoable, with shaders). */
|
|
355
|
+
export const isWarmable = (def) => !!def && def.channel === 'visual' && !!def.revert && !NO_SHADERS.has(def.value);
|
|
351
356
|
const warmed = new Set();
|
|
352
357
|
let warmQueue = Promise.resolve();
|
|
353
358
|
/**
|
|
354
|
-
*
|
|
355
|
-
*
|
|
356
|
-
* shaders). Runs one at a time, in the background; each (effect, target) once.
|
|
357
|
-
* Used when an effect is picked in the editor and, for every effect a scene's
|
|
358
|
-
* interactions and actions use, when the scene loads.
|
|
359
|
+
* What a warm-up covered on a target: its meshes as they are now. A model that lands
|
|
360
|
+
* after the warm-up (its object's group exists first) has other meshes — it is warmed again.
|
|
359
361
|
*/
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
362
|
+
function meshSignature(t) {
|
|
363
|
+
let count = 0, h = 0;
|
|
364
|
+
t.traverse((c) => {
|
|
365
|
+
if (!c.isMesh || c.userData?.__opFxHelper)
|
|
366
|
+
return;
|
|
367
|
+
count++;
|
|
368
|
+
for (let i = 0; i < c.uuid.length; i++)
|
|
369
|
+
h = (h * 31 + c.uuid.charCodeAt(i)) | 0;
|
|
370
|
+
});
|
|
371
|
+
return { count, sig: `${count}:${h}` };
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* The objects a warm-up runs on for one target: the target itself — or, for a group, its
|
|
375
|
+
* member objects (a group never draws an effect itself: groupFanout hands them out).
|
|
376
|
+
*/
|
|
377
|
+
function warmRoots(t) {
|
|
378
|
+
if (t.userData?.objectType !== 'group')
|
|
379
|
+
return [t];
|
|
380
|
+
const out = [];
|
|
381
|
+
t.traverse((c) => {
|
|
382
|
+
if (c === t || !c.userData?.id || !c.userData.objectType || c.userData.objectType === 'group')
|
|
383
|
+
return;
|
|
384
|
+
if (c.userData.visualFx === false)
|
|
385
|
+
return;
|
|
386
|
+
out.push(c);
|
|
387
|
+
});
|
|
388
|
+
return out;
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Warm an operation on these targets: build and compile everything its first real play
|
|
392
|
+
* needs — shaders (in the background), and the CPU work it caches (surface samples,
|
|
393
|
+
* geometry) — showing nothing, then take it off again. Queued, one at a time; each
|
|
394
|
+
* (operation, target, its meshes, parameters) once. Targets with no meshes yet (a model
|
|
395
|
+
* still loading) are skipped and warmed when it has landed.
|
|
396
|
+
*/
|
|
397
|
+
export function warmEffectOn(def, targets, params, self) {
|
|
398
|
+
if (!isWarmable(def))
|
|
363
399
|
return Promise.resolve();
|
|
364
|
-
const
|
|
400
|
+
const paramsKey = JSON.stringify(params);
|
|
365
401
|
const job = async () => {
|
|
366
|
-
const ctx = context(self);
|
|
402
|
+
const ctx = context(self, true);
|
|
367
403
|
if (!ctx)
|
|
368
404
|
return;
|
|
369
|
-
const
|
|
370
|
-
for (const t of def.multi ?
|
|
371
|
-
|
|
405
|
+
const roots = targets.flatMap(warmRoots);
|
|
406
|
+
for (const t of def.multi ? roots : roots.slice(0, 1)) {
|
|
407
|
+
if (!t.parent)
|
|
408
|
+
continue; // left the scene meanwhile
|
|
409
|
+
const { count, sig } = meshSignature(t);
|
|
410
|
+
if (!count)
|
|
411
|
+
continue; // nothing to warm yet (a model still loading)
|
|
412
|
+
const k = `${def.value}:${t.uuid}:${sig}:${paramsKey}`;
|
|
372
413
|
if (warmed.has(k) || isOperationOn(def.value, t))
|
|
373
414
|
continue;
|
|
374
415
|
warmed.add(k);
|
|
416
|
+
const t0 = performance.now();
|
|
375
417
|
// a warm-only run: stages and compiles everything, animates nothing…
|
|
376
418
|
warmMode.on = true;
|
|
377
419
|
let res;
|
|
@@ -381,8 +423,9 @@ export function prewarmEffect(effect, self) {
|
|
|
381
423
|
finally {
|
|
382
424
|
warmMode.on = false;
|
|
383
425
|
}
|
|
426
|
+
noteWork(`warm ${def.value}`, performance.now() - t0);
|
|
384
427
|
await finished(res, { duration: 0 });
|
|
385
|
-
// …then quietly take it off again, unless
|
|
428
|
+
// …then quietly take it off again, unless something started it meanwhile
|
|
386
429
|
if (!isOperationOn(def.value, t))
|
|
387
430
|
await finished(def.revert(t, { ...params, duration: 0.001 }, ctx), { duration: 0 });
|
|
388
431
|
}
|
|
@@ -391,10 +434,20 @@ export function prewarmEffect(effect, self) {
|
|
|
391
434
|
return warmQueue;
|
|
392
435
|
}
|
|
393
436
|
/**
|
|
394
|
-
*
|
|
395
|
-
*
|
|
396
|
-
*
|
|
437
|
+
* Compile an effect's shaders ahead of time, without showing anything, so its
|
|
438
|
+
* first real play starts instantly (see warmEffectOn). Used when an effect is picked
|
|
439
|
+
* in an editor; the project bake (bake.ts) warms everything a scene can play.
|
|
397
440
|
*/
|
|
441
|
+
export function prewarmEffect(effect, self) {
|
|
442
|
+
const def = getOperation(effect.operation);
|
|
443
|
+
if (!isWarmable(def))
|
|
444
|
+
return Promise.resolve();
|
|
445
|
+
return warmEffectOn(def, resolveEffectTargets(effect, self), resolveParams(def, effect.params), self);
|
|
446
|
+
}
|
|
447
|
+
let workListener = null;
|
|
448
|
+
export const setWorkListener = (fn) => { workListener = fn; };
|
|
449
|
+
export function noteWork(label, ms) { if (workListener && ms >= 4)
|
|
450
|
+
workListener(label, ms); }
|
|
398
451
|
export function warmOperation(def, target, params, self, opts) {
|
|
399
452
|
if (def.channel !== 'visual' || !def.revert || NO_SHADERS.has(def.value))
|
|
400
453
|
return Promise.resolve();
|
|
@@ -6,6 +6,7 @@ declare class SceneReadiness {
|
|
|
6
6
|
private loaded;
|
|
7
7
|
private failed;
|
|
8
8
|
private compile;
|
|
9
|
+
private bake;
|
|
9
10
|
private waiters;
|
|
10
11
|
private listeners;
|
|
11
12
|
private timer;
|
|
@@ -18,6 +19,7 @@ declare class SceneReadiness {
|
|
|
18
19
|
/** A published scene starts: wait for these models, then compile, then one frame. */
|
|
19
20
|
begin(modelIds: string[], opts?: {
|
|
20
21
|
compile?: () => Promise<unknown>;
|
|
22
|
+
bake?: () => Promise<unknown>;
|
|
21
23
|
}): void;
|
|
22
24
|
/** A model's Object3D is in the scene. */
|
|
23
25
|
report(id: string): void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"readiness.d.ts","sourceRoot":"","sources":["../../src/triggers/readiness.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"readiness.d.ts","sourceRoot":"","sources":["../../src/triggers/readiness.ts"],"names":[],"mappings":"AAyBA,MAAM,MAAM,oBAAoB,GAAG,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC;AAOhE,cAAM,cAAc;IAChB,OAAO,CAAC,MAAM,CAAgC;IAC9C,OAAO,CAAC,QAAQ,CAAqB;IACrC,oFAAoF;IACpF,OAAO,CAAC,MAAM,CAAqB;IACnC,OAAO,CAAC,MAAM,CAAqB;IACnC,OAAO,CAAC,OAAO,CAAyC;IACxD,OAAO,CAAC,IAAI,CAAyC;IACrD,OAAO,CAAC,OAAO,CAAyB;IACxC,OAAO,CAAC,SAAS,CAAyB;IAC1C,OAAO,CAAC,KAAK,CAA8C;IAC3D,OAAO,CAAC,SAAS,CAAK;IACtB,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,UAAU,CAAK;IAEvB,SAAS,IAAI,oBAAoB;IACjC,kEAAkE;IAClE,OAAO,IAAI,OAAO;IAElB,qFAAqF;IACrF,KAAK,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE,IAAI,GAAE;QAAE,OAAO,CAAC,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAA;KAAO,GAAG,IAAI;IAoB/G,0CAA0C;IAC1C,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI;IAMxB,wEAAwE;IACxE,IAAI,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,GAAG,IAAI;IAOxC,wEAAwE;IACxE,SAAS,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI;IAMrC,SAAS,GAAI,UAAU,MAAM,IAAI,KAAG,CAAC,MAAM,IAAI,CAAC,CAG9C;IAEF,6DAA6D;IAC7D,KAAK,IAAI,IAAI;IAcb,OAAO,CAAC,KAAK;YAMC,MAAM;IAwCpB,OAAO,CAAC,UAAU;IAKlB,OAAO,CAAC,IAAI;CAGf;AAED,eAAO,MAAM,cAAc,gBAAuB,CAAC;AAEnD,kGAAkG;AAClG,wBAAgB,aAAa,IAAI,OAAO,CAEvC"}
|
|
@@ -4,8 +4,11 @@
|
|
|
4
4
|
//
|
|
5
5
|
// A published scene (SceneBuilder) is READY when:
|
|
6
6
|
// 1. every model it holds has loaded (ModelPrimitive reports it) or failed (ModelBoundary), and
|
|
7
|
-
// 2. the scene's shaders are compiled (renderer.compileAsync
|
|
8
|
-
//
|
|
7
|
+
// 2. the scene's shaders are compiled (renderer.compileAsync, as the frame draws it) and its
|
|
8
|
+
// textures uploaded,
|
|
9
|
+
// 3. the project is baked — every effect it can play compiled and prepared (operations/bake),
|
|
10
|
+
// so no first play stalls — and
|
|
11
|
+
// 4. one frame has rendered.
|
|
9
12
|
// Only then do autoplay sequences start, object effects start, onSceneReady fires and the
|
|
10
13
|
// loading cover lifts. A model that never reports doesn't hold the scene forever: after
|
|
11
14
|
// TIMEOUT_MS it goes ready anyway and the missing ids are logged.
|
|
@@ -21,6 +24,7 @@
|
|
|
21
24
|
import { useSyncExternalStore } from 'react';
|
|
22
25
|
const TIMEOUT_MS = 15000;
|
|
23
26
|
const COMPILE_TIMEOUT_MS = 5000;
|
|
27
|
+
const BAKE_TIMEOUT_MS = 20000;
|
|
24
28
|
const log = (...a) => console.info('[3drise] scene', ...a);
|
|
25
29
|
class SceneReadiness {
|
|
26
30
|
status = 'idle';
|
|
@@ -29,6 +33,7 @@ class SceneReadiness {
|
|
|
29
33
|
loaded = new Set();
|
|
30
34
|
failed = new Set();
|
|
31
35
|
compile = null;
|
|
36
|
+
bake = null;
|
|
32
37
|
waiters = [];
|
|
33
38
|
listeners = new Set();
|
|
34
39
|
timer = null;
|
|
@@ -46,6 +51,7 @@ class SceneReadiness {
|
|
|
46
51
|
this.finishing = false;
|
|
47
52
|
this.expected = new Set(modelIds.filter(Boolean));
|
|
48
53
|
this.compile = opts.compile ?? null;
|
|
54
|
+
this.bake = opts.bake ?? null;
|
|
49
55
|
this.startedAt = performance.now();
|
|
50
56
|
this.emit();
|
|
51
57
|
const gen = this.generation;
|
|
@@ -96,6 +102,7 @@ class SceneReadiness {
|
|
|
96
102
|
this.loaded.clear();
|
|
97
103
|
this.failed.clear();
|
|
98
104
|
this.compile = null;
|
|
105
|
+
this.bake = null;
|
|
99
106
|
this.waiters = [];
|
|
100
107
|
this.emit();
|
|
101
108
|
}
|
|
@@ -122,13 +129,31 @@ class SceneReadiness {
|
|
|
122
129
|
console.warn('[3drise] scene: shader compile failed, going on', e);
|
|
123
130
|
}
|
|
124
131
|
}
|
|
132
|
+
const bakeAt = performance.now();
|
|
133
|
+
if (this.bake && gen === this.generation) {
|
|
134
|
+
let timer;
|
|
135
|
+
try {
|
|
136
|
+
const timedOut = await Promise.race([
|
|
137
|
+
this.bake().then(() => false),
|
|
138
|
+
new Promise(r => { timer = setTimeout(() => r(true), BAKE_TIMEOUT_MS); }),
|
|
139
|
+
]);
|
|
140
|
+
if (timedOut)
|
|
141
|
+
console.warn(`[3drise] scene: baking effects took over ${BAKE_TIMEOUT_MS / 1000}s — going ready, the rest bakes on`);
|
|
142
|
+
}
|
|
143
|
+
catch (e) {
|
|
144
|
+
console.warn('[3drise] scene: baking effects failed, going on', e);
|
|
145
|
+
}
|
|
146
|
+
finally {
|
|
147
|
+
clearTimeout(timer);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
125
150
|
// one rendered frame, so what is shown is what runs
|
|
126
151
|
await new Promise(r => requestAnimationFrame(() => requestAnimationFrame(() => r())));
|
|
127
152
|
if (gen !== this.generation)
|
|
128
153
|
return; // reset / begun again meanwhile
|
|
129
154
|
this.status = 'ready';
|
|
130
155
|
const t = (n) => `${Math.round(n - this.startedAt)}ms`;
|
|
131
|
-
log(`ready in ${t(performance.now())} (models ${t(modelsAt)}, ${[...this.expected].filter(id => this.loaded.has(id)).length}/${this.expected.size} loaded${this.failed.size ? `, ${this.failed.size} failed` : ''})`);
|
|
156
|
+
log(`ready in ${t(performance.now())} (models ${t(modelsAt)}, compile ${Math.round(bakeAt - modelsAt)}ms, bake ${Math.round(performance.now() - bakeAt)}ms, ${[...this.expected].filter(id => this.loaded.has(id)).length}/${this.expected.size} loaded${this.failed.size ? `, ${this.failed.size} failed` : ''})`);
|
|
132
157
|
this.emit();
|
|
133
158
|
const run = this.waiters;
|
|
134
159
|
this.waiters = [];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stallLog.d.ts","sourceRoot":"","sources":["../../src/utils/stallLog.ts"],"names":[],"mappings":"AAuBA,wBAAgB,eAAe,IAAI,OAAO,CAEzC;AAED,4EAA4E;AAC5E,wBAAgB,eAAe,CAAC,QAAQ,EAAE,GAAG,GAAG,IAAI,CAgDnD"}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// Stall log (dev) — which frames froze, and why.
|
|
3
|
+
// =============================================================================
|
|
4
|
+
//
|
|
5
|
+
// Switch it on in the browser console: localStorage.setItem('3drise:stalls', '1') (reload)
|
|
6
|
+
// Off: localStorage.removeItem('3drise:stalls')
|
|
7
|
+
//
|
|
8
|
+
// Every frame slower than STALL_MS is logged with what happened in it:
|
|
9
|
+
// compiles shader programs linked INSIDE the frame (three compiling on the spot —
|
|
10
|
+
// what the warm-ups exist to prevent; the material names say whose)
|
|
11
|
+
// uploads textures uploaded inside the frame (an object drawn for the first time)
|
|
12
|
+
// work effect applies / warm-ups that ran on the main thread since the last
|
|
13
|
+
// frame, with their milliseconds (runtime.noteWork)
|
|
14
|
+
// Background compiles (warm-ups) are counted too, so a stall with none of the above is
|
|
15
|
+
// the GPU / driver itself.
|
|
16
|
+
//
|
|
17
|
+
import { setWorkListener } from '../operations/runtime.js';
|
|
18
|
+
const STALL_MS = 50;
|
|
19
|
+
const FLAG = '3drise:stalls';
|
|
20
|
+
let installed = false;
|
|
21
|
+
export function stallLogEnabled() {
|
|
22
|
+
try {
|
|
23
|
+
return typeof window !== 'undefined' && window.localStorage?.getItem(FLAG) === '1';
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/** Install on the scene's renderer, when the dev flag is on. Idempotent. */
|
|
30
|
+
export function installStallLog(renderer) {
|
|
31
|
+
if (installed || !renderer || !stallLogEnabled())
|
|
32
|
+
return;
|
|
33
|
+
const gl = renderer.getContext?.();
|
|
34
|
+
if (!gl)
|
|
35
|
+
return;
|
|
36
|
+
installed = true;
|
|
37
|
+
let inFrame = false;
|
|
38
|
+
let frame = { compiles: 0, uploads: 0, bgCompiles: 0, work: [], programs: new Set() };
|
|
39
|
+
const reset = () => { frame = { compiles: 0, uploads: 0, bgCompiles: 0, work: [], programs: new Set() }; };
|
|
40
|
+
const link = gl.linkProgram.bind(gl);
|
|
41
|
+
gl.linkProgram = (p) => { if (inFrame)
|
|
42
|
+
frame.compiles++;
|
|
43
|
+
else
|
|
44
|
+
frame.bgCompiles++; return link(p); };
|
|
45
|
+
for (const fn of ['texImage2D', 'texSubImage2D', 'texStorage2D', 'compressedTexImage2D']) {
|
|
46
|
+
const o = gl[fn]?.bind(gl);
|
|
47
|
+
if (o)
|
|
48
|
+
gl[fn] = (...a) => { if (inFrame)
|
|
49
|
+
frame.uploads++; return o(...a); };
|
|
50
|
+
}
|
|
51
|
+
// whose programs: three names a program after its material (name / type) when it builds it
|
|
52
|
+
const render = renderer.render.bind(renderer);
|
|
53
|
+
renderer.render = (scene, camera) => {
|
|
54
|
+
const outer = !inFrame;
|
|
55
|
+
inFrame = true;
|
|
56
|
+
const before = renderer.info?.programs?.length ?? 0;
|
|
57
|
+
try {
|
|
58
|
+
return render(scene, camera);
|
|
59
|
+
}
|
|
60
|
+
finally {
|
|
61
|
+
const after = renderer.info?.programs ?? [];
|
|
62
|
+
for (let i = before; i < after.length; i++)
|
|
63
|
+
frame.programs.add(String(after[i]?.name ?? '?'));
|
|
64
|
+
if (outer)
|
|
65
|
+
inFrame = false;
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
setWorkListener((label, ms) => { frame.work.push(`${label} ${Math.round(ms)}ms`); });
|
|
69
|
+
let last = performance.now();
|
|
70
|
+
const tick = () => {
|
|
71
|
+
const now = performance.now();
|
|
72
|
+
const dt = now - last;
|
|
73
|
+
last = now;
|
|
74
|
+
if (dt > STALL_MS) {
|
|
75
|
+
const parts = [
|
|
76
|
+
frame.compiles ? `${frame.compiles} compile(s) in the frame${frame.programs.size ? ` [${[...frame.programs].join(', ')}]` : ''}` : '',
|
|
77
|
+
frame.uploads ? `${frame.uploads} texture upload(s) in the frame` : '',
|
|
78
|
+
frame.work.length ? `work: ${frame.work.join(', ')}` : '',
|
|
79
|
+
frame.bgCompiles ? `${frame.bgCompiles} background compile(s)` : '',
|
|
80
|
+
].filter(Boolean);
|
|
81
|
+
console.warn(`[3drise] stall ${Math.round(dt)}ms — ${parts.length ? parts.join(' · ') : 'nothing recorded (GPU / driver, GC, or other main-thread work)'}`);
|
|
82
|
+
}
|
|
83
|
+
reset();
|
|
84
|
+
requestAnimationFrame(tick);
|
|
85
|
+
};
|
|
86
|
+
requestAnimationFrame(tick);
|
|
87
|
+
console.info('[3drise] stall log on (frames over %dms) — localStorage.removeItem(\'3drise:stalls\') to switch it off', STALL_MS);
|
|
88
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@3driseai/3drise-viewer",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.33",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -39,6 +39,6 @@
|
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
41
|
"@3driseai/3drise-common": "^1.0.1",
|
|
42
|
-
"@3driseai/3drise-core": "^1.0.
|
|
42
|
+
"@3driseai/3drise-core": "^1.0.25"
|
|
43
43
|
}
|
|
44
44
|
}
|