mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 16:25:49 +08:00
405 lines
12 KiB
Go
405 lines
12 KiB
Go
// 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 ofdgo
|
|
|
|
import (
|
|
"fmt"
|
|
"math"
|
|
)
|
|
|
|
// geometryPolyline 保存展开后的子路径,独立记录闭合状态
|
|
type geometryPolyline struct {
|
|
points []Point
|
|
closed bool
|
|
direction Point
|
|
}
|
|
|
|
// Stroke 将曲线、虚线及线帽连接转换为非零填充轮廓
|
|
// 曲线按Tolerance展开,默认误差为0.001毫米,重叠轮廓保持同向绕数
|
|
// 结果未做布尔整理,按非零规则填充,区域运算需能处理重叠子路径的后端
|
|
// 入参: options 描边样式
|
|
// 返回: GeometryPath 描边轮廓, error 无效参数或精度限制
|
|
func (p GeometryPath) Stroke(options StrokeOptions) (GeometryPath, 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 GeometryPath
|
|
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 GeometryPath) 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")
|
|
}
|
|
work := append([]Point(nil), points...)
|
|
left, right := make([]Point, len(points)), make([]Point, len(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] = geometryLerp(work[i], work[i+1], .5)
|
|
}
|
|
}
|
|
if err := flatten(left, budget/2, depth+1); err != nil {
|
|
return err
|
|
}
|
|
return flatten(right, budget/2, depth+1)
|
|
}
|
|
for _, s := range curves {
|
|
switch s.Verb {
|
|
case GeometryMove:
|
|
flush()
|
|
current, start = s.End, s.End
|
|
line.points = []Point{current}
|
|
case GeometryClose:
|
|
if drawn {
|
|
if current != start {
|
|
line.points = append(line.points, start)
|
|
}
|
|
line.closed = true
|
|
}
|
|
flush()
|
|
current = start
|
|
line.points = []Point{current}
|
|
default:
|
|
drawn = true
|
|
points := []Point{current}
|
|
if s.Verb == GeometryQuad || s.Verb == GeometryCubic {
|
|
points = append(points, s.Control1)
|
|
}
|
|
if s.Verb == GeometryCubic {
|
|
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{geometryLerp(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, geometryLerp(a, b, at/length))
|
|
direction = Point{(b.X - a.X) / length, (b.Y - a.Y) / length}
|
|
}
|
|
end := geometryLerp(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 描边样式
|
|
// 返回: GeometryPath 填充轮廓, error 不支持的退化方形线帽
|
|
func (line geometryPolyline) stroke(result GeometryPath, options StrokeOptions) (GeometryPath, 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, GeometrySegment{Verb: GeometryMove, End: points[0]})
|
|
for _, p := range points[1:] {
|
|
result = append(result, GeometrySegment{Verb: GeometryLine, End: p})
|
|
}
|
|
result = append(result, GeometrySegment{Verb: GeometryClose})
|
|
}
|
|
disk := func(p Point) {
|
|
result = append(result, GeometrySegment{Verb: GeometryMove, End: Point{p.X + half, p.Y}},
|
|
GeometrySegment{Verb: GeometryArc, RadiusX: half, RadiusY: half, Sweep: true, End: Point{p.X - half, p.Y}},
|
|
GeometrySegment{Verb: GeometryArc, RadiusX: half, RadiusY: half, Sweep: true, End: Point{p.X + half, p.Y}},
|
|
GeometrySegment{Verb: GeometryClose})
|
|
}
|
|
points := line.points
|
|
if len(points) == 1 {
|
|
if line.closed {
|
|
return nil, fmt.Errorf("directionless closed stroke: %w", ErrBackendUnavailable)
|
|
}
|
|
if options.Cap == "Round" {
|
|
disk(points[0])
|
|
} else if options.Cap == "Square" {
|
|
d := line.direction
|
|
if d == (Point{}) {
|
|
return nil, fmt.Errorf("directionless square stroke cap: %w", ErrBackendUnavailable)
|
|
}
|
|
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, GeometrySegment{Verb: GeometryMove, End: p},
|
|
GeometrySegment{Verb: GeometryLine, End: x},
|
|
GeometrySegment{Verb: GeometryArc, RadiusX: half, RadiusY: half, Sweep: true, End: y},
|
|
GeometrySegment{Verb: GeometryClose})
|
|
return
|
|
}
|
|
limit := options.MiterLimit
|
|
if limit == 0 {
|
|
limit = defaultMiterLimit
|
|
}
|
|
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
|
|
}
|