voxcity 0.6.22__tar.gz → 0.6.24__tar.gz

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.

Potentially problematic release.


This version of voxcity might be problematic. Click here for more details.

Files changed (38) hide show
  1. {voxcity-0.6.22 → voxcity-0.6.24}/PKG-INFO +1 -1
  2. {voxcity-0.6.22 → voxcity-0.6.24}/pyproject.toml +1 -1
  3. {voxcity-0.6.22 → voxcity-0.6.24}/src/voxcity/utils/visualization.py +319 -45
  4. {voxcity-0.6.22 → voxcity-0.6.24}/AUTHORS.rst +0 -0
  5. {voxcity-0.6.22 → voxcity-0.6.24}/LICENSE +0 -0
  6. {voxcity-0.6.22 → voxcity-0.6.24}/README.md +0 -0
  7. {voxcity-0.6.22 → voxcity-0.6.24}/src/voxcity/__init__.py +0 -0
  8. {voxcity-0.6.22 → voxcity-0.6.24}/src/voxcity/downloader/__init__.py +0 -0
  9. {voxcity-0.6.22 → voxcity-0.6.24}/src/voxcity/downloader/citygml.py +0 -0
  10. {voxcity-0.6.22 → voxcity-0.6.24}/src/voxcity/downloader/eubucco.py +0 -0
  11. {voxcity-0.6.22 → voxcity-0.6.24}/src/voxcity/downloader/gee.py +0 -0
  12. {voxcity-0.6.22 → voxcity-0.6.24}/src/voxcity/downloader/mbfp.py +0 -0
  13. {voxcity-0.6.22 → voxcity-0.6.24}/src/voxcity/downloader/oemj.py +0 -0
  14. {voxcity-0.6.22 → voxcity-0.6.24}/src/voxcity/downloader/osm.py +0 -0
  15. {voxcity-0.6.22 → voxcity-0.6.24}/src/voxcity/downloader/overture.py +0 -0
  16. {voxcity-0.6.22 → voxcity-0.6.24}/src/voxcity/downloader/utils.py +0 -0
  17. {voxcity-0.6.22 → voxcity-0.6.24}/src/voxcity/exporter/__init__.py +0 -0
  18. {voxcity-0.6.22 → voxcity-0.6.24}/src/voxcity/exporter/cityles.py +0 -0
  19. {voxcity-0.6.22 → voxcity-0.6.24}/src/voxcity/exporter/envimet.py +0 -0
  20. {voxcity-0.6.22 → voxcity-0.6.24}/src/voxcity/exporter/magicavoxel.py +0 -0
  21. {voxcity-0.6.22 → voxcity-0.6.24}/src/voxcity/exporter/netcdf.py +0 -0
  22. {voxcity-0.6.22 → voxcity-0.6.24}/src/voxcity/exporter/obj.py +0 -0
  23. {voxcity-0.6.22 → voxcity-0.6.24}/src/voxcity/generator.py +0 -0
  24. {voxcity-0.6.22 → voxcity-0.6.24}/src/voxcity/geoprocessor/__init__.py +0 -0
  25. {voxcity-0.6.22 → voxcity-0.6.24}/src/voxcity/geoprocessor/draw.py +0 -0
  26. {voxcity-0.6.22 → voxcity-0.6.24}/src/voxcity/geoprocessor/grid.py +0 -0
  27. {voxcity-0.6.22 → voxcity-0.6.24}/src/voxcity/geoprocessor/mesh.py +0 -0
  28. {voxcity-0.6.22 → voxcity-0.6.24}/src/voxcity/geoprocessor/network.py +0 -0
  29. {voxcity-0.6.22 → voxcity-0.6.24}/src/voxcity/geoprocessor/polygon.py +0 -0
  30. {voxcity-0.6.22 → voxcity-0.6.24}/src/voxcity/geoprocessor/utils.py +0 -0
  31. {voxcity-0.6.22 → voxcity-0.6.24}/src/voxcity/simulator/__init__.py +0 -0
  32. {voxcity-0.6.22 → voxcity-0.6.24}/src/voxcity/simulator/solar.py +0 -0
  33. {voxcity-0.6.22 → voxcity-0.6.24}/src/voxcity/simulator/utils.py +0 -0
  34. {voxcity-0.6.22 → voxcity-0.6.24}/src/voxcity/simulator/view.py +0 -0
  35. {voxcity-0.6.22 → voxcity-0.6.24}/src/voxcity/utils/__init__.py +0 -0
  36. {voxcity-0.6.22 → voxcity-0.6.24}/src/voxcity/utils/lc.py +0 -0
  37. {voxcity-0.6.22 → voxcity-0.6.24}/src/voxcity/utils/material.py +0 -0
  38. {voxcity-0.6.22 → voxcity-0.6.24}/src/voxcity/utils/weather.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: voxcity
3
- Version: 0.6.22
3
+ Version: 0.6.24
4
4
  Summary: voxcity is an easy and one-stop tool to output 3d city models for microclimate simulation by integrating multiple geospatial open-data
5
5
  License: MIT
6
6
  License-File: AUTHORS.rst
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "voxcity"
3
- version = "0.6.22"
3
+ version = "0.6.24"
4
4
  description = "voxcity is an easy and one-stop tool to output 3d city models for microclimate simulation by integrating multiple geospatial open-data"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -2385,6 +2385,23 @@ def _rgb_tuple_to_plotly_color(rgb_tuple):
2385
2385
  except Exception:
2386
2386
  return "rgb(128,128,128)"
2387
2387
 
2388
+ def _mpl_cmap_to_plotly_colorscale(cmap_name, n=256):
2389
+ """
2390
+ Convert a matplotlib colormap name to a Plotly colorscale list.
2391
+ """
2392
+ try:
2393
+ cmap = cm.get_cmap(cmap_name)
2394
+ except Exception:
2395
+ cmap = cm.get_cmap('viridis')
2396
+ if n < 2:
2397
+ n = 2
2398
+ scale = []
2399
+ for i in range(n):
2400
+ x = i / (n - 1)
2401
+ r, g, b, _ = cmap(x)
2402
+ scale.append([x, f"rgb({int(255*r)},{int(255*g)},{int(255*b)})"])
2403
+ return scale
2404
+
2388
2405
 
2389
2406
  def visualize_voxcity_plotly(
2390
2407
  voxel_array,
@@ -2399,49 +2416,123 @@ def visualize_voxcity_plotly(
2399
2416
  height=800,
2400
2417
  show=True,
2401
2418
  return_fig=False,
2419
+ # Building simulation overlay
2420
+ building_sim_mesh=None,
2421
+ building_value_name='svf_values',
2422
+ building_colormap='viridis',
2423
+ building_vmin=None,
2424
+ building_vmax=None,
2425
+ building_nan_color='gray',
2426
+ building_opacity=1.0,
2427
+ building_shaded=False,
2428
+ render_voxel_buildings=False,
2429
+ # Ground simulation surface overlay
2430
+ ground_sim_grid=None,
2431
+ ground_dem_grid=None,
2432
+ ground_z_offset=None,
2433
+ ground_view_point_height=None,
2434
+ ground_colormap='viridis',
2435
+ ground_vmin=None,
2436
+ ground_vmax=None,
2437
+ sim_surface_opacity=0.95,
2438
+ ground_shaded=False,
2402
2439
  ):
2403
2440
  """
2404
- Interactive 3D visualization where each occupied cell is rendered as a cube (six faces)
2405
- using Plotly Mesh3d. One Mesh3d trace per class.
2441
+ Interactive 3D visualization using Plotly Mesh3d which can render:
2442
+ - Voxel cubes (one Mesh3d trace per exposed face per class)
2443
+ - Optional building-surface simulation mesh overlay
2444
+ - Optional ground-level simulation surface overlay (triangulated)
2406
2445
 
2407
- Parameters are similar to visualize_voxcity_plotly, but rendering is via exact cubes.
2446
+ Parameters
2447
+ ----------
2448
+ voxel_array : np.ndarray (nx, ny, nz)
2449
+ Voxel class grid. Required for voxel rendering; can be None if only overlays are shown.
2450
+ meshsize : float
2451
+ Cell size (m) for converting voxel indices to metric coordinates.
2452
+ classes : list[int] or None
2453
+ Classes to render for voxel cubes. Default: all non-zero present in the array.
2454
+ voxel_color_map : str or dict
2455
+ Scheme name understood by get_voxel_color_map, or mapping {class_id: [R,G,B]}.
2456
+ opacity : float
2457
+ Opacity for voxel cubes.
2458
+ max_dimension : int
2459
+ Target maximum dimension for auto-downsampling.
2460
+ downsample : int or None
2461
+ Explicit stride for voxels. If None, auto-downsample when needed.
2462
+ title, width, height, show, return_fig : standard display controls.
2463
+
2464
+ Building overlay
2465
+ ----------------
2466
+ building_sim_mesh : trimesh.Trimesh or similar with .vertices, .faces
2467
+ building_value_name : str (default 'svf_values')
2468
+ building_colormap : str (default 'viridis')
2469
+ building_vmin, building_vmax : float or None
2470
+ building_nan_color : str (default 'gray')
2471
+ building_opacity : float (default 1.0)
2472
+ building_shaded : bool (default False)
2473
+ render_voxel_buildings : bool (default False; when False, exclude class -3 from voxel cubes)
2474
+
2475
+ Ground overlay
2476
+ --------------
2477
+ ground_sim_grid : 2D array of values
2478
+ ground_dem_grid : 2D array of ground elevations
2479
+ ground_z_offset or ground_view_point_height : float above DEM (default 1.5 m)
2480
+ ground_colormap : str (default 'viridis')
2481
+ ground_vmin, ground_vmax : float or None
2482
+ sim_surface_opacity : float (default 0.95)
2483
+ ground_shaded : bool (default False)
2408
2484
  """
2409
2485
  if voxel_array is None or getattr(voxel_array, 'ndim', 0) != 3:
2410
- raise ValueError("voxel_array must be a 3D numpy array (nx, ny, nz)")
2411
-
2412
- vox = voxel_array
2486
+ # Allow overlays without voxels
2487
+ if building_sim_mesh is None and (ground_sim_grid is None or ground_dem_grid is None):
2488
+ raise ValueError("voxel_array must be a 3D numpy array (nx, ny, nz) when no overlays are provided")
2489
+ vox = None
2490
+ else:
2491
+ vox = voxel_array
2413
2492
 
2414
2493
  # Downsample for performance if requested or auto-needed
2415
2494
  # Respect explicit downsample even when it is 1 (no auto-downsample)
2416
- if downsample is not None:
2417
- stride = max(1, int(downsample))
2418
- else:
2419
- stride = 1
2420
- nx, ny, nz = vox.shape
2421
- max_dim = max(nx, ny, nz)
2422
- if max_dim > max_dimension:
2423
- stride = int(np.ceil(max_dim / max_dimension))
2424
-
2425
- if stride > 1:
2426
- vox = vox[::stride, ::stride, ::stride]
2427
-
2428
- nx, ny, nz = vox.shape
2495
+ stride = 1
2496
+ if vox is not None:
2497
+ if downsample is not None:
2498
+ stride = max(1, int(downsample))
2499
+ else:
2500
+ nx_tmp, ny_tmp, nz_tmp = vox.shape
2501
+ max_dim = max(nx_tmp, ny_tmp, nz_tmp)
2502
+ if max_dim > max_dimension:
2503
+ stride = int(np.ceil(max_dim / max_dimension))
2504
+ if stride > 1:
2505
+ vox = vox[::stride, ::stride, ::stride]
2429
2506
 
2430
- # Coordinate of voxel centers in meters
2431
- dx = meshsize * stride
2432
- dy = meshsize * stride
2433
- dz = meshsize * stride
2434
- x = np.arange(nx, dtype=float) * dx
2435
- y = np.arange(ny, dtype=float) * dy
2436
- z = np.arange(nz, dtype=float) * dz
2507
+ nx, ny, nz = vox.shape
2437
2508
 
2438
- # Choose classes
2439
- if classes is None:
2440
- classes = np.unique(vox[vox != 0]).tolist()
2441
- if not classes:
2442
- raise ValueError("No classes to visualize (voxel grid may be empty)")
2509
+ # Coordinate of voxel centers in meters
2510
+ dx = meshsize * stride
2511
+ dy = meshsize * stride
2512
+ dz = meshsize * stride
2513
+ x = np.arange(nx, dtype=float) * dx
2514
+ y = np.arange(ny, dtype=float) * dy
2515
+ z = np.arange(nz, dtype=float) * dz
2516
+
2517
+ # Choose classes
2518
+ if classes is None:
2519
+ classes_all = np.unique(vox[vox != 0]).tolist()
2520
+ else:
2521
+ classes_all = list(classes)
2522
+ # Exclude building voxels (-3) only when a building overlay is provided and hiding is desired
2523
+ if building_sim_mesh is not None and getattr(building_sim_mesh, 'vertices', None) is not None:
2524
+ if render_voxel_buildings:
2525
+ classes_to_draw = classes_all
2526
+ else:
2527
+ classes_to_draw = [c for c in classes_all if int(c) != -3]
2528
+ else:
2529
+ classes_to_draw = classes_all
2443
2530
 
2444
- vox_dict = get_voxel_color_map(voxel_color_map)
2531
+ # Resolve color map: accept scheme name or explicit dict
2532
+ if isinstance(voxel_color_map, dict):
2533
+ vox_dict = voxel_color_map
2534
+ else:
2535
+ vox_dict = get_voxel_color_map(voxel_color_map)
2445
2536
 
2446
2537
  def exposed_face_masks(occ):
2447
2538
  # occ shape (nx, ny, nz)
@@ -2542,21 +2633,204 @@ def visualize_voxcity_plotly(
2542
2633
 
2543
2634
  fig = go.Figure()
2544
2635
 
2545
- for cls in classes:
2546
- if not np.any(vox == cls):
2547
- continue
2548
- occ = (vox == cls)
2549
- posx, negx, posy, negy, posz, negz = exposed_face_masks(occ)
2550
- color_rgb = vox_dict.get(int(cls), [128,128,128])
2551
- add_faces(fig, posx, '+x', color_rgb)
2552
- add_faces(fig, negx, '-x', color_rgb)
2553
- add_faces(fig, posy, '+y', color_rgb)
2554
- add_faces(fig, negy, '-y', color_rgb)
2555
- add_faces(fig, posz, '+z', color_rgb)
2556
- add_faces(fig, negz, '-z', color_rgb)
2636
+ # Draw voxel cubes if available
2637
+ if vox is not None and classes_to_draw:
2638
+ for cls in classes_to_draw:
2639
+ if not np.any(vox == cls):
2640
+ continue
2641
+ occ = (vox == cls)
2642
+ posx, negx, posy, negy, posz, negz = exposed_face_masks(occ)
2643
+ color_rgb = vox_dict.get(int(cls), [128,128,128])
2644
+ add_faces(fig, posx, '+x', color_rgb)
2645
+ add_faces(fig, negx, '-x', color_rgb)
2646
+ add_faces(fig, posy, '+y', color_rgb)
2647
+ add_faces(fig, negy, '-y', color_rgb)
2648
+ add_faces(fig, posz, '+z', color_rgb)
2649
+ add_faces(fig, negz, '-z', color_rgb)
2650
+
2651
+ # Building simulation mesh overlay
2652
+ if building_sim_mesh is not None and getattr(building_sim_mesh, 'vertices', None) is not None:
2653
+ Vb = np.asarray(building_sim_mesh.vertices)
2654
+ Fb = np.asarray(building_sim_mesh.faces)
2655
+
2656
+ # Values can be stored in metadata under building_value_name
2657
+ values = None
2658
+ if hasattr(building_sim_mesh, 'metadata') and isinstance(building_sim_mesh.metadata, dict):
2659
+ values = building_sim_mesh.metadata.get(building_value_name)
2660
+ if values is not None:
2661
+ values = np.asarray(values)
2662
+
2663
+ face_vals = None
2664
+ if values is not None and values.size == len(Fb):
2665
+ face_vals = values.astype(float)
2666
+ elif values is not None and values.size == len(Vb):
2667
+ vals_v = values.astype(float)
2668
+ face_vals = np.nanmean(vals_v[Fb], axis=1)
2669
+
2670
+ facecolor = None
2671
+ if face_vals is not None:
2672
+ finite = np.isfinite(face_vals)
2673
+ vmin_b = building_vmin if building_vmin is not None else (float(np.nanmin(face_vals[finite])) if np.any(finite) else 0.0)
2674
+ vmax_b = building_vmax if building_vmax is not None else (float(np.nanmax(face_vals[finite])) if np.any(finite) else 1.0)
2675
+ norm = mcolors.Normalize(vmin=vmin_b, vmax=vmax_b)
2676
+ cmap = cm.get_cmap(building_colormap)
2677
+ colors_rgba = np.zeros((len(Fb), 4), dtype=float)
2678
+ colors_rgba[finite] = cmap(norm(face_vals[finite]))
2679
+ nan_rgba = np.array(mcolors.to_rgba(building_nan_color))
2680
+ colors_rgba[~finite] = nan_rgba
2681
+ facecolor = [f"rgb({int(255*c[0])},{int(255*c[1])},{int(255*c[2])})" for c in colors_rgba]
2682
+
2683
+ if building_shaded:
2684
+ lighting_b = dict(ambient=0.35, diffuse=1.0, specular=0.4, roughness=0.5, fresnel=0.1)
2685
+ flat_b = False
2686
+ else:
2687
+ lighting_b = dict(ambient=1.0, diffuse=0.0, specular=0.0, roughness=0.0, fresnel=0.0)
2688
+ flat_b = False
2689
+
2690
+ # Place a directional light near mesh center
2691
+ cx = float((Vb[:,0].min() + Vb[:,0].max()) * 0.5)
2692
+ cy = float((Vb[:,1].min() + Vb[:,1].max()) * 0.5)
2693
+ cz = float((Vb[:,2].min() + Vb[:,2].max()) * 0.5)
2694
+ lx = cx + (Vb[:,0].max() - Vb[:,0].min() + meshsize) * 0.9
2695
+ ly = cy + (Vb[:,1].max() - Vb[:,1].min() + meshsize) * 0.6
2696
+ lz = cz + (Vb[:,2].max() - Vb[:,2].min() + meshsize) * 1.4
2697
+
2698
+ fig.add_trace(
2699
+ go.Mesh3d(
2700
+ x=Vb[:,0], y=Vb[:,1], z=Vb[:,2],
2701
+ i=Fb[:,0], j=Fb[:,1], k=Fb[:,2],
2702
+ facecolor=facecolor if facecolor is not None else None,
2703
+ color=None if facecolor is not None else 'rgb(200,200,200)',
2704
+ opacity=float(building_opacity),
2705
+ flatshading=flat_b,
2706
+ lighting=lighting_b,
2707
+ lightposition=dict(x=lx, y=ly, z=lz),
2708
+ name=building_value_name if facecolor is not None else 'building_mesh'
2709
+ )
2710
+ )
2711
+
2712
+ # Colorbar for building overlay
2713
+ if face_vals is not None:
2714
+ colorscale_b = _mpl_cmap_to_plotly_colorscale(building_colormap)
2715
+ fig.add_trace(
2716
+ go.Scatter3d(
2717
+ x=[None], y=[None], z=[None],
2718
+ mode='markers',
2719
+ marker=dict(
2720
+ size=0.1,
2721
+ color=[vmin_b, vmax_b],
2722
+ colorscale=colorscale_b,
2723
+ cmin=vmin_b,
2724
+ cmax=vmax_b,
2725
+ colorbar=dict(title=building_value_name, len=0.5, y=0.8),
2726
+ showscale=True,
2727
+ ),
2728
+ showlegend=False,
2729
+ hoverinfo='skip',
2730
+ )
2731
+ )
2732
+
2733
+ # Ground simulation surface overlay
2734
+ if ground_sim_grid is not None and ground_dem_grid is not None:
2735
+ sim_vals = np.asarray(ground_sim_grid, dtype=float)
2736
+ finite = np.isfinite(sim_vals)
2737
+ vmin_g = ground_vmin if ground_vmin is not None else (float(np.nanmin(sim_vals[finite])) if np.any(finite) else 0.0)
2738
+ vmax_g = ground_vmax if ground_vmax is not None else (float(np.nanmax(sim_vals[finite])) if np.any(finite) else 1.0)
2739
+
2740
+ # Determine z offset
2741
+ z_off = ground_z_offset if ground_z_offset is not None else ground_view_point_height
2742
+ try:
2743
+ z_off = float(z_off) if z_off is not None else 1.5
2744
+ except Exception:
2745
+ z_off = 1.5
2746
+ if meshsize is not None:
2747
+ try:
2748
+ ms = float(meshsize)
2749
+ if z_off < ms:
2750
+ z_off = ms
2751
+ z_off = ms * math.ceil(z_off / ms)
2752
+ except Exception:
2753
+ pass
2754
+
2755
+ # Normalize DEM so its minimum becomes 0, matching voxel Z coordinates
2756
+ try:
2757
+ dem_norm = np.asarray(ground_dem_grid, dtype=float)
2758
+ dem_norm = dem_norm - np.nanmin(dem_norm)
2759
+ except Exception:
2760
+ dem_norm = ground_dem_grid
2761
+
2762
+ sim_mesh = create_sim_surface_mesh(
2763
+ ground_sim_grid,
2764
+ dem_norm,
2765
+ meshsize=meshsize,
2766
+ z_offset=z_off,
2767
+ cmap_name=ground_colormap,
2768
+ vmin=vmin_g,
2769
+ vmax=vmax_g,
2770
+ )
2771
+
2772
+ if sim_mesh is not None and getattr(sim_mesh, 'vertices', None) is not None:
2773
+ V = np.asarray(sim_mesh.vertices)
2774
+ F = np.asarray(sim_mesh.faces)
2775
+
2776
+ facecolor = None
2777
+ try:
2778
+ colors_rgba = np.asarray(sim_mesh.visual.face_colors)
2779
+ if colors_rgba.ndim == 2 and colors_rgba.shape[0] == len(F):
2780
+ facecolor = [f"rgb({int(c[0])},{int(c[1])},{int(c[2])})" for c in colors_rgba]
2781
+ except Exception:
2782
+ facecolor = None
2783
+
2784
+ if ground_shaded:
2785
+ lighting = dict(ambient=0.35, diffuse=1.0, specular=0.4, roughness=0.5, fresnel=0.1)
2786
+ flat = False
2787
+ else:
2788
+ lighting = dict(ambient=1.0, diffuse=0.0, specular=0.0, roughness=0.0, fresnel=0.0)
2789
+ flat = False
2790
+
2791
+ cx = float((V[:,0].min() + V[:,0].max()) * 0.5)
2792
+ cy = float((V[:,1].min() + V[:,1].max()) * 0.5)
2793
+ cz = float((V[:,2].min() + V[:,2].max()) * 0.5)
2794
+ lx = cx + (V[:,0].max() - V[:,0].min() + meshsize) * 0.9
2795
+ ly = cy + (V[:,1].max() - V[:,1].min() + meshsize) * 0.6
2796
+ lz = cz + (V[:,2].max() - V[:,2].min() + meshsize) * 1.4
2797
+
2798
+ fig.add_trace(
2799
+ go.Mesh3d(
2800
+ x=V[:,0], y=V[:,1], z=V[:,2],
2801
+ i=F[:,0], j=F[:,1], k=F[:,2],
2802
+ facecolor=facecolor,
2803
+ color=None if facecolor is not None else 'rgb(200,200,200)',
2804
+ opacity=float(sim_surface_opacity),
2805
+ flatshading=flat,
2806
+ lighting=lighting,
2807
+ lightposition=dict(x=lx, y=ly, z=lz),
2808
+ name='sim_surface'
2809
+ )
2810
+ )
2811
+
2812
+ # Colorbar for ground overlay
2813
+ colorscale_g = _mpl_cmap_to_plotly_colorscale(ground_colormap)
2814
+ fig.add_trace(
2815
+ go.Scatter3d(
2816
+ x=[None], y=[None], z=[None],
2817
+ mode='markers',
2818
+ marker=dict(
2819
+ size=0.1,
2820
+ color=[vmin_g, vmax_g],
2821
+ colorscale=colorscale_g,
2822
+ cmin=vmin_g,
2823
+ cmax=vmax_g,
2824
+ colorbar=dict(title='ground', len=0.5, y=0.2),
2825
+ showscale=True,
2826
+ ),
2827
+ showlegend=False,
2828
+ hoverinfo='skip',
2829
+ )
2830
+ )
2557
2831
 
2558
2832
  fig.update_layout(
2559
- title=title or "VoxCity 3D (Voxel Cubes)",
2833
+ title=title or "VoxCity 3D",
2560
2834
  width=width,
2561
2835
  height=height,
2562
2836
  scene=dict(
File without changes
File without changes
File without changes