@giszhc/three-model-viewer 0.0.0-stage → 1.0.1
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/LICENSE +21 -0
- package/README.md +296 -2
- package/dist/core/EventEmitter.d.ts +9 -0
- package/dist/core/SceneManager.d.ts +28 -0
- package/dist/core/Viewer.d.ts +115 -0
- package/dist/index.cjs +5 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +1875 -0
- package/dist/index.js.map +1 -0
- package/dist/index.umd.js +4230 -0
- package/dist/index.umd.js.map +1 -0
- package/dist/loaders/ModelLoader.d.ts +69 -0
- package/dist/loaders/ZipExtractor.d.ts +14 -0
- package/dist/style.css +2 -0
- package/dist/tools/BoundingBoxTool.d.ts +36 -0
- package/dist/tools/CameraTool.d.ts +39 -0
- package/dist/tools/ClippingTool.d.ts +140 -0
- package/dist/tools/MeasurementTool.d.ts +61 -0
- package/dist/tools/ScreenshotTool.d.ts +21 -0
- package/dist/types/index.d.ts +121 -0
- package/dist/utils/math.d.ts +21 -0
- package/dist/utils/snapping.d.ts +14 -0
- package/package.json +67 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 giszhc
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,297 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @giszhc/three-model-viewer
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> 🚀 现代化、高内聚的 Web 3D 模型检视与工程测量 SDK。基于 **Three.js** 与 **TypeScript** 构建,专为 Web 端三维轻量化展示、尺寸标注、空间测量与截面剖切量身定制。
|
|
4
|
+
|
|
5
|
+
[](https://giszhc.github.io/three-model-viewer/)
|
|
6
|
+
[](https://www.npmjs.com/package/@giszhc/three-model-viewer)
|
|
7
|
+
[](https://www.typescriptlang.org/)
|
|
8
|
+
[](https://opensource.org/licenses/MIT)
|
|
9
|
+
|
|
10
|
+
🌐 **在线演示体验地址**:[https://giszhc.github.io/three-model-viewer/](https://giszhc.github.io/three-model-viewer/)
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## 🌟 核心特性
|
|
15
|
+
|
|
16
|
+
- 📦 **多源格式无缝加载**:支持 `OBJ`、`MTL`,以及包含模型和图片贴图的 `ZIP` 压缩包在浏览器**纯内存解压加载**,无需后端转码服务。
|
|
17
|
+
- 📏 **动态长宽高范围框**:实时绘制模型包围盒与三向尺寸引导线。**随着模型旋转,长宽高标签与指示线自动在当前的三个轴向空间自适应展示**。
|
|
18
|
+
- 🎯 **严格模型表面约束与磁吸**:所有测量与自定义剖切交互**严格限制在模型表面**,模型外部点击自动拦截;内置 BVH 空间索引,鼠标靠近网格顶点时自动实现**毫秒级磁吸(Snapping)**;**表面标点与指示光标采用屏幕空间恒定像素渲染(Screen-Space Fixed Size),放大缩小模型时标点尺寸始终固定不畸变**。
|
|
19
|
+
- 📐 **全功能工程级测量 (单次测量即退,严防误触)**:
|
|
20
|
+
- **距离测量**:空间任意两点直线测距,中点三维浮动尺寸标签。
|
|
21
|
+
- **高度测量**:计算两点高程差 $\Delta H$ 与水平投影跨度,CAD 风格标高引出线。
|
|
22
|
+
- **角度测量**:模型表面点选射线端点、顶点与第二射线端点,实时绘制角度圆弧扇形与精确度数。
|
|
23
|
+
- **面积测量**:模型表面空间多边形点选,双击闭合,自动三角剖分与半透明面积面填充。
|
|
24
|
+
- 🔄 **自动旋转与轴向锁定**:
|
|
25
|
+
- 支持 **X 轴 / Y 轴** 自转,默认沿 X 轴自转。
|
|
26
|
+
- 支持 **顺时针 / 逆时针** 旋转方向自由切换。
|
|
27
|
+
- 支持 **锁定 X / Y / Z 某个轴** 检视模型(例如锁 Z 轴可防止倾斜与滚转,维持正立地平线检视)。
|
|
28
|
+
- ✨ **模型显示形态切换**:
|
|
29
|
+
- 🧊 **实体表面 (Solid)**:支持真实漫反射/PBR 纹理或经典数模纯色蓝模材质(对齐 Digital3DCloud 工业蓝规范)。
|
|
30
|
+
- 🕸️ **网格线框 (Wireframe)**:直观展示三维多边形拓扑结构。
|
|
31
|
+
- ✂️ **灵活剖切系统**:
|
|
32
|
+
- **XYZ 正交轴剖切**:X、Y、Z 三个面独立滑动拖动剖切,支持反向截取。
|
|
33
|
+
- **2点划线自定义剖切**:在模型表面点击起点和终点2个点,如手术刀般沿视线利落切开(纯净截面,不显示遮挡视线的黄色杂乱辅助面),支持一键切换左右/保留另一侧。
|
|
34
|
+
- 🎥 **8 个标准视角平滑过渡**:正视、后视、左视、右视、俯视、仰视、前等轴测、后等轴测,基于 Tween 平滑运镜过渡,符合人体工学。
|
|
35
|
+
- 🎨 **材质与外观控制**:一键切换模型原始纹理贴图与纯色蓝模材质;**支持一键切换亮色(Light Mode)与暗色(Dark Mode)皮肤**;支持自定义背景色、纯黑、浅灰及透明背景输出。
|
|
36
|
+
- 📸 **纯净高清图片无损导出**:支持**原画质(模型实际贴图 1:1 原始物理分辨率)**、1x 标清、2x 高清 (2K)、4x 超清 (4K) 截图渲染并一键触发浏览器下载;**默认纯净滤镜:仅导出模型主体与光影背景,自动隐藏所有辅助网格、范围框与测量标注**。
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## 📦 安装与引入
|
|
41
|
+
|
|
42
|
+
### 1. 工程化包管理器安装 (推荐)
|
|
43
|
+
|
|
44
|
+
适用于 Vue、React、Vite、Webpack、Next.js 等前端现代化项目:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
# npm
|
|
48
|
+
npm install @giszhc/three-model-viewer three
|
|
49
|
+
|
|
50
|
+
# pnpm
|
|
51
|
+
pnpm add @giszhc/three-model-viewer three
|
|
52
|
+
|
|
53
|
+
# yarn
|
|
54
|
+
yarn add @giszhc/three-model-viewer three
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### 2. CDN 直链引入 (开箱即用,免打包)
|
|
58
|
+
|
|
59
|
+
SDK 提供了内置自包含引擎的 UMD 单文件产物,直接通过 `<script>` 标签引入即可自动在全局挂载 `window.ThreeModelViewer`:
|
|
60
|
+
|
|
61
|
+
```html
|
|
62
|
+
<!DOCTYPE html>
|
|
63
|
+
<html lang="zh-CN">
|
|
64
|
+
<head>
|
|
65
|
+
<meta charset="UTF-8">
|
|
66
|
+
<title>3D Model Viewer CDN 示例</title>
|
|
67
|
+
<!-- 1. 引入 3D 尺寸与测量标签样式 -->
|
|
68
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@giszhc/three-model-viewer/dist/style.css">
|
|
69
|
+
<style>
|
|
70
|
+
html, body, #viewport { width: 100%; height: 100%; margin: 0; overflow: hidden; }
|
|
71
|
+
</style>
|
|
72
|
+
</head>
|
|
73
|
+
<body>
|
|
74
|
+
<div id="viewport"></div>
|
|
75
|
+
|
|
76
|
+
<!-- 2. 引入 SDK UMD 单文件产物 (支持 unpkg 或 jsdelivr) -->
|
|
77
|
+
<script src="https://cdn.jsdelivr.net/npm/@giszhc/three-model-viewer/dist/index.umd.js"></script>
|
|
78
|
+
<!-- 或者使用 unpkg: -->
|
|
79
|
+
<!-- <script src="https://unpkg.com/@giszhc/three-model-viewer/dist/index.umd.js"></script> -->
|
|
80
|
+
|
|
81
|
+
<script>
|
|
82
|
+
// 3. 直接通过全局对象初始化
|
|
83
|
+
const { Viewer } = window.ThreeModelViewer;
|
|
84
|
+
|
|
85
|
+
const viewer = new Viewer({
|
|
86
|
+
container: document.getElementById('viewport'),
|
|
87
|
+
background: '#070a14',
|
|
88
|
+
unit: 'm',
|
|
89
|
+
antialias: true
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// 4. 支持直接载入在线 .zip 或 .obj 链接
|
|
93
|
+
viewer.loadZip('https://example.com/models/sample.zip');
|
|
94
|
+
// 或 viewer.loadOBJ('https://example.com/models/sample.obj', 'https://example.com/models/sample.mtl');
|
|
95
|
+
</script>
|
|
96
|
+
</body>
|
|
97
|
+
</html>
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## 🚀 3分钟快速上手
|
|
103
|
+
|
|
104
|
+
### 1. 基础使用
|
|
105
|
+
|
|
106
|
+
```html
|
|
107
|
+
<div id="model-container" style="width: 100vw; height: 100vh;"></div>
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
import { Viewer } from '@giszhc/three-model-viewer';
|
|
112
|
+
import '@giszhc/three-model-viewer/style.css'; // 导入 3D 尺寸与测量标签样式
|
|
113
|
+
|
|
114
|
+
// 1. 初始化查看器
|
|
115
|
+
const viewer = new Viewer({
|
|
116
|
+
container: document.getElementById('model-container')!,
|
|
117
|
+
background: '#181c24', // 背景色,支持 hex 或 null(透明)
|
|
118
|
+
unit: 'm', // 测量与尺寸单位 (m, mm, cm)
|
|
119
|
+
antialias: true, // 开启抗锯齿
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// 2. 加载本地 ZIP 压缩包 (内含 obj + mtl + 贴图)
|
|
123
|
+
const fileInput = document.querySelector('input[type="file"]');
|
|
124
|
+
fileInput.addEventListener('change', async (e) => {
|
|
125
|
+
const file = e.target.files[0];
|
|
126
|
+
if (file) {
|
|
127
|
+
const result = await viewer.loadZip(file);
|
|
128
|
+
console.log('模型加载成功:', result);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// 或者直接通过网络 URL 加载 OBJ/MTL
|
|
133
|
+
// await viewer.loadOBJ('https://example.com/model.obj', 'https://example.com/model.mtl');
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## 📖 核心 API 指南
|
|
139
|
+
|
|
140
|
+
### 1. 模型加载与管理
|
|
141
|
+
|
|
142
|
+
| 方法名 | 参数 | 描述 |
|
|
143
|
+
| :--- | :--- | :--- |
|
|
144
|
+
| `loadZip(source)` | `File \| Blob \| ArrayBuffer \| string` | 加载包含 `.obj`、`.mtl` 和贴图图片的 ZIP 压缩包 |
|
|
145
|
+
| `loadOBJ(objUrl, mtlUrl?)` | `string, string?` | 通过 URL 直接加载 OBJ 及可选的 MTL 材质文件 |
|
|
146
|
+
| `clearModel()` | - | 清除当前加载的模型并释放内存资源 |
|
|
147
|
+
|
|
148
|
+
### 2. 测量系统 (`Measurement`)
|
|
149
|
+
|
|
150
|
+
> **提示**:进入测量模式后,鼠标仅在模型表面可激活点选,点击模型外自动拦截,移动至顶点附近会自动触发磁吸。
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
// 开启测量模式 ('distance' | 'height' | 'angle' | 'area' | 'volume' | 'none')
|
|
154
|
+
viewer.setMeasurementMode('distance'); // 距离测量:点选2点
|
|
155
|
+
viewer.setMeasurementMode('height'); // 高度测量:点选2点
|
|
156
|
+
viewer.setMeasurementMode('angle'); // 角度测量:点选3点 (射线端点、顶点、射线端点)
|
|
157
|
+
viewer.setMeasurementMode('area'); // 面积测量:点选多点,双击完成
|
|
158
|
+
viewer.setMeasurementMode('volume'); // 动态体积盒测量:点击放置3D包围盒
|
|
159
|
+
|
|
160
|
+
// 动态调节体积盒尺寸并锁定
|
|
161
|
+
viewer.setVolumeDimensions(3.0, 2.5, 4.0); // 实时调节长宽高 (实时显示体积)
|
|
162
|
+
viewer.lockVolume(); // 锁定当前体积测量结果
|
|
163
|
+
|
|
164
|
+
// 撤销上一个绘制点
|
|
165
|
+
viewer.undoMeasurement();
|
|
166
|
+
|
|
167
|
+
// 清除所有测量结果与 3D 标签
|
|
168
|
+
viewer.clearMeasurements();
|
|
169
|
+
|
|
170
|
+
// 获取当前所有测量数据
|
|
171
|
+
const results = viewer.getMeasurements();
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### 3. 剖切系统 (`Clipping`)
|
|
175
|
+
|
|
176
|
+
```typescript
|
|
177
|
+
// 1. 正交轴剖切 (0: 完全剖开, 1: 完整模型, inverted: 是否翻转截取方向)
|
|
178
|
+
viewer.setClipping('x', 0.5, false); // 沿 X 轴切到 50% 位置
|
|
179
|
+
viewer.setClipping('y', 0.8, false); // 沿 Y 轴切到 80% 位置
|
|
180
|
+
viewer.disableClippingAxis('x'); // 关闭 X 轴剖切
|
|
181
|
+
|
|
182
|
+
// 2. 自定义 2 点划线切面剖切
|
|
183
|
+
viewer.startCustomClip(); // 激活后在模型表面点击起点和终点2个点,沿视线切开模型(无遮挡纯净剖面)
|
|
184
|
+
viewer.invertCustomClip(); // 切换左右剖切保留方向(保留左侧或右侧)
|
|
185
|
+
viewer.clearCustomClip(); // 清除自定义切面
|
|
186
|
+
|
|
187
|
+
// 3. 重置全部剖切
|
|
188
|
+
viewer.resetClipping();
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### 4. 视角控制 (`Camera & Views`)
|
|
192
|
+
|
|
193
|
+
支持 6 个标准工业/CAD 正交视图,内置平滑补间动画(平滑过渡时间可配置,默认 600ms):
|
|
194
|
+
|
|
195
|
+
```typescript
|
|
196
|
+
// 视角切换:'front' (前视图) | 'back' (后视图) | 'left' (左视图) | 'right' (右视图) | 'top' (顶视图) | 'bottom' (底视图)
|
|
197
|
+
await viewer.setView('front', 500); // 前视图 (模型加载后默认即为前视图)
|
|
198
|
+
await viewer.setView('top', 500); // 顶视图
|
|
199
|
+
await viewer.setView('left', 500); // 左视图
|
|
200
|
+
|
|
201
|
+
// 视角居中复位
|
|
202
|
+
await viewer.fitView(500);
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### 5. 范围框、尺寸与外观控制
|
|
206
|
+
|
|
207
|
+
```typescript
|
|
208
|
+
// 切换模型显示样式: 'solid' (实体) | 'wireframe' (线框)
|
|
209
|
+
viewer.setModelDisplayStyle('solid');
|
|
210
|
+
|
|
211
|
+
// 开启/关闭自动旋转 (默认沿着 X 轴顺时针自转)
|
|
212
|
+
viewer.startAutoRotate({ axis: 'x', direction: 'clockwise', speed: 1.0 });
|
|
213
|
+
viewer.stopAutoRotate();
|
|
214
|
+
|
|
215
|
+
// 锁定视角检视轴向: 'x' | 'y' | 'z' | null
|
|
216
|
+
viewer.setRotationLockAxis('z'); // 锁 Z 轴检视,防止倾斜
|
|
217
|
+
|
|
218
|
+
// 显示/隐藏范围框及长宽高尺寸线 (默认隐藏,传入 true 开启)
|
|
219
|
+
viewer.showBoundingBox(true);
|
|
220
|
+
|
|
221
|
+
// 切换模型贴图开关 (true: 原始贴图材质, false: 经典数模蓝模材质)
|
|
222
|
+
viewer.toggleTexture(false);
|
|
223
|
+
|
|
224
|
+
// 自定义无纹理模式下的材质颜色 (默认已设为先临三维经典蓝 0x4c8cd6)
|
|
225
|
+
viewer.setPlainColor(0x4c8cd6);
|
|
226
|
+
|
|
227
|
+
// 更换背景颜色 (支持十六进制颜色、null 或 transparent)
|
|
228
|
+
viewer.setBackground('#0f172a'); // 深色背景
|
|
229
|
+
viewer.setBackground(null, true); // 透明背景
|
|
230
|
+
|
|
231
|
+
// 地面网格参考线开关
|
|
232
|
+
viewer.setGridVisible(true);
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### 6. 高清图片导出 (`Screenshot`)
|
|
236
|
+
|
|
237
|
+
```typescript
|
|
238
|
+
// 1. 原画质导出 (以模型实际贴图原始尺寸 1:1 分辨率无损输出,纯净模型+背景)
|
|
239
|
+
await viewer.exportImage({
|
|
240
|
+
originalQuality: true, // 开启原画质导出 (按贴图真实物理分辨率 1:1 输出)
|
|
241
|
+
modelOnly: true, // 默认即为 true:纯净导出模型与背景,自动滤除多余辅助线、网格与测量标注
|
|
242
|
+
download: true
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
// 2. 导出 2 倍 (2K) 高清截图并自动触发浏览器下载
|
|
246
|
+
await viewer.exportImage({
|
|
247
|
+
scale: 2, // 缩放倍率 (1: 原始尺寸, 2: 2K高清, 4: 4K超清)
|
|
248
|
+
type: 'image/png', // 格式: 'image/png' 或 'image/jpeg'
|
|
249
|
+
quality: 0.95, // 图像质量 (0-1)
|
|
250
|
+
download: true, // 是否直接触发下载
|
|
251
|
+
filename: 'model-2k.png'
|
|
252
|
+
});
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
### 7. 事件监听 (`Events`)
|
|
256
|
+
|
|
257
|
+
```typescript
|
|
258
|
+
// 测量完成事件
|
|
259
|
+
viewer.on('measure:complete', (result) => {
|
|
260
|
+
console.log(`测量完成 [${result.type}]: ${result.formattedValue}`);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
// 模型加载成功事件
|
|
264
|
+
viewer.on('load:success', (res) => {
|
|
265
|
+
console.log(`模型加载完成,顶点: ${res.vertexCount}, 三角面: ${res.faceCount}`);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
// 模型加载错误事件
|
|
269
|
+
viewer.on('load:error', (error) => {
|
|
270
|
+
console.error('模型加载失败:', error);
|
|
271
|
+
});
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
---
|
|
275
|
+
|
|
276
|
+
## 🛠️ 本地运行与演示体验
|
|
277
|
+
|
|
278
|
+
克隆本仓库到本地后,执行以下命令即可启动交互式全功能演示平台:
|
|
279
|
+
|
|
280
|
+
```bash
|
|
281
|
+
# 1. 安装依赖
|
|
282
|
+
pnpm install
|
|
283
|
+
|
|
284
|
+
# 2. 启动本地开发服务与演示页面
|
|
285
|
+
pnpm dev
|
|
286
|
+
|
|
287
|
+
# 3. 打包构建 SDK (生成 dist/index.js, dist/index.cjs, dist/index.d.ts)
|
|
288
|
+
pnpm build
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
启动后在浏览器打开控制台输出的地址(如 `http://localhost:5173`),即可直接体验**机械装配体/建筑模型、拖放本地 ZIP/OBJ、8 视角切换、三向长宽高动态标注、XYZ 及自定义 3 点剖切、距离/高度/面积/体积测量与 4K 高清导出**全套功能。
|
|
292
|
+
|
|
293
|
+
---
|
|
294
|
+
|
|
295
|
+
## 📄 开源许可证
|
|
296
|
+
|
|
297
|
+
本项目基于 [MIT](LICENSE) 开源协议发布,可自由应用于商业和非商业项目。
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export type Listener<T = unknown> = (...args: T[]) => void;
|
|
2
|
+
export declare class EventEmitter {
|
|
3
|
+
private events;
|
|
4
|
+
constructor();
|
|
5
|
+
on(event: string, listener: Listener): this;
|
|
6
|
+
off(event: string, listener: Listener): this;
|
|
7
|
+
emit(event: string, ...args: unknown[]): boolean;
|
|
8
|
+
removeAllListeners(event?: string): this;
|
|
9
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
|
|
2
|
+
import { TrackballControls } from 'three/addons/controls/TrackballControls.js';
|
|
3
|
+
import { CSS2DRenderer } from 'three/addons/renderers/CSS2DRenderer.js';
|
|
4
|
+
import { ViewerOptions } from '../types';
|
|
5
|
+
import * as THREE from 'three';
|
|
6
|
+
import * as TWEEN from '@tweenjs/tween.js';
|
|
7
|
+
export declare class SceneManager {
|
|
8
|
+
container: HTMLElement;
|
|
9
|
+
scene: THREE.Scene;
|
|
10
|
+
camera: THREE.PerspectiveCamera;
|
|
11
|
+
renderer: THREE.WebGLRenderer;
|
|
12
|
+
labelRenderer: CSS2DRenderer;
|
|
13
|
+
controls: TrackballControls | OrbitControls;
|
|
14
|
+
lightsGroup: THREE.Group;
|
|
15
|
+
gridHelper: THREE.GridHelper;
|
|
16
|
+
tweenGroup: TWEEN.Group;
|
|
17
|
+
onBeforeRender?: () => void;
|
|
18
|
+
private animationFrameId;
|
|
19
|
+
private resizeObserver;
|
|
20
|
+
private isDestroyed;
|
|
21
|
+
constructor(options: ViewerOptions);
|
|
22
|
+
private initLights;
|
|
23
|
+
private initResizeObserver;
|
|
24
|
+
handleResize(): void;
|
|
25
|
+
private startAnimationLoop;
|
|
26
|
+
setBackground(color: string | number | null, alpha?: boolean): void;
|
|
27
|
+
destroy(): void;
|
|
28
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { SceneManager } from './SceneManager';
|
|
2
|
+
import { EventEmitter } from './EventEmitter';
|
|
3
|
+
import { ModelLoader } from '../loaders/ModelLoader';
|
|
4
|
+
import { BoundingBoxTool } from '../tools/BoundingBoxTool';
|
|
5
|
+
import { CameraTool } from '../tools/CameraTool';
|
|
6
|
+
import { ClippingTool } from '../tools/ClippingTool';
|
|
7
|
+
import { MeasurementTool } from '../tools/MeasurementTool';
|
|
8
|
+
import { ScreenshotTool } from '../tools/ScreenshotTool';
|
|
9
|
+
import { ViewerOptions, ModelLoadResult, MeasurementType, MeasurementResult, ClippingAxis, CameraView, ExportImageOptions, BoundingBoxOptions, ViewerEvents, AutoRotateAxis, AutoRotateDirection, AutoRotateOptions, RotationLockAxis, ModelDisplayStyle } from '../types';
|
|
10
|
+
import * as THREE from 'three';
|
|
11
|
+
export declare class Viewer extends EventEmitter {
|
|
12
|
+
sceneManager: SceneManager;
|
|
13
|
+
modelLoader: ModelLoader;
|
|
14
|
+
boundingBoxTool: BoundingBoxTool;
|
|
15
|
+
cameraTool: CameraTool;
|
|
16
|
+
clippingTool: ClippingTool;
|
|
17
|
+
measurementTool: MeasurementTool;
|
|
18
|
+
screenshotTool: ScreenshotTool;
|
|
19
|
+
currentModel: THREE.Group | null;
|
|
20
|
+
currentModelInfo: ModelLoadResult | null;
|
|
21
|
+
private unit;
|
|
22
|
+
private defaultShowBoundingBox;
|
|
23
|
+
constructor(options: ViewerOptions);
|
|
24
|
+
/**
|
|
25
|
+
* Load model from a ZIP file containing .obj, .mtl and textures
|
|
26
|
+
*/
|
|
27
|
+
loadZip(source: File | Blob | ArrayBuffer | string): Promise<ModelLoadResult>;
|
|
28
|
+
/**
|
|
29
|
+
* Load model directly from OBJ URL and optional MTL URL
|
|
30
|
+
*/
|
|
31
|
+
loadOBJ(objUrl: string, mtlUrl?: string): Promise<ModelLoadResult>;
|
|
32
|
+
/**
|
|
33
|
+
* Loads a built-in sample geometry (convenient for immediate testing and demo playground)
|
|
34
|
+
*/
|
|
35
|
+
loadSampleGeometry(type?: 'mechanical' | 'building' | 'torusKnot'): ModelLoadResult;
|
|
36
|
+
private setupLoadedModel;
|
|
37
|
+
clearModel(): void;
|
|
38
|
+
setBackground(color: string | number | null, alpha?: boolean): void;
|
|
39
|
+
/**
|
|
40
|
+
* Set model display style: 'solid' (实体), 'wireframe' (网格线框)
|
|
41
|
+
*/
|
|
42
|
+
setModelDisplayStyle(style: ModelDisplayStyle): void;
|
|
43
|
+
getModelDisplayStyle(): ModelDisplayStyle;
|
|
44
|
+
toggleTexture(showTexture: boolean): void;
|
|
45
|
+
getTextureState(): boolean;
|
|
46
|
+
/**
|
|
47
|
+
* Set color for model when texture is turned off (defaults to Digital3DCloud CAD blue 0x4c8cd6)
|
|
48
|
+
*/
|
|
49
|
+
setPlainColor(color: THREE.ColorRepresentation): void;
|
|
50
|
+
getPlainColor(): number;
|
|
51
|
+
/**
|
|
52
|
+
* Retrieves the maximum original texture resolution of the current model
|
|
53
|
+
*/
|
|
54
|
+
getModelTextureSize(): {
|
|
55
|
+
width: number;
|
|
56
|
+
height: number;
|
|
57
|
+
} | null;
|
|
58
|
+
setGridVisible(visible: boolean): void;
|
|
59
|
+
showBoundingBox(visible: boolean, options?: Partial<BoundingBoxOptions>): void;
|
|
60
|
+
getBoundingBoxVisible(): boolean;
|
|
61
|
+
setClipping(axis: ClippingAxis, progress: number, inverted?: boolean): void;
|
|
62
|
+
disableClippingAxis(axis: ClippingAxis): void;
|
|
63
|
+
toggleClippingAxis(axis: ClippingAxis, enabled?: boolean): boolean;
|
|
64
|
+
invertClippingAxis(axis: ClippingAxis): boolean;
|
|
65
|
+
getClippingAxisState(axis: ClippingAxis): {
|
|
66
|
+
enabled: boolean;
|
|
67
|
+
progress: number;
|
|
68
|
+
inverted: boolean;
|
|
69
|
+
position: number;
|
|
70
|
+
};
|
|
71
|
+
setClippingPlanesVisible(visible: boolean): void;
|
|
72
|
+
getClippingPlanesVisible(): boolean;
|
|
73
|
+
setSectioningEnabled(enabled: boolean): void;
|
|
74
|
+
getSectioningEnabled(): boolean;
|
|
75
|
+
setSectioningAxis(axis: ClippingAxis): void;
|
|
76
|
+
getSectioningAxis(): ClippingAxis;
|
|
77
|
+
setSectioningAxisEnabled(axis: ClippingAxis, enabled: boolean): void;
|
|
78
|
+
toggleSectioningAxis(axis: ClippingAxis, enabled?: boolean): boolean;
|
|
79
|
+
isSectioningAxisEnabled(axis: ClippingAxis): boolean;
|
|
80
|
+
setSectioningAxisReverse(axis: ClippingAxis, reverse: boolean): void;
|
|
81
|
+
invertSectioningAxis(axis: ClippingAxis): boolean;
|
|
82
|
+
setSectioningAxisProgress(axis: ClippingAxis, progress: number): void;
|
|
83
|
+
getSectioningAxisProgress(axis: ClippingAxis): number;
|
|
84
|
+
setSectioningHelperVisible(visible: boolean): void;
|
|
85
|
+
getSectioningHelperVisible(): boolean;
|
|
86
|
+
setSectioningReverse(reverse: boolean): void;
|
|
87
|
+
getSectioningReverse(): boolean;
|
|
88
|
+
resetSectioning(axis?: ClippingAxis): void;
|
|
89
|
+
getSectioningState(): import('..').SectioningState;
|
|
90
|
+
startCustomClip(): void;
|
|
91
|
+
cancelCustomClip(): void;
|
|
92
|
+
clearCustomClip(): void;
|
|
93
|
+
invertCustomClip(): boolean;
|
|
94
|
+
hasCustomClip(): boolean;
|
|
95
|
+
resetClipping(): void;
|
|
96
|
+
setMeasurementMode(mode: MeasurementType): void;
|
|
97
|
+
getMeasurementMode(): MeasurementType;
|
|
98
|
+
clearMeasurements(): void;
|
|
99
|
+
undoMeasurement(): void;
|
|
100
|
+
getMeasurements(): MeasurementResult[];
|
|
101
|
+
setView(view: CameraView, duration?: number): Promise<void>;
|
|
102
|
+
fitView(duration?: number): Promise<void>;
|
|
103
|
+
setAutoRotate(enabled: boolean, axis?: AutoRotateAxis, direction?: AutoRotateDirection, speed?: number): void;
|
|
104
|
+
toggleAutoRotate(): boolean;
|
|
105
|
+
setAutoRotateAxis(axis: AutoRotateAxis): void;
|
|
106
|
+
setAutoRotateDirection(direction: AutoRotateDirection): void;
|
|
107
|
+
setAutoRotateSpeed(speed: number): void;
|
|
108
|
+
getAutoRotateState(): AutoRotateOptions;
|
|
109
|
+
setRotationLockedAxis(axis: RotationLockAxis): void;
|
|
110
|
+
getRotationLockedAxis(): RotationLockAxis;
|
|
111
|
+
exportImage(options?: ExportImageOptions): Promise<string>;
|
|
112
|
+
on<K extends keyof ViewerEvents>(event: K, listener: ViewerEvents[K]): this;
|
|
113
|
+
off<K extends keyof ViewerEvents>(event: K, listener: ViewerEvents[K]): this;
|
|
114
|
+
destroy(): void;
|
|
115
|
+
}
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;l<u;l++)d=c[l],!a.call(e,d)&&d!==o&&t(e,d,{get:(e=>i[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},s=(n,r,s)=>(s=n==null?{}:e(i(n)),o(r||!n||!n.__esModule||!a.call(n,`default`)?t(s,`default`,{value:n,enumerable:!0}):s,n));let c=require("three");c=s(c,1);let l=require("three/addons/controls/OrbitControls.js"),u=require("three/addons/controls/TrackballControls.js"),d=require("three/addons/renderers/CSS2DRenderer.js"),f=require("@tweenjs/tween.js");f=s(f,1);let p=require("three/addons/loaders/OBJLoader.js"),m=require("three/addons/loaders/MTLLoader.js"),h=require("three-mesh-bvh"),g=require("jszip");g=s(g,1);var _=class{container;scene;camera;renderer;labelRenderer;controls;lightsGroup;gridHelper;tweenGroup;onBeforeRender;animationFrameId=null;resizeObserver=null;isDestroyed=!1;constructor(e){this.container=e.container,this.scene=new c.Scene,e.background!==void 0&&e.background!==null?(typeof e.background==`string`||typeof e.background==`number`)&&(this.scene.background=new c.Color(e.background)):this.scene.background=new c.Color(1580068);let t=this.container.clientWidth||800,n=this.container.clientHeight||600;if(this.camera=new c.PerspectiveCamera(e.cameraFov??45,t/n,e.cameraNear??.1,e.cameraFar??5e3),this.camera.position.set(10,10,10),this.renderer=new c.WebGLRenderer({antialias:e.antialias??!0,alpha:e.alpha??!1,preserveDrawingBuffer:!0,powerPreference:`high-performance`}),this.renderer.setSize(t,n),this.renderer.setPixelRatio(Math.min(window.devicePixelRatio,2)),this.renderer.localClippingEnabled=!0,this.renderer.toneMapping=c.ACESFilmicToneMapping,this.renderer.toneMappingExposure=1,this.renderer.outputColorSpace=c.SRGBColorSpace,this.renderer.domElement.style.display=`block`,this.renderer.domElement.style.width=`100%`,this.renderer.domElement.style.height=`100%`,this.container.appendChild(this.renderer.domElement),this.labelRenderer=new d.CSS2DRenderer,this.labelRenderer.setSize(t,n),this.labelRenderer.domElement.style.position=`absolute`,this.labelRenderer.domElement.style.top=`0`,this.labelRenderer.domElement.style.left=`0`,this.labelRenderer.domElement.style.pointerEvents=`none`,this.labelRenderer.domElement.style.width=`100%`,this.labelRenderer.domElement.style.height=`100%`,this.labelRenderer.domElement.className=`gmv-css2d-container`,this.container.appendChild(this.labelRenderer.domElement),(e.controlsType??`trackball`)===`orbit`){let e=new l.OrbitControls(this.camera,this.renderer.domElement);e.enableDamping=!0,e.dampingFactor=.05,e.screenSpacePanning=!0,this.controls=e}else{let e=new u.TrackballControls(this.camera,this.renderer.domElement);e.rotateSpeed=2.5,e.zoomSpeed=1.2,e.panSpeed=.8,e.staticMoving=!1,e.dynamicDampingFactor=.15,this.controls=e}this.tweenGroup=new f.Group,this.lightsGroup=new c.Group,this.lightsGroup.name=`gmv-lights-group`,this.initLights(),this.scene.add(this.lightsGroup),this.gridHelper=new c.GridHelper(50,50,4871528,2963272),this.gridHelper.name=`gmv-grid-helper`,this.gridHelper.position.y=0,this.gridHelper.visible=!1,this.scene.add(this.gridHelper),this.initResizeObserver(),this.startAnimationLoop(),requestAnimationFrame(()=>this.handleResize()),setTimeout(()=>this.handleResize(),50)}initLights(){let e=new c.HemisphereLight(16777215,4674921,1);e.position.set(0,50,0),this.lightsGroup.add(e);let t=new c.AmbientLight(16777215,.8);this.lightsGroup.add(t);let n=new c.DirectionalLight(16777215,1.2);n.position.set(50,100,50),this.lightsGroup.add(n);let r=new c.DirectionalLight(16777215,.7);r.position.set(-50,50,-50),this.lightsGroup.add(r);let i=new c.DirectionalLight(16777215,.9);i.position.set(0,0,1),this.camera.add(i),this.scene.add(this.camera)}initResizeObserver(){this.resizeObserver=new ResizeObserver(()=>{this.handleResize()}),this.resizeObserver.observe(this.container)}handleResize(){if(this.isDestroyed||!this.container)return;let e=this.container.clientWidth,t=this.container.clientHeight;e!==0&&t!==0&&(this.camera.aspect=e/t,this.camera.updateProjectionMatrix(),this.renderer.setSize(e,t),this.labelRenderer.setSize(e,t),`handleResize`in this.controls&&typeof this.controls.handleResize==`function`&&this.controls.handleResize())}startAnimationLoop(){let e=()=>{this.isDestroyed||(this.animationFrameId=requestAnimationFrame(e),this.tweenGroup.update(),this.controls.update(),this.onBeforeRender&&this.onBeforeRender(),this.renderer.render(this.scene,this.camera),this.labelRenderer.render(this.scene,this.camera))};this.animationFrameId=requestAnimationFrame(e)}setBackground(e,t){t||e==null?(this.scene.background=null,this.renderer.setClearAlpha(0)):(this.scene.background=new c.Color(e),this.renderer.setClearAlpha(1))}destroy(){this.isDestroyed=!0,this.animationFrameId!==null&&cancelAnimationFrame(this.animationFrameId),this.resizeObserver&&this.resizeObserver.disconnect(),this.controls.dispose(),this.renderer.dispose(),this.renderer.domElement.parentElement&&this.renderer.domElement.parentElement.removeChild(this.renderer.domElement),this.labelRenderer.domElement.parentElement&&this.labelRenderer.domElement.parentElement.removeChild(this.labelRenderer.domElement)}},v=class{events;constructor(){this.events=new Map}on(e,t){return this.events.has(e)||this.events.set(e,[]),this.events.get(e).push(t),this}off(e,t){let n=this.events.get(e);if(!n)return this;let r=n.indexOf(t);return r!==-1&&n.splice(r,1),this}emit(e,...t){let n=this.events.get(e);return!n||n.length===0?!1:([...n].forEach(n=>{try{n(...t)}catch(t){console.error(`Error in event listener for "${e}":`,t)}}),!0)}removeAllListeners(e){return e?this.events.delete(e):this.events.clear(),this}},y=class{static async extract(e,t){let n;if(t?.(5,`正在读取文件字节流...`),typeof e==`string`){let t=await fetch(e);if(!t.ok)throw Error(`Failed to fetch zip file: ${t.statusText}`);n=await t.arrayBuffer()}else n=e instanceof Blob||e instanceof File?await e.arrayBuffer():e;t?.(15,`正在解压 ZIP 文件包...`);let r=await g.default.loadAsync(n),i=new Map,a=null,o=null,s=[`.png`,`.jpg`,`.jpeg`,`.webp`,`.bmp`,`.tga`,`.dds`],l=Object.keys(r.files),u=l.length||1;for(let e=0;e<l.length;e++){let n=l[e],c=r.files[n];if(c.dir||n.includes(`__MACOSX`)||n.split(`/`).pop()?.startsWith(`._`))continue;let d=n.toLowerCase(),f=Math.round(20+e/u*35);if(d.endsWith(`.obj`)&&!a?(a=c,t?.(f,`找到模型文件: ${n.split(`/`).pop()}`)):d.endsWith(`.mtl`)&&!o&&(o=c,t?.(f,`找到材质定义: ${n.split(`/`).pop()}`)),s.some(e=>d.endsWith(e))){let e=n.split(`/`).pop()||n;t?.(f,`加载贴图: ${e}`);let r=await c.async(`blob`),a=URL.createObjectURL(r);i.set(n,a),i.set(e,a),i.set(e.toLowerCase(),a)}}if(t?.(60,`提取完成,准备解析三维几何网格...`),!a)throw i.forEach(e=>URL.revokeObjectURL(e)),Error(`No .obj file found in the zip archive.`);let d=await a.async(`string`),f=a.name,p=null,m=null;o&&(p=await o.async(`string`),m=o.name);let h=new c.LoadingManager;return h.setURLModifier(e=>{let t=decodeURIComponent(e),n=t.split(`/`).pop()?.split(`\\`).pop()||t;if(i.has(t))return i.get(t);if(i.has(n))return i.get(n);for(let[e,t]of i.entries())if(e.toLowerCase()===n.toLowerCase())return t;return e}),{objContent:d,objFileName:f,mtlContent:p,mtlFileName:m,blobUrls:i,loadingManager:h,dispose:()=>{i.forEach(e=>{try{URL.revokeObjectURL(e)}catch{}}),i.clear()}}}};c.BufferGeometry.prototype.computeBoundsTree=h.computeBoundsTree,c.BufferGeometry.prototype.disposeBoundsTree=h.disposeBoundsTree,c.Mesh.prototype.raycast=h.acceleratedRaycast;var b=class{currentZipContent=null;managedMaterials=[];isTextureEnabled=!0;displayStyle=`solid`;plainColor=5016790;currentClippingPlanes=[];async loadZip(e,t){this.cleanup();let n=await y.extract(e,t);this.currentZipContent=n;let{objContent:r,mtlContent:i,loadingManager:a}=n;t?.(65,`正在解析材质与贴图数据...`);let o=null;i&&(o=new m.MTLLoader(a).parse(i,``),o.preload()),t?.(75,`正在解析 OBJ 网格几何数据...`);let s=new p.OBJLoader(a);o&&s.setMaterials(o);let c=typeof r==`string`?r:new TextDecoder().decode(r),l=s.parse(c);t?.(88,`正在建立空间 BVH 加速结构与几何居中...`);let u=this.postProcessModel(l);return t?.(100,`加载完成`),u}async loadOBJ(e,t,n){this.cleanup(),n?.(20,`正在加载材质定义...`);let r=null;t&&(r=await new m.MTLLoader().loadAsync(t),r.preload()),n?.(50,`正在加载 OBJ 模型几何数据...`);let i=new p.OBJLoader;r&&i.setMaterials(r);let a=await i.loadAsync(e,e=>{if(e.lengthComputable&&e.total>0){let t=Math.round(50+e.loaded/e.total*35);n?.(t,`正在传输模型数据 (${Math.round(e.loaded/e.total*100)}%)...`)}});n?.(88,`正在建立空间 BVH 加速结构与几何居中...`);let o=this.postProcessModel(a);return n?.(100,`加载完成`),o}postProcessModel(e){let t=0,n=0;this.managedMaterials=[];let r=e=>{e&&(e.anisotropy=16,e.generateMipmaps=!0,e.minFilter=c.LinearMipmapLinearFilter,e.magFilter=c.LinearFilter,e.colorSpace=c.SRGBColorSpace,e.needsUpdate=!0)},i=e=>{if(e.side=c.DoubleSide,e.needsUpdate=!0,`color`in e){let t=e;r(t.map),`normalMap`in t&&r(t.normalMap),`bumpMap`in t&&r(t.bumpMap),!t.map&&t.color&&t.color.getHex()<2236962&&t.color.setHex(11581636)}},a=new c.Box3().setFromObject(e),o=new c.Vector3,s=new c.Vector3;return a.getCenter(o),a.getSize(s),e.traverse(e=>{if(e.isMesh){let r=e;r.castShadow=!0,r.receiveShadow=!0,r.material?Array.isArray(r.material)?r.material.forEach(i):i(r.material):r.material=new c.MeshStandardMaterial({color:this.plainColor,roughness:.45,metalness:.12,side:c.DoubleSide});let a=r.geometry;if(a){a.translate(-o.x,-o.y,-o.z),a.attributes.normal||a.computeVertexNormals(),a.computeBoundsTree&&a.computeBoundsTree(),a.attributes.position&&(t+=a.attributes.position.count),a.index?n+=a.index.count/3:a.attributes.position&&(n+=a.attributes.position.count/3);let e=new c.MeshStandardMaterial({color:this.plainColor,roughness:.45,metalness:.12,side:c.DoubleSide,clippingPlanes:this.currentClippingPlanes.length>0?this.currentClippingPlanes:null,clipShadows:!0}),i={mesh:r,originalMaterial:r.material,plainMaterial:e};this.applyStyleToItem(i),this.managedMaterials.push(i)}}}),e.position.set(0,0,0),e.rotation.set(0,0,0),e.scale.set(1,1,1),e.updateMatrixWorld(!0),{model:e,box:new c.Box3().setFromObject(e),size:s,center:o,vertexCount:t,faceCount:Math.round(n)}}applyStyleToItem(e){let t=this.isTextureEnabled?e.originalMaterial:e.plainMaterial;e.mesh.material=t;let n=e=>{e.visible=!0,`wireframe`in e&&(e.wireframe=this.displayStyle===`wireframe`),e.needsUpdate=!0};Array.isArray(t)?t.forEach(n):n(t)}setDisplayStyle(e){this.displayStyle=e;for(let e of this.managedMaterials)this.applyStyleToItem(e)}getDisplayStyle(){return this.displayStyle}toggleTexture(e){this.isTextureEnabled=e;for(let e of this.managedMaterials)this.applyStyleToItem(e)}getTextureState(){return this.isTextureEnabled}setPlainColor(e){this.plainColor=typeof e==`string`?new c.Color(e).getHex():Number(e);for(let e of this.managedMaterials)e.plainMaterial&&`color`in e.plainMaterial&&(e.plainMaterial.color.set(this.plainColor),e.plainMaterial.needsUpdate=!0)}getPlainColor(){return this.plainColor}getModelTextureSize(){let e=0,t=0,n=n=>{if(`map`in n&&n.map){let r=n.map;if(r){let n=r.image||r.source?.data;if(n){let r=n.width||0,i=n.height||0;r>0&&i>0&&r*i>e*t&&(e=r,t=i)}}}};for(let e of this.managedMaterials)Array.isArray(e.originalMaterial)?e.originalMaterial.forEach(n):e.originalMaterial&&n(e.originalMaterial);return e>0&&t>0?{width:e,height:t}:null}getMeshes(){return this.managedMaterials.map(e=>e.mesh)}applyClippingPlanes(e){this.currentClippingPlanes=e;for(let t of this.managedMaterials){let n=t=>{t.clippingPlanes=e.length>0?e:null,t.clipShadows=!0,t.needsUpdate=!0};Array.isArray(t.originalMaterial)?t.originalMaterial.forEach(n):n(t.originalMaterial),n(t.plainMaterial)}}cleanup(){this.currentZipContent&&=(this.currentZipContent.dispose(),null);for(let e of this.managedMaterials)e.plainMaterial&&e.plainMaterial.dispose(),e.mesh.geometry&&e.mesh.geometry.disposeBoundsTree&&e.mesh.geometry.disposeBoundsTree();this.managedMaterials=[]}};function x(e){let t=e.geometry;if(!t)return 0;let n=t.attributes.position;if(!n)return 0;let r=0,i=new c.Vector3,a=new c.Vector3,o=new c.Vector3;e.updateMatrixWorld(!0);let s=e.matrixWorld;if(t.index){let e=t.index;for(let t=0;t<e.count;t+=3){let c=e.getX(t),l=e.getX(t+1),u=e.getX(t+2);i.fromBufferAttribute(n,c).applyMatrix4(s),a.fromBufferAttribute(n,l).applyMatrix4(s),o.fromBufferAttribute(n,u).applyMatrix4(s),r+=C(i,a,o)}}else for(let e=0;e<n.count;e+=3)i.fromBufferAttribute(n,e).applyMatrix4(s),a.fromBufferAttribute(n,e+1).applyMatrix4(s),o.fromBufferAttribute(n,e+2).applyMatrix4(s),r+=C(i,a,o);return Math.abs(r)}function S(e){let t=0;e.traverse(e=>{e.isMesh&&(t+=x(e))});let n=new c.Box3().setFromObject(e),r=new c.Vector3;n.getSize(r);let i=r.x*r.y*r.z;return{meshVolume:t,bboxVolume:i}}function C(e,t,n){return e.dot(t.clone().cross(n))/6}function w(e){let t=e.length;if(t<3)return 0;let n=new c.Vector3(0,0,0);for(let r=0;r<t;r++){let i=e[r],a=e[(r+1)%t];n.x+=(i.y-a.y)*(i.z+a.z),n.y+=(i.z-a.z)*(i.x+a.x),n.z+=(i.x-a.x)*(i.y+a.y)}return n.length()*.5}function T(e,t=`m`,n=2){return Math.abs(e)<.001?`0.00 ${t}`:`${e.toFixed(n)} ${t}`}var E=class{scene;boxGroup;wireframeBox=null;lineX=null;lineY=null;lineZ=null;labelX=null;labelY=null;labelZ=null;currentModel=null;isVisible=!1;unit=`m`;localVertices=[];worldVertices=[new c.Vector3,new c.Vector3,new c.Vector3,new c.Vector3,new c.Vector3,new c.Vector3,new c.Vector3,new c.Vector3];modelSize=new c.Vector3;constructor(e){this.scene=e,this.boxGroup=new c.Group,this.boxGroup.name=`gmv-bounding-box-group`,this.scene.add(this.boxGroup)}attachToModel(e,t){this.detach(),this.currentModel=e,this.unit=t?.unit||`m`,e.updateMatrixWorld(!0);let n=new c.Box3;e.traverse(e=>{if(e.isMesh){let t=e;t.geometry&&(t.geometry.boundingBox||t.geometry.computeBoundingBox(),t.geometry.boundingBox&&n.union(t.geometry.boundingBox))}}),n.isEmpty()&&n.setFromObject(e);let r=n.min,i=n.max;n.getSize(this.modelSize),this.localVertices=[new c.Vector3(r.x,r.y,r.z),new c.Vector3(i.x,r.y,r.z),new c.Vector3(r.x,i.y,r.z),new c.Vector3(i.x,i.y,r.z),new c.Vector3(r.x,r.y,i.z),new c.Vector3(i.x,r.y,i.z),new c.Vector3(r.x,i.y,i.z),new c.Vector3(i.x,i.y,i.z)];let a=[0,1,2,3,4,5,6,7,0,2,1,3,4,6,5,7,0,4,1,5,2,6,3,7],o=new c.BufferGeometry,s=new Float32Array(a.length*3);o.setAttribute(`position`,new c.BufferAttribute(s,3));let l=new c.LineBasicMaterial({color:t?.color?new c.Color(t.color):54015,linewidth:1,transparent:!0,opacity:.6,depthTest:!0});this.wireframeBox=new c.LineSegments(o,l),this.boxGroup.add(this.wireframeBox),this.lineX=this.createDynamicLine(15680580),this.lineY=this.createDynamicLine(1096065),this.lineZ=this.createDynamicLine(3900150);let u=document.createElement(`div`);u.className=`gmv-dimension-badge dim-x`,u.innerHTML=`<span>长 (X):</span> <strong>${T(this.modelSize.x,this.unit)}</strong>`,this.labelX=new d.CSS2DObject(u),this.boxGroup.add(this.labelX);let f=document.createElement(`div`);f.className=`gmv-dimension-badge dim-y`,f.innerHTML=`<span>高 (Y):</span> <strong>${T(this.modelSize.y,this.unit)}</strong>`,this.labelY=new d.CSS2DObject(f),this.boxGroup.add(this.labelY);let p=document.createElement(`div`);p.className=`gmv-dimension-badge dim-z`,p.innerHTML=`<span>宽 (Z):</span> <strong>${T(this.modelSize.z,this.unit)}</strong>`,this.labelZ=new d.CSS2DObject(p),this.boxGroup.add(this.labelZ),this.setVisible(t?.visible??!1)}createDynamicLine(e){let t=new c.BufferGeometry().setFromPoints([new c.Vector3,new c.Vector3]),n=new c.LineBasicMaterial({color:e,linewidth:3,depthTest:!1}),r=new c.Line(t,n);return this.boxGroup.add(r),r}update(e){if(!this.isVisible||!this.currentModel||this.localVertices.length<8)return;this.currentModel.updateMatrixWorld(!0);let t=this.currentModel.matrixWorld;for(let e=0;e<8;e++)this.worldVertices[e].copy(this.localVertices[e]).applyMatrix4(t);if(this.wireframeBox){let e=[0,1,2,3,4,5,6,7,0,2,1,3,4,6,5,7,0,4,1,5,2,6,3,7],t=this.wireframeBox.geometry.attributes.position,n=0;for(let r of e){let e=this.worldVertices[r];t.setXYZ(n++,e.x,e.y,e.z)}t.needsUpdate=!0}let n=0,r=1/0,i=e.position;for(let e=0;e<8;e++){let t=this.worldVertices[e].distanceTo(i);t<r&&(r=t,n=e)}let a=this.worldVertices[n],o=this.worldVertices[n^1],s=this.worldVertices[n^2],c=this.worldVertices[n^4];this.updateLine(this.lineX,a,o),this.updateLine(this.lineY,a,s),this.updateLine(this.lineZ,a,c),this.labelX&&this.labelX.position.copy(a).add(o).multiplyScalar(.5),this.labelY&&this.labelY.position.copy(a).add(s).multiplyScalar(.5),this.labelZ&&this.labelZ.position.copy(a).add(c).multiplyScalar(.5)}updateLine(e,t,n){if(!e)return;let r=e.geometry.attributes.position;r.setXYZ(0,t.x,t.y,t.z),r.setXYZ(1,n.x,n.y,n.z),r.needsUpdate=!0}setVisible(e){this.isVisible=e,this.boxGroup.visible=e}getVisible(){return this.isVisible}setUnit(e){this.unit=e,this.currentModel&&this.attachToModel(this.currentModel,{visible:this.isVisible,unit:e})}detach(){this.wireframeBox&&=(this.wireframeBox.geometry.dispose(),this.wireframeBox.material.dispose(),this.boxGroup.remove(this.wireframeBox),null),this.lineX&&=(this.lineX.geometry.dispose(),this.lineX.material.dispose(),this.boxGroup.remove(this.lineX),null),this.lineY&&=(this.lineY.geometry.dispose(),this.lineY.material.dispose(),this.boxGroup.remove(this.lineY),null),this.lineZ&&=(this.lineZ.geometry.dispose(),this.lineZ.material.dispose(),this.boxGroup.remove(this.lineZ),null),this.labelX&&=(this.boxGroup.remove(this.labelX),null),this.labelY&&=(this.boxGroup.remove(this.labelY),null),this.labelZ&&=(this.boxGroup.remove(this.labelZ),null),this.localVertices=[],this.currentModel=null}destroy(){this.detach(),this.scene.remove(this.boxGroup)}},D=class{camera;controls;tweenGroup;currentTween=null;isAutoRotating=!1;autoRotateAxis=`x`;autoRotateDirection=`clockwise`;autoRotateSpeed=1;lockedAxis=null;lockedPitchAngle=0;lockedYawAngle=0;constructor(e,t,n){this.camera=e,this.controls=t,this.tweenGroup=n||new f.Group}setAutoRotate(e,t,n,r){this.isAutoRotating=e,t!==void 0&&(this.autoRotateAxis=t),n!==void 0&&(this.autoRotateDirection=n),r!==void 0&&(this.autoRotateSpeed=r)}toggleAutoRotate(){return this.isAutoRotating=!this.isAutoRotating,this.isAutoRotating}setAutoRotateAxis(e){this.autoRotateAxis=e}setAutoRotateDirection(e){this.autoRotateDirection=e}setAutoRotateSpeed(e){this.autoRotateSpeed=Math.max(.1,Math.min(10,e))}getAutoRotateState(){return{enabled:this.isAutoRotating,axis:this.autoRotateAxis,direction:this.autoRotateDirection,speed:this.autoRotateSpeed}}setLockedAxis(e){this.lockedAxis=e;let t=this.camera.position.clone().sub(this.controls.target),n=t.length();n>1e-4&&(this.lockedPitchAngle=Math.asin(Math.max(-1,Math.min(1,t.y/n))),this.lockedYawAngle=Math.atan2(t.z,t.x))}getLockedAxis(){return this.lockedAxis}updatePerFrame(){let e=this.controls.target,t=this.camera.position.clone().sub(e),n=t.length();if(this.isAutoRotating&&n>.001){let n=this.autoRotateDirection===`clockwise`?1:-1,r=.008*this.autoRotateSpeed*n;if(this.autoRotateAxis===`x`){let e=new c.Vector3(1,0,0);t.applyAxisAngle(e,r),this.camera.up.applyAxisAngle(e,r)}else{let e=new c.Vector3(0,1,0);t.applyAxisAngle(e,r),this.camera.up.applyAxisAngle(e,r)}this.camera.position.copy(e).add(t),this.camera.lookAt(e)}if(this.lockedAxis&&n>.001){let t=this.camera.position.clone().sub(e),n=t.length();if(this.lockedAxis===`x`){let r=n*Math.cos(this.lockedPitchAngle),i=n*Math.sin(this.lockedPitchAngle),a=Math.atan2(t.z,t.x);t.x=r*Math.cos(a),t.y=i,t.z=r*Math.sin(a),this.camera.position.copy(e).add(t)}else if(this.lockedAxis===`y`){let r=t.y,i=Math.sqrt(Math.max(0,n*n-r*r));t.x=i*Math.cos(this.lockedYawAngle),t.z=i*Math.sin(this.lockedYawAngle),this.camera.position.copy(e).add(t)}else this.lockedAxis===`z`&&this.camera.up.set(0,1,0);this.camera.lookAt(e)}}setView(e,t,n=600){return new Promise(r=>{let i=new c.Box3().setFromObject(t),a=new c.Sphere;i.getBoundingSphere(a);let o=a.center.clone(),s=Math.max(a.radius,1);this.camera.near=Math.max(.01,s/500),this.camera.far=Math.max(1e4,s*50),this.camera.updateProjectionMatrix();let l=this.camera.fov*Math.PI/180,u=s/Math.sin(l/2)*1.15,d=new c.Vector3,p=new c.Vector3(0,1,0);switch(e){case`front`:d.set(0,0,1);break;case`back`:d.set(0,0,-1);break;case`left`:d.set(-1,0,0);break;case`right`:d.set(1,0,0);break;case`top`:d.set(0,1,1e-4),p.set(0,0,-1);break;case`bottom`:d.set(0,-1,1e-4),p.set(0,0,1);break;case`isometric`:d.set(1,1,1).normalize();break;case`isometric-back`:d.set(-1,1,-1).normalize()}let m=o.clone().add(d.multiplyScalar(u));if(this.currentTween&&=(this.currentTween.stop(),null),this.camera.up.copy(p),n<=0){this.camera.position.copy(m),this.controls.target.copy(o),this.controls.update(),r();return}let h=this.camera.position.clone(),g=this.controls.target.clone(),_={t:0};this.currentTween=new f.Tween(_,this.tweenGroup).to({t:1},n).easing(f.Easing.Cubic.Out).onUpdate(()=>{this.camera.position.lerpVectors(h,m,_.t),this.controls.target.lerpVectors(g,o,_.t),this.controls.update()}).onComplete(()=>{this.camera.position.copy(m),this.controls.target.copy(o),this.controls.update(),this.currentTween=null,r()}).start()})}fitToObject(e,t=500){return new Promise(n=>{let r=new c.Box3().setFromObject(e),i=new c.Sphere;r.getBoundingSphere(i);let a=i.center,o=Math.max(i.radius,1);this.camera.near=Math.max(.01,o/500),this.camera.far=Math.max(1e4,o*50),this.camera.updateProjectionMatrix();let s=this.camera.fov*Math.PI/180,l=o/Math.sin(s/2)*1.15,u=this.camera.position.clone().sub(this.controls.target).normalize(),d=a.clone().add(u.multiplyScalar(l)),p=this.camera.position.clone(),m=this.controls.target.clone(),h={t:0};new f.Tween(h,this.tweenGroup).to({t:1},t).easing(f.Easing.Cubic.Out).onUpdate(()=>{this.camera.position.lerpVectors(p,d,h.t),this.controls.target.lerpVectors(m,a,h.t),this.controls.update()}).onComplete(()=>{this.camera.position.copy(d),this.controls.target.copy(a),this.controls.update(),n()}).start()})}};function O(e,t,n,r=15,i=800,a=600){if(!n||n.length===0)return{hit:!1,point:null,normal:null,isVertexSnapped:!1,distanceToVertex:1/0};let o=new c.Raycaster;o.setFromCamera(e,t),o.firstHitOnly=!0;let s=o.intersectObjects(n,!0);if(s.length===0)return{hit:!1,point:null,normal:null,isVertexSnapped:!1,distanceToVertex:1/0};let l=s[0],u=l.point.clone(),d=l.face?l.face.normal.clone():new c.Vector3(0,1,0);l.object&&d.transformDirection(l.object.matrixWorld);let f=null,p=r;if(l.face&&l.object&&l.object.geometry){let n=l.object,r=n.geometry.attributes.position;if(r){let o=[l.face.a,l.face.b,l.face.c],s=new c.Vector3,u=new c.Vector3;for(let c of o){s.fromBufferAttribute(r,c).applyMatrix4(n.matrixWorld),u.copy(s).project(t);let o=(u.x+1)*.5*i,l=(-u.y+1)*.5*a,d=(e.x+1)*.5*i,m=(-e.y+1)*.5*a,h=Math.hypot(o-d,l-m);h<p&&(p=h,f=s.clone())}}}return f?{hit:!0,point:f,normal:d,isVertexSnapped:!0,distanceToVertex:p}:{hit:!0,point:u,normal:d,isVertexSnapped:!1,distanceToVertex:1/0}}var k=class{scene;camera;container;modelLoader;controls=null;emitter=null;planeX;planeY;planeZ;customPlane=null;isEnabled=!1;activeAxis=`x`;helperVisible=!0;planesGroup;interactive=!1;widgets={};isDraggingPlane=!1;draggedWidget=null;dragStartPointer=new c.Vector2;dragStartPosition=0;dragScreenAxis=new c.Vector2;dragScreenAxisLenSq=0;raycaster=new c.Raycaster;hoveredAxis=null;isPickingCustomPoints=!1;customPoints=[];customMarkersGroup;modelBox=new c.Box3;onPointerDownBound;onPointerMoveBound;onPointerMoveDragBound;onPointerUpDragBound;onDoubleClickBound;constructor(e,t,n,r,i,a){this.scene=e,this.camera=t,this.container=n,this.modelLoader=r,this.controls=i||null,this.emitter=a||null,this.planeX=new c.Plane(new c.Vector3(-1,0,0),1e5),this.planeY=new c.Plane(new c.Vector3(0,-1,0),1e5),this.planeZ=new c.Plane(new c.Vector3(0,0,-1),1e5),this.planesGroup=new c.Group,this.planesGroup.name=`gmv-interactive-clipping-planes`,this.planesGroup.visible=!1,this.scene.add(this.planesGroup),this.customMarkersGroup=new c.Group,this.customMarkersGroup.name=`gmv-custom-clip-markers`,this.scene.add(this.customMarkersGroup),this.onPointerDownBound=this.handlePointerDown.bind(this),this.onPointerMoveBound=this.handlePointerMove.bind(this),this.onPointerMoveDragBound=this.handlePointerMoveDrag.bind(this),this.onPointerUpDragBound=this.handlePointerUpDrag.bind(this),this.onDoubleClickBound=this.handleDoubleClick.bind(this),this.container.addEventListener(`pointerdown`,this.onPointerDownBound,{capture:!0}),this.container.addEventListener(`pointermove`,this.onPointerMoveBound),this.container.addEventListener(`dblclick`,this.onDoubleClickBound,{capture:!0})}setControls(e){this.controls=e}setEmitter(e){this.emitter=e}setInteractive(e){this.interactive=e&&this.isEnabled,!this.interactive&&this.container.style.cursor===`grab`&&(this.container.style.cursor=`default`)}setModelBounds(e){this.modelBox.copy(e),this.rebuildPlaneWidgets(),this.resetAll()}rebuildPlaneWidgets(){this.clearPlaneWidgets();let e=new c.Vector3(2,2,2),t=new c.Vector3(0,0,0);this.modelBox.isEmpty()||(this.modelBox.getSize(e),this.modelBox.getCenter(t));let n=Math.max(.001,e.x),r=Math.max(.001,e.y),i=Math.max(.001,e.z);this.widgets.x=this.createPlaneWidget(`x`,15680580,16281969,i,r,t),this.widgets.y=this.createPlaneWidget(`y`,1096065,3462041,n,i,t),this.widgets.z=this.createPlaneWidget(`z`,3900150,6333946,n,r,t),this.widgets.x&&(this.widgets.x.enabled=!0),this.widgets.y&&(this.widgets.y.enabled=!1),this.widgets.z&&(this.widgets.z.enabled=!1),this.updateWidgetsVisibility()}createPlaneWidget(e,t,n,r,i,a){let o=new c.Group;o.name=`gmv-clip-plane-${e}`;let s=new c.PlaneGeometry(r,i),l=new c.MeshBasicMaterial({color:t,transparent:!0,opacity:.25,side:c.DoubleSide,depthWrite:!1}),u=new c.Mesh(s,l);u.userData={axis:e},u.renderOrder=999,o.add(u);let f=new c.EdgesGeometry(s),p=new c.LineBasicMaterial({color:n,transparent:!0,opacity:.85}),m=new c.LineSegments(f,p);o.add(m);let h=new c.Group,g=Math.min(r,i)*.045,_=new c.MeshBasicMaterial({color:n,transparent:!0,opacity:.9,depthTest:!1}),v=new c.SphereGeometry(g*.5,16,16),y=new c.Mesh(v,_);y.userData={axis:e},h.add(y);let b=new c.CylinderGeometry(g*.12,g*.12,g*1.8,12);b.rotateX(Math.PI/2);let x=new c.Mesh(b,_);x.userData={axis:e},h.add(x);let S=new c.ConeGeometry(g*.35,g*.65,16);S.rotateX(Math.PI/2),S.translate(0,0,g*1.1);let C=new c.Mesh(S,_);C.userData={axis:e},h.add(C);let w=new c.ConeGeometry(g*.35,g*.65,16);w.rotateX(-Math.PI/2),w.translate(0,0,-g*1.1);let T=new c.Mesh(w,_);T.userData={axis:e},h.add(T),h.renderOrder=1e3,o.add(h);let E=[u,y,x,C,T],D=document.createElement(`div`);D.className=`gmv-plane-badge gmv-plane-badge-${e}`,D.textContent=`${e.toUpperCase()} 剖切面`;let O=new d.CSS2DObject(D);O.position.set(r/2*.68,i/2*.68,0),o.add(O);let k=new c.Vector3(1,0,0);return e===`x`?(o.rotation.y=Math.PI/2,o.position.set(a.x,a.y,a.z),k=new c.Vector3(1,0,0)):e===`y`?(o.rotation.x=-Math.PI/2,o.position.set(a.x,a.y,a.z),k=new c.Vector3(0,1,0)):(o.rotation.set(0,0,0),o.position.set(a.x,a.y,a.z),k=new c.Vector3(0,0,1)),this.planesGroup.add(o),{axis:e,group:o,mesh:u,border:m,arrowGroup:h,hitMeshes:E,label:O,labelText:D,meshMat:l,borderMat:p,arrowMat:_,normal:k,baseColor:t,borderColor:n,position:a[e],inverted:!1,enabled:!1}}clearPlaneWidgets(){[`x`,`y`,`z`].forEach(e=>{let t=this.widgets[e];t&&(t.mesh.geometry.dispose(),t.meshMat.dispose(),t.border.geometry.dispose(),t.borderMat.dispose(),t.arrowMat.dispose(),this.planesGroup.remove(t.group))}),this.widgets={}}updateWidgetsVisibility(){this.planesGroup.visible=this.isEnabled&&this.helperVisible,this.interactive=this.isEnabled,[`x`,`y`,`z`].forEach(e=>{let t=this.widgets[e];t&&(t.group.visible=this.isEnabled&&this.helperVisible&&t.enabled)})}setSectioningEnabled(e){if(this.isEnabled=e,this.updateWidgetsVisibility(),e){if(![`x`,`y`,`z`].some(e=>this.widgets[e]?.enabled)){let e=this.widgets[this.activeAxis]||this.widgets.x;e&&(e.enabled=!0)}[`x`,`y`,`z`].forEach(e=>{let t=this.widgets[e];t&&t.enabled?this.setPlanePositionExplicit(e,t.position):this.disableAxisInternal(e)})}else this.disableAllAxesPlanes();this.applyPlanes(),this.emitSectioningState()}getSectioningEnabled(){return this.isEnabled}setAxisEnabled(e,t){let n=this.widgets[e];n&&(n.enabled=t,t?(this.activeAxis=e,this.isEnabled&&this.setPlanePositionExplicit(e,n.position)):this.disableAxisInternal(e),this.updateWidgetsVisibility(),this.applyPlanes(),this.emitSectioningState())}toggleAxis(e,t){let n=this.widgets[e],r=t===void 0?!n?.enabled:t;return this.setAxisEnabled(e,r),r}isAxisEnabled(e){return!!this.widgets[e]?.enabled}setSectioningAxis(e){this.activeAxis=e;let t=this.widgets[e];t&&!t.enabled&&(t.enabled=!0,this.isEnabled&&this.setPlanePositionExplicit(e,t.position)),this.updateWidgetsVisibility(),this.emitSectioningState()}getSectioningAxis(){return this.activeAxis}setSectioningHelperVisible(e){this.helperVisible=e,this.updateWidgetsVisibility(),this.emitSectioningState()}getSectioningHelperVisible(){return this.helperVisible}setAxisReverse(e,t){let n=this.widgets[e];if(n){if(n.inverted=t,this.isEnabled&&n.enabled)this.setPlanePositionExplicit(e,n.position);else{let r=this.modelBox.min[e],i=this.modelBox.max[e]-r,a=i>0?(n.position-r)/i:.5;n.labelText.textContent=`${e.toUpperCase()} 剖切面 ${(a*100).toFixed(0)}%${t?` (反向)`:``}`}}this.emitSectioningState()}setSectioningReverse(e){this.setAxisReverse(this.activeAxis,e)}getSectioningReverse(){return!!this.widgets[this.activeAxis]?.inverted}invertAxis(e){let t=this.widgets[e];return t?(this.setAxisReverse(e,!t.inverted),t.inverted):!1}resetSectioning(e){let t=new c.Vector3,n=this.modelBox.isEmpty();n?t.set(0,0,0):this.modelBox.getCenter(t),this.clearCustomPlane(),(e?[e]:[`x`,`y`,`z`]).forEach(r=>{let i=this.widgets[r];if(i){i.position=t[r],i.inverted=!1,i.group.position.set(t.x,t.y,t.z),e||(i.enabled=r===`x`);let a=n?-1:this.modelBox.min[r],o=(n?1:this.modelBox.max[r])-a,s=o>0&&!isNaN(i.position)?Math.max(0,Math.min(1,(i.position-a)/o)):.5;i.labelText.textContent=`${r.toUpperCase()} 剖切面 ${(s*100).toFixed(0)}%`}}),this.activeAxis=e||`x`,this.helperVisible=!0,this.isEnabled=!0,[`x`,`y`,`z`].forEach(e=>{let t=this.widgets[e];t&&t.enabled?this.setPlanePositionExplicit(e,t.position):this.disableAxisInternal(e)}),this.updateWidgetsVisibility(),this.applyPlanes(),this.emitSectioningState()}getSectioningState(){let e=this.modelBox.isEmpty(),t=e?-1:this.modelBox.min.x,n=e?1:this.modelBox.max.x,r=e?-1:this.modelBox.min.y,i=e?1:this.modelBox.max.y,a=e?-1:this.modelBox.min.z,o=e?1:this.modelBox.max.z,s=(e,t,n)=>{let r=n-t;return isNaN(e)||r<=0?.5:Math.max(0,Math.min(1,(e-t)/r))},c=this.widgets.x,l=this.widgets.y,u=this.widgets.z,d={x:{enabled:c?.enabled??!1,inverted:c?.inverted??!1,position:c?.position??0,progress:s(c?.position??0,t,n)},y:{enabled:l?.enabled??!1,inverted:l?.inverted??!1,position:l?.position??0,progress:s(l?.position??0,r,i)},z:{enabled:u?.enabled??!1,inverted:u?.inverted??!1,position:u?.position??0,progress:s(u?.position??0,a,o)}},f=this.widgets[this.activeAxis],p=d[this.activeAxis]?.progress??.5,m=f?.position??0,h=f?.inverted??!1;return{enabled:this.isEnabled,axes:d,helperVisible:this.helperVisible,axis:this.activeAxis,isReverse:h,progress:p,position:m}}emitSectioningState(){this.emitter&&this.emitter.emit(`sectioning:change`,this.getSectioningState())}handlePointerDown(e){if(e.button!==0)return;if(this.isPickingCustomPoints){this.handleCustomPointPick(e),e.stopImmediatePropagation(),e.preventDefault();return}if(!this.interactive||!this.isEnabled||!this.helperVisible)return;let t=this.container.getBoundingClientRect(),n=new c.Vector2((e.clientX-t.left)/t.width*2-1,-((e.clientY-t.top)/t.height)*2+1);this.raycaster.setFromCamera(n,this.camera);let r=[`x`,`y`,`z`].map(e=>this.widgets[e]).filter(e=>!!(e&&e.enabled&&e.group.visible));if(r.length===0)return;let i=null;for(let e of r){let t=this.raycaster.intersectObjects(e.hitMeshes,!0);if(t.length>0){let n=t[0].distance;(!i||n<i.distance)&&(i={widget:e,distance:n})}}if(i){e.stopImmediatePropagation(),e.preventDefault();let n=i.widget;this.activeAxis=n.axis,this.isDraggingPlane=!0,this.draggedWidget=n,this.controls&&(this.controls.enabled=!1,`state`in this.controls&&(this.controls.state=-1,this.controls._moveCurr&&this.controls._movePrev&&this.controls._moveCurr.copy(this.controls._movePrev))),this.container.style.cursor=`grabbing`,n.meshMat.opacity=.65,n.arrowMat.opacity=1,this.dragStartPointer.set(e.clientX,e.clientY),this.dragStartPosition=n.position;let r=new c.Vector3;n.axis===`x`?r.set(1,0,0):n.axis===`y`?r.set(0,1,0):r.set(0,0,1);let a=n.group.position.clone(),o=n.group.position.clone().add(r),s=a.project(this.camera),l=new c.Vector2((s.x+1)*t.width/2,(-s.y+1)*t.height/2),u=o.project(this.camera),d=new c.Vector2((u.x+1)*t.width/2,(-u.y+1)*t.height/2);this.dragScreenAxis.subVectors(d,l),this.dragScreenAxisLenSq=this.dragScreenAxis.lengthSq(),window.addEventListener(`pointermove`,this.onPointerMoveDragBound,{capture:!0}),window.addEventListener(`pointerup`,this.onPointerUpDragBound,{capture:!0})}}handlePointerMoveDrag(e){if(!this.isDraggingPlane||!this.draggedWidget)return;e.stopImmediatePropagation(),e.preventDefault();let t=new c.Vector2(e.clientX-this.dragStartPointer.x,e.clientY-this.dragStartPointer.y),n=0;if(this.dragScreenAxisLenSq>25)n=this.dragScreenAxis.dot(t)/this.dragScreenAxisLenSq;else{let e=new c.Vector3;this.modelBox.getSize(e);let r=Math.max(.1,e[this.draggedWidget.axis]);n=-t.y/280*r}let r=this.modelBox.min[this.draggedWidget.axis],i=this.modelBox.max[this.draggedWidget.axis],a=this.dragStartPosition+n;a=Math.max(r,Math.min(i,a)),this.setPlanePositionExplicit(this.draggedWidget.axis,a)}handlePointerUpDrag(e){this.isDraggingPlane&&(e.stopImmediatePropagation(),e.preventDefault(),this.isDraggingPlane=!1,this.draggedWidget&&=(this.draggedWidget.meshMat.opacity=.25,this.draggedWidget.arrowMat.opacity=.9,null),this.container.style.cursor=`default`,window.removeEventListener(`pointermove`,this.onPointerMoveDragBound,{capture:!0}),window.removeEventListener(`pointerup`,this.onPointerUpDragBound,{capture:!0}),requestAnimationFrame(()=>{this.controls&&(`state`in this.controls&&(this.controls.state=-1,this.controls._moveCurr&&this.controls._movePrev&&this.controls._moveCurr.copy(this.controls._movePrev)),this.controls.enabled=!0)}))}handlePointerMove(e){if(this.isDraggingPlane||this.isPickingCustomPoints||!this.interactive||!this.isEnabled||!this.helperVisible)return;let t=this.container.getBoundingClientRect(),n=new c.Vector2((e.clientX-t.left)/t.width*2-1,-((e.clientY-t.top)/t.height)*2+1);this.raycaster.setFromCamera(n,this.camera);let r=[`x`,`y`,`z`].map(e=>this.widgets[e]).filter(e=>!!(e&&e.enabled&&e.group.visible)),i=null;for(let e of r)if(this.raycaster.intersectObjects(e.hitMeshes,!0).length>0){i=e.axis;break}this.setHoveredAxis(i),i?this.container.style.cursor=`grab`:this.container.style.cursor===`grab`&&(this.container.style.cursor=`default`)}setHoveredAxis(e){this.hoveredAxis!==e&&(this.hoveredAxis=e,[`x`,`y`,`z`].forEach(t=>{let n=this.widgets[t];n&&(t===e?(n.meshMat.opacity=.52,n.borderMat.opacity=1,n.arrowMat.opacity=1):(n.meshMat.opacity=.25,n.borderMat.opacity=.85,n.arrowMat.opacity=.9))}))}handleDoubleClick(e){if(!this.interactive||!this.isEnabled||!this.helperVisible)return;let t=this.container.getBoundingClientRect(),n=new c.Vector2((e.clientX-t.left)/t.width*2-1,-((e.clientY-t.top)/t.height)*2+1);this.raycaster.setFromCamera(n,this.camera);let r=[`x`,`y`,`z`].map(e=>this.widgets[e]).filter(e=>!!(e&&e.enabled&&e.group.visible));for(let t of r)if(this.raycaster.intersectObjects(t.hitMeshes,!0).length>0){e.stopImmediatePropagation(),e.preventDefault(),this.invertAxis(t.axis);break}}setPlanePositionExplicit(e,t){let n=this.widgets[e];if(!n)return;n.position=t,e===`x`?(n.group.position.x=t,n.inverted?this.planeX.set(new c.Vector3(1,0,0),-t):this.planeX.set(new c.Vector3(-1,0,0),t)):e===`y`?(n.group.position.y=t,n.inverted?this.planeY.set(new c.Vector3(0,1,0),-t):this.planeY.set(new c.Vector3(0,-1,0),t)):e===`z`&&(n.group.position.z=t,n.inverted?this.planeZ.set(new c.Vector3(0,0,1),-t):this.planeZ.set(new c.Vector3(0,0,-1),t)),n.enabled=!0,this.applyPlanes();let r=this.modelBox.min[e],i=this.modelBox.max[e]-r,a=i>0?(t-r)/i:.5;n.labelText.textContent=`${e.toUpperCase()} 剖切面 ${(a*100).toFixed(0)}%${n.inverted?` (反向)`:``}`,this.emitSectioningState(),this.emitter&&this.emitter.emit(`clipping:change`,{axis:e,position:t,progress:a,inverted:n.inverted,enabled:!0})}setAxisProgress(e,t){let n=this.modelBox.min[e],r=this.modelBox.max[e],i=n+Math.max(0,Math.min(1,t))*(r-n);this.setPlanePositionExplicit(e,i)}getAxisProgress(e){let t=this.widgets[e];if(!t)return .5;let n=this.modelBox.min[e],r=this.modelBox.max[e]-n;return r>0?Math.max(0,Math.min(1,(t.position-n)/r)):.5}disableAxisInternal(e){e===`x`?this.planeX.set(new c.Vector3(-1,0,0),1e5):e===`y`?this.planeY.set(new c.Vector3(0,-1,0),1e5):e===`z`&&this.planeZ.set(new c.Vector3(0,0,-1),1e5)}disableAllAxesPlanes(){[`x`,`y`,`z`].forEach(e=>{this.disableAxisInternal(e)})}setAxisClipping(e,t,n=!1){this.activeAxis=e;let r=this.widgets[e];r&&(r.inverted=n,r.enabled=!0),this.setSectioningEnabled(!0),this.setAxisProgress(e,t)}disableAxis(e){this.setAxisEnabled(e,!1)}getAxisState(e){let t=this.widgets[e];if(!t)return{enabled:!1,progress:.5,inverted:!1,position:0};let n=this.modelBox.min[e],r=this.modelBox.max[e]-n,i=r>0?(t.position-n)/r:.5;return{enabled:this.isEnabled&&t.enabled,progress:i,inverted:t.inverted,position:t.position}}setPlanesVisible(e){this.setSectioningHelperVisible(e)}getPlanesVisible(){return this.helperVisible}startCustomClip(){this.cancelCustomClip(),this.isPickingCustomPoints=!0,this.customPoints=[],this.container.style.cursor=`crosshair`}cancelCustomClip(){this.isPickingCustomPoints=!1,this.container.style.cursor=`default`,this.clearCustomMarkers()}clearCustomPlane(){this.customPlane=null,this.clearCustomMarkers(),this.applyPlanes()}invertCustomClip(){return this.customPlane?(this.customPlane.negate(),this.applyPlanes(),!0):!1}hasCustomPlane(){return this.customPlane!==null}handleCustomPointPick(e){let t=this.container.getBoundingClientRect(),n=O(new c.Vector2((e.clientX-t.left)/t.width*2-1,-((e.clientY-t.top)/t.height)*2+1),this.camera,this.modelLoader.getMeshes(),15,t.width,t.height);n.hit&&n.point&&this.addCustomPoint(n.point)}addCustomPoint(e){this.customPoints.push(e.clone());let t=new c.SphereGeometry(1,16,16),n=new c.MeshBasicMaterial({color:3718648,depthTest:!1}),r=new c.Mesh(t,n);if(r.position.copy(e),r.userData={fixedScreenRadius:5.5},this.customMarkersGroup.add(r),this.updateSingleMarkerScale(r),this.customPoints.length===2){let e=this.customPoints[0],t=this.customPoints[1],n=new c.Vector3().subVectors(t,e);if(n.lengthSq()<1e-5){this.customPoints.pop(),this.customMarkersGroup.remove(r);return}n.normalize();let i=new c.Vector3;this.camera.getWorldDirection(i);let a=new c.Vector3().crossVectors(n,i).normalize();a.lengthSq()<1e-4&&(a=new c.Vector3().crossVectors(n,this.camera.up).normalize()),this.customPlane=new c.Plane().setFromNormalAndCoplanarPoint(a,e),this.clearCustomMarkers(),this.cancelCustomClip(),this.applyPlanes()}}updatePerFrame(e){if(this.customMarkersGroup.children.length===0)return;let t=e||this.container.clientHeight||window.innerHeight||800,n=this.camera.fov*Math.PI/180,r=this.camera.position,i=Math.tan(n/2);this.customMarkersGroup.traverse(e=>{if(e.userData&&typeof e.userData.fixedScreenRadius==`number`){let n=e.userData.fixedScreenRadius,a=new c.Vector3;e.getWorldPosition(a);let o=2*a.distanceTo(r)*i*n/Math.max(t,1);e.scale.setScalar(o)}})}updateSingleMarkerScale(e,t){if(e.userData&&typeof e.userData.fixedScreenRadius==`number`){let n=e.userData.fixedScreenRadius,r=t||this.container.clientHeight||window.innerHeight||800,i=this.camera.fov*Math.PI/180,a=this.camera.position,o=Math.tan(i/2),s=new c.Vector3;e.getWorldPosition(s);let l=2*s.distanceTo(a)*o*n/Math.max(r,1);e.scale.setScalar(l)}}clearCustomMarkers(){for(;this.customMarkersGroup.children.length>0;){let e=this.customMarkersGroup.children[0];e.geometry&&e.geometry.dispose(),e.material&&e.material.dispose(),this.customMarkersGroup.remove(e)}}applyPlanes(){let e=[];this.isEnabled&&(this.widgets.x?.enabled&&e.push(this.planeX),this.widgets.y?.enabled&&e.push(this.planeY),this.widgets.z?.enabled&&e.push(this.planeZ)),this.customPlane&&e.push(this.customPlane),this.modelLoader.applyClippingPlanes(e)}resetAll(){this.isEnabled=!1,this.activeAxis=`x`,this.helperVisible=!0,this.disableAllAxesPlanes();let e=new c.Vector3;this.modelBox.getCenter(e),[`x`,`y`,`z`].forEach(t=>{let n=this.widgets[t];n&&(n.enabled=t===`x`,n.inverted=!1,n.position=e[t],n.group.position.set(e.x,e.y,e.z),n.labelText.textContent=`${t.toUpperCase()} 剖切面`)}),this.updateWidgetsVisibility(),this.clearCustomPlane(),this.applyPlanes(),this.emitSectioningState()}destroy(){this.cancelCustomClip(),this.resetAll(),this.clearPlaneWidgets(),this.scene.remove(this.planesGroup),this.scene.remove(this.customMarkersGroup),this.container.removeEventListener(`pointerdown`,this.onPointerDownBound,{capture:!0}),this.container.removeEventListener(`pointermove`,this.onPointerMoveBound),this.container.removeEventListener(`dblclick`,this.onDoubleClickBound,{capture:!0}),window.removeEventListener(`pointermove`,this.onPointerMoveDragBound,{capture:!0}),window.removeEventListener(`pointerup`,this.onPointerUpDragBound,{capture:!0})}},A=class{scene;camera;container;modelLoader;emitter;controls=null;currentMode=`none`;unit=`m`;measurementGroup;results=[];activePoints=[];activePreviewGroup;activeMarkersGroup;activeLineGroup;snapCursorMesh;onPointerDownBound;onPointerMoveBound;onDoubleClickBound;constructor(e,t,n,r,i,a){this.scene=e,this.camera=t,this.container=n,this.modelLoader=r,this.emitter=i,this.controls=a||null,this.measurementGroup=new c.Group,this.measurementGroup.name=`gmv-measurements-group`,this.scene.add(this.measurementGroup),this.activePreviewGroup=new c.Group,this.activePreviewGroup.name=`gmv-measure-preview-group`,this.scene.add(this.activePreviewGroup),this.activeMarkersGroup=new c.Group,this.activeMarkersGroup.name=`gmv-active-markers-group`,this.activePreviewGroup.add(this.activeMarkersGroup),this.activeLineGroup=new c.Group,this.activeLineGroup.name=`gmv-active-line-group`,this.activePreviewGroup.add(this.activeLineGroup);let o=new c.SphereGeometry(1,16,16),s=new c.MeshBasicMaterial({color:3718648,transparent:!0,opacity:.9,depthTest:!1});this.snapCursorMesh=new c.Mesh(o,s),this.snapCursorMesh.userData={fixedScreenRadius:5.5},this.snapCursorMesh.visible=!1,this.scene.add(this.snapCursorMesh),this.onPointerDownBound=this.handlePointerDown.bind(this),this.onPointerMoveBound=this.handlePointerMove.bind(this),this.onDoubleClickBound=this.handleDoubleClick.bind(this)}setControls(e){this.controls=e}setUnit(e){this.unit=e}setMode(e){this.currentMode!==e&&(this.cancelActive(),this.currentMode=e,e===`none`?(this.container.style.cursor=`default`,this.snapCursorMesh.visible=!1,this.removeListeners()):(this.container.style.cursor=`crosshair`,this.addListeners(),this.emitter.emit(`measure:start`,e)))}getMode(){return this.currentMode}getControls(){return this.controls}addListeners(){this.removeListeners(),this.container.addEventListener(`pointerdown`,this.onPointerDownBound,{capture:!0}),this.container.addEventListener(`pointermove`,this.onPointerMoveBound),this.container.addEventListener(`dblclick`,this.onDoubleClickBound)}removeListeners(){this.container.removeEventListener(`pointerdown`,this.onPointerDownBound,{capture:!0}),this.container.removeEventListener(`pointermove`,this.onPointerMoveBound),this.container.removeEventListener(`dblclick`,this.onDoubleClickBound)}handlePointerMove(e){if(this.currentMode===`none`)return;let t=this.container.getBoundingClientRect(),n=O(new c.Vector2((e.clientX-t.left)/t.width*2-1,-((e.clientY-t.top)/t.height)*2+1),this.camera,this.modelLoader.getMeshes(),15,t.width,t.height);!n.hit||!n.point?(this.container.style.cursor=`not-allowed`,this.snapCursorMesh.visible=!1):(this.container.style.cursor=`crosshair`,this.snapCursorMesh.visible=!0,this.snapCursorMesh.position.copy(n.point),n.isVertexSnapped?(this.snapCursorMesh.material.color.setHex(16007006),this.snapCursorMesh.userData.fixedScreenRadius=7.5):(this.snapCursorMesh.material.color.setHex(3718648),this.snapCursorMesh.userData.fixedScreenRadius=5.5),this.updateSingleMeshScale(this.snapCursorMesh),this.activePoints.length>0&&this.updateDynamicPreview(n.point))}handlePointerDown(e){if(this.currentMode===`none`||e.button!==0)return;let t=this.container.getBoundingClientRect(),n=O(new c.Vector2((e.clientX-t.left)/t.width*2-1,-((e.clientY-t.top)/t.height)*2+1),this.camera,this.modelLoader.getMeshes(),15,t.width,t.height);if(!n.hit||!n.point)return;let r=n.point.clone();this.activePoints.push(r),this.addActiveMarker(r),this.emitter.emit(`measure:change`,[...this.activePoints]),this.currentMode===`distance`&&this.activePoints.length===2?this.finishDistanceMeasurement():this.currentMode===`height`&&this.activePoints.length===2?this.finishHeightMeasurement():this.currentMode===`angle`&&this.activePoints.length===3&&this.finishAngleMeasurement()}addActiveMarker(e){let t=new c.SphereGeometry(1,16,16),n=new c.MeshBasicMaterial({color:3718648,depthTest:!1}),r=new c.Mesh(t,n);r.position.copy(e),r.userData={fixedScreenRadius:5.5},this.activeMarkersGroup.add(r),this.updateSingleMeshScale(r)}handleDoubleClick(e){this.currentMode===`area`&&this.activePoints.length>=3&&this.finishAreaMeasurement()}updateDynamicPreview(e){this.clearActiveLine();let t=[...this.activePoints,e];if(t.length<2)return;let n=new c.BufferGeometry().setFromPoints(t),r=new c.LineDashedMaterial({color:3718648,dashSize:.2,gapSize:.1,depthTest:!1}),i=new c.Line(n,r);i.computeLineDistances(),this.activeLineGroup.add(i)}finishDistanceMeasurement(){let e=this.activePoints[0],t=this.activePoints[1],n=e.distanceTo(t),r=new c.Vector3().addVectors(e,t).multiplyScalar(.5),i=new c.Group,a=new c.BufferGeometry().setFromPoints([e,t]),o=new c.LineBasicMaterial({color:3900150,linewidth:2,depthTest:!1}),s=new c.Line(a,o);i.add(s),this.addEndSphere(i,e,3900150),this.addEndSphere(i,t,3900150);let l=document.createElement(`div`);l.className=`gmv-measure-badge distance`,l.innerHTML=`<span>距离:</span> <strong>${T(n,this.unit)}</strong>`;let u=new d.CSS2DObject(l);u.position.copy(r),i.add(u),this.measurementGroup.add(i);let f={id:`dist_${Date.now()}`,type:`distance`,value:n,formattedValue:T(n,this.unit),unit:this.unit,points:[e,t]};this.results.push(f),this.emitter.emit(`measure:complete`,f),this.cancelActive(),this.setMode(`none`)}finishHeightMeasurement(){let e=this.activePoints[0],t=this.activePoints[1],n=Math.abs(t.y-e.y),r=new c.Vector2(e.x,e.z).distanceTo(new c.Vector2(t.x,t.z)),i=e.distanceTo(t),a=new c.Group,o=new c.BufferGeometry().setFromPoints([e,t]),s=new c.LineDashedMaterial({color:9741240,dashSize:.15,gapSize:.08,depthTest:!1}),l=new c.Line(o,s);l.computeLineDistances(),a.add(l);let u=new c.Vector3(t.x,e.y,t.z),f=new c.BufferGeometry().setFromPoints([e,u]),p=new c.LineBasicMaterial({color:1096065,depthTest:!1}),m=new c.Line(f,p);a.add(m);let h=new c.BufferGeometry().setFromPoints([u,t]),g=new c.LineBasicMaterial({color:15680580,linewidth:2,depthTest:!1}),_=new c.Line(h,g);a.add(_),this.addEndSphere(a,e,1096065),this.addEndSphere(a,t,15680580),this.addEndSphere(a,u,16436245,4);let v=new c.Vector3().addVectors(u,t).multiplyScalar(.5),y=document.createElement(`div`);y.className=`gmv-measure-badge height`,y.innerHTML=`
|
|
2
|
+
<div><span>高差 (ΔH):</span> <strong>${T(n,this.unit)}</strong></div>
|
|
3
|
+
<div style="font-size:10px;opacity:0.85;">水平: ${T(r,this.unit)} · 直线: ${T(i,this.unit)}</div>
|
|
4
|
+
`;let b=new d.CSS2DObject(y);b.position.copy(v),a.add(b),this.measurementGroup.add(a);let x={id:`height_${Date.now()}`,type:`height`,value:n,formattedValue:T(n,this.unit),unit:this.unit,points:[e,t],extra:{verticalHeight:n,horizontalDistance:r}};this.results.push(x),this.emitter.emit(`measure:complete`,x),this.cancelActive(),this.setMode(`none`)}finishAreaMeasurement(){if(this.activePoints.length<3)return;let e=[...this.activePoints],t=w(e),n=new c.Group,r=[...e,e[0]],i=new c.BufferGeometry().setFromPoints(r),a=new c.LineBasicMaterial({color:9133302,linewidth:2,depthTest:!1}),o=new c.Line(i,a);n.add(o),e.forEach(e=>this.addEndSphere(n,e,9133302));let s=new c.Vector3;e.forEach(e=>s.add(e)),s.divideScalar(e.length);let l=document.createElement(`div`);l.className=`gmv-measure-badge area`,l.innerHTML=`<span>面积:</span> <strong>${T(t,`${this.unit}²`)}</strong>`;let u=new d.CSS2DObject(l);u.position.copy(s),n.add(u),this.measurementGroup.add(n);let f={id:`area_${Date.now()}`,type:`area`,value:t,formattedValue:T(t,`${this.unit}²`),unit:`${this.unit}²`,points:e,extra:{area:t}};this.results.push(f),this.emitter.emit(`measure:complete`,f),this.cancelActive(),this.setMode(`none`)}finishAngleMeasurement(){if(this.activePoints.length!==3)return;let e=this.activePoints[0],t=this.activePoints[1],n=this.activePoints[2],r=new c.Vector3().subVectors(e,t).normalize(),i=new c.Vector3().subVectors(n,t).normalize(),a=r.dot(i);a=Math.max(-1,Math.min(1,a));let o=Math.acos(a),s=c.MathUtils.radToDeg(o),l=new c.Group,u=new c.BufferGeometry().setFromPoints([t,e]),f=new c.BufferGeometry().setFromPoints([t,n]),p=new c.LineBasicMaterial({color:16096779,linewidth:2,depthTest:!1});l.add(new c.Line(u,p)),l.add(new c.Line(f,p)),this.addEndSphere(l,e,16096779),this.addEndSphere(l,n,16096779),this.addEndSphere(l,t,15680580,6.5);let m=Math.min(e.distanceTo(t),n.distanceTo(t))*.35||.4,h=[];for(let e=0;e<=16;e++){let n=e/16,a=new c.Vector3().copy(r).lerp(i,n).normalize().multiplyScalar(m);h.push(new c.Vector3().addVectors(t,a))}let g=new c.BufferGeometry().setFromPoints(h),_=new c.LineBasicMaterial({color:16498468,depthTest:!1});l.add(new c.Line(g,_));let v=new c.Vector3().addVectors(r,i).normalize(),y=t.clone().add(v.multiplyScalar(m*1.3)),b=document.createElement(`div`);b.className=`gmv-measure-badge angle`,b.innerHTML=`<span>角度:</span> <strong>${s.toFixed(1)}°</strong>`;let x=new d.CSS2DObject(b);x.position.copy(y),l.add(x),this.measurementGroup.add(l);let S={id:`angle_${Date.now()}`,type:`angle`,value:s,formattedValue:`${s.toFixed(1)}°`,unit:`°`,points:[e,t,n],extra:{angleDegree:s}};this.results.push(S),this.emitter.emit(`measure:complete`,S),this.cancelActive(),this.setMode(`none`)}addEndSphere(e,t,n,r=5.5){let i=new c.SphereGeometry(1,16,16),a=new c.MeshBasicMaterial({color:n,depthTest:!1}),o=new c.Mesh(i,a);o.position.copy(t),o.userData={fixedScreenRadius:r},e.add(o),this.updateSingleMeshScale(o)}undoLastPoint(){if(this.activePoints.length>0){if(this.activePoints.pop(),this.activeMarkersGroup.children.length>0){let e=this.activeMarkersGroup.children[this.activeMarkersGroup.children.length-1];this.disposeObjectHierarchy(e),this.activeMarkersGroup.remove(e)}this.clearActiveLine(),this.emitter.emit(`measure:change`,[...this.activePoints])}}cancelActive(){this.activePoints=[],this.clearActiveLine(),this.clearActiveMarkers()}clearActiveLine(){for(;this.activeLineGroup.children.length>0;){let e=this.activeLineGroup.children[0];this.disposeObjectHierarchy(e),this.activeLineGroup.remove(e)}}clearActiveMarkers(){for(;this.activeMarkersGroup.children.length>0;){let e=this.activeMarkersGroup.children[0];this.disposeObjectHierarchy(e),this.activeMarkersGroup.remove(e)}}updatePerFrame(e){let t=e||this.container.clientHeight||window.innerHeight||800,n=this.camera.fov*Math.PI/180,r=this.camera.position,i=Math.tan(n/2),a=e=>{if(e.userData&&typeof e.userData.fixedScreenRadius==`number`){let n=e.userData.fixedScreenRadius,a=new c.Vector3;e.getWorldPosition(a);let o=2*a.distanceTo(r)*i*n/Math.max(t,1);e.scale.setScalar(o)}};this.snapCursorMesh&&this.snapCursorMesh.visible&&a(this.snapCursorMesh),this.activeMarkersGroup.traverse(a),this.measurementGroup.traverse(a)}updateSingleMeshScale(e,t){if(e.userData&&typeof e.userData.fixedScreenRadius==`number`){let n=e.userData.fixedScreenRadius,r=t||this.container.clientHeight||window.innerHeight||800,i=this.camera.fov*Math.PI/180,a=this.camera.position,o=Math.tan(i/2),s=new c.Vector3;e.getWorldPosition(s);let l=2*s.distanceTo(a)*o*n/Math.max(r,1);e.scale.setScalar(l)}}clearAll(){for(this.cancelActive();this.measurementGroup.children.length>0;){let e=this.measurementGroup.children[0];this.disposeObjectHierarchy(e),this.measurementGroup.remove(e)}this.snapCursorMesh.visible=!1,this.results=[],this.emitter.emit(`measure:clear`)}disposeObjectHierarchy(e){e.traverse(e=>{if(e.isCSS2DObject&&e.element){let t=e.element;t&&t.parentElement?t.parentElement.removeChild(t):t&&t.remove()}if(e.isMesh||e.isLine){let t=e;t.geometry&&t.geometry.dispose(),t.material&&(Array.isArray(t.material)?t.material.forEach(e=>e.dispose()):t.material.dispose())}})}getResults(){return[...this.results]}destroy(){this.removeListeners(),this.clearAll(),this.scene.remove(this.measurementGroup),this.scene.remove(this.activePreviewGroup),this.scene.remove(this.snapCursorMesh)}},j=class{renderer;scene;camera;getModel;constructor(e,t,n,r){this.renderer=e,this.scene=t,this.camera=n,this.getModel=r||(()=>null)}getSceneMaxTextureSize(){let e=0,t=0;return this.scene.traverse(n=>{if(n.isMesh){let r=n,i=n=>{if(`map`in n){let r=n;if(r.map){let n=r.map.image||r.map.source?.data;if(n){let r=n.width||0,i=n.height||0;r>0&&i>0&&r*i>e*t&&(e=r,t=i)}}}};Array.isArray(r.material)?r.material.forEach(i):r.material&&i(r.material)}}),e>0&&t>0?{width:e,height:t}:null}async exportImage(e={}){let t=e.originalQuality===!0||e.scale===`original`,n=t?1:typeof e.scale==`number`?e.scale:2,r=e.type??`image/png`,i=e.quality??(r===`image/png`?1:.95),a=e.download??!1,o=new c.Vector2;this.renderer.getSize(o);let s=this.renderer.getPixelRatio(),l=this.scene.background,u=o.x/o.y,d,f;if(e.targetSize)d=e.targetSize.width,f=e.targetSize.height;else if(t){let e=this.getSceneMaxTextureSize();if(e&&e.width>0&&e.height>0){let t=Math.max(e.width,e.height);u>=1?(d=t,f=Math.round(t/u)):(f=t,d=Math.round(t*u))}else{let e=Math.max(Math.round(o.x*s),2048);u>=1?(d=e,f=Math.round(e/u)):(f=e,d=Math.round(e*u))}}else d=Math.round(o.x*n),f=Math.round(o.y*n);let p=e.filename??`model-${t?`texture-original-${d}x${f}`:`${n}x`}-${Date.now()}.${r===`image/jpeg`?`jpg`:`png`}`,m=e.modelOnly??!0,h=[];e.background!==void 0&&(e.background===null?(this.scene.background=null,this.renderer.setClearAlpha(0)):(this.scene.background=new c.Color(e.background),this.renderer.setClearAlpha(1)));try{if(m){let e=this.getModel(),t=t=>!!(t.isLight||t.name===`gmv-lights-group`||t.children&&t.children.some(e=>e.isLight)||e&&(t===e||t.uuid===e.uuid)||t.name===`gmv-current-model`);for(let e of this.scene.children)!t(e)&&e.visible&&(h.push({obj:e,wasVisible:!0}),e.visible=!1)}this.renderer.setPixelRatio(1),this.renderer.setSize(d,f,!1),this.camera.aspect=d/f,this.camera.updateProjectionMatrix();let e=this.camera.fov*Math.PI/180,t=this.camera.position,n=Math.tan(e/2);this.scene.traverse(e=>{if(e.userData&&typeof e.userData.fixedScreenRadius==`number`){let r=e.userData.fixedScreenRadius,i=new c.Vector3;e.getWorldPosition(i);let a=2*i.distanceTo(t)*n*r/Math.max(f,1);e.scale.setScalar(a)}}),this.renderer.render(this.scene,this.camera);let o=this.renderer.domElement.toDataURL(r,i);if(a){let e=document.createElement(`a`);e.download=p,e.href=o,document.body.appendChild(e),e.click(),document.body.removeChild(e)}return o}finally{if(h.length>0)for(let{obj:e,wasVisible:t}of h)e.visible=t;this.scene.background=l,this.renderer.setPixelRatio(s),this.renderer.setSize(o.x,o.y,!0),this.camera.aspect=o.x/o.y,this.camera.updateProjectionMatrix();let e=o.y,t=this.camera.fov*Math.PI/180,n=this.camera.position,r=Math.tan(t/2);this.scene.traverse(t=>{if(t.userData&&typeof t.userData.fixedScreenRadius==`number`){let i=t.userData.fixedScreenRadius,a=new c.Vector3;t.getWorldPosition(a);let o=2*a.distanceTo(n)*r*i/Math.max(e,1);t.scale.setScalar(o)}}),this.renderer.render(this.scene,this.camera)}}},M=class extends v{sceneManager;modelLoader;boundingBoxTool;cameraTool;clippingTool;measurementTool;screenshotTool;currentModel=null;currentModelInfo=null;unit=`m`;defaultShowBoundingBox=!1;constructor(e){super(),this.unit=e.unit||`m`,this.defaultShowBoundingBox=e.showBoundingBox??!1,this.sceneManager=new _(e),this.modelLoader=new b,this.boundingBoxTool=new E(this.sceneManager.scene),this.cameraTool=new D(this.sceneManager.camera,this.sceneManager.controls,this.sceneManager.tweenGroup),this.clippingTool=new k(this.sceneManager.scene,this.sceneManager.camera,this.sceneManager.container,this.modelLoader,this.sceneManager.controls,this),this.measurementTool=new A(this.sceneManager.scene,this.sceneManager.camera,this.sceneManager.container,this.modelLoader,this,this.sceneManager.controls),this.measurementTool.setUnit(this.unit),this.screenshotTool=new j(this.sceneManager.renderer,this.sceneManager.scene,this.sceneManager.camera,()=>this.currentModel),this.measurementTool.setUnit(this.unit),this.sceneManager.onBeforeRender=()=>{this.boundingBoxTool.update(this.sceneManager.camera),this.cameraTool.updatePerFrame(),this.measurementTool.updatePerFrame(),this.clippingTool.updatePerFrame()}}async loadZip(e){try{this.emit(`load:start`,typeof e==`string`?e:`ZIP File`),this.clearModel();let t=await this.modelLoader.loadZip(e,(e,t)=>{this.emit(`load:progress`,e,t)});return this.setupLoadedModel(t),this.emit(`load:success`,t),t}catch(e){let t=e instanceof Error?e:Error(String(e));throw this.emit(`load:error`,t),t}}async loadOBJ(e,t){try{this.emit(`load:start`,e),this.clearModel();let n=await this.modelLoader.loadOBJ(e,t,(e,t)=>{this.emit(`load:progress`,e,t)});return this.setupLoadedModel(n),this.emit(`load:success`,n),n}catch(e){let t=e instanceof Error?e:Error(String(e));throw this.emit(`load:error`,t),t}}loadSampleGeometry(e=`mechanical`){this.clearModel();let t=new c.Group;if(e===`mechanical`){let e=new c.BoxGeometry(6,1.2,4),n=new c.MeshStandardMaterial({color:4674921,metalness:.8,roughness:.3}),r=new c.Mesh(e,n);r.position.y=.6,t.add(r);let i=new c.CylinderGeometry(1.2,1.2,3,32),a=new c.MeshStandardMaterial({color:165063,metalness:.6,roughness:.2}),o=new c.Mesh(i,a);o.position.set(0,2.7,0),t.add(o);let s=new c.TorusGeometry(1.6,.25,16,40),l=new c.MeshStandardMaterial({color:14753096,metalness:.7,roughness:.2}),u=new c.Mesh(s,l);u.rotation.x=Math.PI/2,u.position.set(0,3.2,0),t.add(u)}else if(e===`building`){let e=new c.BoxGeometry(4,10,4),n=new c.MeshStandardMaterial({color:3359061,roughness:.4}),r=new c.Mesh(e,n);r.position.y=5,t.add(r);let i=new c.ConeGeometry(3,2.5,4),a=new c.MeshStandardMaterial({color:12131356,roughness:.5}),o=new c.Mesh(i,a);o.position.y=11.25,o.rotation.y=Math.PI/4,t.add(o)}else{let e=new c.TorusKnotGeometry(2.5,.7,100,16),n=new c.MeshStandardMaterial({color:9133302,metalness:.5,roughness:.2}),r=new c.Mesh(e,n);r.position.y=3,t.add(r)}let n=this.modelLoader.postProcessModel(t);return this.setupLoadedModel(n),this.emit(`load:success`,n),n}setupLoadedModel(e){this.currentModel=e.model,this.currentModel.name=`gmv-current-model`,this.currentModelInfo=e,this.sceneManager.scene.add(this.currentModel),this.boundingBoxTool.attachToModel(this.currentModel,{visible:this.defaultShowBoundingBox,unit:this.unit}),this.clippingTool.setModelBounds(e.box),this.cameraTool.setView(`front`,this.currentModel,0)}clearModel(){this.measurementTool.clearAll(),this.clippingTool.resetAll(),this.boundingBoxTool.detach(),this.currentModel&&=(this.sceneManager.scene.remove(this.currentModel),null),this.currentModelInfo=null,this.modelLoader.cleanup()}setBackground(e,t){this.sceneManager.setBackground(e,t)}setModelDisplayStyle(e){this.modelLoader.setDisplayStyle(e),this.emit(`style:change`,e)}getModelDisplayStyle(){return this.modelLoader.getDisplayStyle()}toggleTexture(e){this.modelLoader.toggleTexture(e),this.emit(`texture:toggle`,e)}getTextureState(){return this.modelLoader.getTextureState()}setPlainColor(e){this.modelLoader.setPlainColor(e)}getPlainColor(){return this.modelLoader.getPlainColor()}getModelTextureSize(){return this.modelLoader.getModelTextureSize()}setGridVisible(e){this.sceneManager.gridHelper.visible=e}showBoundingBox(e,t){this.boundingBoxTool.setVisible(e),t?.unit&&(this.unit=t.unit,this.boundingBoxTool.setUnit(t.unit),this.measurementTool.setUnit(t.unit))}getBoundingBoxVisible(){return this.boundingBoxTool.getVisible()}setClipping(e,t,n=!1){this.clippingTool.setAxisClipping(e,t,n)}disableClippingAxis(e){this.clippingTool.disableAxis(e)}toggleClippingAxis(e,t){return this.clippingTool.toggleAxis(e,t)}invertClippingAxis(e){return this.clippingTool.invertAxis(e)}getClippingAxisState(e){return this.clippingTool.getAxisState(e)}setClippingPlanesVisible(e){this.clippingTool.setPlanesVisible(e)}getClippingPlanesVisible(){return this.clippingTool.getPlanesVisible()}setSectioningEnabled(e){this.clippingTool.setSectioningEnabled(e)}getSectioningEnabled(){return this.clippingTool.getSectioningEnabled()}setSectioningAxis(e){this.clippingTool.setSectioningAxis(e)}getSectioningAxis(){return this.clippingTool.getSectioningAxis()}setSectioningAxisEnabled(e,t){this.clippingTool.setAxisEnabled(e,t)}toggleSectioningAxis(e,t){return this.clippingTool.toggleAxis(e,t)}isSectioningAxisEnabled(e){return this.clippingTool.isAxisEnabled(e)}setSectioningAxisReverse(e,t){this.clippingTool.setAxisReverse(e,t)}invertSectioningAxis(e){return this.clippingTool.invertAxis(e)}setSectioningAxisProgress(e,t){this.clippingTool.setAxisProgress(e,t)}getSectioningAxisProgress(e){return this.clippingTool.getAxisProgress(e)}setSectioningHelperVisible(e){this.clippingTool.setSectioningHelperVisible(e)}getSectioningHelperVisible(){return this.clippingTool.getSectioningHelperVisible()}setSectioningReverse(e){this.clippingTool.setSectioningReverse(e)}getSectioningReverse(){return this.clippingTool.getSectioningReverse()}resetSectioning(e){this.clippingTool.resetSectioning(e)}getSectioningState(){return this.clippingTool.getSectioningState()}startCustomClip(){this.measurementTool.setMode(`none`),this.clippingTool.startCustomClip()}cancelCustomClip(){this.clippingTool.cancelCustomClip()}clearCustomClip(){this.clippingTool.clearCustomPlane()}invertCustomClip(){return this.clippingTool.invertCustomClip()}hasCustomClip(){return this.clippingTool.hasCustomPlane()}resetClipping(){this.clippingTool.resetAll()}setMeasurementMode(e){e===`none`?this.clippingTool.setInteractive(!0):(this.clippingTool.cancelCustomClip(),this.clippingTool.setInteractive(!1)),this.measurementTool.setMode(e)}getMeasurementMode(){return this.measurementTool.getMode()}clearMeasurements(){this.measurementTool.clearAll()}undoMeasurement(){this.measurementTool.undoLastPoint()}getMeasurements(){return this.measurementTool.getResults()}async setView(e,t=600){this.currentModel&&(await this.cameraTool.setView(e,this.currentModel,t),this.emit(`view:change`,e))}async fitView(e=500){this.currentModel&&await this.cameraTool.fitToObject(this.currentModel,e)}setAutoRotate(e,t,n,r){this.cameraTool.setAutoRotate(e,t,n,r),this.emit(`autorotate:change`,this.cameraTool.getAutoRotateState())}toggleAutoRotate(){let e=this.cameraTool.toggleAutoRotate();return this.emit(`autorotate:change`,this.cameraTool.getAutoRotateState()),e}setAutoRotateAxis(e){this.cameraTool.setAutoRotateAxis(e),this.emit(`autorotate:change`,this.cameraTool.getAutoRotateState())}setAutoRotateDirection(e){this.cameraTool.setAutoRotateDirection(e),this.emit(`autorotate:change`,this.cameraTool.getAutoRotateState())}setAutoRotateSpeed(e){this.cameraTool.setAutoRotateSpeed(e),this.emit(`autorotate:change`,this.cameraTool.getAutoRotateState())}getAutoRotateState(){return this.cameraTool.getAutoRotateState()}setRotationLockedAxis(e){this.cameraTool.setLockedAxis(e),this.emit(`axislock:change`,e)}getRotationLockedAxis(){return this.cameraTool.getLockedAxis()}async exportImage(e={}){return this.screenshotTool.exportImage(e)}on(e,t){return super.on(e,t)}off(e,t){return super.off(e,t)}destroy(){this.clearModel(),this.measurementTool.destroy(),this.clippingTool.destroy(),this.sceneManager.destroy(),this.removeAllListeners()}};exports.BoundingBoxTool=E,exports.CameraTool=D,exports.ClippingTool=k,exports.EventEmitter=v,exports.MeasurementTool=A,exports.ModelLoader=b,exports.SceneManager=_,exports.ScreenshotTool=j,exports.Viewer=M,exports.ZipExtractor=y,exports.compute3DPolygonArea=w,exports.computeGroupVolume=S,exports.computeMeshVolume=x,exports.findSnapPoint=O,exports.formatUnit=T;
|
|
5
|
+
//# sourceMappingURL=index.cjs.map
|