@thethingteam/colmap-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,117 @@
1
+ # colmap-viewer
2
+
3
+ Browser-based 3D viewer for [COLMAP](https://colmap.github.io/) reconstruction results, built on Three.js.
4
+
5
+ Visualizes point clouds, camera frustums, and coordinate axes from COLMAP binary output files. Works as a drop-in Web Component or as a programmatic TypeScript/JavaScript API.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install @thethingteam/colmap-viewer
11
+ ```
12
+
13
+ **Peer dependencies** — install these alongside the package:
14
+
15
+ ```bash
16
+ npm install three @thethingteam/colmap-wasm
17
+ ```
18
+
19
+ ## Quick Start — Web Component
20
+
21
+ The easiest way to embed the viewer. Point it at your COLMAP binary files and it renders into the element.
22
+
23
+ ```html
24
+ <!-- index.html -->
25
+ <script type="module">
26
+ import '@thethingteam/colmap-viewer'
27
+ </script>
28
+
29
+ <colmap-viewer
30
+ style="width: 100%; height: 600px;"
31
+ cameras-url="/data/cameras.bin"
32
+ images-url="/data/images.bin"
33
+ points3d-url="/data/points3D.bin"
34
+ ></colmap-viewer>
35
+ ```
36
+
37
+ The element registers itself as `<colmap-viewer>` on import. The `points3d-url` attribute is optional — omit it if you only want to visualize cameras.
38
+
39
+ ## Programmatic API — `ColmapRenderer`
40
+
41
+ Use `ColmapRenderer` directly when you need lifecycle control or want to load from pre-fetched buffers.
42
+
43
+ **Load from URLs:**
44
+
45
+ ```ts
46
+ import { ColmapRenderer } from '@thethingteam/colmap-viewer'
47
+
48
+ const container = document.getElementById('viewer')!
49
+
50
+ const renderer = new ColmapRenderer(container, {
51
+ showThemeToggle: true,
52
+ showAxisLegend: true,
53
+ showControlsHint: true,
54
+ })
55
+
56
+ await renderer.loadFromUrls({
57
+ cameras: '/data/cameras.bin',
58
+ images: '/data/images.bin',
59
+ points3d: '/data/points3D.bin', // optional
60
+ })
61
+
62
+ // Clean up when done
63
+ renderer.dispose()
64
+ ```
65
+
66
+ **Load from `ArrayBuffer`:**
67
+
68
+ ```ts
69
+ import { ColmapRenderer } from '@thethingteam/colmap-viewer'
70
+
71
+ const renderer = new ColmapRenderer(container)
72
+
73
+ await renderer.loadFromBuffers({
74
+ cameras: camerasBuffer, // ArrayBuffer
75
+ images: imagesBuffer, // ArrayBuffer
76
+ points3d: points3dBuffer, // ArrayBuffer, optional
77
+ })
78
+ ```
79
+
80
+ ## Options Reference
81
+
82
+ | Attribute (Web Component) | Option (JS) | Type | Default | Description |
83
+ |---|---|---|---|---|
84
+ | `cameras-url` | — | `string` | — | URL of `cameras.bin` (required) |
85
+ | `images-url` | — | `string` | — | URL of `images.bin` (required) |
86
+ | `points3d-url` | — | `string` | — | URL of `points3D.bin` (optional) |
87
+ | `show-theme-toggle` | `showThemeToggle` | `boolean` | `true` | Show light/dark mode toggle in the HUD |
88
+ | `show-axis-legend` | `showAxisLegend` | `boolean` | `true` | Show XYZ axis legend overlay |
89
+ | `show-controls-hint` | `showControlsHint` | `boolean` | `true` | Show mouse controls hint at the bottom |
90
+
91
+ **Boolean attributes** on the Web Component follow standard HTML conventions — presence means `true`, set to `"false"` to disable:
92
+
93
+ ```html
94
+ <colmap-viewer
95
+ cameras-url="/data/cameras.bin"
96
+ images-url="/data/images.bin"
97
+ show-theme-toggle="false"
98
+ show-controls-hint="false"
99
+ ></colmap-viewer>
100
+ ```
101
+
102
+ ## Lower-level Exports
103
+
104
+ For advanced use cases, the library also exports its internal building blocks individually:
105
+
106
+ ```ts
107
+ import { parse, buildScene, createHud } from '@thethingteam/colmap-viewer'
108
+ import type { ColmapFiles, HudOptions, LayerKey } from '@thethingteam/colmap-viewer'
109
+ ```
110
+
111
+ - **`parse(files)`** — parses raw COLMAP binary buffers into structured data
112
+ - **`buildScene(container, result, imageFiles, onProgress)`** — constructs the Three.js scene
113
+ - **`createHud(container, options)`** — mounts the HUD overlay and returns a dispose function
114
+
115
+ ## License
116
+
117
+ MIT
@@ -0,0 +1 @@
1
+ :root{--bg: #f7f8fc;--surface: #ffffff;--surface-2: #eef0f6;--border: #dde0ec;--text: #1a1b2e;--text-muted: #7a7e99;--accent: #0077cc;--accent-dim: #ddeeff;--accent-hover: #c8e4ff;--error: #cc3333;--green: #227744;--overlay-bg: rgba(200, 200, 220, .7)}[data-theme=dark]{--bg: #0d0d18;--surface: #14142a;--surface-2: #1e1e38;--border: #2a2a45;--text: #e0e0f0;--text-muted: #8899bb;--accent: #66ccff;--accent-dim: #1a3a6a;--accent-hover: #1e4a88;--error: #ff5555;--green: #44aa77;--overlay-bg: rgba(5, 5, 15, .78)}.hud{position:absolute;top:12px;right:12px;background:var(--surface);border:1px solid var(--border);border-radius:10px;padding:12px 16px;min-width:160px;box-shadow:0 2px 12px #00000014;z-index:10;display:flex;flex-direction:column;gap:8px;font-size:12px;color:var(--text)}[data-theme=dark] .hud{box-shadow:0 2px 12px #0006}.hud-title{color:var(--accent);font-weight:600;font-size:13px}.hud-stats{display:flex;flex-direction:column;gap:3px;color:var(--text)}.hud-divider{border:none;border-top:1px solid var(--border);margin:0}.hud-toggles{display:flex;flex-direction:column;gap:5px}.hud-toggle{display:flex;align-items:center;gap:6px;cursor:pointer;color:var(--text);user-select:none}.hud-toggle--disabled{opacity:.35;cursor:default}.hud-btn{background:var(--surface-2);border:1px solid var(--border);color:var(--text);padding:5px 10px;border-radius:5px;font-size:11px;cursor:pointer;width:100%;text-align:center;transition:background .15s}.hud-btn:hover{filter:brightness(.95)}[data-theme=dark] .hud-btn:hover{filter:brightness(1.1)}.hud-size-section{display:flex;flex-direction:column;gap:5px}.hud-size-label{color:var(--text-muted);font-size:11px;text-transform:uppercase;letter-spacing:.05em}.hud-slider{width:100%;accent-color:var(--accent);cursor:pointer}.hud-theme-btn{background:var(--surface-2);border:1px solid var(--border);color:var(--text-muted);padding:5px 10px;border-radius:5px;font-size:11px;cursor:pointer;width:100%;text-align:center;transition:color .15s,background .15s}.hud-theme-btn:hover{color:var(--text);background:var(--surface-2);filter:brightness(.95)}[data-theme=dark] .hud-theme-btn:hover{filter:brightness(1.1)}.colmap-renderer-status,.colmap-renderer-error{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:13px;pointer-events:none}.colmap-renderer-status{color:var(--text-muted, #7a7e99)}.colmap-renderer-error{color:var(--error, #cc3333);pointer-events:auto}.controls-hint{position:absolute;bottom:14px;left:50%;transform:translate(-50%);background:var(--overlay-bg);border-radius:4px;padding:5px 14px;font-size:11px;color:var(--text-muted);pointer-events:none;z-index:10;white-space:nowrap;backdrop-filter:blur(4px)}.axis-legend{position:absolute;bottom:42px;left:12px;background:var(--overlay-bg);backdrop-filter:blur(4px);border-radius:6px;padding:6px 10px;display:flex;flex-direction:column;gap:3px;pointer-events:none;z-index:10}.axis-legend-row{display:flex;align-items:center;gap:8px}.axis-legend-name{font-size:12px;font-weight:700;min-width:24px}.axis-legend-sub{font-size:10px;color:var(--text-muted)}
@@ -0,0 +1,923 @@
1
+ import { parseColmap as V } from "@thethingteam/colmap-wasm";
2
+ import * as r from "three";
3
+ import { Controls as W, Vector3 as S, MOUSE as j, TOUCH as k, Quaternion as H, Spherical as Z, Vector2 as P, Ray as G, Plane as q, MathUtils as $ } from "three";
4
+ const F = { type: "change" }, Y = { type: "start" }, B = { type: "end" }, N = new G(), K = new q(), Q = Math.cos(70 * $.DEG2RAD), _ = new S(), y = 2 * Math.PI, d = {
5
+ NONE: -1,
6
+ ROTATE: 0,
7
+ DOLLY: 1,
8
+ PAN: 2,
9
+ TOUCH_ROTATE: 3,
10
+ TOUCH_PAN: 4,
11
+ TOUCH_DOLLY_PAN: 5,
12
+ TOUCH_DOLLY_ROTATE: 6
13
+ }, I = 1e-6;
14
+ class J extends W {
15
+ /**
16
+ * Constructs a new controls instance.
17
+ *
18
+ * @param {Object3D} object - The object that is managed by the controls.
19
+ * @param {?HTMLDOMElement} domElement - The HTML element used for event listeners.
20
+ */
21
+ constructor(t, e = null) {
22
+ super(t, e), this.state = d.NONE, this.target = new S(), this.cursor = new S(), this.minDistance = 0, this.maxDistance = 1 / 0, this.minZoom = 0, this.maxZoom = 1 / 0, this.minTargetRadius = 0, this.maxTargetRadius = 1 / 0, this.minPolarAngle = 0, this.maxPolarAngle = Math.PI, this.minAzimuthAngle = -1 / 0, this.maxAzimuthAngle = 1 / 0, this.enableDamping = !1, this.dampingFactor = 0.05, this.enableZoom = !0, this.zoomSpeed = 1, this.enableRotate = !0, this.rotateSpeed = 1, this.keyRotateSpeed = 1, this.enablePan = !0, this.panSpeed = 1, this.screenSpacePanning = !0, this.keyPanSpeed = 7, this.zoomToCursor = !1, this.autoRotate = !1, this.autoRotateSpeed = 2, this.keys = { LEFT: "ArrowLeft", UP: "ArrowUp", RIGHT: "ArrowRight", BOTTOM: "ArrowDown" }, this.mouseButtons = { LEFT: j.ROTATE, MIDDLE: j.DOLLY, RIGHT: j.PAN }, this.touches = { ONE: k.ROTATE, TWO: k.DOLLY_PAN }, this.target0 = this.target.clone(), this.position0 = this.object.position.clone(), this.zoom0 = this.object.zoom, this._domElementKeyEvents = null, this._lastPosition = new S(), this._lastQuaternion = new H(), this._lastTargetPosition = new S(), this._quat = new H().setFromUnitVectors(t.up, new S(0, 1, 0)), this._quatInverse = this._quat.clone().invert(), this._spherical = new Z(), this._sphericalDelta = new Z(), this._scale = 1, this._panOffset = new S(), this._rotateStart = new P(), this._rotateEnd = new P(), this._rotateDelta = new P(), this._panStart = new P(), this._panEnd = new P(), this._panDelta = new P(), this._dollyStart = new P(), this._dollyEnd = new P(), this._dollyDelta = new P(), this._dollyDirection = new S(), this._mouse = new P(), this._performCursorZoom = !1, this._pointers = [], this._pointerPositions = {}, this._controlActive = !1, this._onPointerMove = et.bind(this), this._onPointerDown = tt.bind(this), this._onPointerUp = st.bind(this), this._onContextMenu = lt.bind(this), this._onMouseWheel = nt.bind(this), this._onKeyDown = at.bind(this), this._onTouchStart = rt.bind(this), this._onTouchMove = ht.bind(this), this._onMouseDown = it.bind(this), this._onMouseMove = ot.bind(this), this._interceptControlDown = ct.bind(this), this._interceptControlUp = dt.bind(this), this.domElement !== null && this.connect(this.domElement), this.update();
23
+ }
24
+ connect(t) {
25
+ super.connect(t), this.domElement.addEventListener("pointerdown", this._onPointerDown), this.domElement.addEventListener("pointercancel", this._onPointerUp), this.domElement.addEventListener("contextmenu", this._onContextMenu), this.domElement.addEventListener("wheel", this._onMouseWheel, { passive: !1 }), this.domElement.getRootNode().addEventListener("keydown", this._interceptControlDown, { passive: !0, capture: !0 }), this.domElement.style.touchAction = "none";
26
+ }
27
+ disconnect() {
28
+ this.domElement.removeEventListener("pointerdown", this._onPointerDown), this.domElement.removeEventListener("pointermove", this._onPointerMove), this.domElement.removeEventListener("pointerup", this._onPointerUp), this.domElement.removeEventListener("pointercancel", this._onPointerUp), this.domElement.removeEventListener("wheel", this._onMouseWheel), this.domElement.removeEventListener("contextmenu", this._onContextMenu), this.stopListenToKeyEvents(), this.domElement.getRootNode().removeEventListener("keydown", this._interceptControlDown, { capture: !0 }), this.domElement.style.touchAction = "auto";
29
+ }
30
+ dispose() {
31
+ this.disconnect();
32
+ }
33
+ /**
34
+ * Get the current vertical rotation, in radians.
35
+ *
36
+ * @return {number} The current vertical rotation, in radians.
37
+ */
38
+ getPolarAngle() {
39
+ return this._spherical.phi;
40
+ }
41
+ /**
42
+ * Get the current horizontal rotation, in radians.
43
+ *
44
+ * @return {number} The current horizontal rotation, in radians.
45
+ */
46
+ getAzimuthalAngle() {
47
+ return this._spherical.theta;
48
+ }
49
+ /**
50
+ * Returns the distance from the camera to the target.
51
+ *
52
+ * @return {number} The distance from the camera to the target.
53
+ */
54
+ getDistance() {
55
+ return this.object.position.distanceTo(this.target);
56
+ }
57
+ /**
58
+ * Adds key event listeners to the given DOM element.
59
+ * `window` is a recommended argument for using this method.
60
+ *
61
+ * @param {HTMLDOMElement} domElement - The DOM element
62
+ */
63
+ listenToKeyEvents(t) {
64
+ t.addEventListener("keydown", this._onKeyDown), this._domElementKeyEvents = t;
65
+ }
66
+ /**
67
+ * Removes the key event listener previously defined with `listenToKeyEvents()`.
68
+ */
69
+ stopListenToKeyEvents() {
70
+ this._domElementKeyEvents !== null && (this._domElementKeyEvents.removeEventListener("keydown", this._onKeyDown), this._domElementKeyEvents = null);
71
+ }
72
+ /**
73
+ * Save the current state of the controls. This can later be recovered with `reset()`.
74
+ */
75
+ saveState() {
76
+ this.target0.copy(this.target), this.position0.copy(this.object.position), this.zoom0 = this.object.zoom;
77
+ }
78
+ /**
79
+ * Reset the controls to their state from either the last time the `saveState()`
80
+ * was called, or the initial state.
81
+ */
82
+ reset() {
83
+ this.target.copy(this.target0), this.object.position.copy(this.position0), this.object.zoom = this.zoom0, this.object.updateProjectionMatrix(), this.dispatchEvent(F), this.update(), this.state = d.NONE;
84
+ }
85
+ update(t = null) {
86
+ const e = this.object.position;
87
+ _.copy(e).sub(this.target), _.applyQuaternion(this._quat), this._spherical.setFromVector3(_), this.autoRotate && this.state === d.NONE && this._rotateLeft(this._getAutoRotationAngle(t)), this.enableDamping ? (this._spherical.theta += this._sphericalDelta.theta * this.dampingFactor, this._spherical.phi += this._sphericalDelta.phi * this.dampingFactor) : (this._spherical.theta += this._sphericalDelta.theta, this._spherical.phi += this._sphericalDelta.phi);
88
+ let i = this.minAzimuthAngle, o = this.maxAzimuthAngle;
89
+ isFinite(i) && isFinite(o) && (i < -Math.PI ? i += y : i > Math.PI && (i -= y), o < -Math.PI ? o += y : o > Math.PI && (o -= y), i <= o ? this._spherical.theta = Math.max(i, Math.min(o, this._spherical.theta)) : this._spherical.theta = this._spherical.theta > (i + o) / 2 ? Math.max(i, this._spherical.theta) : Math.min(o, this._spherical.theta)), this._spherical.phi = Math.max(this.minPolarAngle, Math.min(this.maxPolarAngle, this._spherical.phi)), this._spherical.makeSafe(), this.enableDamping === !0 ? this.target.addScaledVector(this._panOffset, this.dampingFactor) : this.target.add(this._panOffset), this.target.sub(this.cursor), this.target.clampLength(this.minTargetRadius, this.maxTargetRadius), this.target.add(this.cursor);
90
+ let n = !1;
91
+ if (this.zoomToCursor && this._performCursorZoom || this.object.isOrthographicCamera)
92
+ this._spherical.radius = this._clampDistance(this._spherical.radius);
93
+ else {
94
+ const a = this._spherical.radius;
95
+ this._spherical.radius = this._clampDistance(this._spherical.radius * this._scale), n = a != this._spherical.radius;
96
+ }
97
+ if (_.setFromSpherical(this._spherical), _.applyQuaternion(this._quatInverse), e.copy(this.target).add(_), this.object.lookAt(this.target), this.enableDamping === !0 ? (this._sphericalDelta.theta *= 1 - this.dampingFactor, this._sphericalDelta.phi *= 1 - this.dampingFactor, this._panOffset.multiplyScalar(1 - this.dampingFactor)) : (this._sphericalDelta.set(0, 0, 0), this._panOffset.set(0, 0, 0)), this.zoomToCursor && this._performCursorZoom) {
98
+ let a = null;
99
+ if (this.object.isPerspectiveCamera) {
100
+ const l = _.length();
101
+ a = this._clampDistance(l * this._scale);
102
+ const c = l - a;
103
+ this.object.position.addScaledVector(this._dollyDirection, c), this.object.updateMatrixWorld(), n = !!c;
104
+ } else if (this.object.isOrthographicCamera) {
105
+ const l = new S(this._mouse.x, this._mouse.y, 0);
106
+ l.unproject(this.object);
107
+ const c = this.object.zoom;
108
+ this.object.zoom = Math.max(this.minZoom, Math.min(this.maxZoom, this.object.zoom / this._scale)), this.object.updateProjectionMatrix(), n = c !== this.object.zoom;
109
+ const h = new S(this._mouse.x, this._mouse.y, 0);
110
+ h.unproject(this.object), this.object.position.sub(h).add(l), this.object.updateMatrixWorld(), a = _.length();
111
+ } else
112
+ console.warn("WARNING: OrbitControls.js encountered an unknown camera type - zoom to cursor disabled."), this.zoomToCursor = !1;
113
+ a !== null && (this.screenSpacePanning ? this.target.set(0, 0, -1).transformDirection(this.object.matrix).multiplyScalar(a).add(this.object.position) : (N.origin.copy(this.object.position), N.direction.set(0, 0, -1).transformDirection(this.object.matrix), Math.abs(this.object.up.dot(N.direction)) < Q ? this.object.lookAt(this.target) : (K.setFromNormalAndCoplanarPoint(this.object.up, this.target), N.intersectPlane(K, this.target))));
114
+ } else if (this.object.isOrthographicCamera) {
115
+ const a = this.object.zoom;
116
+ this.object.zoom = Math.max(this.minZoom, Math.min(this.maxZoom, this.object.zoom / this._scale)), a !== this.object.zoom && (this.object.updateProjectionMatrix(), n = !0);
117
+ }
118
+ return this._scale = 1, this._performCursorZoom = !1, n || this._lastPosition.distanceToSquared(this.object.position) > I || 8 * (1 - this._lastQuaternion.dot(this.object.quaternion)) > I || this._lastTargetPosition.distanceToSquared(this.target) > I ? (this.dispatchEvent(F), this._lastPosition.copy(this.object.position), this._lastQuaternion.copy(this.object.quaternion), this._lastTargetPosition.copy(this.target), !0) : !1;
119
+ }
120
+ _getAutoRotationAngle(t) {
121
+ return t !== null ? y / 60 * this.autoRotateSpeed * t : y / 60 / 60 * this.autoRotateSpeed;
122
+ }
123
+ _getZoomScale(t) {
124
+ const e = Math.abs(t * 0.01);
125
+ return Math.pow(0.95, this.zoomSpeed * e);
126
+ }
127
+ _rotateLeft(t) {
128
+ this._sphericalDelta.theta -= t;
129
+ }
130
+ _rotateUp(t) {
131
+ this._sphericalDelta.phi -= t;
132
+ }
133
+ _panLeft(t, e) {
134
+ _.setFromMatrixColumn(e, 0), _.multiplyScalar(-t), this._panOffset.add(_);
135
+ }
136
+ _panUp(t, e) {
137
+ this.screenSpacePanning === !0 ? _.setFromMatrixColumn(e, 1) : (_.setFromMatrixColumn(e, 0), _.crossVectors(this.object.up, _)), _.multiplyScalar(t), this._panOffset.add(_);
138
+ }
139
+ // deltaX and deltaY are in pixels; right and down are positive
140
+ _pan(t, e) {
141
+ const i = this.domElement;
142
+ if (this.object.isPerspectiveCamera) {
143
+ const o = this.object.position;
144
+ _.copy(o).sub(this.target);
145
+ let n = _.length();
146
+ n *= Math.tan(this.object.fov / 2 * Math.PI / 180), this._panLeft(2 * t * n / i.clientHeight, this.object.matrix), this._panUp(2 * e * n / i.clientHeight, this.object.matrix);
147
+ } else this.object.isOrthographicCamera ? (this._panLeft(t * (this.object.right - this.object.left) / this.object.zoom / i.clientWidth, this.object.matrix), this._panUp(e * (this.object.top - this.object.bottom) / this.object.zoom / i.clientHeight, this.object.matrix)) : (console.warn("WARNING: OrbitControls.js encountered an unknown camera type - pan disabled."), this.enablePan = !1);
148
+ }
149
+ _dollyOut(t) {
150
+ this.object.isPerspectiveCamera || this.object.isOrthographicCamera ? this._scale /= t : (console.warn("WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled."), this.enableZoom = !1);
151
+ }
152
+ _dollyIn(t) {
153
+ this.object.isPerspectiveCamera || this.object.isOrthographicCamera ? this._scale *= t : (console.warn("WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled."), this.enableZoom = !1);
154
+ }
155
+ _updateZoomParameters(t, e) {
156
+ if (!this.zoomToCursor)
157
+ return;
158
+ this._performCursorZoom = !0;
159
+ const i = this.domElement.getBoundingClientRect(), o = t - i.left, n = e - i.top, a = i.width, l = i.height;
160
+ this._mouse.x = o / a * 2 - 1, this._mouse.y = -(n / l) * 2 + 1, this._dollyDirection.set(this._mouse.x, this._mouse.y, 1).unproject(this.object).sub(this.object.position).normalize();
161
+ }
162
+ _clampDistance(t) {
163
+ return Math.max(this.minDistance, Math.min(this.maxDistance, t));
164
+ }
165
+ //
166
+ // event callbacks - update the object state
167
+ //
168
+ _handleMouseDownRotate(t) {
169
+ this._rotateStart.set(t.clientX, t.clientY);
170
+ }
171
+ _handleMouseDownDolly(t) {
172
+ this._updateZoomParameters(t.clientX, t.clientX), this._dollyStart.set(t.clientX, t.clientY);
173
+ }
174
+ _handleMouseDownPan(t) {
175
+ this._panStart.set(t.clientX, t.clientY);
176
+ }
177
+ _handleMouseMoveRotate(t) {
178
+ this._rotateEnd.set(t.clientX, t.clientY), this._rotateDelta.subVectors(this._rotateEnd, this._rotateStart).multiplyScalar(this.rotateSpeed);
179
+ const e = this.domElement;
180
+ this._rotateLeft(y * this._rotateDelta.x / e.clientHeight), this._rotateUp(y * this._rotateDelta.y / e.clientHeight), this._rotateStart.copy(this._rotateEnd), this.update();
181
+ }
182
+ _handleMouseMoveDolly(t) {
183
+ this._dollyEnd.set(t.clientX, t.clientY), this._dollyDelta.subVectors(this._dollyEnd, this._dollyStart), this._dollyDelta.y > 0 ? this._dollyOut(this._getZoomScale(this._dollyDelta.y)) : this._dollyDelta.y < 0 && this._dollyIn(this._getZoomScale(this._dollyDelta.y)), this._dollyStart.copy(this._dollyEnd), this.update();
184
+ }
185
+ _handleMouseMovePan(t) {
186
+ this._panEnd.set(t.clientX, t.clientY), this._panDelta.subVectors(this._panEnd, this._panStart).multiplyScalar(this.panSpeed), this._pan(this._panDelta.x, this._panDelta.y), this._panStart.copy(this._panEnd), this.update();
187
+ }
188
+ _handleMouseWheel(t) {
189
+ this._updateZoomParameters(t.clientX, t.clientY), t.deltaY < 0 ? this._dollyIn(this._getZoomScale(t.deltaY)) : t.deltaY > 0 && this._dollyOut(this._getZoomScale(t.deltaY)), this.update();
190
+ }
191
+ _handleKeyDown(t) {
192
+ let e = !1;
193
+ switch (t.code) {
194
+ case this.keys.UP:
195
+ t.ctrlKey || t.metaKey || t.shiftKey ? this.enableRotate && this._rotateUp(y * this.keyRotateSpeed / this.domElement.clientHeight) : this.enablePan && this._pan(0, this.keyPanSpeed), e = !0;
196
+ break;
197
+ case this.keys.BOTTOM:
198
+ t.ctrlKey || t.metaKey || t.shiftKey ? this.enableRotate && this._rotateUp(-y * this.keyRotateSpeed / this.domElement.clientHeight) : this.enablePan && this._pan(0, -this.keyPanSpeed), e = !0;
199
+ break;
200
+ case this.keys.LEFT:
201
+ t.ctrlKey || t.metaKey || t.shiftKey ? this.enableRotate && this._rotateLeft(y * this.keyRotateSpeed / this.domElement.clientHeight) : this.enablePan && this._pan(this.keyPanSpeed, 0), e = !0;
202
+ break;
203
+ case this.keys.RIGHT:
204
+ t.ctrlKey || t.metaKey || t.shiftKey ? this.enableRotate && this._rotateLeft(-y * this.keyRotateSpeed / this.domElement.clientHeight) : this.enablePan && this._pan(-this.keyPanSpeed, 0), e = !0;
205
+ break;
206
+ }
207
+ e && (t.preventDefault(), this.update());
208
+ }
209
+ _handleTouchStartRotate(t) {
210
+ if (this._pointers.length === 1)
211
+ this._rotateStart.set(t.pageX, t.pageY);
212
+ else {
213
+ const e = this._getSecondPointerPosition(t), i = 0.5 * (t.pageX + e.x), o = 0.5 * (t.pageY + e.y);
214
+ this._rotateStart.set(i, o);
215
+ }
216
+ }
217
+ _handleTouchStartPan(t) {
218
+ if (this._pointers.length === 1)
219
+ this._panStart.set(t.pageX, t.pageY);
220
+ else {
221
+ const e = this._getSecondPointerPosition(t), i = 0.5 * (t.pageX + e.x), o = 0.5 * (t.pageY + e.y);
222
+ this._panStart.set(i, o);
223
+ }
224
+ }
225
+ _handleTouchStartDolly(t) {
226
+ const e = this._getSecondPointerPosition(t), i = t.pageX - e.x, o = t.pageY - e.y, n = Math.sqrt(i * i + o * o);
227
+ this._dollyStart.set(0, n);
228
+ }
229
+ _handleTouchStartDollyPan(t) {
230
+ this.enableZoom && this._handleTouchStartDolly(t), this.enablePan && this._handleTouchStartPan(t);
231
+ }
232
+ _handleTouchStartDollyRotate(t) {
233
+ this.enableZoom && this._handleTouchStartDolly(t), this.enableRotate && this._handleTouchStartRotate(t);
234
+ }
235
+ _handleTouchMoveRotate(t) {
236
+ if (this._pointers.length == 1)
237
+ this._rotateEnd.set(t.pageX, t.pageY);
238
+ else {
239
+ const i = this._getSecondPointerPosition(t), o = 0.5 * (t.pageX + i.x), n = 0.5 * (t.pageY + i.y);
240
+ this._rotateEnd.set(o, n);
241
+ }
242
+ this._rotateDelta.subVectors(this._rotateEnd, this._rotateStart).multiplyScalar(this.rotateSpeed);
243
+ const e = this.domElement;
244
+ this._rotateLeft(y * this._rotateDelta.x / e.clientHeight), this._rotateUp(y * this._rotateDelta.y / e.clientHeight), this._rotateStart.copy(this._rotateEnd);
245
+ }
246
+ _handleTouchMovePan(t) {
247
+ if (this._pointers.length === 1)
248
+ this._panEnd.set(t.pageX, t.pageY);
249
+ else {
250
+ const e = this._getSecondPointerPosition(t), i = 0.5 * (t.pageX + e.x), o = 0.5 * (t.pageY + e.y);
251
+ this._panEnd.set(i, o);
252
+ }
253
+ this._panDelta.subVectors(this._panEnd, this._panStart).multiplyScalar(this.panSpeed), this._pan(this._panDelta.x, this._panDelta.y), this._panStart.copy(this._panEnd);
254
+ }
255
+ _handleTouchMoveDolly(t) {
256
+ const e = this._getSecondPointerPosition(t), i = t.pageX - e.x, o = t.pageY - e.y, n = Math.sqrt(i * i + o * o);
257
+ this._dollyEnd.set(0, n), this._dollyDelta.set(0, Math.pow(this._dollyEnd.y / this._dollyStart.y, this.zoomSpeed)), this._dollyOut(this._dollyDelta.y), this._dollyStart.copy(this._dollyEnd);
258
+ const a = (t.pageX + e.x) * 0.5, l = (t.pageY + e.y) * 0.5;
259
+ this._updateZoomParameters(a, l);
260
+ }
261
+ _handleTouchMoveDollyPan(t) {
262
+ this.enableZoom && this._handleTouchMoveDolly(t), this.enablePan && this._handleTouchMovePan(t);
263
+ }
264
+ _handleTouchMoveDollyRotate(t) {
265
+ this.enableZoom && this._handleTouchMoveDolly(t), this.enableRotate && this._handleTouchMoveRotate(t);
266
+ }
267
+ // pointers
268
+ _addPointer(t) {
269
+ this._pointers.push(t.pointerId);
270
+ }
271
+ _removePointer(t) {
272
+ delete this._pointerPositions[t.pointerId];
273
+ for (let e = 0; e < this._pointers.length; e++)
274
+ if (this._pointers[e] == t.pointerId) {
275
+ this._pointers.splice(e, 1);
276
+ return;
277
+ }
278
+ }
279
+ _isTrackingPointer(t) {
280
+ for (let e = 0; e < this._pointers.length; e++)
281
+ if (this._pointers[e] == t.pointerId) return !0;
282
+ return !1;
283
+ }
284
+ _trackPointer(t) {
285
+ let e = this._pointerPositions[t.pointerId];
286
+ e === void 0 && (e = new P(), this._pointerPositions[t.pointerId] = e), e.set(t.pageX, t.pageY);
287
+ }
288
+ _getSecondPointerPosition(t) {
289
+ const e = t.pointerId === this._pointers[0] ? this._pointers[1] : this._pointers[0];
290
+ return this._pointerPositions[e];
291
+ }
292
+ //
293
+ _customWheelEvent(t) {
294
+ const e = t.deltaMode, i = {
295
+ clientX: t.clientX,
296
+ clientY: t.clientY,
297
+ deltaY: t.deltaY
298
+ };
299
+ switch (e) {
300
+ case 1:
301
+ i.deltaY *= 16;
302
+ break;
303
+ case 2:
304
+ i.deltaY *= 100;
305
+ break;
306
+ }
307
+ return t.ctrlKey && !this._controlActive && (i.deltaY *= 10), i;
308
+ }
309
+ }
310
+ function tt(s) {
311
+ this.enabled !== !1 && (this._pointers.length === 0 && (this.domElement.setPointerCapture(s.pointerId), this.domElement.addEventListener("pointermove", this._onPointerMove), this.domElement.addEventListener("pointerup", this._onPointerUp)), !this._isTrackingPointer(s) && (this._addPointer(s), s.pointerType === "touch" ? this._onTouchStart(s) : this._onMouseDown(s)));
312
+ }
313
+ function et(s) {
314
+ this.enabled !== !1 && (s.pointerType === "touch" ? this._onTouchMove(s) : this._onMouseMove(s));
315
+ }
316
+ function st(s) {
317
+ switch (this._removePointer(s), this._pointers.length) {
318
+ case 0:
319
+ this.domElement.releasePointerCapture(s.pointerId), this.domElement.removeEventListener("pointermove", this._onPointerMove), this.domElement.removeEventListener("pointerup", this._onPointerUp), this.dispatchEvent(B), this.state = d.NONE;
320
+ break;
321
+ case 1:
322
+ const t = this._pointers[0], e = this._pointerPositions[t];
323
+ this._onTouchStart({ pointerId: t, pageX: e.x, pageY: e.y });
324
+ break;
325
+ }
326
+ }
327
+ function it(s) {
328
+ let t;
329
+ switch (s.button) {
330
+ case 0:
331
+ t = this.mouseButtons.LEFT;
332
+ break;
333
+ case 1:
334
+ t = this.mouseButtons.MIDDLE;
335
+ break;
336
+ case 2:
337
+ t = this.mouseButtons.RIGHT;
338
+ break;
339
+ default:
340
+ t = -1;
341
+ }
342
+ switch (t) {
343
+ case j.DOLLY:
344
+ if (this.enableZoom === !1) return;
345
+ this._handleMouseDownDolly(s), this.state = d.DOLLY;
346
+ break;
347
+ case j.ROTATE:
348
+ if (s.ctrlKey || s.metaKey || s.shiftKey) {
349
+ if (this.enablePan === !1) return;
350
+ this._handleMouseDownPan(s), this.state = d.PAN;
351
+ } else {
352
+ if (this.enableRotate === !1) return;
353
+ this._handleMouseDownRotate(s), this.state = d.ROTATE;
354
+ }
355
+ break;
356
+ case j.PAN:
357
+ if (s.ctrlKey || s.metaKey || s.shiftKey) {
358
+ if (this.enableRotate === !1) return;
359
+ this._handleMouseDownRotate(s), this.state = d.ROTATE;
360
+ } else {
361
+ if (this.enablePan === !1) return;
362
+ this._handleMouseDownPan(s), this.state = d.PAN;
363
+ }
364
+ break;
365
+ default:
366
+ this.state = d.NONE;
367
+ }
368
+ this.state !== d.NONE && this.dispatchEvent(Y);
369
+ }
370
+ function ot(s) {
371
+ switch (this.state) {
372
+ case d.ROTATE:
373
+ if (this.enableRotate === !1) return;
374
+ this._handleMouseMoveRotate(s);
375
+ break;
376
+ case d.DOLLY:
377
+ if (this.enableZoom === !1) return;
378
+ this._handleMouseMoveDolly(s);
379
+ break;
380
+ case d.PAN:
381
+ if (this.enablePan === !1) return;
382
+ this._handleMouseMovePan(s);
383
+ break;
384
+ }
385
+ }
386
+ function nt(s) {
387
+ this.enabled === !1 || this.enableZoom === !1 || this.state !== d.NONE || (s.preventDefault(), this.dispatchEvent(Y), this._handleMouseWheel(this._customWheelEvent(s)), this.dispatchEvent(B));
388
+ }
389
+ function at(s) {
390
+ this.enabled !== !1 && this._handleKeyDown(s);
391
+ }
392
+ function rt(s) {
393
+ switch (this._trackPointer(s), this._pointers.length) {
394
+ case 1:
395
+ switch (this.touches.ONE) {
396
+ case k.ROTATE:
397
+ if (this.enableRotate === !1) return;
398
+ this._handleTouchStartRotate(s), this.state = d.TOUCH_ROTATE;
399
+ break;
400
+ case k.PAN:
401
+ if (this.enablePan === !1) return;
402
+ this._handleTouchStartPan(s), this.state = d.TOUCH_PAN;
403
+ break;
404
+ default:
405
+ this.state = d.NONE;
406
+ }
407
+ break;
408
+ case 2:
409
+ switch (this.touches.TWO) {
410
+ case k.DOLLY_PAN:
411
+ if (this.enableZoom === !1 && this.enablePan === !1) return;
412
+ this._handleTouchStartDollyPan(s), this.state = d.TOUCH_DOLLY_PAN;
413
+ break;
414
+ case k.DOLLY_ROTATE:
415
+ if (this.enableZoom === !1 && this.enableRotate === !1) return;
416
+ this._handleTouchStartDollyRotate(s), this.state = d.TOUCH_DOLLY_ROTATE;
417
+ break;
418
+ default:
419
+ this.state = d.NONE;
420
+ }
421
+ break;
422
+ default:
423
+ this.state = d.NONE;
424
+ }
425
+ this.state !== d.NONE && this.dispatchEvent(Y);
426
+ }
427
+ function ht(s) {
428
+ switch (this._trackPointer(s), this.state) {
429
+ case d.TOUCH_ROTATE:
430
+ if (this.enableRotate === !1) return;
431
+ this._handleTouchMoveRotate(s), this.update();
432
+ break;
433
+ case d.TOUCH_PAN:
434
+ if (this.enablePan === !1) return;
435
+ this._handleTouchMovePan(s), this.update();
436
+ break;
437
+ case d.TOUCH_DOLLY_PAN:
438
+ if (this.enableZoom === !1 && this.enablePan === !1) return;
439
+ this._handleTouchMoveDollyPan(s), this.update();
440
+ break;
441
+ case d.TOUCH_DOLLY_ROTATE:
442
+ if (this.enableZoom === !1 && this.enableRotate === !1) return;
443
+ this._handleTouchMoveDollyRotate(s), this.update();
444
+ break;
445
+ default:
446
+ this.state = d.NONE;
447
+ }
448
+ }
449
+ function lt(s) {
450
+ this.enabled !== !1 && s.preventDefault();
451
+ }
452
+ function ct(s) {
453
+ s.key === "Control" && (this._controlActive = !0, this.domElement.getRootNode().addEventListener("keyup", this._interceptControlUp, { passive: !0, capture: !0 }));
454
+ }
455
+ function dt(s) {
456
+ s.key === "Control" && (this._controlActive = !1, this.domElement.getRootNode().removeEventListener("keyup", this._interceptControlUp, { passive: !0, capture: !0 }));
457
+ }
458
+ function ut(s) {
459
+ const t = s.length, e = new Float32Array(t * 3), i = new Float32Array(t * 3);
460
+ for (let a = 0; a < t; a++) {
461
+ const l = s[a];
462
+ e[a * 3] = l.xyz[0], e[a * 3 + 1] = -l.xyz[1], e[a * 3 + 2] = -l.xyz[2], i[a * 3] = l.rgb[0] / 255, i[a * 3 + 1] = l.rgb[1] / 255, i[a * 3 + 2] = l.rgb[2] / 255;
463
+ }
464
+ const o = new r.BufferGeometry();
465
+ o.setAttribute("position", new r.BufferAttribute(e, 3)), o.setAttribute("color", new r.BufferAttribute(i, 3));
466
+ const n = new r.PointsMaterial({
467
+ size: 0.02,
468
+ vertexColors: !0,
469
+ sizeAttenuation: !0
470
+ });
471
+ return new r.Points(o, n);
472
+ }
473
+ function mt(s) {
474
+ const [t, e, i, o] = s, n = new r.Matrix3();
475
+ return n.set(
476
+ 1 - 2 * (i * i + o * o),
477
+ 2 * (e * i - t * o),
478
+ 2 * (e * o + t * i),
479
+ 2 * (e * i + t * o),
480
+ 1 - 2 * (e * e + o * o),
481
+ 2 * (i * o - t * e),
482
+ 2 * (e * o - t * i),
483
+ 2 * (i * o + t * e),
484
+ 1 - 2 * (e * e + i * i)
485
+ ), n;
486
+ }
487
+ function pt(s, t) {
488
+ const e = s.clone().transpose(), i = new r.Vector3(t[0], t[1], t[2]);
489
+ return i.applyMatrix3(e).negate(), new r.Vector3(i.x, -i.y, -i.z);
490
+ }
491
+ function _t(s, t, e, i, o) {
492
+ const a = t.clone().transpose(), l = [
493
+ new r.Vector3(-i / 2 / e, -o / 2 / e, 1),
494
+ new r.Vector3(i / 2 / e, -o / 2 / e, 1),
495
+ new r.Vector3(i / 2 / e, o / 2 / e, 1),
496
+ new r.Vector3(-i / 2 / e, o / 2 / e, 1)
497
+ ].map((h) => (h.multiplyScalar(0.3), h.applyMatrix3(a), h.set(h.x, -h.y, -h.z), h.add(s), h)), c = [];
498
+ for (const h of l)
499
+ c.push(s.x, s.y, s.z, h.x, h.y, h.z);
500
+ for (let h = 0; h < 4; h++) {
501
+ const f = l[h], u = l[(h + 1) % 4];
502
+ c.push(f.x, f.y, f.z, u.x, u.y, u.z);
503
+ }
504
+ return new Float32Array(c);
505
+ }
506
+ function ft(s, t) {
507
+ const e = new Map(t.map((n) => [n.id, n])), i = new r.Group(), o = [];
508
+ for (const n of s) {
509
+ const a = e.get(n.cameraId);
510
+ if (!a) continue;
511
+ const l = mt(n.qvec), c = pt(l, n.tvec), h = a.params[0] ?? 500, f = _t(c, l, h, a.width, a.height), u = new r.BufferGeometry();
512
+ u.setAttribute("position", new r.BufferAttribute(f, 3));
513
+ const E = new r.LineBasicMaterial({ color: 52479, opacity: 0.7, transparent: !0 });
514
+ i.add(new r.LineSegments(u, E)), o.push(c.x, c.y, c.z);
515
+ }
516
+ return i;
517
+ }
518
+ function bt(s) {
519
+ return new r.AxesHelper(Math.max(s * 0.1, 1));
520
+ }
521
+ function gt(s) {
522
+ const [t, e, i, o] = s, n = new r.Matrix3();
523
+ return n.set(
524
+ 1 - 2 * (i * i + o * o),
525
+ 2 * (e * i - t * o),
526
+ 2 * (e * o + t * i),
527
+ 2 * (e * i + t * o),
528
+ 1 - 2 * (e * e + o * o),
529
+ 2 * (i * o - t * e),
530
+ 2 * (e * o - t * i),
531
+ 2 * (i * o + t * e),
532
+ 1 - 2 * (e * e + i * i)
533
+ ), n;
534
+ }
535
+ async function yt(s) {
536
+ const t = URL.createObjectURL(s);
537
+ return new Promise((e, i) => {
538
+ new r.TextureLoader().load(
539
+ t,
540
+ (o) => {
541
+ URL.revokeObjectURL(t), e(o);
542
+ },
543
+ void 0,
544
+ (o) => {
545
+ URL.revokeObjectURL(t), i(o);
546
+ }
547
+ );
548
+ });
549
+ }
550
+ async function Et(s, t, e) {
551
+ const i = new Map(t.map((a) => [a.id, a])), o = new r.Group();
552
+ o.visible = !1;
553
+ const n = s.map(async (a) => {
554
+ const l = a.name.split("/").pop() ?? a.name, c = e.get(l) ?? e.get(a.name);
555
+ if (!c) return;
556
+ const h = i.get(a.cameraId);
557
+ if (!h) return;
558
+ const f = await yt(c), u = h.width / h.height, E = 0.3, A = new r.PlaneGeometry(E * u, E), C = new r.MeshBasicMaterial({ map: f, side: r.DoubleSide }), p = new r.Mesh(A, C), x = gt(a.qvec).clone().transpose(), M = new r.Vector3(a.tvec[0], a.tvec[1], a.tvec[2]);
559
+ M.applyMatrix3(x).negate(), p.position.set(M.x, -M.y, -M.z);
560
+ const b = new r.Vector3(0, 0, 1).applyMatrix3(x);
561
+ b.set(b.x, -b.y, -b.z), p.lookAt(p.position.clone().add(b)), o.add(p);
562
+ });
563
+ return await Promise.allSettled(n), o;
564
+ }
565
+ function X(s) {
566
+ return s ? new r.Color(11845341) : new r.Color(6581890);
567
+ }
568
+ function wt(s, t, e) {
569
+ const i = e * 2, o = i * 2, n = e / 10, a = new r.PlaneGeometry(o, o);
570
+ a.rotateX(-Math.PI / 2);
571
+ const l = document.documentElement.dataset.theme === "dark", c = new r.ShaderMaterial({
572
+ transparent: !0,
573
+ depthWrite: !1,
574
+ side: r.DoubleSide,
575
+ uniforms: {
576
+ uColor: { value: X(l) },
577
+ uRadius: { value: i },
578
+ uCellSize: { value: n }
579
+ },
580
+ vertexShader: (
581
+ /* glsl */
582
+ `
583
+ varying vec2 vPosition;
584
+ void main() {
585
+ vPosition = position.xz;
586
+ gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
587
+ }
588
+ `
589
+ ),
590
+ fragmentShader: (
591
+ /* glsl */
592
+ `
593
+ uniform vec3 uColor;
594
+ uniform float uRadius;
595
+ uniform float uCellSize;
596
+ varying vec2 vPosition;
597
+
598
+ void main() {
599
+ vec2 coord = vPosition / uCellSize;
600
+ vec2 grid = abs(fract(coord - 0.5) - 0.5) / fwidth(coord);
601
+ float line = min(min(grid.x, grid.y), 1.0);
602
+ float gridAlpha = 1.0 - line;
603
+
604
+ float dist = length(vPosition);
605
+ float fade = 1.0 - smoothstep(uRadius * 0.6, uRadius, dist);
606
+
607
+ float alpha = gridAlpha * fade * 0.7;
608
+ if (alpha < 0.01) discard;
609
+ gl_FragColor = vec4(uColor, alpha);
610
+ }
611
+ `
612
+ )
613
+ }), h = new r.Mesh(a, c);
614
+ return h.position.set(s.x, t, s.z), h;
615
+ }
616
+ async function Pt(s, t, e, i) {
617
+ const o = new r.WebGLRenderer({ antialias: !0 });
618
+ o.setPixelRatio(window.devicePixelRatio), o.setSize(s.clientWidth, s.clientHeight);
619
+ const n = () => document.documentElement.dataset.theme === "dark" ? 855320 : 15265016;
620
+ o.setClearColor(n()), s.appendChild(o.domElement);
621
+ let a;
622
+ const l = new MutationObserver(() => {
623
+ o.setClearColor(n());
624
+ const m = document.documentElement.dataset.theme === "dark";
625
+ a.material.uniforms.uColor.value = X(m);
626
+ });
627
+ l.observe(document.documentElement, { attributes: !0, attributeFilter: ["data-theme"] });
628
+ const c = new r.PerspectiveCamera(
629
+ 60,
630
+ s.clientWidth / s.clientHeight,
631
+ 1e-3,
632
+ 1e4
633
+ );
634
+ c.position.set(0, 0, 5);
635
+ const h = new J(c, o.domElement);
636
+ h.enableDamping = !1;
637
+ const f = new r.Scene();
638
+ i?.("Building point cloud…");
639
+ const u = t.points3d.length > 0 ? ut(t.points3d) : null;
640
+ u && f.add(u), i?.("Building camera frustums…");
641
+ const E = ft(t.images, t.cameras);
642
+ f.add(E);
643
+ const A = new r.Group();
644
+ u && A.add(u.clone()), A.add(E.clone());
645
+ const C = new r.Box3().setFromObject(
646
+ u ?? E
647
+ ), p = new r.Vector3(), T = new r.Sphere();
648
+ C.getBoundingSphere(T), C.getCenter(p), a = wt(p, C.min.y, T.radius), f.add(a);
649
+ const x = bt(T.radius);
650
+ f.add(x), i?.("Loading image planes…");
651
+ const M = await Et(t.images, t.cameras, e);
652
+ f.add(M);
653
+ const b = () => {
654
+ const m = T.radius / Math.sin(c.fov * Math.PI / 360);
655
+ c.position.copy(p).add(new r.Vector3(0, 0, m * 1.5)), c.near = m * 1e-3, c.far = m * 100, c.updateProjectionMatrix(), h.target.copy(p), h.update();
656
+ };
657
+ b();
658
+ const R = T.radius * 5e-3;
659
+ u && (u.material.size = R);
660
+ let O;
661
+ const w = () => {
662
+ O = requestAnimationFrame(w), h.update(), o.render(f, c);
663
+ };
664
+ w();
665
+ const g = new ResizeObserver(() => {
666
+ const m = s.clientWidth, v = s.clientHeight;
667
+ o.setSize(m, v), c.aspect = m / v, c.updateProjectionMatrix();
668
+ });
669
+ return g.observe(s), {
670
+ objects: { pointCloud: u, frustums: E, axes: x, imagePlanes: M, grid: a },
671
+ canvas: o.domElement,
672
+ controls: h,
673
+ defaultPointSize: R,
674
+ resetView: b,
675
+ dispose: () => {
676
+ cancelAnimationFrame(O), g.disconnect(), l.disconnect(), o.dispose(), s.removeChild(o.domElement);
677
+ }
678
+ };
679
+ }
680
+ function Ct(s) {
681
+ return s >= 1e6 ? `${(s / 1e6).toFixed(1)}M` : s >= 1e3 ? `${(s / 1e3).toFixed(0)}k` : String(s);
682
+ }
683
+ function xt(s, t, e, i) {
684
+ const o = document.createElement("label");
685
+ o.className = "hud-toggle" + (e ? "" : " hud-toggle--disabled");
686
+ const n = document.createElement("input");
687
+ return n.type = "checkbox", n.checked = t, n.disabled = !e, e && n.addEventListener("change", () => i(n.checked)), o.append(n, document.createTextNode(" " + s)), o;
688
+ }
689
+ function Mt(s, t) {
690
+ const e = document.createElement("div");
691
+ e.className = "hud";
692
+ const i = document.createElement("div");
693
+ i.className = "hud-title", i.textContent = "COLMAP Viewer";
694
+ const o = document.createElement("div");
695
+ o.className = "hud-stats";
696
+ const n = document.createElement("div");
697
+ n.textContent = `📷 Cameras: ${t.cameraCount}`;
698
+ const a = document.createElement("div");
699
+ a.textContent = `⚫ Points: ${Ct(t.pointCount)}`, o.append(n, a);
700
+ const l = document.createElement("hr");
701
+ l.className = "hud-divider";
702
+ const c = document.createElement("div");
703
+ c.className = "hud-toggles";
704
+ const h = [
705
+ ["grid", "Grid", !0],
706
+ ["pointCloud", "Point Cloud", t.pointCount > 0],
707
+ ["frustums", "Camera Frustums", t.cameraCount > 0],
708
+ ["axes", "Axes", !0],
709
+ ["imagePlanes", "Image Planes", t.hasImagePlanes]
710
+ ];
711
+ for (const [w, g, D] of h) {
712
+ const m = t.objects[w], v = xt(g, m?.visible ?? !0, D, (L) => {
713
+ m && (m.visible = L);
714
+ });
715
+ c.appendChild(v);
716
+ }
717
+ const f = document.createElement("hr");
718
+ f.className = "hud-divider";
719
+ const u = document.createElement("div");
720
+ u.className = "hud-size-section", u.style.display = t.pointCount > 0 ? "" : "none";
721
+ const E = document.createElement("div");
722
+ E.style.cssText = "display:flex;justify-content:space-between;align-items:baseline";
723
+ const A = document.createElement("div");
724
+ A.className = "hud-size-label", A.textContent = "Point Size";
725
+ const C = document.createElement("div");
726
+ C.className = "hud-size-label", C.textContent = "1.0×", E.append(A, C);
727
+ const p = document.createElement("input");
728
+ p.type = "range", p.className = "hud-slider", p.min = "0.1", p.max = "5", p.step = "0.05", p.value = "1", p.addEventListener("input", () => {
729
+ const { pointCloud: w } = t.objects;
730
+ if (!w) return;
731
+ const g = parseFloat(p.value), D = w.material;
732
+ D.size = t.defaultPointSize * g, C.textContent = g.toFixed(1) + "×";
733
+ }), u.append(E, p);
734
+ const T = document.createElement("hr");
735
+ T.className = "hud-divider";
736
+ const x = document.createElement("button");
737
+ x.className = "hud-btn", x.textContent = "Reset View", x.addEventListener("click", t.onReset);
738
+ const M = [i, o, l, c, f, u, T, x];
739
+ if (t.showThemeToggle !== !1) {
740
+ const w = document.createElement("hr");
741
+ w.className = "hud-divider";
742
+ const g = document.createElement("button");
743
+ g.className = "hud-theme-btn";
744
+ const D = () => {
745
+ const m = document.documentElement.dataset.theme === "dark";
746
+ g.textContent = m ? "☀ Light Mode" : "◑ Dark Mode";
747
+ };
748
+ D(), g.addEventListener("click", () => {
749
+ const m = document.documentElement.dataset.theme === "dark" ? "light" : "dark";
750
+ document.documentElement.dataset.theme = m, localStorage.setItem("theme", m), D();
751
+ }), M.push(w, g);
752
+ }
753
+ e.append(...M), s.appendChild(e);
754
+ let b;
755
+ t.onUploadNew && (b = document.createElement("button"), b.className = "upload-new-btn", b.textContent = "⬆ Upload New", b.addEventListener("click", t.onUploadNew), s.appendChild(b));
756
+ let R;
757
+ t.showControlsHint !== !1 && (R = document.createElement("div"), R.className = "controls-hint", R.textContent = "Rotate: drag | Zoom: scroll | Pan: right-drag", s.appendChild(R));
758
+ let O;
759
+ if (t.showAxisLegend !== !1) {
760
+ O = document.createElement("div"), O.className = "axis-legend";
761
+ const w = [
762
+ ["X", "#ff2222", null],
763
+ ["Y ↑", "#00cc00", "COLMAP −Y ↓"],
764
+ ["Z", "#2266ff", "COLMAP −Z"]
765
+ ];
766
+ for (const [g, D, m] of w) {
767
+ const v = document.createElement("div");
768
+ v.className = "axis-legend-row";
769
+ const L = document.createElement("span");
770
+ if (L.className = "axis-legend-name", L.textContent = g, L.style.color = D, v.appendChild(L), m) {
771
+ const z = document.createElement("span");
772
+ z.className = "axis-legend-sub", z.textContent = m, v.appendChild(z);
773
+ }
774
+ O.appendChild(v);
775
+ }
776
+ s.appendChild(O);
777
+ }
778
+ return () => {
779
+ e.remove(), b?.remove(), R?.remove(), O?.remove();
780
+ };
781
+ }
782
+ class St {
783
+ container;
784
+ options;
785
+ scene = null;
786
+ disposeHud;
787
+ statusEl = null;
788
+ abortController = null;
789
+ constructor(t, e = {}) {
790
+ this.container = t, this.options = e;
791
+ }
792
+ async loadFromUrls(t) {
793
+ this.abortController?.abort();
794
+ const e = new AbortController();
795
+ this.abortController = e, this._setStatus("Fetching COLMAP files…");
796
+ try {
797
+ const [i, o, n] = await Promise.all([
798
+ U(t.cameras),
799
+ U(t.images),
800
+ t.points3d ? U(t.points3d) : Promise.resolve(void 0)
801
+ ]);
802
+ if (e.signal.aborted) return;
803
+ await this._render({ cameras: i, images: o, points3d: n }, e.signal);
804
+ } catch (i) {
805
+ if (e.signal.aborted) return;
806
+ this._setError(i instanceof Error ? i.message : String(i));
807
+ }
808
+ }
809
+ async loadFromBuffers(t) {
810
+ this.abortController?.abort();
811
+ const e = new AbortController();
812
+ this.abortController = e, await this._render(t, e.signal);
813
+ }
814
+ dispose() {
815
+ this.abortController?.abort(), this.abortController = null, this._teardown();
816
+ }
817
+ async _render(t, e) {
818
+ this._teardown(), this._setStatus("Parsing…");
819
+ try {
820
+ const i = await V(t);
821
+ if (e.aborted) return;
822
+ this._setStatus("Building scene…");
823
+ const o = await Pt(
824
+ this.container,
825
+ i,
826
+ /* @__PURE__ */ new Map(),
827
+ (n) => this._setStatus(n)
828
+ );
829
+ if (e.aborted) {
830
+ o.dispose();
831
+ return;
832
+ }
833
+ this.scene = o, this._clearStatus(), this.disposeHud = Mt(this.container, {
834
+ cameraCount: i.images.length,
835
+ pointCount: i.points3d.length,
836
+ hasImagePlanes: !1,
837
+ objects: o.objects,
838
+ defaultPointSize: o.defaultPointSize,
839
+ onReset: o.resetView,
840
+ showThemeToggle: this.options.showThemeToggle,
841
+ showAxisLegend: this.options.showAxisLegend,
842
+ showControlsHint: this.options.showControlsHint
843
+ });
844
+ } catch (i) {
845
+ if (e.aborted) return;
846
+ this._setError(i instanceof Error ? i.message : String(i));
847
+ }
848
+ }
849
+ _teardown() {
850
+ this.disposeHud?.(), this.disposeHud = void 0, this.scene?.dispose(), this.scene = null, this._clearStatus();
851
+ }
852
+ _setStatus(t) {
853
+ this.statusEl || (this.statusEl = document.createElement("div"), this.statusEl.className = "colmap-renderer-status", this.container.appendChild(this.statusEl)), this.statusEl.textContent = t;
854
+ }
855
+ _clearStatus() {
856
+ this.statusEl?.remove(), this.statusEl = null;
857
+ }
858
+ _setError(t) {
859
+ this._teardown(), this.statusEl || (this.statusEl = document.createElement("div"), this.container.appendChild(this.statusEl)), this.statusEl.className = "colmap-renderer-error", this.statusEl.textContent = `Error: ${t}`;
860
+ }
861
+ }
862
+ async function U(s) {
863
+ const t = await fetch(s);
864
+ if (!t.ok) throw new Error(`fetch failed: ${t.status} ${t.statusText}`);
865
+ return t.arrayBuffer();
866
+ }
867
+ class Tt extends HTMLElement {
868
+ static observedAttributes = [
869
+ "cameras-url",
870
+ "images-url",
871
+ "points3d-url",
872
+ "show-theme-toggle",
873
+ "show-axis-legend",
874
+ "show-controls-hint"
875
+ ];
876
+ renderer = null;
877
+ connectedCallback() {
878
+ getComputedStyle(this).position === "static" && (this.style.position = "relative"), this.renderer = new St(this, this._readOptions()), this._maybeLoad();
879
+ }
880
+ disconnectedCallback() {
881
+ this.renderer?.dispose(), this.renderer = null;
882
+ }
883
+ attributeChangedCallback() {
884
+ this.renderer && this._maybeLoad();
885
+ }
886
+ _readOptions() {
887
+ return {
888
+ showThemeToggle: this._boolAttr("show-theme-toggle"),
889
+ showAxisLegend: this._boolAttr("show-axis-legend"),
890
+ showControlsHint: this._boolAttr("show-controls-hint")
891
+ };
892
+ }
893
+ _boolAttr(t) {
894
+ const e = this.getAttribute(t);
895
+ if (e !== null)
896
+ return e !== "false";
897
+ }
898
+ _maybeLoad() {
899
+ const t = this.getAttribute("cameras-url"), e = this.getAttribute("images-url");
900
+ !t || !e || this.renderer?.loadFromUrls({
901
+ cameras: t,
902
+ images: e,
903
+ points3d: this.getAttribute("points3d-url") ?? void 0
904
+ });
905
+ }
906
+ }
907
+ customElements.get("colmap-viewer") || customElements.define("colmap-viewer", Tt);
908
+ async function At(s, t) {
909
+ t?.("Parsing cameras…");
910
+ const e = await V({
911
+ cameras: s.cameras,
912
+ images: s.images,
913
+ points3d: s.points3d
914
+ });
915
+ return t?.(`Parsed ${e.cameras.length} cameras, ${e.points3d.length} points`), e;
916
+ }
917
+ export {
918
+ St as ColmapRenderer,
919
+ Tt as ColmapViewerElement,
920
+ Pt as buildScene,
921
+ Mt as createHud,
922
+ At as parse
923
+ };
@@ -0,0 +1,10 @@
1
+ export declare class ColmapViewerElement extends HTMLElement {
2
+ static observedAttributes: string[];
3
+ private renderer;
4
+ connectedCallback(): void;
5
+ disconnectedCallback(): void;
6
+ attributeChangedCallback(): void;
7
+ private _readOptions;
8
+ private _boolAttr;
9
+ private _maybeLoad;
10
+ }
@@ -0,0 +1,8 @@
1
+ export { ColmapViewerElement } from './element';
2
+ export { ColmapRenderer } from './renderer';
3
+ export type { ColmapUrls, ColmapBuffers, ColmapRendererOptions } from './renderer';
4
+ export { parse } from './parser/index';
5
+ export type { ColmapFiles } from './parser/index';
6
+ export { buildScene } from './scene/index';
7
+ export { createHud } from './ui/hud';
8
+ export type { HudOptions, LayerKey } from './ui/hud';
@@ -0,0 +1,9 @@
1
+ import { ColmapResult } from '@thethingteam/colmap-wasm';
2
+ export type { ColmapResult };
3
+ export interface ColmapFiles {
4
+ cameras: ArrayBuffer;
5
+ images: ArrayBuffer;
6
+ points3d?: ArrayBuffer;
7
+ imageFiles: Map<string, File>;
8
+ }
9
+ export declare function parse(files: ColmapFiles, onProgress?: (message: string) => void): Promise<ColmapResult>;
@@ -0,0 +1,32 @@
1
+ export interface ColmapUrls {
2
+ cameras: string;
3
+ images: string;
4
+ points3d?: string;
5
+ }
6
+ export interface ColmapBuffers {
7
+ cameras: ArrayBuffer;
8
+ images: ArrayBuffer;
9
+ points3d?: ArrayBuffer;
10
+ }
11
+ export interface ColmapRendererOptions {
12
+ showThemeToggle?: boolean;
13
+ showAxisLegend?: boolean;
14
+ showControlsHint?: boolean;
15
+ }
16
+ export declare class ColmapRenderer {
17
+ private container;
18
+ private options;
19
+ private scene;
20
+ private disposeHud?;
21
+ private statusEl;
22
+ private abortController;
23
+ constructor(container: HTMLElement, options?: ColmapRendererOptions);
24
+ loadFromUrls(urls: ColmapUrls): Promise<void>;
25
+ loadFromBuffers(data: ColmapBuffers): Promise<void>;
26
+ dispose(): void;
27
+ private _render;
28
+ private _teardown;
29
+ private _setStatus;
30
+ private _clearStatus;
31
+ private _setError;
32
+ }
@@ -0,0 +1,2 @@
1
+ import * as THREE from 'three';
2
+ export declare function createAxes(sceneRadius: number): THREE.AxesHelper;
@@ -0,0 +1,3 @@
1
+ import { Image, Camera } from '@thethingteam/colmap-wasm';
2
+ import * as THREE from 'three';
3
+ export declare function createCameraFrustums(images: Image[], cameras: Camera[]): THREE.Group;
@@ -0,0 +1,3 @@
1
+ import * as THREE from 'three';
2
+ export declare function getGridColor(isDark: boolean): THREE.Color;
3
+ export declare function createGrid(center: THREE.Vector3, minY: number, sceneRadius: number): THREE.Mesh;
@@ -0,0 +1,3 @@
1
+ import { Image, Camera } from '@thethingteam/colmap-wasm';
2
+ import * as THREE from 'three';
3
+ export declare function createImagePlanes(images: Image[], cameras: Camera[], imageFiles: Map<string, File>): Promise<THREE.Group>;
@@ -0,0 +1,19 @@
1
+ import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
2
+ import { ColmapResult } from '../parser/index';
3
+ import * as THREE from 'three';
4
+ export interface SceneObjects {
5
+ pointCloud: THREE.Points | null;
6
+ frustums: THREE.Group;
7
+ axes: THREE.AxesHelper;
8
+ imagePlanes: THREE.Group;
9
+ grid: THREE.Mesh;
10
+ }
11
+ export interface SceneHandle {
12
+ objects: SceneObjects;
13
+ canvas: HTMLCanvasElement;
14
+ controls: OrbitControls;
15
+ defaultPointSize: number;
16
+ resetView: () => void;
17
+ dispose: () => void;
18
+ }
19
+ export declare function buildScene(container: HTMLElement, result: ColmapResult, imageFiles: Map<string, File>, onProgress?: (msg: string) => void): Promise<SceneHandle>;
@@ -0,0 +1,3 @@
1
+ import { Point3D } from '@thethingteam/colmap-wasm';
2
+ import * as THREE from 'three';
3
+ export declare function createPointCloud(points: Point3D[]): THREE.Points;
@@ -0,0 +1,15 @@
1
+ import { SceneObjects } from '../scene/index';
2
+ export type LayerKey = 'pointCloud' | 'frustums' | 'axes' | 'imagePlanes' | 'grid';
3
+ export interface HudOptions {
4
+ cameraCount: number;
5
+ pointCount: number;
6
+ hasImagePlanes: boolean;
7
+ objects: SceneObjects;
8
+ defaultPointSize: number;
9
+ onReset: () => void;
10
+ onUploadNew?: () => void;
11
+ showThemeToggle?: boolean;
12
+ showAxisLegend?: boolean;
13
+ showControlsHint?: boolean;
14
+ }
15
+ export declare function createHud(container: HTMLElement, opts: HudOptions): () => void;
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@thethingteam/colmap-viewer",
3
+ "version": "0.1.0",
4
+ "description": "3D viewer for COLMAP reconstruction results",
5
+ "type": "module",
6
+ "main": "./dist/colmap-viewer.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "default": "./dist/colmap-viewer.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist"
16
+ ],
17
+ "license": "MIT",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/thethingteam/colmap-viewer.git"
21
+ },
22
+ "keywords": [
23
+ "colmap",
24
+ "3d",
25
+ "viewer",
26
+ "sfm",
27
+ "webgl",
28
+ "three"
29
+ ],
30
+ "publishConfig": {
31
+ "access": "public"
32
+ },
33
+ "peerDependencies": {
34
+ "@thethingteam/colmap-wasm": "*",
35
+ "three": ">=0.170.0"
36
+ },
37
+ "devDependencies": {
38
+ "@thethingteam/colmap-wasm": "*",
39
+ "@types/three": "^0.177.0",
40
+ "three": "^0.177.0",
41
+ "typescript": "^5.8.3",
42
+ "vite": "^6.3.5",
43
+ "vite-plugin-dts": "^5.0.1"
44
+ },
45
+ "scripts": {
46
+ "dev": "vite",
47
+ "build": "tsc && vite build",
48
+ "build:lib": "vite build --config vite.lib.config.ts",
49
+ "preview": "vite preview",
50
+ "typecheck": "tsc --noEmit"
51
+ }
52
+ }