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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "anymap-ts",
3
- "version": "0.11.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
- "shpjs": "^6.2.0"
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",
@@ -1,12 +1,23 @@
1
1
  /**
2
2
  * Potree point cloud viewer widget entry point.
3
3
  *
4
- * Potree is a Three.js-based point cloud renderer. This widget provides
5
- * a simple display showing the configured point clouds and settings.
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
- * Format number with appropriate suffix.
44
+ * Potree point cloud viewer widget using potree-core + Three.js.
34
45
  */
35
- function formatNumber(num: number): string {
36
- if (num >= 1000000) return (num / 1000000).toFixed(1) + 'M';
37
- if (num >= 1000) return (num / 1000).toFixed(1) + 'K';
38
- return num.toString();
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
- * Create the widget content.
43
- */
44
- function createWidgetContent(model: PotreeModel): HTMLElement {
45
- const container = document.createElement('div');
46
- container.style.cssText = `
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
- <div style="display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin-bottom: 24px;">
101
- <div style="background: rgba(255,255,255,0.05); padding: 14px; border-radius: 8px; text-align: center;">
102
- <div style="font-size: 20px; font-weight: bold; color: #64b5f6;">${pointCloudCount}</div>
103
- <div style="font-size: 11px; color: #888; margin-top: 4px;">Point Clouds</div>
104
- </div>
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
- ${pointCloudCount > 0 ? `
156
- <div>
157
- <h3 style="margin: 0 0 12px 0; font-size: 14px; color: #888; text-transform: uppercase; letter-spacing: 1px;">Point Clouds</h3>
158
- <div style="background: rgba(255,255,255,0.05); padding: 16px; border-radius: 8px;">
159
- ${pointCloudItems}
160
- </div>
161
- </div>
162
- ` : `
163
- <div style="text-align: center; padding: 24px; color: #666; background: rgba(255,255,255,0.03); border-radius: 8px;">
164
- <svg width="48" height="48" viewBox="0 0 24 24" fill="currentColor" style="opacity: 0.3; margin-bottom: 12px;">
165
- <circle cx="4" cy="4" r="2"/>
166
- <circle cx="12" cy="4" r="2"/>
167
- <circle cx="20" cy="4" r="2"/>
168
- <circle cx="4" cy="12" r="2"/>
169
- <circle cx="12" cy="12" r="2"/>
170
- <circle cx="20" cy="12" r="2"/>
171
- </svg>
172
- <p style="margin: 0;">No point clouds loaded</p>
173
- <p style="margin: 8px 0 0 0; font-size: 12px;">Use <code style="background: rgba(255,255,255,0.1); padding: 2px 6px; border-radius: 4px;">load_point_cloud(url)</code> to add a point cloud</p>
174
- </div>
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
- <div style="margin-top: 24px; padding-top: 16px; border-top: 1px solid rgba(255,255,255,0.1); text-align: center;">
178
- <p style="margin: 0; font-size: 12px; color: #666;">
179
- Use <code style="background: rgba(255,255,255,0.1); padding: 2px 6px; border-radius: 4px;">to_html()</code> to export as standalone 3D viewer
180
- </p>
181
- </div>
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
- return container;
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
- el.style.width = '100%';
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
- model.on('change:point_clouds', updateContent);
214
- model.on('change:point_budget', updateContent);
215
- model.on('change:point_size', updateContent);
216
- model.on('change:background', updateContent);
217
- model.on('change:camera_position', updateContent);
218
- model.on('change:camera_target', updateContent);
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
- model.off('change:point_clouds', updateContent);
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
 
@@ -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
- }