mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 08:46:55 +08:00
1 parent
64f9ec4241
commit
0be872691d
76 files changed
+1925
-1306
No files matched your search
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,194 @@
|
|||||||
|
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package geom
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
)
|
||||||
|
|
||||||
|
// IdentityMatrix 单位矩阵
|
||||||
|
var IdentityMatrix = Matrix{1, 0, 0, 1, 0, 0}
|
||||||
|
|
||||||
|
// Box 矩形区域
|
||||||
|
type Box struct {
|
||||||
|
X, Y, W, H float64
|
||||||
|
}
|
||||||
|
|
||||||
|
// Point 平面坐标点
|
||||||
|
type Point struct {
|
||||||
|
X, Y float64
|
||||||
|
}
|
||||||
|
|
||||||
|
// Matrix 2D仿射变换矩阵
|
||||||
|
type Matrix struct {
|
||||||
|
a, b, c, d, e, f float64
|
||||||
|
}
|
||||||
|
|
||||||
|
// MatrixFromValues 从a、b、c、d、e、f构造矩阵,不经过文本格式化
|
||||||
|
// 入参: values 矩阵分量
|
||||||
|
// 返回: Matrix 变换矩阵
|
||||||
|
func MatrixFromValues(values [6]float64) Matrix {
|
||||||
|
return Matrix{a: values[0], b: values[1], c: values[2], d: values[3], e: values[4], f: values[5]}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Values 返回a、b、c、d、e、f的独立副本,保留原始浮点精度
|
||||||
|
// 返回: [6]float64 矩阵分量
|
||||||
|
func (m Matrix) Values() [6]float64 {
|
||||||
|
return [6]float64{m.a, m.b, m.c, m.d, m.e, m.f}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Multiply 矩阵乘法 (m * o)
|
||||||
|
// 入参: o 右侧矩阵
|
||||||
|
// 返回: Matrix 结果矩阵
|
||||||
|
func (m Matrix) Multiply(o Matrix) Matrix {
|
||||||
|
return Matrix{
|
||||||
|
a: m.a*o.a + m.c*o.b,
|
||||||
|
b: m.b*o.a + m.d*o.b,
|
||||||
|
c: m.a*o.c + m.c*o.d,
|
||||||
|
d: m.b*o.c + m.d*o.d,
|
||||||
|
e: m.a*o.e + m.c*o.f + m.e,
|
||||||
|
f: m.b*o.e + m.d*o.f + m.f,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transform 应用变换矩阵
|
||||||
|
// 入参: x X坐标, y Y坐标
|
||||||
|
// 返回: float64 变换后X, float64 变换后Y
|
||||||
|
func (m Matrix) Transform(x, y float64) (float64, float64) {
|
||||||
|
nx := m.a*x + m.c*y + m.e
|
||||||
|
ny := m.b*x + m.d*y + m.f
|
||||||
|
return nx, ny
|
||||||
|
}
|
||||||
|
|
||||||
|
// TransformBox 获取矩形经仿射变换后的轴对齐边界
|
||||||
|
// 入参: box 原始矩形范围
|
||||||
|
// 返回: Box 变换后的轴对齐边界
|
||||||
|
func (m Matrix) TransformBox(box Box) Box {
|
||||||
|
x, y := m.Transform(box.X, box.Y)
|
||||||
|
left, top, right, bottom := x, y, x, y
|
||||||
|
for _, point := range [][2]float64{{box.X + box.W, box.Y}, {box.X, box.Y + box.H}, {box.X + box.W, box.Y + box.H}} {
|
||||||
|
x, y := m.Transform(point[0], point[1])
|
||||||
|
left, top, right, bottom = math.Min(left, x), math.Min(top, y), math.Max(right, x), math.Max(bottom, y)
|
||||||
|
}
|
||||||
|
return Box{X: left, Y: top, W: right - left, H: bottom - top}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invert 求仿射矩阵的逆变换
|
||||||
|
// 返回: Matrix 逆矩阵,失败时返回单位矩阵, bool 是否存在有限逆矩阵
|
||||||
|
func (m Matrix) Invert() (Matrix, bool) {
|
||||||
|
for _, value := range m.Values() {
|
||||||
|
if !finite(value) {
|
||||||
|
return IdentityMatrix, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
det, ds := matrixProductDifference(m.a, m.d, m.b, m.c)
|
||||||
|
x, xs := matrixProductDifference(m.c, m.f, m.d, m.e)
|
||||||
|
y, ys := matrixProductDifference(m.b, m.e, m.a, m.f)
|
||||||
|
if det != 0 && ds && xs && ys {
|
||||||
|
n := Matrix{a: m.d / det, b: -m.b / det, c: -m.c / det, d: m.a / det, e: x / det, f: y / det}
|
||||||
|
valid := true
|
||||||
|
for _, value := range n.Values() {
|
||||||
|
valid = valid && finite(value)
|
||||||
|
}
|
||||||
|
if valid {
|
||||||
|
return n, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
determinant, exponent := matrixProductSum(m.a, m.d, -m.b, m.c)
|
||||||
|
if determinant == 0 {
|
||||||
|
return IdentityMatrix, false
|
||||||
|
}
|
||||||
|
divide := func(value float64, power int) float64 {
|
||||||
|
mantissa, shift := math.Frexp(value)
|
||||||
|
return math.Ldexp(mantissa/determinant, power+shift-exponent)
|
||||||
|
}
|
||||||
|
x, xe := matrixProductSum(m.c, m.f, -m.d, m.e)
|
||||||
|
y, ye := matrixProductSum(m.b, m.e, -m.a, m.f)
|
||||||
|
n := Matrix{a: divide(m.d, 0), b: divide(-m.b, 0), c: divide(-m.c, 0), d: divide(m.a, 0), e: divide(x, xe), f: divide(y, ye)}
|
||||||
|
for _, value := range n.Values() {
|
||||||
|
if !finite(value) {
|
||||||
|
return IdentityMatrix, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return n, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// matrixProductDifference 计算两个乘积之差,检查溢出、下溢及相消造成的精度损失
|
||||||
|
// 入参: a 第一个乘数, b 第二个乘数, c 第三个乘数, d 第四个乘数
|
||||||
|
// 返回: float64 乘积之差, bool 是否可直接用于求逆
|
||||||
|
func matrixProductDifference(a, b, c, d float64) (float64, bool) {
|
||||||
|
x, y := a*b, c*d
|
||||||
|
value := x - y
|
||||||
|
if !finite(value) || x == 0 && a != 0 && b != 0 || y == 0 && c != 0 && d != 0 {
|
||||||
|
return value, false
|
||||||
|
}
|
||||||
|
return value, x == 0 && y == 0 || math.Abs(value) >= 0x1p-1022 && math.Abs(value) >= 0x1p-48*math.Max(math.Abs(x), math.Abs(y))
|
||||||
|
}
|
||||||
|
|
||||||
|
// matrixProductSum 以尾数及指数累加两个乘积,保留乘法舍入残差
|
||||||
|
// 入参: a 第一个乘数, b 第二个乘数, c 第三个乘数, d 第四个乘数
|
||||||
|
// 返回: float64 结果尾数, int 二进制指数
|
||||||
|
func matrixProductSum(a, b, c, d float64) (float64, int) {
|
||||||
|
a, ae := math.Frexp(a)
|
||||||
|
b, be := math.Frexp(b)
|
||||||
|
c, ce := math.Frexp(c)
|
||||||
|
d, de := math.Frexp(d)
|
||||||
|
x, y := a*b, c*d
|
||||||
|
xe, ye := ae+be, ce+de
|
||||||
|
power := max(xe, ye)
|
||||||
|
if x == 0 {
|
||||||
|
power = ye
|
||||||
|
} else if y == 0 {
|
||||||
|
power = xe
|
||||||
|
}
|
||||||
|
value := math.Ldexp(x, xe-power) + math.Ldexp(y, ye-power)
|
||||||
|
value += math.Ldexp(math.FMA(a, b, -x), xe-power) + math.Ldexp(math.FMA(c, d, -y), ye-power)
|
||||||
|
value, shift := math.Frexp(value)
|
||||||
|
return value, power + shift
|
||||||
|
}
|
||||||
|
|
||||||
|
// TranslationMatrix 构造平移矩阵
|
||||||
|
// 入参: x X轴偏移, y Y轴偏移
|
||||||
|
// 返回: Matrix 平移矩阵
|
||||||
|
func TranslationMatrix(x, y float64) Matrix {
|
||||||
|
return Matrix{a: 1, d: 1, e: x, f: y}
|
||||||
|
}
|
||||||
|
|
||||||
|
// YScale 获取Y轴缩放比例
|
||||||
|
// 返回: float64 缩放比例
|
||||||
|
func (m Matrix) YScale() float64 {
|
||||||
|
return math.Sqrt(m.c*m.c + m.d*m.d)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Equal 比较毫米坐标,使用固定绝对误差,不受绘图库全局精度影响
|
||||||
|
// 入参: a 第一个坐标, b 第二个坐标
|
||||||
|
// 返回: bool 是否在容差内相等
|
||||||
|
func Equal(a, b float64) bool {
|
||||||
|
return math.Abs(a-b) <= 1e-10
|
||||||
|
}
|
||||||
|
|
||||||
|
// AxisAligned 判断变换是否保持坐标轴平行,含直角旋转与镜像
|
||||||
|
// 入参: m 仿射变换矩阵
|
||||||
|
// 返回: bool 是否为非退化的轴对齐变换
|
||||||
|
func AxisAligned(m Matrix) bool {
|
||||||
|
return m.b == 0 && m.c == 0 && m.a != 0 && m.d != 0 || m.a == 0 && m.d == 0 && m.b != 0 && m.c != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vector 变换位移,不包含平移分量
|
||||||
|
// 入参: m 仿射变换矩阵, x 原始横向位移, y 原始纵向位移
|
||||||
|
// 返回: float64 变换后的X位移, float64 变换后的Y位移
|
||||||
|
func Vector(m Matrix, x, y float64) (float64, float64) {
|
||||||
|
return m.a*x + m.c*y, m.b*x + m.d*y
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package geom
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
)
|
||||||
|
|
||||||
|
// finite 判断数值是否有限
|
||||||
|
// 入参: value 数值
|
||||||
|
// 返回: bool 是否有限
|
||||||
|
func finite(value float64) bool {
|
||||||
|
return !math.IsNaN(value) && !math.IsInf(value, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// positive 判断正有限数
|
||||||
|
// 入参: v 待检查数值
|
||||||
|
// 返回: bool 是否为正有限数
|
||||||
|
func positive(v float64) bool { return v > 0 && !math.IsInf(v, 0) && !math.IsNaN(v) }
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package geom
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 几何路径指令,包括直线、曲线、椭圆弧和闭合
|
||||||
|
const (
|
||||||
|
Move Verb = iota
|
||||||
|
Line
|
||||||
|
Quad
|
||||||
|
Cubic
|
||||||
|
Arc
|
||||||
|
ClosePath
|
||||||
|
)
|
||||||
|
|
||||||
|
// Path 保存独立于绘图库的几何路径,不扁平化曲线或舍入坐标
|
||||||
|
// 坐标以左上角为原点,单位为毫米,子路径从Move开始,Close回到当前子路径起点
|
||||||
|
type Path []Segment
|
||||||
|
|
||||||
|
// Verb 表示几何路径指令
|
||||||
|
type Verb uint8
|
||||||
|
|
||||||
|
// Segment 保存路径端点、贝塞尔控制点或端点式椭圆弧参数
|
||||||
|
// 弧线Rotation单位为度,Sweep为true表示顺时针,Large表示大弧
|
||||||
|
type Segment struct {
|
||||||
|
Verb Verb
|
||||||
|
End Point
|
||||||
|
Control1, Control2 Point
|
||||||
|
RadiusX, RadiusY float64
|
||||||
|
Rotation float64
|
||||||
|
Large, Sweep bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// StrokeOptions 描述描边参数,长度与路径坐标采用相同单位
|
||||||
|
// Width须为正有限数;Cap和Join使用标准线帽和连接样式名称,空值分别采用Butt和Miter
|
||||||
|
// MiterLimit为尖角长度与半线宽的比值上限,由调用方设置默认值
|
||||||
|
// MiterLimitSet供调用方区分显式零值与默认值,不参与几何计算
|
||||||
|
// DashOffset为虚线起始偏移,允许负值;Dashes交替指定实线段和间隔长度,奇数项按重复一遍处理
|
||||||
|
// Tolerance为曲线展开的误差目标,0采用0.001
|
||||||
|
type StrokeOptions struct {
|
||||||
|
Width float64
|
||||||
|
Cap, Join string
|
||||||
|
MiterLimit float64
|
||||||
|
MiterLimitSet bool
|
||||||
|
DashOffset float64
|
||||||
|
Tolerance float64
|
||||||
|
Dashes []float64
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close 为填充轮廓补齐隐式闭合边,不改变源路径
|
||||||
|
// 入参: path 填充路径
|
||||||
|
// 返回: Path 显式闭合路径
|
||||||
|
func Close(path Path) Path {
|
||||||
|
open, missing := false, false
|
||||||
|
for _, segment := range path {
|
||||||
|
if segment.Verb == Move && open {
|
||||||
|
missing = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
open = segment.Verb != ClosePath
|
||||||
|
}
|
||||||
|
if !missing && !open {
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
result := make(Path, 0, len(path)+1)
|
||||||
|
open = false
|
||||||
|
for _, segment := range path {
|
||||||
|
if segment.Verb == Move && open {
|
||||||
|
result = append(result, Segment{Verb: ClosePath})
|
||||||
|
}
|
||||||
|
result = append(result, segment)
|
||||||
|
open = segment.Verb != ClosePath
|
||||||
|
}
|
||||||
|
if open {
|
||||||
|
result = append(result, Segment{Verb: ClosePath})
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateStroke 校验各后端共用的描边参数
|
||||||
|
// 入参: options 描边样式
|
||||||
|
// 返回: error 无效样式
|
||||||
|
func ValidateStroke(options StrokeOptions) error {
|
||||||
|
if !finite(options.Width) || options.Width <= 0 || !finite(options.Tolerance) || options.Tolerance < 0 || !finite(options.DashOffset) || !finite(options.MiterLimit) || options.MiterLimit < 0 {
|
||||||
|
return fmt.Errorf("invalid stroke width or tolerance")
|
||||||
|
}
|
||||||
|
if options.Cap != "" && options.Cap != "Butt" && options.Cap != "Round" && options.Cap != "Square" {
|
||||||
|
return fmt.Errorf("invalid stroke cap %q", options.Cap)
|
||||||
|
}
|
||||||
|
if options.Join != "" && options.Join != "Miter" && options.Join != "Round" && options.Join != "Bevel" {
|
||||||
|
return fmt.Errorf("invalid stroke join %q", options.Join)
|
||||||
|
}
|
||||||
|
for _, dash := range options.Dashes {
|
||||||
|
if !finite(dash) || dash < 0 {
|
||||||
|
return fmt.Errorf("invalid stroke dash")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rectangle 构建页面坐标矩形,保留输入精度
|
||||||
|
// 入参: box 矩形范围
|
||||||
|
// 返回: Path 闭合路径
|
||||||
|
func Rectangle(box Box) Path {
|
||||||
|
return Path{
|
||||||
|
{Verb: Move, End: Point{X: box.X, Y: box.Y}},
|
||||||
|
{Verb: Line, End: Point{X: box.X + box.W, Y: box.Y}},
|
||||||
|
{Verb: Line, End: Point{X: box.X + box.W, Y: box.Y + box.H}},
|
||||||
|
{Verb: Line, End: Point{X: box.X, Y: box.Y + box.H}},
|
||||||
|
{Verb: ClosePath, End: Point{X: box.X, Y: box.Y}},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RectangleBounds 识别单个轴对齐矩形,不用包围盒代替实际路径
|
||||||
|
// 入参: path 填充路径
|
||||||
|
// 返回: Box 矩形范围, bool 是否为非退化矩形
|
||||||
|
func RectangleBounds(path Path) (Box, bool) {
|
||||||
|
if len(path) > 0 && path[len(path)-1].Verb == ClosePath {
|
||||||
|
path = path[:len(path)-1]
|
||||||
|
}
|
||||||
|
if len(path) == 5 && path[4].Verb == Line && path[4].End == path[0].End {
|
||||||
|
path = path[:4]
|
||||||
|
}
|
||||||
|
if len(path) != 4 || path[0].Verb != Move || path[1].Verb != Line || path[2].Verb != Line || path[3].Verb != Line {
|
||||||
|
return Box{}, false
|
||||||
|
}
|
||||||
|
a, b, c, d := path[0].End, path[1].End, path[2].End, path[3].End
|
||||||
|
for _, point := range [...]Point{a, b, c, d} {
|
||||||
|
if !finite(point.X) || !finite(point.Y) {
|
||||||
|
return Box{}, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if a.X == c.X || a.Y == c.Y {
|
||||||
|
return Box{}, false
|
||||||
|
}
|
||||||
|
if !(a.X == b.X && b.Y == c.Y && c.X == d.X && d.Y == a.Y || a.Y == b.Y && b.X == c.X && c.Y == d.Y && d.X == a.X) {
|
||||||
|
return Box{}, false
|
||||||
|
}
|
||||||
|
width, height := math.Abs(c.X-a.X), math.Abs(c.Y-a.Y)
|
||||||
|
if !finite(width) || !finite(height) {
|
||||||
|
return Box{}, false
|
||||||
|
}
|
||||||
|
return Box{X: min(a.X, c.X), Y: min(a.Y, c.Y), W: width, H: height}, true
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
@@ -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"
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
@@ -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))
|
||||||
|
}
|
||||||
@@ -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
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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{}
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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 {
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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")
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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
@@ -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]
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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)}})
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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]}})
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
|
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user