@cavegiant/cave-world-r3f 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,14 @@ All notable changes to `@cavegiant/cave-world-r3f` are documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [Unreleased]
9
+
10
+ ## [1.1.0] - 2026-09-18
11
+
12
+ ### Added
13
+
14
+ - **Model colliders** — `baseLayer.colliders` entries with `type: "model"` accept `.ply` mesh URLs in addition to `.obj`. The loader is inferred from the path extension (query/hash ignored).
15
+
8
16
  ## [1.0.0] - 2026-09-17
9
17
 
10
18
  Initial public release.
package/README.md CHANGED
@@ -165,12 +165,8 @@ This repository is the standalone package. From the package root:
165
165
 
166
166
  ```bash
167
167
  npm install
168
- npm run typecheck
169
- npm run lint
170
- npm run format:check
171
- npm run test
168
+ npm run check # format:check + typecheck + lint + test
172
169
  npm run build
173
- npm run check # typecheck + lint + test
174
170
  npm run verify:package # publint + attw
175
171
  npm run verify:pack-size # tarball must stay lean (no examples/)
176
172
  npm run examples # live playground at https://localhost:5173/
@@ -1 +1 @@
1
- {"version":3,"file":"use-connector-preloader.d.ts","sourceRoot":"","sources":["../../../explore/hooks/use-connector-preloader.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,SAAS,EAAY,MAAM,aAAa,CAAA;AAQtD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AAa7D,iEAAiE;AACjE,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,SAAS,GAAG,IAAI,CAMzD;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,eAAe,EACvB,cAAc,EAAE,MAAM,GAAG,SAAS,EAClC,kBAAkB,EAAE,OAAO,GAC1B,IAAI,CAoGN"}
1
+ {"version":3,"file":"use-connector-preloader.d.ts","sourceRoot":"","sources":["../../../explore/hooks/use-connector-preloader.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAY,MAAM,aAAa,CAAA;AAStD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AAa7D,iEAAiE;AACjE,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,SAAS,GAAG,IAAI,CAMzD;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,eAAe,EACvB,cAAc,EAAE,MAAM,GAAG,SAAS,EAClC,kBAAkB,EAAE,OAAO,GAC1B,IAAI,CAoGN"}
@@ -1,25 +1,24 @@
1
1
  import { useEffect as I, startTransition as C } from "react";
2
- import { useLoader as L } from "@react-three/fiber";
3
2
  import { Matrix4 as M } from "three";
4
- import { OBJLoader as E } from "three/examples/jsm/loaders/OBJLoader.js";
5
- import { createLogger as A } from "../../core/logging/logger.js";
6
- import { getConnectorTargetIds as v, matrixFromM16 as b, computeSeamlessGroupMatrix as h } from "../math/space-transforms.js";
7
- import { useCaveExploreStore as w, useCaveExplore as g } from "../stores/explore-context.js";
8
- import { scheduleIdle as y } from "../utils/schedule-idle.js";
9
- const N = A("explore:preload"), R = 1, O = /* @__PURE__ */ new M();
3
+ import { createLogger as E } from "../../core/logging/logger.js";
4
+ import { preloadModelCollider as L } from "../../space/utils/model-collider-format.js";
5
+ import { getConnectorTargetIds as v, matrixFromM16 as A, computeSeamlessGroupMatrix as b } from "../math/space-transforms.js";
6
+ import { useCaveExploreStore as h, useCaveExplore as g } from "../stores/explore-context.js";
7
+ import { scheduleIdle as w } from "../utils/schedule-idle.js";
8
+ const y = E("explore:preload"), N = 1, R = /* @__PURE__ */ new M();
10
9
  function _(i) {
11
10
  for (const c of i.baseLayer?.colliders ?? [])
12
- c.type === "model" && c.url && L.preload(E, c.url);
11
+ c.type === "model" && c.url && L(c.url);
13
12
  }
14
- function J(i, c, T) {
15
- const r = w(), m = g((t) => t.connectedSpaces), x = g((t) => t.spaceTransforms), n = g((t) => t.activeSpaceId), d = n ? m[n] : void 0;
13
+ function q(i, c, T) {
14
+ const r = h(), m = g((t) => t.connectedSpaces), x = g((t) => t.spaceTransforms), n = g((t) => t.activeSpaceId), d = n ? m[n] : void 0;
16
15
  I(() => {
17
16
  if (!n || !T) return;
18
17
  const t = r.getState().connectedSpaces[n];
19
18
  if (!t) return;
20
- const p = v(t);
19
+ const l = v(t);
21
20
  let s = !1;
22
- const o = new Set(p);
21
+ const o = new Set(l);
23
22
  if (o.add(n), c) {
24
23
  o.add(c);
25
24
  const e = r.getState().connectedSpaces[c];
@@ -27,9 +26,9 @@ function J(i, c, T) {
27
26
  for (const f of v(e)) o.add(f);
28
27
  }
29
28
  r.getState().pruneConnectedSpaces(o);
30
- const l = p.filter((e) => !r.getState().connectedSpaces[e]), a = /* @__PURE__ */ new Set(), u = () => {
29
+ const p = l.filter((e) => !r.getState().connectedSpaces[e]), a = /* @__PURE__ */ new Set(), u = () => {
31
30
  if (s) return;
32
- const e = l.shift();
31
+ const e = p.shift();
33
32
  if (e) {
34
33
  if (r.getState().connectedSpaces[e]) {
35
34
  u();
@@ -39,15 +38,15 @@ function J(i, c, T) {
39
38
  if (s) return;
40
39
  let S = () => {
41
40
  };
42
- S = y(() => {
41
+ S = w(() => {
43
42
  a.delete(S), !s && (_(f), C(() => r.getState().setConnectedSpace(e, f)), u());
44
43
  }), a.add(S);
45
44
  }).catch((f) => {
46
- N.warn(`Failed to preload connected space ${e}:`, f), s || u();
45
+ y.warn(`Failed to preload connected space ${e}:`, f), s || u();
47
46
  });
48
47
  }
49
48
  };
50
- for (let e = 0; e < R; e++) u();
49
+ for (let e = 0; e < N; e++) u();
51
50
  return () => {
52
51
  s = !0;
53
52
  for (const e of a) e();
@@ -55,17 +54,17 @@ function J(i, c, T) {
55
54
  };
56
55
  }, [n, c, T, i, r]), I(() => {
57
56
  if (!d || !n) return;
58
- const t = b(x[n]) ?? O, p = d.baseLayer.transform;
57
+ const t = A(x[n]) ?? R, l = d.baseLayer.transform;
59
58
  for (const s of d.baseLayer?.connectors ?? [])
60
59
  for (const o of s.connectorInfo.to) {
61
60
  if (o.transitionType !== "seamless" || !o.transform) continue;
62
- const l = m[o.caveWorldId];
63
- if (!l) continue;
64
- const a = h(
61
+ const p = m[o.caveWorldId];
62
+ if (!p) continue;
63
+ const a = b(
65
64
  t,
66
- p,
65
+ l,
67
66
  o.transform,
68
- l.baseLayer.transform
67
+ p.baseLayer.transform
69
68
  );
70
69
  r.getState().setSpaceTransform(o.caveWorldId, a.toArray());
71
70
  }
@@ -73,6 +72,6 @@ function J(i, c, T) {
73
72
  }
74
73
  export {
75
74
  _ as preloadSpaceAssets,
76
- J as useConnectorPreloader
75
+ q as useConnectorPreloader
77
76
  };
78
77
  //# sourceMappingURL=use-connector-preloader.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"use-connector-preloader.js","sources":["../../../explore/hooks/use-connector-preloader.ts"],"sourcesContent":["import { startTransition, useEffect } from 'react'\nimport { useLoader } from '@react-three/fiber'\nimport { Matrix4 } from 'three'\nimport { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader.js'\nimport { createLogger } from '../../core'\nimport type { CaveSpace, Matrix16 } from '../../types'\nimport {\n computeSeamlessGroupMatrix,\n getConnectorTargetIds,\n matrixFromM16\n} from '../math/space-transforms'\nimport { useCaveExplore, useCaveExploreStore } from '../stores/explore-context'\nimport { scheduleIdle } from '../utils/schedule-idle'\nimport type { CaveSpaceLoader } from '../loaders/cave-loader'\n\nconst logger = createLogger('explore:preload')\n\n/**\n * Max neighbor scenes whose heavy mount runs concurrently. Loading 2-4 at once\n * bursts JSON + splat decode/upload + OBJ parse onto the main thread; a small\n * limit serializes them so each lands in its own idle slot.\n */\nconst MAX_CONCURRENT_PRELOADS = 1\n\nconst IDENTITY_MATRIX4 = /* @__PURE__ */ new Matrix4()\n\n/** Warm the loader cache for a space's heavy collider assets. */\nexport function preloadSpaceAssets(space: CaveSpace): void {\n for (const collider of space.baseLayer?.colliders ?? []) {\n if (collider.type === 'model' && collider.url) {\n useLoader.preload(OBJLoader, collider.url)\n }\n }\n}\n\n/**\n * Preload every space referenced by the active space's connectors, and keep\n * seamless neighbors' world-space group matrices up to date.\n *\n * Neighbor downloads are deferred until `primaryInitialized` (main splat +\n * colliders ready) so they do not compete with the primary scene's assets on\n * the same origin, and each neighbor's mount is scheduled in idle time so the\n * burst doesn't stutter player input.\n *\n * The primary space is treated uniformly — it is registered in\n * `connectedSpaces` with its own (initially identity) transform entry, which is\n * what makes seamless back-traversal (A → B → A) work through the same path as\n * forward traversal.\n */\nexport function useConnectorPreloader(\n loader: CaveSpaceLoader,\n primarySpaceId: string | undefined,\n primaryInitialized: boolean\n): void {\n const store = useCaveExploreStore()\n const connectedSpaces = useCaveExplore((s) => s.connectedSpaces)\n const spaceTransforms = useCaveExplore((s) => s.spaceTransforms)\n const activeSpaceId = useCaveExplore((s) => s.activeSpaceId)\n\n const activeSpace = activeSpaceId ? connectedSpaces[activeSpaceId] : undefined\n\n // Effect 1: prune unreachable spaces, then queue throttled idle-scheduled\n // preloads for the active space's neighbors.\n useEffect(() => {\n if (!activeSpaceId || !primaryInitialized) return\n const active = store.getState().connectedSpaces[activeSpaceId]\n if (!active) return\n\n const targetIds = getConnectorTargetIds(active)\n let cancelled = false\n\n // Keep: active + active's direct neighbors + primary + primary's direct\n // neighbors. The primary stays loaded so seamless back-traversal always\n // works, and its other targets stay warm for the common A → B → A → C path.\n const keepIds = new Set<string>(targetIds)\n keepIds.add(activeSpaceId)\n if (primarySpaceId) {\n keepIds.add(primarySpaceId)\n const primary = store.getState().connectedSpaces[primarySpaceId]\n if (primary) {\n for (const id of getConnectorTargetIds(primary)) keepIds.add(id)\n }\n }\n store.getState().pruneConnectedSpaces(keepIds)\n\n const queue = targetIds.filter((id) => !store.getState().connectedSpaces[id])\n const pendingIdles = new Set<() => void>()\n\n const processNext = () => {\n if (cancelled) return\n const id = queue.shift()\n if (!id) return\n if (store.getState().connectedSpaces[id]) {\n processNext()\n return\n }\n loader(id)\n .then((space) => {\n if (cancelled) return\n let cancelIdle: () => void = () => {}\n const run = () => {\n pendingIdles.delete(cancelIdle)\n if (cancelled) return\n preloadSpaceAssets(space)\n startTransition(() => store.getState().setConnectedSpace(id, space))\n processNext()\n }\n cancelIdle = scheduleIdle(run)\n pendingIdles.add(cancelIdle)\n })\n .catch((error: unknown) => {\n logger.warn(`Failed to preload connected space ${id}:`, error)\n if (!cancelled) processNext()\n })\n }\n\n for (let i = 0; i < MAX_CONCURRENT_PRELOADS; i++) processNext()\n\n return () => {\n cancelled = true\n for (const cancel of pendingIdles) cancel()\n pendingIdles.clear()\n }\n // Intentionally keyed on identity only: `connectedSpaces` is read through\n // `store.getState()` so a neighbor landing doesn't restart the queue.\n }, [activeSpaceId, primarySpaceId, primaryInitialized, loader, store])\n\n // Effect 2: recompute seamless group matrices as neighbors become available.\n useEffect(() => {\n if (!activeSpace || !activeSpaceId) return\n\n // G_source: whatever was previously computed for the active space (identity\n // for the primary on first entry; for back-traversal we honour the current\n // value so round-trips stay self-consistent).\n const sourceGroupMatrix = matrixFromM16(spaceTransforms[activeSpaceId]) ?? IDENTITY_MATRIX4\n const sourceTransform = activeSpace.baseLayer.transform\n\n for (const connector of activeSpace.baseLayer?.connectors ?? []) {\n for (const target of connector.connectorInfo.to) {\n if (target.transitionType !== 'seamless' || !target.transform) continue\n const targetSpace = connectedSpaces[target.caveWorldId]\n if (!targetSpace) continue\n\n const groupMatrix = computeSeamlessGroupMatrix(\n sourceGroupMatrix,\n sourceTransform,\n target.transform,\n targetSpace.baseLayer.transform\n )\n store.getState().setSpaceTransform(target.caveWorldId, groupMatrix.toArray() as Matrix16)\n }\n }\n }, [activeSpaceId, activeSpace, connectedSpaces, spaceTransforms, store])\n}\n"],"names":["logger","createLogger","MAX_CONCURRENT_PRELOADS","IDENTITY_MATRIX4","Matrix4","preloadSpaceAssets","space","collider","useLoader","OBJLoader","useConnectorPreloader","loader","primarySpaceId","primaryInitialized","store","useCaveExploreStore","connectedSpaces","useCaveExplore","s","spaceTransforms","activeSpaceId","activeSpace","useEffect","active","targetIds","getConnectorTargetIds","cancelled","keepIds","primary","id","queue","pendingIdles","processNext","cancelIdle","scheduleIdle","startTransition","error","i","cancel","sourceGroupMatrix","matrixFromM16","sourceTransform","connector","target","targetSpace","groupMatrix","computeSeamlessGroupMatrix"],"mappings":";;;;;;;;AAeA,MAAMA,IAASC,EAAa,iBAAiB,GAOvCC,IAA0B,GAE1BC,wBAAuCC,EAAA;AAGtC,SAASC,EAAmBC,GAAwB;AACzD,aAAWC,KAAYD,EAAM,WAAW,aAAa,CAAA;AACnD,IAAIC,EAAS,SAAS,WAAWA,EAAS,OACxCC,EAAU,QAAQC,GAAWF,EAAS,GAAG;AAG/C;AAgBO,SAASG,EACdC,GACAC,GACAC,GACM;AACN,QAAMC,IAAQC,EAAA,GACRC,IAAkBC,EAAe,CAACC,MAAMA,EAAE,eAAe,GACzDC,IAAkBF,EAAe,CAACC,MAAMA,EAAE,eAAe,GACzDE,IAAgBH,EAAe,CAACC,MAAMA,EAAE,aAAa,GAErDG,IAAcD,IAAgBJ,EAAgBI,CAAa,IAAI;AAIrE,EAAAE,EAAU,MAAM;AACd,QAAI,CAACF,KAAiB,CAACP,EAAoB;AAC3C,UAAMU,IAAST,EAAM,SAAA,EAAW,gBAAgBM,CAAa;AAC7D,QAAI,CAACG,EAAQ;AAEb,UAAMC,IAAYC,EAAsBF,CAAM;AAC9C,QAAIG,IAAY;AAKhB,UAAMC,IAAU,IAAI,IAAYH,CAAS;AAEzC,QADAG,EAAQ,IAAIP,CAAa,GACrBR,GAAgB;AAClB,MAAAe,EAAQ,IAAIf,CAAc;AAC1B,YAAMgB,IAAUd,EAAM,SAAA,EAAW,gBAAgBF,CAAc;AAC/D,UAAIgB;AACF,mBAAWC,KAAMJ,EAAsBG,CAAO,EAAG,CAAAD,EAAQ,IAAIE,CAAE;AAAA,IAEnE;AACA,IAAAf,EAAM,SAAA,EAAW,qBAAqBa,CAAO;AAE7C,UAAMG,IAAQN,EAAU,OAAO,CAACK,MAAO,CAACf,EAAM,SAAA,EAAW,gBAAgBe,CAAE,CAAC,GACtEE,wBAAmB,IAAA,GAEnBC,IAAc,MAAM;AACxB,UAAIN,EAAW;AACf,YAAMG,IAAKC,EAAM,MAAA;AACjB,UAAKD,GACL;AAAA,YAAIf,EAAM,SAAA,EAAW,gBAAgBe,CAAE,GAAG;AACxC,UAAAG,EAAA;AACA;AAAA,QACF;AACA,QAAArB,EAAOkB,CAAE,EACN,KAAK,CAACvB,MAAU;AACf,cAAIoB,EAAW;AACf,cAAIO,IAAyB,MAAM;AAAA,UAAC;AAQpC,UAAAA,IAAaC,EAPD,MAAM;AAEhB,YADAH,EAAa,OAAOE,CAAU,GAC1B,CAAAP,MACJrB,EAAmBC,CAAK,GACxB6B,EAAgB,MAAMrB,EAAM,SAAA,EAAW,kBAAkBe,GAAIvB,CAAK,CAAC,GACnE0B,EAAA;AAAA,UACF,CAC6B,GAC7BD,EAAa,IAAIE,CAAU;AAAA,QAC7B,CAAC,EACA,MAAM,CAACG,MAAmB;AACzB,UAAApC,EAAO,KAAK,qCAAqC6B,CAAE,KAAKO,CAAK,GACxDV,KAAWM,EAAA;AAAA,QAClB,CAAC;AAAA;AAAA,IACL;AAEA,aAASK,IAAI,GAAGA,IAAInC,GAAyBmC,IAAK,CAAAL,EAAA;AAElD,WAAO,MAAM;AACX,MAAAN,IAAY;AACZ,iBAAWY,KAAUP,EAAc,CAAAO,EAAA;AACnC,MAAAP,EAAa,MAAA;AAAA,IACf;AAAA,EAGF,GAAG,CAACX,GAAeR,GAAgBC,GAAoBF,GAAQG,CAAK,CAAC,GAGrEQ,EAAU,MAAM;AACd,QAAI,CAACD,KAAe,CAACD,EAAe;AAKpC,UAAMmB,IAAoBC,EAAcrB,EAAgBC,CAAa,CAAC,KAAKjB,GACrEsC,IAAkBpB,EAAY,UAAU;AAE9C,eAAWqB,KAAarB,EAAY,WAAW,cAAc,CAAA;AAC3D,iBAAWsB,KAAUD,EAAU,cAAc,IAAI;AAC/C,YAAIC,EAAO,mBAAmB,cAAc,CAACA,EAAO,UAAW;AAC/D,cAAMC,IAAc5B,EAAgB2B,EAAO,WAAW;AACtD,YAAI,CAACC,EAAa;AAElB,cAAMC,IAAcC;AAAA,UAClBP;AAAA,UACAE;AAAA,UACAE,EAAO;AAAA,UACPC,EAAY,UAAU;AAAA,QAAA;AAExB,QAAA9B,EAAM,WAAW,kBAAkB6B,EAAO,aAAaE,EAAY,SAAqB;AAAA,MAC1F;AAAA,EAEJ,GAAG,CAACzB,GAAeC,GAAaL,GAAiBG,GAAiBL,CAAK,CAAC;AAC1E;"}
1
+ {"version":3,"file":"use-connector-preloader.js","sources":["../../../explore/hooks/use-connector-preloader.ts"],"sourcesContent":["import { startTransition, useEffect } from 'react'\nimport { Matrix4 } from 'three'\nimport { createLogger } from '../../core'\nimport type { CaveSpace, Matrix16 } from '../../types'\nimport { preloadModelCollider } from '../../space/utils/model-collider-format'\nimport {\n computeSeamlessGroupMatrix,\n getConnectorTargetIds,\n matrixFromM16\n} from '../math/space-transforms'\nimport { useCaveExplore, useCaveExploreStore } from '../stores/explore-context'\nimport { scheduleIdle } from '../utils/schedule-idle'\nimport type { CaveSpaceLoader } from '../loaders/cave-loader'\n\nconst logger = createLogger('explore:preload')\n\n/**\n * Max neighbor scenes whose heavy mount runs concurrently. Loading 2-4 at once\n * bursts JSON + splat decode/upload + OBJ/PLY parse onto the main thread; a small\n * limit serializes them so each lands in its own idle slot.\n */\nconst MAX_CONCURRENT_PRELOADS = 1\n\nconst IDENTITY_MATRIX4 = /* @__PURE__ */ new Matrix4()\n\n/** Warm the loader cache for a space's heavy collider assets. */\nexport function preloadSpaceAssets(space: CaveSpace): void {\n for (const collider of space.baseLayer?.colliders ?? []) {\n if (collider.type === 'model' && collider.url) {\n preloadModelCollider(collider.url)\n }\n }\n}\n\n/**\n * Preload every space referenced by the active space's connectors, and keep\n * seamless neighbors' world-space group matrices up to date.\n *\n * Neighbor downloads are deferred until `primaryInitialized` (main splat +\n * colliders ready) so they do not compete with the primary scene's assets on\n * the same origin, and each neighbor's mount is scheduled in idle time so the\n * burst doesn't stutter player input.\n *\n * The primary space is treated uniformly — it is registered in\n * `connectedSpaces` with its own (initially identity) transform entry, which is\n * what makes seamless back-traversal (A → B → A) work through the same path as\n * forward traversal.\n */\nexport function useConnectorPreloader(\n loader: CaveSpaceLoader,\n primarySpaceId: string | undefined,\n primaryInitialized: boolean\n): void {\n const store = useCaveExploreStore()\n const connectedSpaces = useCaveExplore((s) => s.connectedSpaces)\n const spaceTransforms = useCaveExplore((s) => s.spaceTransforms)\n const activeSpaceId = useCaveExplore((s) => s.activeSpaceId)\n\n const activeSpace = activeSpaceId ? connectedSpaces[activeSpaceId] : undefined\n\n // Effect 1: prune unreachable spaces, then queue throttled idle-scheduled\n // preloads for the active space's neighbors.\n useEffect(() => {\n if (!activeSpaceId || !primaryInitialized) return\n const active = store.getState().connectedSpaces[activeSpaceId]\n if (!active) return\n\n const targetIds = getConnectorTargetIds(active)\n let cancelled = false\n\n // Keep: active + active's direct neighbors + primary + primary's direct\n // neighbors. The primary stays loaded so seamless back-traversal always\n // works, and its other targets stay warm for the common A → B → A → C path.\n const keepIds = new Set<string>(targetIds)\n keepIds.add(activeSpaceId)\n if (primarySpaceId) {\n keepIds.add(primarySpaceId)\n const primary = store.getState().connectedSpaces[primarySpaceId]\n if (primary) {\n for (const id of getConnectorTargetIds(primary)) keepIds.add(id)\n }\n }\n store.getState().pruneConnectedSpaces(keepIds)\n\n const queue = targetIds.filter((id) => !store.getState().connectedSpaces[id])\n const pendingIdles = new Set<() => void>()\n\n const processNext = () => {\n if (cancelled) return\n const id = queue.shift()\n if (!id) return\n if (store.getState().connectedSpaces[id]) {\n processNext()\n return\n }\n loader(id)\n .then((space) => {\n if (cancelled) return\n let cancelIdle: () => void = () => {}\n const run = () => {\n pendingIdles.delete(cancelIdle)\n if (cancelled) return\n preloadSpaceAssets(space)\n startTransition(() => store.getState().setConnectedSpace(id, space))\n processNext()\n }\n cancelIdle = scheduleIdle(run)\n pendingIdles.add(cancelIdle)\n })\n .catch((error: unknown) => {\n logger.warn(`Failed to preload connected space ${id}:`, error)\n if (!cancelled) processNext()\n })\n }\n\n for (let i = 0; i < MAX_CONCURRENT_PRELOADS; i++) processNext()\n\n return () => {\n cancelled = true\n for (const cancel of pendingIdles) cancel()\n pendingIdles.clear()\n }\n // Intentionally keyed on identity only: `connectedSpaces` is read through\n // `store.getState()` so a neighbor landing doesn't restart the queue.\n }, [activeSpaceId, primarySpaceId, primaryInitialized, loader, store])\n\n // Effect 2: recompute seamless group matrices as neighbors become available.\n useEffect(() => {\n if (!activeSpace || !activeSpaceId) return\n\n // G_source: whatever was previously computed for the active space (identity\n // for the primary on first entry; for back-traversal we honour the current\n // value so round-trips stay self-consistent).\n const sourceGroupMatrix = matrixFromM16(spaceTransforms[activeSpaceId]) ?? IDENTITY_MATRIX4\n const sourceTransform = activeSpace.baseLayer.transform\n\n for (const connector of activeSpace.baseLayer?.connectors ?? []) {\n for (const target of connector.connectorInfo.to) {\n if (target.transitionType !== 'seamless' || !target.transform) continue\n const targetSpace = connectedSpaces[target.caveWorldId]\n if (!targetSpace) continue\n\n const groupMatrix = computeSeamlessGroupMatrix(\n sourceGroupMatrix,\n sourceTransform,\n target.transform,\n targetSpace.baseLayer.transform\n )\n store.getState().setSpaceTransform(target.caveWorldId, groupMatrix.toArray() as Matrix16)\n }\n }\n }, [activeSpaceId, activeSpace, connectedSpaces, spaceTransforms, store])\n}\n"],"names":["logger","createLogger","MAX_CONCURRENT_PRELOADS","IDENTITY_MATRIX4","Matrix4","preloadSpaceAssets","space","collider","preloadModelCollider","useConnectorPreloader","loader","primarySpaceId","primaryInitialized","store","useCaveExploreStore","connectedSpaces","useCaveExplore","s","spaceTransforms","activeSpaceId","activeSpace","useEffect","active","targetIds","getConnectorTargetIds","cancelled","keepIds","primary","id","queue","pendingIdles","processNext","cancelIdle","scheduleIdle","startTransition","error","i","cancel","sourceGroupMatrix","matrixFromM16","sourceTransform","connector","target","targetSpace","groupMatrix","computeSeamlessGroupMatrix"],"mappings":"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"}
@@ -3,7 +3,7 @@
3
3
  * where `requestIdleCallback` is unavailable (Safari < 17).
4
4
  *
5
5
  * Used to push the heavy parts of a neighbor-scene preload (subtree mount,
6
- * splat decode/upload, OBJ collider parse) off the critical frame so they don't
6
+ * splat decode/upload, OBJ/PLY collider parse) off the critical frame so they don't
7
7
  * burst against player input the moment the primary scene becomes ready.
8
8
  *
9
9
  * @returns a cancel function; safe to call after the task has already run.
@@ -1 +1 @@
1
- {"version":3,"file":"schedule-idle.js","sources":["../../../explore/utils/schedule-idle.ts"],"sourcesContent":["interface IdleCapableWindow {\n requestIdleCallback?: (callback: () => void, options?: { timeout?: number }) => number\n cancelIdleCallback?: (handle: number) => void\n}\n\n/**\n * Run low-priority work in browser idle time, falling back to a macrotask\n * where `requestIdleCallback` is unavailable (Safari < 17).\n *\n * Used to push the heavy parts of a neighbor-scene preload (subtree mount,\n * splat decode/upload, OBJ collider parse) off the critical frame so they don't\n * burst against player input the moment the primary scene becomes ready.\n *\n * @returns a cancel function; safe to call after the task has already run.\n */\nexport function scheduleIdle(task: () => void, timeoutMs = 1000): () => void {\n if (typeof window === 'undefined') {\n task()\n return () => {}\n }\n\n const idleWindow = window as unknown as IdleCapableWindow\n if (typeof idleWindow.requestIdleCallback === 'function') {\n const handle = idleWindow.requestIdleCallback(task, { timeout: timeoutMs })\n return () => idleWindow.cancelIdleCallback?.(handle)\n }\n\n const handle = window.setTimeout(task, 1)\n return () => window.clearTimeout(handle)\n}\n"],"names":["scheduleIdle","task","timeoutMs","idleWindow","handle"],"mappings":"AAeO,SAASA,EAAaC,GAAkBC,IAAY,KAAkB;AAC3E,MAAI,OAAO,SAAW;AACpB,WAAAD,EAAA,GACO,MAAM;AAAA,IAAC;AAGhB,QAAME,IAAa;AACnB,MAAI,OAAOA,EAAW,uBAAwB,YAAY;AACxD,UAAMC,IAASD,EAAW,oBAAoBF,GAAM,EAAE,SAASC,GAAW;AAC1E,WAAO,MAAMC,EAAW,qBAAqBC,CAAM;AAAA,EACrD;AAEA,QAAMA,IAAS,OAAO,WAAWH,GAAM,CAAC;AACxC,SAAO,MAAM,OAAO,aAAaG,CAAM;AACzC;"}
1
+ {"version":3,"file":"schedule-idle.js","sources":["../../../explore/utils/schedule-idle.ts"],"sourcesContent":["interface IdleCapableWindow {\n requestIdleCallback?: (callback: () => void, options?: { timeout?: number }) => number\n cancelIdleCallback?: (handle: number) => void\n}\n\n/**\n * Run low-priority work in browser idle time, falling back to a macrotask\n * where `requestIdleCallback` is unavailable (Safari < 17).\n *\n * Used to push the heavy parts of a neighbor-scene preload (subtree mount,\n * splat decode/upload, OBJ/PLY collider parse) off the critical frame so they don't\n * burst against player input the moment the primary scene becomes ready.\n *\n * @returns a cancel function; safe to call after the task has already run.\n */\nexport function scheduleIdle(task: () => void, timeoutMs = 1000): () => void {\n if (typeof window === 'undefined') {\n task()\n return () => {}\n }\n\n const idleWindow = window as unknown as IdleCapableWindow\n if (typeof idleWindow.requestIdleCallback === 'function') {\n const handle = idleWindow.requestIdleCallback(task, { timeout: timeoutMs })\n return () => idleWindow.cancelIdleCallback?.(handle)\n }\n\n const handle = window.setTimeout(task, 1)\n return () => window.clearTimeout(handle)\n}\n"],"names":["scheduleIdle","task","timeoutMs","idleWindow","handle"],"mappings":"AAeO,SAASA,EAAaC,GAAkBC,IAAY,KAAkB;AAC3E,MAAI,OAAO,SAAW;AACpB,WAAAD,EAAA,GACO,MAAM;AAAA,IAAC;AAGhB,QAAME,IAAa;AACnB,MAAI,OAAOA,EAAW,uBAAwB,YAAY;AACxD,UAAMC,IAASD,EAAW,oBAAoBF,GAAM,EAAE,SAASC,GAAW;AAC1E,WAAO,MAAMC,EAAW,qBAAqBC,CAAM;AAAA,EACrD;AAEA,QAAMA,IAAS,OAAO,WAAWH,GAAM,CAAC;AACxC,SAAO,MAAM,OAAO,aAAaG,CAAM;AACzC;"}
@@ -1,6 +1,6 @@
1
1
  import { type ReactNode, type RefObject } from 'react';
2
2
  import { type ThreeEvent } from '@react-three/fiber';
3
- import { type Group, type Object3DEventMap } from 'three';
3
+ import { Group, type Object3DEventMap } from 'three';
4
4
  import type { CaveSpace } from '../types/index.js';
5
5
  export interface CaveBaseLayerProps {
6
6
  space?: CaveSpace;
@@ -1 +1 @@
1
- {"version":3,"file":"cave-base-layer.d.ts","sourceRoot":"","sources":["../../space/cave-base-layer.tsx"],"names":[],"mappings":"AAAA,OAAO,EAQL,KAAK,SAAS,EACd,KAAK,SAAS,EACf,MAAM,OAAO,CAAA;AACd,OAAO,EAAa,KAAK,UAAU,EAAE,MAAM,oBAAoB,CAAA;AAG/D,OAAO,EAAqB,KAAK,KAAK,EAAE,KAAK,gBAAgB,EAAE,MAAM,OAAO,CAAA;AAG5E,OAAO,KAAK,EAAE,SAAS,EAA+B,MAAM,UAAU,CAAA;AA2GtE,MAAM,WAAW,kBAAkB;IACjC,KAAK,CAAC,EAAE,SAAS,CAAA;IACjB,4DAA4D;IAC5D,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;IACrC,aAAa,CAAC,EAAE,MAAM,IAAI,CAAA;IAC1B,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,CAAC,UAAU,CAAC,KAAK,IAAI,CAAA;IACjD,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,CAAC,UAAU,CAAC,KAAK,IAAI,CAAA;IACvD,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB;;;;OAIG;IACH,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,oEAAoE;IACpE,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAA;IAC3C,4EAA4E;IAC5E,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,CAAA;IACpC,uDAAuD;IACvD,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,iDAAiD;IACjD,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB;;;;OAIG;IACH,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,QAAQ,CAAC,EAAE,SAAS,CAAA;IACpB,GAAG,CAAC,EAAE,SAAS,CAAC,KAAK,CAAC,gBAAgB,CAAC,CAAC,CAAA;CACzC;AAED;;;GAGG;AACH,MAAM,CAAC,OAAO,UAAU,aAAa,CAAC,KAAK,EAAE,kBAAkB,sCA6O9D"}
1
+ {"version":3,"file":"cave-base-layer.d.ts","sourceRoot":"","sources":["../../space/cave-base-layer.tsx"],"names":[],"mappings":"AAAA,OAAO,EAQL,KAAK,SAAS,EACd,KAAK,SAAS,EACf,MAAM,OAAO,CAAA;AACd,OAAO,EAAa,KAAK,UAAU,EAAE,MAAM,oBAAoB,CAAA;AAE/D,OAAO,EAAE,KAAK,EAA2B,KAAK,gBAAgB,EAAE,MAAM,OAAO,CAAA;AAI7E,OAAO,KAAK,EAAE,SAAS,EAA+B,MAAM,UAAU,CAAA;AAwItE,MAAM,WAAW,kBAAkB;IACjC,KAAK,CAAC,EAAE,SAAS,CAAA;IACjB,4DAA4D;IAC5D,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;IACrC,aAAa,CAAC,EAAE,MAAM,IAAI,CAAA;IAC1B,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,CAAC,UAAU,CAAC,KAAK,IAAI,CAAA;IACjD,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,CAAC,UAAU,CAAC,KAAK,IAAI,CAAA;IACvD,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB;;;;OAIG;IACH,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,oEAAoE;IACpE,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAA;IAC3C,4EAA4E;IAC5E,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,CAAA;IACpC,uDAAuD;IACvD,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,iDAAiD;IACjD,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB;;;;OAIG;IACH,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,QAAQ,CAAC,EAAE,SAAS,CAAA;IACpB,GAAG,CAAC,EAAE,SAAS,CAAC,KAAK,CAAC,gBAAgB,CAAC,CAAC,CAAA;CACzC;AAED;;;GAGG;AACH,MAAM,CAAC,OAAO,UAAU,aAAa,CAAC,KAAK,EAAE,kBAAkB,sCA6O9D"}
@@ -1,141 +1,155 @@
1
- import { jsxs as C, jsx as t } from "react/jsx-runtime";
2
- import { useState as N, useMemo as x, useEffect as S, startTransition as j, useRef as m, useCallback as i, Suspense as w } from "react";
3
- import { useLoader as F } from "@react-three/fiber";
4
- import { BvhPhysicsBody as b, BvhPhysicsSensor as H } from "@react-three/viverse";
5
- import { MeshBasicMaterial as J } from "three";
6
- import { OBJLoader as V } from "three/examples/jsm/loaders/OBJLoader.js";
7
- import { useCaveEventBus as W, useCaveWorldStore as K } from "../core/context/cave-world-context.js";
8
- import X from "./connector-label-host.js";
9
- import { PolygonBoundaryColliders as Y, PolygonBoundaryFence as Z } from "./polygon-boundary.js";
10
- import ee from "./splats/spark/robust-splat-mesh.js";
11
- import { toQuaternionRotation as A } from "./utils/rotation-compat.js";
12
- import { selectSplatLevel as te } from "./utils/select-splat-level.js";
13
- import { createSplatCollider as re } from "./utils/splat-collider-helper.js";
14
- const G = [1, 0, 0, 0], oe = 1e5, ne = /* @__PURE__ */ new J({ visible: !1 });
15
- function se({
16
- connector: o,
17
- onIntersection: r
1
+ import { jsxs as S, jsx as o } from "react/jsx-runtime";
2
+ import { useState as A, useMemo as L, useEffect as d, startTransition as G, useRef as p, useCallback as i, Suspense as J } from "react";
3
+ import { useLoader as N } from "@react-three/fiber";
4
+ import { BvhPhysicsBody as b, BvhPhysicsSensor as V } from "@react-three/viverse";
5
+ import { Group as W, Mesh as Y, MeshBasicMaterial as K } from "three";
6
+ import { OBJLoader as X } from "three/examples/jsm/loaders/OBJLoader.js";
7
+ import { PLYLoader as Z } from "three/examples/jsm/loaders/PLYLoader.js";
8
+ import { useCaveEventBus as ee, useCaveWorldStore as re } from "../core/context/cave-world-context.js";
9
+ import te from "./connector-label-host.js";
10
+ import { PolygonBoundaryColliders as oe, PolygonBoundaryFence as ne } from "./polygon-boundary.js";
11
+ import le from "./splats/spark/robust-splat-mesh.js";
12
+ import { getModelColliderFormat as se } from "./utils/model-collider-format.js";
13
+ import { toQuaternionRotation as O } from "./utils/rotation-compat.js";
14
+ import { selectSplatLevel as ie } from "./utils/select-splat-level.js";
15
+ import { createSplatCollider as ae } from "./utils/splat-collider-helper.js";
16
+ const T = [1, 0, 0, 0], ce = 1e5, _ = /* @__PURE__ */ new K({ visible: !1 });
17
+ function ue({
18
+ connector: r,
19
+ onIntersection: t
18
20
  }) {
19
- const s = i(
20
- (n) => r(o, n),
21
- [o, r]
21
+ const l = i(
22
+ (n) => t(r, n),
23
+ [r, t]
22
24
  );
23
- return /* @__PURE__ */ t(H, { onIntersectedChanged: s, children: /* @__PURE__ */ C(
25
+ return /* @__PURE__ */ o(V, { onIntersectedChanged: l, children: /* @__PURE__ */ S(
24
26
  "mesh",
25
27
  {
26
- position: o.transform.position,
27
- rotation: o.transform.rotation,
28
- scale: o.transform.scale,
28
+ position: r.transform.position,
29
+ rotation: r.transform.rotation,
30
+ scale: r.transform.scale,
29
31
  children: [
30
- /* @__PURE__ */ t("boxGeometry", {}),
31
- /* @__PURE__ */ t("meshStandardMaterial", { visible: !1 })
32
+ /* @__PURE__ */ o("boxGeometry", {}),
33
+ /* @__PURE__ */ o("meshStandardMaterial", { visible: !1 })
32
34
  ]
33
35
  }
34
36
  ) });
35
37
  }
36
- function le({
37
- collider: o,
38
- onLoaded: r
38
+ function w(r) {
39
+ return r.traverse((t) => {
40
+ t.isMesh && (t.material = _);
41
+ }), r.rotation.set(-Math.PI / 2, 0, 0), r;
42
+ }
43
+ function de({ url: r, onLoaded: t }) {
44
+ const l = N(X, r), n = L(() => w(l.clone(!0)), [l]);
45
+ return d(() => {
46
+ t();
47
+ }, [t]), /* @__PURE__ */ o("primitive", { object: n });
48
+ }
49
+ function pe({ url: r, onLoaded: t }) {
50
+ const l = N(Z, r), n = L(() => {
51
+ const s = new W();
52
+ return s.add(new Y(l, _)), w(s);
53
+ }, [l]);
54
+ return d(() => {
55
+ t();
56
+ }, [t]), /* @__PURE__ */ o("primitive", { object: n });
57
+ }
58
+ function me({
59
+ collider: r,
60
+ onLoaded: t
39
61
  }) {
40
- const s = F(V, o.url), n = x(() => {
41
- const l = s.clone(!0);
42
- return l.traverse((a) => {
43
- a.isMesh && (a.material = ne);
44
- }), l.rotation.set(-Math.PI / 2, 0, 0), l;
45
- }, [s]);
46
- return S(() => {
47
- r();
48
- }, [r]), /* @__PURE__ */ t("primitive", { object: n });
62
+ return se(r.url) === "ply" ? /* @__PURE__ */ o(pe, { url: r.url, onLoaded: t }) : /* @__PURE__ */ o(de, { url: r.url, onLoaded: t });
49
63
  }
50
- function ae(o, r) {
51
- const s = m(!1), n = m(0), l = m(!1), [a, L] = N(!1), p = r ? o : 0, c = i(() => {
52
- l.current || s.current && n.current >= p && (l.current = !0, L(!0));
53
- }, [p]), v = i(() => {
54
- s.current = !0, c();
55
- }, [c]), I = i(() => {
56
- n.current += 1, c();
57
- }, [c]);
58
- return { ready: a, markSceneReady: v, markColliderReady: I };
64
+ function fe(r, t) {
65
+ const l = p(!1), n = p(0), s = p(!1), [m, v] = A(!1), f = t ? r : 0, a = i(() => {
66
+ s.current || l.current && n.current >= f && (s.current = !0, v(!0));
67
+ }, [f]), M = i(() => {
68
+ l.current = !0, a();
69
+ }, [a]), g = i(() => {
70
+ n.current += 1, a();
71
+ }, [a]);
72
+ return { ready: m, markSceneReady: M, markColliderReady: g };
59
73
  }
60
- function Ie(o) {
74
+ function je(r) {
61
75
  const {
62
- space: r,
63
- spaceId: s,
76
+ space: t,
77
+ spaceId: l,
64
78
  active: n = !0,
65
- revealOnEnter: l = !1,
66
- onInitialized: a,
67
- ref: L,
68
- connectedSpaces: p = {},
69
- readySpaceIds: c = {},
70
- activeConnectorId: v,
71
- connectorLabels: I = !0
72
- } = o, u = o.physics ?? n, [d, O] = N(null), f = W(), E = K((e) => e.setSpaceLoadError), _ = x(
73
- () => r?.baseLayer?.colliders?.filter((e) => e.type === "model").length ?? 0,
74
- [r?.baseLayer?.colliders]
75
- ), { ready: M, markSceneReady: R, markColliderReady: D } = ae(_, u);
76
- S(() => {
77
- M && j(() => {
78
- f.emit("space:initialized", {}), a?.();
79
+ revealOnEnter: s = !1,
80
+ onInitialized: m,
81
+ ref: v,
82
+ connectedSpaces: f = {},
83
+ readySpaceIds: a = {},
84
+ activeConnectorId: M,
85
+ connectorLabels: g = !0
86
+ } = r, c = r.physics ?? n, [u, D] = A(null), h = ee(), x = re((e) => e.setSpaceLoadError), F = L(
87
+ () => t?.baseLayer?.colliders?.filter((e) => e.type === "model").length ?? 0,
88
+ [t?.baseLayer?.colliders]
89
+ ), { ready: E, markSceneReady: R, markColliderReady: U } = fe(F, c);
90
+ d(() => {
91
+ E && G(() => {
92
+ h.emit("space:initialized", {}), m?.();
79
93
  });
80
- }, [M, f, a]), S(() => {
81
- if (d)
94
+ }, [E, h, m]), d(() => {
95
+ if (u)
82
96
  return () => {
83
- d.geometry.dispose();
97
+ u.geometry.dispose();
84
98
  };
85
- }, [d]);
86
- const k = m(() => {
87
- }), B = m(!1);
88
- S(() => {
99
+ }, [u]);
100
+ const k = p(() => {
101
+ }), B = p(!1);
102
+ d(() => {
89
103
  k.current = (e) => {
90
- j(() => {
91
- !B.current && !r?.baseLayer?.colliders?.length && (B.current = !0, O(re(e, oe))), R();
104
+ G(() => {
105
+ !B.current && !t?.baseLayer?.colliders?.length && (B.current = !0, D(ae(e, ce))), R();
92
106
  });
93
107
  };
94
- }, [r?.baseLayer?.colliders, R]);
95
- const U = i((e) => {
108
+ }, [t?.baseLayer?.colliders, R]);
109
+ const z = i((e) => {
96
110
  k.current(e);
97
- }, []), h = x(
98
- () => te(r?.baseLayer?.scene),
99
- [r?.baseLayer?.scene]
100
- )?.url, z = i(
111
+ }, []), y = L(
112
+ () => ie(t?.baseLayer?.scene),
113
+ [t?.baseLayer?.scene]
114
+ )?.url, Q = i(
101
115
  (e) => {
102
- E({ stage: "splat", url: h, spaceId: s, error: e });
116
+ x({ stage: "splat", url: y, spaceId: l, error: e });
103
117
  },
104
- [E, h, s]
105
- ), Q = i(
106
- (e, y) => {
107
- f.emit(y ? "connector:enter" : "connector:leave", { connector: e });
118
+ [x, y, l]
119
+ ), $ = i(
120
+ (e, C) => {
121
+ h.emit(C ? "connector:enter" : "connector:leave", { connector: e });
108
122
  },
109
- [f]
123
+ [h]
110
124
  );
111
- if (!r?.baseLayer) return null;
112
- const { transform: g, scene: $, colliders: P, boundaries: q, connectors: T } = r.baseLayer;
113
- return /* @__PURE__ */ C(
125
+ if (!t?.baseLayer) return null;
126
+ const { transform: I, scene: H, colliders: P, boundaries: j, connectors: q } = t.baseLayer;
127
+ return /* @__PURE__ */ S(
114
128
  "group",
115
129
  {
116
130
  name: "base-layer",
117
- position: g.position,
118
- quaternion: g.rotation,
119
- scale: g.scale,
120
- ref: L,
131
+ position: I.position,
132
+ quaternion: I.rotation,
133
+ scale: I.scale,
134
+ ref: v,
121
135
  children: [
122
- $?.type === "splat" && h ? /* @__PURE__ */ t(
123
- ee,
136
+ H?.type === "splat" && y ? /* @__PURE__ */ o(
137
+ le,
124
138
  {
125
- url: h,
139
+ url: y,
126
140
  visible: n,
127
- quaternion: G,
128
- spaceId: s,
129
- revealOnEnter: l,
130
- onLoad: U,
131
- onError: n ? z : void 0,
132
- onClick: o.onClick,
133
- onDoubleClick: o.onDoubleClick
141
+ quaternion: T,
142
+ spaceId: l,
143
+ revealOnEnter: s,
144
+ onLoad: z,
145
+ onError: n ? Q : void 0,
146
+ onClick: r.onClick,
147
+ onDoubleClick: r.onDoubleClick
134
148
  }
135
149
  ) : null,
136
150
  P?.map((e) => {
137
151
  if (e.type !== "model" || !e.url) return null;
138
- const y = /* @__PURE__ */ t(le, { collider: e, onLoaded: D });
152
+ const C = /* @__PURE__ */ o(me, { collider: e, onLoaded: U });
139
153
  return (
140
154
  // Each collider gets its own boundary. `useLoader` suspends, and
141
155
  // without a boundary here the suspension unwinds to the viewer's,
@@ -146,44 +160,44 @@ function Ie(o) {
146
160
  // The boundary must stay *outside* `BvhPhysicsBody`: it snapshots its
147
161
  // children into the BVH world in a mount effect, so a child that
148
162
  // arrives later is never registered as a collider.
149
- /* @__PURE__ */ t(w, { fallback: null, children: u ? /* @__PURE__ */ t(b, { children: y }) : (
163
+ /* @__PURE__ */ o(J, { fallback: null, children: c ? /* @__PURE__ */ o(b, { children: C }) : (
150
164
  // Hidden preloads still load collider assets (to satisfy the init
151
165
  // gate) but skip BVH registration.
152
- /* @__PURE__ */ t("group", { visible: !1, children: y })
166
+ /* @__PURE__ */ o("group", { visible: !1, children: C })
153
167
  ) }, e.id)
154
168
  );
155
169
  }),
156
- u ? /* @__PURE__ */ t(b, { children: P?.map(
157
- (e) => e.type === "box" ? /* @__PURE__ */ C(
170
+ c ? /* @__PURE__ */ o(b, { children: P?.map(
171
+ (e) => e.type === "box" ? /* @__PURE__ */ S(
158
172
  "mesh",
159
173
  {
160
174
  position: e.box.position,
161
- quaternion: A(e.box.rotation),
175
+ quaternion: O(e.box.rotation),
162
176
  scale: e.box.scale,
163
177
  children: [
164
- /* @__PURE__ */ t("boxGeometry", {}),
165
- /* @__PURE__ */ t("meshStandardMaterial", { visible: !1 })
178
+ /* @__PURE__ */ o("boxGeometry", {}),
179
+ /* @__PURE__ */ o("meshStandardMaterial", { visible: !1 })
166
180
  ]
167
181
  },
168
182
  e.id
169
183
  ) : null
170
184
  ) }) : null,
171
- u ? /* @__PURE__ */ t(b, { children: q?.map((e) => e.type === "box" && e.collisionEnabled !== !1 ? /* @__PURE__ */ C(
185
+ c ? /* @__PURE__ */ o(b, { children: j?.map((e) => e.type === "box" && e.collisionEnabled !== !1 ? /* @__PURE__ */ S(
172
186
  "mesh",
173
187
  {
174
188
  position: e.box.position,
175
- quaternion: A(e.box.rotation),
189
+ quaternion: O(e.box.rotation),
176
190
  scale: e.box.scale,
177
191
  children: [
178
- /* @__PURE__ */ t("boxGeometry", {}),
179
- /* @__PURE__ */ t("meshStandardMaterial", { visible: !1 })
192
+ /* @__PURE__ */ o("boxGeometry", {}),
193
+ /* @__PURE__ */ o("meshStandardMaterial", { visible: !1 })
180
194
  ]
181
195
  },
182
196
  e.id
183
- ) : e.type === "polygon" ? /* @__PURE__ */ t(Y, { boundary: e }, e.id) : null) }) : null,
184
- n ? q?.map(
185
- (e) => e.type === "polygon" ? /* @__PURE__ */ t(
186
- Z,
197
+ ) : e.type === "polygon" ? /* @__PURE__ */ o(oe, { boundary: e }, e.id) : null) }) : null,
198
+ n ? j?.map(
199
+ (e) => e.type === "polygon" ? /* @__PURE__ */ o(
200
+ ne,
187
201
  {
188
202
  boundary: e,
189
203
  active: n
@@ -191,31 +205,31 @@ function Ie(o) {
191
205
  `polygon-vis-${e.id}`
192
206
  ) : null
193
207
  ) : null,
194
- n ? T?.map((e) => /* @__PURE__ */ t(
195
- se,
208
+ n ? q?.map((e) => /* @__PURE__ */ o(
209
+ ue,
196
210
  {
197
211
  connector: e,
198
- onIntersection: Q
212
+ onIntersection: $
199
213
  },
200
214
  e.id
201
215
  )) : null,
202
- n && I ? T?.map((e) => /* @__PURE__ */ t(
203
- X,
216
+ n && g ? q?.map((e) => /* @__PURE__ */ o(
217
+ te,
204
218
  {
205
219
  connector: e,
206
- connectedSpaces: p,
207
- readySpaceIds: c,
208
- activeConnectorId: v
220
+ connectedSpaces: f,
221
+ readySpaceIds: a,
222
+ activeConnectorId: M
209
223
  },
210
224
  `label-${e.id}`
211
225
  )) : null,
212
- u && d ? /* @__PURE__ */ t(b, { children: /* @__PURE__ */ t("group", { children: /* @__PURE__ */ t("primitive", { object: d, quaternion: G }) }) }) : null,
213
- o.children ?? null
226
+ c && u ? /* @__PURE__ */ o(b, { children: /* @__PURE__ */ o("group", { children: /* @__PURE__ */ o("primitive", { object: u, quaternion: T }) }) }) : null,
227
+ r.children ?? null
214
228
  ]
215
229
  }
216
230
  );
217
231
  }
218
232
  export {
219
- Ie as default
233
+ je as default
220
234
  };
221
235
  //# sourceMappingURL=cave-base-layer.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"cave-base-layer.js","sources":["../../space/cave-base-layer.tsx"],"sourcesContent":["import {\n startTransition,\n Suspense,\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n type ReactNode,\n type RefObject\n} from 'react'\nimport { useLoader, type ThreeEvent } from '@react-three/fiber'\nimport { BvhPhysicsBody, BvhPhysicsSensor } from '@react-three/viverse'\nimport type { Mesh } from 'three'\nimport { MeshBasicMaterial, type Group, type Object3DEventMap } from 'three'\nimport { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader.js'\nimport { useCaveEventBus, useCaveWorldStore } from '../core'\nimport type { CaveSpace, ConnectorDef, ModelCollider } from '../types'\nimport ConnectorLabelHost from './connector-label-host'\nimport { PolygonBoundaryColliders, PolygonBoundaryFence } from './polygon-boundary'\nimport RobustSplatMesh from './splats/spark/robust-splat-mesh'\nimport { type SparkSplatMesh } from './splats/spark/splat-mesh'\nimport { toQuaternionRotation } from './utils/rotation-compat'\nimport { selectSplatLevel } from './utils/select-splat-level'\nimport { createSplatCollider } from './utils/splat-collider-helper'\n\n/** Identity quaternion as `[x, y, z, w]` for the R3F `quaternion` prop. */\nconst INIT_QUATERNION: [number, number, number, number] = [1, 0, 0, 0]\n\n/** Target triangle budget when deriving a collider from splat geometry. */\nconst SPLAT_COLLIDER_SAMPLE_COUNT = 100_000\n\nconst INVISIBLE_MATERIAL = /* @__PURE__ */ new MeshBasicMaterial({ visible: false })\n\nfunction ConnectorSensor({\n connector,\n onIntersection\n}: {\n connector: ConnectorDef\n onIntersection: (connector: ConnectorDef, intersected: boolean) => void\n}) {\n const handleChange = useCallback(\n (intersected: boolean) => onIntersection(connector, intersected),\n [connector, onIntersection]\n )\n\n return (\n <BvhPhysicsSensor onIntersectedChanged={handleChange}>\n <mesh\n position={connector.transform.position}\n rotation={connector.transform.rotation}\n scale={connector.transform.scale}\n >\n <boxGeometry />\n <meshStandardMaterial visible={false} />\n </mesh>\n </BvhPhysicsSensor>\n )\n}\n\nfunction ModelColliderMesh({\n collider,\n onLoaded\n}: {\n collider: ModelCollider\n onLoaded: () => void\n}) {\n const obj = useLoader(OBJLoader, collider.url)\n\n const cloned = useMemo(() => {\n const group = obj.clone(true)\n group.traverse((child) => {\n if ((child as Mesh).isMesh) {\n ;(child as Mesh).material = INVISIBLE_MATERIAL\n }\n })\n // Collider OBJs are authored Z-up; Three.js is Y-up.\n group.rotation.set(-Math.PI / 2, 0, 0)\n return group\n }, [obj])\n\n useEffect(() => {\n onLoaded()\n }, [onLoaded])\n\n return <primitive object={cloned} />\n}\n\n/**\n * Track readiness of the scene (splat) and every model collider, firing\n * `onReady` exactly once when all of them have loaded.\n *\n * Counters live in refs rather than state so each collider landing doesn't\n * re-render the base layer; only the final flip publishes to React.\n */\nfunction useInitGate(modelColliderCount: number, requireModelColliders: boolean) {\n const sceneReady = useRef(false)\n const loadedColliders = useRef(0)\n const fired = useRef(false)\n const [ready, setReady] = useState(false)\n\n const requiredColliders = requireModelColliders ? modelColliderCount : 0\n\n const checkReady = useCallback(() => {\n if (fired.current) return\n if (sceneReady.current && loadedColliders.current >= requiredColliders) {\n fired.current = true\n setReady(true)\n }\n }, [requiredColliders])\n\n const markSceneReady = useCallback(() => {\n sceneReady.current = true\n checkReady()\n }, [checkReady])\n\n const markColliderReady = useCallback(() => {\n loadedColliders.current += 1\n checkReady()\n }, [checkReady])\n\n return { ready, markSceneReady, markColliderReady }\n}\n\nexport interface CaveBaseLayerProps {\n space?: CaveSpace\n /** Space id, threaded down for splat load-error context. */\n spaceId?: string\n onProcess?: (percent: number) => void\n onInitialized?: () => void\n onClick?: (event: ThreeEvent<MouseEvent>) => void\n onDoubleClick?: (event: ThreeEvent<MouseEvent>) => void\n physics?: boolean\n /**\n * When `false` this base layer is preloaded-but-not-current: connector\n * sensors don't mount (no spurious `connector:enter` from an adjacent scene\n * occupying the same world coordinates) and physics defaults off.\n */\n active?: boolean\n /** Connected space configs, for resolving portal label metadata. */\n connectedSpaces?: Record<string, CaveSpace>\n /** Space ids whose base layer finished loading — gates portal travel UI. */\n readySpaceIds?: Record<string, true>\n /** Connector the player is inside; its label hides. */\n activeConnectorId?: string\n /** Mount connector label hosts. @default true */\n connectorLabels?: boolean\n /**\n * Play the spread reveal once when this base layer's splat first loads.\n * Only the primary (entry) scene should set this; neighbors reached through\n * a transition leave it `false` so travel feels instant.\n */\n revealOnEnter?: boolean\n children?: ReactNode\n ref?: RefObject<Group<Object3DEventMap>>\n}\n\n/**\n * The scene's ground truth: the 3DGS/splat or model capture, its colliders,\n * boundaries, and connector trigger volumes.\n */\nexport default function CaveBaseLayer(props: CaveBaseLayerProps) {\n const {\n space,\n spaceId,\n active = true,\n revealOnEnter = false,\n onInitialized,\n ref,\n connectedSpaces = {},\n readySpaceIds = {},\n activeConnectorId,\n connectorLabels = true\n } = props\n const physics = props.physics ?? active\n\n const [splatCollider, setSplatCollider] = useState<Mesh | null>(null)\n const eventBus = useCaveEventBus()\n const setSpaceLoadError = useCaveWorldStore((s) => s.setSpaceLoadError)\n\n const modelColliderCount = useMemo(\n () => space?.baseLayer?.colliders?.filter((c) => c.type === 'model').length ?? 0,\n [space?.baseLayer?.colliders]\n )\n const { ready, markSceneReady, markColliderReady } = useInitGate(modelColliderCount, physics)\n\n useEffect(() => {\n if (!ready) return\n startTransition(() => {\n eventBus.emit('space:initialized', {})\n onInitialized?.()\n })\n }, [ready, eventBus, onInitialized])\n\n // Derived splat colliders own GPU buffers that Three.js won't reclaim for us.\n useEffect(() => {\n if (!splatCollider) return\n return () => {\n splatCollider.geometry.dispose()\n }\n }, [splatCollider])\n\n const onSplatLoadImpl = useRef<(mesh: SparkSplatMesh) => void>(() => {})\n // Guards single-shot fallback collider generation regardless of when physics\n // flips on.\n const splatColliderGenerated = useRef(false)\n\n useEffect(() => {\n onSplatLoadImpl.current = (mesh: SparkSplatMesh) => {\n startTransition(() => {\n // Generate the fallback collider eagerly at load time, even while this\n // scene is a hidden preload: otherwise the first transition into it pays\n // the ~1s generation cost as a visible stutter. Skipped when the scene\n // already declares explicit colliders.\n if (!splatColliderGenerated.current && !space?.baseLayer?.colliders?.length) {\n splatColliderGenerated.current = true\n setSplatCollider(createSplatCollider(mesh, SPLAT_COLLIDER_SAMPLE_COUNT))\n }\n markSceneReady()\n })\n }\n }, [space?.baseLayer?.colliders, markSceneReady])\n\n // Stable identity: the mesh is rebuilt whenever its `args` change, so an\n // unstable onLoad would trigger redundant downloads.\n const onSplatLoad = useCallback((mesh: SparkSplatMesh) => {\n onSplatLoadImpl.current(mesh)\n }, [])\n\n // Pick which splat asset to load: with a `splatLevels` LOD list, take the\n // highest-quality variant this device can handle. Recomputed on scene change\n // only — device caps are read once, so a resize/orientation flip must not\n // trigger a re-download.\n const splatSelection = useMemo(\n () => selectSplatLevel(space?.baseLayer?.scene),\n [space?.baseLayer?.scene]\n )\n const splatUrl = splatSelection?.url\n\n const handleSplatError = useCallback(\n (error: unknown) => {\n setSpaceLoadError({ stage: 'splat', url: splatUrl, spaceId, error })\n },\n [setSpaceLoadError, splatUrl, spaceId]\n )\n\n const handleConnectorIntersection = useCallback(\n (connector: ConnectorDef, intersected: boolean) => {\n eventBus.emit(intersected ? 'connector:enter' : 'connector:leave', { connector })\n },\n [eventBus]\n )\n\n if (!space?.baseLayer) return null\n\n const { transform, scene, colliders, boundaries, connectors } = space.baseLayer\n\n return (\n <group\n name=\"base-layer\"\n position={transform.position}\n quaternion={transform.rotation}\n scale={transform.scale}\n ref={ref}\n >\n {scene?.type === 'splat' && splatUrl ? (\n <RobustSplatMesh\n url={splatUrl}\n visible={active}\n quaternion={INIT_QUATERNION}\n spaceId={spaceId}\n revealOnEnter={revealOnEnter}\n onLoad={onSplatLoad}\n // Only surface a terminal error for the active scene: a hidden\n // neighbor failing in the background must not pop a full-screen\n // prompt over a scene that works. It still auto-retries, and a manual\n // retry re-downloads it too.\n onError={active ? handleSplatError : undefined}\n onClick={props.onClick}\n onDoubleClick={props.onDoubleClick}\n />\n ) : null}\n\n {colliders?.map((collider) => {\n if (collider.type !== 'model' || !collider.url) return null\n const mesh = <ModelColliderMesh collider={collider} onLoaded={markColliderReady} />\n return (\n // Each collider gets its own boundary. `useLoader` suspends, and\n // without a boundary here the suspension unwinds to the viewer's,\n // discarding this whole subtree — including the splat mesh, which\n // spark re-downloads from scratch on every remount. Colliders also\n // load in parallel this way instead of one per retry.\n //\n // The boundary must stay *outside* `BvhPhysicsBody`: it snapshots its\n // children into the BVH world in a mount effect, so a child that\n // arrives later is never registered as a collider.\n <Suspense key={collider.id} fallback={null}>\n {physics ? (\n <BvhPhysicsBody>{mesh}</BvhPhysicsBody>\n ) : (\n // Hidden preloads still load collider assets (to satisfy the init\n // gate) but skip BVH registration.\n <group visible={false}>{mesh}</group>\n )}\n </Suspense>\n )\n })}\n\n {physics ? (\n <BvhPhysicsBody>\n {colliders?.map((collider) =>\n collider.type === 'box' ? (\n <mesh\n key={collider.id}\n position={collider.box.position}\n quaternion={toQuaternionRotation(collider.box.rotation)}\n scale={collider.box.scale}\n >\n <boxGeometry />\n <meshStandardMaterial visible={false} />\n </mesh>\n ) : null\n )}\n </BvhPhysicsBody>\n ) : null}\n\n {physics ? (\n <BvhPhysicsBody>\n {boundaries?.map((boundary) => {\n if (boundary.type === 'box' && boundary.collisionEnabled !== false) {\n return (\n <mesh\n key={boundary.id}\n position={boundary.box.position}\n quaternion={toQuaternionRotation(boundary.box.rotation)}\n scale={boundary.box.scale}\n >\n <boxGeometry />\n <meshStandardMaterial visible={false} />\n </mesh>\n )\n }\n if (boundary.type === 'polygon') {\n return <PolygonBoundaryColliders key={boundary.id} boundary={boundary} />\n }\n return null\n })}\n </BvhPhysicsBody>\n ) : null}\n\n {active\n ? boundaries?.map((boundary) =>\n boundary.type === 'polygon' ? (\n <PolygonBoundaryFence\n key={`polygon-vis-${boundary.id}`}\n boundary={boundary}\n active={active}\n />\n ) : null\n )\n : null}\n\n {/* Sensors mount only for the active scene — otherwise a preloaded\n adjacent scene's connector boxes fire `connector:enter` as the player\n walks past the same world position in the scene they're actually in. */}\n {active\n ? connectors?.map((connector) => (\n <ConnectorSensor\n key={connector.id}\n connector={connector}\n onIntersection={handleConnectorIntersection}\n />\n ))\n : null}\n\n {active && connectorLabels\n ? connectors?.map((connector) => (\n <ConnectorLabelHost\n key={`label-${connector.id}`}\n connector={connector}\n connectedSpaces={connectedSpaces}\n readySpaceIds={readySpaceIds}\n activeConnectorId={activeConnectorId}\n />\n ))\n : null}\n\n {physics && splatCollider ? (\n <BvhPhysicsBody>\n <group>\n <primitive object={splatCollider} quaternion={INIT_QUATERNION} />\n </group>\n </BvhPhysicsBody>\n ) : null}\n\n {props.children ?? null}\n </group>\n )\n}\n"],"names":["INIT_QUATERNION","SPLAT_COLLIDER_SAMPLE_COUNT","INVISIBLE_MATERIAL","MeshBasicMaterial","ConnectorSensor","connector","onIntersection","handleChange","useCallback","intersected","jsx","BvhPhysicsSensor","jsxs","ModelColliderMesh","collider","onLoaded","obj","useLoader","OBJLoader","cloned","useMemo","group","child","useEffect","useInitGate","modelColliderCount","requireModelColliders","sceneReady","useRef","loadedColliders","fired","ready","setReady","useState","requiredColliders","checkReady","markSceneReady","markColliderReady","CaveBaseLayer","props","space","spaceId","active","revealOnEnter","onInitialized","ref","connectedSpaces","readySpaceIds","activeConnectorId","connectorLabels","physics","splatCollider","setSplatCollider","eventBus","useCaveEventBus","setSpaceLoadError","useCaveWorldStore","s","c","startTransition","onSplatLoadImpl","splatColliderGenerated","mesh","createSplatCollider","onSplatLoad","splatUrl","selectSplatLevel","handleSplatError","error","handleConnectorIntersection","transform","scene","colliders","boundaries","connectors","RobustSplatMesh","Suspense","BvhPhysicsBody","toQuaternionRotation","boundary","PolygonBoundaryColliders","PolygonBoundaryFence","ConnectorLabelHost"],"mappings":"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"}
1
+ {"version":3,"file":"cave-base-layer.js","sources":["../../space/cave-base-layer.tsx"],"sourcesContent":["import {\n startTransition,\n Suspense,\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n type ReactNode,\n type RefObject\n} from 'react'\nimport { useLoader, type ThreeEvent } from '@react-three/fiber'\nimport { BvhPhysicsBody, BvhPhysicsSensor } from '@react-three/viverse'\nimport { Group, Mesh, MeshBasicMaterial, type Object3DEventMap } from 'three'\nimport { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader.js'\nimport { PLYLoader } from 'three/examples/jsm/loaders/PLYLoader.js'\nimport { useCaveEventBus, useCaveWorldStore } from '../core'\nimport type { CaveSpace, ConnectorDef, ModelCollider } from '../types'\nimport ConnectorLabelHost from './connector-label-host'\nimport { PolygonBoundaryColliders, PolygonBoundaryFence } from './polygon-boundary'\nimport RobustSplatMesh from './splats/spark/robust-splat-mesh'\nimport { type SparkSplatMesh } from './splats/spark/splat-mesh'\nimport { getModelColliderFormat } from './utils/model-collider-format'\nimport { toQuaternionRotation } from './utils/rotation-compat'\nimport { selectSplatLevel } from './utils/select-splat-level'\nimport { createSplatCollider } from './utils/splat-collider-helper'\n\n/** Identity quaternion as `[x, y, z, w]` for the R3F `quaternion` prop. */\nconst INIT_QUATERNION: [number, number, number, number] = [1, 0, 0, 0]\n\n/** Target triangle budget when deriving a collider from splat geometry. */\nconst SPLAT_COLLIDER_SAMPLE_COUNT = 100_000\n\nconst INVISIBLE_MATERIAL = /* @__PURE__ */ new MeshBasicMaterial({ visible: false })\n\nfunction ConnectorSensor({\n connector,\n onIntersection\n}: {\n connector: ConnectorDef\n onIntersection: (connector: ConnectorDef, intersected: boolean) => void\n}) {\n const handleChange = useCallback(\n (intersected: boolean) => onIntersection(connector, intersected),\n [connector, onIntersection]\n )\n\n return (\n <BvhPhysicsSensor onIntersectedChanged={handleChange}>\n <mesh\n position={connector.transform.position}\n rotation={connector.transform.rotation}\n scale={connector.transform.scale}\n >\n <boxGeometry />\n <meshStandardMaterial visible={false} />\n </mesh>\n </BvhPhysicsSensor>\n )\n}\n\n/** Collider meshes are authored Z-up; Three.js is Y-up. */\nfunction applyColliderFrame(root: Group): Group {\n root.traverse((child) => {\n if ((child as Mesh).isMesh) {\n ;(child as Mesh).material = INVISIBLE_MATERIAL\n }\n })\n root.rotation.set(-Math.PI / 2, 0, 0)\n return root\n}\n\nfunction ObjModelColliderMesh({ url, onLoaded }: { url: string; onLoaded: () => void }) {\n const obj = useLoader(OBJLoader, url)\n\n const cloned = useMemo(() => applyColliderFrame(obj.clone(true)), [obj])\n\n useEffect(() => {\n onLoaded()\n }, [onLoaded])\n\n return <primitive object={cloned} />\n}\n\nfunction PlyModelColliderMesh({ url, onLoaded }: { url: string; onLoaded: () => void }) {\n const geometry = useLoader(PLYLoader, url)\n\n const group = useMemo(() => {\n const wrapper = new Group()\n wrapper.add(new Mesh(geometry, INVISIBLE_MATERIAL))\n return applyColliderFrame(wrapper)\n }, [geometry])\n\n useEffect(() => {\n onLoaded()\n }, [onLoaded])\n\n return <primitive object={group} />\n}\n\n/**\n * Dispatch on URL extension so each loader is called unconditionally inside\n * its own component (`useLoader` cannot be behind a hook-conditional).\n */\nfunction ModelColliderMesh({\n collider,\n onLoaded\n}: {\n collider: ModelCollider\n onLoaded: () => void\n}) {\n if (getModelColliderFormat(collider.url) === 'ply') {\n return <PlyModelColliderMesh url={collider.url} onLoaded={onLoaded} />\n }\n return <ObjModelColliderMesh url={collider.url} onLoaded={onLoaded} />\n}\n\n/**\n * Track readiness of the scene (splat) and every model collider, firing\n * `onReady` exactly once when all of them have loaded.\n *\n * Counters live in refs rather than state so each collider landing doesn't\n * re-render the base layer; only the final flip publishes to React.\n */\nfunction useInitGate(modelColliderCount: number, requireModelColliders: boolean) {\n const sceneReady = useRef(false)\n const loadedColliders = useRef(0)\n const fired = useRef(false)\n const [ready, setReady] = useState(false)\n\n const requiredColliders = requireModelColliders ? modelColliderCount : 0\n\n const checkReady = useCallback(() => {\n if (fired.current) return\n if (sceneReady.current && loadedColliders.current >= requiredColliders) {\n fired.current = true\n setReady(true)\n }\n }, [requiredColliders])\n\n const markSceneReady = useCallback(() => {\n sceneReady.current = true\n checkReady()\n }, [checkReady])\n\n const markColliderReady = useCallback(() => {\n loadedColliders.current += 1\n checkReady()\n }, [checkReady])\n\n return { ready, markSceneReady, markColliderReady }\n}\n\nexport interface CaveBaseLayerProps {\n space?: CaveSpace\n /** Space id, threaded down for splat load-error context. */\n spaceId?: string\n onProcess?: (percent: number) => void\n onInitialized?: () => void\n onClick?: (event: ThreeEvent<MouseEvent>) => void\n onDoubleClick?: (event: ThreeEvent<MouseEvent>) => void\n physics?: boolean\n /**\n * When `false` this base layer is preloaded-but-not-current: connector\n * sensors don't mount (no spurious `connector:enter` from an adjacent scene\n * occupying the same world coordinates) and physics defaults off.\n */\n active?: boolean\n /** Connected space configs, for resolving portal label metadata. */\n connectedSpaces?: Record<string, CaveSpace>\n /** Space ids whose base layer finished loading — gates portal travel UI. */\n readySpaceIds?: Record<string, true>\n /** Connector the player is inside; its label hides. */\n activeConnectorId?: string\n /** Mount connector label hosts. @default true */\n connectorLabels?: boolean\n /**\n * Play the spread reveal once when this base layer's splat first loads.\n * Only the primary (entry) scene should set this; neighbors reached through\n * a transition leave it `false` so travel feels instant.\n */\n revealOnEnter?: boolean\n children?: ReactNode\n ref?: RefObject<Group<Object3DEventMap>>\n}\n\n/**\n * The scene's ground truth: the 3DGS/splat or model capture, its colliders,\n * boundaries, and connector trigger volumes.\n */\nexport default function CaveBaseLayer(props: CaveBaseLayerProps) {\n const {\n space,\n spaceId,\n active = true,\n revealOnEnter = false,\n onInitialized,\n ref,\n connectedSpaces = {},\n readySpaceIds = {},\n activeConnectorId,\n connectorLabels = true\n } = props\n const physics = props.physics ?? active\n\n const [splatCollider, setSplatCollider] = useState<Mesh | null>(null)\n const eventBus = useCaveEventBus()\n const setSpaceLoadError = useCaveWorldStore((s) => s.setSpaceLoadError)\n\n const modelColliderCount = useMemo(\n () => space?.baseLayer?.colliders?.filter((c) => c.type === 'model').length ?? 0,\n [space?.baseLayer?.colliders]\n )\n const { ready, markSceneReady, markColliderReady } = useInitGate(modelColliderCount, physics)\n\n useEffect(() => {\n if (!ready) return\n startTransition(() => {\n eventBus.emit('space:initialized', {})\n onInitialized?.()\n })\n }, [ready, eventBus, onInitialized])\n\n // Derived splat colliders own GPU buffers that Three.js won't reclaim for us.\n useEffect(() => {\n if (!splatCollider) return\n return () => {\n splatCollider.geometry.dispose()\n }\n }, [splatCollider])\n\n const onSplatLoadImpl = useRef<(mesh: SparkSplatMesh) => void>(() => {})\n // Guards single-shot fallback collider generation regardless of when physics\n // flips on.\n const splatColliderGenerated = useRef(false)\n\n useEffect(() => {\n onSplatLoadImpl.current = (mesh: SparkSplatMesh) => {\n startTransition(() => {\n // Generate the fallback collider eagerly at load time, even while this\n // scene is a hidden preload: otherwise the first transition into it pays\n // the ~1s generation cost as a visible stutter. Skipped when the scene\n // already declares explicit colliders.\n if (!splatColliderGenerated.current && !space?.baseLayer?.colliders?.length) {\n splatColliderGenerated.current = true\n setSplatCollider(createSplatCollider(mesh, SPLAT_COLLIDER_SAMPLE_COUNT))\n }\n markSceneReady()\n })\n }\n }, [space?.baseLayer?.colliders, markSceneReady])\n\n // Stable identity: the mesh is rebuilt whenever its `args` change, so an\n // unstable onLoad would trigger redundant downloads.\n const onSplatLoad = useCallback((mesh: SparkSplatMesh) => {\n onSplatLoadImpl.current(mesh)\n }, [])\n\n // Pick which splat asset to load: with a `splatLevels` LOD list, take the\n // highest-quality variant this device can handle. Recomputed on scene change\n // only — device caps are read once, so a resize/orientation flip must not\n // trigger a re-download.\n const splatSelection = useMemo(\n () => selectSplatLevel(space?.baseLayer?.scene),\n [space?.baseLayer?.scene]\n )\n const splatUrl = splatSelection?.url\n\n const handleSplatError = useCallback(\n (error: unknown) => {\n setSpaceLoadError({ stage: 'splat', url: splatUrl, spaceId, error })\n },\n [setSpaceLoadError, splatUrl, spaceId]\n )\n\n const handleConnectorIntersection = useCallback(\n (connector: ConnectorDef, intersected: boolean) => {\n eventBus.emit(intersected ? 'connector:enter' : 'connector:leave', { connector })\n },\n [eventBus]\n )\n\n if (!space?.baseLayer) return null\n\n const { transform, scene, colliders, boundaries, connectors } = space.baseLayer\n\n return (\n <group\n name=\"base-layer\"\n position={transform.position}\n quaternion={transform.rotation}\n scale={transform.scale}\n ref={ref}\n >\n {scene?.type === 'splat' && splatUrl ? (\n <RobustSplatMesh\n url={splatUrl}\n visible={active}\n quaternion={INIT_QUATERNION}\n spaceId={spaceId}\n revealOnEnter={revealOnEnter}\n onLoad={onSplatLoad}\n // Only surface a terminal error for the active scene: a hidden\n // neighbor failing in the background must not pop a full-screen\n // prompt over a scene that works. It still auto-retries, and a manual\n // retry re-downloads it too.\n onError={active ? handleSplatError : undefined}\n onClick={props.onClick}\n onDoubleClick={props.onDoubleClick}\n />\n ) : null}\n\n {colliders?.map((collider) => {\n if (collider.type !== 'model' || !collider.url) return null\n const mesh = <ModelColliderMesh collider={collider} onLoaded={markColliderReady} />\n return (\n // Each collider gets its own boundary. `useLoader` suspends, and\n // without a boundary here the suspension unwinds to the viewer's,\n // discarding this whole subtree — including the splat mesh, which\n // spark re-downloads from scratch on every remount. Colliders also\n // load in parallel this way instead of one per retry.\n //\n // The boundary must stay *outside* `BvhPhysicsBody`: it snapshots its\n // children into the BVH world in a mount effect, so a child that\n // arrives later is never registered as a collider.\n <Suspense key={collider.id} fallback={null}>\n {physics ? (\n <BvhPhysicsBody>{mesh}</BvhPhysicsBody>\n ) : (\n // Hidden preloads still load collider assets (to satisfy the init\n // gate) but skip BVH registration.\n <group visible={false}>{mesh}</group>\n )}\n </Suspense>\n )\n })}\n\n {physics ? (\n <BvhPhysicsBody>\n {colliders?.map((collider) =>\n collider.type === 'box' ? (\n <mesh\n key={collider.id}\n position={collider.box.position}\n quaternion={toQuaternionRotation(collider.box.rotation)}\n scale={collider.box.scale}\n >\n <boxGeometry />\n <meshStandardMaterial visible={false} />\n </mesh>\n ) : null\n )}\n </BvhPhysicsBody>\n ) : null}\n\n {physics ? (\n <BvhPhysicsBody>\n {boundaries?.map((boundary) => {\n if (boundary.type === 'box' && boundary.collisionEnabled !== false) {\n return (\n <mesh\n key={boundary.id}\n position={boundary.box.position}\n quaternion={toQuaternionRotation(boundary.box.rotation)}\n scale={boundary.box.scale}\n >\n <boxGeometry />\n <meshStandardMaterial visible={false} />\n </mesh>\n )\n }\n if (boundary.type === 'polygon') {\n return <PolygonBoundaryColliders key={boundary.id} boundary={boundary} />\n }\n return null\n })}\n </BvhPhysicsBody>\n ) : null}\n\n {active\n ? boundaries?.map((boundary) =>\n boundary.type === 'polygon' ? (\n <PolygonBoundaryFence\n key={`polygon-vis-${boundary.id}`}\n boundary={boundary}\n active={active}\n />\n ) : null\n )\n : null}\n\n {/* Sensors mount only for the active scene — otherwise a preloaded\n adjacent scene's connector boxes fire `connector:enter` as the player\n walks past the same world position in the scene they're actually in. */}\n {active\n ? connectors?.map((connector) => (\n <ConnectorSensor\n key={connector.id}\n connector={connector}\n onIntersection={handleConnectorIntersection}\n />\n ))\n : null}\n\n {active && connectorLabels\n ? connectors?.map((connector) => (\n <ConnectorLabelHost\n key={`label-${connector.id}`}\n connector={connector}\n connectedSpaces={connectedSpaces}\n readySpaceIds={readySpaceIds}\n activeConnectorId={activeConnectorId}\n />\n ))\n : null}\n\n {physics && splatCollider ? (\n <BvhPhysicsBody>\n <group>\n <primitive object={splatCollider} quaternion={INIT_QUATERNION} />\n </group>\n </BvhPhysicsBody>\n ) : null}\n\n {props.children ?? null}\n </group>\n )\n}\n"],"names":["INIT_QUATERNION","SPLAT_COLLIDER_SAMPLE_COUNT","INVISIBLE_MATERIAL","MeshBasicMaterial","ConnectorSensor","connector","onIntersection","handleChange","useCallback","intersected","jsx","BvhPhysicsSensor","jsxs","applyColliderFrame","root","child","ObjModelColliderMesh","url","onLoaded","obj","useLoader","OBJLoader","cloned","useMemo","useEffect","PlyModelColliderMesh","geometry","PLYLoader","group","wrapper","Group","Mesh","ModelColliderMesh","collider","getModelColliderFormat","useInitGate","modelColliderCount","requireModelColliders","sceneReady","useRef","loadedColliders","fired","ready","setReady","useState","requiredColliders","checkReady","markSceneReady","markColliderReady","CaveBaseLayer","props","space","spaceId","active","revealOnEnter","onInitialized","ref","connectedSpaces","readySpaceIds","activeConnectorId","connectorLabels","physics","splatCollider","setSplatCollider","eventBus","useCaveEventBus","setSpaceLoadError","useCaveWorldStore","s","c","startTransition","onSplatLoadImpl","splatColliderGenerated","mesh","createSplatCollider","onSplatLoad","splatUrl","selectSplatLevel","handleSplatError","error","handleConnectorIntersection","transform","scene","colliders","boundaries","connectors","RobustSplatMesh","Suspense","BvhPhysicsBody","toQuaternionRotation","boundary","PolygonBoundaryColliders","PolygonBoundaryFence","ConnectorLabelHost"],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ export type ModelColliderFormat = 'obj' | 'ply';
2
+ /**
3
+ * Infer the mesh loader from a model-collider URL.
4
+ *
5
+ * `.ply` (any case) selects `PLYLoader`; every other path, including a missing
6
+ * extension, stays on `OBJLoader` so existing scene JSON keeps working.
7
+ * Query strings and hashes are ignored (`floor.ply?token=` is still PLY).
8
+ */
9
+ export declare function getModelColliderFormat(url: string): ModelColliderFormat;
10
+ /** Warm the R3F loader cache with the same loader the base layer will use. */
11
+ export declare function preloadModelCollider(url: string): void;
12
+ //# sourceMappingURL=model-collider-format.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"model-collider-format.d.ts","sourceRoot":"","sources":["../../../space/utils/model-collider-format.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,mBAAmB,GAAG,KAAK,GAAG,KAAK,CAAA;AAQ/C;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,MAAM,GAAG,mBAAmB,CAQvE;AAED,8EAA8E;AAC9E,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAMtD"}
@@ -0,0 +1,25 @@
1
+ import { useLoader as e } from "@react-three/fiber";
2
+ import { OBJLoader as r } from "three/examples/jsm/loaders/OBJLoader.js";
3
+ import { PLYLoader as p } from "three/examples/jsm/loaders/PLYLoader.js";
4
+ const i = "https://collider.invalid";
5
+ function l(o) {
6
+ let t = o;
7
+ try {
8
+ t = new URL(o, i).pathname;
9
+ } catch {
10
+ t = o.split("#")[0]?.split("?")[0] ?? o;
11
+ }
12
+ return /\.ply$/i.test(t) ? "ply" : "obj";
13
+ }
14
+ function m(o) {
15
+ if (l(o) === "ply") {
16
+ e.preload(p, o);
17
+ return;
18
+ }
19
+ e.preload(r, o);
20
+ }
21
+ export {
22
+ l as getModelColliderFormat,
23
+ m as preloadModelCollider
24
+ };
25
+ //# sourceMappingURL=model-collider-format.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"model-collider-format.js","sources":["../../../space/utils/model-collider-format.ts"],"sourcesContent":["import { useLoader } from '@react-three/fiber'\nimport { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader.js'\nimport { PLYLoader } from 'three/examples/jsm/loaders/PLYLoader.js'\n\nexport type ModelColliderFormat = 'obj' | 'ply'\n\n/**\n * Dummy base for `new URL(relative)` so path/query/hash split the same way\n * as an absolute collider URL.\n */\nconst RELATIVE_URL_BASE = 'https://collider.invalid'\n\n/**\n * Infer the mesh loader from a model-collider URL.\n *\n * `.ply` (any case) selects `PLYLoader`; every other path, including a missing\n * extension, stays on `OBJLoader` so existing scene JSON keeps working.\n * Query strings and hashes are ignored (`floor.ply?token=` is still PLY).\n */\nexport function getModelColliderFormat(url: string): ModelColliderFormat {\n let path = url\n try {\n path = new URL(url, RELATIVE_URL_BASE).pathname\n } catch {\n path = url.split('#')[0]?.split('?')[0] ?? url\n }\n return /\\.ply$/i.test(path) ? 'ply' : 'obj'\n}\n\n/** Warm the R3F loader cache with the same loader the base layer will use. */\nexport function preloadModelCollider(url: string): void {\n if (getModelColliderFormat(url) === 'ply') {\n useLoader.preload(PLYLoader, url)\n return\n }\n useLoader.preload(OBJLoader, url)\n}\n"],"names":["RELATIVE_URL_BASE","getModelColliderFormat","url","path","preloadModelCollider","useLoader","PLYLoader","OBJLoader"],"mappings":";;;AAUA,MAAMA,IAAoB;AASnB,SAASC,EAAuBC,GAAkC;AACvE,MAAIC,IAAOD;AACX,MAAI;AACF,IAAAC,IAAO,IAAI,IAAID,GAAKF,CAAiB,EAAE;AAAA,EACzC,QAAQ;AACN,IAAAG,IAAOD,EAAI,MAAM,GAAG,EAAE,CAAC,GAAG,MAAM,GAAG,EAAE,CAAC,KAAKA;AAAA,EAC7C;AACA,SAAO,UAAU,KAAKC,CAAI,IAAI,QAAQ;AACxC;AAGO,SAASC,EAAqBF,GAAmB;AACtD,MAAID,EAAuBC,CAAG,MAAM,OAAO;AACzC,IAAAG,EAAU,QAAQC,GAAWJ,CAAG;AAChC;AAAA,EACF;AACA,EAAAG,EAAU,QAAQE,GAAWL,CAAG;AAClC;"}
@@ -125,6 +125,13 @@ export interface GltfCollider {
125
125
  type: 'gltf';
126
126
  src: string;
127
127
  }
128
+ /**
129
+ * Triangle-mesh collider referenced by URL.
130
+ *
131
+ * The loader is chosen from the URL path extension (query/hash ignored):
132
+ * `.ply` → `PLYLoader`, anything else (typically `.obj`) → `OBJLoader`.
133
+ * Meshes are authored Z-up and rotated into Three.js Y-up at load time.
134
+ */
128
135
  export interface ModelCollider {
129
136
  id: string;
130
137
  type: 'model';
@@ -1 +1 @@
1
- {"version":3,"file":"space-types.d.ts","sourceRoot":"","sources":["../../types/space-types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"space-types.d.ts","sourceRoot":"","sources":["../../types/space-types.ts"],"names":[],"mappings":"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"}
package/doc/API.md CHANGED
@@ -605,7 +605,7 @@ function MyApp({ spaceConfig }) {
605
605
  **Scene authoring**
606
606
 
607
607
  - Give the container a definite width and height; layout thrash forces canvas resizes.
608
- - Pre-author `baseLayer.colliders`. Deriving a collider from splat geometry at runtime costs roughly a second.
608
+ - Pre-author `baseLayer.colliders`. Deriving a collider from splat geometry at runtime costs roughly a second. Model colliders (`type: "model"`) load `.obj` or `.ply` from the URL path extension.
609
609
  - Provide `splatLevels` for heavy captures so mobile gets a variant it can actually render.
610
610
  - Prefer `.glb` over `.gltf` for models.
611
611
  - Only declare a `Collider` component on objects that need physics.
@@ -237,7 +237,7 @@ Visually: the whole scene appears immediately as a point cloud (original colors,
237
237
  - `useVisibilityFrameloop()` switches R3F `frameloop` to `never` while the page is hidden — GPU work drops to zero, which is the main heat driver on mobile. R3F's `setFrameloop` restarts the clock, so the first visible frame has no delta spike.
238
238
  - Device-aware splat LOD via `selectSplatLevel`, evaluated once per scene mount so a resize can't trigger a re-download.
239
239
  - One Suspense boundary per suspending subtree — see [6.1](#61-suspense-topology).
240
- - Both viewers call `preloadSpaceAssets()` as soon as the scene JSON lands, so collider OBJs download alongside the splat instead of after the base layer mounts.
240
+ - Both viewers call `preloadSpaceAssets()` as soon as the scene JSON lands, so collider OBJ/PLY meshes download alongside the splat instead of after the base layer mounts.
241
241
  - `ConnectorLabelHost` computes visibility every frame but only publishes to React across a threshold.
242
242
  - Neighbor preloads are serialized and idle-scheduled.
243
243
  - `startTransition` wraps collider generation and scene switches.
@@ -252,7 +252,7 @@ Every leaf that suspends therefore owns a boundary:
252
252
  | Suspending leaf | Suspends via | Boundary lives in |
253
253
  | --------------------------------------------------- | ----------------------------------------------------------------- | ------------------------------------------------------------- |
254
254
  | A space's whole subtree | anything below it | `SpaceSubtree` (`ConnectedSpacesRenderer`), `CaveOrbitViewer` |
255
- | Model collider | `useLoader(OBJLoader, …)` | `CaveBaseLayer`, one per collider |
255
+ | Model collider | `useLoader(OBJLoader \| PLYLoader, …)` (`.ply` vs other) | `CaveBaseLayer`, one per collider |
256
256
  | Scene object (GLB / FBX / texture / video / `Text`) | drei loader hooks | `CustomObject` |
257
257
  | Local character | viverse `suspend()` — once for the model, once per animation clip | `CaveExplore` |
258
258
  | Remote player | `useGLTF` × 7 | `RemoteCharacterController`, one per player |
@@ -68,7 +68,7 @@
68
68
  {
69
69
  "id": "789",
70
70
  "type": "model",
71
- "url": "https://xxxxx.obj",
71
+ "url": "https://xxxxx.obj", // .obj 或 .ply,按路径扩展名选择加载器(query/hash 忽略)
72
72
  "size": 6042883 // 可选,文件大小(字节)
73
73
  }
74
74
  ],
@@ -228,6 +228,19 @@
228
228
  - 世界位姿由 `G_target × T_target × spawn_local` 计算(与 portal 连接器落点同一套矩阵管线)。
229
229
  - seamless / portal 场景切换不会重新应用;缺省时保留 `<SimpleCharacter>` 默认初始位姿。
230
230
 
231
+ #### baseLayer.colliders 字段说明(模型碰撞体)
232
+
233
+ `type: "model"` 通过 `url` 加载三角网格碰撞体:
234
+
235
+ | 字段 | 类型 | 必填 | 说明 |
236
+ | ------ | --------- | ---- | -------------------------------------------------------------------------------------------------------- |
237
+ | `id` | `string` | 是 | 碰撞体标识 |
238
+ | `type` | `'model'` | 是 | 网格碰撞体 |
239
+ | `url` | `string` | 是 | 网格 URL。路径以 `.ply` 结尾(忽略大小写、query、hash)时用 PLY 加载器,其余(通常 `.obj`)用 OBJ 加载器 |
240
+ | `size` | `number` | 否 | 文件大小(字节),作者工具提示;运行时不读取 |
241
+
242
+ 网格按 Z-up 创作,加载时转到 Three.js Y-up。`type: "box"` 使用内联 `box` 变换,不经过该加载路径。
243
+
231
244
  ### 2. 要素结构 (CustomObject)
232
245
 
233
246
  内置渲染器覆盖 `model`、`splat`、`image`、`video`、`poi`、`portal`、`placeholder`;其余为宿主注册类型(见文首说明)。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cavegiant/cave-world-r3f",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Web-based 3D virtual tour SDK — render interactive 3D scenes from JSON configuration",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -80,10 +80,14 @@
80
80
  "test:watch": "vitest",
81
81
  "examples": "vite --config vite.examples.config.ts",
82
82
  "examples:build": "vite build --config vite.examples.config.ts",
83
- "check": "npm run typecheck && npm run lint && npm run test",
83
+ "check": "npm run format:check && npm run typecheck && npm run lint && npm run test",
84
84
  "verify:package": "publint && attw --pack .",
85
85
  "verify:pack-size": "node scripts/verify-pack-size.mjs",
86
- "prepublishOnly": "npm run check && npm run build && npm run verify:package && npm run verify:pack-size"
86
+ "prepublishOnly": "npm run check && npm run build && npm run verify:package && npm run verify:pack-size",
87
+ "prepare": "husky || true"
88
+ },
89
+ "lint-staged": {
90
+ "*": "prettier --write --ignore-unknown"
87
91
  },
88
92
  "peerDependencies": {
89
93
  "@react-three/drei": ">=10.0.0",
@@ -114,7 +118,9 @@
114
118
  "eslint": "^9.39.4",
115
119
  "eslint-plugin-react-hooks": "^7.0.1",
116
120
  "globals": "^17.12.0",
121
+ "husky": "^9.1.7",
117
122
  "jsdom": "^27.4.0",
123
+ "lint-staged": "^16.4.0",
118
124
  "lottie-react": "^2.4.1",
119
125
  "prettier": "^3.8.1",
120
126
  "publint": "^0.3.24",