cad.net 布局+视口

布局

删除布局并切换布局

可能Acad前端需要拦截切换布局,并加入刷新界面的原因,
数据库没有切换布局属性,例如db.Layout = ?
我们仍然需要通过布局管理器,而它的取值会经过WorkingDatabase,
所以后台打开图纸后,要设置WorkingDatabase,否则会报错 eSetFailed,
其实它就是一个全局变量,就像我们的事务栈一样.

那为什么不像 db.CLayer = "0" 它也会引起前端界面变动啊,
真是鬼知道桌子怎么想的.

官网论坛说此会失败:
using var tr = db.TransactionManager.StartOpenCloseTransaction();

// 前台,当前文档
[CommandMethod(nameof(Test2))]
public void Test2() {
    using DBTrans tr = new();
    DeleteVoidLayouts();
}

// 批量任务前台后台
[CommandMethod(nameof(Test1), CommandFlags.Session)]
public void Test1() {
    // 当前进程已经打开的文档们
    Dictionary<string, Document> docMap = new();
    Document? oldDoc = null;
    var dm = Acap.DocumentManager;
    foreach (Document doc in dm) {
        if (doc.IsDisposed || doc.ReadOnly) continue;
        if (doc.IsActive) oldDoc = doc;
        docMap.Add(doc.Name, doc); // doc.Name是文件而不是文件名
    }

    // 遍历文件夹获取dwg文件
    var files = ...().ToHashSet();

    // 除了只读文档,当前进程的文档们必然占用,排除掉,减少IO次数
    var currentFiles = files.Where(file => docMap.ContainsKey(file)).ToList();
    files.ExceptWith(currentFiles);

    // 1,后台任务,进程没有打开的文件
    var unoFiles = files.Where(file => !IsFileOpen(file)).ToHashSet();
    foreach (var file in unoFiles) {
        BackendTask(file, file, ()=> { DeleteVoidLayouts(); });
    }

    // 2,无法处理
    foreach (var file in files.Except(unoFiles)) {
        Env.Print("无法处理已经打开但不是本进程的文件: " + file);
    }

    // 3,前台任务,每个都激活为当前文档
    // 为什么需要激活呢?因为前台打印需要,避免出现不同逻辑
    foreach (var file in currentFiles) {
        var doc = docMap[file];
        if (!doc.IsActive) {
            dm.MdiActiveDocument = doc;
            HostApplicationServices.WorkingDatabase = doc.Database;
        }
        using var docLocker = doc.DocumentLock();
        using (DBTrans tr = new(doc.Database)) {
            DeleteVoidLayouts();
        }
        // 这里要发送异步命令去保存
        // https://www.cnblogs.com/JJBox/p/12156778.html
        doc.SendStringToExecute("_qsave\n", false, true, true);
    }

    // 恢复旧激活
    if (oldDoc is not null) {
        dm.MdiActiveDocument = oldDoc;
        HostApplicationServices.WorkingDatabase = oldDoc.Database;
    }
}

bool IsFileOpen(string file) {
    try {
        // adndev官网推荐用追加模式判断占用
        using (FileStream fs = new(file,
            FileMode.Append, FileAccess.Write, FileShare.None)) { }
        return false;
    } catch {}
    return true;
}


// 后台并没有提供直接操作布局切换的方式,
// 我们仍然需要用布局管理器,而它需要设置WorkingDatabase
// 后台任务,打开图纸删除未使用布局
public void BackendTask(string dwgFile, string newFile, Action action) {
    if (action is null)
        throw new ArgumentNullException(nameof(action), "action is null");

    // 关闭全部图纸执行此函数sdb是空的.
    // 你可以关闭全部文档,然后发送win32API消息,这样就能跑此函数了.
    Database? sdb = HostApplicationServices.WorkingDatabase;
    if (sdb is null)
        throw new ArgumentNullException("必须要存在一个前台文档");
    try {
        using var db = new Database(false, true);
        db.ReadDwgFile(dwgFile, System.IO.FileShare.Read, false, "");
        db.CloseInput(true);
        HostApplicationServices.WorkingDatabase = db;
        using (DBTrans tr = new(db)) {
            action();
            // 一旦用了布局管理器切换布局,始终在提交事务时候出错,
            // AutoCAD错误中断
            // 致命错误: Unhandled Access Violation Reading 0x0010 Exception at CFASC28h
            // 怀疑是全局变量不能保留后台的引用,
            // 我是想象成当它保存关闭后,全局变量此时显示一个已经不存在的引用了,
            // 但是为什么是事务提交时候检查,而不是保存时候检查呢?我并不知道.
            // 所以顶替回来居然好了...真是邪门玩意.
            // 这样还说明了必须要存在一个前台文档,否则切换不回来.(可以set null吗)
            HostApplicationServices.WorkingDatabase = sdb;
        }
        db.SaveAs(newFile, true, DwgVersion.Current, db.SecurityParameters);
    } finally {
        HostApplicationServices.WorkingDatabase = sdb;
    }
}


/*
    注释这种方式删除布局,会导致图纸损坏出现需要修复图纸.
    layout.UpgradeOpen();
    layout.Erase();
    btr.UpgradeOpen();
    btr.Erase();
*/

/// <summary>
/// 刪除未使用布局
/// </summary>
public void DeleteVoidLayouts() {
    var tr = DBTrans.Top;

    // 排除模型后,只有一个布局不能删除,否则破坏DWG结构
    using var lm = LayoutManager.Current;
    int layoutNum = lm.LayoutCount -1;
    if (layoutNum == 1) return;
    // 记录当前空间,用于还原,被删除则不处理
    string? oldLayout = lm.CurrentLayout;
    // 删除前切换到模型,避免悬空指针.
    db.TileMode = true;

    // 删除未使用布局,删除布局2,留下布局1.
    // 按照名称排序
    var olist = GetLayoutMap(db, tr)
        .OrderByDescending(pair => pair.Key);

    // 遍历并初始化,防止出错,需要提权.
    foreach(var pair in olist) {
        using var layout = (Layout)tr.GetObject(pair.Value, OpenMode.ForWrite);
        layout.Initialize();

        // 获取布局内容前要切换否则有错误,这个可能不需要
        // lm.CurrentLayout = layout.Name;
        int entCounter = 0;
        int vpCounter = 0;

        // 打开布局块表记录,视口数量如果大于2就结束.
        // 有了初始化之后,
        // 会有1号视口,加上图元视口,所以要大于2.
        using var btr = (BlockTableRecord)tr.GetObject(layout.BlockTableRecordId, OpenMode.ForRead);
        foreach (var entId in btr) {
            if (!entId.IsOk()) continue;
            entCounter++;
            using var ent = tr.GetObject(entId, OpenMode.ForRead);
            if (ent is Viewport) vpCounter++;
            if (entCounter > 2) break;
        }

        if (entCounter > 2) continue;

        // 没有图元 或 全是视口,就删除
        if (entCounter == 0 || entCounter == vpCounter) {
            if (layout.LayoutName == oldLayout) oldLayout = null;
            lm.DeleteLayout(layout.LayoutName);
            // 剩下唯一布局就终止任务
            layoutNum--;
            if (layoutNum == 1) break;
        }
    }

    // 若已被删除,切换到模型,否则切换回旧布局
    if (oldLayout is null) db.TileMode = true;
    else lm.CurrentLayout = oldLayout;
}


/// <summary>
/// 激活第一个布局为当前布局
/// </summary>
/// <returns>(旧布局名,新布局名)若一样就是没改</returns>
public (string oldName, string newName) ActiveFirstLayout() {
    var tr = DBTrans.Top;
    using var lm = LayoutManager.Current;
    var oldName = lm.CurrentLayout;
    var newName = lm.CurrentLayout;

    // TabOrder[0]是Model跳过它
    using var layouts = (DBDictionary)tr.GetObject(tr.Database.LayoutDictionaryId);
    foreach (DBDictionaryEntry entry in layouts) {
        using var layout = (Layout)tr.GetObject(entry.Value);
        if (layout.TabOrder == 1) {
            if (oldName != layout.LayoutName) {
                newName = layout.LayoutName;
                lm.CurrentLayout = layout.LayoutName;
            }
            break;
        }
    }
    return (oldName, newName);
}

布局名转为map

其实布局管理器就是一个map了...写完才知道...
就可以用layout.BlockTableRecordId来打开遍历了.
这种应该是官方预留索引的方式:
db.NamedObjectsDict.GetAt("ACAD_LAYOUT")

// map<布局名,布局id>
public Dictionary<string, ObjectId> GetLayoutMap(Database db,
    Transaction? tr = null, bool excludeModel = true) {
    tr ??= DBTrans.Top;
    Dictionary<string, ObjectId> map = new();
    using var layouts = (DBDictionary)tr.GetObject(db.LayoutDictionaryId, OpenMode.ForRead);
    foreach(DBDictionaryEntry entry in layouts) {
        var layoutName = entry.Key;
        var layoutId = entry.Value;
        if (!layoutId.IsOk()) continue;
        if (excludeModel && layoutName == "Model") continue;
        map.Add(layoutName, layoutId);
    }
    return map;
}

// 和上面等价,不要用这个,这是遍历块表的,难怪我之前觉得怪怪的
// map<布局名,布局id>
public Dictionary<string, ObjectId> GetLayoutMap(Database db,
    Transaction? tr = null, bool excludeModel = true) {
    tr ??= DBTrans.Top;
    Dictionary<string, ObjectId> map = new();
    using var bt = (BlockTable)tr.GetObject(db.BlockTableId, OpenMode.ForRead);
    foreach (ObjectId id in bt)
        if (!id.IsOk()) continue;
        using var btr = (BlockTableRecord)tr.GetObject(id, OpenMode.ForRead);
        if (!btr.IsLayout) continue;
        if (excludeModel && btr.Name == "*Model_Space") continue;
        map.Add(btr.Name, btr.LayoutId);
    }
    return map;
}

统计布局的图框数量

// map<布局名,图框数量>
// 其实还可以写新函数 map<布局名,(图框名,图框数量)>
public Dictionary<string, int> GetTkMap(Dictionary<string, ObjectId> layoutMap, Transaction? tr = null) {
    tr ??= DBTrans.Top;
    Dictionary<string, int> map = new();
    foreach(var pair in layoutMap) {
        using var layout = (Layout)tr.GetObject(pair.Value, OpenMode.ForRead);
        using var btr = (BlockTableRecord)tr.GetObject(layout.BlockTableRecordId, OpenMode.ForRead);
        int tkNum = 0;
        foreach (var entId in btr) {
            if (!entId.IsOk()) continue;
            using var ent = tr.GetObject(entId, OpenMode.ForRead);
            if (ent is not BlockReference brf) continue;
            if (brf.Name.Contains("图框") || brf.Name.Contains("TK"))
                tkNum++;
        }
        map.Add(pair.Key, tkNum);
    }
    return map;
}

public void PrintTkNum(Dictionary<string, int> tkMap) {
    foreach (var pair in tkMap) {
        Env.Print($"布局名: {pair.Key}");
        Env.Print($"图框数量: {pair.Value}");
    }
}

创建布局

创建布局最好不要和视口在同一个事务内.
为什么需要这样?
因为你想想只用一个事务时,
撤回可能先回滚视口,此时布局还不存在,出现一个悬空指针.
而桌子没有处理这种行为.
不管桌子有没有处理,
基于肯定不处理的形式写确定性代码,我们分两次提交事务.

public class TestCreateLayoutCommands {
    [CommandMethod(nameof(TestCreateLayout))]
    public void TestCreateLayout() {
        // 1,布局管理器
        // 后台使用前设置工作数据库(前台设置也没事)
        var dm = Acap.DocumentManager;
        var doc = dm.MdiActiveDocument;
        HostApplicationServices.WorkingDatabase = doc.Database;

        LayoutManager lm = LayoutManager.Current;
        var name = "新的布局";

        // 2,创建布局后必须立即提交事务
        // 创建并初始化工作,构造1号视口.
        if (!lm.LayoutExists(name)) {
            using DBTrans tr2 = new();
            var layoutId2 = lm.CreateLayout(name);
            using var layout2 = (Layout)tr2.GetObject(layoutId2, OpenMode.ForWrite);
            layout2.Initialize();
        }

        // 3,获取布局内容前要切换,(可能有错误?)
        // lm.CurrentLayout = name;

        // 开新事务做其后的工作
        using DBTrans tr = new();

        // 修改布局内容
        // 方案一: 获取全部视口,包含1号视口和-1号.
        // 跳过一号视口,删除其他视口,你不能用写模式访问1号视口!
        var layoutId = lm.GetLayoutId(name);
        using var layout = (Layout)tr.GetObject(layoutId);
        using var ids = layout.GetViewports();
        foreach(var vpId in ids) {
            using var vp = (Viewport)tr.GetObject(vpId);
            if (vp.Number == 1) continue;
            vp.UpgradeOpen();
            vp.Erase();
        }

        // 方案二: 获取全部视口,包含1号视口和-1号.
        // 布局转为块表记录,遍历每个图元
        using var layout = (Layout)tr.GetObject(layoutId, OpenMode.ForRead);
        using var btr = (BlockTableRecord)tr.GetObject(layout.BlockTableRecordId, OpenMode.ForRead);
        foreach (var entId in btr) {

        }

    }
}

// 其他例子....
// 切换布局,通过id形式.
lm.SetCurrentLayoutId(lm.GetLayoutId(name));

视口

创建视口

using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Runtime;
using Autodesk.AutoCAD.Geometry;

[CommandMethod("CreateViewport")]
public void CreateViewport() {
    Document doc = Application.DocumentManager.MdiActiveDocument;
    Database db = doc.Database;

    using var tr = db.TransactionManager.StartTransaction();
    
    // 打开布局空间
    var ps = (BlockTableRecord)tr.GetObject(db.CurrentSpaceId, OpenMode.ForWrite);

    // 创建一个新的视口
    var vp = new Viewport();
    vp.SetDatabaseDefaults();

    // 布局的视口图形边界:矩形视口计算边界中心点
    double left = rect.Left;
    double bottom = rect.Bottom;
    double right = rect.Right;
    double top = rect.Top;
    vp.CenterPoint = new Point3d((left + right) / 2, (bottom + top) / 2, 0);
    vp.Width = right-left;
    vp.Height = top-bottom;

    // 视口的视图:
    // 绕点:视图目标点,一般情况下和模型原点一致,相对坐标下它会被更改.
    vp.ViewTarget = new Point3d(0,0,0);
    // 视点:视图观察点
    vp.ViewCenter = new Point2d(0, 0);
    // 转轴:视图观察方向(Z轴,朝向屏幕)
    vp.ViewDirection = new Vector3d(0, 0, 1);

    // 扭曲夹角
    // 不改变 ViewCenter 和 ViewDirection 的情况下得到旋转的视觉效果
    // 但是如果有三维视图就变得复杂和彷徨无措起来.
    vp.TwistAngle = 0.0;

    // 自定义比例,也就是比例因子
    vp.CustomScale = 1.0 / 100.0;
    tr.AddNewlyCreatedDBObject(vp, true);
    ps.AppendEntity(vp);
    vp.On = true;
    vp.UpdateDisplay();
    tr.Commit();
}

三者关系

绕点,视点,角度,它们三者的关系

public void UpdateViewCenter(Viewport vp, Point3d basept, Point3d target) {
    // 1. 获取视口的关键参数
    Point3d vt = vp.ViewTarget;   // 绕点,DCS的原点
    Point2d vc = vp.ViewCenter;   // 视点,DCS的点,从绕点开始计算.
    Vector3d vd = vp.ViewDirection;  // 转轴

    // 2. 计算旋转角度
    // 扭角是依照转轴计算,
    // 而在Z轴旋转的时候从X轴逆时针计算而已.
    double wa = 2 * Math.PI - vp.TwistAngle;

    // 3. 构造旋转矩阵
    Matrix3d roMatrix = RotationMatrix(wa, vd);

    // 4. 平移和正旋转
    // 视点相加还是DCS,
    // 只是采用Point3d.Origin旋转简化运算
    Point3d pt = vt + vc;
    pt = pt * roMatrix;

    // 5. 处理坐标系转换和平移
    // 视点根据交互两点进行平移到目标向量
    // 这交互的两点是切换回去模型再交互的,
    // 要保证坐标系一样,所以说UCS转WCS
    basept = ConvertToDcs(basept);
    target = ConvertToDcs(target);
    pt = pt - basept + target;

    // 6. 逆旋转和平移
    pt = pt * roMatrix.Inverse();
    pt = pt - vt;

    // 7. 更新视点
    vp.ViewCenter = pt;
}

    // Acad不需要写 罗德里格公式, 而是调用就好了
    // 求逆Matrix.Inverse()
    // 这个世界有两种人,一种人会把角度改为负数,一种人会改转轴方向
    // 将世界坐标转换为视图坐标,
    public static Matrix3d WorldToEye(Viewport view) =>
        Matrix3d.WorldToPlane(view.ViewDirection) *
        Matrix3d.Displacement(view.ViewTarget.GetAsVector().Negate()) *
        Matrix3d.Rotation(view.ViewTwist, view.ViewDirection, view.ViewTarget);


// 这里夹角可能需要加个负号
// 计算旋转矩阵(罗德里格公式)
public static Matrix3d RotationMatrix(double angle, Vector3d axis) {
    // 单位化旋转轴
    axis = axis.Normalize();
    double kx = axis.X;
    double ky = axis.Y;
    double kz = axis.Z;
    
    double cosθ = Math.Cos(angle);
    double sinθ = Math.Sin(angle);
    
    // 计算各个矩阵元素的值
    double m00 = cosθ + (1 - cosθ) * kx * kx;
    double m01 = kx * ky * (1 - cosθ) - kz * sinθ;
    double m02 = kx * kz * (1 - cosθ) + ky * sinθ;

    double m10 = ky * kx * (1 - cosθ) + kz * sinθ;
    double m11 = cosθ + (1 - cosθ) * ky * ky;
    double m12 = ky * kz * (1 - cosθ) - kx * sinθ;

    double m20 = kz * kx * (1 - cosθ) - ky * sinθ;
    double m21 = kz * ky * (1 - cosθ) + kx * sinθ;
    double m22 = cosθ + (1 - cosθ) * kz * kz;

    // 创建矩阵实例
    return new Matrix3d(
        m00, m01, m02,
        m10, m11, m12,
        m20, m21, m22
    );
}

// 应用,也就是矩阵乘法
private Point3d MultiplyMatrixPoint(Matrix3d matrix, Point3d point) {
    double x = matrix[0] * point.X + matrix[1] * point.Y + matrix[2] * point.Z;
    double y = matrix[3] * point.X + matrix[4] * point.Y + matrix[5] * point.Z;
    double z = matrix[6] * point.X + matrix[7] * point.Y + matrix[8] * point.Z;
    return new Point3d(x, y, z);
}

private Point3d ConvertToDcs(Point3d ucsPoint)
{
    // UCS→WCS转换
    Point3d wcsPoint = ucsPoint.UcsToWcs();
    // WCS→DCS转换
    return wcsPoint.WcsToDcs();
}

注释比例

这个AI生成的不知道对不对

// 获取当前文档和数据库
Document doc = Application.DocumentManager.MdiActiveDocument;
Database db = doc.Database;

using (Transaction tr = db.TransactionManager.StartTransaction())
{
    // 获取当前视口
    Viewport vp = doc.ActiveViewport;

    // 创建注释比例对象
    AnnotationScale asd = new();
    asd.Scale = 1.0; // 设置注释比例为1:1
    asd.Custom = false; // 设置为非自定义注释比例

    // 将注释比例应用到视口
    vp.AnnotationScaleId = asd;

    tr.Commit();
}

任意轴

众所周知,Z轴的旋转是从X轴开始的,并且逆时针为正.
逆时针好懂,因为是右手坐标系决定旋转为正的方向.

但是为什么是X轴呢?其实很简单,是基于自然约定.
如何自然呢?一个3*3的单位矩阵是这样的:
1,0,0
0,1,0
0,0,1
可以看见这个斜三角的美.
单位矩阵延伸出三个基础旋转矩阵,
即为绕X旋转矩阵,绕Y旋转矩阵,绕Z旋转矩阵,
然后就又回到了X轴了

那么三维任意轴的话,按理说有无数根起始轴啊?
此时又是怎么确定起始轴呢?
让我们问问AI,得到的信息是,直接通过 罗德里格公式.

罗德里格公式,它本质上就是Z轴旋转矩阵的一种泛化,
它需要两个参数,
一个是转轴,一个是角度,得到一个旋转矩阵.

也就是你可以很笨的先从任意轴重合到Z轴,
重合这个不难理解,求三个夹角,然后三个轴分别旋转一次就行了.
再实现依照Z轴旋转,最后逆转回去.
不过由于太笨了...罗德里格带着它的公式走来了.

不过,在Acad上面写个啥矩阵,肯定有函数已经写了.

    // 将世界坐标转换为视图坐标
    // 这个世界有两种人,一种人会把角度改为负数,一种人会改转轴方向
    public static Matrix3d WorldToEye(this AbstractViewTableRecord view) =>
        Matrix3d.WorldToPlane(view.ViewDirection) *
        Matrix3d.Displacement(view.Target.GetAsVector().Negate()) *
        Matrix3d.Rotation(view.ViewTwist, view.ViewDirection, view.Target);

首先是根据右手定则,表示旋转数值为正的方向.
其次是单位化向量,但是单位化之后仍然不能确定起始轴,
浅薄的认识,
平行分量和垂直分量构成公式,
点乘是两根向量组成直角三角形,
那么夹角+一根向量的逆运算就可以求出另一个根.

    // 示例:绕任意轴(0.7, 0.9, 6.2) 旋转45°(即π/4弧度)
    public static void Main2()
    {
        Vector3 axis = new Vector3(0.7f, 0.9f, 6.2f);
        double angle = Math.PI / 4; // 逆时针

        Matrix4x4 rotationMatrix = RotationMatrix(axis, angle);

        // 应用旋转矩阵到点(1,0,0)
        Vector3 point = new Vector3(1, 0, 0);
        Vector3 rotatedPoint = Vector3.Transform(point, rotationMatrix);
        Console.WriteLine($"旋转后的点: ({rotatedPoint.X:F4}, {rotatedPoint.Y:F4}, {rotatedPoint.Z:F4})");
    }
using System;
using System.Numerics;

public class RotationExample
{
    // 计算旋转矩阵(罗德里格公式)
    public static Matrix4x4 RotationMatrix(Vector3 axis, double angle)
    {
        // 逆时针为正
        angle = -angle;

        // 单位化旋转轴(转换为double计算)
        double x = axis.X, y = axis.Y, z = axis.Z;
        double norm = Math.Sqrt(x * x + y * y + z * z);
        double ux = x / norm, uy = y / norm, uz = z / norm;

        double costheta = Math.Cos(angle);
        double sintheta = Math.Sin(angle);
        double oneMinusCostheta = 1 - costheta;

        // 计算旋转矩阵元素
        double m00 = costheta + oneMinusCostheta * ux * ux;
        double m01 = oneMinusCostheta * ux * uy - sintheta * uz;
        double m02 = oneMinusCostheta * ux * uz + sintheta * uy;

        double m10 = oneMinusCostheta * uy * ux + sintheta * uz;
        double m11 = costheta + oneMinusCostheta * uy * uy;
        double m12 = oneMinusCostheta * uy * uz - sintheta * ux;

        double m20 = oneMinusCostheta * uz * ux - sintheta * uy;
        double m21 = oneMinusCostheta * uz * uy + sintheta * ux;
        double m22 = costheta + oneMinusCostheta * uz * uz;

        // 转换为float类型并构造Matrix4x4
        return new Matrix4x4(
            (float)m00, (float)m01, (float)m02, 0,
            (float)m10, (float)m11, (float)m12, 0,
            (float)m20, (float)m21, (float)m22, 0,
            0, 0, 0, 1
        );
    }

    // 两个向量生成旋转矩阵
    public static Matrix4x4 RotationBetweenVectors(Vector3 from, Vector3 to)
    {
        // 计算旋转轴(叉积)
        Vector3 axis = Vector3.Cross(from, to);
        // 计算夹角(点积)
        double dot = Vector3.Dot(from, to);
        double angle = Math.Acos(dot / (from.Length() * to.Length())); // 已转为弧度
        return RotationMatrix(axis, angle);
    }

    public static void Main()
    {
        // 测试参数: 旋转轴(0,0,1), 角度π/2弧度, 要逆时针为正
        Vector3 axis = new Vector3(0, 0, 1);
        double angle = Math.PI / 2;

        // 计算旋转矩阵
        Matrix4x4 rotationMatrix = RotationMatrix(axis, angle);

        // 验证标准基向量变换
        Vector3 pointX = new Vector3(1, 0, 0);
        Vector3 rotatedX = Vector3.Transform(pointX, rotationMatrix);
        Console.WriteLine($"X轴向量(1,0,0)旋转后:({rotatedX.X:F2}, {rotatedX.Y:F2}, {rotatedX.Z:F2})");

        Vector3 pointY = new Vector3(0, 1, 0);
        Vector3 rotatedY = Vector3.Transform(pointY, rotationMatrix);
        Console.WriteLine($"Y轴向量(0,1,0)旋转后:({rotatedY.X:F2}, {rotatedY.Y:F2}, {rotatedY.Z:F2})");

        Vector3 pointZ = new Vector3(0, 0, 1);
        Vector3 rotatedZ = Vector3.Transform(pointZ, rotationMatrix);
        Console.WriteLine($"Z轴向量(0,0,1)旋转后:({rotatedZ.X:F2}, {rotatedZ.Y:F2}, {rotatedZ.Z:F2})");
        
        // 预期输出:
        // (0.00, 1.00, 0.00)
        // (-1.00, 0.00, 0.00)
        // (0.00, 0.00, 1.00)
    }
}
posted @   惊惊  阅读(121)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 提示词工程——AI应用必不可少的技术
· 地球OL攻略 —— 某应届生求职总结
· 字符编码:从基础到乱码解决
· SpringCloud带你走进微服务的世界
点击右上角即可分享
微信分享提示