feat(文件拆分): 拆分几何三维基础能力
build.yaml / build (push) Successful in 48s

This commit is contained in:
xiaoqidun committed 2026-10-14 00:44:44 +08:00
1 parent 64f9ec4241
commit 0be872691d
76 files changed
+1926 -1307

No files matched your search

+168
View File
@@ -0,0 +1,168 @@
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package geom
import (
"fmt"
"math"
)
// geometryArc 保存中心式椭圆及有向参数区间
type geometryArc struct {
center, u, v Point
theta, delta float64
}
// Curves 按毫米误差上限将端点式椭圆弧转换为三次贝塞尔曲线,其他指令保持原样
// 入参: tolerance 正有限误差上限
// 返回: Path 独立路径, error 参数错误
func (p Path) Curves(tolerance float64) (Path, error) {
if err := p.Validate(); err != nil {
return nil, err
}
if !positive(tolerance) {
return nil, fmt.Errorf("invalid curve tolerance")
}
result := make(Path, 0, len(p))
var current, start Point
for _, s := range p {
switch s.Verb {
case Move:
start = s.End
case Arc:
curves, err := ArcCurves(current, s, tolerance)
if err != nil {
return nil, err
}
result = append(result, curves...)
current = s.End
continue
case ClosePath:
result = append(result, s)
current = start
continue
case Line, Quad, Cubic:
default:
return nil, fmt.Errorf("unsupported geometry command %d", s.Verb)
}
result = append(result, s)
current = s.End
}
return result, nil
}
// ArcCurves 按标准端点参数求椭圆中心,只在绘制边界近似弧线
// 入参: start 起点, arc 弧线, tolerance 毫米误差上限
// 返回: Path 贝塞尔片段, error 无效弧线
func ArcCurves(start Point, arc Segment, tolerance float64) (Path, error) {
a, err := centerArc(start, arc)
if err != nil {
return nil, err
}
if start == arc.End {
return nil, nil
}
if arc.RadiusX == 0 || arc.RadiusY == 0 {
return Path{{Verb: Line, End: arc.End}}, nil
}
radius := math.Max(math.Hypot(a.u.X, a.u.Y), math.Hypot(a.v.X, a.v.Y))
step := math.Min(math.Pi/2, math.Pow(tolerance/radius*40000, 1.0/6))
n := math.Ceil(math.Abs(a.delta) / step)
if !finite(n) || n < 1 || n > 1<<18 {
return nil, fmt.Errorf("arc subdivision limit exceeded")
}
count := int(n)
step = a.delta / float64(count)
point := func(x, y float64) Point {
return Point{a.center.X + a.u.X*x + a.v.X*y, a.center.Y + a.u.Y*x + a.v.Y*y}
}
result := make(Path, 0, count)
for i := 0; i < count; i++ {
sa, ca := math.Sincos(a.theta + float64(i)*step)
sb, cb := math.Sincos(a.theta + float64(i+1)*step)
k := 4.0 / 3 * math.Tan(step/4)
result = append(result, Segment{Verb: Cubic, Control1: point(ca-k*sa, sa+k*ca), Control2: point(cb+k*sb, sb-k*cb), End: point(cb, sb)})
}
result[len(result)-1].End = arc.End
return result, nil
}
// centerArc 将端点式圆弧解析为中心式,统一半径修正和方向语义
// 入参: start 起点, arc 弧线
// 返回: geometryArc 中心式参数, error 无效参数或数值溢出
func centerArc(start Point, arc Segment) (geometryArc, error) {
rx, ry := arc.RadiusX, arc.RadiusY
if !finite(rx) || !finite(ry) || rx < 0 || ry < 0 || !finite(arc.Rotation) ||
!finite(start.X) || !finite(start.Y) || !finite(arc.End.X) || !finite(arc.End.Y) {
return geometryArc{}, fmt.Errorf("invalid arc parameters")
}
if start == arc.End {
return geometryArc{}, nil
}
if rx == 0 || ry == 0 {
return geometryArc{}, nil
}
angle := math.Mod(arc.Rotation, 360) * math.Pi / 180
sine, cosine := math.Sincos(angle)
dx, dy := (start.X-arc.End.X)/2, (start.Y-arc.End.Y)/2
x, y := cosine*dx+sine*dy, -sine*dx+cosine*dy
scale := math.Hypot(x/rx, y/ry)
if scale > 1 {
rx *= scale
ry *= scale
}
u, v := x/rx, y/ry
square := u*u + v*v
factor := math.Sqrt(math.Max(0, (1-square)/square))
if arc.Large == arc.Sweep {
factor = -factor
}
cx, cy := factor*rx*v, -factor*ry*u
theta := math.Atan2((y-cy)/ry, (x-cx)/rx)
end := math.Atan2((-y-cy)/ry, (-x-cx)/rx)
delta := math.Mod(end-theta, 2*math.Pi)
if arc.Sweep && delta < 0 {
delta += 2 * math.Pi
}
if !arc.Sweep && delta > 0 {
delta -= 2 * math.Pi
}
center := Point{cosine*cx - sine*cy + (start.X+arc.End.X)/2, sine*cx + cosine*cy + (start.Y+arc.End.Y)/2}
if !finite(center.X) || !finite(center.Y) || !finite(rx) || !finite(ry) || !finite(delta) || !finite(theta) || delta == 0 {
return geometryArc{}, fmt.Errorf("invalid arc extent")
}
return geometryArc{center, Point{rx * cosine, rx * sine}, Point{-ry * sine, ry * cosine}, theta, delta}, nil
}
// point 计算椭圆参数对应的坐标
// 入参: angle 参数角
// 返回: Point 椭圆上的点
func (a geometryArc) point(angle float64) Point {
s, c := math.Sincos(angle)
return Point{a.center.X + a.u.X*c + a.v.X*s, a.center.Y + a.u.Y*c + a.v.Y*s}
}
// fraction 判断参数角是否位于有向弧段内
// 入参: angle 参数角
// 返回: float64 弧段比例
func (a geometryArc) fraction(angle float64) float64 {
d := math.Mod(angle-a.theta, 2*math.Pi)
if a.delta > 0 && d < 0 {
d += 2 * math.Pi
} else if a.delta < 0 && d > 0 {
d -= 2 * math.Pi
}
return d / a.delta
}
+129
View File
@@ -0,0 +1,129 @@
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package geom
import (
"fmt"
"math"
)
// Bounds 通过解析极值计算直线、贝塞尔和椭圆弧的轴对齐边界
// 不进行曲线展开且不使用Tolerance,结果受float64舍入误差影响,不保证向外舍入
// 返回: Box 浮点路径范围, error 无效路径、弧线或数值溢出
func (p Path) Bounds() (Box, error) {
if err := p.Validate(); err != nil {
return Box{}, err
}
left, top, right, bottom := math.Inf(1), math.Inf(1), math.Inf(-1), math.Inf(-1)
add := func(p Point) {
left, top = math.Min(left, p.X), math.Min(top, p.Y)
right, bottom = math.Max(right, p.X), math.Max(bottom, p.Y)
}
var current, start Point
for _, s := range p {
switch s.Verb {
case Move:
start = s.End
case ClosePath:
s.End = start
case Arc:
a, err := centerArc(current, s)
if err != nil {
return Box{}, err
}
if a.delta != 0 {
for _, angle := range []float64{math.Atan2(a.v.X, a.u.X), math.Atan2(a.v.Y, a.u.Y)} {
for _, angle := range []float64{angle, angle + math.Pi} {
if f := a.fraction(angle); f > 0 && f < 1 {
add(a.point(angle))
}
}
}
}
case Quad, Cubic:
for _, axis := range [][4]float64{{current.X, s.Control1.X, s.Control2.X, s.End.X}, {current.Y, s.Control1.Y, s.Control2.Y, s.End.Y}} {
if s.Verb == Quad {
axis[2] = 0
}
scale := math.Max(math.Abs(axis[0]), math.Max(math.Abs(axis[1]), math.Max(math.Abs(axis[2]), math.Abs(axis[3]))))
if scale != 0 {
for i := range axis {
axis[i] /= scale
}
}
var roots []float64
if s.Verb == Quad {
roots = geometryRoots(0, axis[0]-2*axis[1]+axis[3], axis[1]-axis[0])
} else {
roots = geometryRoots(-axis[0]+3*axis[1]-3*axis[2]+axis[3], 2*(axis[0]-2*axis[1]+axis[2]), axis[1]-axis[0])
}
for _, t := range roots {
if t > 0 && t < 1 {
q := Lerp(current, s.Control1, t)
if s.Verb == Quad {
q = Lerp(q, Lerp(s.Control1, s.End, t), t)
} else {
r := Lerp(s.Control1, s.Control2, t)
q = Lerp(Lerp(q, r, t), Lerp(r, Lerp(s.Control2, s.End, t), t), t)
}
add(q)
}
}
}
}
add(s.End)
current = s.End
}
if len(p) == 0 {
return Box{}, nil
}
if !finite(left) || !finite(top) || !finite(right-left) || !finite(bottom-top) {
return Box{}, fmt.Errorf("geometry bounds overflow")
}
return Box{left, top, right - left, bottom - top}, nil
}
// geometryRoots 稳定求解一元二次方程的实根
// 入参: a 二次项系数, b 一次项系数, c 常数项
// 返回: []float64 实根
func geometryRoots(a, b, c float64) []float64 {
scale := math.Max(math.Abs(a), math.Max(math.Abs(b), math.Abs(c)))
if scale == 0 {
return nil
}
a, b, c = a/scale, b/scale, c/scale
if a == 0 {
if b == 0 {
return nil
}
return []float64{-c / b}
}
d := b*b - 4*a*c
if d < 0 {
return nil
}
q := -.5 * (b + math.Copysign(math.Sqrt(d), b))
if q == 0 {
return []float64{-b / (2 * a)}
}
return []float64{q / a, c / q}
}
// Lerp 线性插值坐标
// 入参: a 起点, b 终点, t 插值比例
// 返回: Point 插值坐标
func Lerp(a, b Point, t float64) Point {
return Point{(1-t)*a.X + t*b.X, (1-t)*a.Y + t*b.Y}
}
+87
View File
@@ -0,0 +1,87 @@
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package geom
import (
"math"
)
// ConvexContains 判断单个非退化凸直线区域是否包含另一闭合直线区域
// 入参: outer 外部区域, inner 内部区域
// 返回: bool 是否完全包含,曲线及多轮廓返回false
func ConvexContains(outer, inner Path) bool {
a, b := geometryPolygonPoints(outer), geometryPolygonPoints(inner)
if len(a) < 3 || len(b) < 3 {
return false
}
nondegenerate := false
for i, start := range a {
end := a[(i+1)%len(a)]
dx, dy := end.X-start.X, end.Y-start.Y
length := math.Hypot(dx, dy)
if !finite(length) {
return false
}
if length == 0 {
continue
}
dx, dy = dx/length, dy/length
distance := func(point Point) float64 {
x, y := point.X-start.X, point.Y-start.Y
value := math.FMA(dx, y, -dy*x)
bound := (math.Abs(dx*y) + math.Abs(dy*x) + math.Abs(dx)*(math.Abs(point.Y)+math.Abs(start.Y)) + math.Abs(dy)*(math.Abs(point.X)+math.Abs(start.X))) * 0x1p-49
if finite(bound) && math.Abs(value) <= bound {
return 0
}
return value
}
sign := 0.0
for _, point := range a {
value := distance(point)
if !finite(value) || sign*value < 0 {
return false
}
if value != 0 {
sign, nondegenerate = math.Copysign(1, value), true
}
}
for _, point := range b {
value := distance(point)
if !finite(value) || sign*value < 0 {
return false
}
}
}
return nondegenerate
}
// geometryPolygonPoints 读取单个显式闭合直线轮廓的顶点,不修改原路径
// 入参: path 直线轮廓
// 返回: []Point 独立顶点,其他路径返回nil
func geometryPolygonPoints(path Path) []Point {
if len(path) < 4 || path[0].Verb != Move || path[len(path)-1].Verb != ClosePath {
return nil
}
for i, segment := range path[:len(path)-1] {
if i > 0 && segment.Verb != Line || !finite(segment.End.X) || !finite(segment.End.Y) {
return nil
}
}
points := make([]Point, len(path)-1)
for i := range points {
points[i] = path[i].End
}
return points
}
+194
View File
@@ -0,0 +1,194 @@
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package geom
import (
"math"
)
// IdentityMatrix 单位矩阵
var IdentityMatrix = Matrix{1, 0, 0, 1, 0, 0}
// Box 矩形区域
type Box struct {
X, Y, W, H float64
}
// Point 平面坐标点
type Point struct {
X, Y float64
}
// Matrix 2D仿射变换矩阵
type Matrix struct {
a, b, c, d, e, f float64
}
// MatrixFromValues 从a、b、c、d、e、f构造矩阵,不经过文本格式化
// 入参: values 矩阵分量
// 返回: Matrix 变换矩阵
func MatrixFromValues(values [6]float64) Matrix {
return Matrix{a: values[0], b: values[1], c: values[2], d: values[3], e: values[4], f: values[5]}
}
// Values 返回a、b、c、d、e、f的独立副本,保留原始浮点精度
// 返回: [6]float64 矩阵分量
func (m Matrix) Values() [6]float64 {
return [6]float64{m.a, m.b, m.c, m.d, m.e, m.f}
}
// Multiply 矩阵乘法 (m * o)
// 入参: o 右侧矩阵
// 返回: Matrix 结果矩阵
func (m Matrix) Multiply(o Matrix) Matrix {
return Matrix{
a: m.a*o.a + m.c*o.b,
b: m.b*o.a + m.d*o.b,
c: m.a*o.c + m.c*o.d,
d: m.b*o.c + m.d*o.d,
e: m.a*o.e + m.c*o.f + m.e,
f: m.b*o.e + m.d*o.f + m.f,
}
}
// Transform 应用变换矩阵
// 入参: x X坐标, y Y坐标
// 返回: float64 变换后X, float64 变换后Y
func (m Matrix) Transform(x, y float64) (float64, float64) {
nx := m.a*x + m.c*y + m.e
ny := m.b*x + m.d*y + m.f
return nx, ny
}
// TransformBox 获取矩形经仿射变换后的轴对齐边界
// 入参: box 原始矩形范围
// 返回: Box 变换后的轴对齐边界
func (m Matrix) TransformBox(box Box) Box {
x, y := m.Transform(box.X, box.Y)
left, top, right, bottom := x, y, x, y
for _, point := range [][2]float64{{box.X + box.W, box.Y}, {box.X, box.Y + box.H}, {box.X + box.W, box.Y + box.H}} {
x, y := m.Transform(point[0], point[1])
left, top, right, bottom = math.Min(left, x), math.Min(top, y), math.Max(right, x), math.Max(bottom, y)
}
return Box{X: left, Y: top, W: right - left, H: bottom - top}
}
// Invert 求仿射矩阵的逆变换
// 返回: Matrix 逆矩阵,失败时返回单位矩阵, bool 是否存在有限逆矩阵
func (m Matrix) Invert() (Matrix, bool) {
for _, value := range m.Values() {
if !finite(value) {
return IdentityMatrix, false
}
}
det, ds := matrixProductDifference(m.a, m.d, m.b, m.c)
x, xs := matrixProductDifference(m.c, m.f, m.d, m.e)
y, ys := matrixProductDifference(m.b, m.e, m.a, m.f)
if det != 0 && ds && xs && ys {
n := Matrix{a: m.d / det, b: -m.b / det, c: -m.c / det, d: m.a / det, e: x / det, f: y / det}
valid := true
for _, value := range n.Values() {
valid = valid && finite(value)
}
if valid {
return n, true
}
}
determinant, exponent := matrixProductSum(m.a, m.d, -m.b, m.c)
if determinant == 0 {
return IdentityMatrix, false
}
divide := func(value float64, power int) float64 {
mantissa, shift := math.Frexp(value)
return math.Ldexp(mantissa/determinant, power+shift-exponent)
}
x, xe := matrixProductSum(m.c, m.f, -m.d, m.e)
y, ye := matrixProductSum(m.b, m.e, -m.a, m.f)
n := Matrix{a: divide(m.d, 0), b: divide(-m.b, 0), c: divide(-m.c, 0), d: divide(m.a, 0), e: divide(x, xe), f: divide(y, ye)}
for _, value := range n.Values() {
if !finite(value) {
return IdentityMatrix, false
}
}
return n, true
}
// matrixProductDifference 计算两个乘积之差,检查溢出、下溢及相消造成的精度损失
// 入参: a 第一个乘数, b 第二个乘数, c 第三个乘数, d 第四个乘数
// 返回: float64 乘积之差, bool 是否可直接用于求逆
func matrixProductDifference(a, b, c, d float64) (float64, bool) {
x, y := a*b, c*d
value := x - y
if !finite(value) || x == 0 && a != 0 && b != 0 || y == 0 && c != 0 && d != 0 {
return value, false
}
return value, x == 0 && y == 0 || math.Abs(value) >= 0x1p-1022 && math.Abs(value) >= 0x1p-48*math.Max(math.Abs(x), math.Abs(y))
}
// matrixProductSum 以尾数及指数累加两个乘积,保留乘法舍入残差
// 入参: a 第一个乘数, b 第二个乘数, c 第三个乘数, d 第四个乘数
// 返回: float64 结果尾数, int 二进制指数
func matrixProductSum(a, b, c, d float64) (float64, int) {
a, ae := math.Frexp(a)
b, be := math.Frexp(b)
c, ce := math.Frexp(c)
d, de := math.Frexp(d)
x, y := a*b, c*d
xe, ye := ae+be, ce+de
power := max(xe, ye)
if x == 0 {
power = ye
} else if y == 0 {
power = xe
}
value := math.Ldexp(x, xe-power) + math.Ldexp(y, ye-power)
value += math.Ldexp(math.FMA(a, b, -x), xe-power) + math.Ldexp(math.FMA(c, d, -y), ye-power)
value, shift := math.Frexp(value)
return value, power + shift
}
// TranslationMatrix 构造平移矩阵
// 入参: x X轴偏移, y Y轴偏移
// 返回: Matrix 平移矩阵
func TranslationMatrix(x, y float64) Matrix {
return Matrix{a: 1, d: 1, e: x, f: y}
}
// YScale 获取Y轴缩放比例
// 返回: float64 缩放比例
func (m Matrix) YScale() float64 {
return math.Sqrt(m.c*m.c + m.d*m.d)
}
// Equal 比较毫米坐标,使用固定绝对误差,不受绘图库全局精度影响
// 入参: a 第一个坐标, b 第二个坐标
// 返回: bool 是否在容差内相等
func Equal(a, b float64) bool {
return math.Abs(a-b) <= 1e-10
}
// AxisAligned 判断变换是否保持坐标轴平行,含直角旋转与镜像
// 入参: m 仿射变换矩阵
// 返回: bool 是否为非退化的轴对齐变换
func AxisAligned(m Matrix) bool {
return m.b == 0 && m.c == 0 && m.a != 0 && m.d != 0 || m.a == 0 && m.d == 0 && m.b != 0 && m.c != 0
}
// Vector 变换位移,不包含平移分量
// 入参: m 仿射变换矩阵, x 原始横向位移, y 原始纵向位移
// 返回: float64 变换后的X位移, float64 变换后的Y位移
func Vector(m Matrix, x, y float64) (float64, float64) {
return m.a*x + m.c*y, m.b*x + m.d*y
}
+31
View File
@@ -0,0 +1,31 @@
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package geom
import (
"math"
)
// finite 判断数值是否有限
// 入参: value 数值
// 返回: bool 是否有限
func finite(value float64) bool {
return !math.IsNaN(value) && !math.IsInf(value, 0)
}
// positive 判断正有限数
// 入参: v 待检查数值
// 返回: bool 是否为正有限数
func positive(v float64) bool { return v > 0 && !math.IsInf(v, 0) && !math.IsNaN(v) }
+160
View File
@@ -0,0 +1,160 @@
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package geom
import (
"fmt"
"math"
)
// 几何路径指令,包括直线、曲线、椭圆弧和闭合
const (
Move Verb = iota
Line
Quad
Cubic
Arc
ClosePath
)
// Path 保存独立于绘图库的几何路径,不扁平化曲线或舍入坐标
// 坐标以左上角为原点,单位为毫米,子路径从Move开始,Close回到当前子路径起点
type Path []Segment
// Verb 表示几何路径指令
type Verb uint8
// Segment 保存路径端点、贝塞尔控制点或端点式椭圆弧参数
// 弧线Rotation单位为度,Sweep为true表示顺时针,Large表示大弧
type Segment struct {
Verb Verb
End Point
Control1, Control2 Point
RadiusX, RadiusY float64
Rotation float64
Large, Sweep bool
}
// StrokeOptions 描述描边参数,长度与路径坐标采用相同单位
// Width须为正有限数;Cap和Join使用标准线帽和连接样式名称,空值分别采用Butt和Miter
// MiterLimit为尖角长度与半线宽的比值上限,由调用方设置默认值
// MiterLimitSet供调用方区分显式零值与默认值,不参与几何计算
// DashOffset为虚线起始偏移,允许负值;Dashes交替指定实线段和间隔长度,奇数项按重复一遍处理
// Tolerance为曲线展开的误差目标,0采用0.001
type StrokeOptions struct {
Width float64
Cap, Join string
MiterLimit float64
MiterLimitSet bool
DashOffset float64
Tolerance float64
Dashes []float64
}
// Close 为填充轮廓补齐隐式闭合边,不改变源路径
// 入参: path 填充路径
// 返回: Path 显式闭合路径
func Close(path Path) Path {
open, missing := false, false
for _, segment := range path {
if segment.Verb == Move && open {
missing = true
break
}
open = segment.Verb != ClosePath
}
if !missing && !open {
return path
}
result := make(Path, 0, len(path)+1)
open = false
for _, segment := range path {
if segment.Verb == Move && open {
result = append(result, Segment{Verb: ClosePath})
}
result = append(result, segment)
open = segment.Verb != ClosePath
}
if open {
result = append(result, Segment{Verb: ClosePath})
}
return result
}
// ValidateStroke 校验各后端共用的描边参数
// 入参: options 描边样式
// 返回: error 无效样式
func ValidateStroke(options StrokeOptions) error {
if !finite(options.Width) || options.Width <= 0 || !finite(options.Tolerance) || options.Tolerance < 0 || !finite(options.DashOffset) || !finite(options.MiterLimit) || options.MiterLimit < 0 {
return fmt.Errorf("invalid stroke width or tolerance")
}
if options.Cap != "" && options.Cap != "Butt" && options.Cap != "Round" && options.Cap != "Square" {
return fmt.Errorf("invalid stroke cap %q", options.Cap)
}
if options.Join != "" && options.Join != "Miter" && options.Join != "Round" && options.Join != "Bevel" {
return fmt.Errorf("invalid stroke join %q", options.Join)
}
for _, dash := range options.Dashes {
if !finite(dash) || dash < 0 {
return fmt.Errorf("invalid stroke dash")
}
}
return nil
}
// Rectangle 构建页面坐标矩形,保留输入精度
// 入参: box 矩形范围
// 返回: Path 闭合路径
func Rectangle(box Box) Path {
return Path{
{Verb: Move, End: Point{X: box.X, Y: box.Y}},
{Verb: Line, End: Point{X: box.X + box.W, Y: box.Y}},
{Verb: Line, End: Point{X: box.X + box.W, Y: box.Y + box.H}},
{Verb: Line, End: Point{X: box.X, Y: box.Y + box.H}},
{Verb: ClosePath, End: Point{X: box.X, Y: box.Y}},
}
}
// RectangleBounds 识别单个轴对齐矩形,不用包围盒代替实际路径
// 入参: path 填充路径
// 返回: Box 矩形范围, bool 是否为非退化矩形
func RectangleBounds(path Path) (Box, bool) {
if len(path) > 0 && path[len(path)-1].Verb == ClosePath {
path = path[:len(path)-1]
}
if len(path) == 5 && path[4].Verb == Line && path[4].End == path[0].End {
path = path[:4]
}
if len(path) != 4 || path[0].Verb != Move || path[1].Verb != Line || path[2].Verb != Line || path[3].Verb != Line {
return Box{}, false
}
a, b, c, d := path[0].End, path[1].End, path[2].End, path[3].End
for _, point := range [...]Point{a, b, c, d} {
if !finite(point.X) || !finite(point.Y) {
return Box{}, false
}
}
if a.X == c.X || a.Y == c.Y {
return Box{}, false
}
if !(a.X == b.X && b.Y == c.Y && c.X == d.X && d.Y == a.Y || a.Y == b.Y && b.X == c.X && c.Y == d.Y && d.X == a.X) {
return Box{}, false
}
width, height := math.Abs(c.X-a.X), math.Abs(c.Y-a.Y)
if !finite(width) || !finite(height) {
return Box{}, false
}
return Box{X: min(a.X, c.X), Y: min(a.Y, c.Y), W: width, H: height}, true
}
+411
View File
@@ -0,0 +1,411 @@
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package geom
import (
"fmt"
"math"
)
// UnsupportedError 表示当前几何实现无法展开的描边形态
type UnsupportedError string
// geometryPolyline 保存展开后的子路径,独立记录闭合状态
type geometryPolyline struct {
points []Point
closed bool
direction Point
}
// Error 返回不支持的描边原因
// 返回: string 错误信息
func (err UnsupportedError) Error() string {
return string(err)
}
// Stroke 按端点和转角样式将曲线、虚线转换为非零填充轮廓
// 曲线按Tolerance展开,默认误差为0.001毫米,重叠轮廓保持同向绕数
// 结果未做布尔整理,按非零规则填充,区域运算需能处理重叠子路径的后端
// 入参: options 描边样式
// 返回: Path 描边轮廓, error 无效参数或精度限制
func (p Path) Stroke(options StrokeOptions) (Path, error) {
if err := ValidateStroke(options); err != nil {
return nil, err
}
if err := p.Validate(); err != nil {
return nil, err
}
if options.Width/2 == 0 {
return nil, fmt.Errorf("stroke width precision exhausted")
}
dashes := append([]float64(nil), options.Dashes...)
total := 0.0
for _, d := range dashes {
total += d
}
if !finite(total) {
return nil, fmt.Errorf("stroke dash period overflow")
}
if total == 0 {
dashes = nil
} else {
if len(dashes)%2 != 0 {
dashes = append(dashes, dashes...)
total *= 2
if !finite(total) {
return nil, fmt.Errorf("stroke dash period overflow")
}
}
}
tolerance := options.Tolerance
if tolerance == 0 {
tolerance = .001
}
lines, err := p.strokePolylines(tolerance / 2)
if err != nil {
return nil, err
}
var result Path
for _, line := range lines {
parts := []geometryPolyline{line}
if len(dashes) != 0 {
parts, err = line.dash(dashes, total, options.DashOffset)
if err != nil {
return nil, err
}
}
for _, part := range parts {
result, err = part.stroke(result, options)
if err != nil {
return nil, err
}
if len(result) > 1<<20 {
return nil, fmt.Errorf("stroke outline limit exceeded")
}
}
}
if err := result.Validate(); err != nil {
return nil, err
}
return result, nil
}
// strokePolylines 自适应展开贝塞尔,弦长亏损同时约束虚线累计长度误差
// 入参: tolerance 展开误差
// 返回: []geometryPolyline 子路径, error 展开限制
func (p Path) strokePolylines(tolerance float64) ([]geometryPolyline, error) {
curves, err := p.Curves(tolerance / 2)
if err != nil {
return nil, err
}
var result []geometryPolyline
var line geometryPolyline
var current, start Point
drawn := false
flush := func() {
if drawn {
result = append(result, line)
}
line, drawn = geometryPolyline{}, false
}
count := 0
var flatten func([]Point, float64, int) error
flatten = func(points []Point, budget float64, depth int) error {
first, last := points[0], points[len(points)-1]
chord := math.Hypot(last.X-first.X, last.Y-first.Y)
length, distance := 0.0, 0.0
for i := 1; i < len(points); i++ {
length += math.Hypot(points[i].X-points[i-1].X, points[i].Y-points[i-1].Y)
if i == len(points)-1 {
continue
}
if chord == 0 {
distance = math.Max(distance, math.Hypot(points[i].X-first.X, points[i].Y-first.Y))
} else {
distance = math.Max(distance, math.Abs((points[i].X-first.X)*((last.Y-first.Y)/chord)-(points[i].Y-first.Y)*((last.X-first.X)/chord)))
}
}
if !finite(length) || !finite(distance) {
return fmt.Errorf("stroke curve overflow")
}
if distance <= tolerance && length-chord <= budget {
if last != line.points[len(line.points)-1] {
line.points = append(line.points, last)
count++
}
if count > 1<<18 {
return fmt.Errorf("stroke subdivision limit exceeded")
}
return nil
}
if depth == 24 {
return fmt.Errorf("stroke curve tolerance cannot be met")
}
var work, left, right [4]Point
copy(work[:], points)
for n := len(points); n > 0; n-- {
left[len(points)-n], right[n-1] = work[0], work[n-1]
for i := 0; i < n-1; i++ {
work[i] = Lerp(work[i], work[i+1], .5)
}
}
if err := flatten(left[:len(points)], budget/2, depth+1); err != nil {
return err
}
return flatten(right[:len(points)], budget/2, depth+1)
}
for _, s := range curves {
switch s.Verb {
case Move:
flush()
current, start = s.End, s.End
line.points = []Point{current}
case ClosePath:
if drawn {
if current != start {
line.points = append(line.points, start)
}
line.closed = true
}
flush()
current = start
line.points = []Point{current}
default:
drawn = true
var buffer [4]Point
points := append(buffer[:0], current)
if s.Verb == Quad || s.Verb == Cubic {
points = append(points, s.Control1)
}
if s.Verb == Cubic {
points = append(points, s.Control2)
}
points = append(points, s.End)
if err := flatten(points, tolerance/float64(len(curves)), 0); err != nil {
return nil, err
}
current = s.End
}
}
flush()
return result, nil
}
// dash 按子路径重置相位并合并跨闭合接缝的连续虚线
// 入参: pattern 非负偶数虚线数组, total 周期, offset 偏移
// 返回: []geometryPolyline 虚线段, error 长度或细分限制
func (line geometryPolyline) dash(pattern []float64, total, offset float64) ([]geometryPolyline, error) {
phase := math.Mod(offset, total)
if phase < 0 {
phase += total
}
index := 0
for phase > 0 && phase >= pattern[index] {
phase -= pattern[index]
index = (index + 1) % len(pattern)
}
if len(line.points) == 1 {
if index%2 == 0 {
return []geometryPolyline{line}, nil
}
return nil, nil
}
remaining := pattern[index] - phase
var result []geometryPolyline
var run []Point
var direction Point
flush := func() {
if len(run) != 0 {
result = append(result, geometryPolyline{points: run, direction: direction})
run = nil
}
}
steps := 0
for i := 1; i < len(line.points); i++ {
a, b := line.points[i-1], line.points[i]
length := math.Hypot(b.X-a.X, b.Y-a.Y)
if !finite(length) {
return nil, fmt.Errorf("stroke dash length overflow")
}
for at := 0.0; ; {
for remaining == 0 {
steps++
if steps > 1<<18 {
return nil, fmt.Errorf("stroke dash subdivision limit exceeded")
}
if pattern[index] == 0 && index%2 == 0 && len(run) == 0 {
result = append(result, geometryPolyline{points: []Point{Lerp(a, b, at/length)}, direction: Point{(b.X - a.X) / length, (b.Y - a.Y) / length}})
}
index = (index + 1) % len(pattern)
remaining = pattern[index]
if index%2 == 1 && remaining > 0 {
flush()
}
}
if at == length {
break
}
step := math.Min(remaining, length-at)
if at+step == at || steps > 1<<18 {
return nil, fmt.Errorf("stroke dash subdivision limit exceeded")
}
steps++
if index%2 == 0 {
if len(run) == 0 {
run = append(run, Lerp(a, b, at/length))
direction = Point{(b.X - a.X) / length, (b.Y - a.Y) / length}
}
end := Lerp(a, b, (at+step)/length)
if end != run[len(run)-1] {
run = append(run, end)
}
}
at += step
remaining -= step
}
}
flush()
if line.closed && len(result) > 0 {
first, last := &result[0], &result[len(result)-1]
if first.points[0] == line.points[0] && last.points[len(last.points)-1] == line.points[0] {
if len(result) == 1 {
first.closed = len(first.points) > 1
} else {
first.points = append(last.points[:len(last.points)-1], first.points...)
result = result[:len(result)-1]
}
}
}
return result, nil
}
// stroke 追加同向矩形、连接楔形及圆盘组成的非零填充区域
// 入参: result 已有轮廓, options 描边样式
// 返回: Path 填充轮廓, error 不支持的退化方头端点
func (line geometryPolyline) stroke(result Path, options StrokeOptions) (Path, error) {
half := options.Width / 2
polygon := func(points ...Point) {
scale := 0.0
for _, p := range points {
scale = math.Max(scale, math.Max(math.Abs(p.X-points[0].X), math.Abs(p.Y-points[0].Y)))
}
if scale == 0 {
return
}
area := 0.0
for i, p := range points {
q := points[(i+1)%len(points)]
area += ((p.X-points[0].X)/scale)*((q.Y-points[0].Y)/scale) - ((p.Y-points[0].Y)/scale)*((q.X-points[0].X)/scale)
}
if area == 0 {
return
}
if area < 0 {
for i, j := 0, len(points)-1; i < j; i, j = i+1, j-1 {
points[i], points[j] = points[j], points[i]
}
}
result = append(result, Segment{Verb: Move, End: points[0]})
for _, p := range points[1:] {
result = append(result, Segment{Verb: Line, End: p})
}
result = append(result, Segment{Verb: ClosePath})
}
disk := func(p Point) {
result = append(result, Segment{Verb: Move, End: Point{p.X + half, p.Y}},
Segment{Verb: Arc, RadiusX: half, RadiusY: half, Sweep: true, End: Point{p.X - half, p.Y}},
Segment{Verb: Arc, RadiusX: half, RadiusY: half, Sweep: true, End: Point{p.X + half, p.Y}},
Segment{Verb: ClosePath})
}
points := line.points
if len(points) == 1 {
if line.closed {
return nil, UnsupportedError("directionless closed stroke")
}
if options.Cap == "Round" {
disk(points[0])
} else if options.Cap == "Square" {
d := line.direction
if d == (Point{}) {
return nil, UnsupportedError("directionless square stroke cap")
}
p := points[0]
u, v := Point{d.X * half, d.Y * half}, Point{-d.Y * half, d.X * half}
polygon(Point{p.X - u.X - v.X, p.Y - u.Y - v.Y}, Point{p.X + u.X - v.X, p.Y + u.Y - v.Y}, Point{p.X + u.X + v.X, p.Y + u.Y + v.Y}, Point{p.X - u.X + v.X, p.Y - u.Y + v.Y})
}
return result, nil
}
directions := make([]Point, len(points)-1)
for i := range directions {
a, b := points[i], points[i+1]
length := math.Hypot(b.X-a.X, b.Y-a.Y)
if length == 0 || !finite(length) {
return nil, fmt.Errorf("invalid stroke segment length")
}
d := Point{(b.X - a.X) / length, (b.Y - a.Y) / length}
directions[i] = d
n := Point{-half * d.Y, half * d.X}
polygon(Point{a.X + n.X, a.Y + n.Y}, Point{b.X + n.X, b.Y + n.Y}, Point{b.X - n.X, b.Y - n.Y}, Point{a.X - n.X, a.Y - n.Y})
}
join := func(p, a, b Point) {
cross := a.X*b.Y - a.Y*b.X
if cross == 0 {
if options.Join == "Round" && a.X*b.X+a.Y*b.Y < 0 {
disk(p)
}
return
}
side := -math.Copysign(half, cross)
x := Point{p.X - side*a.Y, p.Y + side*a.X}
y := Point{p.X - side*b.Y, p.Y + side*b.X}
if options.Join == "Round" {
if cross < 0 {
x, y = y, x
}
result = append(result, Segment{Verb: Move, End: p},
Segment{Verb: Line, End: x},
Segment{Verb: Arc, RadiusX: half, RadiusY: half, Sweep: true, End: y},
Segment{Verb: ClosePath})
return
}
limit := options.MiterLimit
if options.Join == "" || options.Join == "Miter" {
t := ((y.X-x.X)*b.Y - (y.Y-x.Y)*b.X) / cross
m := Point{x.X + t*a.X, x.Y + t*a.Y}
if math.Hypot(m.X-p.X, m.Y-p.Y) <= half*limit {
polygon(p, x, m, y)
return
}
}
polygon(p, x, y)
}
for i := 1; i < len(points)-1; i++ {
join(points[i], directions[i-1], directions[i])
}
if line.closed {
join(points[0], directions[len(directions)-1], directions[0])
} else {
for _, end := range []struct{ p, d Point }{{points[0], Point{-directions[0].X, -directions[0].Y}}, {points[len(points)-1], directions[len(directions)-1]}} {
if options.Cap == "Round" {
disk(end.p)
} else if options.Cap == "Square" {
p, d := end.p, end.d
n := Point{-half * d.Y, half * d.X}
polygon(Point{p.X + n.X, p.Y + n.Y}, Point{p.X + n.X + half*d.X, p.Y + n.Y + half*d.Y}, Point{p.X - n.X + half*d.X, p.Y - n.Y + half*d.Y}, Point{p.X - n.X, p.Y - n.Y})
}
}
}
return result, nil
}
+164
View File
@@ -0,0 +1,164 @@
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package geom
import (
"fmt"
"math"
"slices"
)
// Transform 精确变换控制点和椭圆轴,退化弧沿投影极值分段保留折返
// 入参: matrix 仿射变换
// 返回: Path 独立路径, error 无效路径或数值溢出
func (p Path) Transform(matrix Matrix) (Path, error) {
if err := p.Validate(); err != nil {
return nil, err
}
for _, value := range matrix.Values() {
if !finite(value) {
return nil, fmt.Errorf("invalid geometry transform")
}
}
matrixScale := math.Max(math.Max(math.Abs(matrix.a), math.Abs(matrix.b)), math.Max(math.Abs(matrix.c), math.Abs(matrix.d)))
determinant := 0.0
if matrixScale != 0 {
determinant = (matrix.a/matrixScale)*(matrix.d/matrixScale) - (matrix.b/matrixScale)*(matrix.c/matrixScale)
}
transform := func(p Point) Point {
x, y := matrix.Transform(p.X, p.Y)
return Point{x, y}
}
var result Path
if len(p) > 0 {
result = make(Path, 0, len(p))
}
var current, start Point
for _, s := range p {
originalSegment := s
original := s.End
s.End = transform(s.End)
switch s.Verb {
case Move:
start = original
case Quad:
s.Control1 = transform(s.Control1)
case Cubic:
s.Control1, s.Control2 = transform(s.Control1), transform(s.Control2)
case ClosePath:
original, s.End = start, transform(start)
case Arc:
a, err := centerArc(current, originalSegment)
if err != nil {
return nil, err
}
if current == original {
current = original
continue
}
if s.RadiusX == 0 || s.RadiusY == 0 {
s = Segment{Verb: Line, End: s.End}
} else {
a.center = transform(a.center)
a.u.X, a.u.Y = Vector(matrix, a.u.X, a.u.Y)
a.v.X, a.v.Y = Vector(matrix, a.v.X, a.v.Y)
scale := math.Max(math.Hypot(a.u.X, a.u.Y), math.Hypot(a.v.X, a.v.Y))
if !finite(scale) {
return nil, fmt.Errorf("arc transform overflow")
}
if scale == 0 {
if matrixScale != 0 {
return nil, fmt.Errorf("arc transform precision exhausted")
}
s = Segment{Verb: Line, End: s.End}
} else {
ux, uy, vx, vy := a.u.X/scale, a.u.Y/scale, a.v.X/scale, a.v.Y/scale
det := ux*vy - uy*vx
if determinant != 0 && det == 0 {
return nil, fmt.Errorf("arc transform precision exhausted")
}
if determinant == 0 {
fractions := []float64{}
for _, angle := range []float64{math.Atan2(a.v.X, a.u.X), math.Atan2(a.v.Y, a.u.Y)} {
for _, angle := range []float64{angle, angle + math.Pi} {
if f := a.fraction(angle); f > 0 && f < 1 {
fractions = append(fractions, f)
}
}
}
slices.Sort(fractions)
for _, f := range slices.Compact(fractions) {
result = append(result, Segment{Verb: Line, End: a.point(a.theta + f*a.delta)})
}
s = Segment{Verb: Line, End: s.End}
} else {
xx, yy, xy := ux*ux+vx*vx, uy*uy+vy*vy, ux*uy+vx*vy
radius := math.Sqrt((xx + yy + math.Hypot(xx-yy, 2*xy)) / 2)
s.RadiusX, s.RadiusY = scale*radius, scale*math.Abs(det)/radius
if s.RadiusY == 0 {
return nil, fmt.Errorf("arc transform precision exhausted")
}
s.Rotation = math.Atan2(2*xy, xx-yy) * 90 / math.Pi
if det < 0 {
s.Sweep = !s.Sweep
}
}
}
}
}
result = append(result, s)
current = original
}
if err := result.Validate(); err != nil {
return nil, err
}
return result, nil
}
// Validate 校验公共路径指令、子路径起点和有限坐标
// 返回: error 无效路径
func (p Path) Validate() error {
started := false
for _, s := range p {
points := [3]Point{s.End, s.Control1, s.Control2}
count := 1
switch s.Verb {
case Move:
started = true
case Line:
case Quad:
count = 2
case Cubic:
count = 3
case Arc:
if !finite(s.RadiusX) || !finite(s.RadiusY) || !finite(s.Rotation) || s.RadiusX < 0 || s.RadiusY < 0 {
return fmt.Errorf("invalid arc parameters")
}
case ClosePath:
count = 0
default:
return fmt.Errorf("unsupported geometry command %d", s.Verb)
}
if !started {
return fmt.Errorf("geometry path must start with move")
}
for _, point := range points[:count] {
if !finite(point.X) || !finite(point.Y) {
return fmt.Errorf("non-finite geometry coordinate")
}
}
}
return nil
}