Need Help Clamping KML Polylines and Polygons to 3D Tileset Using ClassificationPrimitive in CesiumJS — Original Geometries Still Visible/Floating

Hi everyone,

I’m working on a CesiumJS + React-based application where I load multiple KMLs using a custom hook (usePlotAssets). These KMLs include a variety of geometries like

  • Centerlines (solid and dashed polylines)
  • RoW boundaries (polygons with solid or dashed outlines)
  • Station markers

These are loaded from Cesium Ion and are rendered alongside a 3D Tileset (e.g., SLAM or photogrammetry data). Terrain is disabled—the goal is to clamp everything directly onto the 3D Tileset.


:cross_mark: Problem

KMLs do clamp to the 3D Tileset when using Cesium.HeightReference.CLAMP_TO_3D_TILE or ClassificationPrimitive, but the issue is:

:warning: The original KML geometries still remain visible — often floating above or rendering through the 3D tileset — causing double rendering or z-fighting.

Even when clamping works for the new primitives, the original Entity-based KML representations are still being rendered in the scene.


:gear: What I’ve Tried

  • Used viewer.entities.add() with:

    clampToGround: true,
    heightReference: Cesium.HeightReference.CLAMP_TO_3D_TILE,
    classificationType: Cesium.ClassificationType.CESIUM_3D_TILE
    
  • Also tried replacing Entity rendering with ClassificationPrimitive:

    scene.primitives.add(new Cesium.ClassificationPrimitive({ ... }))
    
  • Confirmed that primitives clamp correctly.

  • Tried removing original entity after creating primitive — not always effective since Ion KML loading still auto-adds entities.


:bullseye: Goal

  • Ensure that only the ClassificationPrimitive version is rendered.
  • Prevent or remove the original entity-based geometries loaded via KML that float or pierce through the 3D tiles.
  • Clamp dashed lines, solid lines, and polygon outlines correctly.
  • Manage cleanup and re-rendering cleanly in a React setup.

:pushpin: Questions

  1. How can I prevent or hide the original KML entities that are auto-rendered by Cesium Ion after loading?
  2. Is there a best practice to replace KML entities with primitives while preserving styles (like dashed lines)?
  3. Are there known techniques to fully clamp dashed polylines and polygons to 3D Tilesets using primitives?
  4. Is this a limitation of the KmlDataSource and should I parse and plot geometries manually instead?

:laptop: Tech Stack

  • CesiumJS v1.116
  • React + Redux
  • Cesium Ion for KML loading
  • No terrain — only 3D Tileset
  • scene.primitives.add(...) for manual plots

:laptop: Code Snippet (Custom Hook)

Here’s the relevant code from my usePlotAssets hook. I’m using KmlDataSource to load the KML and then trying to reprocess or overwrite entities:

Click to expand full code
export const usePlotAssets = (viewerRef) => {
  const { kmlLayers, visibleKmlLayers, KMLHeightRefrence: Cesium.HeightReference.CLAMP_TO_3D_TILE } = useSelector(
    (state) => state.gisHome3d
  );
  const viewer = viewerRef.current;

  const dispatch = useDispatch();
  const heightRefCache = useRef(KMLHeightRefrence);

  useEffect(() => {
    heightRefCache.current = KMLHeightRefrence;
  }, [KMLHeightRefrence]);

  const kmlDataSourcesRef = useRef(new Map());
  const manualEntitiesRef = useRef(new Map());
  const loadingKmlsRef = useRef(new Set());

  const plotCenterline = useCallback(
    async (
      viewer,
      assetId,
      color,
      heightOffset = 0,
      isDashed = false,
      heightReference
    ) => {
      if (loadingKmlsRef.current.has(assetId)) return;
      loadingKmlsRef.current.add(assetId);

      dispatch(gisHome3dActions.setLoadingState({
        assetId: 'cesiumKmlAssets',
        type: 'plot',
        value: true,
      }));

      try {
        const resource = await Cesium.IonResource.fromAssetId(assetId);
        const dataSource = await Cesium.KmlDataSource.load(resource, {
          camera: viewer?.scene.camera,
          canvas: viewer?.scene.canvas,
          clampToGround: true,
        });

        const addedEntities = [];

        const addEntity = (e) => {
          const added = viewer?.entities.add(e);
          if (added) addedEntities.push(added);
        };

        dataSource.entities.values.forEach((entity) => {
          if (entity.polyline) {
            const originalPositions = entity.polyline.positions.getValue();
            const adjustedPositions = originalPositions.map((position) => {
              const carto = Cesium.Cartographic.fromCartesian(position);
              return Cesium.Cartesian3.fromRadians(
                carto.longitude,
                carto.latitude,
                carto.height + heightOffset
              );
            });

            const material = isDashed
              ? new Cesium.PolylineDashMaterialProperty({
                  color,
                  dashLength: 16,
                  gapColor: Cesium.Color.TRANSPARENT,
                })
              : color;

            addEntity({
              polyline: {
                positions: adjustedPositions,
                material,
                classificationType: Cesium.ClassificationType.CESIUM_3D_TILE,
                width: 2,
                show: true,
                clampToGround:
                  heightReference === Cesium.HeightReference.CLAMP_TO_3D_TILE,
                heightReference: heightReference,
              },
            });
          } else if (entity.polygon) {
            entity.polygon.outline = true;
            entity.polygon.outlineColor = color;
            entity.polygon.height = 0.5;
            entity.polygon.closeBottom = true;
            entity.polygon.heightReference = heightReference;
            entity.polygon.clampToGround =
              heightReference === Cesium.HeightReference.CLAMP_TO_3D_TILE;
            addEntity(entity);

            const hierarchy = Cesium.Property.getValueOrDefault(
              entity.polygon.hierarchy,
              Cesium.JulianDate.now(),
              {}
            );

            if (hierarchy?.positions) {
              addEntity({
                polyline: {
                  positions: hierarchy.positions,
                  clampToGround: true,
                  heightReference: heightReference,
                  width: 2,
                  material: isDashed
                    ? new Cesium.PolylineDashMaterialProperty({
                        color,
                        dashLength: 14,
                        gapColor: Cesium.Color.TRANSPARENT,
                        dashPattern: 255.0,
                      })
                    : color,
                },
              });
            }
          } else {
            // Add other types (points, billboards, labels, etc.)
            addEntity(entity);
          }
        });

        await viewer.dataSources.add(dataSource);
        kmlDataSourcesRef.current.set(assetId, dataSource);
        manualEntitiesRef.current.set(assetId, addedEntities);
      } catch (err) {
        console.error(`❌ Error plotting KML ${assetId}:`, err);
      } finally {
        loadingKmlsRef.current.delete(assetId);
        dispatch(gisHome3dActions.setLoadingState({
          assetId: 'cesiumKmlAssets',
          type: 'plot',
          value: false,
        }));
      }
    },
    [dispatch]
  );

  const flattenVisibleLayers = useCallback((layersGroup) => {
    const allLayers = [];
    layersGroup.forEach((group) => {
      group.Layers.forEach((layer) => {
        if (visibleKmlLayers[layer.id]?.visible) allLayers.push(layer);
      });
      group.districts?.forEach((district) => {
        district.Layers.forEach((layer) => {
          if (visibleKmlLayers[layer.id]?.visible) allLayers.push(layer);
        });
      });
    });
    return allLayers;
  }, [visibleKmlLayers]);

  const delay = (ms) => new Promise((res) => setTimeout(res, ms));

  useEffect(() => {
    const viewer = viewerRef.current;
    if (!viewer) return;

    Cesium.Ion.defaultAccessToken = import.meta.env.VITE_CESIUM_TOEKN;
    Cesium.Ion.defaultServer = 'https://api.ion.cesium.com';

    const layersToPlot = flattenVisibleLayers(kmlLayers);

    // Remove non-visible KMLs
    kmlDataSourcesRef.current.forEach((dataSource, assetId) => {
      if (!layersToPlot.find((l) => l.id === assetId)) {
        dispatch(gisHome3dActions.setLoadingState({
          assetId: 'cesiumKmlAssetsRemove',
          type: 'remove',
          value: true,
        }));

        const entities = manualEntitiesRef.current.get(assetId);
        if (entities) {
          entities.forEach((ent) => viewer.entities.remove(ent));
          manualEntitiesRef.current.delete(assetId);
        }

        viewer.dataSources.remove(dataSource, true);
        kmlDataSourcesRef.current.delete(assetId);
        delay(300);

        dispatch(gisHome3dActions.setLoadingState({
          assetId: 'cesiumKmlAssetsRemove',
          type: 'remove',
          value: false,
        }));
      }
    });

    // Add new visible KMLs
    layersToPlot.forEach((layer) => {
      if (!kmlDataSourcesRef.current.has(layer.id)) {
        const alpha = visibleKmlLayers[layer.id]?.opacity ?? 1;
        const color = getColorForLayerCesium(layer.name, alpha);
        const dashed = isDashedLine(layer.name);
        plotCenterline(
          viewer,
          layer.id,
          color,
          0,
          dashed,
          heightRefCache.current
        );
      }
    });
  }, [dispatch, flattenVisibleLayers, kmlLayers, plotCenterline, viewerRef, visibleKmlLayers, heightRefCache]);
};

Any help or insights would be greatly appreciated — especially if someone has handled full KML-to-primitive conversion cleanly inside a React + Cesium app.
Happy to share actual code if needed!

Thanks in advance :folded_hands:

1 Like

Hi @pradip27 ,

Thanks for your post and for being part of the Cesium community.
We have a previous thread here that discusses this topic and has guidance on how to clamp KML to 3D Tiles KML clampping to 3Dtiles - #5 by 11115. I think the best course of action is to try to follow the advice there.

It may also be beneficial to create an example using our sandcastle tool https://sandcastle.cesium.com/ of your use case plotting KML on top of 3D Tiles so we can more easily help debug.

Hope the linked thread helps you move forward and please let us know if we can assist with more specific questions.
Best,
Luke