前言
在 Rhino 建模和出图过程中,你是否遇到过这样的困惑:
文字在视图中看起来完全正常,但
Explode成曲线后却发现是反的或倒的?
这个问题困扰了很多用户,尤其在出图、导出 DWG、或进行 CNC 加工时会造成严重错误。本文将深入分析这个问题的原因,并提供完整的解决方案。
一、问题现象
1.1 典型场景
屏幕显示: Explode 后:
┌─────────┐ ┌─────────┐
│ TEXT │ → │ TXET │ ← 镜像了!
└─────────┘ └─────────┘
或者:
┌─────────┐ ┌─────────┐
│ TEXT │ → │ ┴XƎ┴ │ ← 倒转了!
└─────────┘ └─────────┘
1.2 为什么显示时看不出来?
Rhino 的文字对象有一个自动朝向阅读者的功能:
文字属性 → Annotation Style →
☑ Orient text toward reader
这个功能让文字在任何视角下都"面向你",方便阅读。但这也掩盖了文字的真实几何方向。
二、理解文字的平面坐标系
2.1 每个文字都有自己的 Plane
在 Rhino 中,文字是基于一个平面(Plane)创建的:
↑ Y-axis (文字上方)
│
│ T E X T
│
────────┼────────────→ X-axis (文字方向)
│
Origin (文字定位点)
Z-axis = X × Y (垂直于文字表面,指向阅读者)
2.2 用 What 命令查看
Command: What
选择文字对象
输出示例:
Point: (100, 200, 0)
X-axis: (1, 0, 0) ← 文字方向
Y-axis: (0, 1, 0) ← 文字上方
2.3 判断正反向
通过 X 轴和 Y 轴可以计算 Z 轴(法向量):
Z = X × Y (叉乘)
如果 Z.z > 0 → 正向(从上往下看是正的)
如果 Z.z < 0 → 反向(从上往下看是反的)
三、两种常见问题
3.1 问题一:Z 轴朝下(镜像反向)
正常: 反向:
Z = (0, 0, +1) ↑ Z = (0, 0, -1) ↓
│ │
┌───────┐ ┌───────┐
│ TEXT │ │ TXET │ ← 镜像
└───────┘ └───────┘
原因:
- 在错误的 CPlane 上创建文字
- 从底部视图创建
- 复制/镜像操作导致
3.2 问题二:旋转 180°(倒转)
正常: 倒转:
X-axis → (1, 0, 0) X-axis → (-1, 0, 0)
Y-axis ↑ (0, 1, 0) Y-axis ↓ (0, -1, 0)
│ │
┌───────┐ ┌───────┐
│ TEXT │ │ ⊥XƎ⊥ │ ← 上下颠倒
└───────┘ └───────┘
原因:
- 创建时角度输入错误
- 旋转操作时多转了 180°
- 从特定角度的 CPlane 创建
四、手动检查方法
4.1 方法一:使用 Dir 命令
Command: Dir
选择文字
→ 显示法向量箭头
→ 箭头朝上 = 正向
→ 箭头朝下 = 反向
4.2 方法二:使用 Gumball
选中文字 → 观察 Gumball 的 Z 轴(蓝色箭头)
↑ 蓝色箭头朝上 = 正向
↓ 蓝色箭头朝下 = 反向
4.3 方法三:关闭自动朝向
Properties → Annotation →
☐ Orient text toward reader (取消勾选)
→ 文字会显示真实方向
→ 反的就会显示反的
五、Python 自动修正脚本
5.1 核心原理
python
# 1. 获取文字的平面
plane = text.Plane
# 2. 检查 Z 轴方向
if plane.ZAxis.Z < 0:
# 反向,需要翻转
# 3. 检查是否倒转
angle = atan2(plane.XAxis.Y, plane.XAxis.X)
if angle > 90° or angle < -90°:
# 倒转,需要旋转 180°
5.2 完整脚本
python
#coding=utf-8
import Rhino
import rhinoscriptsyntax as rs
import scriptcontext as sc
from Rhino.Geometry import Plane, Vector3d, Transform
import math
def normalize_all_text():
"""
修正所有文字方向:
- Z 轴统一朝上
- 文字不倒转
"""
# 获取所有 annotation 对象
text_ids = rs.ObjectsByType(512)
if not text_ids:
print("No text objects found")
return
stats = {"flipped": 0, "rotated": 0, "ok": 0}
for text_id in text_ids:
obj = sc.doc.Objects.Find(text_id)
if obj is None:
continue
# 确认是文字对象
if not isinstance(obj.Geometry, Rhino.Geometry.TextEntity):
continue
text = obj.Geometry
plane = text.Plane
origin = plane.Origin
x_axis = plane.XAxis
z_axis = plane.ZAxis
# ========== 情况 1:Z 轴朝下 ==========
if z_axis.Z < 0:
# 绕 X 轴旋转 180 度,翻转到正面
xform = Transform.Rotation(math.pi, x_axis, origin)
sc.doc.Objects.Transform(text_id, xform, True)
stats["flipped"] += 1
continue
# ========== 情况 2:倒转 180 度 ==========
angle = math.atan2(x_axis.Y, x_axis.X)
angle_deg = math.degrees(angle)
# X 轴角度在 90° ~ 270° 之间表示文字倒转
if angle_deg > 90 or angle_deg < -90:
# 绕 Z 轴旋转 180 度
xform = Transform.Rotation(math.pi, Vector3d.ZAxis, origin)
sc.doc.Objects.Transform(text_id, xform, True)
stats["rotated"] += 1
continue
stats["ok"] += 1
# 刷新视图
sc.doc.Views.Redraw()
# 输出统计
print("========== Done ==========")
print("Flipped (Z-axis): " + str(stats["flipped"]))
print("Rotated (180 deg): " + str(stats["rotated"]))
print("Already OK: " + str(stats["ok"]))
print("Total: " + str(sum(stats.values())))
# 运行脚本
normalize_all_text()
5.3 使用方法
-
打开 Python 编辑器
Command: EditPythonScript -
粘贴脚本并运行 (F5)
-
查看结果
========== Done ========== Flipped (Z-axis): 12 Rotated (180 deg): 5 Already OK: 83 Total: 100
六、变体脚本
6.1 只处理选中的文字
python
# 替换获取文字的方式
text_ids = rs.GetObjects("Select text to fix", 512)
if not text_ids:
print("No text selected")
return
6.2 只检查不修改(预览模式)
python
# 把修改代码注释掉,只打印信息
if z_axis.Z < 0:
print("Need flip: " + str(text.PlainText))
stats["flipped"] += 1
continue # 不执行 Transform
6.3 处理特定图层
python
target_layer = "Annotation"
for text_id in text_ids:
layer = rs.ObjectLayer(text_id)
if target_layer not in layer:
continue
# ... 后续处理
七、预防措施
7.1 创建文字前设置正确的 CPlane
Command: CPlane
选择 World Top 或目标平面
然后再创建文字
7.2 创建后立即检查
创建文字 → Dir 命令 → 确认箭头朝上
7.3 出图前批量检查
1. 保存文件(备份)
2. 运行修正脚本
3. 关闭 "Orient text toward reader"
4. 视觉检查
5. 导出/出图
八、原理深入:为什么 X × Y 能判断方向?
8.1 右手定则
右手定则:
↑ Z (拇指)
│
│ ╱ Y (中指)
│ ╱
│ ╱
└──────→ X (食指)
X 叉乘 Y = Z
8.2 计算示例
正常文字:
X = (1, 0, 0)
Y = (0, 1, 0)
Z = X × Y = (0×0 - 0×0, 0×0 - 1×0, 1×1 - 0×0) = (0, 0, 1) ✓
反向文字:
X = (1, 0, 0)
Y = (0, -1, 0) ← Y 轴反了
Z = X × Y = (0, 0, 1×(-1) - 0×0) = (0, 0, -1) ✗
8.3 简化判断
对于 XY 平面上的文字:
python
z_component = x_axis.X * y_axis.Y - x_axis.Y * y_axis.X
if z_component > 0:
print("正向")
else:
print("反向")
九、总结
| 问题 | 原因 | 解决方案 |
|---|---|---|
| Explode 后镜像 | Z 轴朝下 | 绕 X 轴翻转 180° |
| Explode 后倒转 | X 轴角度 > 90° | 绕 Z 轴旋转 180° |
| 无法直观判断 | 自动朝向功能 | 用 Dir / Gumball / What |
核心要点:
- 文字有自己的平面坐标系 (Origin, X, Y, Z)
- Z 轴方向决定正反
- X 轴角度决定是否倒转
- 自动朝向功能会掩盖真实方向
- 使用脚本可以批量修正
附录:参考资料
- Rhino Developer Docs: TextEntity Class
- Rhino Python Guide: rhinoscriptsyntax
如有问题或建议,欢迎留言讨论!