事情得从一次选型争论说起。

作为创新研究的踩地雷专业户,虾神准备探索下一代桌面的地理信息系统渲染技术,技术选型的时候经历了一系列的纠结。用 Cpp/QT 吧,老兵不死但日渐凋零(其实还是因为我不会),虽然目前主流的GIS都是,例如QGIS 就是例子。用 Electron 吧,内存消耗先不说,JS的渲染性能能让你怀疑人生,打开一个空窗口先吃你 500MB,我就破笔记本跑起来,CPU风扇响得跟锯木头似的。用 Flutter 吧,就更不用说了,性能好是好了,但是桌面端还是后妈养的,踩坑得自己填。

这时候突然想起,我这段时间写Rust直接从VSCode转移到了Zed上面,它本身就是用 Rust 写的,启动快得离谱,它底层那套 UI 框架叫 GPUI,据说渲染性能很猛。

GPUI 简介

GPUI,全称GPU-accelerated UI framework(GPU 加速 UI 框架),听这个名字,就感觉到,渲染肯定是他的基本功。 它是 Zed Industries(高性能代码编辑器 Zed 的开发商)于 2025 年 10 月 v0.2.0 正式开源的GUI库,此前为 Zed 内部框架。它的设计目标就是替代 Electron,解决高内存、启动慢、渲染卡顿问题;致力于使用用 Rust + GPU 渲染实现60fps 流畅交互。

他的渲染引擎是基于 wgpu(WebGPU 的 Rust 绑定),底层砍掉了Web 中间层,直接进行 GPU 渲染,性能接近底层原生应用。

但是——

  1. 这玩意儿刚刚出到0.2版本,还没有进入生产可用版,后面API改动的幅度可能还挺大。
  2. 他本来说用来做IDE的,从来没人用 GPUI 做过地图渲染,一个都没有。文档里也找不到 polygon、tile、map 这些关键词。

所以就有了这么个想法:要不先写个小 demo,看看 GPUI 的地图渲染能力到底行不行。至于数据嘛,本来shapefile是最好的,但是为了减少依赖,不考虑其他包,而GeoJSON 结构简单,直接用来作为测试数据源正合适。

说干就干。

架构

整个测试项目分两层。底层是功能性代码,跟 UI 无关:读文件、解析坐标、几何计算、多边形三角剖分。上层是 GPUI 渲染,负责把几何数据画到屏幕上。为了分工清楚,我把功能代码放到 util.rs,渲染逻辑留在 main.rs

依赖极简,就两个 crate。GPUI 负责窗口管理和 Canvas 渲染,serde_json 负责解析 GeoJSON。如果一个 UI 框架连最基本的路径绘制都做不好,那地图引擎也就无从谈起了。

[package]
name = "gpui-demo"
version = "0.1.0"
edition = "2024"

[dependencies]
gpui = "0.2.2"
serde_json = "1.0.150"

数据

GeoJSON 的全称是 JavaScript Object Notation for Geographic Features,说白了就是用 JSON 表示地理要素。结构很直观:一个 FeatureCollection 里面装了一堆 Feature,每个 Feature 有属性(properties)和几何形状(geometry)。

几何形状我选用的是面要素,这种复杂类型比较容易测出效果和性能来,特别是还有麻烦的MultiPolygon。

选择 GeoJSON 作为测试数据,主要原因是结构简单,解析成本低,不依赖第三方 GIS 库。

因为没有使用geo-types、geo等已经封装好的数据库结构库,所以我们还得自己定义下数据结构:

// ================================================================
//  数据结构定义
// ================================================================

/// 一个多边形环(外环或内环)。
/// 每个 Ring 由一系列 (经度, 纬度) 坐标对组成,首尾坐标相同(自闭合)。
/// - 外环(outer ring):定义多边形外部边界,应为顺时针(CW)绕行。
/// - 内环(inner ring):定义多边形内部孔洞,应为逆时针(CCW)绕行。
pub type Ring = Vec<(f64, f64)>;

/// 一个多边形,包含一个外环和零个或多个内环(孔洞)。
/// - `rings[0]`:外环(outer ring),定义多边形的主体边界
/// - `rings[1..]`:内环(inner rings),定义多边形内部的孔洞区域
#[derive(Clone)]
pub struct PolygonData {
    pub rings: Vec<Ring>,
}

/// 一个地理要素,对应 GeoJSON FeatureCollection 中的一个 Feature。
/// 例如一个城市或行政区,包含名称和其几何形状。
/// 一个要素可以拥有多个多边形部件(MultiPolygon 结构)。
#[derive(Clone)]
pub struct Feature {
    pub name: String,
    pub polygons: Vec<PolygonData>,
}

/// 解析后的 GeoJSON 数据,包含要素列表和全局包围盒。
/// 作为 MapView 的数据源,与 GPUI 渲染逻辑分离。
#[derive(Clone)]
pub struct GeoData {
    pub features: Vec<Feature>,
    pub geobounds: (f64, f64, f64, f64),
}

GeoJSON 解析用 serde_json 的 Value 类型做无结构解析,不需要定义反序列化结构体,灵活且代码量少。核心逻辑是判断 geometry 类型后,逐层剥开 coordinates 数组:

// ── 解析 FeatureCollection ──
if let Some(features_arr) = val["features"].as_array() {
    for feature_val in features_arr {
        let name = feature_val["properties"]["name"]
            .as_str().unwrap_or("unknown").to_string();
        let geom = &feature_val["geometry"];
        let geom_type = geom["type"].as_str().unwrap_or("");
        let mut polygons = Vec::new();
        match geom_type {
            "MultiPolygon" => {
                if let Some(polygons_arr) = geom["coordinates"].as_array() {
                    for poly_val in polygons_arr {
                        let mut rings = Vec::new();
                        if let Some(rings_arr) = poly_val.as_array() {
                            for ring_val in rings_arr {
                                rings.push(parse_ring(ring_val));
                            }
                        }
                        polygons.push(PolygonData { rings });
                    }
                }
            }
            "Polygon" => {
                if let Some(rings_arr) = geom["coordinates"].as_array() {
                    let mut rings = Vec::new();
                    for ring_val in rings_arr {
                        rings.push(parse_ring(ring_val));
                    }
                    polygons.push(PolygonData { rings });
                }
            }
            _ => {}
        }
        features.push(Feature { name, polygons });
    }
}
let geobounds = compute_geobounds(&features);
GeoData { features, geobounds }

嵌套数组的深度取决于几何类型:Polygon 是三层嵌套 [[[...]]],MultiPolygon 是四层 [[[[...]]]]。每一层都有对应的语义,解析的时候需要搞清楚当前层代表的是要素、多边形还是环。

投影

地理坐标是球面上的经纬度,屏幕坐标是平面上的像素。要把前者映射到后者,需要一次简单的线性变换。先用全局包围盒算出经纬度范围,再根据 Canvas 的宽高确定缩放比例。为了让地图周围留白,缩放时只用了可用空间的 85%。

纬度的 Y 轴需要取反,因为屏幕的原点在左上角,Y 轴向下递增,而地理纬度向上递增。如果不做这个反转,画出来的地图就是上下颠倒的。

/// 将地理坐标(WGS84 经纬度)投影到屏幕像素坐标。
///
/// 投影原理:
/// 1. 计算全局包围盒的跨度 (range_x, range_y)
/// 2. 根据 canvas 尺寸 + 85% 边距计算缩放比例
/// 3. 以地理中心为原点归一化坐标,乘以 scale
/// 4. 加上 canvas 中心偏移,得到屏幕像素位置
/// 5. 纬度 Y 轴取反(屏幕 Y 轴向下,地理纬度向上)
pub fn geo_to_screen(
    lon: f64, lat: f64,
    geobounds: (f64, f64, f64, f64),
    canvas_size: Size<Pixels>,
) -> (f64, f64) {
    let (min_x, max_x, min_y, max_y) = geobounds;
    let range_x = (max_x - min_x).max(1.0);
    let range_y = (max_y - min_y).max(1.0);
    let w = canvas_size.width.to_f64();
    let h = canvas_size.height.to_f64();
    let scale_x = w / range_x * 0.85;
    let scale_y = h / range_y * 0.85;
    let scale = scale_x.min(scale_y);
    let geo_cx = (min_x + max_x) / 2.0;
    let geo_cy = (min_y + max_y) / 2.0;
    let sx = w / 2.0 + (lon - geo_cx) * scale;
    let sy = h / 2.0 - (lat - geo_cy) * scale;
    (sx, sy)
}

包围盒的计算就是遍历所有坐标,找到四个极值。这个计算只在启动时做一次,后续的投影计算是 O(1) 的,不会成为性能瓶颈。地理行政区划有大量的要素都是由巨多个点构成的,例如内蒙的呼伦贝尔市的边界有超过 1500 个坐标点,在做投影计算的时候,要在 f64 精度下完成,才能保证地理坐标到屏幕坐标的精确映射,这一块计算量很大,后期可以考虑改成并行计算来实现,这里因为是做Demo,所以暂时暴力串行。

剖分

GPUI本身不具备基本的渲染API,所以要进行绘制,依赖的是 paint_path 方法,这是类似于web里面canvas、svg的技术路线。最直接的做法是把多边形所有的点连成一个闭合路径,然后交给 paint_path 去填充。这是最简单的方法,但也最容易出问题。

第一个问题来了……

  • GPUI的底层用 “三角扇形(Triangle Fan)” 填充,因为:
  1. 2D UI 形状天然适配
  2. 顶点复用极致省内存
  3. wgpu 渲染管线友好且速度快

三角扇形(TRIANGLE_FAN)是 GPU 的一种基础图元:

  • 第一个顶点是公共中心点
  • 后续每两个点和中心连成一个三角形
  • n 个顶点 → n−2 个三角形,全部共享中心顶点

使用三角扇形,除了完美适配 UI 的凸多边形以外,它特别是在渲染圆形、椭圆、圆弧、圆角、渐变这种无限逼近的形状的时候,能够平滑度可控,且可以自动插值,让它能代码极简、性能极高。

在于 GPUI 底层用的是三角扇形填充——从第一个顶点出发,连接所有其他顶点。对于凸多边形,这么做没问题。但对于城市边界这种典型的凹多边形,扇形三角形会延伸到多边形外部,覆盖到相邻的区域。下面是用山西市级行政区划绘制边界就是个很好的例子,用扇形填充会溢出到隔壁的地盘上。

而且如果自己把多边形切成三角形,每个三角形单独画,结果出现了一堆白色细线。原因是相邻三角形的共享边在抗锯齿渲染时产生了半透明叠加,形成了接缝。仔细看下图:

后来在 GPUI 的 Path API 文档里找到了 move_to 方法。这个方法抬笔移动,不画线。把所有三角形放在同一个 Path 里,用 move_to 分隔,一次 fill 完成全部填充:

// ── 外环:三角剖分 + move_to 合并路径 ──
let tris = triangulate(outer);
if !tris.is_empty() {
    let t0 = &tris[0];
    let mut fill_path = Path::new(to_screen(&outer[t0[0]]));
    fill_path.line_to(to_screen(&outer[t0[1]]));
    fill_path.line_to(to_screen(&outer[t0[2]]));
    for tri in &tris[1..] {
        fill_path.move_to(to_screen(&outer[tri[0]]));
        fill_path.line_to(to_screen(&outer[tri[1]]));
        fill_path.line_to(to_screen(&outer[tri[2]]));
    }
    result.push((fill_path, format!("fill:{}", city_idx)));
}

然后为解决凹多边形的问题,改进了一下剖分算法,这里在AI的帮助下,搞了一个耳切法(我以前写过类似的,但是也是这次知道,这种算法叫做耳切法)

耳切法,核心理论是双耳定理——任何简单多边形至少有两个耳朵。算法循环找耳朵、切除、继续,直到只剩三个点。判断耳朵需要两个条件:顶点是凸的(叉积符号与绕行方向一致),三角形内不包含其他顶点。

/// 耳切法三角剖分,将多边形环分解为三角形列表。
///
/// 参数:
///   ring: 多边形顶点列表(首尾相同,末位是重复点)
/// 返回值:
///   Vec<[usize; 3]> — 三角形顶点索引数组,每个三角形用三个索引表示
///
/// 用途:
/// 返回的三角形可用 move_to 合并为单一 Path(避免接缝),
/// 也可单独渲染每个三角形。
pub fn triangulate(ring: &Ring) -> Vec<[usize; 3]> {
    // ── 去掉末尾重复点(GeoJSON 规范要求首尾相同)──
    let end = if ring.len() > 1 && ring[0] == ring[ring.len() - 1] {
        ring.len() - 1
    } else {
        ring.len()
    };
    if end < 3 { return vec![]; }

    let mut indices: Vec<usize> = (0..end).collect();
    let mut triangles = Vec::new();

    // ── 有向面积判断绕行方向 ──
    let mut area = 0.0;
    for i in 0..end {
        let j = (i + 1) % end;
        area += ring[i].0 * ring[j].1;
        area -= ring[j].0 * ring[i].1;
    }
    let is_ccw = area > 0.0;

    // ── 循环切耳 ──
    while indices.len() > 3 {
        let len = indices.len();
        let mut ear_found = false;

        for i in 0..len {
            let prev = ring[indices[(i + len - 1) % len]];
            let curr = ring[indices[i]];
            let next = ring[indices[(i + 1) % len]];

            // 凸性检查:叉积符号须与绕行方向一致
            let cross = cross_2d(prev, curr, next);
            if is_ccw && cross <= 0.0 { continue; }
            if !is_ccw && cross >= 0.0 { continue; }

            // 检查三角形内是否包含其他顶点
            let mut is_ear = true;
            for &idx in &indices {
                if idx == indices[(i + len - 1) % len]
                    || idx == indices[i]
                    || idx == indices[(i + 1) % len] { continue; }
                if point_in_triangle(ring[idx], prev, curr, next) {
                    is_ear = false; break;
                }
            }

            if is_ear {
                triangles.push([indices[(i + len - 1) % len], indices[i], indices[(i + 1) % len]]);
                indices.remove(i);
                ear_found = true;
                break;
            }
        }

        // ── 强制切耳(容错)──
        if !ear_found {
            let mut best_i = 0;
            let mut best_cross = f64::MAX;
            for i in 0..indices.len() {
                let prev = ring[indices[(i + indices.len() - 1) % indices.len()]];
                let curr = ring[indices[i]];
                let next = ring[indices[(i + 1) % indices.len()]];
                let cross = cross_2d(prev, curr, next).abs();
                if cross < best_cross { best_cross = cross; best_i = i; }
            }
            triangles.push([
                indices[(best_i + indices.len() - 1) % indices.len()],
                indices[best_i],
                indices[(best_i + 1) % indices.len()],
            ]);
            indices.remove(best_i);
        }
    }

    if indices.len() == 3 {
        triangles.push([indices[0], indices[1], indices[2]]);
    }
    triangles
}

对于带孔洞的复杂多边形,严格意义的耳朵可能不存在。算法会在这种情况下退一步,选择最接近凸点的顶点(叉积绝对值最小)强制切除。这个容错机制确保了三角剖分不会因为几何退化而中断。

辅助函数 cross_2d 计算三个点的叉积,判断转向方向。point_in_triangle 用同侧法判断点是否在三角形内部,即分别计算点相对于三条边的叉积,如果三个符号相同则在内部。

/// 计算二维叉积 (B-A) x (C-B)。
/// 正值 = 左转(逆时针转向),负值 = 右转(顺时针转向)。
fn cross_2d(a: (f64, f64), b: (f64, f64), c: (f64, f64)) -> f64 {
    (b.0 - a.0) * (c.1 - b.1) - (b.1 - a.1) * (c.0 - b.0)
}

/// 判断点 P 是否在三角形 (A, B, C) 内部(含边界)。
/// 使用同侧法:分别计算 P 相对于三条边的叉积;
/// 若三个符号全正或全负,则 P 在三角形内部。
fn point_in_triangle(p: (f64, f64), a: (f64, f64), b: (f64, f64), c: (f64, f64)) -> bool {
    let d1 = (b.0 - a.0) * (p.1 - a.1) - (b.1 - a.1) * (p.0 - a.0);
    let d2 = (c.0 - b.0) * (p.1 - b.1) - (c.1 - b.1) * (p.0 - b.0);
    let d3 = (a.0 - c.0) * (p.1 - c.1) - (a.1 - c.1) * (p.0 - c.0);
    let has_neg = d1 < 0.0 || d2 < 0.0 || d3 < 0.0;
    let has_pos = d1 > 0.0 || d2 > 0.0 || d3 > 0.0;
    !(has_neg && has_pos)
}

描边

GPUI 没有提供专门的描边方法。好在官方示例里有个技巧:创建一个细长的平行四边形路径,用 fill 来模拟 stroke。对每条边,判断它更接近水平还是垂直,然后在垂直方向偏移 1px。

// ── 外环逐边描边 ──
// 为外环的每条边创建一条 1px 宽的薄条路径。
// 原理:painting 示例中"绘制线条"的技巧——
//   沿线段创建一个细长的平行四边形,用 fill 模拟 stroke。
//   对于水平边(dx > dy),垂直偏移 1px;
//   对于垂直边(dy >= dx),水平偏移 1px。
let end = outer.len() - 1;
for i in 0..end - 1 {
    let a = to_screen(&outer[i]);
    let b = to_screen(&outer[i + 1]);
    let diff_x = (b.x - a.x).abs();
    let diff_y = (b.y - a.y).abs();
    let (off_x, off_y) = if diff_x > diff_y {
        (px(0.0), px(1.0))
    } else {
        (px(1.0), px(0.0))
    };
    let mut seg = Path::new(a);
    seg.line_to(b);
    seg.line_to(point(b.x + off_x, b.y + off_y));
    seg.line_to(point(a.x + off_x, a.y + off_y));
    result.push((seg, String::from("stroke")));
}

比较大的面要素,例如呼伦贝尔市的边界有超过 1500 个坐标点,每帧要生成 1500 个薄条路径。更准确地说,对于一条 N 个点的边界,描边需要 N-1 条薄条。GPUI 是 GPU 加速的,所以即使要素数量再多,也能保持 60 帧。如果 GPUI 没有 GPU 加速,这个方案根本跑不动。

孔洞

GeoJSON 规范规定多边形的外环是顺时针方向,内环(孔洞)是逆时针方向。这是有意为之的——渲染器可以通过绕行方向来判断一个环是外边界还是内边界。内蒙古自治区的数据中就有带孔洞的要素,比如呼和浩特市的主体边界内包含了其他行政飞地。

处理方式是把内环也用三角剖分剖开,然后用全不透明的白色填充,从视觉上挖空。白色正好是背景色,所以看起来就像是在城市填充色上挖了一个洞。如果三角剖分失败了一个复杂几何形状偶尔会出现这种情况,后备方案是直接用单路径闭合填充。

如果不处理这个问题的话,就是这样的:

处理完之后就变成这样了:

// ── 处理内环(孔洞)──
// GeoJSON 规范中,内环应为逆时针(CCW)绕行。
// 用白色(背景色)填充内环区域,在视觉上"挖空"孔洞。
for inner in polygon.rings.iter().skip(1) {
    if inner.len() < 3 { continue; }
    let hole_tris = triangulate(inner);
    let hole_path = if !hole_tris.is_empty() {
        let h0 = &hole_tris[0];
        let mut p = Path::new(to_screen(&inner[h0[0]]));
        p.line_to(to_screen(&inner[h0[1]]));
        p.line_to(to_screen(&inner[h0[2]]));
        for tri in &hole_tris[1..] {
            p.move_to(to_screen(&inner[tri[0]]));
            p.line_to(to_screen(&inner[tri[1]]));
            p.line_to(to_screen(&inner[tri[2]]));
        }
        p
    } else {
        // 后备:单路径闭合填充
        let mut p = Path::new(to_screen(&inner[0]));
        for coord in &inner[1..inner.len() - 1] {
            p.line_to(to_screen(coord));
        }
        p
    };
    result.push((hole_path, String::from("hole")));
}

管线

下面介绍GPU渲染的一个关键术语:管线。

GPU 渲染管线(Graphics Pipeline)就是指:GPU 把 “3D/2D 数据” 变成 “屏幕上一张图” 的固定流水线工序。

GPU 渲染管线 = 一串固定且可编程的标准化流程,把顶点 → 三角形 → 像素,一步步变成屏幕上的颜色。

GPUI 的 canvas 提供了两个回调。

  • Pre-paint 在布局完成后调用,负责计算几何路径,返回值会传给 paint。
  • Paint 负责实际绘制,根据路径标签设置不同颜色。这个双回调机制是 GPUI 的特色,在布局阶段可以访问实际的像素尺寸,从而精确控制渲染内容。

最后就是标注了:城市名称的标签用 GPUI 的 Div 元素做 absolute 定位,而不是画在 canvas 上。这样做的好处是利用了 GPUI 的文本渲染能力,不需要手动处理字体和排版。标签位置用多边形质心坐标计算,质心直接简单的用是所有顶点的算术平均。

注意:真实是label 渲染,凸多边形没啥问题,但是凹多边形会导致质心跑到图形以外,生产环境下,需要使用基于有向面积的积分公式法‌(也称为鞋带公式的推广或高斯公式法) 来计算,这里为了省事,就不去做了。

另外,文字的位置与label点的位置也要注意计算偏离,不然容易和我演示的一样,文字会跑出去……不过同上,这里为了省事就不去做了。


// 渲染部分的代码
impl Render for MapView {
    fn render(&mut self, window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
        let features = self.data.features.clone();
        let geobounds = self.data.geobounds;
        let viewport = window.viewport_size();

        // ── 城市标签(质心 → 屏幕坐标)──
        let labels: Vec<(f64, f64, String)> = features.iter().filter_map(|f| {
            f.polygons.first()
                .map(|poly| polygon_center(poly))
                .map(|(cx, cy)| {
                    let (sx, sy) = geo_to_screen(cx, cy, geobounds, viewport);
                    (sx, sy, f.name.clone())
                })
        }).collect();

        let label_elements: Vec<Div> = labels.into_iter().map(|(x, y, name)| {
            div().absolute().left(px(x as f32)).top(px(y as f32)).child(name)
        }).collect();

        const FILL_COLORS: [u32; 11] = [
            0x4A90D9, 0xE86C00, 0x8BC34A, 0xE83E3E, 0x9B59B6, 0x20B2AA,
            0xF08529, 0x3C8DBC, 0x8E44AD, 0x27AE60, 0xD35400,
        ];

        div().bg(white()).size_full().child(
            canvas(
                // ── Pre-paint 回调 ──
                move |bounds, _, _| -> Option<Vec<(Path<Pixels>, String)>> {
                    if features.is_empty() { return None; }
                    // ... 计算投影参数,遍历要素,构建路径列表 ...
                    Some(result)
                },
                // ── Paint 回调:按标签类型绘制路径 ──
                move |_, paths, window, _| {
                    if let Some(paths) = paths {
                        for (path, tag) in paths.iter() {
                            match tag.as_str() {
                                "stroke" => window.paint_path(path.clone(), rgba(0x444444FF)),
                                "hole"   => window.paint_path(path.clone(), rgba(0xFFFFFFFF)),
                                _ if tag.starts_with("fill:") => {
                                    let idx: usize = tag[5..].parse().unwrap_or(0);
                                    let base = FILL_COLORS[idx % FILL_COLORS.len()];
                                    window.paint_path(path.clone(), rgba((base << 8) | 0x88));
                                }
                                _ => {}
                            }
                        }
                    }
                },
            ).size_full(),
        ).children(label_elements)
    }
}

最后就是GPUI的入口函数了,这个很简单创建 Application,打开窗口,创建 MapView 作为根视图。从文件加载数据,如果加载失败就创建一个空的数据集。

fn main() {
    Application::new().run(|cx: &mut App| {
        cx.open_window(WindowOptions::default(), |_window, cx| {
            cx.new(|_cx| match MapView::from_geojson_file("data/中国_省.geojson") {
                Ok(view) => {
                    for f in &view.data.features { println!("  - {}", f.name); }
                    view
                }
                Err(e) => {
                    eprintln!("读取文件失败: {}", e);
                    MapView { data: util::parse_geojson_str("{}") }
                }
            })
        }).expect("failed to open window");
    });
}

结论

测试结果比较明确。GPUI 的 Canvas 渲染管线能够处理数千条路径的同时绘制保持 60 帧,Path 的 move_to 机制为复杂的矢量图形渲染提供了灵活的拼接能力。纯 Rust 的依赖栈意味着编译出来的程序启动快、内存占用低,没有 JVM 或 V8 的开销。

目前发现的两个限制。

  • 第一,GPUI 没有内置的描边 API,需要用薄条路径来模拟,对于极端复杂的边界会生成大量临时路径。
  • 第二,GPUI 对于 macOS 和 Linux 平台的支持比 Windows 要好,明显感觉性能要高不少。

总的来说,对于需要高性能矢量图形渲染的桌面 GIS 工具,GPUI 是一个值得关注的方向。渲染管线是 GPU 加速的,Path API 足够灵活,开发体验也不错——就是用 Rust 写 UI 的逻辑需要适应一下。

后记

  1. 文章的中的绝大部分部分代码(超过90%),都是AI直接写的,我的作用就是告诉AI,应该往那个方向走,如下:

例如渲染呼伦贝尔的时候,多部件渲染颜色出来问题,我得告诉他怎么做:

又比如环形在GIS里面的特殊结构

当然,如果你不告诉他,有可能多烧上几块十几块的token,也有可能烧出来,但是你只要给出解决方案,AI解决起来那是嗷嗷的快。

  1. 这篇文章是在我主动蒸馏了虾神博客写作风格的skills的帮助下完成的初稿,我在初稿上做的修改完成的:

下面就虾神的赛博器灵:

我很多时候,觉得我已经废了……

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐