React-create-app使用cesium并且渲染3d倾斜摄影

0 阅读3分钟

先上效果 image.png

一、cesium在react-create-app中的引用

首先 yarn add cesium然后yarn add copy-webpack-plugin -D然后yarn add customize-cra react-app-rewired --dev

设置了customize-cra react-app-rewired就可以改写webpack

image.png 新建一个这个文件,在里面改写webpack

const {
  override,
 
  addWebpackPlugin,
} = require("customize-cra");
const path = require("path");
const CopyWebpackPlugin = require("copy-webpack-plugin");
const webpack = require("webpack");


const cesiumSource = 'node_modules/cesium/Source';
const cesiumWorkers = '../Build/Cesium/Workers';



module.exports = override(
  
 
  addWebpackPlugin(
    new CopyWebpackPlugin({
      patterns: [
        
        { from: path.join(cesiumSource, cesiumWorkers), to: 'cesium/Workers' },
        { from: path.join(cesiumSource, 'Assets'), to: 'cesium/Assets' },
        { from: path.join(cesiumSource, 'Widgets'), to: 'cesium/Widgets' }
      ],
    })
  ),
  addWebpackPlugin(
    new webpack.DefinePlugin({
      // Define relative base path in cesium for loading assets
      CESIUM_BASE_URL: JSON.stringify("/cesium"),
    })
  )
  // addWebpackPlugin(new BundleAnalyzerPlugin())
);


package.json里的打包脚本变成

"scripts": {
    "start": "react-app-rewired start",
    "build": "react-app-rewired build",
    
  },

这样就会走我们新设定的webpack,将将node_modules/cesium/Source/Assets, node_modules/cesium/Source/Widgets, node_modules/cesium/Build/Cesium/Workers自动文件打包到build文件里,如图

image.png 这样yarn start 或者yarn build就可以正常使用cesium了

下面这篇我直接按可发布到掘金的技术文章结构帮你整理好了:有背景、有思路、有代码拆解、有优化建议,基本不需要再大改就能发 👍

二、创建 Viewer 且必须只创建一次!

否则:

  • 内存暴涨
  • WebGL context 丢失
  • 页面卡死
useEffect(() => {
  if (!containerRef.current) return;
  if (viewerRef.current) return;

  const viewer = new Cesium.Viewer(containerRef.current, {
    timeline: false,
    animation: false,
    infoBox: false,
    fullscreenButton: false,
  });

  viewerRef.current = viewer;

  return () => {
    viewerRef.current?.destroy();
    viewerRef.current = null;
  };
}, []);
<div ref={containerRef} className="cesium-container" />

这是 Cesium + React 的标准写法


三、加载倾斜摄影模型(3DTiles)

Cesium.Cesium3DTileset.fromUrl("tileset.json")
  .then((tileset) => {
    viewer.scene.primitives.add(tileset);
    viewer.zoomTo(tileset);
  });

一个强烈建议 ⭐⭐⭐⭐⭐

建议监听瓦片失败:

tileset.tileFailed.addEventListener((error) => {
  console.error("瓦片加载失败:", error);
});

否则生产环境排查问题会非常痛苦。


四、动态绘制监测点(核心)

很多人喜欢用:

👉 Primitive
👉 PointPrimitive

但在业务系统中,我更推荐:

⭐ Entity

因为:

  • 开发简单
  • 支持属性绑定
  • 支持 pick
  • 易维护
function createColorIcon(color: string) {
    const canvas = document.createElement("canvas");
    const size = 48;
    canvas.width = size;
    canvas.height = size;
    const ctx = canvas.getContext("2d");
    if (ctx) {
      const cx = size / 2;
      const r = 12;
      const tipY = size - 6;



      // 图钉形状(圆 + 尖)
      ctx.beginPath();
      ctx.moveTo(cx, tipY);
      ctx.quadraticCurveTo(cx + r, r + 12, cx + r, r + 4);
      ctx.arc(cx, r + 4, r, 0, Math.PI, true);
      ctx.quadraticCurveTo(cx - r, r + 12, cx, tipY);
      ctx.closePath();
      ctx.fillStyle = color;
      ctx.fill();



      ctx.fill();
    }

    return canvas;
  }

  const getWarningColor = (p: any) => {
    const level =
      p?.alarmLevel ?? 0;
    switch (Number(level)) {
      case 4:
        return "#D7263D99"; // 红色预警(约 60% 不透明)
      case 3:
        return "#FF6B0099"; // 橙色预警
      case 2:
        return "#C7A20099"; // 黄色预警
      case 1:
        return "#00BEFF99"; // 蓝色预警
      default:
        return "#d3f26199"; // 约 60% 不透明
    }
  };
  useEffect(() => {
    const viewer = viewerRef.current;
    if (!viewer) return;

    if (dataSource && dataSource?.length > 0) {
      pointEntityIdsRef.current.forEach((id) => {
        try {
          viewer.entities.removeById(id);
        } catch (e) {
          // ignore
        }
      });
      pointEntityIdsRef.current = [];
     
      console.log('dataSource', dataSource);
     

      (dataSource || []).forEach((p: any) => {


        const lng = Number(p.longitude);
        const lat = Number(p.latitude);
        if (Number.isNaN(lng) || Number.isNaN(lat)) return;

        const id = `point-${p.id}`;
        viewer.entities.add({
          id,
          position: Cesium.Cartesian3.fromDegrees(lng, lat, Number(p.height) || 0),
          billboard: {
            image: createColorIcon(getWarningColor(p)),
            verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
            // 防止被倾斜摄影挡住
            disableDepthTestDistance: Number.POSITIVE_INFINITY
          },
          label: {
            text: p.pointName || "",
            font: "bold 15px sans-serif",
            fillColor: Cesium.Color.fromCssColorString("rgba(68, 229, 255, 0.92)"),
            outlineColor: Cesium.Color.fromCssColorString("rgba(124, 121, 121, 0.9)").withAlpha(0.85),
            outlineWidth: 2,
            style: Cesium.LabelStyle.FILL_AND_OUTLINE,
            verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
            pixelOffset: new Cesium.Cartesian2(0, -50),
            disableDepthTestDistance: Number.POSITIVE_INFINITY,
            distanceDisplayCondition: new Cesium.DistanceDisplayCondition(
              0,
              5000
            ),
          },
          properties: {
            pickable: true,
            pointId: p.id,
            pointType: p?.pointType,
            pointName: p?.pointName,
          }
        });

        pointEntityIdsRef.current.push(id);
      });
      const handler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas);

      handler.setInputAction((movement: any) => {
        const picked = viewer.scene.pick(movement.endPosition);


        if (
          Cesium.defined(picked) &&
          picked.id &&                     // Entity
          picked?.id?.properties?.pickable?.getValue()
        ) {

          viewer.canvas.style.cursor = 'pointer';


        } else {
          viewer.canvas.style.cursor = 'default';


        }
      }, Cesium.ScreenSpaceEventType.MOUSE_MOVE);
      // 点击测点
      handler.setInputAction((click: any) => {
        const picked = viewer.scene.pick(click.position);

        if (
          Cesium.defined(picked) &&
          picked.id &&
          picked.id.billboard
        ) {
          const entity = picked.id;

          console.log('点到了:', entity.id, picked?.id?.properties?.pointId?.getValue());

         

          // 示例:相机飞过去
          viewer.flyTo(entity);
          
        }
      }, Cesium.ScreenSpaceEventType.LEFT_CLICK);


    }


  }, [dataSource]);

五、点击测点飞行定位

pick 实体

const picked = viewer.scene.pick(click.position);

判断:

if (Cesium.defined(picked) && picked.id?.billboard) {

然后:

viewer.flyTo(entity);

体验直接拉满。


鼠标 Hover 手型

细节决定高级感:

viewer.canvas.style.cursor = 'pointer';

六、相机环绕动画

核心思路:

👉 clock.onTick

const remove = viewer.clock.onTick.addEventListener(() => {
  heading += Cesium.Math.toRadians(0.2);

  viewer.camera.lookAt(
    center,
    new Cesium.HeadingPitchRange(
      heading,
      Cesium.Math.toRadians(-30),
      range
    )
  );
// 转满一圈停止
  if (heading >= Cesium.Math.TWO_PI) {
    remove();
    viewer.camera.lookAtTransform(Cesium.Matrix4.IDENTITY);
  }
});

本质:

👉 每帧改变 heading。