@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 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
+ 消费者需要安装的公共依赖。
@@ -0,0 +1,2 @@
1
+ export { createViewer } from './viewer';
2
+ export type { Viewer, ViewerEventMap, ViewerFitViewOptions, ViewerHighlightOptions, ViewerOptions, ViewerPickOptions, ViewerPickResult, ViewerViewMode, } from './types';
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
+ };
@@ -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
+ }
@@ -0,0 +1,2 @@
1
+ import type { Viewer, ViewerOptions } from './types';
2
+ export declare function createViewer(options: ViewerOptions): Viewer;
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
+ }