@nice2dev/ui-3d 1.3.15 → 1.3.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/cjs/collaborative/collaborativeScene.js +160 -206
  3. package/dist/cjs/core/editorAsk.js +51 -41
  4. package/dist/cjs/core/i18n.js +13 -14
  5. package/dist/cjs/core/threeCompat.js +29 -42
  6. package/dist/cjs/dance/DanceBridge.js +148 -177
  7. package/dist/cjs/dance/DanceScoreEngine.js +209 -221
  8. package/dist/cjs/dance/PoseDetector.js +302 -275
  9. package/dist/cjs/index.js +110 -113
  10. package/dist/cjs/material/MaterialEditor.module.css.js +242 -5
  11. package/dist/cjs/material/NiceMaterialEditor.js +1213 -757
  12. package/dist/cjs/material/materialEditorTypes.js +35 -49
  13. package/dist/cjs/material/materialEditorUtils.js +684 -747
  14. package/dist/cjs/material/materialNodeDefinitions.js +1411 -1459
  15. package/dist/cjs/model/ModelEditor.js +90 -40
  16. package/dist/cjs/model/ModelEditor.module.css.js +257 -5
  17. package/dist/cjs/model/ModelEditorLeftPanel.js +126 -47
  18. package/dist/cjs/model/ModelEditorMenuBar.js +359 -32
  19. package/dist/cjs/model/ModelEditorRightPanel.js +647 -131
  20. package/dist/cjs/model/ModelEditorSubComponents.js +277 -112
  21. package/dist/cjs/model/ModelEditorTimeline.js +232 -34
  22. package/dist/cjs/model/ModelEditorToolbar.js +170 -14
  23. package/dist/cjs/model/ModelEditorViewport.js +231 -39
  24. package/dist/cjs/model/ModelViewer.js +388 -63
  25. package/dist/cjs/model/ModelViewer.module.css.js +77 -5
  26. package/dist/cjs/model/NiceArmatureEditor.js +473 -274
  27. package/dist/cjs/model/NiceMorphTargetEditor.js +333 -227
  28. package/dist/cjs/model/NiceOctree.js +372 -320
  29. package/dist/cjs/model/NicePhysicsSimulation.js +528 -298
  30. package/dist/cjs/model/NiceProceduralGeometry.js +673 -300
  31. package/dist/cjs/model/NiceTerrainEditor.js +595 -385
  32. package/dist/cjs/model/NiceWeightPainter.js +384 -261
  33. package/dist/cjs/model/NiceXRPreview.js +385 -308
  34. package/dist/cjs/model/cadModeUtils.js +103 -106
  35. package/dist/cjs/model/editorShortcuts.js +171 -194
  36. package/dist/cjs/model/modelEditorTypes.js +100 -118
  37. package/dist/cjs/model/modelEditorUtils.js +1061 -1146
  38. package/dist/cjs/model/simsModeUtils.js +1023 -1053
  39. package/dist/cjs/model/useModelEditor.js +1977 -1933
  40. package/dist/cjs/model/useModelViewer.js +633 -665
  41. package/dist/cjs/nice2dev-ui-3d.css +1 -1
  42. package/dist/cjs/particle/NiceParticleEditor.js +1186 -571
  43. package/dist/cjs/particle/ParticleEditor.module.css.js +122 -5
  44. package/dist/cjs/particle/particleEditorTypes.js +62 -70
  45. package/dist/cjs/particle/particleEditorUtils.js +998 -1013
  46. package/dist/cjs/rendering/NiceCascadedShadows.js +381 -259
  47. package/dist/cjs/rendering/NiceRenderExport.js +424 -337
  48. package/dist/cjs/rendering/NiceSSAO.js +454 -337
  49. package/dist/cjs/rendering/NiceSSR.js +387 -261
  50. package/dist/cjs/rendering/NiceWebGPURenderer.js +359 -230
  51. package/dist/cjs/uv/NiceUVEditor.js +787 -550
  52. package/dist/cjs/uv/UVEditor.module.css.js +53 -5
  53. package/dist/cjs/uv/uvEditorTypes.js +59 -76
  54. package/dist/cjs/uv/uvEditorUtils.js +501 -575
  55. package/dist/esm/collaborative/collaborativeScene.js +163 -205
  56. package/dist/esm/core/editorAsk.js +53 -40
  57. package/dist/esm/core/i18n.js +15 -13
  58. package/dist/esm/core/threeCompat.js +18 -26
  59. package/dist/esm/dance/DanceBridge.js +151 -176
  60. package/dist/esm/dance/DanceScoreEngine.js +211 -220
  61. package/dist/esm/dance/PoseDetector.js +304 -274
  62. package/dist/esm/index.js +258 -36
  63. package/dist/esm/material/MaterialEditor.module.css.js +242 -3
  64. package/dist/esm/material/NiceMaterialEditor.js +1215 -756
  65. package/dist/esm/material/materialEditorTypes.js +45 -48
  66. package/dist/esm/material/materialEditorUtils.js +713 -746
  67. package/dist/esm/material/materialNodeDefinitions.js +1415 -1458
  68. package/dist/esm/model/ModelEditor.js +91 -39
  69. package/dist/esm/model/ModelEditor.module.css.js +257 -3
  70. package/dist/esm/model/ModelEditorLeftPanel.js +126 -45
  71. package/dist/esm/model/ModelEditorMenuBar.js +359 -30
  72. package/dist/esm/model/ModelEditorRightPanel.js +634 -114
  73. package/dist/esm/model/ModelEditorSubComponents.js +265 -94
  74. package/dist/esm/model/ModelEditorTimeline.js +232 -32
  75. package/dist/esm/model/ModelEditorToolbar.js +170 -12
  76. package/dist/esm/model/ModelEditorViewport.js +231 -37
  77. package/dist/esm/model/ModelViewer.js +389 -62
  78. package/dist/esm/model/ModelViewer.module.css.js +77 -3
  79. package/dist/esm/model/NiceArmatureEditor.js +462 -258
  80. package/dist/esm/model/NiceMorphTargetEditor.js +322 -211
  81. package/dist/esm/model/NiceOctree.js +361 -304
  82. package/dist/esm/model/NicePhysicsSimulation.js +517 -282
  83. package/dist/esm/model/NiceProceduralGeometry.js +667 -284
  84. package/dist/esm/model/NiceTerrainEditor.js +584 -369
  85. package/dist/esm/model/NiceWeightPainter.js +373 -245
  86. package/dist/esm/model/NiceXRPreview.js +374 -292
  87. package/dist/esm/model/cadModeUtils.js +95 -88
  88. package/dist/esm/model/editorShortcuts.js +172 -193
  89. package/dist/esm/model/modelEditorTypes.js +90 -100
  90. package/dist/esm/model/modelEditorUtils.js +1056 -1128
  91. package/dist/esm/model/simsModeUtils.js +1021 -1035
  92. package/dist/esm/model/useModelEditor.js +1946 -1920
  93. package/dist/esm/model/useModelViewer.js +619 -647
  94. package/dist/esm/nice2dev-ui-3d.css +1 -1
  95. package/dist/esm/particle/NiceParticleEditor.js +1188 -570
  96. package/dist/esm/particle/ParticleEditor.module.css.js +122 -3
  97. package/dist/esm/particle/particleEditorTypes.js +69 -69
  98. package/dist/esm/particle/particleEditorUtils.js +1027 -1012
  99. package/dist/esm/rendering/NiceCascadedShadows.js +370 -243
  100. package/dist/esm/rendering/NiceRenderExport.js +413 -321
  101. package/dist/esm/rendering/NiceSSAO.js +450 -328
  102. package/dist/esm/rendering/NiceSSR.js +383 -252
  103. package/dist/esm/rendering/NiceWebGPURenderer.js +326 -214
  104. package/dist/esm/uv/NiceUVEditor.js +788 -549
  105. package/dist/esm/uv/UVEditor.module.css.js +53 -3
  106. package/dist/esm/uv/uvEditorTypes.js +68 -75
  107. package/dist/esm/uv/uvEditorUtils.js +515 -557
  108. package/package.json +7 -12
@@ -1,678 +1,646 @@
1
- 'use strict';
2
-
3
- var ui = require('@nice2dev/ui');
4
- var React = require('react');
5
- var THREE = require('three');
6
- var OrbitControls_js = require('three/examples/jsm/controls/OrbitControls.js');
7
- var ColladaLoader_js = require('three/examples/jsm/loaders/ColladaLoader.js');
8
- var FBXLoader_js = require('three/examples/jsm/loaders/FBXLoader.js');
9
- var GLTFLoader_js = require('three/examples/jsm/loaders/GLTFLoader.js');
10
- var OBJLoader_js = require('three/examples/jsm/loaders/OBJLoader.js');
11
- var PLYLoader_js = require('three/examples/jsm/loaders/PLYLoader.js');
12
- var STLLoader_js = require('three/examples/jsm/loaders/STLLoader.js');
13
- var i18n = require('../core/i18n.js');
14
- var threeCompat = require('../core/threeCompat.js');
15
-
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const ui = require("@nice2dev/ui");
4
+ const React = require("react");
5
+ const THREE = require("three");
6
+ const OrbitControls_js = require("three/examples/jsm/controls/OrbitControls.js");
7
+ const ColladaLoader_js = require("three/examples/jsm/loaders/ColladaLoader.js");
8
+ const FBXLoader_js = require("three/examples/jsm/loaders/FBXLoader.js");
9
+ const GLTFLoader_js = require("three/examples/jsm/loaders/GLTFLoader.js");
10
+ const OBJLoader_js = require("three/examples/jsm/loaders/OBJLoader.js");
11
+ const PLYLoader_js = require("three/examples/jsm/loaders/PLYLoader.js");
12
+ const STLLoader_js = require("three/examples/jsm/loaders/STLLoader.js");
13
+ const i18n = require("../core/i18n.js");
14
+ const threeCompat = require("../core/threeCompat.js");
16
15
  function _interopNamespaceDefault(e) {
17
- var n = Object.create(null);
18
- if (e) {
19
- Object.keys(e).forEach(function (k) {
20
- if (k !== 'default') {
21
- var d = Object.getOwnPropertyDescriptor(e, k);
22
- Object.defineProperty(n, k, d.get ? d : {
23
- enumerable: true,
24
- get: function () { return e[k]; }
25
- });
26
- }
27
- });
28
- }
29
- n.default = e;
30
- return Object.freeze(n);
16
+ const n = Object.create(null, { [Symbol.toStringTag]: { value: "Module" } });
17
+ if (e) {
18
+ for (const k in e) {
19
+ if (k !== "default") {
20
+ const d = Object.getOwnPropertyDescriptor(e, k);
21
+ Object.defineProperty(n, k, d.get ? d : {
22
+ enumerable: true,
23
+ get: () => e[k]
24
+ });
25
+ }
26
+ }
27
+ }
28
+ n.default = e;
29
+ return Object.freeze(n);
31
30
  }
32
-
33
- var THREE__namespace = /*#__PURE__*/_interopNamespaceDefault(THREE);
34
-
35
- /**
36
- * useModelViewer — lightweight hook for the view-only 3D model viewer.
37
- *
38
- * Unlike useModelEditor this hook provides NO editing — just loading,
39
- * orbit/pan/zoom navigation, optional WASD first-person camera,
40
- * gamepad support, VR mode, annotations and measurement tools.
41
- *
42
- * Designed for ERP real-estate / rental-space visualisation and
43
- * product presentation.
44
- */
45
- /* ═══════════════════════════════════════════
46
- Quality presets
47
- ═══════════════════════════════════════════ */
31
+ const THREE__namespace = /* @__PURE__ */ _interopNamespaceDefault(THREE);
48
32
  const QUALITY_MAP = {
49
- low: { pixelRatio: 0.75, shadowMapSize: 512, antialias: false },
50
- medium: { pixelRatio: 1, shadowMapSize: 1024, antialias: true },
51
- high: { pixelRatio: 1, shadowMapSize: 2048, antialias: true },
52
- ultra: { pixelRatio: 2, shadowMapSize: 4096, antialias: true },
33
+ low: { pixelRatio: 0.75, shadowMapSize: 512, antialias: false },
34
+ medium: { pixelRatio: 1, shadowMapSize: 1024, antialias: true },
35
+ high: { pixelRatio: 1, shadowMapSize: 2048, antialias: true },
36
+ ultra: { pixelRatio: 2, shadowMapSize: 4096, antialias: true }
53
37
  };
54
38
  const TONE_MAP = {
55
- linear: THREE__namespace.LinearToneMapping,
56
- reinhard: THREE__namespace.ReinhardToneMapping,
57
- cineon: THREE__namespace.CineonToneMapping,
58
- aces: THREE__namespace.ACESFilmicToneMapping,
59
- agx: THREE__namespace.AgXToneMapping,
39
+ linear: THREE__namespace.LinearToneMapping,
40
+ reinhard: THREE__namespace.ReinhardToneMapping,
41
+ cineon: THREE__namespace.CineonToneMapping,
42
+ aces: THREE__namespace.ACESFilmicToneMapping,
43
+ agx: THREE__namespace.AgXToneMapping
60
44
  };
61
- /* ═══════════════════════════════════════════
62
- Hook
63
- ═══════════════════════════════════════════ */
64
45
  function useModelViewer(props = {}) {
65
- var _a;
66
- const { t } = i18n.useNiceTranslation();
67
- const { src, format, navigationMode: initNavMode = 'orbit', quality = 'high', autoRotateSpeed: initAutoRotate = 0, backgroundColor = 'var(--nice-bg, #1a1a2e)', showGrid: initShowGrid = true, shadows = true, gamepad = false, cameraPresets: _cameraPresets = [], annotations: _annotations = [], hotspots: _hotspots = [], floorPlans: _floorPlans = [], onHotspotClick: _onHotspotClick, onAnnotationClick: _onAnnotationClick, onLoadProgress, onLoad, onError, clippingPlanes = [], measureMode: initMeasureMode = false, measurements: _measurements = [], onMeasure: _onMeasure, minDistance = 0.1, maxDistance = 1000, fov = 50, vr: _vr = false, walkSpeed = 5, ambientIntensity = 0.5, directionalIntensity = 0.8, antiAlias = true, toneMapping = 'aces', exposure = 1, } = props;
68
- /* ── refs ── */
69
- const mountRef = React.useRef(null);
70
- const rendererRef = React.useRef(null);
71
- const sceneRef = React.useRef(new THREE__namespace.Scene());
72
- const cameraRef = React.useRef(new THREE__namespace.PerspectiveCamera(fov, 1, 0.01, 10000));
73
- const orbitRef = React.useRef(null);
74
- // Created once: useRef's argument runs on every render, and three r183 warns per Clock.
75
- const clockRef = React.useRef(null);
76
- (_a = clockRef.current) !== null && _a !== void 0 ? _a : (clockRef.current = threeCompat.createFrameClock());
77
- const mixerRef = React.useRef(null);
78
- const rafRef = React.useRef(0);
79
- const rootRef = React.useRef(null);
80
- const gridRef = React.useRef(null);
81
- const clipsRef = React.useRef([]);
82
- const keysRef = React.useRef(new Set());
83
- React.useRef(null);
84
- const disposeRef = React.useRef(false);
85
- /* ── state ── */
86
- const [loading, setLoading] = React.useState(false);
87
- const [loadProgress, setLoadProgress] = React.useState(0);
88
- const [error, setError] = React.useState(null);
89
- const [navMode, setNavMode] = React.useState(initNavMode);
90
- const [projection, setProjection] = React.useState('perspective');
91
- const [wireframe, setWireframe] = React.useState(false);
92
- const [gridVisible, setGridVisible] = React.useState(initShowGrid);
93
- const [autoRotate, setAutoRotate] = React.useState(initAutoRotate);
94
- const [polyCount, setPolyCount] = React.useState(0);
95
- const [meshCount, setMeshCount] = React.useState(0);
96
- const [animNames, setAnimNames] = React.useState([]);
97
- const [activeAnimIdx, setActiveAnimIdx] = React.useState(-1);
98
- const [isPlaying, setIsPlaying] = React.useState(false);
99
- const [animTime, setAnimTime] = React.useState(0);
100
- const [animDuration, setAnimDuration] = React.useState(0);
101
- const [animSpeed, setAnimSpeed] = React.useState(1);
102
- const [isFullscreen, setIsFullscreen] = React.useState(false);
103
- const [activeFloorPlanId, setActiveFloorPlanId] = React.useState(null);
104
- const [annotationsVisible, setAnnotationsVisible] = React.useState(true);
105
- const [clippingEnabled, setClippingEnabled] = React.useState(clippingPlanes.length > 0);
106
- const [clippingPosition, setClippingPosition] = React.useState(0);
107
- const [clippingAxis, setClippingAxis] = React.useState('y');
108
- const [measureModeActive, setMeasureModeActive] = React.useState(initMeasureMode);
109
- const [dimensions, setDimensions] = React.useState(null);
110
- const [statusText, setStatusText] = React.useState('Ready');
111
- /* ── scene setup ── */
112
- React.useEffect(() => {
113
- var _a;
114
- const mount = mountRef.current;
115
- if (!mount) {
116
- return;
117
- }
118
- disposeRef.current = false;
119
- const q = QUALITY_MAP[quality];
120
- const renderer = new THREE__namespace.WebGLRenderer({
121
- antialias: q.antialias && antiAlias,
122
- alpha: backgroundColor === 'transparent',
123
- preserveDrawingBuffer: true,
124
- });
125
- renderer.setPixelRatio(Math.min(window.devicePixelRatio, q.pixelRatio));
126
- renderer.setSize(mount.clientWidth, mount.clientHeight);
127
- renderer.toneMapping = (_a = TONE_MAP[toneMapping]) !== null && _a !== void 0 ? _a : THREE__namespace.ACESFilmicToneMapping;
128
- renderer.toneMappingExposure = exposure;
129
- renderer.outputColorSpace = THREE__namespace.SRGBColorSpace;
130
- if (shadows) {
131
- renderer.shadowMap.enabled = true;
132
- renderer.shadowMap.type = threeCompat.SOFT_SHADOW_MAP;
133
- }
134
- mount.appendChild(renderer.domElement);
135
- rendererRef.current = renderer;
136
- const scene = sceneRef.current;
137
- if (backgroundColor !== 'transparent') {
138
- // The default is a var() token, which THREE.Color cannot parse (it warns and paints
139
- // white); resolve it against the viewer first.
140
- scene.background = new THREE__namespace.Color(ui.resolveCssColor(backgroundColor, mount));
141
- }
142
- // Lights
143
- const amb = new THREE__namespace.AmbientLight(0xffffff, ambientIntensity);
144
- scene.add(amb);
145
- const dir = new THREE__namespace.DirectionalLight(0xffffff, directionalIntensity);
146
- dir.position.set(5, 10, 7);
147
- if (shadows) {
148
- dir.castShadow = true;
149
- dir.shadow.mapSize.setScalar(q.shadowMapSize);
150
- }
151
- scene.add(dir);
152
- const hemi = new THREE__namespace.HemisphereLight(0xaaccff, 0x445522, 0.3);
153
- scene.add(hemi);
154
- // Grid
155
- const grid = new THREE__namespace.GridHelper(50, 50, 0x444444, 0x333333);
156
- grid.visible = initShowGrid;
157
- scene.add(grid);
158
- gridRef.current = grid;
159
- // Camera
160
- const camera = cameraRef.current;
161
- camera.position.set(5, 4, 8);
162
- camera.lookAt(0, 0, 0);
163
- // OrbitControls
164
- const orbit = new OrbitControls_js.OrbitControls(camera, renderer.domElement);
165
- orbit.enableDamping = true;
166
- orbit.dampingFactor = 0.08;
167
- orbit.minDistance = minDistance;
168
- orbit.maxDistance = maxDistance;
169
- orbit.autoRotate = initAutoRotate > 0;
170
- orbit.autoRotateSpeed = initAutoRotate;
171
- orbitRef.current = orbit;
172
- // Resize observer
173
- const ro = new ResizeObserver(() => {
174
- if (!mount || disposeRef.current) {
175
- return;
176
- }
177
- const w = mount.clientWidth;
178
- const h = mount.clientHeight;
179
- camera.aspect = w / h;
180
- camera.updateProjectionMatrix();
181
- renderer.setSize(w, h);
182
- });
183
- ro.observe(mount);
184
- // Animation loop
185
- const animate = () => {
186
- var _a, _b, _c, _d, _e, _f;
187
- if (disposeRef.current) {
188
- return;
189
- }
190
- rafRef.current = requestAnimationFrame(animate);
191
- const dt = (_b = (_a = clockRef.current) === null || _a === void 0 ? void 0 : _a.getDelta()) !== null && _b !== void 0 ? _b : 0;
192
- // Gamepad FPS-style nav
193
- if (gamepad && navMode === 'fps') {
194
- // The pads the input engine read this frame (1.3.0: the engine is the only pad reader).
195
- const gp = (_c = ui.getInputEngine().gamepadSnapshot[0]) !== null && _c !== void 0 ? _c : null;
196
- if (gp) {
197
- const lx = (_d = gp.axes[0]) !== null && _d !== void 0 ? _d : 0;
198
- const ly = (_e = gp.axes[1]) !== null && _e !== void 0 ? _e : 0;
199
- const rx = (_f = gp.axes[2]) !== null && _f !== void 0 ? _f : 0;
200
- const fwd = new THREE__namespace.Vector3();
201
- camera.getWorldDirection(fwd);
202
- fwd.y = 0;
203
- fwd.normalize();
204
- const right = new THREE__namespace.Vector3().crossVectors(fwd, camera.up).normalize();
205
- camera.position.addScaledVector(fwd, -ly * walkSpeed * dt);
206
- camera.position.addScaledVector(right, lx * walkSpeed * dt);
207
- orbit.target.addScaledVector(fwd, -ly * walkSpeed * dt);
208
- orbit.target.addScaledVector(right, lx * walkSpeed * dt);
209
- camera.rotateY(-rx * 2 * dt);
210
- }
211
- }
212
- // Keyboard WASD
213
- if (navMode === 'fps' || navMode === 'walkthrough') {
214
- const fwd = new THREE__namespace.Vector3();
215
- camera.getWorldDirection(fwd);
216
- fwd.y = 0;
217
- fwd.normalize();
218
- const right = new THREE__namespace.Vector3().crossVectors(fwd, camera.up).normalize();
219
- const speed = walkSpeed * dt;
220
- if (keysRef.current.has('w') || keysRef.current.has('arrowup')) {
221
- camera.position.addScaledVector(fwd, speed);
222
- orbit.target.addScaledVector(fwd, speed);
223
- }
224
- if (keysRef.current.has('s') || keysRef.current.has('arrowdown')) {
225
- camera.position.addScaledVector(fwd, -speed);
226
- orbit.target.addScaledVector(fwd, -speed);
227
- }
228
- if (keysRef.current.has('a') || keysRef.current.has('arrowleft')) {
229
- camera.position.addScaledVector(right, -speed);
230
- orbit.target.addScaledVector(right, -speed);
231
- }
232
- if (keysRef.current.has('d') || keysRef.current.has('arrowright')) {
233
- camera.position.addScaledVector(right, speed);
234
- orbit.target.addScaledVector(right, speed);
235
- }
236
- if (keysRef.current.has('q')) {
237
- camera.position.y += speed;
238
- orbit.target.y += speed;
239
- }
240
- if (keysRef.current.has('e')) {
241
- camera.position.y -= speed;
242
- orbit.target.y -= speed;
243
- }
244
- }
245
- if (mixerRef.current) {
246
- mixerRef.current.update(dt);
247
- }
248
- orbit.update();
249
- renderer.render(scene, camera);
250
- };
251
- animate();
252
- // Keyboard listeners
253
- const onKeyDown = (e) => keysRef.current.add(e.key.toLowerCase());
254
- const onKeyUp = (e) => keysRef.current.delete(e.key.toLowerCase());
255
- mount.addEventListener('keydown', onKeyDown);
256
- mount.addEventListener('keyup', onKeyUp);
257
- // Fullscreen
258
- const onFSChange = () => setIsFullscreen(!!document.fullscreenElement);
259
- document.addEventListener('fullscreenchange', onFSChange);
260
- return () => {
261
- disposeRef.current = true;
262
- cancelAnimationFrame(rafRef.current);
263
- ro.disconnect();
264
- orbit.dispose();
265
- renderer.dispose();
266
- mount.removeEventListener('keydown', onKeyDown);
267
- mount.removeEventListener('keyup', onKeyUp);
268
- document.removeEventListener('fullscreenchange', onFSChange);
269
- if (mount.contains(renderer.domElement)) {
270
- mount.removeChild(renderer.domElement);
271
- }
272
- };
273
- // eslint-disable-next-line react-hooks/exhaustive-deps
274
- }, []);
275
- /* ── update grid visibility ── */
276
- React.useEffect(() => {
277
- if (gridRef.current) {
278
- gridRef.current.visible = gridVisible;
279
- }
280
- }, [gridVisible]);
281
- /* ── update auto-rotate ── */
282
- React.useEffect(() => {
283
- if (orbitRef.current) {
284
- orbitRef.current.autoRotate = autoRotate > 0;
285
- orbitRef.current.autoRotateSpeed = autoRotate;
286
- }
287
- }, [autoRotate]);
288
- /* ── wireframe toggle ── */
289
- React.useEffect(() => {
290
- if (!rootRef.current) {
291
- return;
292
- }
293
- rootRef.current.traverse((c) => {
294
- if (c.isMesh) {
295
- const mat = c.material;
296
- const mats = Array.isArray(mat) ? mat : [mat];
297
- mats.forEach((m) => {
298
- if ('wireframe' in m) {
299
- m.wireframe = wireframe;
300
- }
301
- });
302
- }
46
+ const { t } = i18n.useNiceTranslation();
47
+ const {
48
+ src,
49
+ format,
50
+ navigationMode: initNavMode = "orbit",
51
+ quality = "high",
52
+ autoRotateSpeed: initAutoRotate = 0,
53
+ backgroundColor = "var(--nice-bg, #1a1a2e)",
54
+ showGrid: initShowGrid = true,
55
+ shadows = true,
56
+ gamepad = false,
57
+ cameraPresets: _cameraPresets = [],
58
+ annotations: _annotations = [],
59
+ hotspots: _hotspots = [],
60
+ floorPlans: _floorPlans = [],
61
+ onHotspotClick: _onHotspotClick,
62
+ onAnnotationClick: _onAnnotationClick,
63
+ onLoadProgress,
64
+ onLoad,
65
+ onError,
66
+ clippingPlanes = [],
67
+ measureMode: initMeasureMode = false,
68
+ measurements: _measurements = [],
69
+ onMeasure: _onMeasure,
70
+ minDistance = 0.1,
71
+ maxDistance = 1e3,
72
+ fov = 50,
73
+ vr: _vr = false,
74
+ walkSpeed = 5,
75
+ ambientIntensity = 0.5,
76
+ directionalIntensity = 0.8,
77
+ antiAlias = true,
78
+ toneMapping = "aces",
79
+ exposure = 1
80
+ } = props;
81
+ const mountRef = React.useRef(null);
82
+ const rendererRef = React.useRef(null);
83
+ const sceneRef = React.useRef(new THREE__namespace.Scene());
84
+ const cameraRef = React.useRef(new THREE__namespace.PerspectiveCamera(fov, 1, 0.01, 1e4));
85
+ const orbitRef = React.useRef(null);
86
+ const clockRef = React.useRef(null);
87
+ clockRef.current ?? (clockRef.current = threeCompat.createFrameClock());
88
+ const mixerRef = React.useRef(null);
89
+ const rafRef = React.useRef(0);
90
+ const rootRef = React.useRef(null);
91
+ const gridRef = React.useRef(null);
92
+ const clipsRef = React.useRef([]);
93
+ const keysRef = React.useRef(/* @__PURE__ */ new Set());
94
+ React.useRef(null);
95
+ const disposeRef = React.useRef(false);
96
+ const [loading, setLoading] = React.useState(false);
97
+ const [loadProgress, setLoadProgress] = React.useState(0);
98
+ const [error, setError] = React.useState(null);
99
+ const [navMode, setNavMode] = React.useState(initNavMode);
100
+ const [projection, setProjection] = React.useState("perspective");
101
+ const [wireframe, setWireframe] = React.useState(false);
102
+ const [gridVisible, setGridVisible] = React.useState(initShowGrid);
103
+ const [autoRotate, setAutoRotate] = React.useState(initAutoRotate);
104
+ const [polyCount, setPolyCount] = React.useState(0);
105
+ const [meshCount, setMeshCount] = React.useState(0);
106
+ const [animNames, setAnimNames] = React.useState([]);
107
+ const [activeAnimIdx, setActiveAnimIdx] = React.useState(-1);
108
+ const [isPlaying, setIsPlaying] = React.useState(false);
109
+ const [animTime, setAnimTime] = React.useState(0);
110
+ const [animDuration, setAnimDuration] = React.useState(0);
111
+ const [animSpeed, setAnimSpeed] = React.useState(1);
112
+ const [isFullscreen, setIsFullscreen] = React.useState(false);
113
+ const [activeFloorPlanId, setActiveFloorPlanId] = React.useState(null);
114
+ const [annotationsVisible, setAnnotationsVisible] = React.useState(true);
115
+ const [clippingEnabled, setClippingEnabled] = React.useState(clippingPlanes.length > 0);
116
+ const [clippingPosition, setClippingPosition] = React.useState(0);
117
+ const [clippingAxis, setClippingAxis] = React.useState("y");
118
+ const [measureModeActive, setMeasureModeActive] = React.useState(initMeasureMode);
119
+ const [dimensions, setDimensions] = React.useState(null);
120
+ const [statusText, setStatusText] = React.useState("Ready");
121
+ React.useEffect(() => {
122
+ const mount = mountRef.current;
123
+ if (!mount) {
124
+ return;
125
+ }
126
+ disposeRef.current = false;
127
+ const q = QUALITY_MAP[quality];
128
+ const renderer = new THREE__namespace.WebGLRenderer({
129
+ antialias: q.antialias && antiAlias,
130
+ alpha: backgroundColor === "transparent",
131
+ preserveDrawingBuffer: true
132
+ });
133
+ renderer.setPixelRatio(Math.min(window.devicePixelRatio, q.pixelRatio));
134
+ renderer.setSize(mount.clientWidth, mount.clientHeight);
135
+ renderer.toneMapping = TONE_MAP[toneMapping] ?? THREE__namespace.ACESFilmicToneMapping;
136
+ renderer.toneMappingExposure = exposure;
137
+ renderer.outputColorSpace = THREE__namespace.SRGBColorSpace;
138
+ if (shadows) {
139
+ renderer.shadowMap.enabled = true;
140
+ renderer.shadowMap.type = threeCompat.SOFT_SHADOW_MAP;
141
+ }
142
+ mount.appendChild(renderer.domElement);
143
+ rendererRef.current = renderer;
144
+ const scene = sceneRef.current;
145
+ if (backgroundColor !== "transparent") {
146
+ scene.background = new THREE__namespace.Color(ui.resolveCssColor(backgroundColor, mount));
147
+ }
148
+ const amb = new THREE__namespace.AmbientLight(16777215, ambientIntensity);
149
+ scene.add(amb);
150
+ const dir = new THREE__namespace.DirectionalLight(16777215, directionalIntensity);
151
+ dir.position.set(5, 10, 7);
152
+ if (shadows) {
153
+ dir.castShadow = true;
154
+ dir.shadow.mapSize.setScalar(q.shadowMapSize);
155
+ }
156
+ scene.add(dir);
157
+ const hemi = new THREE__namespace.HemisphereLight(11193599, 4478242, 0.3);
158
+ scene.add(hemi);
159
+ const grid = new THREE__namespace.GridHelper(50, 50, 4473924, 3355443);
160
+ grid.visible = initShowGrid;
161
+ scene.add(grid);
162
+ gridRef.current = grid;
163
+ const camera = cameraRef.current;
164
+ camera.position.set(5, 4, 8);
165
+ camera.lookAt(0, 0, 0);
166
+ const orbit = new OrbitControls_js.OrbitControls(camera, renderer.domElement);
167
+ orbit.enableDamping = true;
168
+ orbit.dampingFactor = 0.08;
169
+ orbit.minDistance = minDistance;
170
+ orbit.maxDistance = maxDistance;
171
+ orbit.autoRotate = initAutoRotate > 0;
172
+ orbit.autoRotateSpeed = initAutoRotate;
173
+ orbitRef.current = orbit;
174
+ const ro = new ResizeObserver(() => {
175
+ if (!mount || disposeRef.current) {
176
+ return;
177
+ }
178
+ const w = mount.clientWidth;
179
+ const h = mount.clientHeight;
180
+ camera.aspect = w / h;
181
+ camera.updateProjectionMatrix();
182
+ renderer.setSize(w, h);
183
+ });
184
+ ro.observe(mount);
185
+ const animate = () => {
186
+ if (disposeRef.current) {
187
+ return;
188
+ }
189
+ rafRef.current = requestAnimationFrame(animate);
190
+ const dt = clockRef.current?.getDelta() ?? 0;
191
+ if (gamepad && navMode === "fps") {
192
+ const gp = ui.getInputEngine().gamepadSnapshot[0] ?? null;
193
+ if (gp) {
194
+ const lx = gp.axes[0] ?? 0;
195
+ const ly = gp.axes[1] ?? 0;
196
+ const rx = gp.axes[2] ?? 0;
197
+ const fwd = new THREE__namespace.Vector3();
198
+ camera.getWorldDirection(fwd);
199
+ fwd.y = 0;
200
+ fwd.normalize();
201
+ const right = new THREE__namespace.Vector3().crossVectors(fwd, camera.up).normalize();
202
+ camera.position.addScaledVector(fwd, -ly * walkSpeed * dt);
203
+ camera.position.addScaledVector(right, lx * walkSpeed * dt);
204
+ orbit.target.addScaledVector(fwd, -ly * walkSpeed * dt);
205
+ orbit.target.addScaledVector(right, lx * walkSpeed * dt);
206
+ camera.rotateY(-rx * 2 * dt);
207
+ }
208
+ }
209
+ if (navMode === "fps" || navMode === "walkthrough") {
210
+ const fwd = new THREE__namespace.Vector3();
211
+ camera.getWorldDirection(fwd);
212
+ fwd.y = 0;
213
+ fwd.normalize();
214
+ const right = new THREE__namespace.Vector3().crossVectors(fwd, camera.up).normalize();
215
+ const speed = walkSpeed * dt;
216
+ if (keysRef.current.has("w") || keysRef.current.has("arrowup")) {
217
+ camera.position.addScaledVector(fwd, speed);
218
+ orbit.target.addScaledVector(fwd, speed);
219
+ }
220
+ if (keysRef.current.has("s") || keysRef.current.has("arrowdown")) {
221
+ camera.position.addScaledVector(fwd, -speed);
222
+ orbit.target.addScaledVector(fwd, -speed);
223
+ }
224
+ if (keysRef.current.has("a") || keysRef.current.has("arrowleft")) {
225
+ camera.position.addScaledVector(right, -speed);
226
+ orbit.target.addScaledVector(right, -speed);
227
+ }
228
+ if (keysRef.current.has("d") || keysRef.current.has("arrowright")) {
229
+ camera.position.addScaledVector(right, speed);
230
+ orbit.target.addScaledVector(right, speed);
231
+ }
232
+ if (keysRef.current.has("q")) {
233
+ camera.position.y += speed;
234
+ orbit.target.y += speed;
235
+ }
236
+ if (keysRef.current.has("e")) {
237
+ camera.position.y -= speed;
238
+ orbit.target.y -= speed;
239
+ }
240
+ }
241
+ if (mixerRef.current) {
242
+ mixerRef.current.update(dt);
243
+ }
244
+ orbit.update();
245
+ renderer.render(scene, camera);
246
+ };
247
+ animate();
248
+ const onKeyDown = (e) => keysRef.current.add(e.key.toLowerCase());
249
+ const onKeyUp = (e) => keysRef.current.delete(e.key.toLowerCase());
250
+ mount.addEventListener("keydown", onKeyDown);
251
+ mount.addEventListener("keyup", onKeyUp);
252
+ const onFSChange = () => setIsFullscreen(!!document.fullscreenElement);
253
+ document.addEventListener("fullscreenchange", onFSChange);
254
+ return () => {
255
+ disposeRef.current = true;
256
+ cancelAnimationFrame(rafRef.current);
257
+ ro.disconnect();
258
+ orbit.dispose();
259
+ renderer.dispose();
260
+ mount.removeEventListener("keydown", onKeyDown);
261
+ mount.removeEventListener("keyup", onKeyUp);
262
+ document.removeEventListener("fullscreenchange", onFSChange);
263
+ if (mount.contains(renderer.domElement)) {
264
+ mount.removeChild(renderer.domElement);
265
+ }
266
+ };
267
+ }, []);
268
+ React.useEffect(() => {
269
+ if (gridRef.current) {
270
+ gridRef.current.visible = gridVisible;
271
+ }
272
+ }, [gridVisible]);
273
+ React.useEffect(() => {
274
+ if (orbitRef.current) {
275
+ orbitRef.current.autoRotate = autoRotate > 0;
276
+ orbitRef.current.autoRotateSpeed = autoRotate;
277
+ }
278
+ }, [autoRotate]);
279
+ React.useEffect(() => {
280
+ if (!rootRef.current) {
281
+ return;
282
+ }
283
+ rootRef.current.traverse((c) => {
284
+ if (c.isMesh) {
285
+ const mat = c.material;
286
+ const mats = Array.isArray(mat) ? mat : [mat];
287
+ mats.forEach((m) => {
288
+ if ("wireframe" in m) {
289
+ m.wireframe = wireframe;
290
+ }
303
291
  });
304
- }, [wireframe]);
305
- /* ── clipping ── */
306
- React.useEffect(() => {
307
- const renderer = rendererRef.current;
308
- if (!renderer) {
309
- return;
310
- }
311
- if (clippingEnabled) {
312
- const normal = new THREE__namespace.Vector3(clippingAxis === 'x' ? 1 : 0, clippingAxis === 'y' ? 1 : 0, clippingAxis === 'z' ? 1 : 0);
313
- const plane = new THREE__namespace.Plane(normal, -clippingPosition);
314
- renderer.clippingPlanes = [plane];
315
- }
316
- else {
317
- renderer.clippingPlanes = [];
318
- }
319
- }, [clippingEnabled, clippingPosition, clippingAxis]);
320
- /* ── counting helpers ── */
321
- const countScene = React.useCallback(() => {
322
- let tris = 0;
323
- let meshes = 0;
324
- sceneRef.current.traverse((c) => {
292
+ }
293
+ });
294
+ }, [wireframe]);
295
+ React.useEffect(() => {
296
+ const renderer = rendererRef.current;
297
+ if (!renderer) {
298
+ return;
299
+ }
300
+ if (clippingEnabled) {
301
+ const normal = new THREE__namespace.Vector3(
302
+ clippingAxis === "x" ? 1 : 0,
303
+ clippingAxis === "y" ? 1 : 0,
304
+ clippingAxis === "z" ? 1 : 0
305
+ );
306
+ const plane = new THREE__namespace.Plane(normal, -clippingPosition);
307
+ renderer.clippingPlanes = [plane];
308
+ } else {
309
+ renderer.clippingPlanes = [];
310
+ }
311
+ }, [clippingEnabled, clippingPosition, clippingAxis]);
312
+ const countScene = React.useCallback(() => {
313
+ let tris = 0;
314
+ let meshes = 0;
315
+ sceneRef.current.traverse((c) => {
316
+ if (c.isMesh) {
317
+ meshes++;
318
+ const geo = c.geometry;
319
+ if (geo.index) {
320
+ tris += geo.index.count / 3;
321
+ } else if (geo.attributes.position) {
322
+ tris += geo.attributes.position.count / 3;
323
+ }
324
+ }
325
+ });
326
+ setPolyCount(Math.round(tris));
327
+ setMeshCount(meshes);
328
+ }, []);
329
+ const detectFormat = React.useCallback((url, hint) => {
330
+ if (hint) {
331
+ return hint;
332
+ }
333
+ const ext = url.split(".").pop()?.split("?")[0]?.toLowerCase() ?? "";
334
+ return ext;
335
+ }, []);
336
+ const loadModel = React.useCallback(
337
+ async (url, fmt) => {
338
+ setLoading(true);
339
+ setLoadProgress(0);
340
+ setError(null);
341
+ setStatusText(t("useModelViewer.loading", "Loading…"));
342
+ const detectedFmt = detectFormat(url, fmt);
343
+ const onProgress = (e) => {
344
+ if (e.lengthComputable) {
345
+ const p = e.loaded / e.total;
346
+ setLoadProgress(p);
347
+ onLoadProgress?.(p);
348
+ }
349
+ };
350
+ try {
351
+ let obj = null;
352
+ let clips = [];
353
+ if (detectedFmt === "fbx") {
354
+ const loader = new FBXLoader_js.FBXLoader();
355
+ const group = await loader.loadAsync(url, onProgress);
356
+ clips = group.animations ?? [];
357
+ obj = group;
358
+ } else if (detectedFmt === "glb" || detectedFmt === "gltf") {
359
+ const loader = new GLTFLoader_js.GLTFLoader();
360
+ const gltf = await loader.loadAsync(url, onProgress);
361
+ clips = gltf.animations ?? [];
362
+ obj = gltf.scene;
363
+ } else if (detectedFmt === "obj") {
364
+ const loader = new OBJLoader_js.OBJLoader();
365
+ obj = await loader.loadAsync(url, onProgress);
366
+ } else if (detectedFmt === "dae") {
367
+ const loader = new ColladaLoader_js.ColladaLoader();
368
+ const result = await loader.loadAsync(url, onProgress);
369
+ if (result) {
370
+ clips = result.scene.animations ?? [];
371
+ obj = result.scene;
372
+ }
373
+ } else if (detectedFmt === "stl") {
374
+ const loader = new STLLoader_js.STLLoader();
375
+ const geo = await loader.loadAsync(url, onProgress);
376
+ const mat = new THREE__namespace.MeshStandardMaterial({
377
+ color: 11184810,
378
+ metalness: 0.3,
379
+ roughness: 0.6
380
+ });
381
+ const mesh = new THREE__namespace.Mesh(geo, mat);
382
+ mesh.castShadow = true;
383
+ mesh.receiveShadow = true;
384
+ obj = mesh;
385
+ } else if (detectedFmt === "ply") {
386
+ const loader = new PLYLoader_js.PLYLoader();
387
+ const geo = await loader.loadAsync(url, onProgress);
388
+ geo.computeVertexNormals();
389
+ const mat = new THREE__namespace.MeshStandardMaterial({
390
+ color: 11184810,
391
+ vertexColors: !!geo.attributes.color
392
+ });
393
+ obj = new THREE__namespace.Mesh(geo, mat);
394
+ } else {
395
+ throw new Error(`Unsupported format: ${detectedFmt}`);
396
+ }
397
+ if (obj) {
398
+ if (rootRef.current) {
399
+ sceneRef.current.remove(rootRef.current);
400
+ }
401
+ obj.traverse((c) => {
325
402
  if (c.isMesh) {
326
- meshes++;
327
- const geo = c.geometry;
328
- if (geo.index) {
329
- tris += geo.index.count / 3;
330
- }
331
- else if (geo.attributes.position) {
332
- tris += geo.attributes.position.count / 3;
333
- }
334
- }
335
- });
336
- setPolyCount(Math.round(tris));
337
- setMeshCount(meshes);
338
- }, []);
339
- /* ── detect format from URL ── */
340
- const detectFormat = React.useCallback((url, hint) => {
341
- var _a, _b, _c;
342
- if (hint) {
343
- return hint;
344
- }
345
- const ext = (_c = (_b = (_a = url.split('.').pop()) === null || _a === void 0 ? void 0 : _a.split('?')[0]) === null || _b === void 0 ? void 0 : _b.toLowerCase()) !== null && _c !== void 0 ? _c : '';
346
- return ext;
347
- }, []);
348
- /* ── load model ── */
349
- const loadModel = React.useCallback(async (url, fmt) => {
350
- var _a, _b, _c;
351
- setLoading(true);
352
- setLoadProgress(0);
353
- setError(null);
354
- setStatusText(t('useModelViewer.loading', 'Loading…'));
355
- const detectedFmt = detectFormat(url, fmt);
356
- const onProgress = (e) => {
357
- if (e.lengthComputable) {
358
- const p = e.loaded / e.total;
359
- setLoadProgress(p);
360
- onLoadProgress === null || onLoadProgress === void 0 ? void 0 : onLoadProgress(p);
361
- }
362
- };
363
- try {
364
- let obj = null;
365
- let clips = [];
366
- if (detectedFmt === 'fbx') {
367
- const loader = new FBXLoader_js.FBXLoader();
368
- const group = await loader.loadAsync(url, onProgress);
369
- clips = (_a = group.animations) !== null && _a !== void 0 ? _a : [];
370
- obj = group;
371
- }
372
- else if (detectedFmt === 'glb' || detectedFmt === 'gltf') {
373
- const loader = new GLTFLoader_js.GLTFLoader();
374
- const gltf = await loader.loadAsync(url, onProgress);
375
- clips = (_b = gltf.animations) !== null && _b !== void 0 ? _b : [];
376
- obj = gltf.scene;
377
- }
378
- else if (detectedFmt === 'obj') {
379
- const loader = new OBJLoader_js.OBJLoader();
380
- obj = await loader.loadAsync(url, onProgress);
403
+ c.castShadow = true;
404
+ c.receiveShadow = true;
381
405
  }
382
- else if (detectedFmt === 'dae') {
383
- const loader = new ColladaLoader_js.ColladaLoader();
384
- const result = await loader.loadAsync(url, onProgress);
385
- if (result) {
386
- clips = (_c = result.scene.animations) !== null && _c !== void 0 ? _c : [];
387
- obj = result.scene;
388
- }
406
+ });
407
+ const box = new THREE__namespace.Box3().setFromObject(obj);
408
+ const size = box.getSize(new THREE__namespace.Vector3());
409
+ const maxDim = Math.max(size.x, size.y, size.z);
410
+ if (maxDim > 0) {
411
+ const target = 5;
412
+ if (maxDim > target * 2 || maxDim < target * 0.1) {
413
+ const scale = target / maxDim;
414
+ obj.scale.multiplyScalar(scale);
389
415
  }
390
- else if (detectedFmt === 'stl') {
391
- const loader = new STLLoader_js.STLLoader();
392
- const geo = await loader.loadAsync(url, onProgress);
393
- const mat = new THREE__namespace.MeshStandardMaterial({
394
- color: 0xaaaaaa,
395
- metalness: 0.3,
396
- roughness: 0.6,
397
- });
398
- const mesh = new THREE__namespace.Mesh(geo, mat);
399
- mesh.castShadow = true;
400
- mesh.receiveShadow = true;
401
- obj = mesh;
402
- }
403
- else if (detectedFmt === 'ply') {
404
- const loader = new PLYLoader_js.PLYLoader();
405
- const geo = await loader.loadAsync(url, onProgress);
406
- geo.computeVertexNormals();
407
- const mat = new THREE__namespace.MeshStandardMaterial({
408
- color: 0xaaaaaa,
409
- vertexColors: !!geo.attributes.color,
410
- });
411
- obj = new THREE__namespace.Mesh(geo, mat);
412
- }
413
- else {
414
- throw new Error(`Unsupported format: ${detectedFmt}`);
415
- }
416
- if (obj) {
417
- // Remove previous model
418
- if (rootRef.current) {
419
- sceneRef.current.remove(rootRef.current);
420
- }
421
- // Enable shadows
422
- obj.traverse((c) => {
423
- if (c.isMesh) {
424
- c.castShadow = true;
425
- c.receiveShadow = true;
426
- }
427
- });
428
- // Auto-scale to normalized size
429
- const box = new THREE__namespace.Box3().setFromObject(obj);
430
- const size = box.getSize(new THREE__namespace.Vector3());
431
- const maxDim = Math.max(size.x, size.y, size.z);
432
- if (maxDim > 0) {
433
- const target = 5;
434
- if (maxDim > target * 2 || maxDim < target * 0.1) {
435
- const scale = target / maxDim;
436
- obj.scale.multiplyScalar(scale);
437
- }
438
- }
439
- // Center model
440
- const box2 = new THREE__namespace.Box3().setFromObject(obj);
441
- const center = box2.getCenter(new THREE__namespace.Vector3());
442
- obj.position.sub(center);
443
- obj.position.y += box2.getSize(new THREE__namespace.Vector3()).y / 2;
444
- sceneRef.current.add(obj);
445
- rootRef.current = obj;
446
- // Update dimensions
447
- const finalBox = new THREE__namespace.Box3().setFromObject(obj);
448
- const s = finalBox.getSize(new THREE__namespace.Vector3());
449
- setDimensions({
450
- width: +s.x.toFixed(2),
451
- height: +s.y.toFixed(2),
452
- depth: +s.z.toFixed(2),
453
- });
454
- // Animations
455
- clipsRef.current = clips;
456
- setAnimNames(clips.map((c, i) => c.name || `Clip ${i + 1}`));
457
- if (clips.length > 0) {
458
- const mixer = new THREE__namespace.AnimationMixer(obj);
459
- mixerRef.current = mixer;
460
- }
461
- countScene();
462
- setStatusText(t('useModelViewer.loadedTrisMeshes', 'Loaded – {polyCount} tris, {meshCount} meshes')
463
- .replace('{polyCount}', String(polyCount))
464
- .replace('{meshCount}', String(meshCount)));
465
- onLoad === null || onLoad === void 0 ? void 0 : onLoad();
466
- }
467
- }
468
- catch (err) {
469
- const msg = err instanceof Error ? err.message : String(err);
470
- setError(msg);
471
- setStatusText(t('useModelViewer.error', 'Error: {msg}').replace('{msg}', String(msg)));
472
- onError === null || onError === void 0 ? void 0 : onError(err instanceof Error ? err : new Error(msg));
473
- }
474
- finally {
475
- setLoading(false);
476
- setLoadProgress(1);
477
- }
478
- }, [countScene, detectFormat, onLoad, onError, onLoadProgress, polyCount, meshCount, t]);
479
- /* ── auto-load from src ── */
480
- React.useEffect(() => {
481
- if (src) {
482
- loadModel(src, format);
483
- }
484
- }, [src, format, loadModel]);
485
- /* ── loadFile from disk ── */
486
- const loadFile = React.useCallback(async (file) => {
487
- var _a;
488
- const url = URL.createObjectURL(file);
489
- const ext = (_a = file.name.split('.').pop()) === null || _a === void 0 ? void 0 : _a.toLowerCase();
490
- try {
491
- await loadModel(url, ext);
492
- }
493
- finally {
494
- URL.revokeObjectURL(url);
495
- }
496
- }, [loadModel]);
497
- /* ── loadUrl ── */
498
- const loadUrl = React.useCallback(async (url, fmt) => {
499
- await loadModel(url, fmt);
500
- }, [loadModel]);
501
- /* ── animation controls ── */
502
- const playAnimation = React.useCallback((index) => {
503
- if (!mixerRef.current || index < 0 || index >= clipsRef.current.length) {
504
- return;
505
- }
506
- mixerRef.current.stopAllAction();
507
- const clip = clipsRef.current[index];
508
- const action = mixerRef.current.clipAction(clip);
509
- action.reset().play();
510
- setActiveAnimIdx(index);
511
- setIsPlaying(true);
512
- setAnimDuration(clip.duration);
513
- }, []);
514
- const togglePlay = React.useCallback(() => {
515
- if (!mixerRef.current) {
516
- return;
517
- }
518
- if (isPlaying) {
519
- mixerRef.current.timeScale = 0;
520
- setIsPlaying(false);
521
- }
522
- else {
523
- mixerRef.current.timeScale = animSpeed;
524
- setIsPlaying(true);
525
- if (activeAnimIdx < 0 && clipsRef.current.length > 0) {
526
- playAnimation(0);
527
- }
528
- }
529
- }, [isPlaying, animSpeed, activeAnimIdx, playAnimation]);
530
- const seekAnim = React.useCallback((t) => {
531
- if (mixerRef.current) {
532
- mixerRef.current.setTime(t);
533
- }
534
- setAnimTime(t);
535
- }, []);
536
- /* ── camera presets ── */
537
- const goToPreset = React.useCallback((preset) => {
538
- const cam = cameraRef.current;
539
- const orbit = orbitRef.current;
540
- if (!orbit) {
541
- return;
542
- }
543
- cam.position.set(...preset.position);
544
- orbit.target.set(...preset.target);
545
- if (preset.fov) {
546
- cam.fov = preset.fov;
547
- }
548
- cam.updateProjectionMatrix();
549
- orbit.update();
550
- }, []);
551
- const resetCamera = React.useCallback(() => {
552
- const cam = cameraRef.current;
553
- cam.position.set(5, 4, 8);
554
- if (orbitRef.current) {
555
- orbitRef.current.target.set(0, 0, 0);
556
- orbitRef.current.update();
557
- }
558
- }, []);
559
- const zoomToFit = React.useCallback(() => {
560
- if (!rootRef.current) {
561
- return;
562
- }
563
- const box = new THREE__namespace.Box3().setFromObject(rootRef.current);
564
- const center = box.getCenter(new THREE__namespace.Vector3());
565
- const size = box.getSize(new THREE__namespace.Vector3());
566
- const maxDim = Math.max(size.x, size.y, size.z);
567
- const cam = cameraRef.current;
568
- const dist = maxDim / (2 * Math.tan((cam.fov * Math.PI) / 360));
569
- cam.position.copy(center).add(new THREE__namespace.Vector3(0, 0, dist * 1.5));
570
- if (orbitRef.current) {
571
- orbitRef.current.target.copy(center);
572
- orbitRef.current.update();
573
- }
574
- }, []);
575
- /* ── screenshot ── */
576
- const screenshot = React.useCallback((width, height) => {
577
- const renderer = rendererRef.current;
578
- if (!renderer) {
579
- return '';
580
- }
581
- if (width && height) {
582
- renderer.setSize(width, height);
583
- cameraRef.current.aspect = width / height;
584
- cameraRef.current.updateProjectionMatrix();
585
- }
586
- renderer.render(sceneRef.current, cameraRef.current);
587
- const url = renderer.domElement.toDataURL('image/png');
588
- if (width && height && mountRef.current) {
589
- const w = mountRef.current.clientWidth;
590
- const h = mountRef.current.clientHeight;
591
- renderer.setSize(w, h);
592
- cameraRef.current.aspect = w / h;
593
- cameraRef.current.updateProjectionMatrix();
594
- }
595
- return url;
596
- }, []);
597
- /* ── fullscreen ── */
598
- const toggleFullscreen = React.useCallback(() => {
599
- if (!mountRef.current) {
600
- return;
601
- }
602
- if (document.fullscreenElement) {
603
- document.exitFullscreen();
604
- }
605
- else {
606
- mountRef.current.requestFullscreen();
607
- }
608
- }, []);
609
- /* ── dispose ── */
610
- const dispose = React.useCallback(() => {
611
- var _a;
612
- disposeRef.current = true;
613
- cancelAnimationFrame(rafRef.current);
614
- (_a = rendererRef.current) === null || _a === void 0 ? void 0 : _a.dispose();
615
- }, []);
616
- /* ── finalise status text ── */
617
- React.useEffect(() => {
618
- if (!loading && !error && polyCount > 0) {
619
- setStatusText(t('useModelViewer.trianglesMeshesAnimationS', '{polyCount} triangles · {meshCount} meshes · {animNames} animation(s)')
620
- .replace('{polyCount}', String(polyCount.toLocaleString()))
621
- .replace('{meshCount}', String(meshCount))
622
- .replace('{animNames}', String(animNames.length)));
623
- }
624
- }, [loading, error, polyCount, meshCount, animNames.length, t]);
625
- return {
626
- mountRef,
627
- loading,
628
- loadProgress,
629
- error,
630
- navigationMode: navMode,
631
- setNavigationMode: setNavMode,
632
- projection,
633
- setProjection,
634
- wireframe,
635
- setWireframe,
636
- showGrid: gridVisible,
637
- setShowGrid: setGridVisible,
638
- autoRotateSpeed: autoRotate,
639
- setAutoRotateSpeed: setAutoRotate,
640
- polyCount,
641
- meshCount,
642
- animations: animNames,
643
- activeAnimIdx,
644
- playAnimation,
645
- togglePlay,
646
- isPlaying,
647
- animTime,
648
- animDuration,
649
- seekAnim,
650
- animSpeed,
651
- setAnimSpeed,
652
- goToPreset,
653
- resetCamera,
654
- zoomToFit,
655
- screenshot,
656
- toggleFullscreen,
657
- isFullscreen,
658
- loadFile,
659
- loadUrl,
660
- activeFloorPlanId,
661
- setActiveFloorPlanId,
662
- annotationsVisible,
663
- setAnnotationsVisible,
664
- measureMode: measureModeActive,
665
- setMeasureMode: setMeasureModeActive,
666
- clippingEnabled,
667
- setClippingEnabled,
668
- clippingPosition,
669
- setClippingPosition,
670
- clippingAxis,
671
- setClippingAxis,
672
- dimensions,
673
- statusText,
674
- dispose,
675
- };
416
+ }
417
+ const box2 = new THREE__namespace.Box3().setFromObject(obj);
418
+ const center = box2.getCenter(new THREE__namespace.Vector3());
419
+ obj.position.sub(center);
420
+ obj.position.y += box2.getSize(new THREE__namespace.Vector3()).y / 2;
421
+ sceneRef.current.add(obj);
422
+ rootRef.current = obj;
423
+ const finalBox = new THREE__namespace.Box3().setFromObject(obj);
424
+ const s = finalBox.getSize(new THREE__namespace.Vector3());
425
+ setDimensions({
426
+ width: +s.x.toFixed(2),
427
+ height: +s.y.toFixed(2),
428
+ depth: +s.z.toFixed(2)
429
+ });
430
+ clipsRef.current = clips;
431
+ setAnimNames(clips.map((c, i) => c.name || `Clip ${i + 1}`));
432
+ if (clips.length > 0) {
433
+ const mixer = new THREE__namespace.AnimationMixer(obj);
434
+ mixerRef.current = mixer;
435
+ }
436
+ countScene();
437
+ setStatusText(
438
+ t("useModelViewer.loadedTrisMeshes", "Loaded – {polyCount} tris, {meshCount} meshes").replace("{polyCount}", String(polyCount)).replace("{meshCount}", String(meshCount))
439
+ );
440
+ onLoad?.();
441
+ }
442
+ } catch (err) {
443
+ const msg = err instanceof Error ? err.message : String(err);
444
+ setError(msg);
445
+ setStatusText(t("useModelViewer.error", "Error: {msg}").replace("{msg}", String(msg)));
446
+ onError?.(err instanceof Error ? err : new Error(msg));
447
+ } finally {
448
+ setLoading(false);
449
+ setLoadProgress(1);
450
+ }
451
+ },
452
+ [countScene, detectFormat, onLoad, onError, onLoadProgress, polyCount, meshCount, t]
453
+ );
454
+ React.useEffect(() => {
455
+ if (src) {
456
+ loadModel(src, format);
457
+ }
458
+ }, [src, format, loadModel]);
459
+ const loadFile = React.useCallback(
460
+ async (file) => {
461
+ const url = URL.createObjectURL(file);
462
+ const ext = file.name.split(".").pop()?.toLowerCase();
463
+ try {
464
+ await loadModel(url, ext);
465
+ } finally {
466
+ URL.revokeObjectURL(url);
467
+ }
468
+ },
469
+ [loadModel]
470
+ );
471
+ const loadUrl = React.useCallback(
472
+ async (url, fmt) => {
473
+ await loadModel(url, fmt);
474
+ },
475
+ [loadModel]
476
+ );
477
+ const playAnimation = React.useCallback((index) => {
478
+ if (!mixerRef.current || index < 0 || index >= clipsRef.current.length) {
479
+ return;
480
+ }
481
+ mixerRef.current.stopAllAction();
482
+ const clip = clipsRef.current[index];
483
+ const action = mixerRef.current.clipAction(clip);
484
+ action.reset().play();
485
+ setActiveAnimIdx(index);
486
+ setIsPlaying(true);
487
+ setAnimDuration(clip.duration);
488
+ }, []);
489
+ const togglePlay = React.useCallback(() => {
490
+ if (!mixerRef.current) {
491
+ return;
492
+ }
493
+ if (isPlaying) {
494
+ mixerRef.current.timeScale = 0;
495
+ setIsPlaying(false);
496
+ } else {
497
+ mixerRef.current.timeScale = animSpeed;
498
+ setIsPlaying(true);
499
+ if (activeAnimIdx < 0 && clipsRef.current.length > 0) {
500
+ playAnimation(0);
501
+ }
502
+ }
503
+ }, [isPlaying, animSpeed, activeAnimIdx, playAnimation]);
504
+ const seekAnim = React.useCallback((t2) => {
505
+ if (mixerRef.current) {
506
+ mixerRef.current.setTime(t2);
507
+ }
508
+ setAnimTime(t2);
509
+ }, []);
510
+ const goToPreset = React.useCallback((preset) => {
511
+ const cam = cameraRef.current;
512
+ const orbit = orbitRef.current;
513
+ if (!orbit) {
514
+ return;
515
+ }
516
+ cam.position.set(...preset.position);
517
+ orbit.target.set(...preset.target);
518
+ if (preset.fov) {
519
+ cam.fov = preset.fov;
520
+ }
521
+ cam.updateProjectionMatrix();
522
+ orbit.update();
523
+ }, []);
524
+ const resetCamera = React.useCallback(() => {
525
+ const cam = cameraRef.current;
526
+ cam.position.set(5, 4, 8);
527
+ if (orbitRef.current) {
528
+ orbitRef.current.target.set(0, 0, 0);
529
+ orbitRef.current.update();
530
+ }
531
+ }, []);
532
+ const zoomToFit = React.useCallback(() => {
533
+ if (!rootRef.current) {
534
+ return;
535
+ }
536
+ const box = new THREE__namespace.Box3().setFromObject(rootRef.current);
537
+ const center = box.getCenter(new THREE__namespace.Vector3());
538
+ const size = box.getSize(new THREE__namespace.Vector3());
539
+ const maxDim = Math.max(size.x, size.y, size.z);
540
+ const cam = cameraRef.current;
541
+ const dist = maxDim / (2 * Math.tan(cam.fov * Math.PI / 360));
542
+ cam.position.copy(center).add(new THREE__namespace.Vector3(0, 0, dist * 1.5));
543
+ if (orbitRef.current) {
544
+ orbitRef.current.target.copy(center);
545
+ orbitRef.current.update();
546
+ }
547
+ }, []);
548
+ const screenshot = React.useCallback((width, height) => {
549
+ const renderer = rendererRef.current;
550
+ if (!renderer) {
551
+ return "";
552
+ }
553
+ if (width && height) {
554
+ renderer.setSize(width, height);
555
+ cameraRef.current.aspect = width / height;
556
+ cameraRef.current.updateProjectionMatrix();
557
+ }
558
+ renderer.render(sceneRef.current, cameraRef.current);
559
+ const url = renderer.domElement.toDataURL("image/png");
560
+ if (width && height && mountRef.current) {
561
+ const w = mountRef.current.clientWidth;
562
+ const h = mountRef.current.clientHeight;
563
+ renderer.setSize(w, h);
564
+ cameraRef.current.aspect = w / h;
565
+ cameraRef.current.updateProjectionMatrix();
566
+ }
567
+ return url;
568
+ }, []);
569
+ const toggleFullscreen = React.useCallback(() => {
570
+ if (!mountRef.current) {
571
+ return;
572
+ }
573
+ if (document.fullscreenElement) {
574
+ document.exitFullscreen();
575
+ } else {
576
+ mountRef.current.requestFullscreen();
577
+ }
578
+ }, []);
579
+ const dispose = React.useCallback(() => {
580
+ disposeRef.current = true;
581
+ cancelAnimationFrame(rafRef.current);
582
+ rendererRef.current?.dispose();
583
+ }, []);
584
+ React.useEffect(() => {
585
+ if (!loading && !error && polyCount > 0) {
586
+ setStatusText(
587
+ t(
588
+ "useModelViewer.trianglesMeshesAnimationS",
589
+ "{polyCount} triangles · {meshCount} meshes · {animNames} animation(s)"
590
+ ).replace("{polyCount}", String(polyCount.toLocaleString())).replace("{meshCount}", String(meshCount)).replace("{animNames}", String(animNames.length))
591
+ );
592
+ }
593
+ }, [loading, error, polyCount, meshCount, animNames.length, t]);
594
+ return {
595
+ mountRef,
596
+ loading,
597
+ loadProgress,
598
+ error,
599
+ navigationMode: navMode,
600
+ setNavigationMode: setNavMode,
601
+ projection,
602
+ setProjection,
603
+ wireframe,
604
+ setWireframe,
605
+ showGrid: gridVisible,
606
+ setShowGrid: setGridVisible,
607
+ autoRotateSpeed: autoRotate,
608
+ setAutoRotateSpeed: setAutoRotate,
609
+ polyCount,
610
+ meshCount,
611
+ animations: animNames,
612
+ activeAnimIdx,
613
+ playAnimation,
614
+ togglePlay,
615
+ isPlaying,
616
+ animTime,
617
+ animDuration,
618
+ seekAnim,
619
+ animSpeed,
620
+ setAnimSpeed,
621
+ goToPreset,
622
+ resetCamera,
623
+ zoomToFit,
624
+ screenshot,
625
+ toggleFullscreen,
626
+ isFullscreen,
627
+ loadFile,
628
+ loadUrl,
629
+ activeFloorPlanId,
630
+ setActiveFloorPlanId,
631
+ annotationsVisible,
632
+ setAnnotationsVisible,
633
+ measureMode: measureModeActive,
634
+ setMeasureMode: setMeasureModeActive,
635
+ clippingEnabled,
636
+ setClippingEnabled,
637
+ clippingPosition,
638
+ setClippingPosition,
639
+ clippingAxis,
640
+ setClippingAxis,
641
+ dimensions,
642
+ statusText,
643
+ dispose
644
+ };
676
645
  }
677
-
678
646
  exports.useModelViewer = useModelViewer;