@tonybfox/threejs-tools 1.0.2 → 1.0.3

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.
@@ -64,6 +64,7 @@ var DualCameraControls = class extends CameraControls {
64
64
  const initialCamera = initialMode === "orthographic" ? orthographicCamera : perspectiveCamera;
65
65
  super(initialCamera, domElement);
66
66
  this.updateClock = new THREE.Clock();
67
+ this.externalCamera = null;
67
68
  const initialTarget = toVector3(
68
69
  options.initialTarget,
69
70
  [0, 0, 0],
@@ -99,9 +100,10 @@ var DualCameraControls = class extends CameraControls {
99
100
  * Switch to the perspective camera while keeping the current framing.
100
101
  */
101
102
  switchToPerspective(enableTransition = false) {
102
- if (this.activeMode === "perspective") {
103
+ if (this.activeMode === "perspective" && !this.externalCamera) {
103
104
  return;
104
105
  }
106
+ this.externalCamera = null;
105
107
  const target = this.getTarget(tempVec3A);
106
108
  const position = this.getPosition(tempVec3B);
107
109
  const aspect = resolveAspect(this.renderer, this.domElementRef);
@@ -133,9 +135,10 @@ var DualCameraControls = class extends CameraControls {
133
135
  * Switch to the orthographic camera while keeping the current framing.
134
136
  */
135
137
  switchToOrthographic(enableTransition = false) {
136
- if (this.activeMode === "orthographic") {
138
+ if (this.activeMode === "orthographic" && !this.externalCamera) {
137
139
  return;
138
140
  }
141
+ this.externalCamera = null;
139
142
  const target = this.getTarget(tempVec3A);
140
143
  const position = this.getPosition(tempVec3B);
141
144
  const aspect = resolveAspect(this.renderer, this.domElementRef);
@@ -173,6 +176,92 @@ var DualCameraControls = class extends CameraControls {
173
176
  this.switchToPerspective(enableTransition);
174
177
  }
175
178
  }
179
+ /**
180
+ * Returns true if currently using an external camera.
181
+ */
182
+ get isUsingExternalCamera() {
183
+ return this.externalCamera !== null;
184
+ }
185
+ /**
186
+ * Sets an external camera to use with the controls.
187
+ * This allows using a camera created outside of DualCameraControls.
188
+ * Call `clearExternalCamera()` to return to the internal cameras.
189
+ */
190
+ setCamera(camera, target, enableTransition = false) {
191
+ const previousCamera = this.camera;
192
+ this.externalCamera = camera;
193
+ const aspect = resolveAspect(this.renderer, this.domElementRef);
194
+ if (camera.type === "PerspectiveCamera") {
195
+ ;
196
+ camera.aspect = aspect;
197
+ camera.updateProjectionMatrix();
198
+ } else if (camera.type === "OrthographicCamera") {
199
+ const ortho = camera;
200
+ const currentHeight = (ortho.top - ortho.bottom) * 0.5;
201
+ const newHalfWidth = currentHeight * aspect;
202
+ ortho.left = -newHalfWidth;
203
+ ortho.right = newHalfWidth;
204
+ ortho.updateProjectionMatrix();
205
+ }
206
+ this.camera = camera;
207
+ const mode = camera.type === "PerspectiveCamera" ? "perspective" : "orthographic";
208
+ this.activeMode = mode;
209
+ this.updateInputBindingsForMode(mode);
210
+ const targetVec = toVector3(target, [0, 0, 0], tempVec3A);
211
+ void this.setLookAt(
212
+ camera.position.x,
213
+ camera.position.y,
214
+ camera.position.z,
215
+ targetVec.x,
216
+ targetVec.y,
217
+ targetVec.z,
218
+ enableTransition
219
+ );
220
+ this.dispatchEvent({
221
+ type: "externalcamerachange",
222
+ camera,
223
+ previousCamera
224
+ });
225
+ }
226
+ /**
227
+ * Clears the external camera and returns to using the internal cameras.
228
+ * Will switch to the camera matching the current mode.
229
+ */
230
+ clearExternalCamera(enableTransition = false) {
231
+ if (!this.externalCamera) {
232
+ return;
233
+ }
234
+ const target = this.getTarget(tempVec3A);
235
+ const position = this.getPosition(tempVec3B);
236
+ this.externalCamera = null;
237
+ const internalCamera = this.activeMode === "orthographic" ? this.orthographicCamera : this.perspectiveCamera;
238
+ internalCamera.position.copy(position);
239
+ internalCamera.quaternion.copy(this.camera.quaternion);
240
+ internalCamera.up.copy(this.camera.up);
241
+ const aspect = resolveAspect(this.renderer, this.domElementRef);
242
+ if (this.activeMode === "orthographic") {
243
+ this.updateOrthographicFrustum(position, target, aspect);
244
+ } else {
245
+ this.perspectiveCamera.aspect = aspect;
246
+ this.perspectiveCamera.updateProjectionMatrix();
247
+ }
248
+ this.camera = internalCamera;
249
+ void this.setLookAt(
250
+ position.x,
251
+ position.y,
252
+ position.z,
253
+ target.x,
254
+ target.y,
255
+ target.z,
256
+ enableTransition
257
+ );
258
+ this.dispatchEvent({
259
+ type: "modechange",
260
+ mode: this.activeMode,
261
+ previousMode: this.activeMode,
262
+ camera: internalCamera
263
+ });
264
+ }
176
265
  /**
177
266
  * Update camera projection parameters when the viewport size changes.
178
267
  */
@@ -266,4 +355,4 @@ function resolveAspect(renderer, domElement) {
266
355
  export {
267
356
  DualCameraControls
268
357
  };
269
- //# sourceMappingURL=chunk-WMHEIUXE.mjs.map
358
+ //# sourceMappingURL=chunk-IEE7DQOU.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../packages/camera/src/DualCameraControls.ts"],"sourcesContent":["import * as THREE from 'three'\nimport CameraControls from 'camera-controls'\n\ntype Vector3Tuple = [number, number, number]\ntype Vector3Like = THREE.Vector3 | Vector3Tuple\n\nexport type CameraMode = 'perspective' | 'orthographic'\n\nexport interface PerspectiveCameraConfig {\n fov?: number\n near?: number\n far?: number\n position?: Vector3Like\n zoom?: number\n}\n\nexport interface OrthographicCameraConfig {\n size?: number\n near?: number\n far?: number\n position?: Vector3Like\n zoom?: number\n}\n\nexport interface DualCameraControlsOptions {\n domElement?: HTMLElement\n initialMode?: CameraMode\n initialTarget?: Vector3Like\n perspective?: PerspectiveCameraConfig\n orthographic?: OrthographicCameraConfig\n}\n\nexport interface ModeChangedEvent {\n type: 'modechange'\n mode: CameraMode\n previousMode: CameraMode\n camera: THREE.PerspectiveCamera | THREE.OrthographicCamera\n}\n\nexport interface ExternalCameraChangedEvent {\n type: 'externalcamerachange'\n camera: THREE.PerspectiveCamera | THREE.OrthographicCamera\n previousCamera: THREE.PerspectiveCamera | THREE.OrthographicCamera\n}\n\nlet controlsInstalled = false\n\nconst tempVec3A = new THREE.Vector3()\nconst tempVec3B = new THREE.Vector3()\nconst tempVec2 = new THREE.Vector2()\n\nfunction ensureCameraControlsInstalled() {\n if (!controlsInstalled) {\n CameraControls.install({ THREE })\n controlsInstalled = true\n }\n}\n\nfunction toVector3(\n value: Vector3Like | undefined,\n fallback: Vector3Tuple,\n target: THREE.Vector3\n) {\n if (!value) {\n target.set(fallback[0], fallback[1], fallback[2])\n return target\n }\n\n if (Array.isArray(value)) {\n target.set(value[0], value[1], value[2])\n return target\n }\n\n target.copy(value)\n return target\n}\n\n/**\n * Camera controls that manage both perspective and orthographic cameras while\n * extending {@link CameraControls}. Provides helpers to toggle between the\n * camera types and keep the framing consistent.\n */\nexport class DualCameraControls extends CameraControls {\n readonly perspectiveCamera: THREE.PerspectiveCamera\n readonly orthographicCamera: THREE.OrthographicCamera\n\n private readonly renderer: THREE.WebGLRenderer\n private readonly domElementRef: HTMLElement\n private activeMode: CameraMode\n private readonly minOrthoHalfHeight: number\n private readonly updateClock = new THREE.Clock()\n private externalCamera:\n | THREE.PerspectiveCamera\n | THREE.OrthographicCamera\n | null = null\n\n constructor(\n renderer: THREE.WebGLRenderer,\n options: DualCameraControlsOptions = {}\n ) {\n ensureCameraControlsInstalled()\n\n const { domElement = renderer.domElement } = options\n const aspect = resolveAspect(renderer, domElement)\n\n const perspectiveConfig = options.perspective ?? {}\n const orthographicConfig = options.orthographic ?? {}\n\n const perspectiveCamera = new THREE.PerspectiveCamera(\n perspectiveConfig.fov ?? 60,\n aspect,\n perspectiveConfig.near ?? 0.1,\n perspectiveConfig.far ?? 2000\n )\n\n perspectiveCamera.position.copy(\n toVector3(perspectiveConfig.position, [12, 12, 12], new THREE.Vector3())\n )\n\n if (perspectiveConfig.zoom !== undefined) {\n perspectiveCamera.zoom = perspectiveConfig.zoom\n perspectiveCamera.updateProjectionMatrix()\n }\n\n const orthoHalfHeight = Math.max(orthographicConfig.size ?? 20, 0.001) * 0.5\n const orthoHalfWidth = orthoHalfHeight * aspect\n\n const orthographicCamera = new THREE.OrthographicCamera(\n -orthoHalfWidth,\n orthoHalfWidth,\n orthoHalfHeight,\n -orthoHalfHeight,\n orthographicConfig.near ?? 0.1,\n orthographicConfig.far ?? 2000\n )\n\n orthographicCamera.position.copy(\n toVector3(orthographicConfig.position, [12, 12, 12], new THREE.Vector3())\n )\n\n if (orthographicConfig.zoom !== undefined) {\n orthographicCamera.zoom = orthographicConfig.zoom\n orthographicCamera.updateProjectionMatrix()\n }\n\n const initialMode = options.initialMode ?? 'perspective'\n const initialCamera =\n initialMode === 'orthographic' ? orthographicCamera : perspectiveCamera\n\n super(initialCamera, domElement)\n\n const initialTarget = toVector3(\n options.initialTarget,\n [0, 0, 0],\n new THREE.Vector3()\n )\n void this.setLookAt(\n initialCamera.position.x,\n initialCamera.position.y,\n initialCamera.position.z,\n initialTarget.x,\n initialTarget.y,\n initialTarget.z,\n false\n )\n\n this.renderer = renderer\n this.domElementRef = domElement\n this.perspectiveCamera = perspectiveCamera\n this.orthographicCamera = orthographicCamera\n this.activeMode = initialMode\n this.minOrthoHalfHeight = orthoHalfHeight\n\n this.updateInputBindingsForMode(initialMode)\n }\n\n get mode(): CameraMode {\n return this.activeMode\n }\n\n /**\n * Returns the currently active camera instance.\n */\n get activeCamera(): THREE.PerspectiveCamera | THREE.OrthographicCamera {\n return this.camera\n }\n\n /**\n * Switch to the perspective camera while keeping the current framing.\n */\n switchToPerspective(enableTransition = false) {\n if (this.activeMode === 'perspective' && !this.externalCamera) {\n return\n }\n\n // Clear external camera if set\n this.externalCamera = null\n\n const target = this.getTarget(tempVec3A)\n const position = this.getPosition(tempVec3B)\n\n const aspect = resolveAspect(this.renderer, this.domElementRef)\n this.perspectiveCamera.aspect = aspect\n this.perspectiveCamera.position.copy(position)\n this.perspectiveCamera.quaternion.copy(this.camera.quaternion)\n this.perspectiveCamera.up.copy(this.camera.up)\n this.perspectiveCamera.updateProjectionMatrix()\n\n this.camera = this.perspectiveCamera\n this.activeMode = 'perspective'\n this.updateInputBindingsForMode('perspective')\n\n void this.setLookAt(\n position.x,\n position.y,\n position.z,\n target.x,\n target.y,\n target.z,\n enableTransition\n )\n\n this.dispatchEvent({\n type: 'modechange',\n mode: this.activeMode,\n previousMode: 'orthographic',\n camera: this.perspectiveCamera,\n } satisfies ModeChangedEvent)\n }\n\n /**\n * Switch to the orthographic camera while keeping the current framing.\n */\n switchToOrthographic(enableTransition = false) {\n if (this.activeMode === 'orthographic' && !this.externalCamera) {\n return\n }\n\n // Clear external camera if set\n this.externalCamera = null\n\n const target = this.getTarget(tempVec3A)\n const position = this.getPosition(tempVec3B)\n\n const aspect = resolveAspect(this.renderer, this.domElementRef)\n this.updateOrthographicFrustum(position, target, aspect)\n\n this.orthographicCamera.position.copy(position)\n this.orthographicCamera.quaternion.copy(this.camera.quaternion)\n this.orthographicCamera.up.copy(this.camera.up)\n this.orthographicCamera.updateProjectionMatrix()\n\n this.camera = this.orthographicCamera\n this.activeMode = 'orthographic'\n this.updateInputBindingsForMode('orthographic')\n\n void this.setLookAt(\n position.x,\n position.y,\n position.z,\n target.x,\n target.y,\n target.z,\n enableTransition\n )\n\n this.dispatchEvent({\n type: 'modechange',\n mode: this.activeMode,\n previousMode: 'perspective',\n camera: this.orthographicCamera,\n } satisfies ModeChangedEvent)\n }\n\n /**\n * Toggles between perspective and orthographic camera modes.\n */\n toggleCameraMode(enableTransition = false) {\n if (this.activeMode === 'perspective') {\n this.switchToOrthographic(enableTransition)\n } else {\n this.switchToPerspective(enableTransition)\n }\n }\n\n /**\n * Returns true if currently using an external camera.\n */\n get isUsingExternalCamera(): boolean {\n return this.externalCamera !== null\n }\n\n /**\n * Sets an external camera to use with the controls.\n * This allows using a camera created outside of DualCameraControls.\n * Call `clearExternalCamera()` to return to the internal cameras.\n */\n setCamera(\n camera: THREE.PerspectiveCamera | THREE.OrthographicCamera,\n target?: Vector3Like,\n enableTransition = false\n ) {\n const previousCamera = this.camera\n this.externalCamera = camera\n\n // Update aspect/frustum for the external camera\n const aspect = resolveAspect(this.renderer, this.domElementRef)\n if (camera.type === 'PerspectiveCamera') {\n ;(camera as THREE.PerspectiveCamera).aspect = aspect\n camera.updateProjectionMatrix()\n } else if (camera.type === 'OrthographicCamera') {\n // Maintain the camera's existing frustum proportions but update aspect\n const ortho = camera as THREE.OrthographicCamera\n const currentHeight = (ortho.top - ortho.bottom) * 0.5\n const newHalfWidth = currentHeight * aspect\n ortho.left = -newHalfWidth\n ortho.right = newHalfWidth\n ortho.updateProjectionMatrix()\n }\n\n this.camera = camera\n const mode: CameraMode =\n camera.type === 'PerspectiveCamera' ? 'perspective' : 'orthographic'\n this.activeMode = mode\n this.updateInputBindingsForMode(mode)\n\n // Set up the camera position and target in the controls\n const targetVec = toVector3(target, [0, 0, 0], tempVec3A)\n void this.setLookAt(\n camera.position.x,\n camera.position.y,\n camera.position.z,\n targetVec.x,\n targetVec.y,\n targetVec.z,\n enableTransition\n )\n\n this.dispatchEvent({\n type: 'externalcamerachange',\n camera,\n previousCamera,\n } satisfies ExternalCameraChangedEvent)\n }\n\n /**\n * Clears the external camera and returns to using the internal cameras.\n * Will switch to the camera matching the current mode.\n */\n clearExternalCamera(enableTransition = false) {\n if (!this.externalCamera) {\n return\n }\n\n const target = this.getTarget(tempVec3A)\n const position = this.getPosition(tempVec3B)\n\n this.externalCamera = null\n\n // Return to the internal camera matching the current mode\n const internalCamera =\n this.activeMode === 'orthographic'\n ? this.orthographicCamera\n : this.perspectiveCamera\n\n internalCamera.position.copy(position)\n internalCamera.quaternion.copy(this.camera.quaternion)\n internalCamera.up.copy(this.camera.up)\n\n const aspect = resolveAspect(this.renderer, this.domElementRef)\n if (this.activeMode === 'orthographic') {\n this.updateOrthographicFrustum(position, target, aspect)\n } else {\n this.perspectiveCamera.aspect = aspect\n this.perspectiveCamera.updateProjectionMatrix()\n }\n\n this.camera = internalCamera\n\n void this.setLookAt(\n position.x,\n position.y,\n position.z,\n target.x,\n target.y,\n target.z,\n enableTransition\n )\n\n this.dispatchEvent({\n type: 'modechange',\n mode: this.activeMode,\n previousMode: this.activeMode,\n camera: internalCamera,\n } satisfies ModeChangedEvent)\n }\n\n /**\n * Update camera projection parameters when the viewport size changes.\n */\n handleResize(width: number, height: number) {\n const aspect = height === 0 ? 1 : width / height\n\n this.perspectiveCamera.aspect = aspect\n this.perspectiveCamera.updateProjectionMatrix()\n\n const target = this.getTarget(tempVec3A)\n const position = this.getPosition(tempVec3B)\n this.updateOrthographicFrustum(position, target, aspect)\n\n if (this.activeMode === 'orthographic') {\n this.camera = this.orthographicCamera\n void this.setLookAt(\n position.x,\n position.y,\n position.z,\n target.x,\n target.y,\n target.z,\n false\n )\n }\n }\n\n /**\n * Moves the camera to a new position and target.\n */\n moveCamera(\n position: Vector3Like,\n target: Vector3Like,\n enableTransition = true\n ) {\n toVector3(position, [0, 0, 0], tempVec3A)\n toVector3(target, [0, 0, 0], tempVec3B)\n\n return this.setLookAt(\n tempVec3A.x,\n tempVec3A.y,\n tempVec3A.z,\n tempVec3B.x,\n tempVec3B.y,\n tempVec3B.z,\n enableTransition\n )\n }\n\n /**\n * Updates the controls using an internally managed clock.\n * Useful when you don't want to pass delta time each frame.\n */\n updateDelta(): ReturnType<CameraControls['update']> {\n const delta = this.updateClock.getDelta()\n return super.update(delta)\n }\n\n private updateInputBindingsForMode(mode: CameraMode) {\n const { ACTION } = CameraControls\n\n if (mode === 'orthographic') {\n this.mouseButtons.left = ACTION.TRUCK\n this.mouseButtons.right = ACTION.ROTATE\n this.mouseButtons.wheel = ACTION.ZOOM\n this.touches.one = ACTION.TOUCH_TRUCK\n this.touches.two = ACTION.TOUCH_ZOOM_TRUCK\n } else {\n this.mouseButtons.left = ACTION.ROTATE\n this.mouseButtons.right = ACTION.TRUCK\n this.mouseButtons.wheel = ACTION.DOLLY\n this.touches.one = ACTION.TOUCH_ROTATE\n this.touches.two = ACTION.TOUCH_DOLLY_TRUCK\n }\n }\n\n private updateOrthographicFrustum(\n position: THREE.Vector3,\n target: THREE.Vector3,\n aspect: number\n ) {\n const distance = Math.max(position.distanceTo(target), 0.001)\n const fov = this.perspectiveCamera.fov\n const halfHeight = Math.max(\n distance * Math.tan(THREE.MathUtils.degToRad(fov * 0.5)),\n this.minOrthoHalfHeight\n )\n const halfWidth = halfHeight * aspect\n\n this.orthographicCamera.left = -halfWidth\n this.orthographicCamera.right = halfWidth\n this.orthographicCamera.top = halfHeight\n this.orthographicCamera.bottom = -halfHeight\n this.orthographicCamera.updateProjectionMatrix()\n }\n}\n\nfunction resolveAspect(\n renderer: THREE.WebGLRenderer,\n domElement: HTMLElement\n): number {\n const size = renderer.getSize(tempVec2)\n if (size.y > 0) {\n return size.x / size.y\n }\n\n const { clientWidth, clientHeight } = domElement\n if (clientHeight > 0) {\n return clientWidth / clientHeight\n }\n\n return 1\n}\n"],"mappings":";AAAA,YAAY,WAAW;AACvB,OAAO,oBAAoB;AA4C3B,IAAI,oBAAoB;AAExB,IAAM,YAAY,IAAU,cAAQ;AACpC,IAAM,YAAY,IAAU,cAAQ;AACpC,IAAM,WAAW,IAAU,cAAQ;AAEnC,SAAS,gCAAgC;AACvC,MAAI,CAAC,mBAAmB;AACtB,mBAAe,QAAQ,EAAE,MAAM,CAAC;AAChC,wBAAoB;AAAA,EACtB;AACF;AAEA,SAAS,UACP,OACA,UACA,QACA;AACA,MAAI,CAAC,OAAO;AACV,WAAO,IAAI,SAAS,CAAC,GAAG,SAAS,CAAC,GAAG,SAAS,CAAC,CAAC;AAChD,WAAO;AAAA,EACT;AAEA,MAAI,MAAM,QAAQ,KAAK,GAAG;AACxB,WAAO,IAAI,MAAM,CAAC,GAAG,MAAM,CAAC,GAAG,MAAM,CAAC,CAAC;AACvC,WAAO;AAAA,EACT;AAEA,SAAO,KAAK,KAAK;AACjB,SAAO;AACT;AAOO,IAAM,qBAAN,cAAiC,eAAe;AAAA,EAcrD,YACE,UACA,UAAqC,CAAC,GACtC;AACA,kCAA8B;AAE9B,UAAM,EAAE,aAAa,SAAS,WAAW,IAAI;AAC7C,UAAM,SAAS,cAAc,UAAU,UAAU;AAEjD,UAAM,oBAAoB,QAAQ,eAAe,CAAC;AAClD,UAAM,qBAAqB,QAAQ,gBAAgB,CAAC;AAEpD,UAAM,oBAAoB,IAAU;AAAA,MAClC,kBAAkB,OAAO;AAAA,MACzB;AAAA,MACA,kBAAkB,QAAQ;AAAA,MAC1B,kBAAkB,OAAO;AAAA,IAC3B;AAEA,sBAAkB,SAAS;AAAA,MACzB,UAAU,kBAAkB,UAAU,CAAC,IAAI,IAAI,EAAE,GAAG,IAAU,cAAQ,CAAC;AAAA,IACzE;AAEA,QAAI,kBAAkB,SAAS,QAAW;AACxC,wBAAkB,OAAO,kBAAkB;AAC3C,wBAAkB,uBAAuB;AAAA,IAC3C;AAEA,UAAM,kBAAkB,KAAK,IAAI,mBAAmB,QAAQ,IAAI,IAAK,IAAI;AACzE,UAAM,iBAAiB,kBAAkB;AAEzC,UAAM,qBAAqB,IAAU;AAAA,MACnC,CAAC;AAAA,MACD;AAAA,MACA;AAAA,MACA,CAAC;AAAA,MACD,mBAAmB,QAAQ;AAAA,MAC3B,mBAAmB,OAAO;AAAA,IAC5B;AAEA,uBAAmB,SAAS;AAAA,MAC1B,UAAU,mBAAmB,UAAU,CAAC,IAAI,IAAI,EAAE,GAAG,IAAU,cAAQ,CAAC;AAAA,IAC1E;AAEA,QAAI,mBAAmB,SAAS,QAAW;AACzC,yBAAmB,OAAO,mBAAmB;AAC7C,yBAAmB,uBAAuB;AAAA,IAC5C;AAEA,UAAM,cAAc,QAAQ,eAAe;AAC3C,UAAM,gBACJ,gBAAgB,iBAAiB,qBAAqB;AAExD,UAAM,eAAe,UAAU;AA3DjC,SAAiB,cAAc,IAAU,YAAM;AAC/C,SAAQ,iBAGG;AAyDT,UAAM,gBAAgB;AAAA,MACpB,QAAQ;AAAA,MACR,CAAC,GAAG,GAAG,CAAC;AAAA,MACR,IAAU,cAAQ;AAAA,IACpB;AACA,SAAK,KAAK;AAAA,MACR,cAAc,SAAS;AAAA,MACvB,cAAc,SAAS;AAAA,MACvB,cAAc,SAAS;AAAA,MACvB,cAAc;AAAA,MACd,cAAc;AAAA,MACd,cAAc;AAAA,MACd;AAAA,IACF;AAEA,SAAK,WAAW;AAChB,SAAK,gBAAgB;AACrB,SAAK,oBAAoB;AACzB,SAAK,qBAAqB;AAC1B,SAAK,aAAa;AAClB,SAAK,qBAAqB;AAE1B,SAAK,2BAA2B,WAAW;AAAA,EAC7C;AAAA,EAEA,IAAI,OAAmB;AACrB,WAAO,KAAK;AAAA,EACd;AAAA;AAAA;AAAA;AAAA,EAKA,IAAI,eAAmE;AACrE,WAAO,KAAK;AAAA,EACd;AAAA;AAAA;AAAA;AAAA,EAKA,oBAAoB,mBAAmB,OAAO;AAC5C,QAAI,KAAK,eAAe,iBAAiB,CAAC,KAAK,gBAAgB;AAC7D;AAAA,IACF;AAGA,SAAK,iBAAiB;AAEtB,UAAM,SAAS,KAAK,UAAU,SAAS;AACvC,UAAM,WAAW,KAAK,YAAY,SAAS;AAE3C,UAAM,SAAS,cAAc,KAAK,UAAU,KAAK,aAAa;AAC9D,SAAK,kBAAkB,SAAS;AAChC,SAAK,kBAAkB,SAAS,KAAK,QAAQ;AAC7C,SAAK,kBAAkB,WAAW,KAAK,KAAK,OAAO,UAAU;AAC7D,SAAK,kBAAkB,GAAG,KAAK,KAAK,OAAO,EAAE;AAC7C,SAAK,kBAAkB,uBAAuB;AAE9C,SAAK,SAAS,KAAK;AACnB,SAAK,aAAa;AAClB,SAAK,2BAA2B,aAAa;AAE7C,SAAK,KAAK;AAAA,MACR,SAAS;AAAA,MACT,SAAS;AAAA,MACT,SAAS;AAAA,MACT,OAAO;AAAA,MACP,OAAO;AAAA,MACP,OAAO;AAAA,MACP;AAAA,IACF;AAEA,SAAK,cAAc;AAAA,MACjB,MAAM;AAAA,MACN,MAAM,KAAK;AAAA,MACX,cAAc;AAAA,MACd,QAAQ,KAAK;AAAA,IACf,CAA4B;AAAA,EAC9B;AAAA;AAAA;AAAA;AAAA,EAKA,qBAAqB,mBAAmB,OAAO;AAC7C,QAAI,KAAK,eAAe,kBAAkB,CAAC,KAAK,gBAAgB;AAC9D;AAAA,IACF;AAGA,SAAK,iBAAiB;AAEtB,UAAM,SAAS,KAAK,UAAU,SAAS;AACvC,UAAM,WAAW,KAAK,YAAY,SAAS;AAE3C,UAAM,SAAS,cAAc,KAAK,UAAU,KAAK,aAAa;AAC9D,SAAK,0BAA0B,UAAU,QAAQ,MAAM;AAEvD,SAAK,mBAAmB,SAAS,KAAK,QAAQ;AAC9C,SAAK,mBAAmB,WAAW,KAAK,KAAK,OAAO,UAAU;AAC9D,SAAK,mBAAmB,GAAG,KAAK,KAAK,OAAO,EAAE;AAC9C,SAAK,mBAAmB,uBAAuB;AAE/C,SAAK,SAAS,KAAK;AACnB,SAAK,aAAa;AAClB,SAAK,2BAA2B,cAAc;AAE9C,SAAK,KAAK;AAAA,MACR,SAAS;AAAA,MACT,SAAS;AAAA,MACT,SAAS;AAAA,MACT,OAAO;AAAA,MACP,OAAO;AAAA,MACP,OAAO;AAAA,MACP;AAAA,IACF;AAEA,SAAK,cAAc;AAAA,MACjB,MAAM;AAAA,MACN,MAAM,KAAK;AAAA,MACX,cAAc;AAAA,MACd,QAAQ,KAAK;AAAA,IACf,CAA4B;AAAA,EAC9B;AAAA;AAAA;AAAA;AAAA,EAKA,iBAAiB,mBAAmB,OAAO;AACzC,QAAI,KAAK,eAAe,eAAe;AACrC,WAAK,qBAAqB,gBAAgB;AAAA,IAC5C,OAAO;AACL,WAAK,oBAAoB,gBAAgB;AAAA,IAC3C;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,IAAI,wBAAiC;AACnC,WAAO,KAAK,mBAAmB;AAAA,EACjC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,UACE,QACA,QACA,mBAAmB,OACnB;AACA,UAAM,iBAAiB,KAAK;AAC5B,SAAK,iBAAiB;AAGtB,UAAM,SAAS,cAAc,KAAK,UAAU,KAAK,aAAa;AAC9D,QAAI,OAAO,SAAS,qBAAqB;AACvC;AAAC,MAAC,OAAmC,SAAS;AAC9C,aAAO,uBAAuB;AAAA,IAChC,WAAW,OAAO,SAAS,sBAAsB;AAE/C,YAAM,QAAQ;AACd,YAAM,iBAAiB,MAAM,MAAM,MAAM,UAAU;AACnD,YAAM,eAAe,gBAAgB;AACrC,YAAM,OAAO,CAAC;AACd,YAAM,QAAQ;AACd,YAAM,uBAAuB;AAAA,IAC/B;AAEA,SAAK,SAAS;AACd,UAAM,OACJ,OAAO,SAAS,sBAAsB,gBAAgB;AACxD,SAAK,aAAa;AAClB,SAAK,2BAA2B,IAAI;AAGpC,UAAM,YAAY,UAAU,QAAQ,CAAC,GAAG,GAAG,CAAC,GAAG,SAAS;AACxD,SAAK,KAAK;AAAA,MACR,OAAO,SAAS;AAAA,MAChB,OAAO,SAAS;AAAA,MAChB,OAAO,SAAS;AAAA,MAChB,UAAU;AAAA,MACV,UAAU;AAAA,MACV,UAAU;AAAA,MACV;AAAA,IACF;AAEA,SAAK,cAAc;AAAA,MACjB,MAAM;AAAA,MACN;AAAA,MACA;AAAA,IACF,CAAsC;AAAA,EACxC;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,oBAAoB,mBAAmB,OAAO;AAC5C,QAAI,CAAC,KAAK,gBAAgB;AACxB;AAAA,IACF;AAEA,UAAM,SAAS,KAAK,UAAU,SAAS;AACvC,UAAM,WAAW,KAAK,YAAY,SAAS;AAE3C,SAAK,iBAAiB;AAGtB,UAAM,iBACJ,KAAK,eAAe,iBAChB,KAAK,qBACL,KAAK;AAEX,mBAAe,SAAS,KAAK,QAAQ;AACrC,mBAAe,WAAW,KAAK,KAAK,OAAO,UAAU;AACrD,mBAAe,GAAG,KAAK,KAAK,OAAO,EAAE;AAErC,UAAM,SAAS,cAAc,KAAK,UAAU,KAAK,aAAa;AAC9D,QAAI,KAAK,eAAe,gBAAgB;AACtC,WAAK,0BAA0B,UAAU,QAAQ,MAAM;AAAA,IACzD,OAAO;AACL,WAAK,kBAAkB,SAAS;AAChC,WAAK,kBAAkB,uBAAuB;AAAA,IAChD;AAEA,SAAK,SAAS;AAEd,SAAK,KAAK;AAAA,MACR,SAAS;AAAA,MACT,SAAS;AAAA,MACT,SAAS;AAAA,MACT,OAAO;AAAA,MACP,OAAO;AAAA,MACP,OAAO;AAAA,MACP;AAAA,IACF;AAEA,SAAK,cAAc;AAAA,MACjB,MAAM;AAAA,MACN,MAAM,KAAK;AAAA,MACX,cAAc,KAAK;AAAA,MACnB,QAAQ;AAAA,IACV,CAA4B;AAAA,EAC9B;AAAA;AAAA;AAAA;AAAA,EAKA,aAAa,OAAe,QAAgB;AAC1C,UAAM,SAAS,WAAW,IAAI,IAAI,QAAQ;AAE1C,SAAK,kBAAkB,SAAS;AAChC,SAAK,kBAAkB,uBAAuB;AAE9C,UAAM,SAAS,KAAK,UAAU,SAAS;AACvC,UAAM,WAAW,KAAK,YAAY,SAAS;AAC3C,SAAK,0BAA0B,UAAU,QAAQ,MAAM;AAEvD,QAAI,KAAK,eAAe,gBAAgB;AACtC,WAAK,SAAS,KAAK;AACnB,WAAK,KAAK;AAAA,QACR,SAAS;AAAA,QACT,SAAS;AAAA,QACT,SAAS;AAAA,QACT,OAAO;AAAA,QACP,OAAO;AAAA,QACP,OAAO;AAAA,QACP;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,WACE,UACA,QACA,mBAAmB,MACnB;AACA,cAAU,UAAU,CAAC,GAAG,GAAG,CAAC,GAAG,SAAS;AACxC,cAAU,QAAQ,CAAC,GAAG,GAAG,CAAC,GAAG,SAAS;AAEtC,WAAO,KAAK;AAAA,MACV,UAAU;AAAA,MACV,UAAU;AAAA,MACV,UAAU;AAAA,MACV,UAAU;AAAA,MACV,UAAU;AAAA,MACV,UAAU;AAAA,MACV;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,cAAoD;AAClD,UAAM,QAAQ,KAAK,YAAY,SAAS;AACxC,WAAO,MAAM,OAAO,KAAK;AAAA,EAC3B;AAAA,EAEQ,2BAA2B,MAAkB;AACnD,UAAM,EAAE,OAAO,IAAI;AAEnB,QAAI,SAAS,gBAAgB;AAC3B,WAAK,aAAa,OAAO,OAAO;AAChC,WAAK,aAAa,QAAQ,OAAO;AACjC,WAAK,aAAa,QAAQ,OAAO;AACjC,WAAK,QAAQ,MAAM,OAAO;AAC1B,WAAK,QAAQ,MAAM,OAAO;AAAA,IAC5B,OAAO;AACL,WAAK,aAAa,OAAO,OAAO;AAChC,WAAK,aAAa,QAAQ,OAAO;AACjC,WAAK,aAAa,QAAQ,OAAO;AACjC,WAAK,QAAQ,MAAM,OAAO;AAC1B,WAAK,QAAQ,MAAM,OAAO;AAAA,IAC5B;AAAA,EACF;AAAA,EAEQ,0BACN,UACA,QACA,QACA;AACA,UAAM,WAAW,KAAK,IAAI,SAAS,WAAW,MAAM,GAAG,IAAK;AAC5D,UAAM,MAAM,KAAK,kBAAkB;AACnC,UAAM,aAAa,KAAK;AAAA,MACtB,WAAW,KAAK,IAAU,gBAAU,SAAS,MAAM,GAAG,CAAC;AAAA,MACvD,KAAK;AAAA,IACP;AACA,UAAM,YAAY,aAAa;AAE/B,SAAK,mBAAmB,OAAO,CAAC;AAChC,SAAK,mBAAmB,QAAQ;AAChC,SAAK,mBAAmB,MAAM;AAC9B,SAAK,mBAAmB,SAAS,CAAC;AAClC,SAAK,mBAAmB,uBAAuB;AAAA,EACjD;AACF;AAEA,SAAS,cACP,UACA,YACQ;AACR,QAAM,OAAO,SAAS,QAAQ,QAAQ;AACtC,MAAI,KAAK,IAAI,GAAG;AACd,WAAO,KAAK,IAAI,KAAK;AAAA,EACvB;AAEA,QAAM,EAAE,aAAa,aAAa,IAAI;AACtC,MAAI,eAAe,GAAG;AACpB,WAAO,cAAc;AAAA,EACvB;AAEA,SAAO;AACT;","names":[]}
@@ -3,6 +3,7 @@ import * as THREE from "three";
3
3
  import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
4
4
  import { FBXLoader } from "three/examples/jsm/loaders/FBXLoader.js";
5
5
  import { OBJLoader } from "three/examples/jsm/loaders/OBJLoader.js";
6
+ import { USDLoader } from "three/examples/jsm/loaders/USDLoader.js";
6
7
  var AssetLoader = class extends THREE.EventDispatcher {
7
8
  constructor() {
8
9
  super();
@@ -15,6 +16,7 @@ var AssetLoader = class extends THREE.EventDispatcher {
15
16
  this.gltfLoader = new GLTFLoader();
16
17
  this.fbxLoader = new FBXLoader();
17
18
  this.objLoader = new OBJLoader();
19
+ this.usdLoader = new USDLoader();
18
20
  }
19
21
  /**
20
22
  * Create a placeholder cube with shader effect
@@ -32,7 +34,9 @@ var AssetLoader = class extends THREE.EventDispatcher {
32
34
  opacity: { value: opacity },
33
35
  fillProgress: { value: 0 },
34
36
  time: { value: 0 },
35
- isError: { value: 0 }
37
+ isError: { value: 0 },
38
+ yMin: { value: -height / 2 },
39
+ yMax: { value: height / 2 }
36
40
  },
37
41
  vertexShader: `
38
42
  varying vec3 vPosition;
@@ -47,15 +51,17 @@ var AssetLoader = class extends THREE.EventDispatcher {
47
51
  uniform float fillProgress;
48
52
  uniform float time;
49
53
  uniform float isError;
54
+ uniform float yMin;
55
+ uniform float yMax;
50
56
  varying vec3 vPosition;
51
57
 
52
58
  void main() {
53
- float normalizedY = (vPosition.y + 1.0) / 2.0; // Normalize from -1,1 to 0,1
59
+ float normalizedY = (vPosition.y - yMin) / (yMax - yMin); // Normalize based on actual geometry bounds
54
60
  float alpha = opacity;
55
61
 
56
62
  // Create fill-up effect
57
63
  if (normalizedY > fillProgress) {
58
- alpha *= 0.3; // Reduce opacity for unfilled parts
64
+ alpha *= 0.1; // Reduce opacity for unfilled parts
59
65
  }
60
66
 
61
67
  // Error state effects
@@ -181,7 +187,7 @@ var AssetLoader = class extends THREE.EventDispatcher {
181
187
  lowResUrl,
182
188
  enableCaching = true,
183
189
  placeholderColor = 5227511,
184
- placeholderOpacity = 0.8,
190
+ placeholderOpacity = 0.4,
185
191
  errorColor = 16729156,
186
192
  errorOpacity = 0.5
187
193
  } = options;
@@ -242,14 +248,17 @@ var AssetLoader = class extends THREE.EventDispatcher {
242
248
  loadModel(type, url, isLowRes) {
243
249
  return new Promise((resolve, reject) => {
244
250
  const onProgress = (event) => {
245
- const percentage = event.loaded / event.total * 100;
251
+ let percentage = -1;
252
+ if (event.lengthComputable && event.total > 0 && event.loaded <= event.total) {
253
+ percentage = event.loaded / event.total * 100;
254
+ }
246
255
  this.dispatchEvent({
247
256
  type: "progress",
248
257
  loaded: event.loaded,
249
258
  total: event.total,
250
259
  percentage
251
260
  });
252
- if (!isLowRes) {
261
+ if (!isLowRes && percentage >= 0) {
253
262
  this.updatePlaceholder(percentage / 100);
254
263
  }
255
264
  };
@@ -291,6 +300,18 @@ var AssetLoader = class extends THREE.EventDispatcher {
291
300
  onError
292
301
  );
293
302
  break;
303
+ case "usd":
304
+ case "usdz":
305
+ this.usdLoader.load(
306
+ url,
307
+ (usd) => {
308
+ this.positionAssetAtBottomCenter(usd);
309
+ resolve(usd);
310
+ },
311
+ onProgress,
312
+ onError
313
+ );
314
+ break;
294
315
  default:
295
316
  reject(new Error(`Unsupported asset type: ${type}`));
296
317
  }
@@ -337,4 +358,4 @@ var AssetLoader = class extends THREE.EventDispatcher {
337
358
  export {
338
359
  AssetLoader
339
360
  };
340
- //# sourceMappingURL=chunk-L4VIIJZD.mjs.map
361
+ //# sourceMappingURL=chunk-WZ6IGBSE.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../packages/asset-loader/src/AssetLoader.ts"],"sourcesContent":["import * as THREE from 'three'\nimport { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'\nimport { FBXLoader } from 'three/examples/jsm/loaders/FBXLoader.js'\nimport { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader.js'\nimport { USDLoader } from 'three/examples/jsm/loaders/USDLoader.js'\n\n// Event types for asset loading\ninterface AssetLoaderEventMap {\n progress: { loaded: number; total: number; percentage: number }\n loaded: { asset: THREE.Object3D }\n error: { error: Error }\n placeholderCreated: { placeholder: THREE.Object3D }\n lowResLoaded: { lowRes: THREE.Object3D }\n}\n\nexport type AssetType = 'gltf' | 'fbx' | 'obj' | 'usd' | 'usdz'\n\nexport interface AssetLoaderOptions {\n type: AssetType\n url: string\n size?: [number, number, number] // Optional size for placeholder\n lowResUrl?: string // Optional low-res model URL\n enableCaching?: boolean\n placeholderColor?: number\n placeholderOpacity?: number\n errorColor?: number // Color to use when loading fails\n errorOpacity?: number // Opacity to use when loading fails\n}\n\nexport class AssetLoader extends THREE.EventDispatcher<AssetLoaderEventMap> {\n private cache: Map<string, THREE.Object3D> = new Map()\n private gltfLoader: GLTFLoader\n private fbxLoader: FBXLoader\n private objLoader: OBJLoader\n private usdLoader: USDLoader\n private placeholder: THREE.Object3D | null = null\n private loadedAsset: THREE.Object3D | null = null\n private lowResAsset: THREE.Object3D | null = null\n private errorColor: number = 0xff4444\n private errorOpacity: number = 0.5\n\n constructor() {\n super()\n this.gltfLoader = new GLTFLoader()\n this.fbxLoader = new FBXLoader()\n this.objLoader = new OBJLoader()\n this.usdLoader = new USDLoader()\n }\n\n /**\n * Create a placeholder cube with shader effect\n */\n private createPlaceholder(\n size: [number, number, number],\n color: number = 0x4fc3f7,\n opacity: number = 0.3\n ): THREE.Object3D {\n const [width, height, depth] = size\n const geometry = new THREE.BoxGeometry(width, height, depth)\n\n // Custom shader material with fill-up effect\n const material = new THREE.ShaderMaterial({\n side: THREE.DoubleSide,\n depthWrite: false,\n blending: THREE.AdditiveBlending,\n transparent: true,\n uniforms: {\n color: { value: new THREE.Color(color) },\n opacity: { value: opacity },\n fillProgress: { value: 0.0 },\n time: { value: 0.0 },\n isError: { value: 0.0 },\n yMin: { value: -height / 2 },\n yMax: { value: height / 2 },\n },\n vertexShader: `\n varying vec3 vPosition;\n void main() {\n vPosition = position;\n gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\n }\n `,\n fragmentShader: `\n uniform vec3 color;\n uniform float opacity;\n uniform float fillProgress;\n uniform float time;\n uniform float isError;\n uniform float yMin;\n uniform float yMax;\n varying vec3 vPosition;\n \n void main() {\n float normalizedY = (vPosition.y - yMin) / (yMax - yMin); // Normalize based on actual geometry bounds\n float alpha = opacity;\n \n // Create fill-up effect\n if (normalizedY > fillProgress) {\n alpha *= 0.1; // Reduce opacity for unfilled parts\n }\n \n // Error state effects\n if (isError > 0.5) {\n // Add pulsing effect for error state\n float pulse = 0.5 + 0.5 * sin(time * 4.0);\n alpha *= (0.3 + 0.4 * pulse);\n \n // Add error pattern\n float stripe = sin(vPosition.y * 20.0 + time * 2.0);\n alpha *= (0.7 + 0.3 * step(0.0, stripe));\n }\n \n // Add edge glow\n vec3 viewDirection = normalize(cameraPosition - vPosition);\n float edgeIntensity = pow(1.0 - abs(dot(viewDirection, normalize(vPosition))), 2.0);\n \n vec3 finalColor = color + edgeIntensity * 0.5;\n gl_FragColor = vec4(finalColor, alpha);\n }\n `,\n })\n\n const mesh = new THREE.Mesh(geometry, material)\n this.positionAssetAtBottomCenter(mesh)\n return mesh\n }\n\n /**\n * Update placeholder fill progress based on loading progress\n */\n private updatePlaceholder(progress: number) {\n if (this.placeholder && this.placeholder instanceof THREE.Mesh) {\n const material = this.placeholder.material as THREE.ShaderMaterial\n if (material.uniforms && material.uniforms.fillProgress) {\n material.uniforms.fillProgress.value = progress\n }\n }\n }\n\n /**\n * Set placeholder to error state with configurable color and opacity\n */\n private setPlaceholderError() {\n if (this.placeholder && this.placeholder instanceof THREE.Mesh) {\n const material = this.placeholder.material as THREE.ShaderMaterial\n if (material.uniforms) {\n // Change to error color\n if (material.uniforms.color) {\n material.uniforms.color.value = new THREE.Color(this.errorColor)\n }\n // Set error opacity\n if (material.uniforms.opacity) {\n material.uniforms.opacity.value = this.errorOpacity\n }\n // Set fill progress to indicate failure\n if (material.uniforms.fillProgress) {\n material.uniforms.fillProgress.value = 0.0\n }\n // Enable error state\n if (material.uniforms.isError) {\n material.uniforms.isError.value = 1.0\n }\n }\n }\n }\n\n /**\n * Update placeholder animation time (call this in your render loop)\n */\n public updatePlaceholderAnimation(deltaTime: number) {\n if (this.placeholder && this.placeholder instanceof THREE.Mesh) {\n const material = this.placeholder.material as THREE.ShaderMaterial\n if (material.uniforms && material.uniforms.time) {\n material.uniforms.time.value += deltaTime\n }\n }\n }\n\n /**\n * Reposition an asset so that its bottom-center sits at the local origin.\n */\n private positionAssetAtBottomCenter(object: THREE.Object3D) {\n object.updateMatrixWorld(true)\n\n const boundingBox = new THREE.Box3().setFromObject(object)\n if (boundingBox.isEmpty()) {\n return\n }\n\n const center = boundingBox.getCenter(new THREE.Vector3())\n const bottomCenter = new THREE.Vector3(\n center.x,\n boundingBox.min.y + 0.01,\n center.z\n )\n\n if (\n !Number.isFinite(bottomCenter.x) ||\n !Number.isFinite(bottomCenter.y) ||\n !Number.isFinite(bottomCenter.z)\n ) {\n return\n }\n\n object.position.sub(bottomCenter)\n object.userData.bottomCenterOffset = bottomCenter\n object.updateMatrixWorld(true)\n }\n\n private ensureBottomCenterOffset(\n object: THREE.Object3D\n ): THREE.Vector3 | null {\n const offset = object.userData.bottomCenterOffset\n if (offset instanceof THREE.Vector3) {\n return offset\n }\n\n if (offset && typeof offset === 'object') {\n const {\n x = 0,\n y = 0,\n z = 0,\n } = offset as Partial<Record<'x' | 'y' | 'z', number>>\n const normalized = new THREE.Vector3(x ?? 0, y ?? 0, z ?? 0)\n object.userData.bottomCenterOffset = normalized\n return normalized\n }\n\n return null\n }\n\n private normalizeBottomCenterData(object: THREE.Object3D): boolean {\n const hasOffset = this.ensureBottomCenterOffset(object) !== null\n object.children.forEach((child) => this.normalizeBottomCenterData(child))\n return hasOffset\n }\n\n /**\n * Load an asset with the specified options\n */\n async load(options: AssetLoaderOptions): Promise<THREE.Object3D> {\n const {\n type,\n url,\n size,\n lowResUrl,\n enableCaching = true,\n placeholderColor = 0x4fc3f7,\n placeholderOpacity = 0.4,\n errorColor = 0xff4444,\n errorOpacity = 0.5,\n } = options\n\n // Check cache first\n if (enableCaching && this.cache.has(url)) {\n const cachedClone = this.cache.get(url)!.clone(true)\n const hasOffset = this.normalizeBottomCenterData(cachedClone)\n\n if (!hasOffset) {\n this.positionAssetAtBottomCenter(cachedClone)\n } else {\n cachedClone.updateMatrixWorld(true)\n }\n\n this.loadedAsset = cachedClone\n this.dispatchEvent({ type: 'loaded', asset: cachedClone })\n return cachedClone\n }\n\n // Store error styling options\n this.errorColor = errorColor\n this.errorOpacity = errorOpacity\n\n // Create placeholder if size is provided\n if (size) {\n this.placeholder = this.createPlaceholder(\n size,\n placeholderColor,\n placeholderOpacity\n )\n this.dispatchEvent({\n type: 'placeholderCreated',\n placeholder: this.placeholder,\n })\n }\n\n try {\n // Load low-res model first if provided\n if (lowResUrl) {\n const lowRes = await this.loadModel(type, lowResUrl, true)\n this.lowResAsset = lowRes\n this.dispatchEvent({ type: 'lowResLoaded', lowRes })\n }\n\n // Load main asset\n const asset = await this.loadModel(type, url, false)\n this.loadedAsset = asset\n\n // Cache if enabled\n if (enableCaching) {\n const cacheEntry = asset.clone(true)\n const hasOffset = this.normalizeBottomCenterData(cacheEntry)\n if (!hasOffset) {\n this.positionAssetAtBottomCenter(cacheEntry)\n } else {\n cacheEntry.updateMatrixWorld(true)\n }\n this.cache.set(url, cacheEntry)\n }\n\n this.dispatchEvent({ type: 'loaded', asset })\n return asset\n } catch (error) {\n // Set placeholder to error state\n this.setPlaceholderError()\n this.dispatchEvent({ type: 'error', error: error as Error })\n throw error\n }\n }\n\n /**\n * Load a model based on type\n */\n private loadModel(\n type: AssetType,\n url: string,\n isLowRes: boolean\n ): Promise<THREE.Object3D> {\n return new Promise((resolve, reject) => {\n const onProgress = (event: ProgressEvent) => {\n // Handle cases where loaded > total (compressed content mismatch)\n // or total is 0 (unknown content length)\n let percentage = -1 // Default to indeterminate\n\n // Only calculate percentage if lengthComputable is true AND loaded <= total\n // This prevents false 100% when server reports compressed size\n if (\n event.lengthComputable &&\n event.total > 0 &&\n event.loaded <= event.total\n ) {\n percentage = (event.loaded / event.total) * 100\n }\n\n this.dispatchEvent({\n type: 'progress',\n loaded: event.loaded,\n total: event.total,\n percentage,\n })\n\n // Update placeholder fill only with valid percentage\n if (!isLowRes && percentage >= 0) {\n this.updatePlaceholder(percentage / 100)\n }\n }\n\n const onError = (error: unknown) => {\n reject(error)\n }\n\n switch (type) {\n case 'gltf':\n this.gltfLoader.load(\n url,\n (gltf) => {\n const scene = gltf.scene\n this.positionAssetAtBottomCenter(scene)\n resolve(scene)\n },\n onProgress,\n onError\n )\n break\n\n case 'fbx':\n this.fbxLoader.load(\n url,\n (fbx) => {\n this.positionAssetAtBottomCenter(fbx)\n resolve(fbx)\n },\n onProgress,\n onError\n )\n break\n\n case 'obj':\n this.objLoader.load(\n url,\n (obj) => {\n this.positionAssetAtBottomCenter(obj)\n resolve(obj)\n },\n onProgress,\n onError\n )\n break\n\n case 'usd':\n case 'usdz':\n this.usdLoader.load(\n url,\n (usd) => {\n this.positionAssetAtBottomCenter(usd)\n resolve(usd)\n },\n onProgress,\n onError\n )\n break\n\n default:\n reject(new Error(`Unsupported asset type: ${type}`))\n }\n })\n }\n\n /**\n * Get the placeholder object\n */\n getPlaceholder(): THREE.Object3D | null {\n return this.placeholder\n }\n\n /**\n * Get the loaded asset\n */\n getAsset(): THREE.Object3D | null {\n return this.loadedAsset\n }\n\n /**\n * Get the low-res asset\n */\n getLowResAsset(): THREE.Object3D | null {\n return this.lowResAsset\n }\n\n /**\n * Clear the cache\n */\n clearCache() {\n this.cache.clear()\n }\n\n /**\n * Remove an item from cache\n */\n removeFromCache(url: string) {\n this.cache.delete(url)\n }\n\n /**\n * Get cache size\n */\n getCacheSize(): number {\n return this.cache.size\n }\n}\n"],"mappings":"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","names":[]}
package/dist/index.cjs CHANGED
@@ -51,6 +51,7 @@ var THREE = __toESM(require("three"));
51
51
  var import_GLTFLoader = require("three/examples/jsm/loaders/GLTFLoader.js");
52
52
  var import_FBXLoader = require("three/examples/jsm/loaders/FBXLoader.js");
53
53
  var import_OBJLoader = require("three/examples/jsm/loaders/OBJLoader.js");
54
+ var import_USDLoader = require("three/examples/jsm/loaders/USDLoader.js");
54
55
  var AssetLoader = class extends THREE.EventDispatcher {
55
56
  constructor() {
56
57
  super();
@@ -63,6 +64,7 @@ var AssetLoader = class extends THREE.EventDispatcher {
63
64
  this.gltfLoader = new import_GLTFLoader.GLTFLoader();
64
65
  this.fbxLoader = new import_FBXLoader.FBXLoader();
65
66
  this.objLoader = new import_OBJLoader.OBJLoader();
67
+ this.usdLoader = new import_USDLoader.USDLoader();
66
68
  }
67
69
  /**
68
70
  * Create a placeholder cube with shader effect
@@ -80,7 +82,9 @@ var AssetLoader = class extends THREE.EventDispatcher {
80
82
  opacity: { value: opacity },
81
83
  fillProgress: { value: 0 },
82
84
  time: { value: 0 },
83
- isError: { value: 0 }
85
+ isError: { value: 0 },
86
+ yMin: { value: -height / 2 },
87
+ yMax: { value: height / 2 }
84
88
  },
85
89
  vertexShader: `
86
90
  varying vec3 vPosition;
@@ -95,15 +99,17 @@ var AssetLoader = class extends THREE.EventDispatcher {
95
99
  uniform float fillProgress;
96
100
  uniform float time;
97
101
  uniform float isError;
102
+ uniform float yMin;
103
+ uniform float yMax;
98
104
  varying vec3 vPosition;
99
105
 
100
106
  void main() {
101
- float normalizedY = (vPosition.y + 1.0) / 2.0; // Normalize from -1,1 to 0,1
107
+ float normalizedY = (vPosition.y - yMin) / (yMax - yMin); // Normalize based on actual geometry bounds
102
108
  float alpha = opacity;
103
109
 
104
110
  // Create fill-up effect
105
111
  if (normalizedY > fillProgress) {
106
- alpha *= 0.3; // Reduce opacity for unfilled parts
112
+ alpha *= 0.1; // Reduce opacity for unfilled parts
107
113
  }
108
114
 
109
115
  // Error state effects
@@ -229,7 +235,7 @@ var AssetLoader = class extends THREE.EventDispatcher {
229
235
  lowResUrl,
230
236
  enableCaching = true,
231
237
  placeholderColor = 5227511,
232
- placeholderOpacity = 0.8,
238
+ placeholderOpacity = 0.4,
233
239
  errorColor = 16729156,
234
240
  errorOpacity = 0.5
235
241
  } = options;
@@ -290,14 +296,17 @@ var AssetLoader = class extends THREE.EventDispatcher {
290
296
  loadModel(type, url, isLowRes) {
291
297
  return new Promise((resolve, reject) => {
292
298
  const onProgress = (event) => {
293
- const percentage = event.loaded / event.total * 100;
299
+ let percentage = -1;
300
+ if (event.lengthComputable && event.total > 0 && event.loaded <= event.total) {
301
+ percentage = event.loaded / event.total * 100;
302
+ }
294
303
  this.dispatchEvent({
295
304
  type: "progress",
296
305
  loaded: event.loaded,
297
306
  total: event.total,
298
307
  percentage
299
308
  });
300
- if (!isLowRes) {
309
+ if (!isLowRes && percentage >= 0) {
301
310
  this.updatePlaceholder(percentage / 100);
302
311
  }
303
312
  };
@@ -339,6 +348,18 @@ var AssetLoader = class extends THREE.EventDispatcher {
339
348
  onError
340
349
  );
341
350
  break;
351
+ case "usd":
352
+ case "usdz":
353
+ this.usdLoader.load(
354
+ url,
355
+ (usd) => {
356
+ this.positionAssetAtBottomCenter(usd);
357
+ resolve(usd);
358
+ },
359
+ onProgress,
360
+ onError
361
+ );
362
+ break;
342
363
  default:
343
364
  reject(new Error(`Unsupported asset type: ${type}`));
344
365
  }
@@ -448,6 +469,7 @@ var DualCameraControls = class extends import_camera_controls.default {
448
469
  const initialCamera = initialMode === "orthographic" ? orthographicCamera : perspectiveCamera;
449
470
  super(initialCamera, domElement);
450
471
  this.updateClock = new THREE2.Clock();
472
+ this.externalCamera = null;
451
473
  const initialTarget = toVector3(
452
474
  options.initialTarget,
453
475
  [0, 0, 0],
@@ -483,9 +505,10 @@ var DualCameraControls = class extends import_camera_controls.default {
483
505
  * Switch to the perspective camera while keeping the current framing.
484
506
  */
485
507
  switchToPerspective(enableTransition = false) {
486
- if (this.activeMode === "perspective") {
508
+ if (this.activeMode === "perspective" && !this.externalCamera) {
487
509
  return;
488
510
  }
511
+ this.externalCamera = null;
489
512
  const target = this.getTarget(tempVec3A);
490
513
  const position = this.getPosition(tempVec3B);
491
514
  const aspect = resolveAspect(this.renderer, this.domElementRef);
@@ -517,9 +540,10 @@ var DualCameraControls = class extends import_camera_controls.default {
517
540
  * Switch to the orthographic camera while keeping the current framing.
518
541
  */
519
542
  switchToOrthographic(enableTransition = false) {
520
- if (this.activeMode === "orthographic") {
543
+ if (this.activeMode === "orthographic" && !this.externalCamera) {
521
544
  return;
522
545
  }
546
+ this.externalCamera = null;
523
547
  const target = this.getTarget(tempVec3A);
524
548
  const position = this.getPosition(tempVec3B);
525
549
  const aspect = resolveAspect(this.renderer, this.domElementRef);
@@ -557,6 +581,92 @@ var DualCameraControls = class extends import_camera_controls.default {
557
581
  this.switchToPerspective(enableTransition);
558
582
  }
559
583
  }
584
+ /**
585
+ * Returns true if currently using an external camera.
586
+ */
587
+ get isUsingExternalCamera() {
588
+ return this.externalCamera !== null;
589
+ }
590
+ /**
591
+ * Sets an external camera to use with the controls.
592
+ * This allows using a camera created outside of DualCameraControls.
593
+ * Call `clearExternalCamera()` to return to the internal cameras.
594
+ */
595
+ setCamera(camera, target, enableTransition = false) {
596
+ const previousCamera = this.camera;
597
+ this.externalCamera = camera;
598
+ const aspect = resolveAspect(this.renderer, this.domElementRef);
599
+ if (camera.type === "PerspectiveCamera") {
600
+ ;
601
+ camera.aspect = aspect;
602
+ camera.updateProjectionMatrix();
603
+ } else if (camera.type === "OrthographicCamera") {
604
+ const ortho = camera;
605
+ const currentHeight = (ortho.top - ortho.bottom) * 0.5;
606
+ const newHalfWidth = currentHeight * aspect;
607
+ ortho.left = -newHalfWidth;
608
+ ortho.right = newHalfWidth;
609
+ ortho.updateProjectionMatrix();
610
+ }
611
+ this.camera = camera;
612
+ const mode = camera.type === "PerspectiveCamera" ? "perspective" : "orthographic";
613
+ this.activeMode = mode;
614
+ this.updateInputBindingsForMode(mode);
615
+ const targetVec = toVector3(target, [0, 0, 0], tempVec3A);
616
+ void this.setLookAt(
617
+ camera.position.x,
618
+ camera.position.y,
619
+ camera.position.z,
620
+ targetVec.x,
621
+ targetVec.y,
622
+ targetVec.z,
623
+ enableTransition
624
+ );
625
+ this.dispatchEvent({
626
+ type: "externalcamerachange",
627
+ camera,
628
+ previousCamera
629
+ });
630
+ }
631
+ /**
632
+ * Clears the external camera and returns to using the internal cameras.
633
+ * Will switch to the camera matching the current mode.
634
+ */
635
+ clearExternalCamera(enableTransition = false) {
636
+ if (!this.externalCamera) {
637
+ return;
638
+ }
639
+ const target = this.getTarget(tempVec3A);
640
+ const position = this.getPosition(tempVec3B);
641
+ this.externalCamera = null;
642
+ const internalCamera = this.activeMode === "orthographic" ? this.orthographicCamera : this.perspectiveCamera;
643
+ internalCamera.position.copy(position);
644
+ internalCamera.quaternion.copy(this.camera.quaternion);
645
+ internalCamera.up.copy(this.camera.up);
646
+ const aspect = resolveAspect(this.renderer, this.domElementRef);
647
+ if (this.activeMode === "orthographic") {
648
+ this.updateOrthographicFrustum(position, target, aspect);
649
+ } else {
650
+ this.perspectiveCamera.aspect = aspect;
651
+ this.perspectiveCamera.updateProjectionMatrix();
652
+ }
653
+ this.camera = internalCamera;
654
+ void this.setLookAt(
655
+ position.x,
656
+ position.y,
657
+ position.z,
658
+ target.x,
659
+ target.y,
660
+ target.z,
661
+ enableTransition
662
+ );
663
+ this.dispatchEvent({
664
+ type: "modechange",
665
+ mode: this.activeMode,
666
+ previousMode: this.activeMode,
667
+ camera: internalCamera
668
+ });
669
+ }
560
670
  /**
561
671
  * Update camera projection parameters when the viewport size changes.
562
672
  */