mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 16:44:23 +08:00
1 parent
64f9ec4241
commit
0be872691d
76 files changed
+1926
-1307
No files matched your search
@@ -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
|
||||
}
|
||||
@@ -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}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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) }
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in new issue
Block a user