anymap-ts 0.11.0 → 0.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/anymap_ts/static/potree.js +4424 -117
- package/package.json +4 -2
- package/src/potree/index.ts +411 -187
- package/src/types/potree.ts +0 -21
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "anymap-ts",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.1",
|
|
4
4
|
"description": "TypeScript frontend for anymap-ts interactive maps",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -61,7 +61,9 @@
|
|
|
61
61
|
"maplibre-gl-lidar": "^0.11.1",
|
|
62
62
|
"ol": "^10.8.0",
|
|
63
63
|
"pmtiles": "^4.4.0",
|
|
64
|
-
"
|
|
64
|
+
"potree-core": "^2.0.12",
|
|
65
|
+
"shpjs": "^6.2.0",
|
|
66
|
+
"three": "^0.154.0"
|
|
65
67
|
},
|
|
66
68
|
"devDependencies": {
|
|
67
69
|
"@anywidget/types": "^0.2.0",
|
package/src/potree/index.ts
CHANGED
|
@@ -1,12 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Potree point cloud viewer widget entry point.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* For full interactivity, use the to_html() export method.
|
|
4
|
+
* Uses potree-core (npm) + Three.js for bundled point cloud rendering.
|
|
5
|
+
* No CDN loading required — everything is bundled by esbuild.
|
|
7
6
|
*/
|
|
8
7
|
|
|
9
8
|
import type { AnyModel } from '@anywidget/types';
|
|
9
|
+
import {
|
|
10
|
+
Scene,
|
|
11
|
+
PerspectiveCamera,
|
|
12
|
+
WebGLRenderer,
|
|
13
|
+
Color,
|
|
14
|
+
Vector3,
|
|
15
|
+
Box3,
|
|
16
|
+
AmbientLight,
|
|
17
|
+
Euler,
|
|
18
|
+
} from 'three';
|
|
19
|
+
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
|
|
20
|
+
import { Potree, PointCloudOctree, PotreeRenderer } from 'potree-core';
|
|
10
21
|
|
|
11
22
|
interface PotreeModel extends AnyModel {
|
|
12
23
|
get(key: 'width'): string;
|
|
@@ -30,204 +41,417 @@ interface PotreeModel extends AnyModel {
|
|
|
30
41
|
}
|
|
31
42
|
|
|
32
43
|
/**
|
|
33
|
-
*
|
|
44
|
+
* Potree point cloud viewer widget using potree-core + Three.js.
|
|
34
45
|
*/
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
46
|
+
class PotreeWidget {
|
|
47
|
+
private model: PotreeModel;
|
|
48
|
+
private el: HTMLElement;
|
|
49
|
+
private container: HTMLDivElement | null = null;
|
|
50
|
+
private canvas: HTMLCanvasElement | null = null;
|
|
40
51
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
48
|
-
background: linear-gradient(135deg, #0f0f23 0%, #1a1a3e 100%);
|
|
49
|
-
color: #fff;
|
|
50
|
-
padding: 24px;
|
|
51
|
-
border-radius: 8px;
|
|
52
|
-
box-shadow: 0 4px 20px rgba(0,0,0,0.4);
|
|
53
|
-
`;
|
|
54
|
-
|
|
55
|
-
const pointBudget = model.get('point_budget') || 1000000;
|
|
56
|
-
const pointSize = model.get('point_size') || 1.0;
|
|
57
|
-
const fov = model.get('fov') || 60;
|
|
58
|
-
const background = model.get('background') || '#000000';
|
|
59
|
-
const edlEnabled = model.get('edl_enabled') !== false;
|
|
60
|
-
const pointClouds = model.get('point_clouds') || {};
|
|
61
|
-
const cameraPosition = model.get('camera_position') || [0, 0, 100];
|
|
62
|
-
const cameraTarget = model.get('camera_target') || [0, 0, 0];
|
|
63
|
-
|
|
64
|
-
const pointCloudCount = Object.keys(pointClouds).length;
|
|
65
|
-
const pointCloudItems = Object.entries(pointClouds).map(([id, pc]: [string, any]) => {
|
|
66
|
-
return `
|
|
67
|
-
<div style="display: flex; justify-content: space-between; align-items: center; padding: 10px 0; border-bottom: 1px solid rgba(255,255,255,0.1);">
|
|
68
|
-
<div>
|
|
69
|
-
<span style="color: #64b5f6;">${pc.name || id}</span>
|
|
70
|
-
<div style="font-size: 11px; color: #666; margin-top: 2px;">${pc.url ? pc.url.substring(0, 40) + '...' : 'No URL'}</div>
|
|
71
|
-
</div>
|
|
72
|
-
<span style="color: ${pc.visible ? '#81c784' : '#e57373'}; font-size: 12px;">
|
|
73
|
-
${pc.visible ? '● Visible' : '○ Hidden'}
|
|
74
|
-
</span>
|
|
75
|
-
</div>
|
|
76
|
-
`;
|
|
77
|
-
}).join('');
|
|
78
|
-
|
|
79
|
-
container.innerHTML = `
|
|
80
|
-
<div style="display: flex; align-items: center; margin-bottom: 20px;">
|
|
81
|
-
<div style="width: 48px; height: 48px; background: linear-gradient(135deg, #64b5f6, #42a5f5); border-radius: 12px; display: flex; align-items: center; justify-content: center; margin-right: 16px;">
|
|
82
|
-
<svg width="28" height="28" viewBox="0 0 24 24" fill="white">
|
|
83
|
-
<circle cx="4" cy="4" r="2"/>
|
|
84
|
-
<circle cx="12" cy="4" r="2"/>
|
|
85
|
-
<circle cx="20" cy="4" r="2"/>
|
|
86
|
-
<circle cx="4" cy="12" r="2"/>
|
|
87
|
-
<circle cx="12" cy="12" r="2"/>
|
|
88
|
-
<circle cx="20" cy="12" r="2"/>
|
|
89
|
-
<circle cx="4" cy="20" r="2"/>
|
|
90
|
-
<circle cx="12" cy="20" r="2"/>
|
|
91
|
-
<circle cx="20" cy="20" r="2"/>
|
|
92
|
-
</svg>
|
|
93
|
-
</div>
|
|
94
|
-
<div>
|
|
95
|
-
<h2 style="margin: 0; font-size: 20px; font-weight: 600;">Potree Viewer</h2>
|
|
96
|
-
<p style="margin: 4px 0 0 0; font-size: 13px; color: #888;">WebGL Point Cloud Visualization</p>
|
|
97
|
-
</div>
|
|
98
|
-
</div>
|
|
52
|
+
private scene: Scene | null = null;
|
|
53
|
+
private camera: PerspectiveCamera | null = null;
|
|
54
|
+
private renderer: WebGLRenderer | null = null;
|
|
55
|
+
private controls: OrbitControls | null = null;
|
|
56
|
+
private potree: Potree | null = null;
|
|
57
|
+
private potreeRenderer: PotreeRenderer | null = null;
|
|
99
58
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
<div style="background: rgba(255,255,255,0.05); padding: 14px; border-radius: 8px; text-align: center;">
|
|
106
|
-
<div style="font-size: 20px; font-weight: bold; color: #81c784;">${formatNumber(pointBudget)}</div>
|
|
107
|
-
<div style="font-size: 11px; color: #888; margin-top: 4px;">Point Budget</div>
|
|
108
|
-
</div>
|
|
109
|
-
<div style="background: rgba(255,255,255,0.05); padding: 14px; border-radius: 8px; text-align: center;">
|
|
110
|
-
<div style="font-size: 20px; font-weight: bold; color: #ffd54f;">${pointSize}</div>
|
|
111
|
-
<div style="font-size: 11px; color: #888; margin-top: 4px;">Point Size</div>
|
|
112
|
-
</div>
|
|
113
|
-
<div style="background: rgba(255,255,255,0.05); padding: 14px; border-radius: 8px; text-align: center;">
|
|
114
|
-
<div style="font-size: 20px; font-weight: bold; color: #ce93d8;">${fov}°</div>
|
|
115
|
-
<div style="font-size: 11px; color: #888; margin-top: 4px;">FOV</div>
|
|
116
|
-
</div>
|
|
117
|
-
</div>
|
|
118
|
-
|
|
119
|
-
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-bottom: 20px;">
|
|
120
|
-
<div>
|
|
121
|
-
<h3 style="margin: 0 0 12px 0; font-size: 14px; color: #888; text-transform: uppercase; letter-spacing: 1px;">Camera</h3>
|
|
122
|
-
<div style="background: rgba(255,255,255,0.05); padding: 16px; border-radius: 8px;">
|
|
123
|
-
<div style="display: flex; justify-content: space-between; margin-bottom: 8px;">
|
|
124
|
-
<span style="color: #888;">Position</span>
|
|
125
|
-
<span style="color: #fff; font-family: monospace; font-size: 12px;">
|
|
126
|
-
[${cameraPosition.map(v => v.toFixed(1)).join(', ')}]
|
|
127
|
-
</span>
|
|
128
|
-
</div>
|
|
129
|
-
<div style="display: flex; justify-content: space-between;">
|
|
130
|
-
<span style="color: #888;">Target</span>
|
|
131
|
-
<span style="color: #fff; font-family: monospace; font-size: 12px;">
|
|
132
|
-
[${cameraTarget.map(v => v.toFixed(1)).join(', ')}]
|
|
133
|
-
</span>
|
|
134
|
-
</div>
|
|
135
|
-
</div>
|
|
136
|
-
</div>
|
|
137
|
-
<div>
|
|
138
|
-
<h3 style="margin: 0 0 12px 0; font-size: 14px; color: #888; text-transform: uppercase; letter-spacing: 1px;">Rendering</h3>
|
|
139
|
-
<div style="background: rgba(255,255,255,0.05); padding: 16px; border-radius: 8px;">
|
|
140
|
-
<div style="display: flex; justify-content: space-between; margin-bottom: 8px;">
|
|
141
|
-
<span style="color: #888;">EDL</span>
|
|
142
|
-
<span style="color: ${edlEnabled ? '#81c784' : '#e57373'};">${edlEnabled ? 'Enabled' : 'Disabled'}</span>
|
|
143
|
-
</div>
|
|
144
|
-
<div style="display: flex; justify-content: space-between; align-items: center;">
|
|
145
|
-
<span style="color: #888;">Background</span>
|
|
146
|
-
<div style="display: flex; align-items: center; gap: 8px;">
|
|
147
|
-
<div style="width: 20px; height: 20px; background: ${background}; border-radius: 4px; border: 1px solid rgba(255,255,255,0.2);"></div>
|
|
148
|
-
<span style="color: #fff; font-family: monospace; font-size: 12px;">${background}</span>
|
|
149
|
-
</div>
|
|
150
|
-
</div>
|
|
151
|
-
</div>
|
|
152
|
-
</div>
|
|
153
|
-
</div>
|
|
59
|
+
private pointClouds: Map<string, PointCloudOctree> = new Map();
|
|
60
|
+
private pointCloudList: PointCloudOctree[] = [];
|
|
61
|
+
private lastProcessedCallId: number = 0;
|
|
62
|
+
private animationId: number = 0;
|
|
63
|
+
private resizeObserver: ResizeObserver | null = null;
|
|
154
64
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
65
|
+
constructor(model: PotreeModel, el: HTMLElement) {
|
|
66
|
+
this.model = model;
|
|
67
|
+
this.el = el;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async initialize(): Promise<void> {
|
|
71
|
+
this.el.style.width = '100%';
|
|
72
|
+
this.el.style.display = 'block';
|
|
73
|
+
|
|
74
|
+
// Create container
|
|
75
|
+
this.container = document.createElement('div');
|
|
76
|
+
this.container.style.width = this.model.get('width') || '100%';
|
|
77
|
+
this.container.style.height = this.model.get('height') || '600px';
|
|
78
|
+
this.container.style.position = 'relative';
|
|
79
|
+
this.container.style.minWidth = '200px';
|
|
80
|
+
this.el.appendChild(this.container);
|
|
81
|
+
|
|
82
|
+
// Create canvas
|
|
83
|
+
this.canvas = document.createElement('canvas');
|
|
84
|
+
this.canvas.style.width = '100%';
|
|
85
|
+
this.canvas.style.height = '100%';
|
|
86
|
+
this.canvas.style.display = 'block';
|
|
87
|
+
this.container.appendChild(this.canvas);
|
|
88
|
+
|
|
89
|
+
// Three.js setup
|
|
90
|
+
const fov = this.model.get('fov') || 60;
|
|
91
|
+
const bg = this.model.get('background') || '#000000';
|
|
92
|
+
|
|
93
|
+
this.scene = new Scene();
|
|
94
|
+
this.scene.background = new Color(bg);
|
|
95
|
+
this.scene.add(new AmbientLight(0xffffff));
|
|
96
|
+
|
|
97
|
+
const rect = this.container.getBoundingClientRect();
|
|
98
|
+
const width = rect.width || 800;
|
|
99
|
+
const height = rect.height || 600;
|
|
100
|
+
|
|
101
|
+
this.camera = new PerspectiveCamera(fov, width / height, 0.1, 10000);
|
|
102
|
+
const camPos = this.model.get('camera_position') || [0, 0, 100];
|
|
103
|
+
this.camera.position.set(camPos[0], camPos[1], camPos[2]);
|
|
104
|
+
|
|
105
|
+
this.renderer = new WebGLRenderer({
|
|
106
|
+
canvas: this.canvas,
|
|
107
|
+
alpha: true,
|
|
108
|
+
antialias: true,
|
|
109
|
+
precision: 'highp',
|
|
110
|
+
powerPreference: 'high-performance',
|
|
111
|
+
});
|
|
112
|
+
this.renderer.setSize(width, height);
|
|
113
|
+
this.renderer.setPixelRatio(window.devicePixelRatio);
|
|
114
|
+
|
|
115
|
+
// OrbitControls for mouse navigation
|
|
116
|
+
this.controls = new OrbitControls(this.camera, this.canvas);
|
|
117
|
+
const camTarget = this.model.get('camera_target') || [0, 0, 0];
|
|
118
|
+
this.controls.target.set(camTarget[0], camTarget[1], camTarget[2]);
|
|
119
|
+
this.controls.update();
|
|
120
|
+
|
|
121
|
+
// Potree setup
|
|
122
|
+
this.potree = new Potree();
|
|
123
|
+
this.potree.pointBudget = this.model.get('point_budget') || 1000000;
|
|
124
|
+
|
|
125
|
+
const edlEnabled = this.model.get('edl_enabled') !== false;
|
|
126
|
+
const edlRadius = this.model.get('edl_radius') || 1.4;
|
|
127
|
+
const edlStrength = this.model.get('edl_strength') || 0.4;
|
|
128
|
+
|
|
129
|
+
this.potreeRenderer = new PotreeRenderer({
|
|
130
|
+
edl: {
|
|
131
|
+
enabled: edlEnabled,
|
|
132
|
+
strength: edlStrength,
|
|
133
|
+
radius: edlRadius,
|
|
134
|
+
opacity: 1.0,
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// Load point clouds from state
|
|
139
|
+
const pointCloudsState = this.model.get('point_clouds') || {};
|
|
140
|
+
for (const [name, config] of Object.entries(pointCloudsState)) {
|
|
141
|
+
await this.loadPointCloud(config.url, name, config.material, config.visible);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Process pending JS calls
|
|
145
|
+
this.processJsCalls();
|
|
146
|
+
|
|
147
|
+
// Listen for model changes
|
|
148
|
+
this.model.on('change:_js_calls', () => this.processJsCalls());
|
|
149
|
+
|
|
150
|
+
// Resize observer
|
|
151
|
+
this.resizeObserver = new ResizeObserver(() => this.onResize());
|
|
152
|
+
this.resizeObserver.observe(this.container);
|
|
153
|
+
|
|
154
|
+
// Start animation loop
|
|
155
|
+
this.loop();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private onResize(): void {
|
|
159
|
+
if (!this.container || !this.renderer || !this.camera) return;
|
|
160
|
+
const rect = this.container.getBoundingClientRect();
|
|
161
|
+
if (rect.width === 0 || rect.height === 0) return;
|
|
162
|
+
this.renderer.setSize(rect.width, rect.height);
|
|
163
|
+
this.camera.aspect = rect.width / rect.height;
|
|
164
|
+
this.camera.updateProjectionMatrix();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
private loop = (): void => {
|
|
168
|
+
this.animationId = requestAnimationFrame(this.loop);
|
|
169
|
+
if (!this.potree || !this.camera || !this.renderer || !this.scene || !this.controls || !this.potreeRenderer) return;
|
|
170
|
+
|
|
171
|
+
this.potree.updatePointClouds(this.pointCloudList, this.camera, this.renderer);
|
|
172
|
+
this.controls.update();
|
|
173
|
+
this.potreeRenderer.render({
|
|
174
|
+
renderer: this.renderer,
|
|
175
|
+
scene: this.scene,
|
|
176
|
+
camera: this.camera,
|
|
177
|
+
pointClouds: this.pointCloudList,
|
|
178
|
+
});
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
private processJsCalls(): void {
|
|
182
|
+
const jsCalls = this.model.get('_js_calls') || [];
|
|
183
|
+
for (const call of jsCalls) {
|
|
184
|
+
if (call.id > this.lastProcessedCallId) {
|
|
185
|
+
this.executeMethod(call);
|
|
186
|
+
this.lastProcessedCallId = call.id;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
private executeMethod(call: { method: string; args: unknown[]; kwargs: Record<string, unknown> }): void {
|
|
192
|
+
const { method, args, kwargs } = call;
|
|
193
|
+
const handler = (this as any)[`handle_${method}`];
|
|
194
|
+
if (handler) {
|
|
195
|
+
try {
|
|
196
|
+
handler.call(this, args, kwargs);
|
|
197
|
+
} catch (error) {
|
|
198
|
+
console.error(`Error executing method ${method}:`, error);
|
|
199
|
+
}
|
|
200
|
+
} else {
|
|
201
|
+
console.warn(`Unknown Potree method: ${method}`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ---------------------------------------------------------------------------
|
|
206
|
+
// Point Cloud Methods
|
|
207
|
+
// ---------------------------------------------------------------------------
|
|
208
|
+
|
|
209
|
+
private async loadPointCloud(
|
|
210
|
+
url: string,
|
|
211
|
+
name: string,
|
|
212
|
+
material?: Record<string, unknown>,
|
|
213
|
+
visible?: boolean
|
|
214
|
+
): Promise<void> {
|
|
215
|
+
if (!this.potree || !this.scene) return;
|
|
176
216
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
`;
|
|
217
|
+
try {
|
|
218
|
+
// Split URL into baseUrl + filename for potree-core API
|
|
219
|
+
const lastSlash = url.lastIndexOf('/');
|
|
220
|
+
const baseUrl = url.substring(0, lastSlash + 1);
|
|
221
|
+
const filename = url.substring(lastSlash + 1);
|
|
183
222
|
|
|
184
|
-
|
|
223
|
+
const pco = await this.potree.loadPointCloud(filename, baseUrl);
|
|
224
|
+
|
|
225
|
+
// Point clouds typically need rotation to align Y-up to Z-up
|
|
226
|
+
pco.rotation.copy(new Euler(-Math.PI / 2, 0, 0));
|
|
227
|
+
|
|
228
|
+
// Apply material settings
|
|
229
|
+
pco.material.inputColorEncoding = 1;
|
|
230
|
+
pco.material.outputColorEncoding = 1;
|
|
231
|
+
|
|
232
|
+
if (material) {
|
|
233
|
+
if (material.size !== undefined) pco.material.size = material.size as number;
|
|
234
|
+
if (material.pointSizeType) {
|
|
235
|
+
const types: Record<string, number> = { fixed: 0, attenuated: 1, adaptive: 2 };
|
|
236
|
+
pco.material.pointSizeType = types[material.pointSizeType as string] ?? 2;
|
|
237
|
+
}
|
|
238
|
+
if (material.shape) {
|
|
239
|
+
const shapes: Record<string, number> = { square: 0, circle: 1, paraboloid: 2 };
|
|
240
|
+
pco.material.shape = shapes[material.shape as string] ?? 1;
|
|
241
|
+
}
|
|
242
|
+
} else {
|
|
243
|
+
pco.material.size = this.model.get('point_size') || 1.0;
|
|
244
|
+
pco.material.shape = 1; // circle
|
|
245
|
+
pco.material.pointSizeType = 2; // adaptive
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
pco.visible = visible !== false;
|
|
249
|
+
|
|
250
|
+
this.scene.add(pco);
|
|
251
|
+
this.pointClouds.set(name, pco);
|
|
252
|
+
this.pointCloudList = Array.from(this.pointClouds.values());
|
|
253
|
+
|
|
254
|
+
// Fit camera to show the point cloud
|
|
255
|
+
this.fitToPointClouds();
|
|
256
|
+
} catch (error) {
|
|
257
|
+
console.error('Error loading point cloud:', error);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
private fitToPointClouds(): void {
|
|
262
|
+
if (!this.camera || !this.controls || this.pointCloudList.length === 0) return;
|
|
263
|
+
|
|
264
|
+
const box = new Box3();
|
|
265
|
+
for (const pco of this.pointCloudList) {
|
|
266
|
+
if (pco.pcoGeometry?.boundingBox) {
|
|
267
|
+
const pcBox = pco.pcoGeometry.boundingBox.clone();
|
|
268
|
+
pcBox.applyMatrix4(pco.matrixWorld);
|
|
269
|
+
box.union(pcBox);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (box.isEmpty()) return;
|
|
274
|
+
|
|
275
|
+
const center = box.getCenter(new Vector3());
|
|
276
|
+
const size = box.getSize(new Vector3());
|
|
277
|
+
const maxDim = Math.max(size.x, size.y, size.z);
|
|
278
|
+
|
|
279
|
+
this.camera.position.copy(center).add(new Vector3(0, -maxDim * 1.5, maxDim * 0.8));
|
|
280
|
+
this.controls.target.copy(center);
|
|
281
|
+
this.controls.update();
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
handle_loadPointCloud(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
285
|
+
this.loadPointCloud(
|
|
286
|
+
kwargs.url as string,
|
|
287
|
+
kwargs.name as string,
|
|
288
|
+
kwargs.material as Record<string, unknown>,
|
|
289
|
+
kwargs.visible as boolean
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
handle_removePointCloud(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
294
|
+
const name = kwargs.name as string;
|
|
295
|
+
const pco = this.pointClouds.get(name);
|
|
296
|
+
if (pco && this.scene) {
|
|
297
|
+
this.scene.remove(pco);
|
|
298
|
+
pco.dispose();
|
|
299
|
+
this.pointClouds.delete(name);
|
|
300
|
+
this.pointCloudList = Array.from(this.pointClouds.values());
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
handle_setPointCloudVisibility(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
305
|
+
const pco = this.pointClouds.get(kwargs.name as string);
|
|
306
|
+
if (pco) {
|
|
307
|
+
pco.visible = kwargs.visible as boolean;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// ---------------------------------------------------------------------------
|
|
312
|
+
// Camera Methods
|
|
313
|
+
// ---------------------------------------------------------------------------
|
|
314
|
+
|
|
315
|
+
handle_setCameraPosition(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
316
|
+
if (!this.camera) return;
|
|
317
|
+
this.camera.position.set(
|
|
318
|
+
kwargs.x as number,
|
|
319
|
+
kwargs.y as number,
|
|
320
|
+
kwargs.z as number
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
handle_setCameraTarget(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
325
|
+
if (!this.controls) return;
|
|
326
|
+
this.controls.target.set(
|
|
327
|
+
kwargs.x as number,
|
|
328
|
+
kwargs.y as number,
|
|
329
|
+
kwargs.z as number
|
|
330
|
+
);
|
|
331
|
+
this.controls.update();
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
handle_flyToPointCloud(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
335
|
+
const name = kwargs.name as string;
|
|
336
|
+
if (name) {
|
|
337
|
+
const pco = this.pointClouds.get(name);
|
|
338
|
+
if (pco && this.camera && this.controls && pco.pcoGeometry?.boundingBox) {
|
|
339
|
+
const box = pco.pcoGeometry.boundingBox.clone();
|
|
340
|
+
box.applyMatrix4(pco.matrixWorld);
|
|
341
|
+
const center = box.getCenter(new Vector3());
|
|
342
|
+
const size = box.getSize(new Vector3());
|
|
343
|
+
const maxDim = Math.max(size.x, size.y, size.z);
|
|
344
|
+
this.camera.position.copy(center).add(new Vector3(0, -maxDim * 1.5, maxDim * 0.8));
|
|
345
|
+
this.controls.target.copy(center);
|
|
346
|
+
this.controls.update();
|
|
347
|
+
}
|
|
348
|
+
} else {
|
|
349
|
+
this.fitToPointClouds();
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
handle_resetCamera(): void {
|
|
354
|
+
this.fitToPointClouds();
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// ---------------------------------------------------------------------------
|
|
358
|
+
// Visualization Settings
|
|
359
|
+
// ---------------------------------------------------------------------------
|
|
360
|
+
|
|
361
|
+
handle_setPointBudget(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
362
|
+
if (!this.potree) return;
|
|
363
|
+
this.potree.pointBudget = kwargs.budget as number;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
handle_setPointSize(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
367
|
+
const size = kwargs.size as number;
|
|
368
|
+
this.pointClouds.forEach(pco => {
|
|
369
|
+
pco.material.size = size;
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
handle_setFOV(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
374
|
+
if (!this.camera) return;
|
|
375
|
+
this.camera.fov = kwargs.fov as number;
|
|
376
|
+
this.camera.updateProjectionMatrix();
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
handle_setBackground(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
380
|
+
if (!this.scene) return;
|
|
381
|
+
this.scene.background = new Color(kwargs.color as string);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
handle_setEDL(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
385
|
+
if (!this.potreeRenderer) return;
|
|
386
|
+
this.potreeRenderer.setEDL({
|
|
387
|
+
enabled: kwargs.enabled as boolean,
|
|
388
|
+
radius: kwargs.radius as number,
|
|
389
|
+
strength: kwargs.strength as number,
|
|
390
|
+
opacity: 1.0,
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// ---------------------------------------------------------------------------
|
|
395
|
+
// Cleanup
|
|
396
|
+
// ---------------------------------------------------------------------------
|
|
397
|
+
|
|
398
|
+
destroy(): void {
|
|
399
|
+
cancelAnimationFrame(this.animationId);
|
|
400
|
+
|
|
401
|
+
if (this.resizeObserver) {
|
|
402
|
+
this.resizeObserver.disconnect();
|
|
403
|
+
this.resizeObserver = null;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
this.pointClouds.forEach(pco => pco.dispose());
|
|
407
|
+
this.pointClouds.clear();
|
|
408
|
+
this.pointCloudList = [];
|
|
409
|
+
|
|
410
|
+
this.potreeRenderer?.dispose();
|
|
411
|
+
this.potreeRenderer = null;
|
|
412
|
+
|
|
413
|
+
this.controls?.dispose();
|
|
414
|
+
this.controls = null;
|
|
415
|
+
|
|
416
|
+
this.renderer?.dispose();
|
|
417
|
+
this.renderer = null;
|
|
418
|
+
|
|
419
|
+
this.scene = null;
|
|
420
|
+
this.camera = null;
|
|
421
|
+
this.potree = null;
|
|
422
|
+
|
|
423
|
+
if (this.container && this.container.parentNode) {
|
|
424
|
+
this.container.parentNode.removeChild(this.container);
|
|
425
|
+
}
|
|
426
|
+
this.container = null;
|
|
427
|
+
this.canvas = null;
|
|
428
|
+
}
|
|
185
429
|
}
|
|
186
430
|
|
|
187
431
|
/**
|
|
188
432
|
* anywidget render function.
|
|
189
433
|
*/
|
|
190
|
-
function render({ model, el }: { model: PotreeModel; el: HTMLElement }): () => void {
|
|
191
|
-
|
|
192
|
-
el.style.display = 'block';
|
|
193
|
-
|
|
194
|
-
// Create container
|
|
195
|
-
const container = document.createElement('div');
|
|
196
|
-
container.style.width = model.get('width') || '100%';
|
|
197
|
-
container.style.height = model.get('height') || 'auto';
|
|
198
|
-
container.style.minHeight = '300px';
|
|
199
|
-
container.style.position = 'relative';
|
|
200
|
-
el.appendChild(container);
|
|
201
|
-
|
|
202
|
-
// Create widget content
|
|
203
|
-
const content = createWidgetContent(model);
|
|
204
|
-
container.appendChild(content);
|
|
205
|
-
|
|
206
|
-
// Handle model changes
|
|
207
|
-
const updateContent = () => {
|
|
208
|
-
container.innerHTML = '';
|
|
209
|
-
const newContent = createWidgetContent(model);
|
|
210
|
-
container.appendChild(newContent);
|
|
211
|
-
};
|
|
434
|
+
async function render({ model, el }: { model: PotreeModel; el: HTMLElement }): Promise<() => void> {
|
|
435
|
+
const widget = new PotreeWidget(model as PotreeModel, el);
|
|
212
436
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
437
|
+
try {
|
|
438
|
+
await widget.initialize();
|
|
439
|
+
} catch (error) {
|
|
440
|
+
console.error('Failed to initialize Potree viewer:', error);
|
|
441
|
+
el.innerHTML = `
|
|
442
|
+
<div style="display:flex;align-items:center;justify-content:center;height:400px;
|
|
443
|
+
background:linear-gradient(135deg,#0f0f23,#1a1a3e);color:#e57373;font-family:sans-serif;
|
|
444
|
+
border-radius:8px;padding:24px;">
|
|
445
|
+
<div style="text-align:center">
|
|
446
|
+
<div style="font-size:20px;margin-bottom:12px;">Failed to initialize Potree viewer</div>
|
|
447
|
+
<div style="font-size:12px;color:#666;margin-top:12px;">${error}</div>
|
|
448
|
+
</div>
|
|
449
|
+
</div>
|
|
450
|
+
`;
|
|
451
|
+
}
|
|
219
452
|
|
|
220
|
-
// Return cleanup function
|
|
221
453
|
return () => {
|
|
222
|
-
|
|
223
|
-
model.off('change:point_budget', updateContent);
|
|
224
|
-
model.off('change:point_size', updateContent);
|
|
225
|
-
model.off('change:background', updateContent);
|
|
226
|
-
model.off('change:camera_position', updateContent);
|
|
227
|
-
model.off('change:camera_target', updateContent);
|
|
228
|
-
if (container.parentNode) {
|
|
229
|
-
container.parentNode.removeChild(container);
|
|
230
|
-
}
|
|
454
|
+
widget.destroy();
|
|
231
455
|
};
|
|
232
456
|
}
|
|
233
457
|
|
package/src/types/potree.ts
CHANGED
|
@@ -37,24 +37,3 @@ export interface CameraConfig {
|
|
|
37
37
|
far?: number;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
export interface MeasurementConfig {
|
|
41
|
-
type: 'point' | 'distance' | 'area' | 'angle' | 'height' | 'profile';
|
|
42
|
-
showLabels?: boolean;
|
|
43
|
-
showArea?: boolean;
|
|
44
|
-
closed?: boolean;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export interface AnnotationConfig {
|
|
48
|
-
position: [number, number, number];
|
|
49
|
-
title?: string;
|
|
50
|
-
description?: string;
|
|
51
|
-
cameraPosition?: [number, number, number];
|
|
52
|
-
cameraTarget?: [number, number, number];
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export interface ClippingVolumeConfig {
|
|
56
|
-
type: 'box' | 'polygon' | 'plane';
|
|
57
|
-
position?: [number, number, number];
|
|
58
|
-
scale?: [number, number, number];
|
|
59
|
-
rotation?: [number, number, number];
|
|
60
|
-
}
|