@nebula-spatial/viewer 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +487 -0
- package/dist/types.d.ts +83 -0
- package/dist/viewer.d.ts +2 -0
- package/package.json +48 -0
package/README.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# @nebula-spatial/viewer
|
|
2
|
+
|
|
3
|
+
只读的 Three.js Viewer Facade。它接收已经由 Loader 构建好的
|
|
4
|
+
`THREE.Object3D`,负责场景挂载、相机视图、拾取和渲染生命周期,不解析文件,
|
|
5
|
+
也不包含编辑命令、历史或工具系统。
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { createViewer } from '@nebula-spatial/viewer';
|
|
9
|
+
|
|
10
|
+
const viewer = createViewer({
|
|
11
|
+
viewport: document.querySelector<HTMLElement>('#viewport')!,
|
|
12
|
+
canvas: document.querySelector<HTMLCanvasElement>('#viewport canvas')!,
|
|
13
|
+
// Set false when a format-specific selection store owns selection state.
|
|
14
|
+
autoSelectOnPick: false,
|
|
15
|
+
// Skip generic raycasting when the consumer resolves semantic picks itself.
|
|
16
|
+
pointerPick: false,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
viewer.addObject(geoNode);
|
|
20
|
+
viewer.fitView(geoNode);
|
|
21
|
+
viewer.setSelection(geoNode);
|
|
22
|
+
viewer.setHighlight(geoNode);
|
|
23
|
+
|
|
24
|
+
const stopPicking = viewer.on('pick', ({ result }) => {
|
|
25
|
+
console.log(result?.object ?? null);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const point = viewer.screenToWorldOnPlane(clientX, clientY, 0);
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
`selectedObject` 和 `highlightedObject` 是 Viewer 会话维护的只读状态。Viewer
|
|
32
|
+
会把画布上的短距离主键点击转换为 `pick` 事件和 selection 更新,也允许调用方
|
|
33
|
+
通过 `pick()` 完成自定义交互编排。`on()` 返回的函数用于注销监听。
|
|
34
|
+
`screenToWorldOnPlane()` 只负责通用的屏幕坐标到世界平面转换;CAD Block 等
|
|
35
|
+
格式语义仍由对应 Loader 或 Product App 解析。
|
|
36
|
+
自定义语义选择可以监听 `pointer-click`,并通过 `pointerPick: false` 跳过默认
|
|
37
|
+
Raycaster;默认配置保持自动 `pick` 和单对象 selection 行为。
|
|
38
|
+
|
|
39
|
+
Viewer 借用传入的 `Object3D`。`removeObject()` 和 `dispose()` 只解除场景挂载,
|
|
40
|
+
不会释放 Geometry、Material、Texture,也不会调用 Loader 对象的 `dispose()`。
|
|
41
|
+
调用方仍然负责 `GeoNode` 或其他 Loader 资源的生命周期。Viewer 自己创建的
|
|
42
|
+
`SceneRuntime`、Renderer、Camera、Controls 和事件监听由 `dispose()` 统一释放。
|
|
43
|
+
|
|
44
|
+
`three` 是唯一的运行时 peer dependency。私有的
|
|
45
|
+
`@nebula-spatial/scene-runtime` 只在构建时使用并内联到 Viewer bundle,不是
|
|
46
|
+
消费者需要安装的公共依赖。
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
var P = Object.defineProperty;
|
|
2
|
+
var C = (c, e, t) => e in c ? P(c, e, { enumerable: !0, configurable: !0, writable: !0, value: t }) : c[e] = t;
|
|
3
|
+
var a = (c, e, t) => C(c, typeof e != "symbol" ? e + "" : e, t);
|
|
4
|
+
import { Texture as x, Scene as j, Group as E, Raycaster as V, Vector2 as O, WebGLRenderer as F, Color as I, OrthographicCamera as T, PerspectiveCamera as L, MOUSE as M, TOUCH as R, Vector3 as v, Plane as S, Box3 as w, Object3D as z, BoxHelper as N } from "three";
|
|
5
|
+
import { MapControls as q } from "three/addons/controls/MapControls.js";
|
|
6
|
+
import { OrbitControls as D } from "three/addons/controls/OrbitControls.js";
|
|
7
|
+
const H = [
|
|
8
|
+
"alphaMap",
|
|
9
|
+
"aoMap",
|
|
10
|
+
"bumpMap",
|
|
11
|
+
"clearcoatMap",
|
|
12
|
+
"clearcoatNormalMap",
|
|
13
|
+
"clearcoatRoughnessMap",
|
|
14
|
+
"displacementMap",
|
|
15
|
+
"emissiveMap",
|
|
16
|
+
"envMap",
|
|
17
|
+
"gradientMap",
|
|
18
|
+
"iridescenceMap",
|
|
19
|
+
"iridescenceThicknessMap",
|
|
20
|
+
"lightMap",
|
|
21
|
+
"map",
|
|
22
|
+
"metalnessMap",
|
|
23
|
+
"normalMap",
|
|
24
|
+
"roughnessMap",
|
|
25
|
+
"sheenColorMap",
|
|
26
|
+
"sheenRoughnessMap",
|
|
27
|
+
"specularColorMap",
|
|
28
|
+
"specularIntensityMap",
|
|
29
|
+
"specularMap",
|
|
30
|
+
"thicknessMap",
|
|
31
|
+
"transmissionMap"
|
|
32
|
+
];
|
|
33
|
+
function g(c, e) {
|
|
34
|
+
e.has(c) || (e.add(c), c.dispose());
|
|
35
|
+
}
|
|
36
|
+
function _(c, e, t) {
|
|
37
|
+
if (e) {
|
|
38
|
+
const i = c;
|
|
39
|
+
for (const s of H) {
|
|
40
|
+
const r = i[s];
|
|
41
|
+
r instanceof x && g(r, t);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
g(c, t);
|
|
45
|
+
}
|
|
46
|
+
function A(c, e = {}) {
|
|
47
|
+
const t = e.disposedResources ?? /* @__PURE__ */ new WeakSet(), i = e.disposeTextures !== !1;
|
|
48
|
+
c.traverse((s) => {
|
|
49
|
+
const r = s;
|
|
50
|
+
r.geometry && g(r.geometry, t);
|
|
51
|
+
const n = Array.isArray(r.material) ? r.material : [r.material];
|
|
52
|
+
for (const o of n)
|
|
53
|
+
o && _(o, i, t);
|
|
54
|
+
r.renderTarget && g(r.renderTarget, t);
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
const Y = 1e3, W = -1e4, X = 1e4, y = 10, U = 2e6;
|
|
58
|
+
class B {
|
|
59
|
+
constructor(e) {
|
|
60
|
+
a(this, "canvasArea");
|
|
61
|
+
a(this, "canvas");
|
|
62
|
+
a(this, "scene", new j());
|
|
63
|
+
a(this, "overlayRoot", new E());
|
|
64
|
+
a(this, "renderer");
|
|
65
|
+
a(this, "camera2d");
|
|
66
|
+
a(this, "camera3d");
|
|
67
|
+
a(this, "activeCamera");
|
|
68
|
+
a(this, "controls2d");
|
|
69
|
+
a(this, "controls3d");
|
|
70
|
+
a(this, "options");
|
|
71
|
+
a(this, "mountedObjects", /* @__PURE__ */ new Set());
|
|
72
|
+
a(this, "ownedObjects", /* @__PURE__ */ new Set());
|
|
73
|
+
a(this, "overlays", /* @__PURE__ */ new Map());
|
|
74
|
+
a(this, "eventCleanups", []);
|
|
75
|
+
a(this, "raycaster", new V());
|
|
76
|
+
a(this, "pointerNdc", new O());
|
|
77
|
+
a(this, "viewModeValue");
|
|
78
|
+
a(this, "initialized", !1);
|
|
79
|
+
a(this, "disposed", !1);
|
|
80
|
+
a(this, "renderRequested", !0);
|
|
81
|
+
a(this, "animationFrameId", null);
|
|
82
|
+
a(this, "animationLoopActive", !1);
|
|
83
|
+
a(this, "lastFrameTime", null);
|
|
84
|
+
a(this, "infoFrames", 0);
|
|
85
|
+
a(this, "infoLastTime", 0);
|
|
86
|
+
this.options = e, this.canvasArea = e.canvasArea, this.canvas = e.canvas, this.viewModeValue = e.initialViewMode ?? "2d", this.overlayRoot.name = "scene-runtime:overlays", this.scene.add(this.overlayRoot);
|
|
87
|
+
}
|
|
88
|
+
initialize() {
|
|
89
|
+
if (this.disposed) throw new Error("SceneRuntime has been disposed.");
|
|
90
|
+
if (this.initialized) return;
|
|
91
|
+
const e = this.options.factories, t = (e == null ? void 0 : e.createRenderer) ?? ((h) => new F(h)), i = (e == null ? void 0 : e.createMapControls) ?? ((h, d) => new q(h, d)), s = (e == null ? void 0 : e.createOrbitControls) ?? ((h, d) => new D(h, d));
|
|
92
|
+
this.renderer = t({
|
|
93
|
+
canvas: this.canvas,
|
|
94
|
+
antialias: this.options.antialias ?? !0,
|
|
95
|
+
alpha: this.options.alpha ?? !1,
|
|
96
|
+
powerPreference: this.options.powerPreference ?? "high-performance"
|
|
97
|
+
}), this.options.clearColor !== void 0 && this.renderer.setClearColor(new I(this.options.clearColor));
|
|
98
|
+
const r = Math.max(1, this.canvasArea.clientWidth || 1), n = Math.max(1, this.canvasArea.clientHeight || 1), o = r / n;
|
|
99
|
+
this.renderer.setPixelRatio(this.readDevicePixelRatio()), this.renderer.setSize(r, n, !1), this.camera2d = new T(-o, o, 1, -1, W, X), this.camera2d.position.set(0, 0, 1), this.camera2d.lookAt(0, 0, 0), this.camera3d = new L(40, o, y, U), this.camera3d.up.set(0, 0, 1), this.camera3d.position.set(1e4, 0, 1e4), this.camera3d.lookAt(0, 0, 0), this.controls2d = i(this.camera2d, this.renderer.domElement), this.controls2d.enableRotate = !1, this.controls2d.screenSpacePanning = !0, this.controls2d.zoomToCursor = !0, this.controls2d.minZoom = 2e-3, this.controls2d.maxZoom = 5e4, this.controls2d.mouseButtons = {
|
|
100
|
+
LEFT: M.PAN,
|
|
101
|
+
MIDDLE: M.DOLLY,
|
|
102
|
+
RIGHT: M.PAN
|
|
103
|
+
}, this.controls2d.touches = {
|
|
104
|
+
ONE: R.PAN,
|
|
105
|
+
TWO: R.DOLLY_PAN
|
|
106
|
+
}, this.controls3d = s(this.camera3d, this.renderer.domElement), this.controls3d.enablePan = !1, this.listen(this.controls2d, "change", () => this.requestRender()), this.listen(this.controls3d, "change", () => this.requestRender()), this.initialized = !0, this.setViewMode(this.viewModeValue);
|
|
107
|
+
}
|
|
108
|
+
get isInitialized() {
|
|
109
|
+
return this.initialized;
|
|
110
|
+
}
|
|
111
|
+
get isDisposed() {
|
|
112
|
+
return this.disposed;
|
|
113
|
+
}
|
|
114
|
+
get viewMode() {
|
|
115
|
+
return this.viewModeValue;
|
|
116
|
+
}
|
|
117
|
+
get needsRender() {
|
|
118
|
+
return this.renderRequested;
|
|
119
|
+
}
|
|
120
|
+
set needsRender(e) {
|
|
121
|
+
this.renderRequested = e;
|
|
122
|
+
}
|
|
123
|
+
requestRender() {
|
|
124
|
+
this.disposed || (this.renderRequested = !0);
|
|
125
|
+
}
|
|
126
|
+
setClearColor(e) {
|
|
127
|
+
var t;
|
|
128
|
+
(t = this.renderer) == null || t.setClearColor(e), this.requestRender();
|
|
129
|
+
}
|
|
130
|
+
setViewMode(e) {
|
|
131
|
+
this.viewModeValue = e, !(!this.camera2d || !this.camera3d) && (this.activeCamera = e === "3d" ? this.camera3d : this.camera2d, this.controls2d && (this.controls2d.enabled = e === "2d"), this.controls3d && (this.controls3d.enabled = e === "3d"), this.requestRender());
|
|
132
|
+
}
|
|
133
|
+
setPerspectivePose(e, t) {
|
|
134
|
+
var i;
|
|
135
|
+
return !this.camera3d || ![e.x, e.y, e.z, t.x, t.y, t.z].every(Number.isFinite) ? !1 : (this.camera3d.position.copy(e), this.camera3d.lookAt(t), this.camera3d.updateProjectionMatrix(), (i = this.controls3d) == null || i.target.copy(t), this.requestRender(), !0);
|
|
136
|
+
}
|
|
137
|
+
setPerspectiveAspect(e) {
|
|
138
|
+
return !this.camera3d || !Number.isFinite(e) || e <= 0 ? !1 : (this.camera3d.aspect = e, this.camera3d.updateProjectionMatrix(), this.requestRender(), !0);
|
|
139
|
+
}
|
|
140
|
+
setOrthographicViewport(e) {
|
|
141
|
+
var t;
|
|
142
|
+
return !this.camera2d || ![
|
|
143
|
+
e.left,
|
|
144
|
+
e.right,
|
|
145
|
+
e.top,
|
|
146
|
+
e.bottom,
|
|
147
|
+
e.zoom,
|
|
148
|
+
e.positionX,
|
|
149
|
+
e.positionY,
|
|
150
|
+
e.targetX,
|
|
151
|
+
e.targetY
|
|
152
|
+
].every(Number.isFinite) || e.left >= e.right || e.bottom >= e.top || e.zoom <= 0 ? !1 : (this.camera2d.left = e.left, this.camera2d.right = e.right, this.camera2d.top = e.top, this.camera2d.bottom = e.bottom, this.camera2d.zoom = e.zoom, this.camera2d.position.set(e.positionX, e.positionY, 1), this.camera2d.updateProjectionMatrix(), (t = this.controls2d) != null && t.target && (this.controls2d.target.set(e.targetX, e.targetY, 0), this.controls2d.update()), this.requestRender(), !0);
|
|
153
|
+
}
|
|
154
|
+
addObject(e, t = {}) {
|
|
155
|
+
if (this.disposed) throw new Error("SceneRuntime has been disposed.");
|
|
156
|
+
this.mountedObjects.has(e) && e.parent === this.scene || (this.scene.add(e), this.mountedObjects.add(e), t.owned && this.ownedObjects.add(e), this.requestRender());
|
|
157
|
+
}
|
|
158
|
+
removeObject(e, t = {}) {
|
|
159
|
+
if (!this.mountedObjects.has(e) && e.parent !== this.scene) return !1;
|
|
160
|
+
e.removeFromParent(), this.mountedObjects.delete(e);
|
|
161
|
+
const i = this.ownedObjects.delete(e);
|
|
162
|
+
return (t.dispose === !0 || t.dispose === void 0 && i) && A(e), this.requestRender(), !0;
|
|
163
|
+
}
|
|
164
|
+
setObjectVisible(e, t) {
|
|
165
|
+
e.visible !== t && (e.visible = t, this.requestRender());
|
|
166
|
+
}
|
|
167
|
+
mountOverlay(e, t, i = {}) {
|
|
168
|
+
this.removeOverlay(e), this.overlayRoot.parent !== this.scene && this.scene.add(this.overlayRoot), this.overlayRoot.add(t), this.overlays.set(e, { object: t, owned: i.owned === !0 }), this.requestRender();
|
|
169
|
+
}
|
|
170
|
+
removeOverlay(e, t = {}) {
|
|
171
|
+
const i = this.overlays.get(e);
|
|
172
|
+
return i ? (this.overlays.delete(e), i.object.removeFromParent(), (t.dispose === !0 || t.dispose === void 0 && i.owned) && A(i.object), this.requestRender(), i.object) : null;
|
|
173
|
+
}
|
|
174
|
+
setHighlight(e, t = {}) {
|
|
175
|
+
if (!e) {
|
|
176
|
+
this.removeOverlay("highlight");
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
this.mountOverlay("highlight", e, t);
|
|
180
|
+
}
|
|
181
|
+
resize() {
|
|
182
|
+
if (!this.renderer) return !1;
|
|
183
|
+
const e = this.canvasArea.clientWidth, t = this.canvasArea.clientHeight;
|
|
184
|
+
if (!Number.isFinite(e) || !Number.isFinite(t) || e <= 0 || t <= 0) return !1;
|
|
185
|
+
if (this.renderer.setPixelRatio(this.readDevicePixelRatio()), this.renderer.setSize(e, t, !1), this.camera2d) {
|
|
186
|
+
const i = (this.camera2d.top - this.camera2d.bottom) / 2, s = i * (e / t), r = (this.camera2d.left + this.camera2d.right) / 2, n = (this.camera2d.top + this.camera2d.bottom) / 2;
|
|
187
|
+
this.camera2d.left = r - s, this.camera2d.right = r + s, this.camera2d.top = n + i, this.camera2d.bottom = n - i, this.camera2d.updateProjectionMatrix();
|
|
188
|
+
}
|
|
189
|
+
return this.camera3d && (this.camera3d.aspect = e / t, this.camera3d.updateProjectionMatrix()), this.requestRender(), !0;
|
|
190
|
+
}
|
|
191
|
+
fitView(e, t = {}) {
|
|
192
|
+
if (!this.camera2d || !this.camera3d) return !1;
|
|
193
|
+
const i = this.resolveBox3(e);
|
|
194
|
+
if (!i || i.isEmpty()) return !1;
|
|
195
|
+
const s = i.getSize(new v()), r = i.getCenter(new v());
|
|
196
|
+
if (![s.x, s.y, s.z, r.x, r.y, r.z].every(Number.isFinite)) return !1;
|
|
197
|
+
const n = Math.max(1, t.padding ?? 1.05);
|
|
198
|
+
if ((t.viewMode ?? this.viewModeValue) === "2d") {
|
|
199
|
+
const h = Math.max(s.x, 1e-6) * n, d = Math.max(s.y, 1e-6) * n, l = Math.max(1, this.canvasArea.clientWidth), f = Math.max(1, this.canvasArea.clientHeight), p = l / f;
|
|
200
|
+
let u = h / 2, m = d / 2;
|
|
201
|
+
u / m < p ? u = m * p : m = u / p, this.camera2d.left = -u, this.camera2d.right = u, this.camera2d.top = m, this.camera2d.bottom = -m, this.camera2d.zoom = 1, this.camera2d.position.set(r.x, r.y, Math.max(r.z + 1, 1)), this.camera2d.updateProjectionMatrix(), this.controls2d.target.copy(r), this.controls2d.update();
|
|
202
|
+
} else {
|
|
203
|
+
const h = r.clone(), d = this.camera3d.fov * Math.PI / 180, l = 2 * Math.atan(Math.tan(d / 2) * Math.max(this.camera3d.aspect, 1e-6)), f = Math.max(s.length() / 2, 1e-6), p = Math.max(Math.min(d, l) / 2, 1e-6), u = f / Math.sin(p) * n, m = this.camera3d.position.clone().sub(this.controls3d.target);
|
|
204
|
+
m.lengthSq() <= 1e-12 && m.set(1, -1, 1), m.normalize(), this.controls3d.target.copy(h), this.camera3d.position.copy(h).addScaledVector(m, u), this.camera3d.lookAt(h), this.camera3d.near = Math.min(
|
|
205
|
+
y,
|
|
206
|
+
Math.max(u * 0.01, 1e-6)
|
|
207
|
+
), this.camera3d.updateProjectionMatrix(), this.controls3d.update();
|
|
208
|
+
}
|
|
209
|
+
return this.requestRender(), !0;
|
|
210
|
+
}
|
|
211
|
+
clientToNdc(e, t, i = new O()) {
|
|
212
|
+
var n;
|
|
213
|
+
const s = (n = this.renderer) == null ? void 0 : n.domElement;
|
|
214
|
+
if (!s) return null;
|
|
215
|
+
const r = s.getBoundingClientRect();
|
|
216
|
+
return !Number.isFinite(r.width) || !Number.isFinite(r.height) || r.width <= 0 || r.height <= 0 ? null : (i.set(
|
|
217
|
+
(e - r.left) / r.width * 2 - 1,
|
|
218
|
+
-((t - r.top) / r.height) * 2 + 1
|
|
219
|
+
), i);
|
|
220
|
+
}
|
|
221
|
+
screenToWorldOnPlane(e, t, i = 0) {
|
|
222
|
+
const s = this.clientToNdc(e, t, this.pointerNdc);
|
|
223
|
+
return !s || !this.activeCamera ? null : (this.raycaster.setFromCamera(s, this.activeCamera), this.raycaster.ray.intersectPlane(new S(new v(0, 0, 1), -i), new v()));
|
|
224
|
+
}
|
|
225
|
+
worldToClient(e, t = new O()) {
|
|
226
|
+
var n;
|
|
227
|
+
const i = (n = this.renderer) == null ? void 0 : n.domElement;
|
|
228
|
+
if (!i || !this.activeCamera) return null;
|
|
229
|
+
const s = i.getBoundingClientRect();
|
|
230
|
+
if (!Number.isFinite(s.width) || !Number.isFinite(s.height) || s.width <= 0 || s.height <= 0) return null;
|
|
231
|
+
const r = e.clone().project(this.activeCamera);
|
|
232
|
+
return t.set(
|
|
233
|
+
s.left + (r.x + 1) / 2 * s.width,
|
|
234
|
+
s.top + (1 - r.y) / 2 * s.height
|
|
235
|
+
), t;
|
|
236
|
+
}
|
|
237
|
+
pick(e, t, i = {}) {
|
|
238
|
+
const s = this.clientToNdc(e, t, this.pointerNdc);
|
|
239
|
+
if (!s || !this.activeCamera) return null;
|
|
240
|
+
const r = i.roots ?? [...this.mountedObjects], n = i.includeInvisible ? r : r.filter((l) => l.visible);
|
|
241
|
+
this.raycaster.setFromCamera(s, this.activeCamera);
|
|
242
|
+
const [o] = this.raycaster.intersectObjects([...n], i.recursive !== !1);
|
|
243
|
+
if (!o) return null;
|
|
244
|
+
const d = n.find((l) => l === o.object || this.isDescendantOf(o.object, l)) ?? o.object;
|
|
245
|
+
return {
|
|
246
|
+
object: o.object,
|
|
247
|
+
root: d,
|
|
248
|
+
point: o.point.clone(),
|
|
249
|
+
distance: o.distance,
|
|
250
|
+
faceIndex: o.faceIndex ?? null,
|
|
251
|
+
instanceId: o.instanceId,
|
|
252
|
+
intersection: o
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
startAnimation(e = {}) {
|
|
256
|
+
if (this.disposed || this.animationLoopActive) return;
|
|
257
|
+
this.animationLoopActive = !0, this.infoFrames = 0, this.infoLastTime = performance.now();
|
|
258
|
+
const t = e.infoUpdateIntervalMs ?? Y, i = (s = performance.now()) => {
|
|
259
|
+
var o, h, d, l, f, p, u;
|
|
260
|
+
if (!this.animationLoopActive || this.disposed) {
|
|
261
|
+
this.animationFrameId = null;
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
this.animationFrameId = requestAnimationFrame(i);
|
|
265
|
+
const r = this.lastFrameTime === null ? 0 : Math.max(0, (s - this.lastFrameTime) / 1e3);
|
|
266
|
+
this.lastFrameTime = s, this.viewModeValue === "2d" && ((o = this.controls2d) == null || o.update()), (h = e.onFrame) == null || h.call(e, s, r);
|
|
267
|
+
let n = !1;
|
|
268
|
+
this.renderRequested && (((d = e.shouldRender) == null ? void 0 : d.call(e, s)) ?? !0) && ((l = e.beforeRender) == null || l.call(e, s), this.renderer.render(this.scene, this.activeCamera), this.renderRequested = !1, n = !0, (f = e.afterRender) == null || f.call(e, s)), this.infoFrames += 1, s - this.infoLastTime >= t ? (this.infoFrames = 0, this.infoLastTime = s, (p = e.onInfoUpdate) == null || p.call(e)) : n && ((u = e.onInfoUpdate) == null || u.call(e));
|
|
269
|
+
};
|
|
270
|
+
i();
|
|
271
|
+
}
|
|
272
|
+
stopAnimation() {
|
|
273
|
+
this.animationLoopActive = !1, this.animationFrameId !== null && typeof cancelAnimationFrame == "function" && cancelAnimationFrame(this.animationFrameId), this.animationFrameId = null, this.lastFrameTime = null;
|
|
274
|
+
}
|
|
275
|
+
dispose() {
|
|
276
|
+
var e, t, i, s, r, n, o;
|
|
277
|
+
if (!this.disposed) {
|
|
278
|
+
for (this.disposed = !0, this.stopAnimation(); this.eventCleanups.length > 0; ) (e = this.eventCleanups.pop()) == null || e();
|
|
279
|
+
for (const h of [...this.overlays.keys()]) this.removeOverlay(h);
|
|
280
|
+
for (const h of [...this.mountedObjects]) this.removeObject(h);
|
|
281
|
+
(i = (t = this.controls2d) == null ? void 0 : t.dispose) == null || i.call(t), (r = (s = this.controls3d) == null ? void 0 : s.dispose) == null || r.call(s), (o = (n = this.renderer) == null ? void 0 : n.dispose) == null || o.call(n);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
readDevicePixelRatio() {
|
|
285
|
+
const e = globalThis.devicePixelRatio;
|
|
286
|
+
return typeof e == "number" && Number.isFinite(e) && e > 0 ? e : 1;
|
|
287
|
+
}
|
|
288
|
+
resolveBox3(e) {
|
|
289
|
+
if (e instanceof w) return e.clone();
|
|
290
|
+
if (e instanceof z) return new w().setFromObject(e);
|
|
291
|
+
const { minX: t, minY: i, maxX: s, maxY: r } = e;
|
|
292
|
+
return ![t, i, s, r].every(Number.isFinite) || s < t || r < i ? null : new w(new v(t, i, 0), new v(s, r, 0));
|
|
293
|
+
}
|
|
294
|
+
isDescendantOf(e, t) {
|
|
295
|
+
let i = e.parent;
|
|
296
|
+
for (; i; ) {
|
|
297
|
+
if (i === t) return !0;
|
|
298
|
+
i = i.parent;
|
|
299
|
+
}
|
|
300
|
+
return !1;
|
|
301
|
+
}
|
|
302
|
+
listen(e, t, i) {
|
|
303
|
+
e.addEventListener(t, i), this.eventCleanups.push(() => {
|
|
304
|
+
var s;
|
|
305
|
+
return (s = e.removeEventListener) == null ? void 0 : s.call(e, t, i);
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
const b = "viewer:highlight", K = 6;
|
|
310
|
+
class G {
|
|
311
|
+
constructor(e, t) {
|
|
312
|
+
a(this, "mountedObjects", /* @__PURE__ */ new Set());
|
|
313
|
+
a(this, "listeners", /* @__PURE__ */ new Map());
|
|
314
|
+
a(this, "selectedObjectValue", null);
|
|
315
|
+
a(this, "highlightedObjectValue", null);
|
|
316
|
+
a(this, "pointerStart", null);
|
|
317
|
+
a(this, "disposed", !1);
|
|
318
|
+
a(this, "handlePointerDown", (e) => {
|
|
319
|
+
this.disposed || (this.pointerStart = {
|
|
320
|
+
pointerId: e.pointerId,
|
|
321
|
+
clientX: e.clientX,
|
|
322
|
+
clientY: e.clientY,
|
|
323
|
+
button: e.button
|
|
324
|
+
});
|
|
325
|
+
});
|
|
326
|
+
a(this, "handlePointerUp", (e) => {
|
|
327
|
+
const t = this.pointerStart;
|
|
328
|
+
if (this.pointerStart = null, this.disposed || !t || t.pointerId !== e.pointerId || t.button !== 0 || e.button !== 0 || Math.hypot(e.clientX - t.clientX, e.clientY - t.clientY) > K)
|
|
329
|
+
return;
|
|
330
|
+
const i = {
|
|
331
|
+
clientX: e.clientX,
|
|
332
|
+
clientY: e.clientY,
|
|
333
|
+
shiftKey: e.shiftKey,
|
|
334
|
+
ctrlKey: e.ctrlKey,
|
|
335
|
+
metaKey: e.metaKey
|
|
336
|
+
};
|
|
337
|
+
if (this.emit("pointer-click", i), this.options.pointerPick === !1) return;
|
|
338
|
+
const s = this.pick(e.clientX, e.clientY);
|
|
339
|
+
this.emit("pick", {
|
|
340
|
+
...i,
|
|
341
|
+
result: s
|
|
342
|
+
}), this.options.autoSelectOnPick !== !1 && this.setSelection((s == null ? void 0 : s.object) ?? null);
|
|
343
|
+
});
|
|
344
|
+
a(this, "handlePointerCancel", () => {
|
|
345
|
+
this.pointerStart = null;
|
|
346
|
+
});
|
|
347
|
+
var i, s, r, n, o, h;
|
|
348
|
+
this.options = e, this.runtime = t, (s = (i = this.options.canvas).addEventListener) == null || s.call(i, "pointerdown", this.handlePointerDown), (n = (r = this.options.canvas).addEventListener) == null || n.call(r, "pointerup", this.handlePointerUp), (h = (o = this.options.canvas).addEventListener) == null || h.call(o, "pointercancel", this.handlePointerCancel);
|
|
349
|
+
}
|
|
350
|
+
get viewport() {
|
|
351
|
+
return this.options.viewport;
|
|
352
|
+
}
|
|
353
|
+
get canvas() {
|
|
354
|
+
return this.options.canvas;
|
|
355
|
+
}
|
|
356
|
+
get viewMode() {
|
|
357
|
+
return this.runtime.viewMode;
|
|
358
|
+
}
|
|
359
|
+
get selectedObject() {
|
|
360
|
+
return this.selectedObjectValue;
|
|
361
|
+
}
|
|
362
|
+
get highlightedObject() {
|
|
363
|
+
return this.highlightedObjectValue;
|
|
364
|
+
}
|
|
365
|
+
get isDisposed() {
|
|
366
|
+
return this.disposed;
|
|
367
|
+
}
|
|
368
|
+
addObject(e) {
|
|
369
|
+
this.assertActive(), !this.mountedObjects.has(e) && (this.runtime.addObject(e), this.mountedObjects.add(e));
|
|
370
|
+
}
|
|
371
|
+
removeObject(e) {
|
|
372
|
+
if (this.assertActive(), !this.mountedObjects.has(e)) return !1;
|
|
373
|
+
this.selectedObjectValue && this.isObjectWithinRoot(this.selectedObjectValue, e) && this.setSelection(null), this.highlightedObjectValue && this.isObjectWithinRoot(this.highlightedObjectValue, e) && this.setHighlight(null);
|
|
374
|
+
const t = this.runtime.removeObject(e, { dispose: !1 });
|
|
375
|
+
return t && this.mountedObjects.delete(e), t;
|
|
376
|
+
}
|
|
377
|
+
setVisible(e, t) {
|
|
378
|
+
this.assertActive(), this.assertMounted(e), this.runtime.setObjectVisible(e, t);
|
|
379
|
+
}
|
|
380
|
+
setViewMode(e) {
|
|
381
|
+
this.assertActive(), e !== this.runtime.viewMode && (this.runtime.setViewMode(e), this.emit("view-mode-change", { viewMode: e }));
|
|
382
|
+
}
|
|
383
|
+
fitView(e, t) {
|
|
384
|
+
this.assertActive(), e && !(e instanceof w) && this.assertMounted(e);
|
|
385
|
+
const i = e ?? this.resolveMountedBounds();
|
|
386
|
+
return i ? this.runtime.fitView(i, t) : !1;
|
|
387
|
+
}
|
|
388
|
+
screenToWorldOnPlane(e, t, i = 0) {
|
|
389
|
+
var s;
|
|
390
|
+
return this.assertActive(), ((s = this.runtime.screenToWorldOnPlane(e, t, i)) == null ? void 0 : s.clone()) ?? null;
|
|
391
|
+
}
|
|
392
|
+
pick(e, t, i) {
|
|
393
|
+
var r;
|
|
394
|
+
this.assertActive(), (r = i == null ? void 0 : i.roots) == null || r.forEach((n) => this.assertMounted(n));
|
|
395
|
+
const s = this.runtime.pick(e, t, i);
|
|
396
|
+
return s ? {
|
|
397
|
+
object: s.object,
|
|
398
|
+
root: s.root,
|
|
399
|
+
point: s.point.clone(),
|
|
400
|
+
distance: s.distance,
|
|
401
|
+
faceIndex: s.faceIndex,
|
|
402
|
+
...s.instanceId === void 0 ? {} : { instanceId: s.instanceId }
|
|
403
|
+
} : null;
|
|
404
|
+
}
|
|
405
|
+
setSelection(e) {
|
|
406
|
+
this.assertActive(), e && this.assertMounted(e), e !== this.selectedObjectValue && (this.selectedObjectValue = e, this.runtime.requestRender(), this.emit("selection-change", { object: e }));
|
|
407
|
+
}
|
|
408
|
+
setHighlight(e, t = {}) {
|
|
409
|
+
if (this.assertActive(), e && this.assertMounted(e), !(this.highlightedObjectValue === null && e === null)) {
|
|
410
|
+
if (this.runtime.removeOverlay(b), this.highlightedObjectValue = e, e) {
|
|
411
|
+
const i = new N(e, t.color ?? 6000111);
|
|
412
|
+
i.name = b, this.runtime.mountOverlay(b, i, { owned: !0 });
|
|
413
|
+
}
|
|
414
|
+
this.emit("highlight-change", { object: e });
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
on(e, t) {
|
|
418
|
+
this.assertActive();
|
|
419
|
+
const i = t, s = this.listeners.get(e) ?? /* @__PURE__ */ new Set();
|
|
420
|
+
s.add(i), this.listeners.set(e, s);
|
|
421
|
+
let r = !0;
|
|
422
|
+
return () => {
|
|
423
|
+
r && (r = !1, s.delete(i), s.size === 0 && this.listeners.delete(e));
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
resize() {
|
|
427
|
+
this.assertActive();
|
|
428
|
+
const e = this.runtime.resize();
|
|
429
|
+
return this.emit("resize", { resized: e }), e;
|
|
430
|
+
}
|
|
431
|
+
requestRender() {
|
|
432
|
+
this.assertActive(), this.runtime.requestRender();
|
|
433
|
+
}
|
|
434
|
+
dispose() {
|
|
435
|
+
var e, t, i, s, r, n;
|
|
436
|
+
if (!this.disposed) {
|
|
437
|
+
this.disposed = !0, (t = (e = this.options.canvas).removeEventListener) == null || t.call(e, "pointerdown", this.handlePointerDown), (s = (i = this.options.canvas).removeEventListener) == null || s.call(i, "pointerup", this.handlePointerUp), (n = (r = this.options.canvas).removeEventListener) == null || n.call(r, "pointercancel", this.handlePointerCancel), this.pointerStart = null, this.highlightedObjectValue && this.runtime.removeOverlay(b), this.highlightedObjectValue = null, this.selectedObjectValue = null;
|
|
438
|
+
for (const o of this.mountedObjects)
|
|
439
|
+
this.runtime.removeObject(o, { dispose: !1 });
|
|
440
|
+
this.mountedObjects.clear(), this.listeners.clear(), this.runtime.dispose();
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
resolveMountedBounds() {
|
|
444
|
+
const e = new w();
|
|
445
|
+
let t = !1;
|
|
446
|
+
for (const i of this.mountedObjects)
|
|
447
|
+
i.visible && (e.expandByObject(i), t = !0);
|
|
448
|
+
return t && !e.isEmpty() ? e : null;
|
|
449
|
+
}
|
|
450
|
+
assertActive() {
|
|
451
|
+
if (this.disposed) throw new Error("Viewer has been disposed.");
|
|
452
|
+
}
|
|
453
|
+
assertMounted(e) {
|
|
454
|
+
for (const t of this.mountedObjects)
|
|
455
|
+
if (this.isObjectWithinRoot(e, t)) return;
|
|
456
|
+
throw new Error("Viewer can only modify mounted objects.");
|
|
457
|
+
}
|
|
458
|
+
isObjectWithinRoot(e, t) {
|
|
459
|
+
let i = e;
|
|
460
|
+
for (; i; ) {
|
|
461
|
+
if (i === t) return !0;
|
|
462
|
+
i = i.parent;
|
|
463
|
+
}
|
|
464
|
+
return !1;
|
|
465
|
+
}
|
|
466
|
+
emit(e, t) {
|
|
467
|
+
for (const i of this.listeners.get(e) ?? []) i(t);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
function ee(c) {
|
|
471
|
+
const e = new B({
|
|
472
|
+
canvasArea: c.viewport,
|
|
473
|
+
canvas: c.canvas,
|
|
474
|
+
clearColor: c.clearColor,
|
|
475
|
+
antialias: c.antialias,
|
|
476
|
+
alpha: c.alpha,
|
|
477
|
+
powerPreference: c.powerPreference,
|
|
478
|
+
initialViewMode: c.initialViewMode
|
|
479
|
+
});
|
|
480
|
+
return Z(c, e);
|
|
481
|
+
}
|
|
482
|
+
function Z(c, e) {
|
|
483
|
+
return e.initialize(), e.startAnimation(), new G(c, e);
|
|
484
|
+
}
|
|
485
|
+
export {
|
|
486
|
+
ee as createViewer
|
|
487
|
+
};
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { Box3, ColorRepresentation, Object3D, Vector3 } from 'three';
|
|
2
|
+
export type ViewerViewMode = '2d' | '3d';
|
|
3
|
+
export interface ViewerOptions {
|
|
4
|
+
viewport: HTMLElement;
|
|
5
|
+
canvas: HTMLCanvasElement;
|
|
6
|
+
clearColor?: ColorRepresentation;
|
|
7
|
+
antialias?: boolean;
|
|
8
|
+
alpha?: boolean;
|
|
9
|
+
powerPreference?: WebGLPowerPreference;
|
|
10
|
+
initialViewMode?: ViewerViewMode;
|
|
11
|
+
autoSelectOnPick?: boolean;
|
|
12
|
+
pointerPick?: boolean;
|
|
13
|
+
}
|
|
14
|
+
export interface ViewerFitViewOptions {
|
|
15
|
+
padding?: number;
|
|
16
|
+
viewMode?: ViewerViewMode;
|
|
17
|
+
}
|
|
18
|
+
export interface ViewerPickOptions {
|
|
19
|
+
roots?: readonly Object3D[];
|
|
20
|
+
recursive?: boolean;
|
|
21
|
+
includeInvisible?: boolean;
|
|
22
|
+
}
|
|
23
|
+
export interface ViewerPickResult {
|
|
24
|
+
object: Object3D;
|
|
25
|
+
root: Object3D;
|
|
26
|
+
point: Vector3;
|
|
27
|
+
distance: number;
|
|
28
|
+
faceIndex: number | null;
|
|
29
|
+
instanceId?: number;
|
|
30
|
+
}
|
|
31
|
+
export interface ViewerHighlightOptions {
|
|
32
|
+
color?: ColorRepresentation;
|
|
33
|
+
}
|
|
34
|
+
export interface ViewerEventMap {
|
|
35
|
+
'pointer-click': {
|
|
36
|
+
clientX: number;
|
|
37
|
+
clientY: number;
|
|
38
|
+
shiftKey: boolean;
|
|
39
|
+
ctrlKey: boolean;
|
|
40
|
+
metaKey: boolean;
|
|
41
|
+
};
|
|
42
|
+
pick: {
|
|
43
|
+
clientX: number;
|
|
44
|
+
clientY: number;
|
|
45
|
+
shiftKey: boolean;
|
|
46
|
+
ctrlKey: boolean;
|
|
47
|
+
metaKey: boolean;
|
|
48
|
+
result: ViewerPickResult | null;
|
|
49
|
+
};
|
|
50
|
+
'selection-change': {
|
|
51
|
+
object: Object3D | null;
|
|
52
|
+
};
|
|
53
|
+
'highlight-change': {
|
|
54
|
+
object: Object3D | null;
|
|
55
|
+
};
|
|
56
|
+
'view-mode-change': {
|
|
57
|
+
viewMode: ViewerViewMode;
|
|
58
|
+
};
|
|
59
|
+
resize: {
|
|
60
|
+
resized: boolean;
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
export interface Viewer {
|
|
64
|
+
readonly viewport: HTMLElement;
|
|
65
|
+
readonly canvas: HTMLCanvasElement;
|
|
66
|
+
readonly viewMode: ViewerViewMode;
|
|
67
|
+
readonly selectedObject: Object3D | null;
|
|
68
|
+
readonly highlightedObject: Object3D | null;
|
|
69
|
+
readonly isDisposed: boolean;
|
|
70
|
+
addObject(object: Object3D): void;
|
|
71
|
+
removeObject(object: Object3D): boolean;
|
|
72
|
+
setVisible(object: Object3D, visible: boolean): void;
|
|
73
|
+
setViewMode(mode: ViewerViewMode): void;
|
|
74
|
+
fitView(target?: Object3D | Box3, options?: ViewerFitViewOptions): boolean;
|
|
75
|
+
screenToWorldOnPlane(clientX: number, clientY: number, planeZ?: number): Vector3 | null;
|
|
76
|
+
pick(clientX: number, clientY: number, options?: ViewerPickOptions): ViewerPickResult | null;
|
|
77
|
+
setSelection(object: Object3D | null): void;
|
|
78
|
+
setHighlight(object: Object3D | null, options?: ViewerHighlightOptions): void;
|
|
79
|
+
on<K extends keyof ViewerEventMap>(type: K, listener: (event: ViewerEventMap[K]) => void): () => void;
|
|
80
|
+
resize(): boolean;
|
|
81
|
+
requestRender(): void;
|
|
82
|
+
dispose(): void;
|
|
83
|
+
}
|
package/dist/viewer.d.ts
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nebula-spatial/viewer",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Read-only Three.js viewer facade",
|
|
5
|
+
"license": "ISC",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=18"
|
|
8
|
+
},
|
|
9
|
+
"type": "module",
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"README.md",
|
|
13
|
+
"package.json"
|
|
14
|
+
],
|
|
15
|
+
"main": "./dist/index.js",
|
|
16
|
+
"module": "./dist/index.js",
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"import": "./dist/index.js",
|
|
22
|
+
"default": "./dist/index.js"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"sideEffects": false,
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "npm run check:boundary && npm run typecheck && npm run build:bundle && npm run build:types && npm run check:dist",
|
|
28
|
+
"build:bundle": "vite build --config vite.config.ts",
|
|
29
|
+
"build:types": "tsc --project tsconfig.build.json",
|
|
30
|
+
"check:boundary": "node scripts/check-boundary.mjs",
|
|
31
|
+
"check:dist": "node scripts/check-dist.mjs",
|
|
32
|
+
"typecheck": "tsc --project tsconfig.json --noEmit",
|
|
33
|
+
"test": "vitest run --config vitest.config.ts",
|
|
34
|
+
"pack:dry": "npm pack --dry-run",
|
|
35
|
+
"prepack": "npm run build"
|
|
36
|
+
},
|
|
37
|
+
"peerDependencies": {
|
|
38
|
+
"three": "0.182.0"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@types/node": "^25.6.0",
|
|
42
|
+
"@types/three": "0.182.0",
|
|
43
|
+
"three": "0.182.0",
|
|
44
|
+
"typescript": "^5.6.3",
|
|
45
|
+
"vite": "^5.4.10",
|
|
46
|
+
"vitest": "^2.1.8"
|
|
47
|
+
}
|
|
48
|
+
}
|