Files
ofdgo/ofdgo_renderer_geom_canvas.go
2026-10-21 18:28:57 +08:00

642 lines
21 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"
basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom"
"math"
"slices"
"strconv"
"sync"
"github.com/tdewolff/canvas"
)
// canvasGeometryMu 保护底层路径运算的共享扫描线对象池
var canvasGeometryMu sync.Mutex
// settleCanvasPath 在共享对象池保护下归整路径
// 入参: path 路径, rule 填充规则
// 返回: *canvas.Path 归整路径
func settleCanvasPath(path *canvas.Path, rule canvas.FillRule) *canvas.Path {
canvasGeometryMu.Lock()
defer canvasGeometryMu.Unlock()
return path.Settle(rule)
}
// combineCanvasPaths 在共享对象池保护下计算路径布尔运算
// 入参: left 左路径, right 右路径, operation 运算名称
// 返回: *canvas.Path 运算结果
func combineCanvasPaths(left, right *canvas.Path, operation string) *canvas.Path {
canvasGeometryMu.Lock()
defer canvasGeometryMu.Unlock()
switch operation {
case "or":
return left.Or(right)
case "not":
return left.Not(right)
case "xor":
return left.Xor(right)
default:
return left.And(right)
}
}
// strokeCanvasPath 在共享对象池保护下生成描边轮廓
// 入参: path 路径, width 线宽, cap 端点, join 连接, tolerance 容差
// 返回: *canvas.Path 描边轮廓
func strokeCanvasPath(path *canvas.Path, width float64, cap canvas.Capper, join canvas.Joiner, tolerance float64) *canvas.Path {
canvasGeometryMu.Lock()
defer canvasGeometryMu.Unlock()
return path.Stroke(width, cap, join, tolerance)
}
// Curves 在输出边界转换椭圆弧,其他曲线保持原样
// 入参: path 页面路径
// 返回: GeometryPath 贝塞尔路径, error 路径错误
func (CanvasBackend) Curves(path GeometryPath) (GeometryPath, error) {
p, err := geometryToCanvasPath(&path)
if err != nil {
return nil, err
}
return *geometryFromCanvasPath(p.ReplaceArcs()), nil
}
// Path 解析对象的标准紧缩路径并转换到页面坐标
// 入参: object 路径对象
// 返回: GeometryPath 页面路径, error 无效路径错误
func (b CanvasBackend) Path(object PathObject) (GeometryPath, error) {
return objectGeometryPath(b, object)
}
// Region 解析动作区域并保持椭圆弧及退化变换的既有处理
// 入参: region 动作区域, matrix 页面变换
// 返回: GeometryPath 页面路径, error 解析错误
func (CanvasBackend) Region(region *Region, matrix Matrix) (GeometryPath, error) {
p := actionRegionPath(region, matrix)
return *geometryFromCanvasPath(p.Transform(canvas.Matrix{{1, 0, 0}, {0, -1, 0}})), nil
}
// Bounds 计算曲线路径的精确范围
// 入参: path 页面路径
// 返回: Box 路径范围, error 无效路径错误
func (CanvasBackend) Bounds(path GeometryPath) (Box, error) {
p, err := geometryToCanvasPath(&path)
if err != nil {
return Box{}, err
}
if p.Empty() {
return Box{}, nil
}
box := p.Bounds()
return Box{X: box.X0, Y: -box.Y1, W: box.W(), H: box.H()}, nil
}
// Transform 变换路径,保持曲线与椭圆弧,不修改源路径
// 入参: path 页面路径, matrix 页面变换
// 返回: GeometryPath 新路径, error 无效路径错误
func (CanvasBackend) Transform(path GeometryPath, matrix Matrix) (GeometryPath, error) {
curves := true
for _, segment := range path {
if segment.Verb == GeometryArc {
curves = false
break
}
}
if curves {
return path.Transform(matrix)
}
p, err := geometryToCanvasPath(&path)
if err != nil {
return nil, err
}
m := matrix.Values()
for _, value := range m {
if !finite(value) {
return nil, fmt.Errorf("invalid geometry transform")
}
}
if basegeom.Equal(m[0]*m[3]-m[1]*m[2], 0) {
p = p.ReplaceArcs()
}
p = p.Transform(canvas.Matrix{{m[0], -m[2], m[4]}, {-m[1], m[3], -m[5]}})
return *geometryFromCanvasPath(p), nil
}
// Normalize 按填充规则整理自交区域
// 入参: path 页面路径, evenOdd 是否采用奇偶规则
// 返回: GeometryPath 非零填充路径, error 无效路径错误
func (CanvasBackend) Normalize(path GeometryPath, evenOdd bool) (GeometryPath, error) {
p, err := geometryToCanvasPath(&path)
if err != nil {
return nil, err
}
rule := canvas.NonZero
if evenOdd {
rule = canvas.EvenOdd
}
p = settleCanvasPath(p, rule)
if p.Empty() {
return nil, nil
}
return *geometryFromCanvasPath(p), nil
}
// Combine 计算两个非零填充区域的布尔运算
// 入参: left 左路径, right 右路径, operation 运算类型
// 返回: GeometryPath 结果路径, error 运算错误
func (CanvasBackend) Combine(left, right GeometryPath, operation GeometryOperation) (GeometryPath, error) {
a, err := geometryToCanvasPath(&left)
if err != nil {
return nil, err
}
b, err := geometryToCanvasPath(&right)
if err != nil {
return nil, err
}
var result *canvas.Path
switch operation {
case GeometryIntersect:
result = applyClipPath(a, b)
case GeometryUnion:
result = combineCanvasPaths(a, b, "or")
case GeometrySubtract:
result = combineCanvasPaths(a, b, "not")
case GeometryXor:
result = combineCanvasPaths(a, b, "xor")
default:
return nil, fmt.Errorf("unsupported geometry operation %d", operation)
}
if result.Empty() {
return nil, nil
}
return *geometryFromCanvasPath(result), nil
}
// Stroke 将绝对长度虚线与描边转换为填充区域,几何后端异常时使用本库非零描边
// 入参: path 页面路径, options 描边样式
// 返回: GeometryPath 描边区域, error 样式或路径错误
func (CanvasBackend) Stroke(path GeometryPath, options StrokeOptions) (result GeometryPath, err error) {
if err := basegeom.ValidateStroke(options); err != nil {
return nil, err
}
if slices.Contains(options.Dashes, 0) {
return path.Stroke(options)
}
if options.Cap == "Round" {
if pointPath, found := canvasPointPath(path); found {
return pointPath.Stroke(options)
}
}
p, err := geometryToCanvasPath(&path)
if err != nil {
return nil, err
}
if p.Empty() && options.Cap == "Round" {
return path.Stroke(options)
}
tolerance := options.Tolerance
if tolerance == 0 {
tolerance = canvas.Tolerance
}
options.Tolerance = tolerance
defer func() {
if failure := recover(); failure != nil {
result, err = path.Stroke(options)
if err != nil {
err = fmt.Errorf("stroke fallback after %v: %w", failure, err)
}
}
}()
style := pathStyle{lineJoin: canvas.MiterJoin, lineCap: canvas.ButtCap, miterLimit: defaultMiterLimit}
style.applyLineJoin(options.Join, options.MiterLimit, options.MiterLimitSet)
style.lineCap = pathLineCap(options.Cap, style.lineCap)
p = p.Dash(options.DashOffset, options.Dashes...)
p = flattenCanvasPath(p, tolerance)
p = strokeCanvasPath(p, options.Width, style.lineCap, style.lineJoin, tolerance)
return *geometryFromCanvasPath(p), nil
}
// canvasPointPath 保留普通闭合轮廓,将闭合单点改为同位置绘制段
// 入参: path 原始几何路径
// 返回: GeometryPath 只在需要时复制的路径, bool 是否含实际绘制的单点子路径
func canvasPointPath(path GeometryPath) (GeometryPath, bool) {
result := path
var start Point
point, drawn, active, found, copied := false, false, false, false, false
for i, segment := range path {
switch segment.Verb {
case GeometryMove:
found = found || point && drawn
start = segment.End
point, drawn, active = true, false, true
case GeometryClose:
if point && active {
if !copied {
result = slices.Clone(path)
copied = true
}
result[i] = GeometrySegment{Verb: GeometryLine, End: start}
found = true
}
point, drawn, active = true, false, false
case GeometryLine:
point = point && segment.End == start
drawn, active = true, true
case GeometryQuad:
point = point && segment.End == start && segment.Control1 == start
drawn, active = true, true
case GeometryCubic:
point = point && segment.End == start && segment.Control1 == start && segment.Control2 == start
drawn, active = true, true
default:
point, drawn, active = false, true, true
}
}
return result, found || point && drawn
}
// Clip 解析对象裁剪并与父裁剪相交,保留未裁剪与完全裁去的区别
// 入参: r 渲染器, clips OFD裁剪, matrix 裁剪坐标到页面的变换, parent 父裁剪
// 返回: *GeometryPath 页面裁剪, error 无效父路径错误
func (CanvasBackend) Clip(r *Renderer, clips *Clips, matrix Matrix, parent *GeometryPath) (*GeometryPath, error) {
p, err := geometryToCanvasPath(parent)
if err != nil {
return nil, err
}
renderer := *r
renderer.renderError = nil
clip := renderer.buildClipPath(clips, 0, 0, 0, matrix)
if renderer.renderError != nil {
return nil, renderer.renderError
}
return geometryFromCanvasPath(r.intersectCachedClipPath(p, clip)), nil
}
// flattenCanvasPath 按子路径展开曲线,避免每次替换都复制后续独立轮廓
// 入参: path 原始路径, tolerance 展开误差
// 返回: *canvas.Path 直线路径,不修改原始路径
func flattenCanvasPath(path *canvas.Path, tolerance float64) *canvas.Path {
if path.IsFlat() {
return path
}
if data := path.Data(); len(data) > 0 && data[len(data)-1] == canvas.MoveToCmd {
return path.Flatten(tolerance)
}
parts := path.Split()
if len(parts) <= 1 {
return path.Flatten(tolerance)
}
data := make([]float64, 0, len(path.Data()))
for _, part := range parts {
data = append(data, part.Flatten(tolerance).Data()...)
}
return canvas.NewPathFromData(data)
}
// canvasNativeStroke 判断描边是否由Canvas提供
// 入参: geometry 几何后端
// 返回: bool 是否采用原生描边
func canvasNativeStroke(geometry GeometryBackend) bool {
_, ok := geometry.(CanvasBackend)
return ok
}
// canvasStrokeOptions 转换Canvas描边样式,虚线由调用方传入
// 入参: width 线宽, cap 端点样式, join 转角样式, tolerance 展开误差
// 返回: StrokeOptions 公共描边样式
func canvasStrokeOptions(width float64, cap canvas.Capper, join canvas.Joiner, tolerance float64) StrokeOptions {
options := StrokeOptions{Width: width, Cap: "Butt", Join: "Miter", MiterLimit: defaultMiterLimit, Tolerance: tolerance}
switch cap.(type) {
case canvas.RoundCapper:
options.Cap = "Round"
case canvas.SquareCapper:
options.Cap = "Square"
}
switch value := join.(type) {
case canvas.RoundJoiner:
options.Join = "Round"
case canvas.BevelJoiner:
options.Join = "Bevel"
case canvas.MiterJoiner:
options.MiterLimit = value.Limit
options.MiterLimitSet = value.Limit == 0
}
return options
}
// canvasStrokePath 通过配置的几何后端生成描边,不修改源路径
// 入参: geometry 几何后端, path Canvas路径, options 描边样式
// 返回: *canvas.Path 描边轮廓, error 几何错误
func canvasStrokePath(geometry GeometryBackend, path *canvas.Path, options StrokeOptions) (*canvas.Path, error) {
outline, err := geometry.Stroke(*geometryFromCanvasPath(path), options)
if err != nil {
return nil, err
}
return geometryToCanvasPath(&outline)
}
// canvasStrokeFillPath 为直接非零填充展开复杂描边,不将重叠轮廓交给区域运算
// 入参: geometry 几何后端, path Canvas路径, options 描边样式
// 返回: *canvas.Path 描边轮廓, error 几何错误
func canvasStrokeFillPath(geometry GeometryBackend, path *canvas.Path, options StrokeOptions) (*canvas.Path, error) {
if canvasNativeStroke(geometry) && !slices.Contains(options.Dashes, 0) {
if options.Tolerance == 0 {
options.Tolerance = canvas.Tolerance
}
if err := basegeom.ValidateStroke(options); err != nil {
return nil, err
}
if len(path.Dash(options.DashOffset, options.Dashes...).Data()) > 256 {
outline, err := geometryFromCanvasPath(path).Stroke(options)
if err != nil {
return canvasStrokePath(geometry, path, options)
}
return geometryToCanvasPath(&outline)
}
}
return canvasStrokePath(geometry, path, options)
}
// canvasFillClip 检查描边轮廓能否直接填充或按凸窗口独立裁剪
// 入参: clip 裁剪路径
// 返回: bool 是否不要求整理重叠区域
func canvasFillClip(clip *canvas.Path) bool {
if clip == nil {
return true
}
_, ok := convexCanvasPolygon(clip)
return ok
}
// strokeCanvasPath 将文字和复杂画刷的描边交给配置的几何后端
// 入参: path Canvas路径, width 线宽, cap 端点样式, join 转角样式
// 返回: *canvas.Path 描边轮廓
func (r *Renderer) strokeCanvasPath(path *canvas.Path, width float64, cap canvas.Capper, join canvas.Joiner) *canvas.Path {
return r.strokeDashedCanvasPath(path, width, cap, join, 0, nil, false)
}
// strokeDashedCanvasPath 通过公共几何后端展开虚线和端点样式,保留零长度绘制段
// 入参: path 路径, width 线宽, cap 端点样式, join 转角样式, offset 虚线偏移, dashes 虚线间隔序列, fill 是否直接填充或凸窗口裁剪
// 返回: *canvas.Path 描边填充轮廓
func (r *Renderer) strokeDashedCanvasPath(path *canvas.Path, width float64, cap canvas.Capper, join canvas.Joiner, offset float64, dashes []float64, fill bool) *canvas.Path {
geometry, err := r.Geometry()
if err != nil {
r.renderError = err
return &canvas.Path{}
}
options := canvasStrokeOptions(width, cap, join, canvas.Tolerance)
options.DashOffset, options.Dashes = offset, dashes
stroke := canvasStrokePath
if fill {
stroke = canvasStrokeFillPath
}
result, err := stroke(geometry, path, options)
if err != nil {
r.renderError = err
return &canvas.Path{}
}
return result
}
// geometryToCanvasPath 将页面坐标路径转换为默认几何引擎的向上纵轴
// 入参: path 独立几何路径,nil表示不裁剪
// 返回: *canvas.Path 默认引擎路径, error 无效路径
func geometryToCanvasPath(path *GeometryPath) (*canvas.Path, error) {
if path == nil {
return nil, nil
}
if err := path.validate(); err != nil {
return nil, err
}
p := canvas.NewPathFromData(make([]float64, 0, canvasPathCapacity(*path)))
for _, s := range *path {
switch s.Verb {
case GeometryMove:
p.MoveTo(s.End.X, -s.End.Y)
case GeometryLine:
p.LineTo(s.End.X, -s.End.Y)
case GeometryQuad:
p.QuadTo(s.Control1.X, -s.Control1.Y, s.End.X, -s.End.Y)
case GeometryCubic:
p.CubeTo(s.Control1.X, -s.Control1.Y, s.Control2.X, -s.Control2.Y, s.End.X, -s.End.Y)
case GeometryArc:
p.ArcTo(s.RadiusX, s.RadiusY, -s.Rotation, s.Large, !s.Sweep, s.End.X, -s.End.Y)
case GeometryClose:
if data := p.Data(); len(data) != 0 && data[len(data)-1] == canvas.MoveToCmd {
p = canvas.NewPathFromData(append(data, canvas.CloseCmd, data[len(data)-3], data[len(data)-2], canvas.CloseCmd))
} else {
p.Close()
}
default:
return nil, fmt.Errorf("unsupported geometry command %d", s.Verb)
}
}
return p, nil
}
// canvasObjectPath 建立对象局部路径,保留折返线和退化贝塞尔的原始端点
// 入参: path 独立路径
// 返回: *canvas.Path 适配路径, error 不支持的指令
func canvasObjectPath(path GeometryPath) (*canvas.Path, error) {
data := make([]float64, 0, canvasPathCapacity(path))
for _, s := range path {
end := s.End
switch s.Verb {
case GeometryMove:
data = append(data, canvas.MoveToCmd, end.X, end.Y, canvas.MoveToCmd)
case GeometryLine:
data = append(data, canvas.LineToCmd, end.X, end.Y, canvas.LineToCmd)
case GeometryQuad:
data = append(data, canvas.QuadToCmd, s.Control1.X, s.Control1.Y, end.X, end.Y, canvas.QuadToCmd)
case GeometryCubic:
data = append(data, canvas.CubeToCmd, s.Control1.X, s.Control1.Y, s.Control2.X, s.Control2.Y, end.X, end.Y, canvas.CubeToCmd)
case GeometryArc:
p := canvas.NewPathFromData(data)
p.ArcTo(s.RadiusX, s.RadiusY, s.Rotation, s.Large, s.Sweep, end.X, end.Y)
data = p.Data()
case GeometryClose:
p := canvas.NewPathFromData(data)
if len(data) != 0 && data[len(data)-1] == canvas.MoveToCmd {
p = canvas.NewPathFromData(append(data, canvas.CloseCmd, data[len(data)-3], data[len(data)-2], canvas.CloseCmd))
} else {
p.Close()
}
data = p.Data()
default:
return nil, fmt.Errorf("unsupported geometry command %d", s.Verb)
}
}
return canvas.NewPathFromData(data), nil
}
// canvasPathCapacity 按指令估算路径容量,避免长路径逐段扩容
// 入参: path 独立几何路径
// 返回: int 坐标存储容量
func canvasPathCapacity(path GeometryPath) int {
count := 0
for _, segment := range path {
switch segment.Verb {
case GeometryMove, GeometryLine, GeometryClose:
count += 4
case GeometryQuad:
count += 6
default:
count += 8
}
}
return count
}
// geometryFromCanvasPath 将默认引擎路径转换为独立的页面坐标路径,保留曲线和椭圆弧
// 入参: path 默认引擎路径
// 返回: *GeometryPath 独立路径,nil表示不裁剪
func geometryFromCanvasPath(path *canvas.Path) *GeometryPath {
if path == nil {
return nil
}
result := make(GeometryPath, 0, path.Len())
scanner := path.Scanner()
for scanner.Scan() {
end := scanner.End()
segment := GeometrySegment{End: Point{X: end.X, Y: -end.Y}}
switch scanner.Cmd() {
case canvas.MoveToCmd:
segment.Verb = GeometryMove
case canvas.LineToCmd:
segment.Verb = GeometryLine
case canvas.QuadToCmd:
control := scanner.CP1()
segment.Verb, segment.Control1 = GeometryQuad, Point{X: control.X, Y: -control.Y}
case canvas.CubeToCmd:
c1, c2 := scanner.CP1(), scanner.CP2()
segment.Verb = GeometryCubic
segment.Control1, segment.Control2 = Point{X: c1.X, Y: -c1.Y}, Point{X: c2.X, Y: -c2.Y}
case canvas.ArcToCmd:
segment.Verb = GeometryArc
segment.RadiusX, segment.RadiusY, segment.Rotation, segment.Large, segment.Sweep = scanner.Arc()
segment.Rotation, segment.Sweep = -segment.Rotation, !segment.Sweep
case canvas.CloseCmd:
segment.Verb = GeometryClose
}
result = append(result, segment)
}
return &result
}
// clipLinearFillGeometry 裁剪直线填充轮廓,保留原填充规则,不用于区域布尔运算
// 入参: path 闭合绘制轮廓, clip 凸裁剪窗口
// 返回: GeometryPath 填充轮廓, bool 是否适用, error 路径错误
func clipLinearFillGeometry(path, clip GeometryPath) (GeometryPath, bool, error) {
a, err := geometryToCanvasPath(&path)
if err != nil {
return nil, false, err
}
b, err := geometryToCanvasPath(&clip)
if err != nil {
return nil, false, err
}
result, ok := intersectLinearCanvasContours(a, b)
if !ok {
return nil, false, nil
}
return *geometryFromCanvasPath(result), true, nil
}
// invertCanvasMatrix 对有限可逆矩阵求逆,保留非零小尺度变换
// 入参: matrix 画布仿射矩阵
// 返回: canvas.Matrix 逆矩阵, bool 是否存在有限逆矩阵
func invertCanvasMatrix(matrix canvas.Matrix) (canvas.Matrix, bool) {
inverse, ok := MatrixFromValues([6]float64{matrix[0][0], matrix[1][0], matrix[0][1], matrix[1][1], matrix[0][2], matrix[1][2]}).Invert()
return canvas.Matrix{{inverse.a, inverse.c, inverse.e}, {inverse.b, inverse.d, inverse.f}}, ok
}
// actionCanvasMatrix 转换页面坐标矩阵,不翻转Y轴
// 入参: matrix 页面变换
// 返回: canvas.Matrix 路径变换
func actionCanvasMatrix(matrix Matrix) canvas.Matrix {
return canvas.Matrix{{matrix.a, matrix.c, matrix.e}, {matrix.b, matrix.d, matrix.f}}
}
// actionRegionPath 解析复杂点击区域并转换为页面坐标
// 入参: region 动作区域, matrix 坐标变换
// 返回: *canvas.Path 点击路径
func actionRegionPath(region *Region, matrix Matrix) *canvas.Path {
path := &canvas.Path{}
for _, area := range region.Area {
start := parseFloats(area.Start)
if len(start) != 2 {
continue
}
part := &canvas.Path{}
part.MoveTo(start[0], start[1])
for _, command := range area.Command {
p1, p2, p3 := parseFloats(command.Point1), parseFloats(command.Point2), parseFloats(command.Point3)
switch command.Type {
case "Move":
if len(p1) == 2 {
part.Close()
part.MoveTo(p1[0], p1[1])
}
case "Line":
if len(p1) == 2 {
part.LineTo(p1[0], p1[1])
}
case "QuadraticBezier":
if len(p1) == 2 && len(p2) == 2 {
part.QuadTo(p1[0], p1[1], p2[0], p2[1])
}
case "CubicBezier":
if len(p3) == 2 {
first, second := part.Pos(), canvas.Point{X: p3[0], Y: p3[1]}
if len(p1) == 2 {
first = canvas.Point{X: p1[0], Y: p1[1]}
}
if len(p2) == 2 {
second = canvas.Point{X: p2[0], Y: p2[1]}
}
part.CubeTo(first.X, first.Y, second.X, second.Y, p3[0], p3[1])
}
case "Arc":
size, end := parseFloats(command.EllipseSize), parseFloats(command.EndPoint)
angle, err := strconv.ParseFloat(command.RotationAngle, 64)
large, largeErr := strconv.ParseBool(command.LargeArc)
sweep, sweepErr := strconv.ParseBool(command.SweepDirection)
if len(end) == 2 && finite(end[0]) && finite(end[1]) && err == nil && finite(angle) && largeErr == nil && sweepErr == nil {
rx, ry := 0.0, 0.0
if len(size) > 0 {
rx, ry = math.Abs(size[0]), math.Abs(size[0])
}
if len(size) > 1 {
rx, ry = rx/2, math.Abs(size[1])/2
}
if finite(rx) && finite(ry) {
part.ArcTo(rx, ry, math.Mod(angle, 360), large, sweep, end[0], end[1])
}
}
case "Close":
part.Close()
}
}
part.Close()
path = path.Append(part)
}
if canvas.Equal(matrix.a*matrix.d-matrix.b*matrix.c, 0) {
path = path.ReplaceArcs()
}
return path.Transform(actionCanvasMatrix(matrix))
}