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
+1925 -1306

No files matched your search

+18 -18
View File
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package ofdgo package geom
import ( import (
"fmt" "fmt"
@@ -27,33 +27,33 @@ type geometryArc struct {
// Curves 按毫米误差上限将端点式椭圆弧转换为三次贝塞尔曲线,其他指令保持原样 // Curves 按毫米误差上限将端点式椭圆弧转换为三次贝塞尔曲线,其他指令保持原样
// 入参: tolerance 正有限误差上限 // 入参: tolerance 正有限误差上限
// 返回: GeometryPath 独立路径, error 参数错误 // 返回: Path 独立路径, error 参数错误
func (p GeometryPath) Curves(tolerance float64) (GeometryPath, error) { func (p Path) Curves(tolerance float64) (Path, error) {
if err := p.validate(); err != nil { if err := p.Validate(); err != nil {
return nil, err return nil, err
} }
if !rasterPositive(tolerance) { if !positive(tolerance) {
return nil, fmt.Errorf("invalid curve tolerance") return nil, fmt.Errorf("invalid curve tolerance")
} }
result := make(GeometryPath, 0, len(p)) result := make(Path, 0, len(p))
var current, start Point var current, start Point
for _, s := range p { for _, s := range p {
switch s.Verb { switch s.Verb {
case GeometryMove: case Move:
start = s.End start = s.End
case GeometryArc: case Arc:
curves, err := arcCurves(current, s, tolerance) curves, err := ArcCurves(current, s, tolerance)
if err != nil { if err != nil {
return nil, err return nil, err
} }
result = append(result, curves...) result = append(result, curves...)
current = s.End current = s.End
continue continue
case GeometryClose: case ClosePath:
result = append(result, s) result = append(result, s)
current = start current = start
continue continue
case GeometryLine, GeometryQuad, GeometryCubic: case Line, Quad, Cubic:
default: default:
return nil, fmt.Errorf("unsupported geometry command %d", s.Verb) return nil, fmt.Errorf("unsupported geometry command %d", s.Verb)
} }
@@ -63,10 +63,10 @@ func (p GeometryPath) Curves(tolerance float64) (GeometryPath, error) {
return result, nil return result, nil
} }
// arcCurves 按标准端点参数求椭圆中心,只在绘制边界近似弧线 // ArcCurves 按标准端点参数求椭圆中心,只在绘制边界近似弧线
// 入参: start 起点, arc 弧线, tolerance 毫米误差上限 // 入参: start 起点, arc 弧线, tolerance 毫米误差上限
// 返回: GeometryPath 贝塞尔片段, error 无效弧线 // 返回: Path 贝塞尔片段, error 无效弧线
func arcCurves(start Point, arc GeometrySegment, tolerance float64) (GeometryPath, error) { func ArcCurves(start Point, arc Segment, tolerance float64) (Path, error) {
a, err := centerArc(start, arc) a, err := centerArc(start, arc)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -75,7 +75,7 @@ func arcCurves(start Point, arc GeometrySegment, tolerance float64) (GeometryPat
return nil, nil return nil, nil
} }
if arc.RadiusX == 0 || arc.RadiusY == 0 { if arc.RadiusX == 0 || arc.RadiusY == 0 {
return GeometryPath{{Verb: GeometryLine, End: arc.End}}, nil 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)) 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)) step := math.Min(math.Pi/2, math.Pow(tolerance/radius*40000, 1.0/6))
@@ -88,12 +88,12 @@ func arcCurves(start Point, arc GeometrySegment, tolerance float64) (GeometryPat
point := func(x, y float64) Point { 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} 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(GeometryPath, 0, count) result := make(Path, 0, count)
for i := 0; i < count; i++ { for i := 0; i < count; i++ {
sa, ca := math.Sincos(a.theta + float64(i)*step) sa, ca := math.Sincos(a.theta + float64(i)*step)
sb, cb := math.Sincos(a.theta + float64(i+1)*step) sb, cb := math.Sincos(a.theta + float64(i+1)*step)
k := 4.0 / 3 * math.Tan(step/4) k := 4.0 / 3 * math.Tan(step/4)
result = append(result, GeometrySegment{Verb: GeometryCubic, Control1: point(ca-k*sa, sa+k*ca), Control2: point(cb+k*sb, sb-k*cb), End: point(cb, sb)}) 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 result[len(result)-1].End = arc.End
return result, nil return result, nil
@@ -102,7 +102,7 @@ func arcCurves(start Point, arc GeometrySegment, tolerance float64) (GeometryPat
// centerArc 将端点式圆弧解析为中心式,统一半径修正和方向语义 // centerArc 将端点式圆弧解析为中心式,统一半径修正和方向语义
// 入参: start 起点, arc 弧线 // 入参: start 起点, arc 弧线
// 返回: geometryArc 中心式参数, error 无效参数或数值溢出 // 返回: geometryArc 中心式参数, error 无效参数或数值溢出
func centerArc(start Point, arc GeometrySegment) (geometryArc, error) { func centerArc(start Point, arc Segment) (geometryArc, error) {
rx, ry := arc.RadiusX, arc.RadiusY rx, ry := arc.RadiusX, arc.RadiusY
if !finite(rx) || !finite(ry) || rx < 0 || ry < 0 || !finite(arc.Rotation) || 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) { !finite(start.X) || !finite(start.Y) || !finite(arc.End.X) || !finite(arc.End.Y) {
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package ofdgo package geom
import ( import (
"fmt" "fmt"
@@ -22,8 +22,8 @@ import (
// Bounds 通过解析极值计算直线、贝塞尔和椭圆弧的轴对齐边界 // Bounds 通过解析极值计算直线、贝塞尔和椭圆弧的轴对齐边界
// 不进行曲线展开且不使用Tolerance,结果受float64舍入误差影响,不保证向外舍入 // 不进行曲线展开且不使用Tolerance,结果受float64舍入误差影响,不保证向外舍入
// 返回: Box 浮点路径范围, error 无效路径、弧线或数值溢出 // 返回: Box 浮点路径范围, error 无效路径、弧线或数值溢出
func (p GeometryPath) Bounds() (Box, error) { func (p Path) Bounds() (Box, error) {
if err := p.validate(); err != nil { if err := p.Validate(); err != nil {
return Box{}, err return Box{}, err
} }
left, top, right, bottom := math.Inf(1), math.Inf(1), math.Inf(-1), math.Inf(-1) left, top, right, bottom := math.Inf(1), math.Inf(1), math.Inf(-1), math.Inf(-1)
@@ -34,11 +34,11 @@ func (p GeometryPath) Bounds() (Box, error) {
var current, start Point var current, start Point
for _, s := range p { for _, s := range p {
switch s.Verb { switch s.Verb {
case GeometryMove: case Move:
start = s.End start = s.End
case GeometryClose: case ClosePath:
s.End = start s.End = start
case GeometryArc: case Arc:
a, err := centerArc(current, s) a, err := centerArc(current, s)
if err != nil { if err != nil {
return Box{}, err return Box{}, err
@@ -52,9 +52,9 @@ func (p GeometryPath) Bounds() (Box, error) {
} }
} }
} }
case GeometryQuad, GeometryCubic: 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}} { 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 == GeometryQuad { if s.Verb == Quad {
axis[2] = 0 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])))) scale := math.Max(math.Abs(axis[0]), math.Max(math.Abs(axis[1]), math.Max(math.Abs(axis[2]), math.Abs(axis[3]))))
@@ -64,19 +64,19 @@ func (p GeometryPath) Bounds() (Box, error) {
} }
} }
var roots []float64 var roots []float64
if s.Verb == GeometryQuad { if s.Verb == Quad {
roots = geometryRoots(0, axis[0]-2*axis[1]+axis[3], axis[1]-axis[0]) roots = geometryRoots(0, axis[0]-2*axis[1]+axis[3], axis[1]-axis[0])
} else { } 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]) 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 { for _, t := range roots {
if t > 0 && t < 1 { if t > 0 && t < 1 {
q := geometryLerp(current, s.Control1, t) q := Lerp(current, s.Control1, t)
if s.Verb == GeometryQuad { if s.Verb == Quad {
q = geometryLerp(q, geometryLerp(s.Control1, s.End, t), t) q = Lerp(q, Lerp(s.Control1, s.End, t), t)
} else { } else {
r := geometryLerp(s.Control1, s.Control2, t) r := Lerp(s.Control1, s.Control2, t)
q = geometryLerp(geometryLerp(q, r, t), geometryLerp(r, geometryLerp(s.Control2, s.End, t), t), t) q = Lerp(Lerp(q, r, t), Lerp(r, Lerp(s.Control2, s.End, t), t), t)
} }
add(q) add(q)
} }
@@ -121,9 +121,9 @@ func geometryRoots(a, b, c float64) []float64 {
return []float64{q / a, c / q} return []float64{q / a, c / q}
} }
// geometryLerp 线性插值坐标 // Lerp 线性插值坐标
// 入参: a 起点, b 终点, t 插值比例 // 入参: a 起点, b 终点, t 插值比例
// 返回: Point 插值坐标 // 返回: Point 插值坐标
func geometryLerp(a, b Point, t float64) 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} return Point{(1-t)*a.X + t*b.X, (1-t)*a.Y + t*b.Y}
} }
@@ -12,14 +12,16 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package ofdgo package geom
import "math" import (
"math"
)
// geometryConvexContains 判断单个非退化凸直线区域是否包含另一闭合直线区域 // ConvexContains 判断单个非退化凸直线区域是否包含另一闭合直线区域
// 入参: outer 外部区域, inner 内部区域 // 入参: outer 外部区域, inner 内部区域
// 返回: bool 是否完全包含,曲线及多轮廓返回false // 返回: bool 是否完全包含,曲线及多轮廓返回false
func geometryConvexContains(outer, inner GeometryPath) bool { func ConvexContains(outer, inner Path) bool {
a, b := geometryPolygonPoints(outer), geometryPolygonPoints(inner) a, b := geometryPolygonPoints(outer), geometryPolygonPoints(inner)
if len(a) < 3 || len(b) < 3 { if len(a) < 3 || len(b) < 3 {
return false return false
@@ -68,12 +70,12 @@ func geometryConvexContains(outer, inner GeometryPath) bool {
// geometryPolygonPoints 读取单个显式闭合直线轮廓的顶点,不修改原路径 // geometryPolygonPoints 读取单个显式闭合直线轮廓的顶点,不修改原路径
// 入参: path 直线轮廓 // 入参: path 直线轮廓
// 返回: []Point 独立顶点,其他路径返回nil // 返回: []Point 独立顶点,其他路径返回nil
func geometryPolygonPoints(path GeometryPath) []Point { func geometryPolygonPoints(path Path) []Point {
if len(path) < 4 || path[0].Verb != GeometryMove || path[len(path)-1].Verb != GeometryClose { if len(path) < 4 || path[0].Verb != Move || path[len(path)-1].Verb != ClosePath {
return nil return nil
} }
for i, segment := range path[:len(path)-1] { for i, segment := range path[:len(path)-1] {
if i > 0 && segment.Verb != GeometryLine || !finite(segment.End.X) || !finite(segment.End.Y) { if i > 0 && segment.Verb != Line || !finite(segment.End.X) || !finite(segment.End.Y) {
return nil return nil
} }
} }
+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
}
@@ -12,13 +12,16 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package ofdgo package geom
import ( import (
"fmt" "fmt"
"math" "math"
) )
// UnsupportedError 表示当前几何实现无法展开的描边形态
type UnsupportedError string
// geometryPolyline 保存展开后的子路径,独立记录闭合状态 // geometryPolyline 保存展开后的子路径,独立记录闭合状态
type geometryPolyline struct { type geometryPolyline struct {
points []Point points []Point
@@ -26,16 +29,22 @@ type geometryPolyline struct {
direction Point direction Point
} }
// Error 返回不支持的描边原因
// 返回: string 错误信息
func (err UnsupportedError) Error() string {
return string(err)
}
// Stroke 按端点和转角样式将曲线、虚线转换为非零填充轮廓 // Stroke 按端点和转角样式将曲线、虚线转换为非零填充轮廓
// 曲线按Tolerance展开,默认误差为0.001毫米,重叠轮廓保持同向绕数 // 曲线按Tolerance展开,默认误差为0.001毫米,重叠轮廓保持同向绕数
// 结果未做布尔整理,按非零规则填充,区域运算需能处理重叠子路径的后端 // 结果未做布尔整理,按非零规则填充,区域运算需能处理重叠子路径的后端
// 入参: options 描边样式 // 入参: options 描边样式
// 返回: GeometryPath 描边轮廓, error 无效参数或精度限制 // 返回: Path 描边轮廓, error 无效参数或精度限制
func (p GeometryPath) Stroke(options StrokeOptions) (GeometryPath, error) { func (p Path) Stroke(options StrokeOptions) (Path, error) {
if err := validateStroke(options); err != nil { if err := ValidateStroke(options); err != nil {
return nil, err return nil, err
} }
if err := p.validate(); err != nil { if err := p.Validate(); err != nil {
return nil, err return nil, err
} }
if options.Width/2 == 0 { if options.Width/2 == 0 {
@@ -68,7 +77,7 @@ func (p GeometryPath) Stroke(options StrokeOptions) (GeometryPath, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
var result GeometryPath var result Path
for _, line := range lines { for _, line := range lines {
parts := []geometryPolyline{line} parts := []geometryPolyline{line}
if len(dashes) != 0 { if len(dashes) != 0 {
@@ -87,7 +96,7 @@ func (p GeometryPath) Stroke(options StrokeOptions) (GeometryPath, error) {
} }
} }
} }
if err := result.validate(); err != nil { if err := result.Validate(); err != nil {
return nil, err return nil, err
} }
return result, nil return result, nil
@@ -96,7 +105,7 @@ func (p GeometryPath) Stroke(options StrokeOptions) (GeometryPath, error) {
// strokePolylines 自适应展开贝塞尔,弦长亏损同时约束虚线累计长度误差 // strokePolylines 自适应展开贝塞尔,弦长亏损同时约束虚线累计长度误差
// 入参: tolerance 展开误差 // 入参: tolerance 展开误差
// 返回: []geometryPolyline 子路径, error 展开限制 // 返回: []geometryPolyline 子路径, error 展开限制
func (p GeometryPath) strokePolylines(tolerance float64) ([]geometryPolyline, error) { func (p Path) strokePolylines(tolerance float64) ([]geometryPolyline, error) {
curves, err := p.Curves(tolerance / 2) curves, err := p.Curves(tolerance / 2)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -149,7 +158,7 @@ func (p GeometryPath) strokePolylines(tolerance float64) ([]geometryPolyline, er
for n := len(points); n > 0; n-- { for n := len(points); n > 0; n-- {
left[len(points)-n], right[n-1] = work[0], work[n-1] left[len(points)-n], right[n-1] = work[0], work[n-1]
for i := 0; i < n-1; i++ { for i := 0; i < n-1; i++ {
work[i] = geometryLerp(work[i], work[i+1], .5) work[i] = Lerp(work[i], work[i+1], .5)
} }
} }
if err := flatten(left[:len(points)], budget/2, depth+1); err != nil { if err := flatten(left[:len(points)], budget/2, depth+1); err != nil {
@@ -159,11 +168,11 @@ func (p GeometryPath) strokePolylines(tolerance float64) ([]geometryPolyline, er
} }
for _, s := range curves { for _, s := range curves {
switch s.Verb { switch s.Verb {
case GeometryMove: case Move:
flush() flush()
current, start = s.End, s.End current, start = s.End, s.End
line.points = []Point{current} line.points = []Point{current}
case GeometryClose: case ClosePath:
if drawn { if drawn {
if current != start { if current != start {
line.points = append(line.points, start) line.points = append(line.points, start)
@@ -177,10 +186,10 @@ func (p GeometryPath) strokePolylines(tolerance float64) ([]geometryPolyline, er
drawn = true drawn = true
var buffer [4]Point var buffer [4]Point
points := append(buffer[:0], current) points := append(buffer[:0], current)
if s.Verb == GeometryQuad || s.Verb == GeometryCubic { if s.Verb == Quad || s.Verb == Cubic {
points = append(points, s.Control1) points = append(points, s.Control1)
} }
if s.Verb == GeometryCubic { if s.Verb == Cubic {
points = append(points, s.Control2) points = append(points, s.Control2)
} }
points = append(points, s.End) points = append(points, s.End)
@@ -237,7 +246,7 @@ func (line geometryPolyline) dash(pattern []float64, total, offset float64) ([]g
return nil, fmt.Errorf("stroke dash subdivision limit exceeded") return nil, fmt.Errorf("stroke dash subdivision limit exceeded")
} }
if pattern[index] == 0 && index%2 == 0 && len(run) == 0 { 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}}) 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) index = (index + 1) % len(pattern)
remaining = pattern[index] remaining = pattern[index]
@@ -255,10 +264,10 @@ func (line geometryPolyline) dash(pattern []float64, total, offset float64) ([]g
steps++ steps++
if index%2 == 0 { if index%2 == 0 {
if len(run) == 0 { if len(run) == 0 {
run = append(run, geometryLerp(a, b, at/length)) run = append(run, Lerp(a, b, at/length))
direction = Point{(b.X - a.X) / length, (b.Y - a.Y) / length} direction = Point{(b.X - a.X) / length, (b.Y - a.Y) / length}
} }
end := geometryLerp(a, b, (at+step)/length) end := Lerp(a, b, (at+step)/length)
if end != run[len(run)-1] { if end != run[len(run)-1] {
run = append(run, end) run = append(run, end)
} }
@@ -284,8 +293,8 @@ func (line geometryPolyline) dash(pattern []float64, total, offset float64) ([]g
// stroke 追加同向矩形、连接楔形及圆盘组成的非零填充区域 // stroke 追加同向矩形、连接楔形及圆盘组成的非零填充区域
// 入参: result 已有轮廓, options 描边样式 // 入参: result 已有轮廓, options 描边样式
// 返回: GeometryPath 填充轮廓, error 不支持的退化方头端点 // 返回: Path 填充轮廓, error 不支持的退化方头端点
func (line geometryPolyline) stroke(result GeometryPath, options StrokeOptions) (GeometryPath, error) { func (line geometryPolyline) stroke(result Path, options StrokeOptions) (Path, error) {
half := options.Width / 2 half := options.Width / 2
polygon := func(points ...Point) { polygon := func(points ...Point) {
scale := 0.0 scale := 0.0
@@ -308,29 +317,29 @@ func (line geometryPolyline) stroke(result GeometryPath, options StrokeOptions)
points[i], points[j] = points[j], points[i] points[i], points[j] = points[j], points[i]
} }
} }
result = append(result, GeometrySegment{Verb: GeometryMove, End: points[0]}) result = append(result, Segment{Verb: Move, End: points[0]})
for _, p := range points[1:] { for _, p := range points[1:] {
result = append(result, GeometrySegment{Verb: GeometryLine, End: p}) result = append(result, Segment{Verb: Line, End: p})
} }
result = append(result, GeometrySegment{Verb: GeometryClose}) result = append(result, Segment{Verb: ClosePath})
} }
disk := func(p Point) { disk := func(p Point) {
result = append(result, GeometrySegment{Verb: GeometryMove, End: Point{p.X + half, p.Y}}, result = append(result, Segment{Verb: Move, End: Point{p.X + half, p.Y}},
GeometrySegment{Verb: GeometryArc, 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}},
GeometrySegment{Verb: GeometryArc, 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}},
GeometrySegment{Verb: GeometryClose}) Segment{Verb: ClosePath})
} }
points := line.points points := line.points
if len(points) == 1 { if len(points) == 1 {
if line.closed { if line.closed {
return nil, fmt.Errorf("directionless closed stroke: %w", ErrBackendUnavailable) return nil, UnsupportedError("directionless closed stroke")
} }
if options.Cap == "Round" { if options.Cap == "Round" {
disk(points[0]) disk(points[0])
} else if options.Cap == "Square" { } else if options.Cap == "Square" {
d := line.direction d := line.direction
if d == (Point{}) { if d == (Point{}) {
return nil, fmt.Errorf("directionless square stroke cap: %w", ErrBackendUnavailable) return nil, UnsupportedError("directionless square stroke cap")
} }
p := points[0] p := points[0]
u, v := Point{d.X * half, d.Y * half}, Point{-d.Y * half, d.X * half} u, v := Point{d.X * half, d.Y * half}, Point{-d.Y * half, d.X * half}
@@ -365,16 +374,13 @@ func (line geometryPolyline) stroke(result GeometryPath, options StrokeOptions)
if cross < 0 { if cross < 0 {
x, y = y, x x, y = y, x
} }
result = append(result, GeometrySegment{Verb: GeometryMove, End: p}, result = append(result, Segment{Verb: Move, End: p},
GeometrySegment{Verb: GeometryLine, End: x}, Segment{Verb: Line, End: x},
GeometrySegment{Verb: GeometryArc, RadiusX: half, RadiusY: half, Sweep: true, End: y}, Segment{Verb: Arc, RadiusX: half, RadiusY: half, Sweep: true, End: y},
GeometrySegment{Verb: GeometryClose}) Segment{Verb: ClosePath})
return return
} }
limit := options.MiterLimit limit := options.MiterLimit
if limit == 0 && !options.MiterLimitSet {
limit = defaultMiterLimit
}
if options.Join == "" || options.Join == "Miter" { if options.Join == "" || options.Join == "Miter" {
t := ((y.X-x.X)*b.Y - (y.Y-x.Y)*b.X) / cross 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} m := Point{x.X + t*a.X, x.Y + t*a.Y}
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package ofdgo package geom
import ( import (
"fmt" "fmt"
@@ -22,9 +22,9 @@ import (
// Transform 精确变换控制点和椭圆轴,退化弧沿投影极值分段保留折返 // Transform 精确变换控制点和椭圆轴,退化弧沿投影极值分段保留折返
// 入参: matrix 仿射变换 // 入参: matrix 仿射变换
// 返回: GeometryPath 独立路径, error 无效路径或数值溢出 // 返回: Path 独立路径, error 无效路径或数值溢出
func (p GeometryPath) Transform(matrix Matrix) (GeometryPath, error) { func (p Path) Transform(matrix Matrix) (Path, error) {
if err := p.validate(); err != nil { if err := p.Validate(); err != nil {
return nil, err return nil, err
} }
for _, value := range matrix.Values() { for _, value := range matrix.Values() {
@@ -41,9 +41,9 @@ func (p GeometryPath) Transform(matrix Matrix) (GeometryPath, error) {
x, y := matrix.Transform(p.X, p.Y) x, y := matrix.Transform(p.X, p.Y)
return Point{x, y} return Point{x, y}
} }
var result GeometryPath var result Path
if len(p) > 0 { if len(p) > 0 {
result = make(GeometryPath, 0, len(p)) result = make(Path, 0, len(p))
} }
var current, start Point var current, start Point
for _, s := range p { for _, s := range p {
@@ -51,15 +51,15 @@ func (p GeometryPath) Transform(matrix Matrix) (GeometryPath, error) {
original := s.End original := s.End
s.End = transform(s.End) s.End = transform(s.End)
switch s.Verb { switch s.Verb {
case GeometryMove: case Move:
start = original start = original
case GeometryQuad: case Quad:
s.Control1 = transform(s.Control1) s.Control1 = transform(s.Control1)
case GeometryCubic: case Cubic:
s.Control1, s.Control2 = transform(s.Control1), transform(s.Control2) s.Control1, s.Control2 = transform(s.Control1), transform(s.Control2)
case GeometryClose: case ClosePath:
original, s.End = start, transform(start) original, s.End = start, transform(start)
case GeometryArc: case Arc:
a, err := centerArc(current, originalSegment) a, err := centerArc(current, originalSegment)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -69,11 +69,11 @@ func (p GeometryPath) Transform(matrix Matrix) (GeometryPath, error) {
continue continue
} }
if s.RadiusX == 0 || s.RadiusY == 0 { if s.RadiusX == 0 || s.RadiusY == 0 {
s = GeometrySegment{Verb: GeometryLine, End: s.End} s = Segment{Verb: Line, End: s.End}
} else { } else {
a.center = transform(a.center) a.center = transform(a.center)
a.u.X, a.u.Y = matrixVector(matrix, a.u.X, a.u.Y) a.u.X, a.u.Y = Vector(matrix, a.u.X, a.u.Y)
a.v.X, a.v.Y = matrixVector(matrix, a.v.X, a.v.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)) scale := math.Max(math.Hypot(a.u.X, a.u.Y), math.Hypot(a.v.X, a.v.Y))
if !finite(scale) { if !finite(scale) {
return nil, fmt.Errorf("arc transform overflow") return nil, fmt.Errorf("arc transform overflow")
@@ -82,7 +82,7 @@ func (p GeometryPath) Transform(matrix Matrix) (GeometryPath, error) {
if matrixScale != 0 { if matrixScale != 0 {
return nil, fmt.Errorf("arc transform precision exhausted") return nil, fmt.Errorf("arc transform precision exhausted")
} }
s = GeometrySegment{Verb: GeometryLine, End: s.End} s = Segment{Verb: Line, End: s.End}
} else { } else {
ux, uy, vx, vy := a.u.X/scale, a.u.Y/scale, a.v.X/scale, a.v.Y/scale 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 det := ux*vy - uy*vx
@@ -100,9 +100,9 @@ func (p GeometryPath) Transform(matrix Matrix) (GeometryPath, error) {
} }
slices.Sort(fractions) slices.Sort(fractions)
for _, f := range slices.Compact(fractions) { for _, f := range slices.Compact(fractions) {
result = append(result, GeometrySegment{Verb: GeometryLine, End: a.point(a.theta + f*a.delta)}) result = append(result, Segment{Verb: Line, End: a.point(a.theta + f*a.delta)})
} }
s = GeometrySegment{Verb: GeometryLine, End: s.End} s = Segment{Verb: Line, End: s.End}
} else { } else {
xx, yy, xy := ux*ux+vx*vx, uy*uy+vy*vy, ux*uy+vx*vy 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) radius := math.Sqrt((xx + yy + math.Hypot(xx-yy, 2*xy)) / 2)
@@ -121,32 +121,32 @@ func (p GeometryPath) Transform(matrix Matrix) (GeometryPath, error) {
result = append(result, s) result = append(result, s)
current = original current = original
} }
if err := result.validate(); err != nil { if err := result.Validate(); err != nil {
return nil, err return nil, err
} }
return result, nil return result, nil
} }
// validate 校验公共路径指令、子路径起点和有限坐标 // Validate 校验公共路径指令、子路径起点和有限坐标
// 返回: error 无效路径 // 返回: error 无效路径
func (p GeometryPath) validate() error { func (p Path) Validate() error {
started := false started := false
for _, s := range p { for _, s := range p {
points := [3]Point{s.End, s.Control1, s.Control2} points := [3]Point{s.End, s.Control1, s.Control2}
count := 1 count := 1
switch s.Verb { switch s.Verb {
case GeometryMove: case Move:
started = true started = true
case GeometryLine: case Line:
case GeometryQuad: case Quad:
count = 2 count = 2
case GeometryCubic: case Cubic:
count = 3 count = 3
case GeometryArc: case Arc:
if !finite(s.RadiusX) || !finite(s.RadiusY) || !finite(s.Rotation) || s.RadiusX < 0 || s.RadiusY < 0 { if !finite(s.RadiusX) || !finite(s.RadiusY) || !finite(s.Rotation) || s.RadiusX < 0 || s.RadiusY < 0 {
return fmt.Errorf("invalid arc parameters") return fmt.Errorf("invalid arc parameters")
} }
case GeometryClose: case ClosePath:
count = 0 count = 0
default: default:
return fmt.Errorf("unsupported geometry command %d", s.Verb) return fmt.Errorf("unsupported geometry command %d", s.Verb)
+681
View File
@@ -0,0 +1,681 @@
// 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 threed 提供U3D模型解码、场景求值及软件绘制能力
package threed
import (
"bytes"
"context"
"encoding/binary"
"fmt"
"io"
"math"
)
// DefaultBytes 限制默认解码数据规模,不包括调用方已有输入及运行时开销
const DefaultBytes = 256 << 20
// U3DOptions 控制U3D解码规模,MaxDecodedBytes为0时采用256MiB上限
type U3DOptions struct {
MaxDecodedBytes int64
}
// U3DModel 保存独立于PDF的U3D场景及动作数据,不执行动作或读取外部文件
type U3DModel struct {
Major, Minor int16
DefinedUnits bool
Units float64
Meshes []U3DMesh
Nodes []U3DNode
Materials []U3DMaterial
Shaders []U3DShader
Lights []U3DLight
Views []U3DViewResource
Textures []U3DTexture
Motions []U3DMotion
Metadata []U3DMetadata
}
// U3DMetadata 保留块所属名称与原始元数据,数据不与输入共享存储
type U3DMetadata struct {
Block uint32
Name string
Data []byte
}
// U3DNode 保存组、模型、光源或视图节点,父节点矩阵按列存储
type U3DNode struct {
Name, Kind, Resource string
Parents []U3DParent
Visibility uint32
Shaders [][]string
LineShaders [][]string
PointShaders [][]string
View *U3DView
BoneWeights []U3DBoneWeights
Animations []U3DAnimation
}
// U3DParent 保存父节点名称及局部到父节点的变换,空名称引用默认节点
type U3DParent struct {
Name string
Transform [16]float32
}
// U3DMaterial 保存材质参数,Attributes各位决定相应参数是否生效
type U3DMaterial struct {
Name string
Attributes uint32
Ambient, Diffuse, Specular, Emissive [3]float32
Reflectivity, Opacity float32
}
// U3DShader 保存材质与八个纹理通道,Textures中的空项表示未启用
type U3DShader struct {
Name, Material string
Attributes, AlphaFunction, Blend uint32
RenderPass uint32
AlphaReference float32
Textures [8]*U3DTextureLayer
}
// U3DLight 保存环境、方向、点或聚光资源,Kind依次为0至3
type U3DLight struct {
Name string
Attributes uint32
Kind byte
Color, Attenuation [3]float32
SpotAngle, Intensity float32
}
// u3dBlock 保存校验后的块边界,数据切片仅在解码期间引用输入
type u3dBlock struct {
kind uint32
data, meta []byte
}
// u3dDecoder 保存单次解码预算及场景索引,不跨调用缓存输入
type u3dDecoder struct {
ctx context.Context
model U3DModel
remaining uint64
plain bool
meshes map[string]*u3dMeshDeclaration
meshOrder []string
nodes map[string]int
palettes map[uint32]map[string]bool
textures map[string]int
}
// DecodeU3D 按ECMA-363解码静态几何、骨架、场景节点及材质、纹理图像等资源
// 保留动作轨道和动画修饰器,不在解码时求值;绘制能力由RenderU3D另行校验
// 渐进网格尚未支持
// 点集球面预测暂限不超过两个非对跖方向,不推测多方向合并次序
// 入参: ctx 取消上下文, data U3D数据, options 解码限制
// 返回: *U3DModel 独立场景数据, error 格式、能力、预算或取消错误
func DecodeU3D(ctx context.Context, data []byte, options U3DOptions) (*U3DModel, error) {
if ctx == nil || options.MaxDecodedBytes < 0 {
return nil, fmt.Errorf("invalid U3D decode options")
}
if err := ctx.Err(); err != nil {
return nil, err
}
limit := options.MaxDecodedBytes
if limit == 0 {
limit = DefaultBytes
}
d := u3dDecoder{ctx: ctx, remaining: uint64(limit), meshes: make(map[string]*u3dMeshDeclaration), nodes: make(map[string]int), palettes: make(map[uint32]map[string]bool)}
header, next, err := u3dReadBlock(data, 0)
if err != nil {
return nil, err
}
if header.kind != 0x00443355 {
return nil, fmt.Errorf("invalid U3D file header")
}
r := u3dValues{data: header.data}
version, profile := r.u32(), r.u32()
d.model.Major, d.model.Minor = int16(version), int16(version>>16)
if d.model.Major > 0 || profile & ^uint32(14) != 0 {
return nil, fmt.Errorf("unsupported U3D version or profile")
}
declarationEnd := uint64(r.u32())
fileSize := uint64(r.u32())
fileSize |= uint64(r.u32()) << 32
if r.u32() != 106 {
return nil, fmt.Errorf("unsupported U3D character encoding")
}
d.plain = profile&4 != 0
d.model.DefinedUnits, d.model.Units = profile&8 != 0, 1
if d.model.DefinedUnits {
unitBytes := r.take(8)
if r.err == nil {
d.model.Units = math.Float64frombits(binary.LittleEndian.Uint64(unitBytes))
if !finite(d.model.Units) || d.model.Units <= 0 {
return nil, fmt.Errorf("invalid U3D units")
}
}
}
if err := r.done(); err != nil {
return nil, err
}
if fileSize != uint64(len(data)) || declarationEnd < uint64(next) || declarationEnd > fileSize || declarationEnd%4 != 0 {
return nil, fmt.Errorf("invalid U3D file size")
}
if err := d.metadata(header, ""); err != nil {
return nil, err
}
priority := uint32(0)
for offset := next; offset < len(data); {
if err := ctx.Err(); err != nil {
return nil, err
}
block, end, err := u3dReadBlock(data, offset)
if err != nil {
return nil, fmt.Errorf("U3D block at %d: %w", offset, err)
}
continuation := block.kind == 0xffffff15 || block.kind == 0xffffff3b || block.kind == 0xffffff3c || block.kind == 0xffffff3e || block.kind == 0xffffff3f || block.kind == 0xffffff5c
if uint64(offset) < declarationEnd && (uint64(end) > declarationEnd || continuation) || uint64(offset) >= declarationEnd && !continuation {
return nil, fmt.Errorf("invalid U3D declaration boundary at %d", offset)
}
if block.kind == 0xffffff15 {
r := u3dValues{data: block.data}
value := r.u32()
if err := r.done(); err != nil {
return nil, err
}
if value == 0 || value < priority || value > 0x7fffffff {
return nil, fmt.Errorf("invalid U3D block priority")
}
priority = value
err = d.metadata(block, "")
} else if block.kind == 0xffffff14 {
err = d.chain(block)
} else {
_, err = d.block(block, "", -1, 0)
}
if err != nil {
return nil, fmt.Errorf("U3D block %#x at %d: %w", block.kind, offset, err)
}
offset = end
}
for _, name := range d.meshOrder {
mesh := d.meshes[name]
if !mesh.decoded && mesh.counts[1] != 0 {
return nil, fmt.Errorf("missing U3D base mesh %q", name)
}
d.model.Meshes = append(d.model.Meshes, mesh.mesh)
}
if err := d.checkTextures(); err != nil {
return nil, err
}
if err := d.checkParents(); err != nil {
return nil, err
}
if err := d.checkBoneWeights(); err != nil {
return nil, err
}
if err := ctx.Err(); err != nil {
return nil, err
}
return &d.model, nil
}
// u3dReadBlock 按64位长度计算校验块数据与零填充,避免长度溢出
// 入参: data 块序列, offset 块起点
// 返回: u3dBlock 块, int 下一块起点, error 边界或填充错误
func u3dReadBlock(data []byte, offset int) (u3dBlock, int, error) {
if offset < 0 || offset > len(data) || len(data)-offset < 12 {
return u3dBlock{}, 0, io.ErrUnexpectedEOF
}
header := data[offset : offset+12]
size, meta := uint64(binary.LittleEndian.Uint32(header[4:])), uint64(binary.LittleEndian.Uint32(header[8:]))
start := uint64(offset) + 12
metaStart := start + ((size + 3) &^ 3)
end := metaStart + ((meta + 3) &^ 3)
if end > uint64(len(data)) {
return u3dBlock{}, 0, io.ErrUnexpectedEOF
}
for _, padding := range [][]byte{data[start+size : metaStart], data[metaStart+meta : end]} {
for _, value := range padding {
if value != 0 {
return u3dBlock{}, 0, fmt.Errorf("invalid U3D padding")
}
}
}
return u3dBlock{binary.LittleEndian.Uint32(header), data[start : start+size], data[metaStart : metaStart+meta]}, int(end), nil
}
// reserve 在分配解码数组前检查总预算及取消状态
// 入参: count 元素数量, size 元素规模上界
// 返回: error 预算或取消错误
func (d *u3dDecoder) reserve(count, size uint64) error {
if err := d.ctx.Err(); err != nil {
return err
}
if size == 0 || count > d.remaining/size || count > uint64(^uint(0)>>1)/size {
return fmt.Errorf("U3D decoded data exceeds limit")
}
d.remaining -= count * size
return nil
}
// metadata 校验元数据结构并保留原始数据,不解释或执行其中的动作
// 入参: block 数据块, name 所属资源名
// 返回: error 结构、预算或取消错误
func (d *u3dDecoder) metadata(block u3dBlock, name string) error {
if len(block.meta) == 0 {
return nil
}
if err := d.reserve(uint64(len(block.meta))+128, 1); err != nil {
return err
}
r := u3dValues{data: block.meta}
count := r.u32()
if uint64(count) > uint64(len(block.meta))/8 {
return fmt.Errorf("invalid U3D metadata count")
}
for range count {
if err := d.ctx.Err(); err != nil {
return err
}
flags := r.u32()
r.text()
if flags&1 != 0 {
size := r.u32()
if uint64(size) > uint64(len(r.data)-r.pos) {
return io.ErrUnexpectedEOF
}
r.take(int(size))
} else {
r.text()
}
if r.err != nil {
return r.err
}
}
if err := r.done(); err != nil {
return err
}
d.model.Metadata = append(d.model.Metadata, U3DMetadata{block.kind, name, bytes.Clone(block.meta)})
return nil
}
// chain 解析单层修饰链并校验名称、链类型与修饰顺序
// 入参: block 修饰链块
// 返回: error 结构、能力或资源错误
func (d *u3dDecoder) chain(block u3dBlock) error {
if err := d.reserve(256, 1); err != nil {
return err
}
r := u3dValues{data: block.data}
name, kind, flags := r.text(), r.u32(), r.u32()
if r.err != nil {
return r.err
}
if name == "" {
return fmt.Errorf("cannot redefine U3D default resource")
}
if kind > 2 || flags & ^uint32(3) != 0 {
return fmt.Errorf("invalid U3D modifier chain")
}
if flags&1 != 0 {
for range 4 {
r.f32()
}
}
if flags&2 != 0 {
for range 6 {
r.f32()
}
}
for r.pos%4 != 0 && r.err == nil {
if r.u8() != 0 {
return fmt.Errorf("invalid U3D chain padding")
}
}
count := r.u32()
if r.err != nil {
return r.err
}
if uint64(count) > uint64(len(r.data)-r.pos)/12 {
return fmt.Errorf("invalid U3D modifier count")
}
for index := uint32(0); index < count; index++ {
if err := d.ctx.Err(); err != nil {
return err
}
nested, next, err := u3dReadBlock(r.data, r.pos)
if err != nil {
return err
}
actual, err := d.block(nested, name, int(kind), index)
if err != nil {
return err
}
if actual != name {
return fmt.Errorf("U3D modifier name mismatch")
}
r.pos = next
}
if err := r.done(); err != nil {
return err
}
return d.metadata(block, name)
}
// block 解析支持的静态场景块,不丢弃未实现的修饰器或几何
// 入参: block 数据块, owner 所属链名, chain 链类别,负值表示独立块, index 修饰位置
// 返回: string 资源名, error 格式、能力或资源错误
func (d *u3dDecoder) block(block u3dBlock, owner string, chain int, index uint32) (string, error) {
if err := d.ctx.Err(); err != nil {
return "", err
}
if err := d.reserve(uint64(len(block.data))+256, 1); err != nil {
return "", err
}
r := u3dValues{data: block.data}
name := r.text()
if r.err != nil {
return "", r.err
}
if name == "" {
return "", fmt.Errorf("cannot redefine U3D default resource")
}
var err error
switch block.kind {
case 0xffffff21, 0xffffff22, 0xffffff23, 0xffffff24:
if chain >= 0 && (chain != 0 || index != 0) {
return name, fmt.Errorf("invalid U3D node chain")
}
err = d.node(&r, block.kind, name)
case 0xffffff31:
if chain >= 0 && (chain != 1 || index != 0) {
return name, fmt.Errorf("invalid U3D mesh chain")
}
err = d.meshDeclaration(&r, name)
case 0xffffff3b:
if chain >= 0 {
return name, fmt.Errorf("invalid U3D mesh continuation location")
}
err = d.baseMesh(&r, name)
case 0xffffff36, 0xffffff37:
if chain >= 0 && (chain != 1 || index != 0) {
return name, fmt.Errorf("invalid U3D primitive chain")
}
err = d.primitiveDeclaration(&r, name, block.kind == 0xffffff36)
case 0xffffff3e, 0xffffff3f:
if chain >= 0 {
return name, fmt.Errorf("invalid U3D primitive continuation location")
}
err = d.primitiveContinuation(&r, name, block.kind == 0xffffff3e)
case 0xffffff43, 0xffffff44:
if (chain != 0 && chain != 1) || index == 0 || owner != name || r.u32() != index {
return name, fmt.Errorf("invalid U3D animation or bone weight modifier chain")
}
if _, exists := d.nodes[name]; chain == 0 && !exists || chain == 1 && d.meshes[name] == nil {
return name, fmt.Errorf("missing U3D animation or bone weight modifier target")
}
if block.kind == 0xffffff43 {
err = d.animation(&r, name, chain, index)
} else {
err = d.boneWeights(&r, name, chain, index)
}
case 0xffffff45:
if chain != 0 || index == 0 || owner != name || r.u32() != index {
return name, fmt.Errorf("invalid U3D shading modifier chain")
}
err = d.shading(&r, name)
case 0xffffff51, 0xffffff52, 0xffffff53, 0xffffff54, 0xffffff56:
if chain >= 0 {
return name, fmt.Errorf("invalid U3D palette resource location")
}
err = d.resource(&r, block.kind, name)
case 0xffffff55:
if chain >= 0 && (chain != 2 || index != 0) {
return name, fmt.Errorf("invalid U3D texture chain")
}
err = d.textureDeclaration(&r, name)
case 0xffffff5c:
if chain >= 0 {
return name, fmt.Errorf("invalid U3D texture continuation location")
}
err = d.textureContinuation(&r, name)
default:
return name, fmt.Errorf("unsupported U3D block %#x", block.kind)
}
if err != nil {
return name, err
}
if err := r.done(); err != nil {
return name, err
}
return name, d.metadata(block, name)
}
// node 读取节点与父变换,允许引用后续声明的父节点
// 入参: r 字段读取器, kind 节点类型, name 节点名
// 返回: error 结构或预算错误
func (d *u3dDecoder) node(r *u3dValues, kind uint32, name string) error {
if _, exists := d.nodes[name]; exists {
return fmt.Errorf("duplicate U3D node %q", name)
}
node := U3DNode{Name: name, Kind: "Group"}
count := r.u32()
if uint64(count) > uint64(len(r.data)-r.pos)/66 {
return io.ErrUnexpectedEOF
}
if err := d.reserve(uint64(count), 96); err != nil {
return err
}
node.Parents = make([]U3DParent, int(count))
for index := range node.Parents {
if index%256 == 0 {
if err := d.ctx.Err(); err != nil {
return err
}
}
node.Parents[index].Name = r.text()
for element := range node.Parents[index].Transform {
node.Parents[index].Transform[element] = r.f32()
}
}
if kind == 0xffffff22 {
node.Kind, node.Resource, node.Visibility = "Model", r.text(), r.u32()
if node.Visibility > 3 {
return fmt.Errorf("invalid U3D model visibility")
}
} else if kind == 0xffffff23 {
node.Kind, node.Resource = "Light", r.text()
} else if kind == 0xffffff24 {
node.Kind, node.Resource = "View", r.text()
var err error
node.View, err = d.view(r)
if err != nil {
return err
}
}
d.nodes[name] = len(d.model.Nodes)
d.model.Nodes = append(d.model.Nodes, node)
return r.err
}
// shading 分别读取网格、线集和点集着色列表,保留列表内各着色器的先后次序
// 入参: r 字段读取器, name 目标节点名
// 返回: error 结构、能力或预算错误
func (d *u3dDecoder) shading(r *u3dValues, name string) error {
index, exists := d.nodes[name]
if !exists || d.model.Nodes[index].Kind != "Model" {
return fmt.Errorf("missing U3D shading target")
}
flags := r.u32()
if flags & ^uint32(7) != 0 {
return fmt.Errorf("unsupported U3D shading modifier target")
}
count := r.u32()
if uint64(count) > uint64(len(r.data)-r.pos)/4 {
return io.ErrUnexpectedEOF
}
if err := d.reserve(uint64(count), 24); err != nil {
return err
}
lists := make([][]string, int(count))
for index := range lists {
n := r.u32()
if uint64(n) > uint64(len(r.data)-r.pos)/2 {
return io.ErrUnexpectedEOF
}
if err := d.reserve(uint64(n), 16); err != nil {
return err
}
lists[index] = make([]string, int(n))
for j := range lists[index] {
if j%256 == 0 {
if err := d.ctx.Err(); err != nil {
return err
}
}
lists[index][j] = r.text()
}
}
if flags&1 != 0 {
d.model.Nodes[index].Shaders = lists
}
if flags&2 != 0 {
d.model.Nodes[index].LineShaders = lists
}
if flags&4 != 0 {
d.model.Nodes[index].PointShaders = lists
}
return r.err
}
// resource 读取独立材质、着色器、光源、视图和动作资源
// 入参: r 字段读取器, kind 资源类型, name 资源名
// 返回: error 结构或能力错误
func (d *u3dDecoder) resource(r *u3dValues, kind uint32, name string) error {
palette := d.palettes[kind]
if palette == nil {
palette = make(map[string]bool)
d.palettes[kind] = palette
}
if palette[name] {
return fmt.Errorf("duplicate U3D resource %q", name)
}
palette[name] = true
switch kind {
case 0xffffff56:
return d.motion(r, name)
case 0xffffff52:
return d.viewResource(r, name)
case 0xffffff54:
material := U3DMaterial{Name: name, Attributes: r.u32()}
if material.Attributes & ^uint32(63) != 0 {
return fmt.Errorf("invalid U3D material attributes")
}
for _, color := range []*[3]float32{&material.Ambient, &material.Diffuse, &material.Specular, &material.Emissive} {
for index := range color {
color[index] = r.f32()
}
}
material.Reflectivity, material.Opacity = r.f32(), r.f32()
d.model.Materials = append(d.model.Materials, material)
case 0xffffff53:
shader := U3DShader{Name: name, Attributes: r.u32(), AlphaReference: r.f32(), AlphaFunction: r.u32(), Blend: r.u32(), RenderPass: r.u32()}
channels, alpha := r.u32(), r.u32()
shader.Material = r.text()
if shader.Attributes & ^uint32(7) != 0 || shader.AlphaFunction < 0x610 || shader.AlphaFunction > 0x617 || shader.Blend < 0x604 || shader.Blend > 0x607 || channels & ^uint32(255) != 0 || alpha & ^channels != 0 {
return fmt.Errorf("invalid U3D shader parameters")
}
for channel := range shader.Textures {
if channels&(1<<channel) == 0 {
continue
}
if err := d.reserve(1, 192); err != nil {
return err
}
layer := &U3DTextureLayer{Texture: r.text(), Intensity: r.f32(), Blend: r.u8(), BlendSource: r.u8(), BlendConstant: r.f32(), Mode: r.u8(), Alpha: alpha&(1<<channel) != 0}
for i := range layer.Transform {
layer.Transform[i] = r.f32()
}
for i := range layer.WrapTransform {
layer.WrapTransform[i] = r.f32()
}
layer.Repeat = r.u8()
if r.err != nil {
return r.err
}
if layer.Blend > 3 || layer.BlendSource > 1 || layer.Mode > 4 || layer.Repeat > 3 {
return fmt.Errorf("invalid U3D texture layer")
}
shader.Textures[channel] = layer
}
d.model.Shaders = append(d.model.Shaders, shader)
case 0xffffff51:
light := U3DLight{Name: name, Attributes: r.u32(), Kind: r.u8()}
if light.Attributes & ^uint32(7) != 0 || light.Kind > 3 {
return fmt.Errorf("invalid U3D light parameters")
}
for index := range light.Color {
light.Color[index] = r.f32()
}
if r.f32() != 1 {
return fmt.Errorf("invalid U3D light reserved parameter")
}
for index := range light.Attenuation {
light.Attenuation[index] = r.f32()
}
light.SpotAngle, light.Intensity = r.f32(), r.f32()
d.model.Lights = append(d.model.Lights, light)
}
return r.err
}
// checkParents 迭代检查场景父子环路,不递归展开多父节点的实例组合
// 返回: error 环路或取消错误
func (d *u3dDecoder) checkParents() error {
state := make([]byte, len(d.model.Nodes))
type frame struct{ node, parent int }
var stack []frame
for index := range d.model.Nodes {
if state[index] != 0 {
continue
}
stack = append(stack[:0], frame{node: index})
state[index] = 1
for len(stack) != 0 {
if err := d.ctx.Err(); err != nil {
return err
}
current := &stack[len(stack)-1]
parents := d.model.Nodes[current.node].Parents
if current.parent == len(parents) {
state[current.node] = 2
stack = stack[:len(stack)-1]
continue
}
parent, exists := d.nodes[parents[current.parent].Name]
current.parent++
if !exists || state[parent] == 2 {
continue
}
if state[parent] == 1 {
return fmt.Errorf("cyclic U3D node parents")
}
state[parent] = 1
stack = append(stack, frame{node: parent})
}
}
return nil
}
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package ofdgo package threed
import ( import (
"context" "context"
@@ -110,7 +110,7 @@ func (animation U3DAnimation) validate(ctx context.Context) error {
// checkAnimations 校验全部动作引用,仅对接入场景的修饰器检查求值能力 // checkAnimations 校验全部动作引用,仅对接入场景的修饰器检查求值能力
// 返回: error 引用、能力或取消错误 // 返回: error 引用、能力或取消错误
func (r *u3dRender) checkAnimations() error { func (r *Renderer) checkAnimations() error {
check := func(animations []U3DAnimation, evaluate bool) error { check := func(animations []U3DAnimation, evaluate bool) error {
for _, animation := range animations { for _, animation := range animations {
if err := animation.validate(r.ctx); err != nil { if err := animation.validate(r.ctx); err != nil {
@@ -138,7 +138,7 @@ func (r *u3dRender) checkAnimations() error {
} }
var active []bool var active []bool
if r.connected != nil { if r.connected != nil {
if err := r.reserve(uint64(len(r.model.Meshes)), 1); err != nil { if err := r.Reserve(uint64(len(r.model.Meshes)), 1); err != nil {
return err return err
} }
defer func() { r.remaining += uint64(len(active)) }() defer func() { r.remaining += uint64(len(active)) }()
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package ofdgo package threed
import ( import (
"encoding/binary" "encoding/binary"
+75
View File
@@ -0,0 +1,75 @@
// 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 threed
import (
"fmt"
"math"
)
// DepthRange 计算可见模型几何的相机深度范围,不为自动裁剪复制顶点
// 返回: float64 最近深度, float64 最远深度, bool 是否含几何, error 几何或取消错误
func (r *Renderer) DepthRange() (float64, float64, bool, error) {
minimum, maximum := math.Inf(1), math.Inf(-1)
for i, instance := range r.scene {
if i%256 == 0 {
if err := r.ctx.Err(); err != nil {
return 0, 0, false, err
}
}
node := r.model.Nodes[instance.node]
index, ok := r.meshes[node.Resource]
if node.Kind != "Model" || instance.modelVisibility(&node) == 0 || instance.opacity == 0 || !ok {
continue
}
mesh := &r.model.Meshes[index]
matrix := r.toCamera.multiply(instance.world)
for j := range len(mesh.Faces) + len(mesh.Lines) + len(mesh.Points) {
_, corners := mesh.primitive(j)
if j%256 == 0 {
if err := r.ctx.Err(); err != nil {
return 0, 0, false, err
}
}
for _, corner := range corners {
if uint64(corner.Position) >= uint64(len(mesh.Positions)) {
return 0, 0, false, fmt.Errorf("invalid U3D position index")
}
p := mesh.Positions[corner.Position]
z := matrix[2]*float64(p[0]) + matrix[6]*float64(p[1]) + matrix[10]*float64(p[2]) + matrix[14]
if !finite(z) {
return 0, 0, false, fmt.Errorf("invalid U3D camera depth")
}
minimum, maximum = min(minimum, z), max(maximum, z)
}
}
}
for i, section := range r.sections {
if i%256 == 0 {
if err := r.ctx.Err(); err != nil {
return 0, 0, false, err
}
}
if section.opacity > 0 {
for _, corner := range section.corners {
minimum, maximum = min(minimum, corner.point[2]), max(maximum, corner.point[2])
}
}
}
if math.IsInf(minimum, 1) {
return 0, 0, false, nil
}
return minimum, maximum, true, nil
}
+26
View File
@@ -0,0 +1,26 @@
// 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 threed
import (
"math"
)
// finite 判断数值是否有限
// 入参: value 数值
// 返回: bool 是否有限
func finite(value float64) bool {
return !math.IsNaN(value) && !math.IsInf(value, 0)
}
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package ofdgo package threed
import ( import (
"fmt" "fmt"
@@ -47,7 +47,7 @@ type u3dRenderStyle struct {
// collectLights 收集本轮根节点下启用的光源实例 // collectLights 收集本轮根节点下启用的光源实例
// 返回: error 参数、预算或取消错误 // 返回: error 参数、预算或取消错误
func (r *u3dRender) collectLights() error { func (r *Renderer) collectLights() error {
if r.lighting != nil { if r.lighting != nil {
return nil return nil
} }
@@ -101,7 +101,7 @@ func (r *u3dRender) collectLights() error {
} }
if len(r.lights) == cap(r.lights) { if len(r.lights) == cap(r.lights) {
capacity := max(8, cap(r.lights)*2) capacity := max(8, cap(r.lights)*2)
if err := r.reserve(uint64(capacity), 128); err != nil { if err := r.Reserve(uint64(capacity), 128); err != nil {
return err return err
} }
values := make([]u3dRenderLight, len(r.lights), capacity) values := make([]u3dRenderLight, len(r.lights), capacity)
@@ -117,7 +117,7 @@ func (r *u3dRender) collectLights() error {
// overrideLighting 校验并建立替代光源,不依赖模型光源节点 // overrideLighting 校验并建立替代光源,不依赖模型光源节点
// 入参: lighting 灯光覆盖,空值保留模型灯光 // 入参: lighting 灯光覆盖,空值保留模型灯光
// 返回: error 参数、预算或取消错误 // 返回: error 参数、预算或取消错误
func (r *u3dRender) overrideLighting(lighting *U3DLighting) error { func (r *Renderer) overrideLighting(lighting *U3DLighting) error {
if lighting == nil { if lighting == nil {
return nil return nil
} }
@@ -126,7 +126,7 @@ func (r *u3dRender) overrideLighting(lighting *U3DLighting) error {
return fmt.Errorf("invalid U3D diffuse offset") return fmt.Errorf("invalid U3D diffuse offset")
} }
} }
if err := r.reserve(uint64(len(lighting.Lights)), 128); err != nil { if err := r.Reserve(uint64(len(lighting.Lights)), 128); err != nil {
return err return err
} }
r.lights = make([]u3dRenderLight, len(lighting.Lights)) r.lights = make([]u3dRenderLight, len(lighting.Lights))
@@ -158,7 +158,7 @@ func (r *u3dRender) overrideLighting(lighting *U3DLighting) error {
// shaderPass 返回着色器的绘制轮次位图,默认着色器仅参与第一轮 // shaderPass 返回着色器的绘制轮次位图,默认着色器仅参与第一轮
// 入参: name 着色器名 // 入参: name 着色器名
// 返回: uint32 绘制轮次位图 // 返回: uint32 绘制轮次位图
func (r *u3dRender) shaderPass(name string) uint32 { func (r *Renderer) shaderPass(name string) uint32 {
if index, exists := r.shaders[name]; exists { if index, exists := r.shaders[name]; exists {
return r.model.Shaders[index].RenderPass return r.model.Shaders[index].RenderPass
} }
@@ -168,7 +168,7 @@ func (r *u3dRender) shaderPass(name string) uint32 {
// style 解析着色器与材质,缺失资源采用ECMA-363第8.4节的默认资源 // style 解析着色器与材质,缺失资源采用ECMA-363第8.4节的默认资源
// 入参: name 着色器名 // 入参: name 着色器名
// 返回: u3dRenderStyle 有效绘制参数, error 参数错误 // 返回: u3dRenderStyle 有效绘制参数, error 参数错误
func (r *u3dRender) style(name string) (u3dRenderStyle, error) { func (r *Renderer) style(name string) (u3dRenderStyle, error) {
style := u3dRenderStyle{shader: U3DShader{Attributes: 1, Blend: 0x606, AlphaFunction: 0x617, RenderPass: 1}, ambient: [3]float64{.75, .75, .75}, opacity: 1} style := u3dRenderStyle{shader: U3DShader{Attributes: 1, Blend: 0x606, AlphaFunction: 0x617, RenderPass: 1}, ambient: [3]float64{.75, .75, .75}, opacity: 1}
if index, ok := r.shaders[name]; ok { if index, ok := r.shaders[name]; ok {
style.shader = r.model.Shaders[index] style.shader = r.model.Shaders[index]
@@ -215,7 +215,7 @@ func (r *u3dRender) style(name string) (u3dRenderStyle, error) {
// shade 按世界坐标计算逐顶点漫反射、镜面与环境光 // shade 按世界坐标计算逐顶点漫反射、镜面与环境光
// 入参: position 顶点, normal 法线, style 材质, diffuse 顶点漫反射, specular 顶点镜面颜色 // 入参: position 顶点, normal 法线, style 材质, diffuse 顶点漫反射, specular 顶点镜面颜色
// 返回: [4]float64 直通RGBA, error 参数或取消错误 // 返回: [4]float64 直通RGBA, error 参数或取消错误
func (r *u3dRender) shade(position, normal u3dVector, style u3dRenderStyle, diffuse, specular *[4]float32) ([4]float64, error) { func (r *Renderer) shade(position, normal u3dVector, style u3dRenderStyle, diffuse, specular *[4]float32) ([4]float64, error) {
color := [4]float64{style.diffuse[0], style.diffuse[1], style.diffuse[2], style.opacity} color := [4]float64{style.diffuse[0], style.diffuse[1], style.diffuse[2], style.opacity}
if style.shader.Attributes&4 != 0 { if style.shader.Attributes&4 != 0 {
if diffuse != nil { if diffuse != nil {
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package ofdgo package threed
import ( import (
"fmt" "fmt"
@@ -22,7 +22,7 @@ import (
// drawPrimitives 绘制作者点集及线集,独立着色且不参与面的背向剔除 // drawPrimitives 绘制作者点集及线集,独立着色且不参与面的背向剔除
// 入参: instance 场景实例, node 模型节点, mesh 属性数组, positions 世界位置, normals 世界法线, pass 绘制轮次, passIndex 轮次索引 // 入参: instance 场景实例, node 模型节点, mesh 属性数组, positions 世界位置, normals 世界法线, pass 绘制轮次, passIndex 轮次索引
// 返回: error 几何、资源或取消错误 // 返回: error 几何、资源或取消错误
func (r *u3dRender) drawPrimitives(instance u3dInstance, node *U3DNode, mesh *U3DMesh, positions, normals []u3dVector, pass U3DViewPass, passIndex int) error { func (r *Renderer) drawPrimitives(instance u3dInstance, node *U3DNode, mesh *U3DMesh, positions, normals []u3dVector, pass U3DViewPass, passIndex int) error {
if r.depthOnly { if r.depthOnly {
return nil return nil
} }
@@ -158,7 +158,7 @@ func (r *u3dRender) drawPrimitives(instance u3dInstance, node *U3DNode, mesh *U3
// lineIntersection 绘制作者线段与剖切面的交点,不绘制共面线段 // lineIntersection 绘制作者线段与剖切面的交点,不绘制共面线段
// 入参: line 相机空间线段, index 剖切面索引 // 入参: line 相机空间线段, index 剖切面索引
// 返回: error 数值、资源或取消错误 // 返回: error 数值、资源或取消错误
func (r *u3dRender) lineIntersection(line [2]u3dVertex, index int) error { func (r *Renderer) lineIntersection(line [2]u3dVertex, index int) error {
section := r.sections[index] section := r.sections[index]
a, b := u3dPlaneDistance(section.plane, line[0].point), u3dPlaneDistance(section.plane, line[1].point) a, b := u3dPlaneDistance(section.plane, line[0].point), u3dPlaneDistance(section.plane, line[1].point)
if !finite(a) || !finite(b) { if !finite(a) || !finite(b) {
@@ -201,7 +201,7 @@ func (r *u3dRender) lineIntersection(line [2]u3dVertex, index int) error {
// frustumPlanes 返回保留正侧的六个相机视锥面 // frustumPlanes 返回保留正侧的六个相机视锥面
// 返回: [6][4]float64 近、远、左右及上下平面 // 返回: [6][4]float64 近、远、左右及上下平面
func (r *u3dRender) frustumPlanes() [6][4]float64 { func (r *Renderer) frustumPlanes() [6][4]float64 {
w, h := r.viewport[2]/2, r.viewport[3]/2 w, h := r.viewport[2]/2, r.viewport[3]/2
planes := [6][4]float64{{0, 0, 1, -r.camera.Near}, {0, 0, -1, r.camera.Far}, {r.camera.ScaleX, 0, 0, w + r.camera.ShiftX}, {-r.camera.ScaleX, 0, 0, w - r.camera.ShiftX}, {0, r.camera.ScaleY, 0, h - r.camera.ShiftY}, {0, -r.camera.ScaleY, 0, h + r.camera.ShiftY}} planes := [6][4]float64{{0, 0, 1, -r.camera.Near}, {0, 0, -1, r.camera.Far}, {r.camera.ScaleX, 0, 0, w + r.camera.ShiftX}, {-r.camera.ScaleX, 0, 0, w - r.camera.ShiftX}, {0, r.camera.ScaleY, 0, h - r.camera.ShiftY}, {0, -r.camera.ScaleY, 0, h + r.camera.ShiftY}}
if r.camera.Perspective { if r.camera.Perspective {
@@ -261,7 +261,7 @@ func u3dInterpolate(a, b u3dVertex, t float64) u3dVertex {
// sectionIntersection 绘制原三角面与指定剖切面的交线,其他剖切面仍参与裁剪 // sectionIntersection 绘制原三角面与指定剖切面的交线,其他剖切面仍参与裁剪
// 入参: triangle 原三角面, index 剖切面索引 // 入参: triangle 原三角面, index 剖切面索引
// 返回: error 数值、资源或取消错误 // 返回: error 数值、资源或取消错误
func (r *u3dRender) sectionIntersection(triangle [3]u3dVertex, index int) error { func (r *Renderer) sectionIntersection(triangle [3]u3dVertex, index int) error {
if err := r.ctx.Err(); err != nil { if err := r.ctx.Err(); err != nil {
return err return err
} }
@@ -352,7 +352,7 @@ func u3dClipLine(a, b *u3dVertex, plane [4]float64) (bool, error) {
// rasterSectionLine 绘制单像素不透明交线,透视深度使用倒数插值 // rasterSectionLine 绘制单像素不透明交线,透视深度使用倒数插值
// 入参: a 已裁剪起点, b 已裁剪终点, rgb 交线颜色, normal 原面法线, origin 原面上一点 // 入参: a 已裁剪起点, b 已裁剪终点, rgb 交线颜色, normal 原面法线, origin 原面上一点
// 返回: error 数值、资源或取消错误 // 返回: error 数值、资源或取消错误
func (r *u3dRender) rasterSectionLine(a, b u3dVertex, rgb [3]float64, normal, origin u3dVector) error { func (r *Renderer) rasterSectionLine(a, b u3dVertex, rgb [3]float64, normal, origin u3dVector) error {
a.color = [4]float64{rgb[0], rgb[1], rgb[2], 1} a.color = [4]float64{rgb[0], rgb[1], rgb[2], 1}
b.color = a.color b.color = a.color
return r.rasterLine(a, b, normal, origin, U3DShader{Blend: 0x606}, U3DViewPass{}) return r.rasterLine(a, b, normal, origin, U3DShader{Blend: 0x606}, U3DViewPass{})
@@ -361,7 +361,7 @@ func (r *u3dRender) rasterSectionLine(a, b u3dVertex, rgb [3]float64, normal, or
// rasterLine 绘制单像素线段,颜色与深度按透视插值,面深度偏移防止自身遮挡 // rasterLine 绘制单像素线段,颜色与深度按透视插值,面深度偏移防止自身遮挡
// 入参: a 已裁剪起点, b 已裁剪终点, normal 面法线, origin 面上点, shader 着色器, pass 绘制轮次 // 入参: a 已裁剪起点, b 已裁剪终点, normal 面法线, origin 面上点, shader 着色器, pass 绘制轮次
// 返回: error 数值、资源或取消错误 // 返回: error 数值、资源或取消错误
func (r *u3dRender) rasterLine(a, b u3dVertex, normal, origin u3dVector, shader U3DShader, pass U3DViewPass) error { func (r *Renderer) rasterLine(a, b u3dVertex, normal, origin u3dVector, shader U3DShader, pass U3DViewPass) error {
textures, err := r.shaderTextures(shader) textures, err := r.shaderTextures(shader)
if err != nil { if err != nil {
return err return err
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package ofdgo package threed
import ( import (
"fmt" "fmt"
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package ofdgo package threed
import ( import (
"fmt" "fmt"
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package ofdgo package threed
import ( import (
"fmt" "fmt"
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package ofdgo package threed
import ( import (
"fmt" "fmt"
@@ -57,7 +57,7 @@ type u3dTexturedPrimitiveKey struct {
// uniquePrimitive 在当前网格内去重着色边点,缓存按条目计入绘制预算 // uniquePrimitive 在当前网格内去重着色边点,缓存按条目计入绘制预算
// 入参: a 起点或顶点, b 终点或相同顶点, shader 着色器 // 入参: a 起点或顶点, b 终点或相同顶点, shader 着色器
// 返回: bool 是否首次绘制, error 预算或取消错误 // 返回: bool 是否首次绘制, error 预算或取消错误
func (r *u3dRender) uniquePrimitive(a, b u3dVertex, shader U3DShader) (bool, error) { func (r *Renderer) uniquePrimitive(a, b u3dVertex, shader U3DShader) (bool, error) {
if r.primitives == nil { if r.primitives == nil {
return true, nil return true, nil
} }
@@ -81,7 +81,7 @@ func (r *u3dRender) uniquePrimitive(a, b u3dVertex, shader U3DShader) (bool, err
if _, ok := r.texturedPrimitives[textured]; ok { if _, ok := r.texturedPrimitives[textured]; ok {
return false, nil return false, nil
} }
if err := r.reserve(1, 1024); err != nil { if err := r.Reserve(1, 1024); err != nil {
return false, err return false, err
} }
if r.texturedPrimitives == nil { if r.texturedPrimitives == nil {
@@ -93,7 +93,7 @@ func (r *u3dRender) uniquePrimitive(a, b u3dVertex, shader U3DShader) (bool, err
if _, ok := r.primitives[key]; ok { if _, ok := r.primitives[key]; ok {
return false, nil return false, nil
} }
if err := r.reserve(1, 512); err != nil { if err := r.Reserve(1, 512); err != nil {
return false, err return false, err
} }
r.primitives[key] = struct{}{} r.primitives[key] = struct{}{}
@@ -115,7 +115,7 @@ func (mode U3DRenderMode) silhouette() bool {
// modeTriangle 根据当前模式绘制原三角面、原始边或顶点,不为裁剪边界生成网格边 // modeTriangle 根据当前模式绘制原三角面、原始边或顶点,不为裁剪边界生成网格边
// 入参: triangle 相机空间顶点, shader 着色器, pass 绘制轮次 // 入参: triangle 相机空间顶点, shader 着色器, pass 绘制轮次
// 返回: error 数值、资源或取消错误 // 返回: error 数值、资源或取消错误
func (r *u3dRender) modeTriangle(triangle [3]u3dVertex, shader U3DShader, pass U3DViewPass) error { func (r *Renderer) modeTriangle(triangle [3]u3dVertex, shader U3DShader, pass U3DViewPass) error {
if r.depthOnly || r.mode == U3DRenderSolid || r.mode.silhouette() { if r.depthOnly || r.mode == U3DRenderSolid || r.mode.silhouette() {
return r.sectionTriangle(triangle, shader, pass) return r.sectionTriangle(triangle, shader, pass)
} }
@@ -171,7 +171,7 @@ func (r *u3dRender) modeTriangle(triangle [3]u3dVertex, shader U3DShader, pass U
// modeLine 对原始网格边应用剖切与视锥裁剪,再按像素深度绘制 // modeLine 对原始网格边应用剖切与视锥裁剪,再按像素深度绘制
// 入参: a 起点, b 终点, normal 面法线, origin 面上点, shader 着色器, pass 绘制轮次 // 入参: a 起点, b 终点, normal 面法线, origin 面上点, shader 着色器, pass 绘制轮次
// 返回: error 数值、资源或取消错误 // 返回: error 数值、资源或取消错误
func (r *u3dRender) modeLine(a, b u3dVertex, normal, origin u3dVector, shader U3DShader, pass U3DViewPass) error { func (r *Renderer) modeLine(a, b u3dVertex, normal, origin u3dVector, shader U3DShader, pass U3DViewPass) error {
for i, section := range r.sections { for i, section := range r.sections {
if i%256 == 0 { if i%256 == 0 {
if err := r.ctx.Err(); err != nil { if err := r.ctx.Err(); err != nil {
@@ -198,7 +198,7 @@ func (r *u3dRender) modeLine(a, b u3dVertex, normal, origin u3dVector, shader U3
// modePoint 绘制原始顶点,不在裁剪边界补充顶点 // modePoint 绘制原始顶点,不在裁剪边界补充顶点
// 入参: vertex 顶点, shader 着色器, pass 绘制轮次 // 入参: vertex 顶点, shader 着色器, pass 绘制轮次
// 返回: error 数值、资源或取消错误 // 返回: error 数值、资源或取消错误
func (r *u3dRender) modePoint(vertex u3dVertex, shader U3DShader, pass U3DViewPass) error { func (r *Renderer) modePoint(vertex u3dVertex, shader U3DShader, pass U3DViewPass) error {
if err := r.ctx.Err(); err != nil { if err := r.ctx.Err(); err != nil {
return err return err
} }
@@ -262,7 +262,7 @@ func (r *u3dRender) modePoint(vertex u3dVertex, shader U3DShader, pass U3DViewPa
// drawBounds 在模型局部空间计算包围盒,再应用实例变换,不以世界坐标轴重建包围盒 // drawBounds 在模型局部空间计算包围盒,再应用实例变换,不以世界坐标轴重建包围盒
// 入参: instance 场景实例, node 模型节点, mesh 网格, pass 绘制轮次, passIndex 轮次索引 // 入参: instance 场景实例, node 模型节点, mesh 网格, pass 绘制轮次, passIndex 轮次索引
// 返回: error 几何、数值、预算或取消错误 // 返回: error 几何、数值、预算或取消错误
func (r *u3dRender) drawBounds(instance u3dInstance, node *U3DNode, mesh *U3DMesh, pass U3DViewPass, passIndex int) error { func (r *Renderer) drawBounds(instance u3dInstance, node *U3DNode, mesh *U3DMesh, pass U3DViewPass, passIndex int) error {
minimum := u3dVector{math.Inf(1), math.Inf(1), math.Inf(1)} minimum := u3dVector{math.Inf(1), math.Inf(1), math.Inf(1)}
maximum := u3dVector{math.Inf(-1), math.Inf(-1), math.Inf(-1)} maximum := u3dVector{math.Inf(-1), math.Inf(-1), math.Inf(-1)}
visible := false visible := false
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package ofdgo package threed
import ( import (
"fmt" "fmt"
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package ofdgo package threed
import ( import (
"fmt" "fmt"
@@ -39,7 +39,7 @@ type u3dOutlineEdge struct {
// outlineFace 收集当前绘制轮次中的原始网格边,正面法线在同一半圆内取角度极值 // outlineFace 收集当前绘制轮次中的原始网格边,正面法线在同一半圆内取角度极值
// 入参: outline 共享边索引, world 世界坐标, vertices 相机顶点, front 是否正面, visible 是否绘制面, shaders 着色列表, passIndex 轮次 // 入参: outline 共享边索引, world 世界坐标, vertices 相机顶点, front 是否正面, visible 是否绘制面, shaders 着色列表, passIndex 轮次
// 返回: error 数值、预算或取消错误 // 返回: error 数值、预算或取消错误
func (r *u3dRender) outlineFace(outline *u3dOutline, world [3]u3dVector, vertices [3]u3dVertex, front, visible bool, shaders []string, passIndex int) error { func (r *Renderer) outlineFace(outline *u3dOutline, world [3]u3dVector, vertices [3]u3dVertex, front, visible bool, shaders []string, passIndex int) error {
active := false active := false
for i, name := range shaders { for i, name := range shaders {
if i%128 == 0 { if i%128 == 0 {
@@ -75,7 +75,7 @@ func (r *u3dRender) outlineFace(outline *u3dOutline, world [3]u3dVector, vertice
key := [2]u3dVector{a, b} key := [2]u3dVector{a, b}
index, exists := outline.index[key] index, exists := outline.index[key]
if !exists { if !exists {
if err := r.reserve(1, 1024); err != nil { if err := r.Reserve(1, 1024); err != nil {
return err return err
} }
index = len(outline.edges) index = len(outline.edges)
@@ -116,7 +116,7 @@ func (r *u3dRender) outlineFace(outline *u3dOutline, world [3]u3dVector, vertice
// drawOutline 绘制外缘、正背面交界和达到夹角的共享边,沿用像素深度去除遮挡 // drawOutline 绘制外缘、正背面交界和达到夹角的共享边,沿用像素深度去除遮挡
// 入参: outline 共享边索引, pass 绘制轮次 // 入参: outline 共享边索引, pass 绘制轮次
// 返回: error 数值、资源或取消错误 // 返回: error 数值、资源或取消错误
func (r *u3dRender) drawOutline(outline *u3dOutline, pass U3DViewPass) error { func (r *Renderer) drawOutline(outline *u3dOutline, pass U3DViewPass) error {
shader := U3DShader{Blend: 0x606} shader := U3DShader{Blend: 0x606}
color := [4]float64{r.auxiliaryColor[0], r.auxiliaryColor[1], r.auxiliaryColor[2], 1} color := [4]float64{r.auxiliaryColor[0], r.auxiliaryColor[1], r.auxiliaryColor[2], 1}
for i, edge := range outline.edges { for i, edge := range outline.edges {
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package ofdgo package threed
import ( import (
"cmp" "cmp"
@@ -36,7 +36,7 @@ type u3dPlaybackKey struct {
// u3dPlaybackCache 在一次动画求值中共享轨道时间范围和采样结果 // u3dPlaybackCache 在一次动画求值中共享轨道时间范围和采样结果
type u3dPlaybackCache struct { type u3dPlaybackCache struct {
render *u3dRender render *Renderer
bounds map[*U3DMotionTrack][2]float64 bounds map[*U3DMotionTrack][2]float64
samples map[u3dPlaybackKey]U3DKeyFrame samples map[u3dPlaybackKey]U3DKeyFrame
bytes uint64 bytes uint64
@@ -68,13 +68,13 @@ func (c *u3dPlaybackCache) limits(track *U3DMotionTrack) ([2]float64, error) {
bounds[0], bounds[1] = min(bounds[0], float64(frame.Time)), max(bounds[1], float64(frame.Time)) bounds[0], bounds[1] = min(bounds[0], float64(frame.Time)), max(bounds[1], float64(frame.Time))
} }
if c.bounds == nil { if c.bounds == nil {
if err := c.render.reserve(1, 1024); err != nil { if err := c.render.Reserve(1, 1024); err != nil {
return bounds, err return bounds, err
} }
c.bytes += 1024 c.bytes += 1024
c.bounds = make(map[*U3DMotionTrack][2]float64) c.bounds = make(map[*U3DMotionTrack][2]float64)
} }
if err := c.render.reserve(1, 96); err != nil { if err := c.render.Reserve(1, 96); err != nil {
return bounds, err return bounds, err
} }
c.bytes += 96 c.bytes += 96
@@ -98,13 +98,13 @@ func (c *u3dPlaybackCache) sample(track *U3DMotionTrack, time float64) (U3DKeyFr
return frame, err return frame, err
} }
if c.samples == nil { if c.samples == nil {
if err := c.render.reserve(1, 1024); err != nil { if err := c.render.Reserve(1, 1024); err != nil {
return frame, err return frame, err
} }
c.bytes += 1024 c.bytes += 1024
c.samples = make(map[u3dPlaybackKey]U3DKeyFrame) c.samples = make(map[u3dPlaybackKey]U3DKeyFrame)
} }
if err := c.render.reserve(1, 128); err != nil { if err := c.render.Reserve(1, 128); err != nil {
return frame, err return frame, err
} }
c.bytes += 128 c.bytes += 128
@@ -115,7 +115,7 @@ func (c *u3dPlaybackCache) sample(track *U3DMotionTrack, time float64) (U3DKeyFr
// prepareAnimations 按资源链、节点链顺序求值,不保存上一帧状态或推进实际时钟 // prepareAnimations 按资源链、节点链顺序求值,不保存上一帧状态或推进实际时钟
// 入参: time 从初始时刻起算的秒数 // 入参: time 从初始时刻起算的秒数
// 返回: error 动作、骨架、修饰顺序、预算或取消错误 // 返回: error 动作、骨架、修饰顺序、预算或取消错误
func (r *u3dRender) prepareAnimations(time float64) error { func (r *Renderer) prepareAnimations(time float64) error {
cache := u3dPlaybackCache{render: r} cache := u3dPlaybackCache{render: r}
defer func() { r.remaining += cache.bytes }() defer func() { r.remaining += cache.bytes }()
for i := range r.model.Nodes { for i := range r.model.Nodes {
@@ -136,7 +136,7 @@ func (r *u3dRender) prepareAnimations(time float64) error {
for chain, group := range groups { for chain, group := range groups {
ordered := group ordered := group
if len(group) > 1 { if len(group) > 1 {
if err := r.reserve(uint64(len(group)), 48); err != nil { if err := r.Reserve(uint64(len(group)), 48); err != nil {
return err return err
} }
ordered = slices.Clone(group) ordered = slices.Clone(group)
@@ -167,12 +167,12 @@ func (r *u3dRender) prepareAnimations(time float64) error {
} }
if used { if used {
if r.animated == nil { if r.animated == nil {
if err := r.reserve(1, 1024); err != nil { if err := r.Reserve(1, 1024); err != nil {
return err return err
} }
r.animated = make(map[string]u3dAnimationState) r.animated = make(map[string]u3dAnimationState)
} }
if err := r.reserve(1, 384); err != nil { if err := r.Reserve(1, 384); err != nil {
return err return err
} }
r.animated[node.Name] = state r.animated[node.Name] = state
@@ -184,7 +184,7 @@ func (r *u3dRender) prepareAnimations(time float64) error {
// animationTime 将毫秒偏移和两级时间倍率换算为动作时间,已绑定轨道共用循环区间 // animationTime 将毫秒偏移和两级时间倍率换算为动作时间,已绑定轨道共用循环区间
// 入参: animation 动画修饰器, tracks 动作轨道, bindings 骨骼绑定,空值表示全部轨道, time 从初始时刻起算的秒数, cache 本次采样缓存 // 入参: animation 动画修饰器, tracks 动作轨道, bindings 骨骼绑定,空值表示全部轨道, time 从初始时刻起算的秒数, cache 本次采样缓存
// 返回: float64 动作采样时间, error 时间数值或取消错误 // 返回: float64 动作采样时间, error 时间数值或取消错误
func (r *u3dRender) animationTime(animation *U3DAnimation, tracks []U3DMotionTrack, bindings map[string]int, time float64, cache *u3dPlaybackCache) (float64, error) { func (r *Renderer) animationTime(animation *U3DAnimation, tracks []U3DMotionTrack, bindings map[string]int, time float64, cache *u3dPlaybackCache) (float64, error) {
reference := animation.Motions[0] reference := animation.Motions[0]
if animation.Attributes&1 == 0 { if animation.Attributes&1 == 0 {
time = 0 time = 0
@@ -225,7 +225,7 @@ func (r *u3dRender) animationTime(animation *U3DAnimation, tracks []U3DMotionTra
// sampleAnimation 求值单个动作的节点或骨骼姿态,锁定根骨骼时保留其初始变换 // sampleAnimation 求值单个动作的节点或骨骼姿态,锁定根骨骼时保留其初始变换
// 入参: state 本节点累计状态, node 节点, mesh 模型几何, animation 修饰器, motion 动作, chain 链类别, time 时间, cache 本次采样缓存 // 入参: state 本节点累计状态, node 节点, mesh 模型几何, animation 修饰器, motion 动作, chain 链类别, time 时间, cache 本次采样缓存
// 返回: error 轨道、修饰顺序、预算或取消错误 // 返回: error 轨道、修饰顺序、预算或取消错误
func (r *u3dRender) sampleAnimation(state *u3dAnimationState, node *U3DNode, mesh *U3DMesh, animation *U3DAnimation, motion *U3DMotion, chain int, time float64, cache *u3dPlaybackCache) error { func (r *Renderer) sampleAnimation(state *u3dAnimationState, node *U3DNode, mesh *U3DMesh, animation *U3DAnimation, motion *U3DMotion, chain int, time float64, cache *u3dPlaybackCache) error {
if animation.Attributes&4 != 0 { if animation.Attributes&4 != 0 {
if chain == 0 { if chain == 0 {
return fmt.Errorf("unsupported U3D resource single-track animation") return fmt.Errorf("unsupported U3D resource single-track animation")
@@ -256,7 +256,7 @@ func (r *u3dRender) sampleAnimation(state *u3dAnimationState, node *U3DNode, mes
if state.weights != nil { if state.weights != nil {
return fmt.Errorf("unsupported U3D successive skeletal animations") return fmt.Errorf("unsupported U3D successive skeletal animations")
} }
if err := r.reserve(uint64(len(mesh.Bones)), 96); err != nil { if err := r.Reserve(uint64(len(mesh.Bones)), 96); err != nil {
return err return err
} }
defer func() { r.remaining += uint64(len(mesh.Bones)) * 96 }() defer func() { r.remaining += uint64(len(mesh.Bones)) * 96 }()
@@ -294,7 +294,7 @@ func (r *u3dRender) sampleAnimation(state *u3dAnimationState, node *U3DNode, mes
if err != nil { if err != nil {
return err return err
} }
if err := r.reserve(uint64(len(mesh.Bones)), 64); err != nil { if err := r.Reserve(uint64(len(mesh.Bones)), 64); err != nil {
return err return err
} }
state.bones = make([]U3DBoneState, 0, len(mesh.Bones)) state.bones = make([]U3DBoneState, 0, len(mesh.Bones))
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package ofdgo package threed
import ( import (
"fmt" "fmt"
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package ofdgo package threed
import ( import (
"context" "context"
@@ -80,8 +80,8 @@ type u3dFragment struct {
next int32 next int32
} }
// u3dRender 保存一次绘制的有限预算,不修改调用方场景 // Renderer 保存一次绘制的有限预算,不修改调用方场景
type u3dRender struct { type Renderer struct {
ctx context.Context ctx context.Context
model *U3DModel model *U3DModel
remaining uint64 remaining uint64
@@ -134,29 +134,29 @@ type u3dRender struct {
// 入参: ctx 取消上下文, model 场景, options 视图、输出与资源限制 // 入参: ctx 取消上下文, model 场景, options 视图、输出与资源限制
// 返回: *image.RGBA 图像, error 参数、能力、预算或取消错误 // 返回: *image.RGBA 图像, error 参数、能力、预算或取消错误
func RenderU3D(ctx context.Context, model *U3DModel, options U3DRenderOptions) (*image.RGBA, error) { func RenderU3D(ctx context.Context, model *U3DModel, options U3DRenderOptions) (*image.RGBA, error) {
r, err := newU3DRender(ctx, model, options) r, err := NewRenderer(ctx, model, options)
if err != nil { if err != nil {
return nil, err return nil, err
} }
passes, err := r.selectView(options) passes, err := r.SelectView(options)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return r.render(options, passes) return r.Render(options, passes)
} }
// newU3DRender 建立有预算限制的场景实例与资源索引 // NewRenderer 建立有预算限制的场景实例与资源索引
// 入参: ctx 取消上下文, model 场景, options 输出与资源限制, states 后续节点覆盖分组 // 入参: ctx 取消上下文, model 场景, options 输出与资源限制, states 后续节点覆盖分组
// 返回: *u3dRender 绘制状态, error 参数、场景、预算或取消错误 // 返回: *Renderer 绘制状态, error 参数、场景、预算或取消错误
func newU3DRender(ctx context.Context, model *U3DModel, options U3DRenderOptions, states ...[]U3DNodeState) (*u3dRender, error) { func NewRenderer(ctx context.Context, model *U3DModel, options U3DRenderOptions, states ...[]U3DNodeState) (*Renderer, error) {
if ctx == nil || model == nil || options.Width <= 0 || options.Height <= 0 || options.MaxMemoryBytes < 0 || options.MaxInstances < 0 || options.Instance < 0 || !finite(options.Time) { if ctx == nil || model == nil || options.Width <= 0 || options.Height <= 0 || options.MaxMemoryBytes < 0 || options.MaxInstances < 0 || options.Instance < 0 || !finite(options.Time) {
return nil, fmt.Errorf("invalid U3D render options") return nil, fmt.Errorf("invalid U3D render options")
} }
limit := options.MaxMemoryBytes limit := options.MaxMemoryBytes
if limit == 0 { if limit == 0 {
limit = u3dDefaultBytes limit = DefaultBytes
} }
r := &u3dRender{ctx: ctx, model: model, remaining: uint64(limit), opacity: -1, mode: options.Mode, creaseAngle: 45, resolveTexture: options.ResolveTexture} r := &Renderer{ctx: ctx, model: model, remaining: uint64(limit), opacity: -1, mode: options.Mode, creaseAngle: 45, resolveTexture: options.ResolveTexture}
if options.Mode > U3DRenderShadedIllustration { if options.Mode > U3DRenderShadedIllustration {
return nil, fmt.Errorf("unsupported U3D render mode") return nil, fmt.Errorf("unsupported U3D render mode")
} }
@@ -177,7 +177,7 @@ func newU3DRender(ctx context.Context, model *U3DModel, options U3DRenderOptions
return nil, fmt.Errorf("invalid U3D render dimensions") return nil, fmt.Errorf("invalid U3D render dimensions")
} }
pixels := uint64(options.Width) * uint64(options.Height) pixels := uint64(options.Width) * uint64(options.Height)
if err := r.reserve(pixels, 60); err != nil { if err := r.Reserve(pixels, 60); err != nil {
return nil, err return nil, err
} }
if err := r.resources(); err != nil { if err := r.resources(); err != nil {
@@ -207,19 +207,19 @@ func newU3DRender(ctx context.Context, model *U3DModel, options U3DRenderOptions
if err := r.poseNodes(); err != nil { if err := r.poseNodes(); err != nil {
return nil, err return nil, err
} }
if err := r.reserve(uint64(len(r.scene)), 1); err != nil { if err := r.Reserve(uint64(len(r.scene)), 1); err != nil {
return nil, err return nil, err
} }
r.members = make([]bool, len(r.scene)) r.members = make([]bool, len(r.scene))
return r, nil return r, nil
} }
// render 在已选择的相机和轮次下生成图像 // Render 在已选择的相机和轮次下生成图像
// 入参: options 输出与灯光背景, passes 绘制轮次 // 入参: options 输出与灯光背景, passes 绘制轮次
// 返回: *image.RGBA 图像, error 参数、绘制、预算或取消错误 // 返回: *image.RGBA 图像, error 参数、绘制、预算或取消错误
func (r *u3dRender) render(options U3DRenderOptions, passes []U3DViewPass) (*image.RGBA, error) { func (r *Renderer) Render(options U3DRenderOptions, passes []U3DViewPass) (*image.RGBA, error) {
if r.sections == nil { if r.sections == nil {
if err := r.prepareSections(options.Sections); err != nil { if err := r.PrepareSections(options.Sections); err != nil {
return nil, err return nil, err
} }
} }
@@ -292,10 +292,10 @@ func (r *u3dRender) render(options U3DRenderOptions, passes []U3DViewPass) (*ima
return r.output, nil return r.output, nil
} }
// reserve 在分配绘制缓存前扣减预算并检查取消状态 // Reserve 在分配绘制缓存前扣减预算并检查取消状态
// 入参: count 元素数量, size 元素预算 // 入参: count 元素数量, size 元素预算
// 返回: error 预算或取消错误 // 返回: error 预算或取消错误
func (r *u3dRender) reserve(count, size uint64) error { func (r *Renderer) Reserve(count, size uint64) error {
if err := r.ctx.Err(); err != nil { if err := r.ctx.Err(); err != nil {
return err return err
} }
@@ -306,10 +306,10 @@ func (r *u3dRender) reserve(count, size uint64) error {
return nil return nil
} }
// selectView 解析视图与相机覆盖,内部视口使用左上角坐标 // SelectView 解析视图与相机覆盖,内部视口使用左上角坐标
// 入参: options 绘制选项 // 入参: options 绘制选项
// 返回: []U3DViewPass 绘制轮次, error 视图或投影错误 // 返回: []U3DViewPass 绘制轮次, error 视图或投影错误
func (r *u3dRender) selectView(options U3DRenderOptions) ([]U3DViewPass, error) { func (r *Renderer) SelectView(options U3DRenderOptions) ([]U3DViewPass, error) {
r.view = nil r.view = nil
r.viewport = [4]float64{0, 0, float64(options.Width), float64(options.Height)} r.viewport = [4]float64{0, 0, float64(options.Width), float64(options.Height)}
selected := -1 selected := -1
@@ -413,9 +413,9 @@ func (r *u3dRender) selectView(options U3DRenderOptions) ([]U3DViewPass, error)
// resources 建立只读资源索引,空名称保留给标准默认资源 // resources 建立只读资源索引,空名称保留给标准默认资源
// 返回: error 名称、预算或取消错误 // 返回: error 名称、预算或取消错误
func (r *u3dRender) resources() error { func (r *Renderer) resources() error {
count := uint64(len(r.model.Meshes)) + uint64(len(r.model.Shaders)) + uint64(len(r.model.Materials)) + uint64(len(r.model.Lights)) + uint64(len(r.model.Textures)) + uint64(len(r.model.Views)) + uint64(len(r.model.Motions)) count := uint64(len(r.model.Meshes)) + uint64(len(r.model.Shaders)) + uint64(len(r.model.Materials)) + uint64(len(r.model.Lights)) + uint64(len(r.model.Textures)) + uint64(len(r.model.Views)) + uint64(len(r.model.Motions))
if err := r.reserve(count, 96); err != nil { if err := r.Reserve(count, 96); err != nil {
return err return err
} }
r.meshes = make(map[string]int, len(r.model.Meshes)) r.meshes = make(map[string]int, len(r.model.Meshes))
@@ -481,7 +481,7 @@ func (r *u3dRender) resources() error {
// renderPass 分别解析遮挡与混合,绘制轮次之间保留颜色并重置深度 // renderPass 分别解析遮挡与混合,绘制轮次之间保留颜色并重置深度
// 入参: pass 轮次参数, passIndex 轮次索引, last 是否为最后一轮 // 入参: pass 轮次参数, passIndex 轮次索引, last 是否为最后一轮
// 返回: error 场景、预算或取消错误 // 返回: error 场景、预算或取消错误
func (r *u3dRender) renderPass(pass U3DViewPass, passIndex int, last bool) error { func (r *Renderer) renderPass(pass U3DViewPass, passIndex int, last bool) error {
if pass.Attributes > 1 || pass.FogMode > 2 { if pass.Attributes > 1 || pass.FogMode > 2 {
return fmt.Errorf("invalid U3D render pass") return fmt.Errorf("invalid U3D render pass")
} }
@@ -540,7 +540,7 @@ func (r *u3dRender) renderPass(pass U3DViewPass, passIndex int, last bool) error
// drawScene 绘制当前轮次中的可见模型,不重置深度及片元缓存 // drawScene 绘制当前轮次中的可见模型,不重置深度及片元缓存
// 入参: pass 绘制轮次, passIndex 轮次索引 // 入参: pass 绘制轮次, passIndex 轮次索引
// 返回: error 场景、资源或取消错误 // 返回: error 场景、资源或取消错误
func (r *u3dRender) drawScene(pass U3DViewPass, passIndex int) error { func (r *Renderer) drawScene(pass U3DViewPass, passIndex int) error {
for i, instance := range r.scene { for i, instance := range r.scene {
if err := r.ctx.Err(); err != nil { if err := r.ctx.Err(); err != nil {
return err return err
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package ofdgo package threed
import ( import (
"fmt" "fmt"
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package ofdgo package threed
import ( import (
"context" "context"
@@ -183,7 +183,7 @@ func (frame U3DKeyFrame) Matrix() ([16]float64, error) {
// trackMatrix 按资源和轨道名称采样节点变换,不推测未命名轨道的绑定关系 // trackMatrix 按资源和轨道名称采样节点变换,不推测未命名轨道的绑定关系
// 入参: state 动作轨道状态 // 入参: state 动作轨道状态
// 返回: [16]float64 节点变换, error 引用、数值或取消错误 // 返回: [16]float64 节点变换, error 引用、数值或取消错误
func (r *u3dRender) trackMatrix(state U3DTrackState) ([16]float64, error) { func (r *Renderer) trackMatrix(state U3DTrackState) ([16]float64, error) {
var track *U3DMotionTrack var track *U3DMotionTrack
if err := r.ctx.Err(); err != nil { if err := r.ctx.Err(); err != nil {
return [16]float64{}, err return [16]float64{}, err
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package ofdgo package threed
import ( import (
"fmt" "fmt"
@@ -176,9 +176,9 @@ func u3dTriangleNormal(points [3]u3dVector) u3dVector {
// sceneGraph 校验父子关系并确定拓扑顺序,只有默认根的后代接入场景 // sceneGraph 校验父子关系并确定拓扑顺序,只有默认根的后代接入场景
// 返回: []int 拓扑顺序, map[string]int 节点索引, error 结构、预算或取消错误 // 返回: []int 拓扑顺序, map[string]int 节点索引, error 结构、预算或取消错误
func (r *u3dRender) sceneGraph() ([]int, map[string]int, error) { func (r *Renderer) sceneGraph() ([]int, map[string]int, error) {
nodes := r.model.Nodes nodes := r.model.Nodes
if err := r.reserve(uint64(len(nodes)), 161); err != nil { if err := r.Reserve(uint64(len(nodes)), 161); err != nil {
return nil, nil, err return nil, nil, err
} }
index := make(map[string]int, len(nodes)) index := make(map[string]int, len(nodes))
@@ -203,7 +203,7 @@ func (r *u3dRender) sceneGraph() ([]int, map[string]int, error) {
r.byNode = make([][]int, len(nodes)) r.byNode = make([][]int, len(nodes))
r.connected = make([]bool, len(nodes)) r.connected = make([]bool, len(nodes))
for i, node := range nodes { for i, node := range nodes {
if err := r.reserve(uint64(len(node.Parents)), 16); err != nil { if err := r.Reserve(uint64(len(node.Parents)), 16); err != nil {
return nil, nil, err return nil, nil, err
} }
for k, parent := range node.Parents { for k, parent := range node.Parents {
@@ -270,7 +270,7 @@ func (r *u3dRender) sceneGraph() ([]int, map[string]int, error) {
// instances 按拓扑顺序展开接入场景的父路径,不将未解析的父节点视为默认根 // instances 按拓扑顺序展开接入场景的父路径,不将未解析的父节点视为默认根
// 入参: order 拓扑顺序, index 节点索引, limit 最大实例数 // 入参: order 拓扑顺序, index 节点索引, limit 最大实例数
// 返回: error 变换、预算、实例上限或取消错误 // 返回: error 变换、预算、实例上限或取消错误
func (r *u3dRender) instances(order []int, index map[string]int, limit int) error { func (r *Renderer) instances(order []int, index map[string]int, limit int) error {
nodes := r.model.Nodes nodes := r.model.Nodes
for _, i := range order { for _, i := range order {
if err := r.ctx.Err(); err != nil { if err := r.ctx.Err(); err != nil {
@@ -311,7 +311,7 @@ func (r *u3dRender) instances(order []int, index map[string]int, limit int) erro
if len(r.scene) >= limit { if len(r.scene) >= limit {
return fmt.Errorf("U3D instance count exceeds limit") return fmt.Errorf("U3D instance count exceeds limit")
} }
if err := r.reserve(1, 384); err != nil { if err := r.Reserve(1, 384); err != nil {
return err return err
} }
world := local world := local
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package ofdgo package threed
import ( import (
"fmt" "fmt"
@@ -37,17 +37,17 @@ type u3dSection struct {
opacity float64 opacity float64
} }
// prepareSections 准备剖切缓存,不复制网格或修改调用方参数 // PrepareSections 准备剖切缓存,不复制网格或修改调用方参数
// 入参: sections 世界坐标剖切面 // 入参: sections 世界坐标剖切面
// 返回: error 参数、场景、预算或取消错误 // 返回: error 参数、场景、预算或取消错误
func (r *u3dRender) prepareSections(sections []U3DCrossSection) error { func (r *Renderer) PrepareSections(sections []U3DCrossSection) error {
if len(sections) == 0 { if len(sections) == 0 {
return nil return nil
} }
if err := r.reserve(uint64(len(sections)), 1152); err != nil { if err := r.Reserve(uint64(len(sections)), 1152); err != nil {
return err return err
} }
if err := r.reserve(uint64(len(sections))+3, 512); err != nil { if err := r.Reserve(uint64(len(sections))+3, 512); err != nil {
return err return err
} }
r.sections = make([]u3dSection, len(sections)) r.sections = make([]u3dSection, len(sections))
@@ -132,7 +132,7 @@ func (r *u3dRender) prepareSections(sections []U3DCrossSection) error {
// sceneDiagonal 计算静态场景世界包围盒的对角线,包含隐藏节点的几何 // sceneDiagonal 计算静态场景世界包围盒的对角线,包含隐藏节点的几何
// 返回: float64 对角线长度, error 几何、数值或取消错误 // 返回: float64 对角线长度, error 几何、数值或取消错误
func (r *u3dRender) sceneDiagonal() (float64, error) { func (r *Renderer) sceneDiagonal() (float64, error) {
minimum := u3dVector{math.Inf(1), math.Inf(1), math.Inf(1)} minimum := u3dVector{math.Inf(1), math.Inf(1), math.Inf(1)}
maximum := u3dVector{math.Inf(-1), math.Inf(-1), math.Inf(-1)} maximum := u3dVector{math.Inf(-1), math.Inf(-1), math.Inf(-1)}
for _, instance := range r.scene { for _, instance := range r.scene {
@@ -180,7 +180,7 @@ func (r *u3dRender) sceneDiagonal() (float64, error) {
// sectionTriangle 用复用的凸多边形缓存应用全部剖切面,再进行视锥裁剪 // sectionTriangle 用复用的凸多边形缓存应用全部剖切面,再进行视锥裁剪
// 入参: triangle 三角面, shader 着色器, pass 绘制轮次 // 入参: triangle 三角面, shader 着色器, pass 绘制轮次
// 返回: error 数值、资源或取消错误 // 返回: error 数值、资源或取消错误
func (r *u3dRender) sectionTriangle(triangle [3]u3dVertex, shader U3DShader, pass U3DViewPass) error { func (r *Renderer) sectionTriangle(triangle [3]u3dVertex, shader U3DShader, pass U3DViewPass) error {
if len(r.sections) == 0 { if len(r.sections) == 0 {
return r.clipTriangle(triangle, shader, pass) return r.clipTriangle(triangle, shader, pass)
} }
@@ -264,7 +264,7 @@ func u3dAppendSectionVertex(buffer []u3dVertex, count int, vertex u3dVertex) (in
// drawSectionPlanes 在最后一轮绘制平面方形,不把平面本身当作待剖切几何 // drawSectionPlanes 在最后一轮绘制平面方形,不把平面本身当作待剖切几何
// 返回: error 数值、资源或取消错误 // 返回: error 数值、资源或取消错误
func (r *u3dRender) drawSectionPlanes() error { func (r *Renderer) drawSectionPlanes() error {
for _, section := range r.sections { for _, section := range r.sections {
if err := r.ctx.Err(); err != nil { if err := r.ctx.Err(); err != nil {
return err return err
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package ofdgo package threed
import ( import (
"fmt" "fmt"
@@ -48,7 +48,7 @@ type u3dTextureSet struct {
// shaderTextures 校验并缓存启用的纹理,未使用的着色器不会触发图像解码 // shaderTextures 校验并缓存启用的纹理,未使用的着色器不会触发图像解码
// 入参: shader 着色器 // 入参: shader 着色器
// 返回: *u3dTextureSet 纹理组合,空值表示无纹理, error 参数、能力、预算或取消错误 // 返回: *u3dTextureSet 纹理组合,空值表示无纹理, error 参数、能力、预算或取消错误
func (r *u3dRender) shaderTextures(shader U3DShader) (*u3dTextureSet, error) { func (r *Renderer) shaderTextures(shader U3DShader) (*u3dTextureSet, error) {
if shader.Textures == ([8]*U3DTextureLayer{}) { if shader.Textures == ([8]*U3DTextureLayer{}) {
return nil, nil return nil, nil
} }
@@ -58,7 +58,7 @@ func (r *u3dRender) shaderTextures(shader U3DShader) (*u3dTextureSet, error) {
if cached := r.textureSets[shader.Textures]; cached != nil { if cached := r.textureSets[shader.Textures]; cached != nil {
return cached, nil return cached, nil
} }
if err := r.reserve(1, 512); err != nil { if err := r.Reserve(1, 512); err != nil {
return nil, err return nil, err
} }
set := &u3dTextureSet{layers: shader.Textures} set := &u3dTextureSet{layers: shader.Textures}
@@ -77,7 +77,7 @@ func (r *u3dRender) shaderTextures(shader U3DShader) (*u3dTextureSet, error) {
} }
} }
if layer.Mode >= 1 && layer.Mode <= 3 { if layer.Mode >= 1 && layer.Mode <= 3 {
if err := r.reserve(1, 256); err != nil { if err := r.Reserve(1, 256); err != nil {
return nil, err return nil, err
} }
inverse, err := u3dTextureInverse(layer.WrapTransform) inverse, err := u3dTextureInverse(layer.WrapTransform)
@@ -175,7 +175,7 @@ func (s *u3dTextureSet) coordinates(vertex *u3dVertex, mesh *U3DMesh, shading U3
// textureColor 合成纹理后恢复节点指定的透明度,再叠加视图透明度 // textureColor 合成纹理后恢复节点指定的透明度,再叠加视图透明度
// 入参: textures 纹理组合, color 顶点插值颜色, coordinates 纹理输入, position 相机空间位置, override 是否指定节点透明度 // 入参: textures 纹理组合, color 顶点插值颜色, coordinates 纹理输入, position 相机空间位置, override 是否指定节点透明度
// 返回: error 纹理映射或混合错误 // 返回: error 纹理映射或混合错误
func (r *u3dRender) textureColor(textures *u3dTextureSet, color *[4]float64, coordinates *[8][3]float64, position u3dVector, override bool) error { func (r *Renderer) textureColor(textures *u3dTextureSet, color *[4]float64, coordinates *[8][3]float64, position u3dVector, override bool) error {
if textures == nil { if textures == nil {
return nil return nil
} }
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package ofdgo package threed
import ( import (
"fmt" "fmt"
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package ofdgo package threed
import ( import (
"fmt" "fmt"
@@ -44,7 +44,7 @@ type u3dSkinPose struct {
// nodeMesh 返回节点本次绘制使用的几何,未变形节点直接引用原资源 // nodeMesh 返回节点本次绘制使用的几何,未变形节点直接引用原资源
// 入参: node 模型节点 // 入参: node 模型节点
// 返回: *U3DMesh 几何,资源缺失时为空 // 返回: *U3DMesh 几何,资源缺失时为空
func (r *u3dRender) nodeMesh(node *U3DNode) *U3DMesh { func (r *Renderer) nodeMesh(node *U3DNode) *U3DMesh {
if mesh := r.posed[node.Name]; mesh != nil { if mesh := r.posed[node.Name]; mesh != nil {
return mesh return mesh
} }
@@ -56,7 +56,7 @@ func (r *u3dRender) nodeMesh(node *U3DNode) *U3DMesh {
// poseNodes 应用动画和显式骨骼姿态,同源同姿态的节点及父路径共用变形几何 // poseNodes 应用动画和显式骨骼姿态,同源同姿态的节点及父路径共用变形几何
// 返回: error 骨架、权重、预算或取消错误 // 返回: error 骨架、权重、预算或取消错误
func (r *u3dRender) poseNodes() error { func (r *Renderer) poseNodes() error {
if len(r.states) == 0 && len(r.animated) == 0 { if len(r.states) == 0 && len(r.animated) == 0 {
return r.ctx.Err() return r.ctx.Err()
} }
@@ -81,12 +81,12 @@ func (r *u3dRender) poseNodes() error {
return fmt.Errorf("missing U3D bone state geometry for %q", node.Name) return fmt.Errorf("missing U3D bone state geometry for %q", node.Name)
} }
if r.posed == nil { if r.posed == nil {
if err := r.reserve(1, 1024); err != nil { if err := r.Reserve(1, 1024); err != nil {
return err return err
} }
r.posed = make(map[string]*U3DMesh) r.posed = make(map[string]*U3DMesh)
} }
if err := r.reserve(1, 128); err != nil { if err := r.Reserve(1, 128); err != nil {
return err return err
} }
var temporary uint64 var temporary uint64
@@ -95,7 +95,7 @@ func (r *u3dRender) poseNodes() error {
states = animation.bones states = animation.bones
} else { } else {
count := uint64(len(animation.bones)) + uint64(len(states)) count := uint64(len(animation.bones)) + uint64(len(states))
if err := r.reserve(count, 64); err != nil { if err := r.Reserve(count, 64); err != nil {
return err return err
} }
temporary = count * 64 temporary = count * 64
@@ -148,19 +148,19 @@ func (r *u3dRender) poseNodes() error {
r.posed[node.Name] = posed r.posed[node.Name] = posed
if len(r.model.Nodes) > 1 { if len(r.model.Nodes) > 1 {
if cache == nil { if cache == nil {
if err := r.reserve(1, 1024); err != nil { if err := r.Reserve(1, 1024); err != nil {
return err return err
} }
cacheBytes += 1024 cacheBytes += 1024
cache = make(map[*U3DMesh]u3dSkinPose) cache = make(map[*U3DMesh]u3dSkinPose)
} }
if !exists { if !exists {
if err := r.reserve(1, 128); err != nil { if err := r.Reserve(1, 128); err != nil {
return err return err
} }
cacheBytes += 128 cacheBytes += 128
} }
if err := r.reserve(uint64(len(states)), 64); err != nil { if err := r.Reserve(uint64(len(states)), 64); err != nil {
return err return err
} }
cacheBytes += uint64(len(states)) * 64 cacheBytes += uint64(len(states)) * 64
@@ -179,7 +179,7 @@ func (r *u3dRender) poseNodes() error {
// boneTransforms 连接骨骼局部姿态,以初始姿态的逆变换将作者坐标映射到当前姿态 // boneTransforms 连接骨骼局部姿态,以初始姿态的逆变换将作者坐标映射到当前姿态
// 入参: bones 按父项在前排列的骨架, states 显式姿态 // 入参: bones 按父项在前排列的骨架, states 显式姿态
// 返回: []u3dSkinBone 变形矩阵, error 骨架、姿态或取消错误 // 返回: []u3dSkinBone 变形矩阵, error 骨架、姿态或取消错误
func (r *u3dRender) boneTransforms(bones []U3DBone, states []U3DBoneState) ([]u3dSkinBone, error) { func (r *Renderer) boneTransforms(bones []U3DBone, states []U3DBoneState) ([]u3dSkinBone, error) {
index := make(map[string]int, len(bones)) index := make(map[string]int, len(bones))
for i, bone := range bones { for i, bone := range bones {
if i%256 == 0 { if i%256 == 0 {
@@ -260,14 +260,14 @@ func (r *u3dRender) boneTransforms(bones []U3DBone, states []U3DBoneState) ([]u3
// skinWeights 选取与几何类型相符的末项权重,节点链覆盖资源链 // skinWeights 选取与几何类型相符的末项权重,节点链覆盖资源链
// 入参: mesh 原始几何, sets 节点权重 // 入参: mesh 原始几何, sets 节点权重
// 返回: *U3DBoneWeights 有效权重, error 类型、引用或取消错误 // 返回: *U3DBoneWeights 有效权重, error 类型、引用或取消错误
func (r *u3dRender) skinWeights(mesh *U3DMesh, sets []U3DBoneWeights) (*U3DBoneWeights, error) { func (r *Renderer) skinWeights(mesh *U3DMesh, sets []U3DBoneWeights) (*U3DBoneWeights, error) {
return r.skinWeightSet(mesh, sets, math.MaxUint64, math.MaxUint64) return r.skinWeightSet(mesh, sets, math.MaxUint64, math.MaxUint64)
} }
// skinWeightSource 按修饰链截止位置选取权重,不使用动画后面的权重输出 // skinWeightSource 按修饰链截止位置选取权重,不使用动画后面的权重输出
// 入参: mesh 原始几何, sets 节点权重, resourceEnd、nodeEnd 两条链中首个不参与的索引 // 入参: mesh 原始几何, sets 节点权重, resourceEnd、nodeEnd 两条链中首个不参与的索引
// 返回: *U3DBoneWeights 权重来源,内容尚未校验, error 类型、引用或取消错误 // 返回: *U3DBoneWeights 权重来源,内容尚未校验, error 类型、引用或取消错误
func (r *u3dRender) skinWeightSource(mesh *U3DMesh, sets []U3DBoneWeights, resourceEnd, nodeEnd uint64) (*U3DBoneWeights, error) { func (r *Renderer) skinWeightSource(mesh *U3DMesh, sets []U3DBoneWeights, resourceEnd, nodeEnd uint64) (*U3DBoneWeights, error) {
kind := uint32(0) kind := uint32(0)
for i, count := range []int{len(mesh.Faces), len(mesh.Lines), len(mesh.Points)} { for i, count := range []int{len(mesh.Faces), len(mesh.Lines), len(mesh.Points)} {
if count != 0 { if count != 0 {
@@ -307,7 +307,7 @@ func (r *u3dRender) skinWeightSource(mesh *U3DMesh, sets []U3DBoneWeights, resou
// skinWeightSet 选取并校验指定修饰链位置之前的权重 // skinWeightSet 选取并校验指定修饰链位置之前的权重
// 入参: mesh 原始几何, sets 节点权重, resourceEnd、nodeEnd 两条链中首个不参与的索引 // 入参: mesh 原始几何, sets 节点权重, resourceEnd、nodeEnd 两条链中首个不参与的索引
// 返回: *U3DBoneWeights 有效权重, error 类型、引用、数值或取消错误 // 返回: *U3DBoneWeights 有效权重, error 类型、引用、数值或取消错误
func (r *u3dRender) skinWeightSet(mesh *U3DMesh, sets []U3DBoneWeights, resourceEnd, nodeEnd uint64) (*U3DBoneWeights, error) { func (r *Renderer) skinWeightSet(mesh *U3DMesh, sets []U3DBoneWeights, resourceEnd, nodeEnd uint64) (*U3DBoneWeights, error) {
selected, err := r.skinWeightSource(mesh, sets, resourceEnd, nodeEnd) selected, err := r.skinWeightSource(mesh, sets, resourceEnd, nodeEnd)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -355,7 +355,7 @@ func (weights *U3DBoneWeights) influence(position uint32) []U3DBoneWeight {
// skinVector 对位置或法线应用加权骨骼变换,法线在累加后统一归一化 // skinVector 对位置或法线应用加权骨骼变换,法线在累加后统一归一化
// 入参: value 原向量, weights 位置权重, bones 骨骼变换, normal 是否处理法线 // 入参: value 原向量, weights 位置权重, bones 骨骼变换, normal 是否处理法线
// 返回: [3]float32 变形结果, error 数值、奇异变换或取消错误 // 返回: [3]float32 变形结果, error 数值、奇异变换或取消错误
func (r *u3dRender) skinVector(value [3]float32, weights []U3DBoneWeight, bones []u3dSkinBone, normal bool) ([3]float32, error) { func (r *Renderer) skinVector(value [3]float32, weights []U3DBoneWeight, bones []u3dSkinBone, normal bool) ([3]float32, error) {
input := u3dVector{float64(value[0]), float64(value[1]), float64(value[2])} input := u3dVector{float64(value[0]), float64(value[1]), float64(value[2])}
for _, component := range input { for _, component := range input {
if !finite(component) { if !finite(component) {
@@ -408,15 +408,15 @@ func (r *u3dRender) skinVector(value [3]float32, weights []U3DBoneWeight, bones
// skinMesh 复制受变形影响的属性,按位置与法线的组合保留独立法线索引 // skinMesh 复制受变形影响的属性,按位置与法线的组合保留独立法线索引
// 入参: mesh 原始几何, sets 节点权重, states 显式骨骼姿态 // 入参: mesh 原始几何, sets 节点权重, states 显式骨骼姿态
// 返回: *U3DMesh 独立姿态几何, error 结构、能力、预算或取消错误 // 返回: *U3DMesh 独立姿态几何, error 结构、能力、预算或取消错误
func (r *u3dRender) skinMesh(mesh *U3DMesh, sets []U3DBoneWeights, states []U3DBoneState) (*U3DMesh, error) { func (r *Renderer) skinMesh(mesh *U3DMesh, sets []U3DBoneWeights, states []U3DBoneState) (*U3DMesh, error) {
weights, err := r.skinWeights(mesh, sets) weights, err := r.skinWeights(mesh, sets)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if err := r.reserve(uint64(len(mesh.Bones)), 768); err != nil { if err := r.Reserve(uint64(len(mesh.Bones)), 768); err != nil {
return nil, err return nil, err
} }
if err := r.reserve(uint64(len(states)), 128); err != nil { if err := r.Reserve(uint64(len(states)), 128); err != nil {
r.remaining += uint64(len(mesh.Bones)) * 768 r.remaining += uint64(len(mesh.Bones)) * 768
return nil, err return nil, err
} }
@@ -426,10 +426,10 @@ func (r *u3dRender) skinMesh(mesh *U3DMesh, sets []U3DBoneWeights, states []U3DB
if err != nil { if err != nil {
return nil, err return nil, err
} }
if err := r.reserve(1, 768); err != nil { if err := r.Reserve(1, 768); err != nil {
return nil, err return nil, err
} }
if err := r.reserve(uint64(len(mesh.Positions)), 12); err != nil { if err := r.Reserve(uint64(len(mesh.Positions)), 12); err != nil {
return nil, err return nil, err
} }
result := *mesh result := *mesh
@@ -449,12 +449,12 @@ func (r *u3dRender) skinMesh(mesh *U3DMesh, sets []U3DBoneWeights, states []U3DB
return &result, r.ctx.Err() return &result, r.ctx.Err()
} }
bytes := uint64(len(mesh.Faces))*148 + uint64(len(mesh.Lines))*100 + uint64(len(mesh.Points))*52 bytes := uint64(len(mesh.Faces))*148 + uint64(len(mesh.Lines))*100 + uint64(len(mesh.Points))*52
if err := r.reserve(bytes, 1); err != nil { if err := r.Reserve(bytes, 1); err != nil {
return nil, err return nil, err
} }
result.Faces, result.Lines, result.Points = slices.Clone(mesh.Faces), slices.Clone(mesh.Lines), slices.Clone(mesh.Points) result.Faces, result.Lines, result.Points = slices.Clone(mesh.Faces), slices.Clone(mesh.Lines), slices.Clone(mesh.Points)
result.Normals = nil result.Normals = nil
if err := r.reserve(1, 1024); err != nil { if err := r.Reserve(1, 1024); err != nil {
return nil, err return nil, err
} }
index := make(map[[2]uint32]uint32) index := make(map[[2]uint32]uint32)
@@ -474,7 +474,7 @@ func (r *u3dRender) skinMesh(mesh *U3DMesh, sets []U3DBoneWeights, states []U3DB
key := [2]uint32{corner.Position, corner.Normal} key := [2]uint32{corner.Position, corner.Normal}
normal, exists := index[key] normal, exists := index[key]
if !exists { if !exists {
if err := r.reserve(1, 96); err != nil { if err := r.Reserve(1, 96); err != nil {
return nil, err return nil, err
} }
if len(result.Normals) == cap(result.Normals) { if len(result.Normals) == cap(result.Normals) {
@@ -482,7 +482,7 @@ func (r *u3dRender) skinMesh(mesh *U3DMesh, sets []U3DBoneWeights, states []U3DB
if capacity > math.MaxUint32 { if capacity > math.MaxUint32 {
return nil, fmt.Errorf("U3D skin normal count exceeds limit") return nil, fmt.Errorf("U3D skin normal count exceeds limit")
} }
if err := r.reserve(capacity, 12); err != nil { if err := r.Reserve(capacity, 12); err != nil {
return nil, err return nil, err
} }
grown := make([][3]float32, len(result.Normals), int(capacity)) grown := make([][3]float32, len(result.Normals), int(capacity))
@@ -12,9 +12,11 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package ofdgo package threed
import "fmt" import (
"fmt"
)
// U3DNodeState 按名称覆盖节点,未指定透明度或可见性时沿父路径继承 // U3DNodeState 按名称覆盖节点,未指定透明度或可见性时沿父路径继承
// Opacity范围为0到1,采用标准透明混合;Matrix为相对父节点的列式仿射矩阵 // Opacity范围为0到1,采用标准透明混合;Matrix为相对父节点的列式仿射矩阵
@@ -34,7 +36,7 @@ type U3DNodeState struct {
// nodeStates 建立有预算限制的覆盖索引,重复名称整体替换而不合并字段 // nodeStates 建立有预算限制的覆盖索引,重复名称整体替换而不合并字段
// 入参: groups 按优先级递增排列的节点覆盖分组 // 入参: groups 按优先级递增排列的节点覆盖分组
// 返回: error 数值、预算或取消错误 // 返回: error 数值、预算或取消错误
func (r *u3dRender) nodeStates(groups ...[]U3DNodeState) error { func (r *Renderer) nodeStates(groups ...[]U3DNodeState) error {
var count uint64 var count uint64
for _, states := range groups { for _, states := range groups {
if uint64(len(states)) > ^uint64(0)-count { if uint64(len(states)) > ^uint64(0)-count {
@@ -45,7 +47,7 @@ func (r *u3dRender) nodeStates(groups ...[]U3DNodeState) error {
if count == 0 { if count == 0 {
return nil return nil
} }
if err := r.reserve(count, 160); err != nil { if err := r.Reserve(count, 160); err != nil {
return err return err
} }
r.states = make(map[string]U3DNodeState, int(count)) r.states = make(map[string]U3DNodeState, int(count))
@@ -80,14 +82,14 @@ func (r *u3dRender) nodeStates(groups ...[]U3DNodeState) error {
return fmt.Errorf("invalid U3D node motion state") return fmt.Errorf("invalid U3D node motion state")
} }
if samples == nil { if samples == nil {
if err := r.reserve(1, 1024); err != nil { if err := r.Reserve(1, 1024); err != nil {
return err return err
} }
samples = make(map[U3DTrackState]*[16]float64) samples = make(map[U3DTrackState]*[16]float64)
} }
matrix := samples[*state.Motion] matrix := samples[*state.Motion]
if matrix == nil { if matrix == nil {
if err := r.reserve(1, 320); err != nil { if err := r.Reserve(1, 320); err != nil {
return err return err
} }
value, err := r.trackMatrix(*state.Motion) value, err := r.trackMatrix(*state.Motion)
@@ -12,9 +12,11 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package ofdgo package threed
import "fmt" import (
"fmt"
)
// U3DTexture 保存纹理尺寸、图像类型及各通道的图像数据 // U3DTexture 保存纹理尺寸、图像类型及各通道的图像数据
type U3DTexture struct { type U3DTexture struct {
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package ofdgo package threed
import ( import (
"bytes" "bytes"
@@ -64,19 +64,19 @@ func (r *u3dImageReader) ReadAt(p []byte, offset int64) (int, error) {
// texture 按需合成纹理并缓存,空名称使用ECMA-363第8.4.1节规定的默认纹理 // texture 按需合成纹理并缓存,空名称使用ECMA-363第8.4.1节规定的默认纹理
// 入参: name 纹理名称 // 入参: name 纹理名称
// 返回: *image.NRGBA64 非预乘纹理, error 资源、解码、预算或取消错误 // 返回: *image.NRGBA64 非预乘纹理, error 资源、解码、预算或取消错误
func (r *u3dRender) texture(name string) (*image.NRGBA64, error) { func (r *Renderer) texture(name string) (*image.NRGBA64, error) {
if err := r.ctx.Err(); err != nil { if err := r.ctx.Err(); err != nil {
return nil, err return nil, err
} }
if cached := r.textures[name]; cached != nil { if cached := r.textures[name]; cached != nil {
return cached, nil return cached, nil
} }
if err := r.reserve(1, 256); err != nil { if err := r.Reserve(1, 256); err != nil {
return nil, err return nil, err
} }
var pixels *image.NRGBA64 var pixels *image.NRGBA64
if name == "" { if name == "" {
if err := r.reserve(64, 8); err != nil { if err := r.Reserve(64, 8); err != nil {
return nil, err return nil, err
} }
pixels = image.NewNRGBA64(image.Rect(0, 0, 8, 8)) pixels = image.NewNRGBA64(image.Rect(0, 0, 8, 8))
@@ -110,7 +110,7 @@ func (r *u3dRender) texture(name string) (*image.NRGBA64, error) {
// decodeTexture 合成各续图像指定的通道,保留十六位颜色与非预乘透明度 // decodeTexture 合成各续图像指定的通道,保留十六位颜色与非预乘透明度
// 入参: texture 纹理资源 // 入参: texture 纹理资源
// 返回: *image.NRGBA64 合成图像, error 格式、预算或取消错误 // 返回: *image.NRGBA64 合成图像, error 格式、预算或取消错误
func (r *u3dRender) decodeTexture(texture *U3DTexture) (*image.NRGBA64, error) { func (r *Renderer) decodeTexture(texture *U3DTexture) (*image.NRGBA64, error) {
switch texture.ImageType { switch texture.ImageType {
case 1, 14, 15, 16, 17: case 1, 14, 15, 16, 17:
default: default:
@@ -139,7 +139,7 @@ func (r *u3dRender) decodeTexture(texture *U3DTexture) (*image.NRGBA64, error) {
if covered != u3dTextureChannels(texture.ImageType) { if covered != u3dTextureChannels(texture.ImageType) {
return nil, fmt.Errorf("incomplete image channels") return nil, fmt.Errorf("incomplete image channels")
} }
if err := r.reserve(uint64(texture.Width)*uint64(texture.Height), 8); err != nil { if err := r.Reserve(uint64(texture.Width)*uint64(texture.Height), 8); err != nil {
return nil, err return nil, err
} }
pixels := image.NewNRGBA64(image.Rect(0, 0, int(texture.Width), int(texture.Height))) pixels := image.NewNRGBA64(image.Rect(0, 0, int(texture.Width), int(texture.Height)))
@@ -167,7 +167,7 @@ func (r *u3dRender) decodeTexture(texture *U3DTexture) (*image.NRGBA64, error) {
// textureSource 读取内置通道或按顺序尝试外部地址,不修改模型中的图像声明 // textureSource 读取内置通道或按顺序尝试外部地址,不修改模型中的图像声明
// 入参: pixels 目标纹理, source 通道图像 // 入参: pixels 目标纹理, source 通道图像
// 返回: error 读取、解码、预算或取消错误 // 返回: error 读取、解码、预算或取消错误
func (r *u3dRender) textureSource(pixels *image.NRGBA64, source U3DTextureImage) error { func (r *Renderer) textureSource(pixels *image.NRGBA64, source U3DTextureImage) error {
if !source.External { if !source.External {
return r.textureChannels(pixels, source) return r.textureChannels(pixels, source)
} }
@@ -202,7 +202,7 @@ func (r *u3dRender) textureSource(pixels *image.NRGBA64, source U3DTextureImage)
// textureChannels 解码并合成一幅续图像,临时预算包括像素、分块及压缩数据开销 // textureChannels 解码并合成一幅续图像,临时预算包括像素、分块及压缩数据开销
// 入参: pixels 目标纹理, source 通道图像 // 入参: pixels 目标纹理, source 通道图像
// 返回: error 格式、预算或取消错误 // 返回: error 格式、预算或取消错误
func (r *u3dRender) textureChannels(pixels *image.NRGBA64, source U3DTextureImage) error { func (r *Renderer) textureChannels(pixels *image.NRGBA64, source U3DTextureImage) error {
var config func(io.Reader) (image.Config, error) var config func(io.Reader) (image.Config, error)
var decode func(io.Reader) (image.Image, error) var decode func(io.Reader) (image.Image, error)
switch source.Compression { switch source.Compression {
@@ -217,10 +217,10 @@ func (r *u3dRender) textureChannels(pixels *image.NRGBA64, source U3DTextureImag
} }
budget := r.remaining budget := r.remaining
defer func() { r.remaining = budget }() defer func() { r.remaining = budget }()
if err := r.reserve(uint64(len(source.Data)), 4); err != nil { if err := r.Reserve(uint64(len(source.Data)), 4); err != nil {
return err return err
} }
if err := r.reserve(1, 1<<20); err != nil { if err := r.Reserve(1, 1<<20); err != nil {
return err return err
} }
reader := &u3dImageReader{ctx: r.ctx, data: bytes.NewReader(source.Data)} reader := &u3dImageReader{ctx: r.ctx, data: bytes.NewReader(source.Data)}
@@ -238,7 +238,7 @@ func (r *u3dRender) textureChannels(pixels *image.NRGBA64, source U3DTextureImag
if source.Compression == 4 { if source.Compression == 4 {
w, h = max(w, 1024), max(h, 1024) w, h = max(w, 1024), max(h, 1024)
} }
if err := r.reserve(w*h, 32); err != nil { if err := r.Reserve(w*h, 32); err != nil {
return err return err
} }
reader.data.Reset(source.Data) reader.data.Reset(source.Data)
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package ofdgo package threed
import ( import (
"fmt" "fmt"
@@ -32,7 +32,7 @@ type u3dVertex struct {
// drawMesh 按实例复用顶点变换,着色列表按指定顺序绘制 // drawMesh 按实例复用顶点变换,着色列表按指定顺序绘制
// 入参: instance 场景实例, node 模型节点, mesh 网格, pass 绘制轮次, passIndex 轮次索引 // 入参: instance 场景实例, node 模型节点, mesh 网格, pass 绘制轮次, passIndex 轮次索引
// 返回: error 几何、资源或取消错误 // 返回: error 几何、资源或取消错误
func (r *u3dRender) drawMesh(instance u3dInstance, node *U3DNode, mesh *U3DMesh, pass U3DViewPass, passIndex int) error { func (r *Renderer) drawMesh(instance u3dInstance, node *U3DNode, mesh *U3DMesh, pass U3DViewPass, passIndex int) error {
if r.mode >= U3DRenderBoundingBox && r.mode <= U3DRenderBoundingBoxOutline { if r.mode >= U3DRenderBoundingBox && r.mode <= U3DRenderBoundingBoxOutline {
return r.drawBounds(instance, node, mesh, pass, passIndex) return r.drawBounds(instance, node, mesh, pass, passIndex)
} }
@@ -42,14 +42,14 @@ func (r *u3dRender) drawMesh(instance u3dInstance, node *U3DNode, mesh *U3DMesh,
shaded := r.mode.shaded() && !r.depthOnly shaded := r.mode.shaded() && !r.depthOnly
var outline *u3dOutline var outline *u3dOutline
if r.mode.silhouette() { if r.mode.silhouette() {
if err := r.reserve(1, 1024); err != nil { if err := r.Reserve(1, 1024); err != nil {
return err return err
} }
outline = &u3dOutline{index: make(map[[2]u3dVector]int)} outline = &u3dOutline{index: make(map[[2]u3dVector]int)}
defer func() { r.remaining += 1024 + uint64(len(outline.edges))*1024 }() defer func() { r.remaining += 1024 + uint64(len(outline.edges))*1024 }()
} }
if r.mode == U3DRenderShadedWireframe || r.mode == U3DRenderShadedVertices { if r.mode == U3DRenderShadedWireframe || r.mode == U3DRenderShadedVertices {
if err := r.reserve(1, 1024); err != nil { if err := r.Reserve(1, 1024); err != nil {
return err return err
} }
r.primitives = make(map[u3dPrimitiveKey]struct{}) r.primitives = make(map[u3dPrimitiveKey]struct{})
@@ -64,7 +64,7 @@ func (r *u3dRender) drawMesh(instance u3dInstance, node *U3DNode, mesh *U3DMesh,
normalCount = len(mesh.Normals) normalCount = len(mesh.Normals)
} }
count := uint64(len(mesh.Positions)) + uint64(normalCount) count := uint64(len(mesh.Positions)) + uint64(normalCount)
if err := r.reserve(count, 24); err != nil { if err := r.Reserve(count, 24); err != nil {
return err return err
} }
defer func() { r.remaining += count * 24 }() defer func() { r.remaining += count * 24 }()
@@ -244,7 +244,7 @@ func (r *u3dRender) drawMesh(instance u3dInstance, node *U3DNode, mesh *U3DMesh,
// clipTriangle 在透视除法前裁剪六个视锥面,避免近面穿越或屏外巨大坐标 // clipTriangle 在透视除法前裁剪六个视锥面,避免近面穿越或屏外巨大坐标
// 入参: triangle 三角面, shader 着色器, pass 绘制轮次 // 入参: triangle 三角面, shader 着色器, pass 绘制轮次
// 返回: error 数值、资源或取消错误 // 返回: error 数值、资源或取消错误
func (r *u3dRender) clipTriangle(triangle [3]u3dVertex, shader U3DShader, pass U3DViewPass) error { func (r *Renderer) clipTriangle(triangle [3]u3dVertex, shader U3DShader, pass U3DViewPass) error {
if shader.Textures == ([8]*U3DTextureLayer{}) && shader.Blend == 0x606 && triangle[0].color[3] == 0 && triangle[1].color[3] == 0 && triangle[2].color[3] == 0 { if shader.Textures == ([8]*U3DTextureLayer{}) && shader.Blend == 0x606 && triangle[0].color[3] == 0 && triangle[1].color[3] == 0 && triangle[2].color[3] == 0 {
return r.ctx.Err() return r.ctx.Err()
} }
@@ -301,7 +301,7 @@ func (r *u3dRender) clipTriangle(triangle [3]u3dVertex, shader U3DShader, pass U
// rasterTriangle 使用像素中心和左上边规则光栅化,透视插值保持颜色与深度一致 // rasterTriangle 使用像素中心和左上边规则光栅化,透视插值保持颜色与深度一致
// 入参: triangle 已裁剪三角面, shader 着色器, pass 绘制轮次 // 入参: triangle 已裁剪三角面, shader 着色器, pass 绘制轮次
// 返回: error 数值、预算或取消错误 // 返回: error 数值、预算或取消错误
func (r *u3dRender) rasterTriangle(triangle [3]u3dVertex, shader U3DShader, pass U3DViewPass) error { func (r *Renderer) rasterTriangle(triangle [3]u3dVertex, shader U3DShader, pass U3DViewPass) error {
textures, err := r.shaderTextures(shader) textures, err := r.shaderTextures(shader)
if err != nil { if err != nil {
return err return err
@@ -432,7 +432,7 @@ func (r *u3dRender) rasterTriangle(triangle [3]u3dVertex, shader U3DShader, pass
// fragment 直接保存不透明覆盖,透明及其他混合方式保留逐像素深度 // fragment 直接保存不透明覆盖,透明及其他混合方式保留逐像素深度
// 入参: pixel 像素索引, depth 深度, color 颜色, blend 混合方式 // 入参: pixel 像素索引, depth 深度, color 颜色, blend 混合方式
// 返回: error 预算或取消错误 // 返回: error 预算或取消错误
func (r *u3dRender) fragment(pixel int, depth float64, color [4]float64, blend uint32) error { func (r *Renderer) fragment(pixel int, depth float64, color [4]float64, blend uint32) error {
if !finite(depth) { if !finite(depth) {
return fmt.Errorf("invalid U3D fragment depth") return fmt.Errorf("invalid U3D fragment depth")
} }
@@ -451,7 +451,7 @@ func (r *u3dRender) fragment(pixel int, depth float64, color [4]float64, blend u
} }
chunk := r.fragments / 512 chunk := r.fragments / 512
if chunk == len(r.chunks) { if chunk == len(r.chunks) {
if err := r.reserve(1, 512*56+48); err != nil { if err := r.Reserve(1, 512*56+48); err != nil {
return err return err
} }
r.chunks = append(r.chunks, make([]u3dFragment, 512)) r.chunks = append(r.chunks, make([]u3dFragment, 512))
@@ -464,7 +464,7 @@ func (r *u3dRender) fragment(pixel int, depth float64, color [4]float64, blend u
// resolveFragments 按每个像素的远近排序,正确处理相交透明面和着色器叠加 // resolveFragments 按每个像素的远近排序,正确处理相交透明面和着色器叠加
// 返回: error 预算或取消错误 // 返回: error 预算或取消错误
func (r *u3dRender) resolveFragments() error { func (r *Renderer) resolveFragments() error {
if r.fragments == 0 { if r.fragments == 0 {
return r.ctx.Err() return r.ctx.Err()
} }
@@ -491,7 +491,7 @@ func (r *u3dRender) resolveFragments() error {
} }
if len(r.scratch) == cap(r.scratch) { if len(r.scratch) == cap(r.scratch) {
capacity := max(8, cap(r.scratch)*2) capacity := max(8, cap(r.scratch)*2)
if err := r.reserve(uint64(capacity), 8); err != nil { if err := r.Reserve(uint64(capacity), 8); err != nil {
return err return err
} }
values := make([]int, len(r.scratch), capacity) values := make([]int, len(r.scratch), capacity)
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package ofdgo package threed
import ( import (
"fmt" "fmt"
+39
View File
@@ -0,0 +1,39 @@
// 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 threed
import (
"image"
"math"
)
// viewImageBounds 计算采样器可能写入的像素范围,并在整数转换前裁至目标边界
// 入参: source 原始像素边界, m 像素变换, target 目标边界
// 返回: image.Rectangle 受影响的目标像素区域
func viewImageBounds(source image.Rectangle, m [6]float64, target image.Rectangle) image.Rectangle {
minX, minY, maxX, maxY := math.Inf(1), math.Inf(1), math.Inf(-1), math.Inf(-1)
for _, p := range [4]image.Point{source.Min, {X: source.Max.X, Y: source.Min.Y}, {X: source.Min.X, Y: source.Max.Y}, source.Max} {
x := math.Floor(float64(m[0]*float64(p.X)) + float64(m[2]*float64(p.Y)) + m[4])
y := math.Floor(float64(m[1]*float64(p.X)) + float64(m[3]*float64(p.Y)) + m[5])
minX, minY = math.Min(minX, x), math.Min(minY, y)
maxX, maxY = math.Max(maxX, x+1), math.Max(maxY, y+1)
}
minX, minY = math.Max(minX, float64(target.Min.X)), math.Max(minY, float64(target.Min.Y))
maxX, maxY = math.Min(maxX, float64(target.Max.X)), math.Min(maxY, float64(target.Max.Y))
if minX >= maxX || minY >= maxY {
return image.Rectangle{}
}
return image.Rect(int(minX), int(minY), int(maxX), int(maxY))
}
+68
View File
@@ -0,0 +1,68 @@
// 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 threed
import (
"errors"
)
// 视图相机路径解析错误,由文档适配层转换为相应格式的错误
var (
ErrCameraNotFound = errors.New("missing U3D camera path")
ErrAmbiguousCamera = errors.New("ambiguous U3D camera instance")
)
// ViewCamera 返回视图节点指定父路径的世界变换,不修改绘制会话
// 入参: selected 视图节点索引, path 从根至节点的路径, exactRoot 是否要求路径始于根
// 返回: [16]float64 世界变换, error 缺失、歧义或取消错误
func (r *Renderer) ViewCamera(selected int, path []string, exactRoot bool) ([16]float64, error) {
var transform [16]float64
if selected < 0 || selected >= len(r.model.Nodes) || r.model.Nodes[selected].Kind != "View" {
return transform, ErrCameraNotFound
}
found := false
for _, index := range r.byNode[selected] {
if err := r.ctx.Err(); err != nil {
return transform, err
}
current, match := index, true
for j := len(path) - 1; j >= 0; j-- {
if j%256 == 0 {
if err := r.ctx.Err(); err != nil {
return transform, err
}
}
if current < 0 || r.model.Nodes[r.scene[current].node].Name != path[j] || r.model.Nodes[r.scene[current].node].Kind != "View" {
match = false
break
}
current = r.scene[current].parent
}
if exactRoot && current >= 0 {
match = false
}
if !match {
continue
}
if found {
return transform, ErrAmbiguousCamera
}
transform, found = r.scene[index].world, true
}
if !found {
return transform, ErrCameraNotFound
}
return transform, nil
}
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package ofdgo package threed
import ( import (
"fmt" "fmt"
@@ -23,7 +23,7 @@ import (
// viewLayers 按ECMA-363第9.5.4节依次绘制背景层或覆盖层 // viewLayers 按ECMA-363第9.5.4节依次绘制背景层或覆盖层
// 入参: layers 按后至前排列的纹理层 // 入参: layers 按后至前排列的纹理层
// 返回: error 参数、资源、预算或取消错误 // 返回: error 参数、资源、预算或取消错误
func (r *u3dRender) viewLayers(layers []U3DViewLayer) error { func (r *Renderer) viewLayers(layers []U3DViewLayer) error {
for _, layer := range layers { for _, layer := range layers {
if err := r.ctx.Err(); err != nil { if err := r.ctx.Err(); err != nil {
return err return err
@@ -49,8 +49,8 @@ func (r *u3dRender) viewLayers(layers []U3DViewLayer) error {
} }
sx, sy := float64(layer.Scale[0]), float64(layer.Scale[1]) sx, sy := float64(layer.Scale[0]), float64(layer.Scale[1])
rx, ry := float64(layer.Registration[0]), float64(layer.Registration[1]) rx, ry := float64(layer.Registration[0]), float64(layer.Registration[1])
m := RasterMatrix{cos * sx, -sin * sx, sin * sy, cos * sy, x - cos*sx*rx - sin*sy*ry, y + sin*sx*rx - cos*sy*ry} m := [6]float64{cos * sx, -sin * sx, sin * sy, cos * sy, x - cos*sx*rx - sin*sy*ry, y + sin*sx*rx - cos*sy*ry}
bounds := rasterImageBounds(texture.Bounds(), m, r.output.Bounds()) bounds := viewImageBounds(texture.Bounds(), m, r.output.Bounds())
for py := bounds.Min.Y; py < bounds.Max.Y; py++ { for py := bounds.Min.Y; py < bounds.Max.Y; py++ {
for px := bounds.Min.X; px < bounds.Max.X; px++ { for px := bounds.Min.X; px < bounds.Max.X; px++ {
if px%4096 == 0 || px == bounds.Min.X { if px%4096 == 0 || px == bounds.Min.X {
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package ofdgo package threed
import ( import (
"fmt" "fmt"
+3 -2
View File
@@ -17,6 +17,7 @@ package ofdgo
import ( import (
"encoding/xml" "encoding/xml"
"fmt" "fmt"
basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom"
"net/url" "net/url"
"strconv" "strconv"
) )
@@ -482,8 +483,8 @@ func (r *Renderer) appendGraphicActionSources(sources []actionSource, object Gra
if placement.a == 1 && placement.b == 0 && placement.c == 0 && placement.d == 1 { if placement.a == 1 && placement.b == 0 && placement.c == 0 && placement.d == 1 {
source.Box.W, source.Box.H = box.W, box.H source.Box.W, source.Box.H = box.W, box.H
} }
if box.W > 0 && box.H > 0 && !axisAlignedMatrix(placement) { if box.W > 0 && box.H > 0 && !basegeom.AxisAligned(basegeom.MatrixFromValues(placement.Values())) {
path := geometryRectangle(Box{W: box.W, H: box.H}) path := GeometryPath(basegeom.Rectangle(Box{W: box.W, H: box.H}))
for i := range path { for i := range path {
path[i].End.X, path[i].End.Y = placement.Transform(path[i].End.X, path[i].End.Y) path[i].End.X, path[i].End.Y = placement.Transform(path[i].End.X, path[i].End.Y)
} }
+2 -2
View File
@@ -178,7 +178,7 @@ func validateActionRegion(region *Region) error {
if err != nil { if err != nil {
return err return err
} }
current := Point{values[0], values[1]} current := Point{X: values[0], Y: values[1]}
start := current start := current
if len(area.Command) == 0 { if len(area.Command) == 0 {
return fmt.Errorf("action area requires a path command") return fmt.Errorf("action area requires a path command")
@@ -222,7 +222,7 @@ func validateActionRegion(region *Region) error {
if err != nil { if err != nil {
return err return err
} }
end = Point{values[0], values[1]} end = Point{X: values[0], Y: values[1]}
} }
if command.Type == "Arc" && end == current { if command.Type == "Arc" && end == current {
return fmt.Errorf("arc endpoint equals current point") return fmt.Errorf("arc endpoint equals current point")
+5 -4
View File
@@ -18,6 +18,7 @@ import (
"cmp" "cmp"
"context" "context"
"fmt" "fmt"
basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom"
"reflect" "reflect"
"slices" "slices"
"strconv" "strconv"
@@ -591,7 +592,7 @@ func editorDistribution(boxes []Box, indexes []editorObjectPosition, axis string
gap := (positions[last] + sizes[last] - positions[first] - total) / float64(len(order)-1) gap := (positions[last] + sizes[last] - positions[first] - total) / float64(len(order)-1)
for i, index := range order[:len(order)-1] { for i, index := range order[:len(order)-1] {
step := sizes[index] + gap step := sizes[index] + gap
if geometryEqual(step, 0) { if basegeom.Equal(step, 0) {
step = 0 step = 0
} }
if step < 0 || step == 0 && compareEditorPosition(indexes[index], indexes[order[i+1]]) > 0 { if step < 0 || step == 0 && compareEditorPosition(indexes[index], indexes[order[i+1]]) > 0 {
@@ -602,7 +603,7 @@ func editorDistribution(boxes []Box, indexes []editorObjectPosition, axis string
for _, index := range order[1 : len(order)-1] { for _, index := range order[1 : len(order)-1] {
position := next position := next
next += sizes[index] + gap next += sizes[index] + gap
if geometryEqual(position, positions[index]) { if basegeom.Equal(position, positions[index]) {
continue continue
} }
dx, dy := position-positions[index], 0.0 dx, dy := position-positions[index], 0.0
@@ -791,10 +792,10 @@ func editorAlignment(box, target Box, alignment string) Matrix {
case "bottom": case "bottom":
matrix.f = target.Y + target.H - box.H - box.Y matrix.f = target.Y + target.H - box.H - box.Y
} }
if geometryEqual(matrix.e, 0) { if basegeom.Equal(matrix.e, 0) {
matrix.e = 0 matrix.e = 0
} }
if geometryEqual(matrix.f, 0) { if basegeom.Equal(matrix.f, 0) {
matrix.f = 0 matrix.f = 0
} }
return matrix return matrix
+2 -1
View File
@@ -18,6 +18,7 @@ import (
"bytes" "bytes"
"encoding/xml" "encoding/xml"
"fmt" "fmt"
basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom"
"maps" "maps"
"slices" "slices"
) )
@@ -345,7 +346,7 @@ func (n *editorCompositeNode) preserveActionRegions(matrix Matrix) (bool, error)
if n.object.Type == "ImageObject" && ctm == "" && (matrix.a != 1 || matrix.b != 0 || matrix.c != 0 || matrix.d != 1) { if n.object.Type == "ImageObject" && ctm == "" && (matrix.a != 1 || matrix.b != 0 || matrix.c != 0 || matrix.d != 1) {
return false, nil return false, nil
} }
if axisAlignedMatrix(matrix) { if basegeom.AxisAligned(basegeom.MatrixFromValues(matrix.Values())) {
return true, nil return true, nil
} }
var targets []*editorXML var targets []*editorXML
+3 -2
View File
@@ -18,6 +18,7 @@ import (
"bytes" "bytes"
"context" "context"
"fmt" "fmt"
basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom"
"maps" "maps"
"slices" "slices"
@@ -165,7 +166,7 @@ func (m CompositeMember) Shape() (ShapeKind, Box) {
if lineShapeKind(kind) { if lineShapeKind(kind) {
return kind, transformLineBox(box, parent) return kind, transformLineBox(box, parent)
} }
if !axisAlignedMatrix(parent) { if !basegeom.AxisAligned(basegeom.MatrixFromValues(parent.Values())) {
return "", Box{} return "", Box{}
} }
return kind, parent.TransformBox(box) return kind, parent.TransformBox(box)
@@ -303,7 +304,7 @@ func (m CompositeMember) shapeParent() (Matrix, bool) {
// 返回: Box 变换后的端点 // 返回: Box 变换后的端点
func transformLineBox(box Box, matrix Matrix) Box { func transformLineBox(box Box, matrix Matrix) Box {
x, y := matrix.Transform(box.X, box.Y) x, y := matrix.Transform(box.X, box.Y)
w, h := matrixVector(matrix, box.W, box.H) w, h := basegeom.Vector(basegeom.MatrixFromValues(matrix.Values()), box.W, box.H)
return Box{X: x, Y: y, W: w, H: h} return Box{X: x, Y: y, W: w, H: h}
} }
+5 -4
View File
@@ -16,6 +16,7 @@ package ofdgo
import ( import (
"fmt" "fmt"
basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom"
"math" "math"
"reflect" "reflect"
"slices" "slices"
@@ -58,7 +59,7 @@ func (e *Editor) EraseCompositeObjects(page int, path ObjectPath, indexes []int,
if err := validateCreationBox(box); err != nil { if err := validateCreationBox(box); err != nil {
return err return err
} }
return e.eraseCompositeObjects(page, path, indexes, geometryRectangle(box)) return e.eraseCompositeObjects(page, path, indexes, GeometryPath(basegeom.Rectangle(box)))
} }
// EraseCompositeObjectsPath 按闭合折线擦除内部成员,末点自动连接起点 // EraseCompositeObjectsPath 按闭合折线擦除内部成员,末点自动连接起点
@@ -113,7 +114,7 @@ func (e *Editor) eraseCompositeObjects(page int, path ObjectPath, indexes []int,
if box.W <= 0 || box.H <= 0 { if box.W <= 0 || box.H <= 0 {
continue continue
} }
bounds := geometryRectangle(box) bounds := GeometryPath(basegeom.Rectangle(box))
intersection, err := geometry.Combine(bounds, region, GeometryIntersect) intersection, err := geometry.Combine(bounds, region, GeometryIntersect)
if err != nil { if err != nil {
return err return err
@@ -137,7 +138,7 @@ func (e *Editor) eraseCompositeObjects(page int, path ObjectPath, indexes []int,
if !ok { if !ok {
return fmt.Errorf("composite clip transform is not invertible") return fmt.Errorf("composite clip transform is not invertible")
} }
outer := geometryRectangle(Box{X: box.X - 1, Y: box.Y - 1, W: box.W + 2, H: box.H + 2}) outer := GeometryPath(basegeom.Rectangle(Box{X: box.X - 1, Y: box.Y - 1, W: box.W + 2, H: box.H + 2}))
remainder, err = geometry.Combine(outer, region, GeometrySubtract) remainder, err = geometry.Combine(outer, region, GeometrySubtract)
if err != nil { if err != nil {
return err return err
@@ -178,7 +179,7 @@ func (e *Editor) eraseObjects(page int, ids []string, box Box, points []Point, r
} }
covered := x == bounds.X && y == bounds.Y && right == bounds.X+bounds.W && bottom == bounds.Y+bounds.H covered := x == bounds.X && y == bounds.Y && right == bounds.X+bounds.W && bottom == bounds.Y+bounds.H
if region != nil { if region != nil {
path := geometryRectangle(bounds) path := GeometryPath(basegeom.Rectangle(bounds))
intersection, err := e.backends.Geometry.Combine(path, region, GeometryIntersect) intersection, err := e.backends.Geometry.Combine(path, region, GeometryIntersect)
if err != nil { if err != nil {
return err return err
+3 -2
View File
@@ -18,6 +18,7 @@ import (
"bytes" "bytes"
"encoding/xml" "encoding/xml"
"fmt" "fmt"
basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom"
"math" "math"
"slices" "slices"
) )
@@ -206,7 +207,7 @@ func (obj TextObject) ResizeTextFrame(offset, width float64) (TextObject, error)
if err != nil { if err != nil {
return TextObject{}, err return TextObject{}, err
} }
if !axisAlignedMatrix(NewMatrix(obj.CTM)) { if !basegeom.AxisAligned(basegeom.MatrixFromValues(NewMatrix(obj.CTM).Values())) {
return TextObject{}, fmt.Errorf("text frame resizing requires an axis-aligned transform") return TextObject{}, fmt.Errorf("text frame resizing requires an axis-aligned transform")
} }
if !finite(offset) || !finite(width) || width <= 0 { if !finite(offset) || !finite(width) || width <= 0 {
@@ -474,7 +475,7 @@ func transformEditorMatrix(object GraphicObject, matrix Matrix) GraphicObject {
if clips := editorObjectClips(&object); *clips != nil && !(*clips).usesObjectMatrix() { if clips := editorObjectClips(&object); *clips != nil && !(*clips).usesObjectMatrix() {
*clips = transformObjectClips(*clips, local) *clips = transformObjectClips(*clips, local)
} }
if !axisAlignedMatrix(matrix) && before.W > 0 && before.H > 0 { if !basegeom.AxisAligned(basegeom.MatrixFromValues(matrix.Values())) && before.W > 0 && before.H > 0 {
clips := editorObjectClips(&object) clips := editorObjectClips(&object)
if *clips == nil { if *clips == nil {
*clips = &Clips{} *clips = &Clips{}
+4 -4
View File
@@ -50,25 +50,25 @@ type glyphGeometry struct{ path GeometryPath }
// MoveTo 开始子路径 // MoveTo 开始子路径
// 入参: x 横坐标, y 纵坐标 // 入参: x 横坐标, y 纵坐标
func (p *glyphGeometry) MoveTo(x, y float64) { func (p *glyphGeometry) MoveTo(x, y float64) {
p.path = append(p.path, GeometrySegment{Verb: GeometryMove, End: Point{x, -y}}) p.path = append(p.path, GeometrySegment{Verb: GeometryMove, End: Point{X: x, Y: -y}})
} }
// LineTo 添加直线 // LineTo 添加直线
// 入参: x 横坐标, y 纵坐标 // 入参: x 横坐标, y 纵坐标
func (p *glyphGeometry) LineTo(x, y float64) { func (p *glyphGeometry) LineTo(x, y float64) {
p.path = append(p.path, GeometrySegment{Verb: GeometryLine, End: Point{x, -y}}) p.path = append(p.path, GeometrySegment{Verb: GeometryLine, End: Point{X: x, Y: -y}})
} }
// QuadTo 添加二次贝塞尔曲线 // QuadTo 添加二次贝塞尔曲线
// 入参: cx 控制点横坐标, cy 控制点纵坐标, x 终点横坐标, y 终点纵坐标 // 入参: cx 控制点横坐标, cy 控制点纵坐标, x 终点横坐标, y 终点纵坐标
func (p *glyphGeometry) QuadTo(cx, cy, x, y float64) { func (p *glyphGeometry) QuadTo(cx, cy, x, y float64) {
p.path = append(p.path, GeometrySegment{Verb: GeometryQuad, Control1: Point{cx, -cy}, End: Point{x, -y}}) p.path = append(p.path, GeometrySegment{Verb: GeometryQuad, Control1: Point{X: cx, Y: -cy}, End: Point{X: x, Y: -y}})
} }
// CubeTo 添加三次贝塞尔曲线 // CubeTo 添加三次贝塞尔曲线
// 入参: ax 第一控制点横坐标, ay 第一控制点纵坐标, bx 第二控制点横坐标, by 第二控制点纵坐标, x 终点横坐标, y 终点纵坐标 // 入参: ax 第一控制点横坐标, ay 第一控制点纵坐标, bx 第二控制点横坐标, by 第二控制点纵坐标, x 终点横坐标, y 终点纵坐标
func (p *glyphGeometry) CubeTo(ax, ay, bx, by, x, y float64) { func (p *glyphGeometry) CubeTo(ax, ay, bx, by, x, y float64) {
p.path = append(p.path, GeometrySegment{Verb: GeometryCubic, Control1: Point{ax, -ay}, Control2: Point{bx, -by}, End: Point{x, -y}}) p.path = append(p.path, GeometrySegment{Verb: GeometryCubic, Control1: Point{X: ax, Y: -ay}, Control2: Point{X: bx, Y: -by}, End: Point{X: x, Y: -y}})
} }
// Close 闭合当前子路径 // Close 闭合当前子路径
+12 -115
View File
@@ -16,24 +16,21 @@ package ofdgo
import ( import (
"fmt" "fmt"
"math"
"slices" "slices"
"strconv" "strconv"
"strings" "strings"
basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom"
) )
// IdentityMatrix 单位矩阵 // IdentityMatrix 单位矩阵
var IdentityMatrix = Matrix{1, 0, 0, 1, 0, 0} var IdentityMatrix = Matrix{1, 0, 0, 1, 0, 0}
// Box 矩形区域 // Box 矩形区域
type Box struct { type Box = basegeom.Box
X, Y, W, H float64
}
// Point 平面坐标点 // Point 平面坐标点
type Point struct { type Point = basegeom.Point
X, Y float64
}
// Matrix 2D仿射变换矩阵 // Matrix 2D仿射变换矩阵
type Matrix struct { type Matrix struct {
@@ -95,36 +92,21 @@ func NewMatrix(s string) Matrix {
// 入参: o 右侧矩阵 // 入参: o 右侧矩阵
// 返回: Matrix 结果矩阵 // 返回: Matrix 结果矩阵
func (m Matrix) Multiply(o Matrix) Matrix { func (m Matrix) Multiply(o Matrix) Matrix {
return Matrix{ return MatrixFromValues(basegeom.MatrixFromValues(m.Values()).Multiply(basegeom.MatrixFromValues(o.Values())).Values())
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 应用变换矩阵 // Transform 应用变换矩阵
// 入参: x X坐标, y Y坐标 // 入参: x X坐标, y Y坐标
// 返回: float64 变换后X, float64 变换后Y // 返回: float64 变换后X, float64 变换后Y
func (m Matrix) Transform(x, y float64) (float64, float64) { func (m Matrix) Transform(x, y float64) (float64, float64) {
nx := m.a*x + m.c*y + m.e return basegeom.MatrixFromValues(m.Values()).Transform(x, y)
ny := m.b*x + m.d*y + m.f
return nx, ny
} }
// TransformBox 获取矩形经仿射变换后的轴对齐边界 // TransformBox 获取矩形经仿射变换后的轴对齐边界
// 入参: box 原始矩形范围 // 入参: box 原始矩形范围
// 返回: Box 变换后的轴对齐边界 // 返回: Box 变换后的轴对齐边界
func (m Matrix) TransformBox(box Box) Box { func (m Matrix) TransformBox(box Box) Box {
x, y := m.Transform(box.X, box.Y) return basegeom.MatrixFromValues(m.Values()).TransformBox(box)
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}
} }
// String 返回标准CTM属性值 // String 返回标准CTM属性值
@@ -136,109 +118,24 @@ func (m Matrix) String() string {
// Invert 求仿射矩阵的逆变换 // Invert 求仿射矩阵的逆变换
// 返回: Matrix 逆矩阵,失败时返回单位矩阵, bool 是否存在有限逆矩阵 // 返回: Matrix 逆矩阵,失败时返回单位矩阵, bool 是否存在有限逆矩阵
func (m Matrix) Invert() (Matrix, bool) { func (m Matrix) Invert() (Matrix, bool) {
for _, value := range m.Values() { result, ok := basegeom.MatrixFromValues(m.Values()).Invert()
if !finite(value) { if !ok {
return IdentityMatrix, false return IdentityMatrix, false
} }
} return MatrixFromValues(result.Values()), true
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 构造平移矩阵 // TranslationMatrix 构造平移矩阵
// 入参: x X轴偏移, y Y轴偏移 // 入参: x X轴偏移, y Y轴偏移
// 返回: Matrix 平移矩阵 // 返回: Matrix 平移矩阵
func TranslationMatrix(x, y float64) Matrix { func TranslationMatrix(x, y float64) Matrix {
return Matrix{a: 1, d: 1, e: x, f: y} return MatrixFromValues(basegeom.TranslationMatrix(x, y).Values())
} }
// YScale 获取Y轴缩放比例 // YScale 获取Y轴缩放比例
// 返回: float64 缩放比例 // 返回: float64 缩放比例
func (m Matrix) YScale() float64 { func (m Matrix) YScale() float64 {
return math.Sqrt(m.c*m.c + m.d*m.d) return basegeom.MatrixFromValues(m.Values()).YScale()
}
// geometryEqual 比较毫米坐标,使用固定绝对误差,不受绘图库全局精度影响
// 入参: a 第一个坐标, b 第二个坐标
// 返回: bool 是否在容差内相等
func geometryEqual(a, b float64) bool {
return math.Abs(a-b) <= 1e-10
}
// axisAlignedMatrix 判断变换是否保持坐标轴平行,含直角旋转与镜像
// 入参: m 仿射变换矩阵
// 返回: bool 是否为非退化的轴对齐变换
func axisAlignedMatrix(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
}
// matrixVector 变换位移,不包含平移分量
// 入参: m 仿射变换矩阵, x 原始横向位移, y 原始纵向位移
// 返回: float64 变换后的X位移, float64 变换后的Y位移
func matrixVector(m Matrix, x, y float64) (float64, float64) {
return m.a*x + m.c*y, m.b*x + m.d*y
} }
// parseFloats 解析浮点数数组 // parseFloats 解析浮点数数组
+7 -80
View File
@@ -16,7 +16,8 @@ package ofdgo
import ( import (
"fmt" "fmt"
"math"
basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom"
) )
// 路径填充区域的交集、并集、差集和异或运算 // 路径填充区域的交集、并集、差集和异或运算
@@ -35,15 +36,7 @@ type GeometryOperation uint8
// MiterLimit为尖角长度与半线宽的比值上限,MiterLimitSet区分显式零值与默认值 // MiterLimit为尖角长度与半线宽的比值上限,MiterLimitSet区分显式零值与默认值
// DashOffset为虚线起始偏移,允许负值;Dashes交替指定实线段和间隔长度,奇数项按重复一遍处理 // DashOffset为虚线起始偏移,允许负值;Dashes交替指定实线段和间隔长度,奇数项按重复一遍处理
// Tolerance为曲线展开的误差目标,0采用后端默认精度 // Tolerance为曲线展开的误差目标,0采用后端默认精度
type StrokeOptions struct { type StrokeOptions = basegeom.StrokeOptions
Width float64
Cap, Join string
MiterLimit float64
MiterLimitSet bool
DashOffset float64
Tolerance float64
Dashes []float64
}
// GeometryBackend 操作库自有路径,坐标以页面左上角为原点,不修改任何输入 // GeometryBackend 操作库自有路径,坐标以页面左上角为原点,不修改任何输入
// Clip返回nil表示不裁剪,空路径指针表示完全裁去,矩阵将裁剪坐标映射到页面坐标 // Clip返回nil表示不裁剪,空路径指针表示完全裁去,矩阵将裁剪坐标映射到页面坐标
@@ -97,72 +90,6 @@ func (r *Renderer) PathOutline(object PathObject) (string, error) {
return path.SVG() return path.SVG()
} }
// 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
}
// geometryRectangle 构建页面坐标矩形,保留输入精度
// 入参: box 矩形范围
// 返回: GeometryPath 闭合路径
func geometryRectangle(box Box) GeometryPath {
return GeometryPath{
{Verb: GeometryMove, End: Point{X: box.X, Y: box.Y}},
{Verb: GeometryLine, End: Point{X: box.X + box.W, Y: box.Y}},
{Verb: GeometryLine, End: Point{X: box.X + box.W, Y: box.Y + box.H}},
{Verb: GeometryLine, End: Point{X: box.X, Y: box.Y + box.H}},
{Verb: GeometryClose, End: Point{X: box.X, Y: box.Y}},
}
}
// geometryRectangleBounds 识别单个轴对齐矩形,不用包围盒代替实际路径
// 入参: path 填充路径
// 返回: Box 矩形范围, bool 是否为非退化矩形
func geometryRectangleBounds(path GeometryPath) (Box, bool) {
if len(path) > 0 && path[len(path)-1].Verb == GeometryClose {
path = path[:len(path)-1]
}
if len(path) == 5 && path[4].Verb == GeometryLine && path[4].End == path[0].End {
path = path[:4]
}
if len(path) != 4 || path[0].Verb != GeometryMove || path[1].Verb != GeometryLine || path[2].Verb != GeometryLine || path[3].Verb != GeometryLine {
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
}
// clipGeometry 将非零填充路径与页面裁剪相交 // clipGeometry 将非零填充路径与页面裁剪相交
// 入参: geometry 几何后端, path 页面路径, clip 页面裁剪 // 入参: geometry 几何后端, path 页面路径, clip 页面裁剪
// 返回: GeometryPath 裁剪路径, error 几何错误 // 返回: GeometryPath 裁剪路径, error 几何错误
@@ -173,14 +100,14 @@ func clipGeometry(geometry GeometryBackend, path GeometryPath, clip *GeometryPat
if len(*clip) == 0 { if len(*clip) == 0 {
return nil, nil return nil, nil
} }
if rect, ok := geometryRectangleBounds(*clip); ok { if rect, ok := basegeom.RectangleBounds(basegeom.Path(*clip)); ok {
if bounds, ok := geometryRectangleBounds(path); ok { if bounds, ok := basegeom.RectangleBounds(basegeom.Path(path)); ok {
left, top := max(rect.X, bounds.X), max(rect.Y, bounds.Y) left, top := max(rect.X, bounds.X), max(rect.Y, bounds.Y)
right, bottom := min(rect.X+rect.W, bounds.X+bounds.W), min(rect.Y+rect.H, bounds.Y+bounds.H) right, bottom := min(rect.X+rect.W, bounds.X+bounds.W), min(rect.Y+rect.H, bounds.Y+bounds.H)
if right <= left || bottom <= top { if right <= left || bottom <= top {
return nil, nil return nil, nil
} }
return geometryRectangle(Box{X: left, Y: top, W: right - left, H: bottom - top}), nil return GeometryPath(basegeom.Rectangle(Box{X: left, Y: top, W: right - left, H: bottom - top})), nil
} }
if err := path.validate(); err != nil { if err := path.validate(); err != nil {
return nil, err return nil, err
@@ -214,7 +141,7 @@ func clipGeometry(geometry GeometryBackend, path GeometryPath, clip *GeometryPat
return path, nil return path, nil
} }
} }
if rect, ok := geometryRectangleBounds(path); ok { if rect, ok := basegeom.RectangleBounds(basegeom.Path(path)); ok {
bounds, err := geometry.Bounds(*clip) bounds, err := geometry.Bounds(*clip)
if err != nil { if err != nil {
return nil, err return nil, err
+60 -50
View File
@@ -18,16 +18,18 @@ import (
"fmt" "fmt"
"strconv" "strconv"
"strings" "strings"
basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom"
) )
// 几何路径指令,包括直线、曲线、椭圆弧和闭合 // 几何路径指令,包括直线、曲线、椭圆弧和闭合
const ( const (
GeometryMove GeometryVerb = iota GeometryMove = basegeom.Move
GeometryLine GeometryLine = basegeom.Line
GeometryQuad GeometryQuad = basegeom.Quad
GeometryCubic GeometryCubic = basegeom.Cubic
GeometryArc GeometryArc = basegeom.Arc
GeometryClose GeometryClose = basegeom.ClosePath
) )
// GeometryPath 保存独立于绘图库的几何路径,不扁平化曲线或舍入坐标 // GeometryPath 保存独立于绘图库的几何路径,不扁平化曲线或舍入坐标
@@ -35,18 +37,11 @@ const (
type GeometryPath []GeometrySegment type GeometryPath []GeometrySegment
// GeometryVerb 表示几何路径指令 // GeometryVerb 表示几何路径指令
type GeometryVerb uint8 type GeometryVerb = basegeom.Verb
// GeometrySegment 保存路径端点、贝塞尔控制点或端点式椭圆弧参数 // GeometrySegment 保存路径端点、贝塞尔控制点或端点式椭圆弧参数
// 弧线Rotation单位为度,Sweep为true表示顺时针,Large表示大弧 // 弧线Rotation单位为度,Sweep为true表示顺时针,Large表示大弧
type GeometrySegment struct { type GeometrySegment = basegeom.Segment
Verb GeometryVerb
End Point
Control1, Control2 Point
RadiusX, RadiusY float64
Rotation float64
Large, Sweep bool
}
// ParseGeometryPath 解析标准OFD紧缩路径,保留弧线和原始数值精度 // ParseGeometryPath 解析标准OFD紧缩路径,保留弧线和原始数值精度
// 入参: data 紧缩路径 // 入参: data 紧缩路径
@@ -137,18 +132,18 @@ func geometryPathSegment(command string, args []string) (GeometrySegment, error)
segment := GeometrySegment{} segment := GeometrySegment{}
switch command { switch command {
case "M", "S": case "M", "S":
segment.Verb, segment.End = GeometryMove, Point{values[0], values[1]} segment.Verb, segment.End = GeometryMove, Point{X: values[0], Y: values[1]}
case "L": case "L":
segment.Verb, segment.End = GeometryLine, Point{values[0], values[1]} segment.Verb, segment.End = GeometryLine, Point{X: values[0], Y: values[1]}
case "Q": case "Q":
segment.Verb, segment.Control1, segment.End = GeometryQuad, Point{values[0], values[1]}, Point{values[2], values[3]} segment.Verb, segment.Control1, segment.End = GeometryQuad, Point{X: values[0], Y: values[1]}, Point{X: values[2], Y: values[3]}
case "B": case "B":
segment.Verb, segment.Control1, segment.Control2, segment.End = GeometryCubic, Point{values[0], values[1]}, Point{values[2], values[3]}, Point{values[4], values[5]} segment.Verb, segment.Control1, segment.Control2, segment.End = GeometryCubic, Point{X: values[0], Y: values[1]}, Point{X: values[2], Y: values[3]}, Point{X: values[4], Y: values[5]}
case "A": case "A":
if values[0] < 0 || values[1] < 0 || values[3] != 0 && values[3] != 1 || values[4] != 0 && values[4] != 1 { if values[0] < 0 || values[1] < 0 || values[3] != 0 && values[3] != 1 || values[4] != 0 && values[4] != 1 {
return GeometrySegment{}, fmt.Errorf("invalid arc radii or flags") return GeometrySegment{}, fmt.Errorf("invalid arc radii or flags")
} }
segment = GeometrySegment{Verb: GeometryArc, RadiusX: values[0], RadiusY: values[1], Rotation: values[2], Large: values[3] != 0, Sweep: values[4] != 0, End: Point{values[5], values[6]}} segment = GeometrySegment{Verb: GeometryArc, RadiusX: values[0], RadiusY: values[1], Rotation: values[2], Large: values[3] != 0, Sweep: values[4] != 0, End: Point{X: values[5], Y: values[6]}}
case "C": case "C":
segment.Verb = GeometryClose segment.Verb = GeometryClose
} }
@@ -187,36 +182,6 @@ func objectGeometryPath(geometry GeometryBackend, object PathObject) (GeometryPa
return geometry.Transform(path, TranslationMatrix(box.X, box.Y).Multiply(NewMatrix(object.CTM))) return geometry.Transform(path, TranslationMatrix(box.X, box.Y).Multiply(NewMatrix(object.CTM)))
} }
// closedGeometry 为填充轮廓补齐隐式闭合边,不改变源路径
// 入参: path 填充路径
// 返回: GeometryPath 显式闭合路径
func closedGeometry(path GeometryPath) GeometryPath {
open, missing := false, false
for _, segment := range path {
if segment.Verb == GeometryMove && open {
missing = true
break
}
open = segment.Verb != GeometryClose
}
if !missing && !open {
return path
}
result := make(GeometryPath, 0, len(path)+1)
open = false
for _, segment := range path {
if segment.Verb == GeometryMove && open {
result = append(result, GeometrySegment{Verb: GeometryClose})
}
result = append(result, segment)
open = segment.Verb != GeometryClose
}
if open {
result = append(result, GeometrySegment{Verb: GeometryClose})
}
return result
}
// encode 按目标格式写入路径,圆弧标志采用两种格式均支持的0和1 // encode 按目标格式写入路径,圆弧标志采用两种格式均支持的0和1
// 入参: svg 是否使用SVG指令 // 入参: svg 是否使用SVG指令
// 返回: string 路径数据, error 不支持的路径指令 // 返回: string 路径数据, error 不支持的路径指令
@@ -275,3 +240,48 @@ func geometryClipPath(path GeometryPath) (PathObject, error) {
box.W, box.H = max(box.W, 1e-6), max(box.H, 1e-6) box.W, box.H = max(box.W, 1e-6), max(box.H, 1e-6)
return PathObject{Boundary: editorBoxString(box), CTM: TranslationMatrix(-box.X, -box.Y).String(), Fill: &fill, Stroke: &stroke, AbbreviatedData: data}, nil return PathObject{Boundary: editorBoxString(box), CTM: TranslationMatrix(-box.X, -box.Y).String(), Fill: &fill, Stroke: &stroke, AbbreviatedData: data}, nil
} }
// Bounds 通过解析极值计算直线、贝塞尔和椭圆弧的轴对齐边界
// 不进行曲线展开且不使用Tolerance,结果受float64舍入误差影响,不保证向外舍入
// 返回: Box 浮点路径范围, error 无效路径、弧线或数值溢出
func (p GeometryPath) Bounds() (Box, error) {
return basegeom.Path(p).Bounds()
}
// Curves 按毫米误差上限将端点式椭圆弧转换为三次贝塞尔曲线,其他指令保持原样
// 入参: tolerance 正有限误差上限
// 返回: GeometryPath 独立路径, error 参数错误
func (p GeometryPath) Curves(tolerance float64) (GeometryPath, error) {
result, err := basegeom.Path(p).Curves(tolerance)
return GeometryPath(result), err
}
// Stroke 按端点和转角样式将曲线、虚线转换为非零填充轮廓
// 曲线按Tolerance展开,默认误差为0.001毫米,重叠轮廓保持同向绕数
// 结果未做布尔整理,按非零规则填充,区域运算需能处理重叠子路径的后端
// 入参: options 描边样式
// 返回: GeometryPath 描边轮廓, error 无效参数或精度限制
func (p GeometryPath) Stroke(options StrokeOptions) (GeometryPath, error) {
if options.MiterLimit == 0 && !options.MiterLimitSet {
options.MiterLimit = defaultMiterLimit
}
result, err := basegeom.Path(p).Stroke(options)
if unsupported, ok := err.(basegeom.UnsupportedError); ok {
err = fmt.Errorf("%s: %w", unsupported, ErrBackendUnavailable)
}
return GeometryPath(result), err
}
// Transform 精确变换控制点和椭圆轴,退化弧沿投影极值分段保留折返
// 入参: matrix 仿射变换
// 返回: GeometryPath 独立路径, error 无效路径或数值溢出
func (p GeometryPath) Transform(matrix Matrix) (GeometryPath, error) {
result, err := basegeom.Path(p).Transform(basegeom.MatrixFromValues(matrix.Values()))
return GeometryPath(result), err
}
// validate 校验公共路径指令、子路径起点和有限坐标
// 返回: error 无效路径
func (p GeometryPath) validate() error {
return basegeom.Path(p).Validate()
}
+1 -1
View File
@@ -239,7 +239,7 @@ func (r *Renderer) resolveMesh(fill *FillColor) (*MeshShading, error) {
if value := mergeAlpha(point.Color.Alpha, fill.Alpha); value != nil { if value := mergeAlpha(point.Color.Alpha, fill.Alpha); value != nil {
alpha = float64(*value) / 255 alpha = float64(*value) / 255
} }
vertices[i] = MeshVertex{Point: Point{point.X, point.Y}, Space: kind, Values: values, Alpha: alpha, profile: profile} vertices[i] = MeshVertex{Point: Point{X: point.X, Y: point.Y}, Space: kind, Values: values, Alpha: alpha, profile: profile}
} }
mesh := &MeshShading{} mesh := &MeshShading{}
if extend == 1 && back != nil { if extend == 1 && back != nil {
+5 -2
View File
@@ -14,7 +14,10 @@
package ofdgo package ofdgo
import "image/color" import (
basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom"
"image/color"
)
// 画刷类型,区分纯色、渐变和图案填充 // 画刷类型,区分纯色、渐变和图案填充
const ( const (
@@ -94,7 +97,7 @@ func (r *Renderer) ResolvePaint(fill *FillColor) (Paint, error) {
if err != nil { if err != nil {
return Paint{}, err return Paint{}, err
} }
if len(start) >= 2 && len(end) >= 2 && len(stops) != 0 && (!geometryEqual(start[0], end[0]) || !geometryEqual(start[1], end[1])) { if len(start) >= 2 && len(end) >= 2 && len(stops) != 0 && (!basegeom.Equal(start[0], end[0]) || !basegeom.Equal(start[1], end[1])) {
paint.Kind = PaintLinear paint.Kind = PaintLinear
paint.Gradient = &Shading{Start: Point{X: start[0], Y: start[1]}, End: Point{X: end[0], Y: end[1]}, Stops: stops, Extend: node.Extend, MapType: node.MapType, MapUnit: node.MapUnit} paint.Gradient = &Shading{Start: Point{X: start[0], Y: start[1]}, End: Point{X: end[0], Y: end[1]}, Stops: stops, Extend: node.Extend, MapType: node.MapType, MapUnit: node.MapUnit}
paint.Gradient.Sample = sample paint.Gradient.Sample = sample
+3 -3
View File
@@ -431,7 +431,7 @@ func (b *pdfPointBounds) box() Box {
if !b.found { if !b.found {
return Box{} return Box{}
} }
return Box{b.minX, b.minY, math.Max(b.maxX-b.minX, 1e-6), math.Max(b.maxY-b.minY, 1e-6)} return Box{X: b.minX, Y: b.minY, W: math.Max(b.maxX-b.minX, 1e-6), H: math.Max(b.maxY-b.minY, 1e-6)}
} }
// pathBoundsContext 逐段计算变换后的控制点边界,不创建临时点集合 // pathBoundsContext 逐段计算变换后的控制点边界,不创建临时点集合
@@ -465,7 +465,7 @@ func (p *pdfImporter) pathBoundsContext(ctx context.Context, path pdfgo.Path) (B
if !found { if !found {
return Box{}, nil return Box{}, nil
} }
return Box{minX, minY, math.Max(maxX-minX, 1e-6), math.Max(maxY-minY, 1e-6)}, nil return Box{X: minX, Y: minY, W: math.Max(maxX-minX, 1e-6), H: math.Max(maxY-minY, 1e-6)}, nil
} }
// color 保留设备色彩分量并复用文档颜色空间 // color 保留设备色彩分量并复用文档颜色空间
@@ -662,7 +662,7 @@ func (p *pdfImporter) appendPath(mark pdfgo.PathMark) error {
} }
if mark.Stroke { if mark.Stroke {
margin := mark.Style.LineWidth * marginScale / 2 * math.Max(1, mark.Style.MiterLimit) margin := mark.Style.LineWidth * marginScale / 2 * math.Max(1, mark.Style.MiterLimit)
box = Box{box.X - margin, box.Y - margin, box.W + 2*margin, box.H + 2*margin} box = Box{X: box.X - margin, Y: box.Y - margin, W: box.W + 2*margin, H: box.H + 2*margin}
} }
if mark.StrokeMatrix == (pdfgo.Matrix{}) { if mark.StrokeMatrix == (pdfgo.Matrix{}) {
origin = box origin = box
+26 -101
View File
@@ -22,6 +22,7 @@ import (
"math" "math"
"strings" "strings"
basethreed "github.com/xiaoqidun/ofdgo/internal/base/threed"
"github.com/xiaoqidun/pdfgo" "github.com/xiaoqidun/pdfgo"
) )
@@ -222,22 +223,22 @@ func RenderPDFThreeD(ctx context.Context, model *U3DModel, source pdfgo.ThreeD,
if err := pdfThreeDSections(ctx, presentation.CrossSections, &render); err != nil { if err := pdfThreeDSections(ctx, presentation.CrossSections, &render); err != nil {
return nil, err return nil, err
} }
r, err := newU3DRender(ctx, model, render, options.Nodes) r, err := basethreed.NewRenderer(ctx, model, render, options.Nodes)
if err != nil { if err != nil {
return nil, err return nil, err
} }
camera, view, err := r.pdfCamera(source.Camera) camera, view, err := pdfThreeDCamera(ctx, model, r, source.Camera)
if err != nil { if err != nil {
return nil, err return nil, err
} }
render.View = view render.View = view
camera.ScaleX, camera.ScaleY, camera.Near = 1, 1, 1 camera.ScaleX, camera.ScaleY, camera.Near = 1, 1, 1
render.Camera = &camera render.Camera = &camera
passes, err := r.selectView(render) passes, err := r.SelectView(render)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if err := r.prepareSections(render.Sections); err != nil { if err := r.PrepareSections(render.Sections); err != nil {
return nil, err return nil, err
} }
scale, err := projection.ScaleFactor(source.ViewBox, source.AnnotationBox) scale, err := projection.ScaleFactor(source.ViewBox, source.AnnotationBox)
@@ -268,7 +269,7 @@ func RenderPDFThreeD(ctx context.Context, model *U3DModel, source pdfgo.ThreeD,
} }
} }
case "ANF": case "ANF":
minimum, maximum, found, err := r.depthRange() minimum, maximum, found, err := r.DepthRange()
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -292,11 +293,11 @@ func RenderPDFThreeD(ctx context.Context, model *U3DModel, source pdfgo.ThreeD,
default: default:
return nil, fmt.Errorf("invalid PDF 3D clipping style") return nil, fmt.Errorf("invalid PDF 3D clipping style")
} }
if _, err := r.selectView(render); err != nil { if _, err := r.SelectView(render); err != nil {
return nil, err return nil, err
} }
if source.Lighting.Subtype != "Artwork" && source.Lighting.Subtype != "" { if source.Lighting.Subtype != "Artwork" && source.Lighting.Subtype != "" {
if err := r.reserve(uint64(len(source.Lighting.Lights)), 56); err != nil { if err := r.Reserve(uint64(len(source.Lighting.Lights)), 56); err != nil {
return nil, err return nil, err
} }
render.Lighting = &U3DLighting{DiffuseOffset: source.Lighting.Ambient, Lights: make([]U3DDirectionalLight, len(source.Lighting.Lights))} render.Lighting = &U3DLighting{DiffuseOffset: source.Lighting.Ambient, Lights: make([]U3DDirectionalLight, len(source.Lighting.Lights))}
@@ -313,18 +314,18 @@ func RenderPDFThreeD(ctx context.Context, model *U3DModel, source pdfgo.ThreeD,
render.Lighting.Lights[i] = U3DDirectionalLight{Color: light.Color, Direction: direction, CameraRelative: light.CameraAttached} render.Lighting.Lights[i] = U3DDirectionalLight{Color: light.Color, Direction: direction, CameraRelative: light.CameraAttached}
} }
} }
return r.render(render, passes) return r.Render(render, passes)
} }
// pdfCamera 解析PDF相机来源并保留模型视图的绘制轮次 // pdfThreeDCamera 解析PDF相机来源并保留模型视图的绘制轮次
// 入参: source 相机来源 // 入参: ctx 取消上下文, model 三维模型, renderer 场景实例, source 相机来源
// 返回: U3DCamera 相机变换, string 视图节点, error 路径或变换错误 // 返回: U3DCamera 相机变换, string 视图节点, error 路径或变换错误
func (r *u3dRender) pdfCamera(source pdfgo.ThreeDCamera) (U3DCamera, string, error) { func pdfThreeDCamera(ctx context.Context, model *U3DModel, renderer *basethreed.Renderer, source pdfgo.ThreeDCamera) (U3DCamera, string, error) {
var camera U3DCamera var camera U3DCamera
selected := -1 selected := -1
for i, node := range r.model.Nodes { for i, node := range model.Nodes {
if i%256 == 0 { if i%256 == 0 {
if err := r.ctx.Err(); err != nil { if err := ctx.Err(); err != nil {
return camera, "", err return camera, "", err
} }
} }
@@ -342,9 +343,9 @@ func (r *u3dRender) pdfCamera(source pdfgo.ThreeDCamera) (U3DCamera, string, err
} }
if source.Source == "U3D" { if source.Source == "U3D" {
selected = -1 selected = -1
for i, node := range r.model.Nodes { for i, node := range model.Nodes {
if i%256 == 0 { if i%256 == 0 {
if err := r.ctx.Err(); err != nil { if err := ctx.Err(); err != nil {
return camera, "", err return camera, "", err
} }
} }
@@ -354,104 +355,28 @@ func (r *u3dRender) pdfCamera(source pdfgo.ThreeDCamera) (U3DCamera, string, err
} }
} }
} }
if selected < 0 || r.model.Nodes[selected].Kind != "View" { if selected < 0 || model.Nodes[selected].Kind != "View" {
return camera, "", fmt.Errorf("missing PDF 3D camera node") return camera, "", fmt.Errorf("missing PDF 3D camera node")
} }
found := false var err error
for _, index := range r.byNode[selected] { camera.ToWorld, err = renderer.ViewCamera(selected, source.Path, source.Source == "U3D")
if err := r.ctx.Err(); err != nil { if errors.Is(err, basethreed.ErrAmbiguousCamera) {
return camera, "", err
}
current, match := index, true
for j := len(source.Path) - 1; j >= 0; j-- {
if j%256 == 0 {
if err := r.ctx.Err(); err != nil {
return camera, "", err
}
}
if current < 0 || r.model.Nodes[r.scene[current].node].Name != source.Path[j] || r.model.Nodes[r.scene[current].node].Kind != "View" {
match = false
break
}
current = r.scene[current].parent
}
if source.Source == "U3D" && current >= 0 {
match = false
}
if !match {
continue
}
if found {
return camera, "", &pdfgo.UnsupportedError{Feature: "ambiguous 3D camera instance"} return camera, "", &pdfgo.UnsupportedError{Feature: "ambiguous 3D camera instance"}
} }
camera.ToWorld, found = r.scene[index].world, true if errors.Is(err, basethreed.ErrCameraNotFound) {
}
if !found {
return camera, "", fmt.Errorf("missing PDF 3D camera path") return camera, "", fmt.Errorf("missing PDF 3D camera path")
} }
if err != nil {
return camera, "", err
}
} }
view := "" view := ""
if selected >= 0 { if selected >= 0 {
view = r.model.Nodes[selected].Name view = model.Nodes[selected].Name
} }
return camera, view, nil return camera, view, nil
} }
// depthRange 计算可见模型几何的相机深度范围,不为自动裁剪复制顶点
// 返回: float64 最近深度, float64 最远深度, bool 是否含几何, error 几何或取消错误
func (r *u3dRender) depthRange() (float64, float64, bool, error) {
minimum, maximum := math.Inf(1), math.Inf(-1)
for i, instance := range r.scene {
if i%256 == 0 {
if err := r.ctx.Err(); err != nil {
return 0, 0, false, err
}
}
node := r.model.Nodes[instance.node]
index, ok := r.meshes[node.Resource]
if node.Kind != "Model" || instance.modelVisibility(&node) == 0 || instance.opacity == 0 || !ok {
continue
}
mesh := &r.model.Meshes[index]
matrix := r.toCamera.multiply(instance.world)
for j := range len(mesh.Faces) + len(mesh.Lines) + len(mesh.Points) {
_, corners := mesh.primitive(j)
if j%256 == 0 {
if err := r.ctx.Err(); err != nil {
return 0, 0, false, err
}
}
for _, corner := range corners {
if uint64(corner.Position) >= uint64(len(mesh.Positions)) {
return 0, 0, false, fmt.Errorf("invalid U3D position index")
}
p := mesh.Positions[corner.Position]
z := matrix[2]*float64(p[0]) + matrix[6]*float64(p[1]) + matrix[10]*float64(p[2]) + matrix[14]
if !finite(z) {
return 0, 0, false, fmt.Errorf("invalid U3D camera depth")
}
minimum, maximum = min(minimum, z), max(maximum, z)
}
}
}
for i, section := range r.sections {
if i%256 == 0 {
if err := r.ctx.Err(); err != nil {
return 0, 0, false, err
}
}
if section.opacity > 0 {
for _, corner := range section.corners {
minimum, maximum = min(minimum, corner.point[2]), max(maximum, corner.point[2])
}
}
}
if math.IsInf(minimum, 1) {
return 0, 0, false, nil
}
return minimum, maximum, true, nil
}
// pdfThreeDNodeStates 将PDF节点参数转为场景覆盖,转换缓存计入绘制预算 // pdfThreeDNodeStates 将PDF节点参数转为场景覆盖,转换缓存计入绘制预算
// 入参: ctx 取消上下文, nodes 节点参数, options 绘制选项 // 入参: ctx 取消上下文, nodes 节点参数, options 绘制选项
// 返回: error 预算或取消错误 // 返回: error 预算或取消错误
@@ -461,7 +386,7 @@ func pdfThreeDNodeStates(ctx context.Context, nodes []pdfgo.ThreeDNode, options
} }
limit := options.MaxMemoryBytes limit := options.MaxMemoryBytes
if limit == 0 { if limit == 0 {
limit = u3dDefaultBytes limit = basethreed.DefaultBytes
} }
if limit <= 0 || uint64(len(nodes)) >= uint64(limit)/168 { if limit <= 0 || uint64(len(nodes)) >= uint64(limit)/168 {
return fmt.Errorf("PDF 3D node memory exceeds limit") return fmt.Errorf("PDF 3D node memory exceeds limit")
+2 -1
View File
@@ -19,6 +19,7 @@ import (
"fmt" "fmt"
"math" "math"
basethreed "github.com/xiaoqidun/ofdgo/internal/base/threed"
"github.com/xiaoqidun/pdfgo" "github.com/xiaoqidun/pdfgo"
) )
@@ -31,7 +32,7 @@ func pdfThreeDSections(ctx context.Context, sections []pdfgo.ThreeDCrossSection,
} }
limit := options.MaxMemoryBytes limit := options.MaxMemoryBytes
if limit == 0 { if limit == 0 {
limit = u3dDefaultBytes limit = basethreed.DefaultBytes
} }
if limit <= 0 || uint64(len(sections)) >= uint64(limit)/136 { if limit <= 0 || uint64(len(sections)) >= uint64(limit)/136 {
return fmt.Errorf("PDF 3D section memory exceeds limit") return fmt.Errorf("PDF 3D section memory exceeds limit")
+8 -7
View File
@@ -19,6 +19,7 @@ import (
"crypto/sha256" "crypto/sha256"
"encoding/binary" "encoding/binary"
"fmt" "fmt"
basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom"
"image/color" "image/color"
"math" "math"
@@ -110,7 +111,7 @@ func (p *pdfImporter) compileCoverage(node pdfCompositeNode, stroke bool) (*Rast
path, err = pdfGeometryPath(p.ctx, node.path.Path, p.matrix) path, err = pdfGeometryPath(p.ctx, node.path.Path, p.matrix)
evenOdd = node.path.Path.EvenOdd evenOdd = node.path.Path.EvenOdd
} else { } else {
path, err = geometry.Transform(geometryRectangle(Box{W: 1, H: 1}), MatrixFromValues([6]float64(p.matrix.Mul(node.image.Matrix)))) path, err = geometry.Transform(GeometryPath(basegeom.Rectangle(Box{W: 1, H: 1})), MatrixFromValues([6]float64(p.matrix.Mul(node.image.Matrix))))
} }
if err != nil { if err != nil {
return nil, Box{}, err return nil, Box{}, err
@@ -118,7 +119,7 @@ func (p *pdfImporter) compileCoverage(node pdfCompositeNode, stroke bool) (*Rast
if len(path) == 0 { if len(path) == 0 {
return nil, Box{}, fmt.Errorf("empty PDF painted path") return nil, Box{}, fmt.Errorf("empty PDF painted path")
} }
path = closedGeometry(path) path = GeometryPath(basegeom.Close(basegeom.Path(path)))
clip, err := p.coverageClip(style.Clips, geometry) clip, err := p.coverageClip(style.Clips, geometry)
if err != nil || len(clip) == 0 { if err != nil || len(clip) == 0 {
return nil, Box{}, err return nil, Box{}, err
@@ -134,7 +135,7 @@ func (p *pdfImporter) compileCoverage(node pdfCompositeNode, stroke bool) (*Rast
bounds.add(pdfgo.Point{X: segment.Control2.X, Y: segment.Control2.Y}) bounds.add(pdfgo.Point{X: segment.Control2.X, Y: segment.Control2.Y})
} }
} }
boundary := geometryRectangle(bounds.box()) boundary := GeometryPath(basegeom.Rectangle(bounds.box()))
clip, err = clipGeometry(geometry, clip, &boundary) clip, err = clipGeometry(geometry, clip, &boundary)
if err != nil { if err != nil {
return nil, Box{}, err return nil, Box{}, err
@@ -149,7 +150,7 @@ func (p *pdfImporter) compileCoverage(node pdfCompositeNode, stroke bool) (*Rast
if err != nil { if err != nil {
return nil, Box{}, err return nil, Box{}, err
} }
rect, rectangle := geometryRectangleBounds(clip) rect, rectangle := basegeom.RectangleBounds(basegeom.Path(clip))
if !rectangle || box.X < rect.X || box.Y < rect.Y || box.X+box.W > rect.X+rect.W || box.Y+box.H > rect.Y+rect.H { if !rectangle || box.X < rect.X || box.Y < rect.Y || box.X+box.W > rect.X+rect.W || box.Y+box.H > rect.Y+rect.H {
if evenOdd { if evenOdd {
path, err = geometry.Normalize(path, true) path, err = geometry.Normalize(path, true)
@@ -189,7 +190,7 @@ func (p *pdfImporter) coverageClip(paths []pdfgo.Path, geometry GeometryBackend)
if err := p.ctx.Err(); err != nil { if err := p.ctx.Err(); err != nil {
return nil, err return nil, err
} }
clip := geometryRectangle(Box{W: p.pageWidth, H: p.pageHeight}) clip := GeometryPath(basegeom.Rectangle(Box{W: p.pageWidth, H: p.pageHeight}))
if len(paths) == 0 { if len(paths) == 0 {
return clip, nil return clip, nil
} }
@@ -216,8 +217,8 @@ func (p *pdfImporter) coverageClip(paths []pdfgo.Path, geometry GeometryBackend)
if err != nil { if err != nil {
return nil, err return nil, err
} }
part = closedGeometry(part) part = GeometryPath(basegeom.Close(basegeom.Path(part)))
if _, rectangle := geometryRectangleBounds(part); source.EvenOdd && !rectangle { if _, rectangle := basegeom.RectangleBounds(basegeom.Path(part)); source.EvenOdd && !rectangle {
part, err = geometry.Normalize(part, true) part, err = geometry.Normalize(part, true)
if err != nil { if err != nil {
return nil, err return nil, err
+1 -1
View File
@@ -545,7 +545,7 @@ func (p *pdfImporter) text(mark pdfgo.TextMark) error {
} }
lineWidth := mark.Style.LineWidth * math.Hypot(p.matrix[0], p.matrix[1]) * sx lineWidth := mark.Style.LineWidth * math.Hypot(p.matrix[0], p.matrix[1]) * sx
margin := lineWidth / 2 * math.Max(1, mark.Style.MiterLimit) margin := lineWidth / 2 * math.Max(1, mark.Style.MiterLimit)
box = Box{box.X - margin, box.Y - margin, box.W + 2*margin, box.H + 2*margin} box = Box{X: box.X - margin, Y: box.Y - margin, W: box.W + 2*margin, H: box.H + 2*margin}
object.LineWidth = lineWidth / scale object.LineWidth = lineWidth / scale
object.LineWidthSet = object.LineWidth == 0 object.LineWidthSet = object.LineWidth == 0
object.Cap = []string{"Butt", "Round", "Square"}[mark.Style.Cap] object.Cap = []string{"Butt", "Round", "Square"}[mark.Style.Cap]
+2 -1
View File
@@ -16,6 +16,7 @@ package ofdgo
import ( import (
"fmt" "fmt"
basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom"
"image" "image"
"math" "math"
@@ -373,7 +374,7 @@ func pdfRadialBackground(gradient *pdfgo.RadialGradient, matrix pdfgo.Matrix, bo
} }
var circles, path GeometryPath var circles, path GeometryPath
if !extend[1] { if !extend[1] {
path = geometryRectangle(Box{W: box.W, H: box.H}) path = GeometryPath(basegeom.Rectangle(Box{W: box.W, H: box.H}))
circles = geometryCircle(RasterPoint{X: outer.X, Y: outer.Y}, r1) circles = geometryCircle(RasterPoint{X: outer.X, Y: outer.Y}, r1)
} }
if !extend[0] && r0 > 0 { if !extend[0] && r0 > 0 {
+2 -2
View File
@@ -35,8 +35,8 @@ func pdfGlyphPath(outline GeometryPath, matrix pdfgo.Matrix) (pdfgo.Path, error)
case GeometryLine: case GeometryLine:
path.Segments = append(path.Segments, pdfgo.Segment{Operator: "L", Points: []pdfgo.Point{point(segment.End)}}) path.Segments = append(path.Segments, pdfgo.Segment{Operator: "L", Points: []pdfgo.Point{point(segment.End)}})
case GeometryQuad: case GeometryQuad:
c1 := Point{current.X + (segment.Control1.X-current.X)*2/3, current.Y + (segment.Control1.Y-current.Y)*2/3} c1 := Point{X: current.X + (segment.Control1.X-current.X)*2/3, Y: current.Y + (segment.Control1.Y-current.Y)*2/3}
c2 := Point{segment.End.X + (segment.Control1.X-segment.End.X)*2/3, segment.End.Y + (segment.Control1.Y-segment.End.Y)*2/3} c2 := Point{X: segment.End.X + (segment.Control1.X-segment.End.X)*2/3, Y: segment.End.Y + (segment.Control1.Y-segment.End.Y)*2/3}
path.Segments = append(path.Segments, pdfgo.Segment{Operator: "B", Points: []pdfgo.Point{point(c1), point(c2), point(segment.End)}}) path.Segments = append(path.Segments, pdfgo.Segment{Operator: "B", Points: []pdfgo.Point{point(c1), point(c2), point(segment.End)}})
case GeometryCubic: case GeometryCubic:
path.Segments = append(path.Segments, pdfgo.Segment{Operator: "B", Points: []pdfgo.Point{point(segment.Control1), point(segment.Control2), point(segment.End)}}) path.Segments = append(path.Segments, pdfgo.Segment{Operator: "B", Points: []pdfgo.Point{point(segment.Control1), point(segment.Control2), point(segment.End)}})
+3 -2
View File
@@ -17,6 +17,7 @@ package ofdgo
import ( import (
"context" "context"
"fmt" "fmt"
basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom"
) )
// Bounds 度量已编译指令的页面坐标范围,不重新定位文字或读取图片像素 // Bounds 度量已编译指令的页面坐标范围,不重新定位文字或读取图片像素
@@ -74,7 +75,7 @@ func rasterCommandGeometry(geometry GeometryBackend, command RasterCommand) (Geo
if bounds.Empty() { if bounds.Empty() {
return nil, nil return nil, nil
} }
path = geometryRectangle(Box{X: float64(bounds.Min.X), Y: float64(bounds.Min.Y), W: float64(bounds.Dx()), H: float64(bounds.Dy())}) path = GeometryPath(basegeom.Rectangle(Box{X: float64(bounds.Min.X), Y: float64(bounds.Min.Y), W: float64(bounds.Dx()), H: float64(bounds.Dy())}))
} else { } else {
if len(command.Path) == 0 || command.Paint.Gradient == nil && command.Paint.Color.A == 0 { if len(command.Path) == 0 || command.Paint.Gradient == nil && command.Paint.Color.A == 0 {
return nil, nil return nil, nil
@@ -92,7 +93,7 @@ func rasterCommandGeometry(geometry GeometryBackend, command RasterCommand) (Geo
return nil, err return nil, err
} }
if command.Image == nil && command.Stroke != nil { if command.Image == nil && command.Stroke != nil {
if err := validateStroke(*command.Stroke); err != nil { if err := basegeom.ValidateStroke(*command.Stroke); err != nil {
return nil, err return nil, err
} }
path, err = geometry.Stroke(path, *command.Stroke) path, err = geometry.Stroke(path, *command.Stroke)
+3 -2
View File
@@ -16,6 +16,7 @@ package ofdgo
import ( import (
"fmt" "fmt"
basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom"
"math" "math"
"strconv" "strconv"
) )
@@ -127,7 +128,7 @@ func (c *semanticCompiler) addBounds(path GeometryPath, evenOdd bool) error {
} }
c.measurement.Bounds = unionTextBox(c.measurement.Bounds, box) c.measurement.Bounds = unionTextBox(c.measurement.Bounds, box)
if c.contours { if c.contours {
svg, err := closedGeometry(path).SVG() svg, err := GeometryPath(basegeom.Close(basegeom.Path(path))).SVG()
if err != nil { if err != nil {
return err return err
} }
@@ -143,7 +144,7 @@ func (c *semanticCompiler) fill(path GeometryPath, paint Paint, evenOdd bool, cl
if len(path) == 0 || paint.Kind == PaintNone || paint.Kind == PaintSolid && paint.Color.A == 0 { if len(path) == 0 || paint.Kind == PaintNone || paint.Kind == PaintSolid && paint.Color.A == 0 {
return nil return nil
} }
path = closedGeometry(path) path = GeometryPath(basegeom.Close(basegeom.Path(path)))
if !c.measure && paint.Kind != PaintPattern { if !c.measure && paint.Kind != PaintPattern {
return c.command(path, paint, evenOdd, clip, shadingMatrix, nil) return c.command(path, paint, evenOdd, clip, shadingMatrix, nil)
} }
+7 -6
View File
@@ -18,6 +18,7 @@ import (
"bytes" "bytes"
"context" "context"
"fmt" "fmt"
basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom"
"image" "image"
"math" "math"
"slices" "slices"
@@ -50,7 +51,7 @@ func (c *semanticCompiler) imageObject(object ImageObject, state RenderState) er
return err return err
} }
if c.measure { if c.measure {
path, err := c.geometry.Transform(geometryRectangle(Box{W: 1, H: 1}), matrix) path, err := c.geometry.Transform(GeometryPath(basegeom.Rectangle(Box{W: 1, H: 1})), matrix)
if err != nil { if err != nil {
return err return err
} }
@@ -117,7 +118,7 @@ func imageBorderObject(object ImageObject, box Box) PathObject {
func (c *semanticCompiler) addImage(img image.Image, matrix Matrix, clip *GeometryPath) error { func (c *semanticCompiler) addImage(img image.Image, matrix Matrix, clip *GeometryPath) error {
command := RasterCommand{Image: img, Transform: RasterMatrix(matrix.Values())} command := RasterCommand{Image: img, Transform: RasterMatrix(matrix.Values())}
if clip != nil { if clip != nil {
if box, ok := geometryRectangleBounds(*clip); ok { if box, ok := basegeom.RectangleBounds(basegeom.Path(*clip)); ok {
bounds := img.Bounds() bounds := img.Bounds()
visible := matrix.TransformBox(Box{X: float64(bounds.Min.X), Y: float64(bounds.Min.Y), W: float64(bounds.Dx()), H: float64(bounds.Dy())}) visible := matrix.TransformBox(Box{X: float64(bounds.Min.X), Y: float64(bounds.Min.Y), W: float64(bounds.Dx()), H: float64(bounds.Dy())})
if box.X <= visible.X && box.Y <= visible.Y && visible.X+visible.W <= box.X+box.W && visible.Y+visible.H <= box.Y+box.H { if box.X <= visible.X && box.Y <= visible.Y && visible.X+visible.W <= box.X+box.W && visible.Y+visible.H <= box.Y+box.H {
@@ -125,11 +126,11 @@ func (c *semanticCompiler) addImage(img image.Image, matrix Matrix, clip *Geomet
} }
} else { } else {
bounds := img.Bounds() bounds := img.Bounds()
outline, err := geometryRectangle(Box{X: float64(bounds.Min.X), Y: float64(bounds.Min.Y), W: float64(bounds.Dx()), H: float64(bounds.Dy())}).Transform(matrix) outline, err := GeometryPath(basegeom.Rectangle(Box{X: float64(bounds.Min.X), Y: float64(bounds.Min.Y), W: float64(bounds.Dx()), H: float64(bounds.Dy())})).Transform(matrix)
if err != nil { if err != nil {
return err return err
} }
if geometryConvexContains(*clip, outline) { if basegeom.ConvexContains(basegeom.Path(*clip), basegeom.Path(outline)) {
clip = nil clip = nil
} }
} }
@@ -184,7 +185,7 @@ func (c *semanticCompiler) drawStampContent(stamp Stamp) error {
var clip GeometryPath var clip GeometryPath
var segments []RasterSegment var segments []RasterSegment
if stamp.Clip != nil { if stamp.Clip != nil {
clip = geometryRectangle(Box{X: stamp.Box.X + stamp.Clip.X, Y: stamp.Box.Y + stamp.Clip.Y, W: stamp.Clip.W, H: stamp.Clip.H}) clip = GeometryPath(basegeom.Rectangle(Box{X: stamp.Box.X + stamp.Clip.X, Y: stamp.Box.Y + stamp.Clip.Y, W: stamp.Clip.W, H: stamp.Clip.H}))
segments, err = c.segments(clip) segments, err = c.segments(clip)
if err != nil { if err != nil {
return err return err
@@ -248,7 +249,7 @@ func (c *semanticCompiler) stampImage(img image.Image, stamp Stamp) error {
box := stamp.Box box := stamp.Box
var clip *GeometryPath var clip *GeometryPath
if stamp.Clip != nil { if stamp.Clip != nil {
path := geometryRectangle(Box{X: box.X + stamp.Clip.X, Y: box.Y + stamp.Clip.Y, W: stamp.Clip.W, H: stamp.Clip.H}) path := GeometryPath(basegeom.Rectangle(Box{X: box.X + stamp.Clip.X, Y: box.Y + stamp.Clip.Y, W: stamp.Clip.W, H: stamp.Clip.H}))
clip = &path clip = &path
} }
bounds := img.Bounds() bounds := img.Bounds()
+1 -1
View File
@@ -242,7 +242,7 @@ func geometryFromRaster(path []RasterSegment) GeometryPath {
if s.Verb == RasterClose { if s.Verb == RasterClose {
verb = GeometryClose verb = GeometryClose
} }
result = append(result, GeometrySegment{Verb: verb, End: Point{s.End.X, s.End.Y}, Control1: Point{s.Control1.X, s.Control1.Y}, Control2: Point{s.Control2.X, s.Control2.Y}}) result = append(result, GeometrySegment{Verb: verb, End: Point{X: s.End.X, Y: s.End.Y}, Control1: Point{X: s.Control1.X, Y: s.Control1.Y}, Control2: Point{X: s.Control2.X, Y: s.Control2.Y}})
} }
return result return result
} }
+5 -2
View File
@@ -14,7 +14,10 @@
package ofdgo package ofdgo
import "math" import (
basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom"
"math"
)
// reflectShdPosition 获取镜像周期内的位置 // reflectShdPosition 获取镜像周期内的位置
// 入参: position 渐变位置 // 入参: position 渐变位置
@@ -68,7 +71,7 @@ func (c *semanticCompiler) shadingClip(path GeometryPath, paint Paint, matrix Ma
if box.W <= 0 { if box.W <= 0 {
return nil, nil return nil, nil
} }
clip, err := c.geometry.Transform(geometryRectangle(box), axis) clip, err := c.geometry.Transform(GeometryPath(basegeom.Rectangle(box)), axis)
if err != nil { if err != nil {
return nil, err return nil, err
} }
+3 -2
View File
@@ -16,6 +16,7 @@ package ofdgo
import ( import (
"fmt" "fmt"
basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom"
"math" "math"
"strings" "strings"
) )
@@ -101,7 +102,7 @@ func (r *Renderer) positionText(object TextObject, state RenderState, contentOnl
} }
box, _ := ParseBox(object.Boundary) box, _ := ParseBox(object.Boundary)
boundary, _ := renderObjectMatrix(object.Boundary, IdentityMatrix, state) boundary, _ := renderObjectMatrix(object.Boundary, IdentityMatrix, state)
path, err := geometry.Transform(geometryRectangle(Box{W: box.W, H: box.H}), boundary) path, err := geometry.Transform(GeometryPath(basegeom.Rectangle(Box{W: box.W, H: box.H})), boundary)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -430,7 +431,7 @@ func (run *TextRun) addGeometrySpan(geometry GeometryBackend, start, end int, bo
return nil return nil
} }
if clip != nil { if clip != nil {
path, err := geometry.Transform(geometryRectangle(bounds), matrix) path, err := geometry.Transform(GeometryPath(basegeom.Rectangle(bounds)), matrix)
if err != nil { if err != nil {
return err return err
} }
+5 -2
View File
@@ -14,7 +14,10 @@
package ofdgo package ofdgo
import "fmt" import (
"fmt"
basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom"
)
// RenderState 保存对象继承的样式、父变换和页面坐标裁剪,所有引用只读 // RenderState 保存对象继承的样式、父变换和页面坐标裁剪,所有引用只读
// Clip为nil表示未提供裁剪,空路径表示完全裁去,单位为毫米 // Clip为nil表示未提供裁剪,空路径表示完全裁去,单位为毫米
@@ -344,7 +347,7 @@ func (r *Renderer) geometryExtentClip(width, height float64, matrix Matrix, pare
if err != nil { if err != nil {
return nil, err return nil, err
} }
clip, err := geometry.Transform(geometryRectangle(Box{W: width, H: height}), matrix) clip, err := geometry.Transform(GeometryPath(basegeom.Rectangle(Box{W: width, H: height})), matrix)
if err != nil { if err != nil {
return nil, err return nil, err
} }
+2 -1
View File
@@ -16,6 +16,7 @@ package ofdgo
import ( import (
"fmt" "fmt"
basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom"
"image" "image"
"image/color" "image/color"
"image/draw" "image/draw"
@@ -165,7 +166,7 @@ func (b CanvasBackend) renderImage(page *RasterPage, img *image.RGBA) (image.Ima
m := cmd.Transform m := cmd.Transform
if cmd.Stroke != nil { if cmd.Stroke != nil {
options := *cmd.Stroke options := *cmd.Stroke
if err := validateStroke(options); err != nil { if err := basegeom.ValidateStroke(options); err != nil {
return nil, err return nil, err
} }
p = p.Transform(canvas.Matrix{{m[0], m[2], m[4]}, {m[1], m[3], m[5]}}) p = p.Transform(canvas.Matrix{{m[0], m[2], m[4]}, {m[1], m[3], m[5]}})
+2 -1
View File
@@ -15,6 +15,7 @@
package ofdgo package ofdgo
import ( import (
basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom"
"math" "math"
"github.com/tdewolff/canvas" "github.com/tdewolff/canvas"
@@ -133,7 +134,7 @@ func intersectCanvasPolygons(a, b []canvas.Point) (*canvas.Path, bool) {
if (d >= 0) != (previousDistance >= 0) { if (d >= 0) != (previousDistance >= 0) {
scale := math.Max(math.Abs(d), math.Abs(previousDistance)) scale := math.Max(math.Abs(d), math.Abs(previousDistance))
fraction := (previousDistance / scale) / (previousDistance/scale - d/scale) fraction := (previousDistance / scale) / (previousDistance/scale - d/scale)
intersection := geometryLerp(Point{previous.X, previous.Y}, Point{p.X, p.Y}, fraction) intersection := basegeom.Lerp(Point{X: previous.X, Y: previous.Y}, Point{X: p.X, Y: p.Y}, fraction)
if dx == 0 { if dx == 0 {
intersection.X = start.X intersection.X = start.X
} }
+5 -2
View File
@@ -14,7 +14,10 @@
package ofdgo package ofdgo
import "image/color" import (
basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom"
"image/color"
)
// OFDCompiler 使用库自有页面语义,字体和几何操作交给当前后端配置 // OFDCompiler 使用库自有页面语义,字体和几何操作交给当前后端配置
type OFDCompiler struct{} type OFDCompiler struct{}
@@ -40,7 +43,7 @@ func (OFDCompiler) CompilePage(r *Renderer, page *PageContent) (*RasterPage, err
return nil, err return nil, err
} }
if !r.TransparentBackground { if !r.TransparentBackground {
if err := c.fill(geometryRectangle(Box{W: box.W, H: box.H}), Paint{Kind: PaintSolid, Color: color.RGBA{255, 255, 255, 255}}, false, nil, IdentityMatrix, IdentityMatrix); err != nil { if err := c.fill(GeometryPath(basegeom.Rectangle(Box{W: box.W, H: box.H})), Paint{Kind: PaintSolid, Color: color.RGBA{255, 255, 255, 255}}, false, nil, IdentityMatrix, IdentityMatrix); err != nil {
return nil, err return nil, err
} }
} }
+4 -3
View File
@@ -16,6 +16,7 @@ package ofdgo
import ( import (
"fmt" "fmt"
basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom"
"math" "math"
"slices" "slices"
"strconv" "strconv"
@@ -88,7 +89,7 @@ func (CanvasBackend) Transform(path GeometryPath, matrix Matrix) (GeometryPath,
return nil, fmt.Errorf("invalid geometry transform") return nil, fmt.Errorf("invalid geometry transform")
} }
} }
if geometryEqual(m[0]*m[3]-m[1]*m[2], 0) { if basegeom.Equal(m[0]*m[3]-m[1]*m[2], 0) {
p = p.ReplaceArcs() p = p.ReplaceArcs()
} }
p = p.Transform(canvas.Matrix{{m[0], -m[2], m[4]}, {-m[1], m[3], -m[5]}}) p = p.Transform(canvas.Matrix{{m[0], -m[2], m[4]}, {-m[1], m[3], -m[5]}})
@@ -149,7 +150,7 @@ func (CanvasBackend) Combine(left, right GeometryPath, operation GeometryOperati
// 入参: path 页面路径, options 描边样式 // 入参: path 页面路径, options 描边样式
// 返回: GeometryPath 描边区域, error 样式或路径错误 // 返回: GeometryPath 描边区域, error 样式或路径错误
func (CanvasBackend) Stroke(path GeometryPath, options StrokeOptions) (result GeometryPath, err error) { func (CanvasBackend) Stroke(path GeometryPath, options StrokeOptions) (result GeometryPath, err error) {
if err := validateStroke(options); err != nil { if err := basegeom.ValidateStroke(options); err != nil {
return nil, err return nil, err
} }
if slices.Contains(options.Dashes, 0) { if slices.Contains(options.Dashes, 0) {
@@ -248,7 +249,7 @@ func canvasStrokeFillPath(geometry GeometryBackend, path *canvas.Path, options S
if options.Tolerance == 0 { if options.Tolerance == 0 {
options.Tolerance = canvas.Tolerance options.Tolerance = canvas.Tolerance
} }
if err := validateStroke(options); err != nil { if err := basegeom.ValidateStroke(options); err != nil {
return nil, err return nil, err
} }
if len(path.Dash(options.DashOffset, options.Dashes...).Data()) > 256 { if len(path.Dash(options.DashOffset, options.Dashes...).Data()) > 256 {
+2 -1
View File
@@ -18,6 +18,7 @@ import (
"bytes" "bytes"
"context" "context"
"fmt" "fmt"
basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom"
"image" "image"
"math" "math"
@@ -177,7 +178,7 @@ func imageClipContains(img image.Image, clipPath *canvas.Path, m canvas.Matrix)
if rect, ok := rectangularPath(clipPath); ok { if rect, ok := rectangularPath(clipPath); ok {
return rect.Contains(outline.FastBounds()) return rect.Contains(outline.FastBounds())
} }
if geometryConvexContains(*geometryFromCanvasPath(clipPath), *geometryFromCanvasPath(outline)) { if basegeom.ConvexContains(basegeom.Path(*geometryFromCanvasPath(clipPath)), basegeom.Path(*geometryFromCanvasPath(outline))) {
return true return true
} }
return clipPath.Contains(outline) return clipPath.Contains(outline)
+2 -1
View File
@@ -17,6 +17,7 @@ package ofdgo
import ( import (
"context" "context"
"fmt" "fmt"
basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom"
"slices" "slices"
"github.com/tdewolff/canvas" "github.com/tdewolff/canvas"
@@ -56,7 +57,7 @@ func (n *pdfNavigation) addActions(renderer *Renderer, page int, source actionSo
if value == nil { if value == nil {
continue continue
} }
path := geometryRectangle(box) path := GeometryPath(basegeom.Rectangle(box))
if outline != "" { if outline != "" {
parsed, err := canvas.ParseSVGPath(outline) parsed, err := canvas.ParseSVGPath(outline)
if err != nil { if err != nil {
+5 -4
View File
@@ -17,6 +17,7 @@ package ofdgo
import ( import (
"context" "context"
"fmt" "fmt"
basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom"
"math" "math"
"slices" "slices"
@@ -60,7 +61,7 @@ func pdfActionRegion(ctx context.Context, outline string, pageH float64) (*pdfgo
if path.Len() > pdfLinkEdgeLimit { if path.Len() > pdfLinkEdgeLimit {
return nil, fmt.Errorf("PDF link region exceeds edge limit") return nil, fmt.Errorf("PDF link region exceeds edge limit")
} }
if _, ok := geometryRectangleBounds(*geometryFromCanvasPath(path)); ok { if _, ok := basegeom.RectangleBounds(basegeom.Path(*geometryFromCanvasPath(path))); ok {
return nil, nil return nil, nil
} }
path, err = flattenPDFLink(ctx, path) path, err = flattenPDFLink(ctx, path)
@@ -279,7 +280,7 @@ func flattenPDFLink(ctx context.Context, path *canvas.Path) (*canvas.Path, error
for n := len(points); n > 0; n-- { for n := len(points); n > 0; n-- {
left[len(points)-n], right[n-1] = work[0], work[n-1] left[len(points)-n], right[n-1] = work[0], work[n-1]
for i := 0; i < n-1; i++ { for i := 0; i < n-1; i++ {
work[i] = geometryLerp(work[i], work[i+1], .5) work[i] = basegeom.Lerp(work[i], work[i+1], .5)
} }
} }
if err := flatten(left[:len(points)], depth+1); err != nil { if err := flatten(left[:len(points)], depth+1); err != nil {
@@ -299,10 +300,10 @@ func flattenPDFLink(ctx context.Context, path *canvas.Path) (*canvas.Path, error
result.Close() result.Close()
current = start current = start
default: default:
curves := GeometryPath{segment} curves := basegeom.Path{segment}
if segment.Verb == GeometryArc { if segment.Verb == GeometryArc {
var err error var err error
curves, err = arcCurves(current, segment, pdfLinkTolerance/2) curves, err = basegeom.ArcCurves(current, segment, pdfLinkTolerance/2)
if err != nil { if err != nil {
return nil, err return nil, err
} }
+4 -3
View File
@@ -16,6 +16,7 @@ package ofdgo
import ( import (
"fmt" "fmt"
basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom"
"math" "math"
"strconv" "strconv"
"strings" "strings"
@@ -157,12 +158,12 @@ func (p PathObject) Shape() (ShapeKind, Box) {
boundary, err := ParseBox(p.Boundary) boundary, err := ParseBox(p.Boundary)
m := NewMatrix(p.CTM) m := NewMatrix(p.CTM)
_, invertible := m.Invert() _, invertible := m.Invert()
if err != nil || !axisAlignedMatrix(m) && (!line || !invertible) { if err != nil || !basegeom.AxisAligned(basegeom.MatrixFromValues(m.Values())) && (!line || !invertible) {
return "", Box{} return "", Box{}
} }
if line { if line {
x, y := m.Transform(box.X, box.Y) x, y := m.Transform(box.X, box.Y)
w, h := matrixVector(m, box.W, box.H) w, h := basegeom.Vector(basegeom.MatrixFromValues(m.Values()), box.W, box.H)
box = Box{X: boundary.X + x, Y: boundary.Y + y, W: w, H: h} box = Box{X: boundary.X + x, Y: boundary.Y + y, W: w, H: h}
} else { } else {
box = TranslationMatrix(boundary.X, boundary.Y).Multiply(m).TransformBox(box) box = TranslationMatrix(boundary.X, boundary.Y).Multiply(m).TransformBox(box)
@@ -293,7 +294,7 @@ func (p PathObject) reshape(kind ShapeKind, box Box) (PathObject, error) {
w, h = box.H/m.b, box.W/m.c w, h = box.H/m.b, box.W/m.c
} else { } else {
inverse, _ := m.Invert() inverse, _ := m.Invert()
w, h = matrixVector(inverse, box.W, box.H) w, h = basegeom.Vector(basegeom.MatrixFromValues(inverse.Values()), box.W, box.H)
} }
if !line { if !line {
w, h = math.Abs(w), math.Abs(h) w, h = math.Abs(w), math.Abs(h)
+173 -631
View File
@@ -15,108 +15,185 @@
package ofdgo package ofdgo
import ( import (
"bytes"
"context" "context"
"encoding/binary" "image"
"fmt"
"io" basethreed "github.com/xiaoqidun/ofdgo/internal/base/threed"
"math"
) )
// u3dDefaultBytes 限制默认解码数据规模,不包括调用方已有输入及运行时开销 const (
const u3dDefaultBytes = 256 << 20 U3DRenderSolid = basethreed.U3DRenderSolid // 实体
U3DRenderSolidWireframe = basethreed.U3DRenderSolidWireframe // 实体线框
U3DRenderWireframe = basethreed.U3DRenderWireframe // 线框
U3DRenderShadedWireframe = basethreed.U3DRenderShadedWireframe // 着色线框
U3DRenderHiddenWireframe = basethreed.U3DRenderHiddenWireframe // 隐藏线框
U3DRenderVertices = basethreed.U3DRenderVertices // 顶点
U3DRenderShadedVertices = basethreed.U3DRenderShadedVertices // 着色顶点
U3DRenderBoundingBox = basethreed.U3DRenderBoundingBox // 包围盒线框
U3DRenderBoundingBoxFaces = basethreed.U3DRenderBoundingBoxFaces // 包围盒面
U3DRenderBoundingBoxOutline = basethreed.U3DRenderBoundingBoxOutline // 包围盒面及线框
U3DRenderIllustration = basethreed.U3DRenderIllustration // 插图
U3DRenderSolidOutline = basethreed.U3DRenderSolidOutline // 实体轮廓
U3DRenderShadedIllustration = basethreed.U3DRenderShadedIllustration // 着色插图
)
// U3DOptions 控制U3D解码规模,MaxDecodedBytes为0时采用256MiB上限 // U3DOptions 控制U3D解码规模,MaxDecodedBytes为0时采用256MiB上限
type U3DOptions struct { type U3DOptions = basethreed.U3DOptions
MaxDecodedBytes int64
}
// U3DModel 保存独立于PDF的U3D场景及动作数据,不执行动作或读取外部文件 // U3DModel 保存独立于PDF的U3D场景及动作数据,不执行动作或读取外部文件
type U3DModel struct { type U3DModel = basethreed.U3DModel
Major, Minor int16
DefinedUnits bool
Units float64
Meshes []U3DMesh
Nodes []U3DNode
Materials []U3DMaterial
Shaders []U3DShader
Lights []U3DLight
Views []U3DViewResource
Textures []U3DTexture
Motions []U3DMotion
Metadata []U3DMetadata
}
// U3DMetadata 保留块所属名称与原始元数据,数据不与输入共享存储 // U3DMetadata 保留块所属名称与原始元数据,数据不与输入共享存储
type U3DMetadata struct { type U3DMetadata = basethreed.U3DMetadata
Block uint32
Name string
Data []byte
}
// U3DNode 保存组、模型、光源或视图节点,父节点矩阵按列存储 // U3DNode 保存组、模型、光源或视图节点,父节点矩阵按列存储
type U3DNode struct { type U3DNode = basethreed.U3DNode
Name, Kind, Resource string
Parents []U3DParent
Visibility uint32
Shaders [][]string
LineShaders [][]string
PointShaders [][]string
View *U3DView
BoneWeights []U3DBoneWeights
Animations []U3DAnimation
}
// U3DParent 保存父节点名称及局部到父节点的变换,空名称引用默认节点 // U3DParent 保存父节点名称及局部到父节点的变换,空名称引用默认节点
type U3DParent struct { type U3DParent = basethreed.U3DParent
Name string
Transform [16]float32
}
// U3DMaterial 保存材质参数,Attributes各位决定相应参数是否生效 // U3DMaterial 保存材质参数,Attributes各位决定相应参数是否生效
type U3DMaterial struct { type U3DMaterial = basethreed.U3DMaterial
Name string
Attributes uint32
Ambient, Diffuse, Specular, Emissive [3]float32
Reflectivity, Opacity float32
}
// U3DShader 保存材质与八个纹理通道,Textures中的空项表示未启用 // U3DShader 保存材质与八个纹理通道,Textures中的空项表示未启用
type U3DShader struct { type U3DShader = basethreed.U3DShader
Name, Material string
Attributes, AlphaFunction, Blend uint32
RenderPass uint32
AlphaReference float32
Textures [8]*U3DTextureLayer
}
// U3DLight 保存环境、方向、点或聚光资源,Kind依次为0至3 // U3DLight 保存环境、方向、点或聚光资源,Kind依次为0至3
type U3DLight struct { type U3DLight = basethreed.U3DLight
Name string
Attributes uint32
Kind byte
Color, Attenuation [3]float32
SpotAngle, Intensity float32
}
// u3dBlock 保存校验后的块边界,数据切片仅在解码期间引用输入 // U3DAnimation 保存ECMA-363第9.7.3节的动画修饰器,Index为链内位置
type u3dBlock struct { // Attributes的1、2、4、8分别表示启动、锁定根骨骼、单轨播放和动作间平滑过渡
kind uint32 // TimeScale为整体时间倍率,BlendTime以毫秒计;Motions为空时使用无轨道的默认动作
data, meta []byte type U3DAnimation = basethreed.U3DAnimation
}
// u3dDecoder 保存单次解码预算及场景索引,不跨调用缓存输入 // U3DAnimationMotion 保存动画中的动作引用,空名称引用默认动作
type u3dDecoder struct { // Attributes的1、2分别表示循环和同步结束,TimeOffset以毫秒计,TimeScale为动作时间倍率
ctx context.Context type U3DAnimationMotion = basethreed.U3DAnimationMotion
model U3DModel
remaining uint64 // U3DDirectionalLight 定义无限远光源,Direction为发射方向,CameraRelative使用相机坐标
plain bool type U3DDirectionalLight = basethreed.U3DDirectionalLight
meshes map[string]*u3dMeshDeclaration
meshOrder []string // U3DLighting 替换场景灯光,空灯光列表表示无光源,DiffuseOffset加到受光材质的漫反射颜色
nodes map[string]int // 颜色及漫反射增量的各分量取值为0到1;不修改材质自发光或不受光着色器
palettes map[uint32]map[string]bool type U3DLighting = basethreed.U3DLighting
textures map[string]int
} // U3DMesh 保存静态三角面、线段和点的共享属性数组,索引对应原始作者几何
type U3DMesh = basethreed.U3DMesh
// U3DShading 描述图元使用的颜色及纹理坐标,纹理维数依层排列
type U3DShading = basethreed.U3DShading
// U3DFace 保存着色描述索引和依原始绕序排列的三个角点
type U3DFace = basethreed.U3DFace
// U3DLine 保存着色描述及线段两端的独立属性索引
type U3DLine = basethreed.U3DLine
// U3DPoint 保存点集着色描述及独立属性索引
type U3DPoint = basethreed.U3DPoint
// U3DCorner 保存各属性索引,未启用的属性不可用于访问数组
type U3DCorner = basethreed.U3DCorner
// U3DRenderMode 定义静态几何的呈现方式,边线和顶点使用单像素覆盖
type U3DRenderMode = basethreed.U3DRenderMode
// U3DMotion 保存骨骼或节点动作轨道,解码不执行动画
type U3DMotion = basethreed.U3DMotion
// U3DMotionTrack 保存命名轨道及其关键帧,保持文件中的时间与帧次序
type U3DMotionTrack = basethreed.U3DMotionTrack
// U3DKeyFrame 保存关键帧时间及相对父项的变换,Rotation按W、X、Y、Z排列
type U3DKeyFrame = basethreed.U3DKeyFrame
// U3DCamera 定义沿正Z方向观察的相机,ScaleX和ScaleY为投影到像素的倍率
// ToWorld按列存储,全零表示单位矩阵;Far为0且ClipFar为假时不设置远裁剪面
// ShiftX和ShiftY为投影原点相对视口中心的像素偏移,向右及向下为正
type U3DCamera = basethreed.U3DCamera
// U3DRenderOptions 控制静态绘制,指定Camera时使用整个输出视口
// Camera为空时使用View指定的节点或首个视图节点,Instance指定其父路径实例
// Lighting为空时沿用模型灯光,非空时替换全部模型灯光
// Opacity为空时沿用着色器混合,非空时叠加面片、着色边点及包围盒轮廓的透明度
// 纹理合成后先应用节点透明度,再应用Opacity
// Mode控制绘制方式,AuxiliaryColor用于固定颜色的边点,FaceColor用于包围盒面及插图面,空值分别使用黑色和背景色
// CreaseAngle为轮廓夹角,单位为度,空值使用45;着色插图以漫反射色的1/4补充自发光
// Nodes按名称覆盖节点,重复名称使用最后一项,每次绘制均从原场景开始
// Time以秒指定动画采样时刻,零值为初始时刻,不随实际时钟推进
// ResolveTexture提供外部纹理,空值不访问外部资源;回调自身的内存与超时由调用方控制
// Background为空时使用白色;缓存与实例上限为0时分别使用256MiB和65536,不含输入及运行时开销
type U3DRenderOptions = basethreed.U3DRenderOptions
// U3DTrackState 指定动作资源、轨道和采样时间,Time与关键帧使用相同单位
// Loop将时间循环到首末帧之间,未启用时停留在端点,不自动解释动画修饰器的时间参数
type U3DTrackState = basethreed.U3DTrackState
// U3DCrossSection 定义世界坐标中的剖切面,AxisU与AxisV须为非零正交方向
// 两轴叉积指向被裁去的一侧;平面方形边长取静态场景包围盒对角线
// Opacity为0时不显示平面,Intersection为空时不显示交线,颜色分量范围为0到1
type U3DCrossSection = basethreed.U3DCrossSection
// U3DTextureLayer 保存单个着色器纹理通道,矩阵按列排列
// Blend依次以0至3表示相乘、相加、替换及线性混合,BlendSource为1时使用BlendConstant
// Mode依次以0至4表示模型坐标、平面、圆柱、球面及反射映射
// Alpha控制是否使用纹理透明度,Repeat低两位分别启用横向及纵向平铺
// 平面映射直接取参考位置的XY;圆柱以Z为纵坐标,圆柱与球面的经线从正X轴朝正Y轴递增
// 球面纬度从正Z极到负Z极映射为0至1;反射映射使用相机空间球面环境图,图像纵坐标向下
type U3DTextureLayer = basethreed.U3DTextureLayer
// U3DBone 保存相对父骨骼的初始位姿,Orientation按W、X、Y、Z排列
// Attributes标志决定链接、关节字段是否存在,以及各轴旋转约束是否启用和限幅
// RotationConstraints按X、Y、Z轴排列,每项依次为上限、下限;未指定骨骼姿态时不变形
type U3DBone = basethreed.U3DBone
// U3DBoneJoint 保存骨骼局部坐标系内关节椭圆的中心偏移和两轴尺寸
type U3DBoneJoint = basethreed.U3DBoneJoint
// U3DBoneState 按骨骼名称指定相对父骨骼末端的姿态,根骨骼相对模型原点
// Frame可由动作轨道采样得到,Time不参与变形;单位缩放需显式设为[3]float32{1, 1, 1}
// 未覆盖项沿用文件或动画求值后的局部姿态,重复名称使用最后一项,不施加逆运动学约束
type U3DBoneState = basethreed.U3DBoneState
// U3DNodeState 按名称覆盖节点,未指定透明度或可见性时沿父路径继承
// Opacity范围为0到1,采用标准透明混合;Matrix为相对父节点的列式仿射矩阵
// 显式Opacity覆盖材质和纹理合成的透明度,之后仍应用视图附加透明度
// 未匹配的名称不影响场景,Matrix和Motion均为空时保留每条父路径的原始变换
// Motion以指定轨道替换节点变换,与Matrix互斥,不驱动骨架;显式变换优先于动画修饰器
// Bones覆盖动画求值后的指定骨骼姿态,未指定项沿用当前局部变换,不修改共享几何
type U3DNodeState = basethreed.U3DNodeState
// U3DTexture 保存纹理尺寸、图像类型及各通道的图像数据
type U3DTexture = basethreed.U3DTexture
// U3DTextureImage 保存压缩格式、通道位图及完整压缩数据,外部图像仅保留候选URL
// Compression依次以1至4表示JPEG-24、PNG、JPEG-8和TIFF,ByteCount仅用于内置图像
type U3DTextureImage = basethreed.U3DTextureImage
// U3DTextureResolver 由调用方按原始URL读取外部纹理,地址解析、访问策略和读取限额由调用方控制
// 返回的数据在本次绘制结束前不得修改;失败时尝试下一候选地址,取消及超时错误立即返回
// 入参: ctx 取消上下文, url 绝对或相对地址
// 返回: []byte 压缩图像数据, error 读取错误
type U3DTextureResolver = basethreed.U3DTextureResolver
// U3DView 保存视图节点的投影、裁剪、视口及叠加层参数
type U3DView = basethreed.U3DView
// U3DViewLayer 保存背景或覆盖纹理,注册点使用纹理像素,旋转使用弧度
type U3DViewLayer = basethreed.U3DViewLayer
// U3DViewResource 保存视图的绘制轮次,各轮独立指定根节点及雾效
type U3DViewResource = basethreed.U3DViewResource
// U3DViewPass 保存单轮根节点与雾效,Attributes最低位启用雾效
type U3DViewPass = basethreed.U3DViewPass
// U3DBoneWeight 保存某骨骼对一个位置的影响系数
type U3DBoneWeight = basethreed.U3DBoneWeight
// U3DBoneWeights 保存修饰链中的权重,Index为链内位置,Attributes的1、2、4分别表示网格、线集、点集
// 第i个位置的权重为Weights[Offsets[i]:Offsets[i+1]],空区间表示没有骨骼影响
type U3DBoneWeights = basethreed.U3DBoneWeights
// DecodeU3D 按ECMA-363解码静态几何、骨架、场景节点及材质、纹理图像等资源 // DecodeU3D 按ECMA-363解码静态几何、骨架、场景节点及材质、纹理图像等资源
// 保留动作轨道和动画修饰器,不在解码时求值;绘制能力由RenderU3D另行校验 // 保留动作轨道和动画修饰器,不在解码时求值;绘制能力由RenderU3D另行校验
@@ -125,556 +202,21 @@ type u3dDecoder struct {
// 入参: ctx 取消上下文, data U3D数据, options 解码限制 // 入参: ctx 取消上下文, data U3D数据, options 解码限制
// 返回: *U3DModel 独立场景数据, error 格式、能力、预算或取消错误 // 返回: *U3DModel 独立场景数据, error 格式、能力、预算或取消错误
func DecodeU3D(ctx context.Context, data []byte, options U3DOptions) (*U3DModel, error) { func DecodeU3D(ctx context.Context, data []byte, options U3DOptions) (*U3DModel, error) {
if ctx == nil || options.MaxDecodedBytes < 0 { return basethreed.DecodeU3D(ctx, data, options)
return nil, fmt.Errorf("invalid U3D decode options")
}
if err := ctx.Err(); err != nil {
return nil, err
}
limit := options.MaxDecodedBytes
if limit == 0 {
limit = u3dDefaultBytes
}
d := u3dDecoder{ctx: ctx, remaining: uint64(limit), meshes: make(map[string]*u3dMeshDeclaration), nodes: make(map[string]int), palettes: make(map[uint32]map[string]bool)}
header, next, err := u3dReadBlock(data, 0)
if err != nil {
return nil, err
}
if header.kind != 0x00443355 {
return nil, fmt.Errorf("invalid U3D file header")
}
r := u3dValues{data: header.data}
version, profile := r.u32(), r.u32()
d.model.Major, d.model.Minor = int16(version), int16(version>>16)
if d.model.Major > 0 || profile & ^uint32(14) != 0 {
return nil, fmt.Errorf("unsupported U3D version or profile")
}
declarationEnd := uint64(r.u32())
fileSize := uint64(r.u32())
fileSize |= uint64(r.u32()) << 32
if r.u32() != 106 {
return nil, fmt.Errorf("unsupported U3D character encoding")
}
d.plain = profile&4 != 0
d.model.DefinedUnits, d.model.Units = profile&8 != 0, 1
if d.model.DefinedUnits {
unitBytes := r.take(8)
if r.err == nil {
d.model.Units = math.Float64frombits(binary.LittleEndian.Uint64(unitBytes))
if !finite(d.model.Units) || d.model.Units <= 0 {
return nil, fmt.Errorf("invalid U3D units")
}
}
}
if err := r.done(); err != nil {
return nil, err
}
if fileSize != uint64(len(data)) || declarationEnd < uint64(next) || declarationEnd > fileSize || declarationEnd%4 != 0 {
return nil, fmt.Errorf("invalid U3D file size")
}
if err := d.metadata(header, ""); err != nil {
return nil, err
}
priority := uint32(0)
for offset := next; offset < len(data); {
if err := ctx.Err(); err != nil {
return nil, err
}
block, end, err := u3dReadBlock(data, offset)
if err != nil {
return nil, fmt.Errorf("U3D block at %d: %w", offset, err)
}
continuation := block.kind == 0xffffff15 || block.kind == 0xffffff3b || block.kind == 0xffffff3c || block.kind == 0xffffff3e || block.kind == 0xffffff3f || block.kind == 0xffffff5c
if uint64(offset) < declarationEnd && (uint64(end) > declarationEnd || continuation) || uint64(offset) >= declarationEnd && !continuation {
return nil, fmt.Errorf("invalid U3D declaration boundary at %d", offset)
}
if block.kind == 0xffffff15 {
r := u3dValues{data: block.data}
value := r.u32()
if err := r.done(); err != nil {
return nil, err
}
if value == 0 || value < priority || value > 0x7fffffff {
return nil, fmt.Errorf("invalid U3D block priority")
}
priority = value
err = d.metadata(block, "")
} else if block.kind == 0xffffff14 {
err = d.chain(block)
} else {
_, err = d.block(block, "", -1, 0)
}
if err != nil {
return nil, fmt.Errorf("U3D block %#x at %d: %w", block.kind, offset, err)
}
offset = end
}
for _, name := range d.meshOrder {
mesh := d.meshes[name]
if !mesh.decoded && mesh.counts[1] != 0 {
return nil, fmt.Errorf("missing U3D base mesh %q", name)
}
d.model.Meshes = append(d.model.Meshes, mesh.mesh)
}
if err := d.checkTextures(); err != nil {
return nil, err
}
if err := d.checkParents(); err != nil {
return nil, err
}
if err := d.checkBoneWeights(); err != nil {
return nil, err
}
if err := ctx.Err(); err != nil {
return nil, err
}
return &d.model, nil
} }
// u3dReadBlock 按64位长度计算校验块数据与零填充,避免长度溢出 // RenderU3D 绘制几何及视图纹理层,返回指定时刻的不透明图像,不执行脚本
// 入参: data 块序列, offset 块起点 // 可通过节点覆盖指定动作轨道与采样时间,逐次生成独立姿态,不修改原模型
// 返回: u3dBlock 块, int 下一块起点, error 边界或填充错误 // 显式骨骼姿态按权重变换位置和法线,暂不生成骨骼链接或推导缺失权重
func u3dReadBlock(data []byte, offset int) (u3dBlock, int, error) { // 动画修饰器支持单动作的节点和骨骼求值,尚未实现的动作衔接返回明确错误
if offset < 0 || offset > len(data) || len(data)-offset < 12 { // 几何纹理支持模型坐标与程序化映射,参考形状约定见U3DTextureLayer
return u3dBlock{}, 0, io.ErrUnexpectedEOF // 纹理按需解码并复用,不自动访问外部图像
} // 采用逐顶点光照及逐像素深度混合,空名称引用标准默认资源
header := data[offset : offset+12] // 缺失几何、材质和着色器采用默认资源,缺失的非空光源引用不产生光照
size, meta := uint64(binary.LittleEndian.Uint32(header[4:])), uint64(binary.LittleEndian.Uint32(header[8:])) // 缺失纹理返回错误,空纹理名称使用标准默认图像
start := uint64(offset) + 12 // 仅绘制默认根的后代,非空父节点引用缺失时不展开该父路径
metaStart := start + ((size + 3) &^ 3) // 入参: ctx 取消上下文, model 场景, options 视图、输出与资源限制
end := metaStart + ((meta + 3) &^ 3) // 返回: *image.RGBA 图像, error 参数、能力、预算或取消错误
if end > uint64(len(data)) { func RenderU3D(ctx context.Context, model *U3DModel, options U3DRenderOptions) (*image.RGBA, error) {
return u3dBlock{}, 0, io.ErrUnexpectedEOF return basethreed.RenderU3D(ctx, model, options)
}
for _, padding := range [][]byte{data[start+size : metaStart], data[metaStart+meta : end]} {
for _, value := range padding {
if value != 0 {
return u3dBlock{}, 0, fmt.Errorf("invalid U3D padding")
}
}
}
return u3dBlock{binary.LittleEndian.Uint32(header), data[start : start+size], data[metaStart : metaStart+meta]}, int(end), nil
}
// reserve 在分配解码数组前检查总预算及取消状态
// 入参: count 元素数量, size 元素规模上界
// 返回: error 预算或取消错误
func (d *u3dDecoder) reserve(count, size uint64) error {
if err := d.ctx.Err(); err != nil {
return err
}
if size == 0 || count > d.remaining/size || count > uint64(^uint(0)>>1)/size {
return fmt.Errorf("U3D decoded data exceeds limit")
}
d.remaining -= count * size
return nil
}
// metadata 校验元数据结构并保留原始数据,不解释或执行其中的动作
// 入参: block 数据块, name 所属资源名
// 返回: error 结构、预算或取消错误
func (d *u3dDecoder) metadata(block u3dBlock, name string) error {
if len(block.meta) == 0 {
return nil
}
if err := d.reserve(uint64(len(block.meta))+128, 1); err != nil {
return err
}
r := u3dValues{data: block.meta}
count := r.u32()
if uint64(count) > uint64(len(block.meta))/8 {
return fmt.Errorf("invalid U3D metadata count")
}
for range count {
if err := d.ctx.Err(); err != nil {
return err
}
flags := r.u32()
r.text()
if flags&1 != 0 {
size := r.u32()
if uint64(size) > uint64(len(r.data)-r.pos) {
return io.ErrUnexpectedEOF
}
r.take(int(size))
} else {
r.text()
}
if r.err != nil {
return r.err
}
}
if err := r.done(); err != nil {
return err
}
d.model.Metadata = append(d.model.Metadata, U3DMetadata{block.kind, name, bytes.Clone(block.meta)})
return nil
}
// chain 解析单层修饰链并校验名称、链类型与修饰顺序
// 入参: block 修饰链块
// 返回: error 结构、能力或资源错误
func (d *u3dDecoder) chain(block u3dBlock) error {
if err := d.reserve(256, 1); err != nil {
return err
}
r := u3dValues{data: block.data}
name, kind, flags := r.text(), r.u32(), r.u32()
if r.err != nil {
return r.err
}
if name == "" {
return fmt.Errorf("cannot redefine U3D default resource")
}
if kind > 2 || flags & ^uint32(3) != 0 {
return fmt.Errorf("invalid U3D modifier chain")
}
if flags&1 != 0 {
for range 4 {
r.f32()
}
}
if flags&2 != 0 {
for range 6 {
r.f32()
}
}
for r.pos%4 != 0 && r.err == nil {
if r.u8() != 0 {
return fmt.Errorf("invalid U3D chain padding")
}
}
count := r.u32()
if r.err != nil {
return r.err
}
if uint64(count) > uint64(len(r.data)-r.pos)/12 {
return fmt.Errorf("invalid U3D modifier count")
}
for index := uint32(0); index < count; index++ {
if err := d.ctx.Err(); err != nil {
return err
}
nested, next, err := u3dReadBlock(r.data, r.pos)
if err != nil {
return err
}
actual, err := d.block(nested, name, int(kind), index)
if err != nil {
return err
}
if actual != name {
return fmt.Errorf("U3D modifier name mismatch")
}
r.pos = next
}
if err := r.done(); err != nil {
return err
}
return d.metadata(block, name)
}
// block 解析支持的静态场景块,不丢弃未实现的修饰器或几何
// 入参: block 数据块, owner 所属链名, chain 链类别,负值表示独立块, index 修饰位置
// 返回: string 资源名, error 格式、能力或资源错误
func (d *u3dDecoder) block(block u3dBlock, owner string, chain int, index uint32) (string, error) {
if err := d.ctx.Err(); err != nil {
return "", err
}
if err := d.reserve(uint64(len(block.data))+256, 1); err != nil {
return "", err
}
r := u3dValues{data: block.data}
name := r.text()
if r.err != nil {
return "", r.err
}
if name == "" {
return "", fmt.Errorf("cannot redefine U3D default resource")
}
var err error
switch block.kind {
case 0xffffff21, 0xffffff22, 0xffffff23, 0xffffff24:
if chain >= 0 && (chain != 0 || index != 0) {
return name, fmt.Errorf("invalid U3D node chain")
}
err = d.node(&r, block.kind, name)
case 0xffffff31:
if chain >= 0 && (chain != 1 || index != 0) {
return name, fmt.Errorf("invalid U3D mesh chain")
}
err = d.meshDeclaration(&r, name)
case 0xffffff3b:
if chain >= 0 {
return name, fmt.Errorf("invalid U3D mesh continuation location")
}
err = d.baseMesh(&r, name)
case 0xffffff36, 0xffffff37:
if chain >= 0 && (chain != 1 || index != 0) {
return name, fmt.Errorf("invalid U3D primitive chain")
}
err = d.primitiveDeclaration(&r, name, block.kind == 0xffffff36)
case 0xffffff3e, 0xffffff3f:
if chain >= 0 {
return name, fmt.Errorf("invalid U3D primitive continuation location")
}
err = d.primitiveContinuation(&r, name, block.kind == 0xffffff3e)
case 0xffffff43, 0xffffff44:
if (chain != 0 && chain != 1) || index == 0 || owner != name || r.u32() != index {
return name, fmt.Errorf("invalid U3D animation or bone weight modifier chain")
}
if _, exists := d.nodes[name]; chain == 0 && !exists || chain == 1 && d.meshes[name] == nil {
return name, fmt.Errorf("missing U3D animation or bone weight modifier target")
}
if block.kind == 0xffffff43 {
err = d.animation(&r, name, chain, index)
} else {
err = d.boneWeights(&r, name, chain, index)
}
case 0xffffff45:
if chain != 0 || index == 0 || owner != name || r.u32() != index {
return name, fmt.Errorf("invalid U3D shading modifier chain")
}
err = d.shading(&r, name)
case 0xffffff51, 0xffffff52, 0xffffff53, 0xffffff54, 0xffffff56:
if chain >= 0 {
return name, fmt.Errorf("invalid U3D palette resource location")
}
err = d.resource(&r, block.kind, name)
case 0xffffff55:
if chain >= 0 && (chain != 2 || index != 0) {
return name, fmt.Errorf("invalid U3D texture chain")
}
err = d.textureDeclaration(&r, name)
case 0xffffff5c:
if chain >= 0 {
return name, fmt.Errorf("invalid U3D texture continuation location")
}
err = d.textureContinuation(&r, name)
default:
return name, fmt.Errorf("unsupported U3D block %#x", block.kind)
}
if err != nil {
return name, err
}
if err := r.done(); err != nil {
return name, err
}
return name, d.metadata(block, name)
}
// node 读取节点与父变换,允许引用后续声明的父节点
// 入参: r 字段读取器, kind 节点类型, name 节点名
// 返回: error 结构或预算错误
func (d *u3dDecoder) node(r *u3dValues, kind uint32, name string) error {
if _, exists := d.nodes[name]; exists {
return fmt.Errorf("duplicate U3D node %q", name)
}
node := U3DNode{Name: name, Kind: "Group"}
count := r.u32()
if uint64(count) > uint64(len(r.data)-r.pos)/66 {
return io.ErrUnexpectedEOF
}
if err := d.reserve(uint64(count), 96); err != nil {
return err
}
node.Parents = make([]U3DParent, int(count))
for index := range node.Parents {
if index%256 == 0 {
if err := d.ctx.Err(); err != nil {
return err
}
}
node.Parents[index].Name = r.text()
for element := range node.Parents[index].Transform {
node.Parents[index].Transform[element] = r.f32()
}
}
if kind == 0xffffff22 {
node.Kind, node.Resource, node.Visibility = "Model", r.text(), r.u32()
if node.Visibility > 3 {
return fmt.Errorf("invalid U3D model visibility")
}
} else if kind == 0xffffff23 {
node.Kind, node.Resource = "Light", r.text()
} else if kind == 0xffffff24 {
node.Kind, node.Resource = "View", r.text()
var err error
node.View, err = d.view(r)
if err != nil {
return err
}
}
d.nodes[name] = len(d.model.Nodes)
d.model.Nodes = append(d.model.Nodes, node)
return r.err
}
// shading 分别读取网格、线集和点集着色列表,保留列表内各着色器的先后次序
// 入参: r 字段读取器, name 目标节点名
// 返回: error 结构、能力或预算错误
func (d *u3dDecoder) shading(r *u3dValues, name string) error {
index, exists := d.nodes[name]
if !exists || d.model.Nodes[index].Kind != "Model" {
return fmt.Errorf("missing U3D shading target")
}
flags := r.u32()
if flags & ^uint32(7) != 0 {
return fmt.Errorf("unsupported U3D shading modifier target")
}
count := r.u32()
if uint64(count) > uint64(len(r.data)-r.pos)/4 {
return io.ErrUnexpectedEOF
}
if err := d.reserve(uint64(count), 24); err != nil {
return err
}
lists := make([][]string, int(count))
for index := range lists {
n := r.u32()
if uint64(n) > uint64(len(r.data)-r.pos)/2 {
return io.ErrUnexpectedEOF
}
if err := d.reserve(uint64(n), 16); err != nil {
return err
}
lists[index] = make([]string, int(n))
for j := range lists[index] {
if j%256 == 0 {
if err := d.ctx.Err(); err != nil {
return err
}
}
lists[index][j] = r.text()
}
}
if flags&1 != 0 {
d.model.Nodes[index].Shaders = lists
}
if flags&2 != 0 {
d.model.Nodes[index].LineShaders = lists
}
if flags&4 != 0 {
d.model.Nodes[index].PointShaders = lists
}
return r.err
}
// resource 读取独立材质、着色器、光源、视图和动作资源
// 入参: r 字段读取器, kind 资源类型, name 资源名
// 返回: error 结构或能力错误
func (d *u3dDecoder) resource(r *u3dValues, kind uint32, name string) error {
palette := d.palettes[kind]
if palette == nil {
palette = make(map[string]bool)
d.palettes[kind] = palette
}
if palette[name] {
return fmt.Errorf("duplicate U3D resource %q", name)
}
palette[name] = true
switch kind {
case 0xffffff56:
return d.motion(r, name)
case 0xffffff52:
return d.viewResource(r, name)
case 0xffffff54:
material := U3DMaterial{Name: name, Attributes: r.u32()}
if material.Attributes & ^uint32(63) != 0 {
return fmt.Errorf("invalid U3D material attributes")
}
for _, color := range []*[3]float32{&material.Ambient, &material.Diffuse, &material.Specular, &material.Emissive} {
for index := range color {
color[index] = r.f32()
}
}
material.Reflectivity, material.Opacity = r.f32(), r.f32()
d.model.Materials = append(d.model.Materials, material)
case 0xffffff53:
shader := U3DShader{Name: name, Attributes: r.u32(), AlphaReference: r.f32(), AlphaFunction: r.u32(), Blend: r.u32(), RenderPass: r.u32()}
channels, alpha := r.u32(), r.u32()
shader.Material = r.text()
if shader.Attributes & ^uint32(7) != 0 || shader.AlphaFunction < 0x610 || shader.AlphaFunction > 0x617 || shader.Blend < 0x604 || shader.Blend > 0x607 || channels & ^uint32(255) != 0 || alpha & ^channels != 0 {
return fmt.Errorf("invalid U3D shader parameters")
}
for channel := range shader.Textures {
if channels&(1<<channel) == 0 {
continue
}
if err := d.reserve(1, 192); err != nil {
return err
}
layer := &U3DTextureLayer{Texture: r.text(), Intensity: r.f32(), Blend: r.u8(), BlendSource: r.u8(), BlendConstant: r.f32(), Mode: r.u8(), Alpha: alpha&(1<<channel) != 0}
for i := range layer.Transform {
layer.Transform[i] = r.f32()
}
for i := range layer.WrapTransform {
layer.WrapTransform[i] = r.f32()
}
layer.Repeat = r.u8()
if r.err != nil {
return r.err
}
if layer.Blend > 3 || layer.BlendSource > 1 || layer.Mode > 4 || layer.Repeat > 3 {
return fmt.Errorf("invalid U3D texture layer")
}
shader.Textures[channel] = layer
}
d.model.Shaders = append(d.model.Shaders, shader)
case 0xffffff51:
light := U3DLight{Name: name, Attributes: r.u32(), Kind: r.u8()}
if light.Attributes & ^uint32(7) != 0 || light.Kind > 3 {
return fmt.Errorf("invalid U3D light parameters")
}
for index := range light.Color {
light.Color[index] = r.f32()
}
if r.f32() != 1 {
return fmt.Errorf("invalid U3D light reserved parameter")
}
for index := range light.Attenuation {
light.Attenuation[index] = r.f32()
}
light.SpotAngle, light.Intensity = r.f32(), r.f32()
d.model.Lights = append(d.model.Lights, light)
}
return r.err
}
// checkParents 迭代检查场景父子环路,不递归展开多父节点的实例组合
// 返回: error 环路或取消错误
func (d *u3dDecoder) checkParents() error {
state := make([]byte, len(d.model.Nodes))
type frame struct{ node, parent int }
var stack []frame
for index := range d.model.Nodes {
if state[index] != 0 {
continue
}
stack = append(stack[:0], frame{node: index})
state[index] = 1
for len(stack) != 0 {
if err := d.ctx.Err(); err != nil {
return err
}
current := &stack[len(stack)-1]
parents := d.model.Nodes[current.node].Parents
if current.parent == len(parents) {
state[current.node] = 2
stack = stack[:len(stack)-1]
continue
}
parent, exists := d.nodes[parents[current.parent].Name]
current.parent++
if !exists || state[parent] == 2 {
continue
}
if state[parent] == 1 {
return fmt.Errorf("cyclic U3D node parents")
}
state[parent] = 1
stack = append(stack, frame{node: parent})
}
}
return nil
} }