@3driseai/3drise-core 1.0.9 → 1.0.10

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.
@@ -8,6 +8,7 @@ export * from './GalleryLayouts';
8
8
  export * from './gridUtils';
9
9
  export * from './idUtils';
10
10
  export * from './loadingManager';
11
+ export * from './modelCache';
11
12
  export * from './materialApplicationUtils';
12
13
  export * from './materialUtils';
13
14
  export * from './meshUtils';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,eAAe,CAAC;AAC9B,cAAc,mBAAmB,CAAC;AAClC,cAAc,sBAAsB,CAAC;AACrC,cAAc,kBAAkB,CAAC;AACjC,cAAc,kBAAkB,CAAC;AACjC,cAAc,kBAAkB,CAAC;AACjC,cAAc,aAAa,CAAC;AAC5B,cAAc,WAAW,CAAC;AAC1B,cAAc,kBAAkB,CAAC;AACjC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,iBAAiB,CAAC;AAChC,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC;AAC5B,cAAc,yBAAyB,CAAC;AACxC,cAAc,YAAY,CAAC;AAC3B,cAAc,SAAS,CAAC;AACxB,cAAc,aAAa,CAAC;AAC5B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,mBAAmB,CAAC;AAClC,cAAc,0BAA0B,CAAC;AACzC,cAAc,yBAAyB,CAAC;AACxC,cAAc,yBAAyB,CAAC;AACxC,cAAc,yBAAyB,CAAC;AACxC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,kBAAkB,CAAC;AACjC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,2BAA2B,CAAC;AAC1C,cAAc,wBAAwB,CAAC;AACvC,cAAc,wBAAwB,CAAC;AACvC,cAAc,cAAc,CAAC;AAC7B,cAAc,uBAAuB,CAAC;AACtC,cAAc,oBAAoB,CAAC;AACnC,cAAc,oBAAoB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,eAAe,CAAC;AAC9B,cAAc,mBAAmB,CAAC;AAClC,cAAc,sBAAsB,CAAC;AACrC,cAAc,kBAAkB,CAAC;AACjC,cAAc,kBAAkB,CAAC;AACjC,cAAc,kBAAkB,CAAC;AACjC,cAAc,aAAa,CAAC;AAC5B,cAAc,WAAW,CAAC;AAC1B,cAAc,kBAAkB,CAAC;AACjC,cAAc,cAAc,CAAC;AAC7B,cAAc,4BAA4B,CAAC;AAC3C,cAAc,iBAAiB,CAAC;AAChC,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC;AAC5B,cAAc,yBAAyB,CAAC;AACxC,cAAc,YAAY,CAAC;AAC3B,cAAc,SAAS,CAAC;AACxB,cAAc,aAAa,CAAC;AAC5B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,mBAAmB,CAAC;AAClC,cAAc,0BAA0B,CAAC;AACzC,cAAc,yBAAyB,CAAC;AACxC,cAAc,yBAAyB,CAAC;AACxC,cAAc,yBAAyB,CAAC;AACxC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,kBAAkB,CAAC;AACjC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,2BAA2B,CAAC;AAC1C,cAAc,wBAAwB,CAAC;AACvC,cAAc,wBAAwB,CAAC;AACvC,cAAc,cAAc,CAAC;AAC7B,cAAc,uBAAuB,CAAC;AACtC,cAAc,oBAAoB,CAAC;AACnC,cAAc,oBAAoB,CAAC"}
@@ -8,6 +8,7 @@ export * from './GalleryLayouts.js';
8
8
  export * from './gridUtils.js';
9
9
  export * from './idUtils.js';
10
10
  export * from './loadingManager.js';
11
+ export * from './modelCache.js';
11
12
  export * from './materialApplicationUtils.js';
12
13
  export * from './materialUtils.js';
13
14
  export * from './meshUtils.js';
@@ -0,0 +1,78 @@
1
+ import type { AnimationClip, Object3D } from 'three';
2
+ /**
3
+ * URL-keyed model cache.
4
+ *
5
+ * One fetch per URL for the life of the page, shared by every consumer: the project
6
+ * loaders, the object API, and any model leaf that mounts without a preload pass. The
7
+ * cached value is the *master* — it is never mounted into a scene. Consumers take a clone.
8
+ *
9
+ * Loading goes through the `gltfLoader` singleton in `loadingManager.ts`, so the Draco
10
+ * decoder configuration and the LoadingManager progress events stay in one place.
11
+ */
12
+ export interface ModelAsset {
13
+ /** The master scene. Never mount this — clone it. */
14
+ scene: Object3D;
15
+ /** Clips shipped with the GLTF, to bind against a clone's root. */
16
+ animations: AnimationClip[];
17
+ }
18
+ export interface ModelCacheEntry {
19
+ url: string;
20
+ promise: Promise<ModelAsset>;
21
+ /** Set once the load resolves. */
22
+ asset?: ModelAsset;
23
+ /** Set once the load rejects. */
24
+ error?: unknown;
25
+ }
26
+ export interface ModelInstance {
27
+ object: Object3D;
28
+ animations: AnimationClip[];
29
+ }
30
+ /**
31
+ * Start (or join) the load of a model URL. Safe to call from anywhere, any number of
32
+ * times — concurrent callers share one request and one master.
33
+ */
34
+ export declare const loadModelAsset: (url: string) => ModelCacheEntry;
35
+ /**
36
+ * Suspense-flavoured read: returns the asset if it is already here, otherwise throws the
37
+ * promise (React suspends) or the error (the nearest error boundary catches it).
38
+ *
39
+ * Call this from render, never from an effect.
40
+ */
41
+ export declare const readModelAsset: (url: string) => ModelAsset;
42
+ /** The resolved asset if the URL is already cached, otherwise null. Never suspends. */
43
+ export declare const peekModelAsset: (url: string) => ModelAsset | null;
44
+ /**
45
+ * Warm the cache. Never rejects — a model that fails to load is reported by the leaf that
46
+ * needs it, not by the preload.
47
+ */
48
+ export declare const preloadModels: (urls: string[]) => Promise<void>;
49
+ /**
50
+ * Clone a master for mounting.
51
+ *
52
+ * `SkeletonUtils.clone` rather than `Object3D.clone` so skinned meshes and their bones are
53
+ * rebound to the copy. Geometry stays shared with the master — cheap, and never disposed by
54
+ * an instance. Materials are cloned per instance because `useMaterialApplication` mutates
55
+ * them per scene object; sharing them would make one object's colour change repaint every
56
+ * instance of the same URL.
57
+ */
58
+ export declare const cloneModelAsset: (asset: ModelAsset) => ModelInstance;
59
+ /**
60
+ * Release what a single mounted instance owns.
61
+ *
62
+ * Only the per-instance material clones. Geometry and textures are reached from the cache
63
+ * master and are shared by every other instance of that URL — disposing them here is how
64
+ * you make every other copy of the model turn black.
65
+ */
66
+ export declare const disposeModelInstance: (object: Object3D | null | undefined) => void;
67
+ /** Claim a mounted instance. Cancels a disposal another component scheduled for it. */
68
+ export declare const retainModelInstance: (object: Object3D | null | undefined) => void;
69
+ /** Give up a mounted instance. Frees it a tick later if nothing else claims it first. */
70
+ export declare const releaseModelInstance: (object: Object3D | null | undefined) => void;
71
+ /** Drop cache entries. For tests and debugging — masters are not disposed. */
72
+ export declare const clearModelCache: (url?: string) => void;
73
+ /** Diagnostics: what the cache is holding right now. */
74
+ export declare const modelCacheStats: () => {
75
+ url: string;
76
+ state: "loading" | "ready" | "error";
77
+ }[];
78
+ //# sourceMappingURL=modelCache.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"modelCache.d.ts","sourceRoot":"","sources":["../../src/utils/modelCache.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAY,QAAQ,EAAE,MAAM,OAAO,CAAC;AAG/D;;;;;;;;;GASG;AAEH,MAAM,WAAW,UAAU;IACvB,qDAAqD;IACrD,KAAK,EAAE,QAAQ,CAAC;IAChB,mEAAmE;IACnE,UAAU,EAAE,aAAa,EAAE,CAAC;CAC/B;AAED,MAAM,WAAW,eAAe;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,OAAO,CAAC,UAAU,CAAC,CAAC;IAC7B,kCAAkC;IAClC,KAAK,CAAC,EAAE,UAAU,CAAC;IACnB,iCAAiC;IACjC,KAAK,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,aAAa;IAC1B,MAAM,EAAE,QAAQ,CAAC;IACjB,UAAU,EAAE,aAAa,EAAE,CAAC;CAC/B;AAOD;;;GAGG;AACH,eAAO,MAAM,cAAc,GAAI,KAAK,MAAM,KAAG,eAyB5C,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,cAAc,GAAI,KAAK,MAAM,KAAG,UAK5C,CAAC;AAEF,uFAAuF;AACvF,eAAO,MAAM,cAAc,GAAI,KAAK,MAAM,KAAG,UAAU,GAAG,IACvB,CAAC;AAEpC;;;GAGG;AACH,eAAO,MAAM,aAAa,GAAU,MAAM,MAAM,EAAE,KAAG,OAAO,CAAC,IAAI,CAKhE,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,eAAe,GAAI,OAAO,UAAU,KAAG,aAoBnD,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,GAAI,QAAQ,QAAQ,GAAG,IAAI,GAAG,SAAS,KAAG,IAU1E,CAAC;AAkBF,uFAAuF;AACvF,eAAO,MAAM,mBAAmB,GAAI,QAAQ,QAAQ,GAAG,IAAI,GAAG,SAAS,KAAG,IAQzE,CAAC;AAEF,yFAAyF;AACzF,eAAO,MAAM,oBAAoB,GAAI,QAAQ,QAAQ,GAAG,IAAI,GAAG,SAAS,KAAG,IAgB1E,CAAC;AAEF,8EAA8E;AAC9E,eAAO,MAAM,eAAe,GAAI,MAAM,MAAM,KAAG,IAG9C,CAAC;AAEF,wDAAwD;AACxD,eAAO,MAAM,eAAe,QAAO;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,SAAS,GAAG,OAAO,GAAG,OAAO,CAAA;CAAE,EAIjF,CAAC"}
@@ -0,0 +1,164 @@
1
+ import { clone as cloneSkeleton } from 'three/addons/utils/SkeletonUtils.js';
2
+ import { gltfLoader } from './loadingManager.js';
3
+ /** Marks a material this instance owns and may dispose. */
4
+ const OWNED_MATERIAL = '__3driseOwnedMaterial';
5
+ const entries = new Map();
6
+ /**
7
+ * Start (or join) the load of a model URL. Safe to call from anywhere, any number of
8
+ * times — concurrent callers share one request and one master.
9
+ */
10
+ export const loadModelAsset = (url) => {
11
+ const existing = entries.get(url);
12
+ if (existing)
13
+ return existing;
14
+ const entry = {
15
+ url,
16
+ promise: gltfLoader
17
+ .loadAsync(url)
18
+ .then((gltf) => {
19
+ const asset = {
20
+ scene: gltf.scene,
21
+ animations: gltf.animations ?? [],
22
+ };
23
+ entry.asset = asset;
24
+ return asset;
25
+ })
26
+ .catch((error) => {
27
+ console.error(`[modelCache] Failed to load model: ${url}`, error);
28
+ entry.error = error;
29
+ throw error;
30
+ }),
31
+ };
32
+ entries.set(url, entry);
33
+ return entry;
34
+ };
35
+ /**
36
+ * Suspense-flavoured read: returns the asset if it is already here, otherwise throws the
37
+ * promise (React suspends) or the error (the nearest error boundary catches it).
38
+ *
39
+ * Call this from render, never from an effect.
40
+ */
41
+ export const readModelAsset = (url) => {
42
+ const entry = loadModelAsset(url);
43
+ if (entry.error)
44
+ throw entry.error;
45
+ if (!entry.asset)
46
+ throw entry.promise;
47
+ return entry.asset;
48
+ };
49
+ /** The resolved asset if the URL is already cached, otherwise null. Never suspends. */
50
+ export const peekModelAsset = (url) => entries.get(url)?.asset ?? null;
51
+ /**
52
+ * Warm the cache. Never rejects — a model that fails to load is reported by the leaf that
53
+ * needs it, not by the preload.
54
+ */
55
+ export const preloadModels = async (urls) => {
56
+ const unique = [...new Set(urls.filter(Boolean))];
57
+ await Promise.all(unique.map(url => loadModelAsset(url).promise.catch(() => undefined)));
58
+ };
59
+ /**
60
+ * Clone a master for mounting.
61
+ *
62
+ * `SkeletonUtils.clone` rather than `Object3D.clone` so skinned meshes and their bones are
63
+ * rebound to the copy. Geometry stays shared with the master — cheap, and never disposed by
64
+ * an instance. Materials are cloned per instance because `useMaterialApplication` mutates
65
+ * them per scene object; sharing them would make one object's colour change repaint every
66
+ * instance of the same URL.
67
+ */
68
+ export const cloneModelAsset = (asset) => {
69
+ const object = cloneSkeleton(asset.scene);
70
+ object.traverse(child => {
71
+ const material = child.material;
72
+ if (!material)
73
+ return;
74
+ if (Array.isArray(material)) {
75
+ child.material = material.map(m => {
76
+ const copy = m.clone();
77
+ copy.userData[OWNED_MATERIAL] = true;
78
+ return copy;
79
+ });
80
+ }
81
+ else {
82
+ const copy = material.clone();
83
+ copy.userData[OWNED_MATERIAL] = true;
84
+ child.material = copy;
85
+ }
86
+ });
87
+ return { object, animations: asset.animations };
88
+ };
89
+ /**
90
+ * Release what a single mounted instance owns.
91
+ *
92
+ * Only the per-instance material clones. Geometry and textures are reached from the cache
93
+ * master and are shared by every other instance of that URL — disposing them here is how
94
+ * you make every other copy of the model turn black.
95
+ */
96
+ export const disposeModelInstance = (object) => {
97
+ if (!object)
98
+ return;
99
+ object.traverse(child => {
100
+ const material = child.material;
101
+ if (!material)
102
+ return;
103
+ const list = Array.isArray(material) ? material : [material];
104
+ list.forEach(m => {
105
+ if (m?.userData?.[OWNED_MATERIAL])
106
+ m.dispose();
107
+ });
108
+ });
109
+ };
110
+ /**
111
+ * Mount/unmount bookkeeping for cloned instances.
112
+ *
113
+ * An instance can be mounted by one component at a time, but the *component* mounting it
114
+ * changes more often than you would think: React StrictMode double-mounts, a re-parent in
115
+ * the object tree remounts, and adopting a saved object's real id in place of its temp id
116
+ * changes the React key, which unmounts one leaf and mounts another around the same
117
+ * Object3D in a single commit.
118
+ *
119
+ * Disposing on the unmount half of any of those turns a model that is still on screen
120
+ * black. So disposal is refcounted and deferred by a tick: whoever mounts next cancels it,
121
+ * and only a release that is never followed by a retain actually frees anything.
122
+ */
123
+ const instanceRefCounts = new Map();
124
+ const pendingDisposals = new Map();
125
+ /** Claim a mounted instance. Cancels a disposal another component scheduled for it. */
126
+ export const retainModelInstance = (object) => {
127
+ if (!object)
128
+ return;
129
+ const pending = pendingDisposals.get(object);
130
+ if (pending !== undefined) {
131
+ clearTimeout(pending);
132
+ pendingDisposals.delete(object);
133
+ }
134
+ instanceRefCounts.set(object, (instanceRefCounts.get(object) ?? 0) + 1);
135
+ };
136
+ /** Give up a mounted instance. Frees it a tick later if nothing else claims it first. */
137
+ export const releaseModelInstance = (object) => {
138
+ if (!object)
139
+ return;
140
+ const next = (instanceRefCounts.get(object) ?? 1) - 1;
141
+ if (next > 0) {
142
+ instanceRefCounts.set(object, next);
143
+ return;
144
+ }
145
+ instanceRefCounts.delete(object);
146
+ if (pendingDisposals.has(object))
147
+ return;
148
+ pendingDisposals.set(object, setTimeout(() => {
149
+ pendingDisposals.delete(object);
150
+ disposeModelInstance(object);
151
+ }, 0));
152
+ };
153
+ /** Drop cache entries. For tests and debugging — masters are not disposed. */
154
+ export const clearModelCache = (url) => {
155
+ if (url)
156
+ entries.delete(url);
157
+ else
158
+ entries.clear();
159
+ };
160
+ /** Diagnostics: what the cache is holding right now. */
161
+ export const modelCacheStats = () => [...entries.values()].map(entry => ({
162
+ url: entry.url,
163
+ state: entry.error ? 'error' : entry.asset ? 'ready' : 'loading',
164
+ }));
@@ -8,6 +8,14 @@ export declare const createModelFromUrl: (model: ModelData) => Promise<CreatedRe
8
8
  export declare const buildQueryString: (params: Record<string, any>) => string;
9
9
  export declare const loadModels: (models: ModelData[]) => Promise<CreatedRef[]>;
10
10
  export declare const dontAnimate: (key: string) => boolean;
11
+ /**
12
+ * Preload a project's models and register one instance per scene object id.
13
+ *
14
+ * This is an optimisation, not a precondition: a model leaf that mounts without this
15
+ * having run loads itself through the same cache under Suspense (see
16
+ * `useModelObject` in the viewer package). Keep calling it so a scene reveals whole
17
+ * instead of popping in model by model.
18
+ */
11
19
  export declare const loadAssets: (models: ModelData[]) => Promise<CreatedRef[]>;
12
20
  export declare const createUniformsText: (uniforms: any[]) => string;
13
21
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/utils/utils.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAGtC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAC/C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAEvC,eAAO,MAAM,wBAAwB,GAAI,QAAQ,QAAQ,KAAG,WAY3D,CAAC;AACF,eAAO,MAAM,kBAAkB,GAAI,OAAO,QAAQ,EAAE,UAAS,MAAU,KAAG,MAWzE,CAAC;AAEF,eAAO,MAAM,kBAAkB,GAAU,OAAO,SAAS,KAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CA8BpF,CAAC;AACF,eAAO,MAAM,gBAAgB,GAAI,QAAQ,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,KAAG,MAQ9D,CAAC;AACF,eAAO,MAAM,UAAU,GAAU,QAAQ,SAAS,EAAE,KAAG,OAAO,CAAC,UAAU,EAAE,CA0C1E,CAAC;AACF,eAAO,MAAM,WAAW,GAAI,KAAK,MAAM,KAAG,OAGzC,CAAC;AACF,eAAO,MAAM,UAAU,GAAU,QAAQ,SAAS,EAAE,KAAG,OAAO,CAAC,UAAU,EAAE,CAqB1E,CAAC;AACF,eAAO,MAAM,kBAAkB,GAAI,UAAU,GAAG,EAAE,KAAG,MAQpD,CAAC;AACF;;;GAGG;AACH,eAAO,MAAM,cAAc,GAAI,UAAU,GAAG,EAAE,EAAE,cAAc,MAAM,EAAE,gBAAgB,MAAM,KAAG;IACvF,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;CAOxB,CAAC"}
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/utils/utils.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAGtC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAC/C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAEvC,eAAO,MAAM,wBAAwB,GAAI,QAAQ,QAAQ,KAAG,WAY3D,CAAC;AACF,eAAO,MAAM,kBAAkB,GAAI,OAAO,QAAQ,EAAE,UAAS,MAAU,KAAG,MAWzE,CAAC;AAEF,eAAO,MAAM,kBAAkB,GAAU,OAAO,SAAS,KAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAgCpF,CAAC;AACF,eAAO,MAAM,gBAAgB,GAAI,QAAQ,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,KAAG,MAQ9D,CAAC;AACF,eAAO,MAAM,UAAU,GAAU,QAAQ,SAAS,EAAE,KAAG,OAAO,CAAC,UAAU,EAAE,CA0C1E,CAAC;AACF,eAAO,MAAM,WAAW,GAAI,KAAK,MAAM,KAAG,OAGzC,CAAC;AACF;;;;;;;GAOG;AACH,eAAO,MAAM,UAAU,GAAU,QAAQ,SAAS,EAAE,KAAG,OAAO,CAAC,UAAU,EAAE,CAyB1E,CAAC;AACF,eAAO,MAAM,kBAAkB,GAAI,UAAU,GAAG,EAAE,KAAG,MAQpD,CAAC;AACF;;;GAGG;AACH,eAAO,MAAM,cAAc,GAAI,UAAU,GAAG,EAAE,EAAE,cAAc,MAAM,EAAE,gBAAgB,MAAM,KAAG;IACvF,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;CAOxB,CAAC"}
@@ -1,5 +1,5 @@
1
1
  import { Box3, Vector3 } from "three";
2
- import { loadModelGLTF } from "./loadingManager.js";
2
+ import { cloneModelAsset, loadModelAsset, preloadModels } from "./modelCache.js";
3
3
  import ObjectManager from "../services/ObjectManager.js";
4
4
  export const createDefaultEffectState = (object) => {
5
5
  const material = object.material;
@@ -30,8 +30,10 @@ export const calculateAutoScale = (model, maxSize = 5) => {
30
30
  export const createModelFromUrl = async (model) => {
31
31
  try {
32
32
  console.log(`Loading model from model: `, model);
33
- const loadedModel = await loadModelGLTF(model.url);
34
- // console.log("Model loaded from URL:", loadedModel);
33
+ // One fetch per URL for the life of the page; this is a per-instance clone of the
34
+ // cached master, so geometry is shared and materials are this object's own.
35
+ const asset = await loadModelAsset(model.url).promise;
36
+ const { object: loadedModel } = cloneModelAsset(asset);
35
37
  if (loadedModel) {
36
38
  loadedModel.name = model.name || `model_${Date.now()}`;
37
39
  loadedModel.scale.set(...(model.scale || [1, 1, 1]));
@@ -114,6 +116,14 @@ export const dontAnimate = (key) => {
114
116
  return key.toLowerCase().includes('texture') ||
115
117
  key.toLowerCase().includes('noise');
116
118
  };
119
+ /**
120
+ * Preload a project's models and register one instance per scene object id.
121
+ *
122
+ * This is an optimisation, not a precondition: a model leaf that mounts without this
123
+ * having run loads itself through the same cache under Suspense (see
124
+ * `useModelObject` in the viewer package). Keep calling it so a scene reveals whole
125
+ * instead of popping in model by model.
126
+ */
117
127
  export const loadAssets = async (models) => {
118
128
  console.log(`[loadAssets] Starting to load ${models.length} models`);
119
129
  if (models.length === 0) {
@@ -132,6 +142,10 @@ export const loadAssets = async (models) => {
132
142
  return true;
133
143
  });
134
144
  console.log(`[loadAssets] Loading ${cleanedModels.length} valid models (filtered ${models.length - cleanedModels.length})`);
145
+ // Warm the cache for every URL in parallel first — loadModels then walks the list
146
+ // sequentially, but each step is a clone of an already-resolved master rather than a
147
+ // fresh download. Repeated URLs cost one fetch, not one per scene object.
148
+ await preloadModels(cleanedModels.map(model => model.url));
135
149
  const createdObjects = await loadModels(cleanedModels);
136
150
  console.log(`[loadAssets] Completed. Loaded ${createdObjects.length}/${cleanedModels.length} models`);
137
151
  return createdObjects;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@3driseai/3drise-core",
3
- "version": "1.0.9",
3
+ "version": "1.0.10",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",