@3driseai/3drise-viewer 0.1.31 → 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.
Files changed (34) hide show
  1. package/dist/components/CameraController.d.ts.map +1 -1
  2. package/dist/components/CameraController.js +9 -0
  3. package/dist/components/ObjectsRenderer.d.ts.map +1 -1
  4. package/dist/components/ObjectsRenderer.js +26 -19
  5. package/dist/components/SceneBuilder.d.ts.map +1 -1
  6. package/dist/components/SceneBuilder.js +7 -4
  7. package/dist/components/land/LandPatch.d.ts.map +1 -1
  8. package/dist/components/land/LandPatch.js +15 -1
  9. package/dist/constants.d.ts +1 -1
  10. package/dist/constants.d.ts.map +1 -1
  11. package/dist/constants.js +1 -1
  12. package/dist/hooks/useContinuousEffects.js +1 -1
  13. package/dist/hooks/useOrbitTargetFollow.d.ts.map +1 -1
  14. package/dist/hooks/useOrbitTargetFollow.js +6 -4
  15. package/dist/operations/bake.d.ts +36 -0
  16. package/dist/operations/bake.d.ts.map +1 -0
  17. package/dist/operations/bake.js +113 -0
  18. package/dist/operations/catalogue/shaderLayers.js +2 -2
  19. package/dist/operations/catalogue/warmup.d.ts +23 -1
  20. package/dist/operations/catalogue/warmup.d.ts.map +1 -1
  21. package/dist/operations/catalogue/warmup.js +200 -12
  22. package/dist/operations/index.d.ts +2 -0
  23. package/dist/operations/index.d.ts.map +1 -1
  24. package/dist/operations/index.js +2 -0
  25. package/dist/operations/runtime.d.ts +17 -9
  26. package/dist/operations/runtime.d.ts.map +1 -1
  27. package/dist/operations/runtime.js +71 -18
  28. package/dist/triggers/readiness.d.ts +2 -0
  29. package/dist/triggers/readiness.d.ts.map +1 -1
  30. package/dist/triggers/readiness.js +28 -3
  31. package/dist/utils/stallLog.d.ts +4 -0
  32. package/dist/utils/stallLog.d.ts.map +1 -0
  33. package/dist/utils/stallLog.js +88 -0
  34. 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,CA4H5D,CAAC;AACF,eAAe,gBAAgB,CAAC"}
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;AAQ9E,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,CAwFlD,CAAC;AAEH,eAAe,eAAe,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 { actionEffectsOf, commandsOf, effectsOf } from '../triggers/index.js';
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
- // Compile, in the background, the shaders of every effect this scene's interactions
76
- // and actions use, so a visitor's first click never stalls. Each (effect, object) once.
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
- const t = setTimeout(() => {
79
- for (const o of objectsRef.current ?? []) {
80
- const ref = findById(o.id);
81
- if (!ref)
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":"AAGA,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;qCA4LnB,CAAC;AAIH,eAAe,YAAY,CAAC"}
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
- compile: () => {
85
- const t = threeRef.current;
86
- return t?.gl?.compileAsync ? t.gl.compileAsync(t.scene, t.camera) : Promise.resolve();
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;AA4ChE,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,CAyIvC,CAAC;AAEF,eAAe,SAAS,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 segments = LAND_SEGMENTS[base.quality] ?? 256;
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;
@@ -1,3 +1,3 @@
1
- export declare const dataCenterServer = "http://localhost:3001/api/data/";
1
+ export declare const dataCenterServer = "https://3drise.ai/api/data/";
2
2
  export declare const SELF_MANAGED_MATERIAL: Set<string>;
3
3
  //# sourceMappingURL=constants.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAKA,eAAO,MAAM,gBAAgB,oCAA2B,CAAC;AACzD,eAAO,MAAM,qBAAqB,aAqBhC,CAAC"}
1
+ {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAKA,eAAO,MAAM,gBAAgB,gCAAgC,CAAC;AAC9D,eAAO,MAAM,qBAAqB,aAqBhC,CAAC"}
package/dist/constants.js CHANGED
@@ -3,7 +3,7 @@ const PRODUCTION_DATA_CENTER_SERVER = 'https://3drise.ai/api/data/';
3
3
  const isLocalHostRuntime = typeof window !== 'undefined' &&
4
4
  (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1');
5
5
  // export const dataCenterServer = isLocalHostRuntime ? LOCAL_DATA_CENTER_SERVER : PRODUCTION_DATA_CENTER_SERVER;
6
- export const dataCenterServer = LOCAL_DATA_CENTER_SERVER;
6
+ export const dataCenterServer = PRODUCTION_DATA_CENTER_SERVER;
7
7
  export const SELF_MANAGED_MATERIAL = new Set([
8
8
  // The landscape owns its shader: splat arrays, height blending and the
9
9
  // heightmap displacement are the material. useMaterialApplication would
@@ -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":"AA+CA,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,QAqE3B;AAED,eAAe,oBAAoB,CAAC"}
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 come from `controls`.
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 minH = typeof auth.minHeight === 'number' ? auth.minHeight : -Infinity;
177
- const maxH = typeof auth.maxHeight === 'number' ? auth.maxHeight : Infinity;
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":"AAwBA,OAAO,KAAK,EAAY,QAAQ,EAAE,MAAM,OAAO,CAAC;AAGhD,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,CAGhD;AAED,sGAAsG;AACtG,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,QAAQ,EAAE,SAAS,SAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAQnF;AAUD,iGAAiG;AACjG,wBAAgB,aAAa,CAAC,GAAG,EAAE,QAAQ,QAI1C;AAED,0FAA0F;AAC1F,wBAAgB,WAAW,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAQzD;AAED;;;GAGG;AACH,eAAO,MAAM,QAAQ;;CAAgB,CAAC;AAStC,wBAAgB,cAAc,CAAC,IAAI,EAAE,QAAQ,QAmB5C"}
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) => { if (c.material)
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"}
@@ -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
- * Compile an effect's shaders ahead of time, without showing anything, so its
93
- * first real play starts instantly. Visual effects only (the others have no
94
- * shaders). Runs one at a time, in the background; each (effect, target) once.
95
- * Used when an effect is picked in the editor and, for every effect a scene's
96
- * interactions and actions use, when the scene loads.
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 prewarmEffect(effect: OperationEffect, self?: Object3D | null): Promise<void>;
100
+ export declare function warmEffectOn(def: OperationDefinition, targets: Object3D[], params: OperationParams, self?: Object3D | null): Promise<void>;
99
101
  /**
100
- * A warm-only apply: stages and compiles what the operation needs on the target and
101
- * animates nothing. It is left staged at its start — apply it for real next, or revert it.
102
- * Used to get an effect ready while its object is still hidden (see objectEffects, Visible).
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":"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"}
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
- runs.set(key, finished(def.apply(target, params, ctx), params));
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
- * Compile an effect's shaders ahead of time, without showing anything, so its
355
- * first real play starts instantly. Visual effects only (the others have no
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
- export function prewarmEffect(effect, self) {
361
- const def = getOperation(effect.operation);
362
- if (!def || def.channel !== 'visual' || !def.revert || NO_SHADERS.has(def.value))
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 params = resolveParams(def, effect.params);
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 targets = resolveEffectTargets(effect, self);
370
- for (const t of def.multi ? targets : targets.slice(0, 1)) {
371
- const k = `${def.value}:${t.uuid}`;
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 the user started it meanwhile
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
- * A warm-only apply: stages and compiles what the operation needs on the target and
395
- * animates nothing. It is left staged at its start — apply it for real next, or revert it.
396
- * Used to get an effect ready while its object is still hidden (see objectEffects, Visible).
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":"AAsBA,MAAM,MAAM,oBAAoB,GAAG,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC;AAMhE,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,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,CAAA;KAAO,GAAG,IAAI;IAmBhF,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;IAab,OAAO,CAAC,KAAK;YAMC,MAAM;IA2BpB,OAAO,CAAC,UAAU;IAKlB,OAAO,CAAC,IAAI;CAGf;AAED,eAAO,MAAM,cAAc,gBAAuB,CAAC;AAEnD,kGAAkG;AAClG,wBAAgB,aAAa,IAAI,OAAO,CAEvC"}
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), and
8
- // 3. one frame has rendered.
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,4 @@
1
+ export declare function stallLogEnabled(): boolean;
2
+ /** Install on the scene's renderer, when the dev flag is on. Idempotent. */
3
+ export declare function installStallLog(renderer: any): void;
4
+ //# sourceMappingURL=stallLog.d.ts.map
@@ -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.31",
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.24"
42
+ "@3driseai/3drise-core": "^1.0.25"
43
43
  }
44
44
  }