From 0be872691deb65bcbe24c8164cf66acc5e81072c Mon Sep 17 00:00:00 2001 From: xiaoqidun Date: Wed, 14 Oct 2026 00:44:44 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E6=96=87=E4=BB=B6=E6=8B=86=E5=88=86):=20?= =?UTF-8?q?=E6=8B=86=E5=88=86=E5=87=A0=E4=BD=95=E4=B8=89=E7=BB=B4=E5=9F=BA?= =?UTF-8?q?=E7=A1=80=E8=83=BD=E5=8A=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../base/geom/arc.go | 36 +- .../base/geom/bounds.go | 32 +- .../base/geom/contain.go | 16 +- internal/base/geom/matrix.go | 194 +++++ internal/base/geom/number.go | 31 + internal/base/geom/path.go | 160 ++++ .../base/geom/stroke.go | 74 +- .../base/geom/transform.go | 52 +- internal/base/threed/u3d.go | 681 +++++++++++++++ .../base/threed/u3d_animation.go | 6 +- .../base/threed/u3d_bits.go | 2 +- internal/base/threed/u3d_depth.go | 75 ++ internal/base/threed/u3d_finite.go | 26 + .../base/threed/u3d_light.go | 16 +- .../base/threed/u3d_line.go | 14 +- .../base/threed/u3d_lineset.go | 2 +- .../base/threed/u3d_mapping.go | 2 +- .../base/threed/u3d_mesh.go | 2 +- .../base/threed/u3d_mode.go | 16 +- .../base/threed/u3d_motion.go | 2 +- .../base/threed/u3d_outline.go | 8 +- .../base/threed/u3d_playback.go | 28 +- .../base/threed/u3d_pointset.go | 2 +- .../base/threed/u3d_render.go | 48 +- .../base/threed/u3d_resource_reader.go | 2 +- .../base/threed/u3d_sample.go | 4 +- .../base/threed/u3d_scene.go | 12 +- .../base/threed/u3d_section.go | 16 +- .../base/threed/u3d_shader.go | 10 +- .../base/threed/u3d_skeleton.go | 2 +- .../base/threed/u3d_skin.go | 46 +- .../base/threed/u3d_state.go | 14 +- .../base/threed/u3d_texture.go | 6 +- .../base/threed/u3d_texture_decode.go | 22 +- .../base/threed/u3d_triangle.go | 22 +- .../base/threed/u3d_view.go | 2 +- internal/base/threed/u3d_view_bounds.go | 39 + internal/base/threed/u3d_view_camera.go | 68 ++ .../base/threed/u3d_view_render.go | 8 +- .../base/threed/u3d_weights.go | 2 +- ofdgo_action.go | 5 +- ofdgo_editor_action.go | 4 +- ofdgo_editor_batch.go | 9 +- ofdgo_editor_composite_copy.go | 3 +- ofdgo_editor_composite_ops.go | 5 +- ofdgo_editor_erase.go | 9 +- ofdgo_editor_transform.go | 5 +- ofdgo_font_metrics.go | 8 +- ofdgo_geom.go | 127 +-- ofdgo_geom_backend.go | 87 +- ofdgo_geom_path.go | 110 +-- ofdgo_paint_mesh.go | 2 +- ofdgo_paint_model.go | 7 +- ofdgo_pdf.go | 6 +- ofdgo_pdf_3d.go | 129 +-- ofdgo_pdf_3d_section.go | 3 +- ofdgo_pdf_geometry.go | 15 +- ofdgo_pdf_object.go | 2 +- ofdgo_pdf_shading.go | 3 +- ofdgo_pdf_text_path.go | 4 +- ofdgo_raster_bounds.go | 5 +- ofdgo_render_compile.go | 5 +- ofdgo_render_compile_image.go | 13 +- ofdgo_render_pattern.go | 2 +- ofdgo_render_shading.go | 7 +- ofdgo_render_text.go | 5 +- ofdgo_render_walk.go | 7 +- ofdgo_renderer_canvas.go | 3 +- ofdgo_renderer_clip_convex_canvas.go | 3 +- ofdgo_renderer_compile.go | 7 +- ofdgo_renderer_geom_canvas.go | 7 +- ofdgo_renderer_image_canvas.go | 3 +- ofdgo_renderer_pdf_action.go | 3 +- ofdgo_renderer_pdf_link.go | 9 +- ofdgo_shape.go | 7 +- ofdgo_u3d.go | 804 ++++-------------- 76 files changed, 1926 insertions(+), 1307 deletions(-) rename ofdgo_geom_arc.go => internal/base/geom/arc.go (82%) rename ofdgo_geom_bounds.go => internal/base/geom/bounds.go (83%) rename ofdgo_geom_contain.go => internal/base/geom/contain.go (83%) create mode 100644 internal/base/geom/matrix.go create mode 100644 internal/base/geom/number.go create mode 100644 internal/base/geom/path.go rename ofdgo_geom_stroke.go => internal/base/geom/stroke.go (83%) rename ofdgo_geom_transform.go => internal/base/geom/transform.go (79%) create mode 100644 internal/base/threed/u3d.go rename ofdgo_u3d_animation.go => internal/base/threed/u3d_animation.go (97%) rename ofdgo_u3d_bits.go => internal/base/threed/u3d_bits.go (99%) create mode 100644 internal/base/threed/u3d_depth.go create mode 100644 internal/base/threed/u3d_finite.go rename ofdgo_u3d_light.go => internal/base/threed/u3d_light.go (96%) rename ofdgo_u3d_line.go => internal/base/threed/u3d_line.go (95%) rename ofdgo_u3d_lineset.go => internal/base/threed/u3d_lineset.go (99%) rename ofdgo_u3d_mapping.go => internal/base/threed/u3d_mapping.go (99%) rename ofdgo_u3d_mesh.go => internal/base/threed/u3d_mesh.go (99%) rename ofdgo_u3d_mode.go => internal/base/threed/u3d_mode.go (95%) rename ofdgo_u3d_motion.go => internal/base/threed/u3d_motion.go (99%) rename ofdgo_u3d_outline.go => internal/base/threed/u3d_outline.go (93%) rename ofdgo_u3d_playback.go => internal/base/threed/u3d_playback.go (90%) rename ofdgo_u3d_pointset.go => internal/base/threed/u3d_pointset.go (99%) rename ofdgo_u3d_render.go => internal/base/threed/u3d_render.go (92%) rename ofdgo_u3d_resource_reader.go => internal/base/threed/u3d_resource_reader.go (99%) rename ofdgo_u3d_sample.go => internal/base/threed/u3d_sample.go (98%) rename ofdgo_u3d_scene.go => internal/base/threed/u3d_scene.go (96%) rename ofdgo_u3d_section.go => internal/base/threed/u3d_section.go (94%) rename ofdgo_u3d_shader.go => internal/base/threed/u3d_shader.go (96%) rename ofdgo_u3d_skeleton.go => internal/base/threed/u3d_skeleton.go (99%) rename ofdgo_u3d_skin.go => internal/base/threed/u3d_skin.go (90%) rename ofdgo_u3d_state.go => internal/base/threed/u3d_state.go (93%) rename ofdgo_u3d_texture.go => internal/base/threed/u3d_texture.go (99%) rename ofdgo_u3d_texture_decode.go => internal/base/threed/u3d_texture_decode.go (92%) rename ofdgo_u3d_triangle.go => internal/base/threed/u3d_triangle.go (95%) rename ofdgo_u3d_view.go => internal/base/threed/u3d_view.go (99%) create mode 100644 internal/base/threed/u3d_view_bounds.go create mode 100644 internal/base/threed/u3d_view_camera.go rename ofdgo_u3d_view_render.go => internal/base/threed/u3d_view_render.go (93%) rename ofdgo_u3d_weights.go => internal/base/threed/u3d_weights.go (99%) diff --git a/ofdgo_geom_arc.go b/internal/base/geom/arc.go similarity index 82% rename from ofdgo_geom_arc.go rename to internal/base/geom/arc.go index 2c98247..d668f46 100644 --- a/ofdgo_geom_arc.go +++ b/internal/base/geom/arc.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package ofdgo +package geom import ( "fmt" @@ -27,33 +27,33 @@ type geometryArc struct { // Curves 按毫米误差上限将端点式椭圆弧转换为三次贝塞尔曲线,其他指令保持原样 // 入参: tolerance 正有限误差上限 -// 返回: GeometryPath 独立路径, error 参数错误 -func (p GeometryPath) Curves(tolerance float64) (GeometryPath, error) { - if err := p.validate(); err != nil { +// 返回: Path 独立路径, error 参数错误 +func (p Path) Curves(tolerance float64) (Path, error) { + if err := p.Validate(); err != nil { return nil, err } - if !rasterPositive(tolerance) { + if !positive(tolerance) { return nil, fmt.Errorf("invalid curve tolerance") } - result := make(GeometryPath, 0, len(p)) + result := make(Path, 0, len(p)) var current, start Point for _, s := range p { switch s.Verb { - case GeometryMove: + case Move: start = s.End - case GeometryArc: - curves, err := arcCurves(current, s, tolerance) + case Arc: + curves, err := ArcCurves(current, s, tolerance) if err != nil { return nil, err } result = append(result, curves...) current = s.End continue - case GeometryClose: + case ClosePath: result = append(result, s) current = start continue - case GeometryLine, GeometryQuad, GeometryCubic: + case Line, Quad, Cubic: default: 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 } -// arcCurves 按标准端点参数求椭圆中心,只在绘制边界近似弧线 +// ArcCurves 按标准端点参数求椭圆中心,只在绘制边界近似弧线 // 入参: start 起点, arc 弧线, tolerance 毫米误差上限 -// 返回: GeometryPath 贝塞尔片段, error 无效弧线 -func arcCurves(start Point, arc GeometrySegment, tolerance float64) (GeometryPath, error) { +// 返回: Path 贝塞尔片段, error 无效弧线 +func ArcCurves(start Point, arc Segment, tolerance float64) (Path, error) { a, err := centerArc(start, arc) if err != nil { return nil, err @@ -75,7 +75,7 @@ func arcCurves(start Point, arc GeometrySegment, tolerance float64) (GeometryPat return nil, nil } 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)) 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 { 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++ { sa, ca := math.Sincos(a.theta + float64(i)*step) sb, cb := math.Sincos(a.theta + float64(i+1)*step) k := 4.0 / 3 * math.Tan(step/4) - result = append(result, 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 return result, nil @@ -102,7 +102,7 @@ func arcCurves(start Point, arc GeometrySegment, tolerance float64) (GeometryPat // centerArc 将端点式圆弧解析为中心式,统一半径修正和方向语义 // 入参: start 起点, arc 弧线 // 返回: geometryArc 中心式参数, error 无效参数或数值溢出 -func centerArc(start Point, arc GeometrySegment) (geometryArc, error) { +func centerArc(start Point, arc Segment) (geometryArc, error) { rx, ry := arc.RadiusX, arc.RadiusY if !finite(rx) || !finite(ry) || rx < 0 || ry < 0 || !finite(arc.Rotation) || !finite(start.X) || !finite(start.Y) || !finite(arc.End.X) || !finite(arc.End.Y) { diff --git a/ofdgo_geom_bounds.go b/internal/base/geom/bounds.go similarity index 83% rename from ofdgo_geom_bounds.go rename to internal/base/geom/bounds.go index c1a8304..d6c64b8 100644 --- a/ofdgo_geom_bounds.go +++ b/internal/base/geom/bounds.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package ofdgo +package geom import ( "fmt" @@ -22,8 +22,8 @@ import ( // Bounds 通过解析极值计算直线、贝塞尔和椭圆弧的轴对齐边界 // 不进行曲线展开且不使用Tolerance,结果受float64舍入误差影响,不保证向外舍入 // 返回: Box 浮点路径范围, error 无效路径、弧线或数值溢出 -func (p GeometryPath) Bounds() (Box, error) { - if err := p.validate(); err != nil { +func (p Path) Bounds() (Box, error) { + if err := p.Validate(); err != nil { return Box{}, err } left, top, right, bottom := math.Inf(1), math.Inf(1), math.Inf(-1), math.Inf(-1) @@ -34,11 +34,11 @@ func (p GeometryPath) Bounds() (Box, error) { var current, start Point for _, s := range p { switch s.Verb { - case GeometryMove: + case Move: start = s.End - case GeometryClose: + case ClosePath: s.End = start - case GeometryArc: + case Arc: a, err := centerArc(current, s) if err != nil { 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}} { - if s.Verb == GeometryQuad { + if s.Verb == Quad { axis[2] = 0 } scale := math.Max(math.Abs(axis[0]), math.Max(math.Abs(axis[1]), math.Max(math.Abs(axis[2]), math.Abs(axis[3])))) @@ -64,19 +64,19 @@ func (p GeometryPath) Bounds() (Box, error) { } } 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]) } else { roots = geometryRoots(-axis[0]+3*axis[1]-3*axis[2]+axis[3], 2*(axis[0]-2*axis[1]+axis[2]), axis[1]-axis[0]) } for _, t := range roots { if t > 0 && t < 1 { - q := geometryLerp(current, s.Control1, t) - if s.Verb == GeometryQuad { - q = geometryLerp(q, geometryLerp(s.Control1, s.End, t), t) + q := Lerp(current, s.Control1, t) + if s.Verb == Quad { + q = Lerp(q, Lerp(s.Control1, s.End, t), t) } else { - r := geometryLerp(s.Control1, s.Control2, t) - q = geometryLerp(geometryLerp(q, r, t), geometryLerp(r, geometryLerp(s.Control2, s.End, t), t), t) + r := Lerp(s.Control1, s.Control2, t) + q = Lerp(Lerp(q, r, t), Lerp(r, Lerp(s.Control2, s.End, t), t), t) } add(q) } @@ -121,9 +121,9 @@ func geometryRoots(a, b, c float64) []float64 { return []float64{q / a, c / q} } -// geometryLerp 线性插值坐标 +// Lerp 线性插值坐标 // 入参: a 起点, b 终点, t 插值比例 // 返回: 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} } diff --git a/ofdgo_geom_contain.go b/internal/base/geom/contain.go similarity index 83% rename from ofdgo_geom_contain.go rename to internal/base/geom/contain.go index 7543a46..5e228e1 100644 --- a/ofdgo_geom_contain.go +++ b/internal/base/geom/contain.go @@ -12,14 +12,16 @@ // See the License for the specific language governing permissions and // limitations under the License. -package ofdgo +package geom -import "math" +import ( + "math" +) -// geometryConvexContains 判断单个非退化凸直线区域是否包含另一闭合直线区域 +// ConvexContains 判断单个非退化凸直线区域是否包含另一闭合直线区域 // 入参: outer 外部区域, inner 内部区域 // 返回: bool 是否完全包含,曲线及多轮廓返回false -func geometryConvexContains(outer, inner GeometryPath) bool { +func ConvexContains(outer, inner Path) bool { a, b := geometryPolygonPoints(outer), geometryPolygonPoints(inner) if len(a) < 3 || len(b) < 3 { return false @@ -68,12 +70,12 @@ func geometryConvexContains(outer, inner GeometryPath) bool { // geometryPolygonPoints 读取单个显式闭合直线轮廓的顶点,不修改原路径 // 入参: path 直线轮廓 // 返回: []Point 独立顶点,其他路径返回nil -func geometryPolygonPoints(path GeometryPath) []Point { - if len(path) < 4 || path[0].Verb != GeometryMove || path[len(path)-1].Verb != GeometryClose { +func geometryPolygonPoints(path Path) []Point { + if len(path) < 4 || path[0].Verb != Move || path[len(path)-1].Verb != ClosePath { return nil } for i, segment := range path[:len(path)-1] { - if i > 0 && segment.Verb != 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 } } diff --git a/internal/base/geom/matrix.go b/internal/base/geom/matrix.go new file mode 100644 index 0000000..77a7d62 --- /dev/null +++ b/internal/base/geom/matrix.go @@ -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 +} diff --git a/internal/base/geom/number.go b/internal/base/geom/number.go new file mode 100644 index 0000000..03ab796 --- /dev/null +++ b/internal/base/geom/number.go @@ -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) } diff --git a/internal/base/geom/path.go b/internal/base/geom/path.go new file mode 100644 index 0000000..fc0d98b --- /dev/null +++ b/internal/base/geom/path.go @@ -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 +} diff --git a/ofdgo_geom_stroke.go b/internal/base/geom/stroke.go similarity index 83% rename from ofdgo_geom_stroke.go rename to internal/base/geom/stroke.go index dd4fbda..7d56a5e 100644 --- a/ofdgo_geom_stroke.go +++ b/internal/base/geom/stroke.go @@ -12,13 +12,16 @@ // See the License for the specific language governing permissions and // limitations under the License. -package ofdgo +package geom import ( "fmt" "math" ) +// UnsupportedError 表示当前几何实现无法展开的描边形态 +type UnsupportedError string + // geometryPolyline 保存展开后的子路径,独立记录闭合状态 type geometryPolyline struct { points []Point @@ -26,16 +29,22 @@ type geometryPolyline struct { direction Point } +// Error 返回不支持的描边原因 +// 返回: string 错误信息 +func (err UnsupportedError) Error() string { + return string(err) +} + // Stroke 按端点和转角样式将曲线、虚线转换为非零填充轮廓 // 曲线按Tolerance展开,默认误差为0.001毫米,重叠轮廓保持同向绕数 // 结果未做布尔整理,按非零规则填充,区域运算需能处理重叠子路径的后端 // 入参: options 描边样式 -// 返回: GeometryPath 描边轮廓, error 无效参数或精度限制 -func (p GeometryPath) Stroke(options StrokeOptions) (GeometryPath, error) { - if err := validateStroke(options); err != nil { +// 返回: Path 描边轮廓, error 无效参数或精度限制 +func (p Path) Stroke(options StrokeOptions) (Path, error) { + if err := ValidateStroke(options); err != nil { return nil, err } - if err := p.validate(); err != nil { + if err := p.Validate(); err != nil { return nil, err } if options.Width/2 == 0 { @@ -68,7 +77,7 @@ func (p GeometryPath) Stroke(options StrokeOptions) (GeometryPath, error) { if err != nil { return nil, err } - var result GeometryPath + var result Path for _, line := range lines { parts := []geometryPolyline{line} 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 result, nil @@ -96,7 +105,7 @@ func (p GeometryPath) Stroke(options StrokeOptions) (GeometryPath, error) { // strokePolylines 自适应展开贝塞尔,弦长亏损同时约束虚线累计长度误差 // 入参: tolerance 展开误差 // 返回: []geometryPolyline 子路径, error 展开限制 -func (p GeometryPath) strokePolylines(tolerance float64) ([]geometryPolyline, error) { +func (p Path) strokePolylines(tolerance float64) ([]geometryPolyline, error) { curves, err := p.Curves(tolerance / 2) if err != nil { return nil, err @@ -149,7 +158,7 @@ func (p GeometryPath) strokePolylines(tolerance float64) ([]geometryPolyline, er for n := len(points); n > 0; n-- { left[len(points)-n], right[n-1] = work[0], work[n-1] for i := 0; i < n-1; i++ { - work[i] = geometryLerp(work[i], work[i+1], .5) + work[i] = Lerp(work[i], work[i+1], .5) } } 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 { switch s.Verb { - case GeometryMove: + case Move: flush() current, start = s.End, s.End line.points = []Point{current} - case GeometryClose: + case ClosePath: if drawn { if current != start { line.points = append(line.points, start) @@ -177,10 +186,10 @@ func (p GeometryPath) strokePolylines(tolerance float64) ([]geometryPolyline, er drawn = true var buffer [4]Point points := append(buffer[:0], current) - if s.Verb == GeometryQuad || s.Verb == GeometryCubic { + if s.Verb == Quad || s.Verb == Cubic { points = append(points, s.Control1) } - if s.Verb == GeometryCubic { + if s.Verb == Cubic { points = append(points, s.Control2) } 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") } 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) remaining = pattern[index] @@ -255,10 +264,10 @@ func (line geometryPolyline) dash(pattern []float64, total, offset float64) ([]g steps++ if index%2 == 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} } - end := geometryLerp(a, b, (at+step)/length) + end := Lerp(a, b, (at+step)/length) if end != run[len(run)-1] { run = append(run, end) } @@ -284,8 +293,8 @@ func (line geometryPolyline) dash(pattern []float64, total, offset float64) ([]g // stroke 追加同向矩形、连接楔形及圆盘组成的非零填充区域 // 入参: result 已有轮廓, options 描边样式 -// 返回: GeometryPath 填充轮廓, error 不支持的退化方头端点 -func (line geometryPolyline) stroke(result GeometryPath, options StrokeOptions) (GeometryPath, error) { +// 返回: Path 填充轮廓, error 不支持的退化方头端点 +func (line geometryPolyline) stroke(result Path, options StrokeOptions) (Path, error) { half := options.Width / 2 polygon := func(points ...Point) { scale := 0.0 @@ -308,29 +317,29 @@ func (line geometryPolyline) stroke(result GeometryPath, options StrokeOptions) 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:] { - 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) { - result = append(result, GeometrySegment{Verb: GeometryMove, End: Point{p.X + half, p.Y}}, - GeometrySegment{Verb: GeometryArc, RadiusX: half, RadiusY: half, Sweep: true, End: Point{p.X - half, p.Y}}, - GeometrySegment{Verb: GeometryArc, RadiusX: half, RadiusY: half, Sweep: true, End: Point{p.X + half, p.Y}}, - GeometrySegment{Verb: GeometryClose}) + result = append(result, Segment{Verb: Move, End: Point{p.X + half, p.Y}}, + Segment{Verb: Arc, RadiusX: half, RadiusY: half, Sweep: true, End: Point{p.X - half, p.Y}}, + Segment{Verb: Arc, RadiusX: half, RadiusY: half, Sweep: true, End: Point{p.X + half, p.Y}}, + Segment{Verb: ClosePath}) } points := line.points if len(points) == 1 { if line.closed { - return nil, fmt.Errorf("directionless closed stroke: %w", ErrBackendUnavailable) + return nil, UnsupportedError("directionless closed stroke") } if options.Cap == "Round" { disk(points[0]) } else if options.Cap == "Square" { d := line.direction if d == (Point{}) { - return nil, fmt.Errorf("directionless square stroke cap: %w", ErrBackendUnavailable) + return nil, UnsupportedError("directionless square stroke cap") } p := points[0] u, v := Point{d.X * half, d.Y * half}, Point{-d.Y * half, d.X * half} @@ -365,16 +374,13 @@ func (line geometryPolyline) stroke(result GeometryPath, options StrokeOptions) if cross < 0 { x, y = y, x } - result = append(result, GeometrySegment{Verb: GeometryMove, End: p}, - GeometrySegment{Verb: GeometryLine, End: x}, - GeometrySegment{Verb: GeometryArc, RadiusX: half, RadiusY: half, Sweep: true, End: y}, - GeometrySegment{Verb: GeometryClose}) + result = append(result, Segment{Verb: Move, End: p}, + Segment{Verb: Line, End: x}, + Segment{Verb: Arc, RadiusX: half, RadiusY: half, Sweep: true, End: y}, + Segment{Verb: ClosePath}) return } limit := options.MiterLimit - if limit == 0 && !options.MiterLimitSet { - limit = defaultMiterLimit - } if options.Join == "" || options.Join == "Miter" { t := ((y.X-x.X)*b.Y - (y.Y-x.Y)*b.X) / cross m := Point{x.X + t*a.X, x.Y + t*a.Y} diff --git a/ofdgo_geom_transform.go b/internal/base/geom/transform.go similarity index 79% rename from ofdgo_geom_transform.go rename to internal/base/geom/transform.go index 706824a..9b4899c 100644 --- a/ofdgo_geom_transform.go +++ b/internal/base/geom/transform.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package ofdgo +package geom import ( "fmt" @@ -22,9 +22,9 @@ import ( // Transform 精确变换控制点和椭圆轴,退化弧沿投影极值分段保留折返 // 入参: matrix 仿射变换 -// 返回: GeometryPath 独立路径, error 无效路径或数值溢出 -func (p GeometryPath) Transform(matrix Matrix) (GeometryPath, error) { - if err := p.validate(); err != nil { +// 返回: Path 独立路径, error 无效路径或数值溢出 +func (p Path) Transform(matrix Matrix) (Path, error) { + if err := p.Validate(); err != nil { return nil, err } 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) return Point{x, y} } - var result GeometryPath + var result Path if len(p) > 0 { - result = make(GeometryPath, 0, len(p)) + result = make(Path, 0, len(p)) } var current, start Point for _, s := range p { @@ -51,15 +51,15 @@ func (p GeometryPath) Transform(matrix Matrix) (GeometryPath, error) { original := s.End s.End = transform(s.End) switch s.Verb { - case GeometryMove: + case Move: start = original - case GeometryQuad: + case Quad: s.Control1 = transform(s.Control1) - case GeometryCubic: + case Cubic: s.Control1, s.Control2 = transform(s.Control1), transform(s.Control2) - case GeometryClose: + case ClosePath: original, s.End = start, transform(start) - case GeometryArc: + case Arc: a, err := centerArc(current, originalSegment) if err != nil { return nil, err @@ -69,11 +69,11 @@ func (p GeometryPath) Transform(matrix Matrix) (GeometryPath, error) { continue } if s.RadiusX == 0 || s.RadiusY == 0 { - s = GeometrySegment{Verb: GeometryLine, End: s.End} + s = Segment{Verb: Line, End: s.End} } else { a.center = transform(a.center) - a.u.X, a.u.Y = matrixVector(matrix, a.u.X, a.u.Y) - a.v.X, a.v.Y = matrixVector(matrix, a.v.X, a.v.Y) + a.u.X, a.u.Y = Vector(matrix, a.u.X, a.u.Y) + a.v.X, a.v.Y = Vector(matrix, a.v.X, a.v.Y) scale := math.Max(math.Hypot(a.u.X, a.u.Y), math.Hypot(a.v.X, a.v.Y)) if !finite(scale) { return nil, fmt.Errorf("arc transform overflow") @@ -82,7 +82,7 @@ func (p GeometryPath) Transform(matrix Matrix) (GeometryPath, error) { if matrixScale != 0 { return nil, fmt.Errorf("arc transform precision exhausted") } - s = GeometrySegment{Verb: GeometryLine, End: s.End} + s = Segment{Verb: Line, End: s.End} } else { ux, uy, vx, vy := a.u.X/scale, a.u.Y/scale, a.v.X/scale, a.v.Y/scale det := ux*vy - uy*vx @@ -100,9 +100,9 @@ func (p GeometryPath) Transform(matrix Matrix) (GeometryPath, error) { } slices.Sort(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 { 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) @@ -121,32 +121,32 @@ func (p GeometryPath) Transform(matrix Matrix) (GeometryPath, error) { result = append(result, s) current = original } - if err := result.validate(); err != nil { + if err := result.Validate(); err != nil { return nil, err } return result, nil } -// validate 校验公共路径指令、子路径起点和有限坐标 +// Validate 校验公共路径指令、子路径起点和有限坐标 // 返回: error 无效路径 -func (p GeometryPath) validate() error { +func (p Path) Validate() error { started := false for _, s := range p { points := [3]Point{s.End, s.Control1, s.Control2} count := 1 switch s.Verb { - case GeometryMove: + case Move: started = true - case GeometryLine: - case GeometryQuad: + case Line: + case Quad: count = 2 - case GeometryCubic: + case Cubic: count = 3 - case GeometryArc: + case Arc: if !finite(s.RadiusX) || !finite(s.RadiusY) || !finite(s.Rotation) || s.RadiusX < 0 || s.RadiusY < 0 { return fmt.Errorf("invalid arc parameters") } - case GeometryClose: + case ClosePath: count = 0 default: return fmt.Errorf("unsupported geometry command %d", s.Verb) diff --git a/internal/base/threed/u3d.go b/internal/base/threed/u3d.go new file mode 100644 index 0000000..bcc9949 --- /dev/null +++ b/internal/base/threed/u3d.go @@ -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< 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 +} diff --git a/ofdgo_u3d_animation.go b/internal/base/threed/u3d_animation.go similarity index 97% rename from ofdgo_u3d_animation.go rename to internal/base/threed/u3d_animation.go index c6bd339..6b8950b 100644 --- a/ofdgo_u3d_animation.go +++ b/internal/base/threed/u3d_animation.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package ofdgo +package threed import ( "context" @@ -110,7 +110,7 @@ func (animation U3DAnimation) validate(ctx context.Context) error { // checkAnimations 校验全部动作引用,仅对接入场景的修饰器检查求值能力 // 返回: error 引用、能力或取消错误 -func (r *u3dRender) checkAnimations() error { +func (r *Renderer) checkAnimations() error { check := func(animations []U3DAnimation, evaluate bool) error { for _, animation := range animations { if err := animation.validate(r.ctx); err != nil { @@ -138,7 +138,7 @@ func (r *u3dRender) checkAnimations() error { } var active []bool 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 } defer func() { r.remaining += uint64(len(active)) }() diff --git a/ofdgo_u3d_bits.go b/internal/base/threed/u3d_bits.go similarity index 99% rename from ofdgo_u3d_bits.go rename to internal/base/threed/u3d_bits.go index a1657ad..ec2c866 100644 --- a/ofdgo_u3d_bits.go +++ b/internal/base/threed/u3d_bits.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package ofdgo +package threed import ( "encoding/binary" diff --git a/internal/base/threed/u3d_depth.go b/internal/base/threed/u3d_depth.go new file mode 100644 index 0000000..f7751b5 --- /dev/null +++ b/internal/base/threed/u3d_depth.go @@ -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 +} diff --git a/internal/base/threed/u3d_finite.go b/internal/base/threed/u3d_finite.go new file mode 100644 index 0000000..69f3059 --- /dev/null +++ b/internal/base/threed/u3d_finite.go @@ -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) +} diff --git a/ofdgo_u3d_light.go b/internal/base/threed/u3d_light.go similarity index 96% rename from ofdgo_u3d_light.go rename to internal/base/threed/u3d_light.go index 44a58a9..4b10235 100644 --- a/ofdgo_u3d_light.go +++ b/internal/base/threed/u3d_light.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package ofdgo +package threed import ( "fmt" @@ -47,7 +47,7 @@ type u3dRenderStyle struct { // collectLights 收集本轮根节点下启用的光源实例 // 返回: error 参数、预算或取消错误 -func (r *u3dRender) collectLights() error { +func (r *Renderer) collectLights() error { if r.lighting != nil { return nil } @@ -101,7 +101,7 @@ func (r *u3dRender) collectLights() error { } if len(r.lights) == cap(r.lights) { 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 } values := make([]u3dRenderLight, len(r.lights), capacity) @@ -117,7 +117,7 @@ func (r *u3dRender) collectLights() error { // overrideLighting 校验并建立替代光源,不依赖模型光源节点 // 入参: lighting 灯光覆盖,空值保留模型灯光 // 返回: error 参数、预算或取消错误 -func (r *u3dRender) overrideLighting(lighting *U3DLighting) error { +func (r *Renderer) overrideLighting(lighting *U3DLighting) error { if lighting == nil { return nil } @@ -126,7 +126,7 @@ func (r *u3dRender) overrideLighting(lighting *U3DLighting) error { 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 } r.lights = make([]u3dRenderLight, len(lighting.Lights)) @@ -158,7 +158,7 @@ func (r *u3dRender) overrideLighting(lighting *U3DLighting) error { // shaderPass 返回着色器的绘制轮次位图,默认着色器仅参与第一轮 // 入参: name 着色器名 // 返回: uint32 绘制轮次位图 -func (r *u3dRender) shaderPass(name string) uint32 { +func (r *Renderer) shaderPass(name string) uint32 { if index, exists := r.shaders[name]; exists { return r.model.Shaders[index].RenderPass } @@ -168,7 +168,7 @@ func (r *u3dRender) shaderPass(name string) uint32 { // style 解析着色器与材质,缺失资源采用ECMA-363第8.4节的默认资源 // 入参: name 着色器名 // 返回: 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} if index, ok := r.shaders[name]; ok { style.shader = r.model.Shaders[index] @@ -215,7 +215,7 @@ func (r *u3dRender) style(name string) (u3dRenderStyle, error) { // shade 按世界坐标计算逐顶点漫反射、镜面与环境光 // 入参: position 顶点, normal 法线, style 材质, diffuse 顶点漫反射, specular 顶点镜面颜色 // 返回: [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} if style.shader.Attributes&4 != 0 { if diffuse != nil { diff --git a/ofdgo_u3d_line.go b/internal/base/threed/u3d_line.go similarity index 95% rename from ofdgo_u3d_line.go rename to internal/base/threed/u3d_line.go index a572635..14a7fad 100644 --- a/ofdgo_u3d_line.go +++ b/internal/base/threed/u3d_line.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package ofdgo +package threed import ( "fmt" @@ -22,7 +22,7 @@ import ( // drawPrimitives 绘制作者点集及线集,独立着色且不参与面的背向剔除 // 入参: instance 场景实例, node 模型节点, mesh 属性数组, positions 世界位置, normals 世界法线, pass 绘制轮次, passIndex 轮次索引 // 返回: 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 { return nil } @@ -158,7 +158,7 @@ func (r *u3dRender) drawPrimitives(instance u3dInstance, node *U3DNode, mesh *U3 // lineIntersection 绘制作者线段与剖切面的交点,不绘制共面线段 // 入参: line 相机空间线段, index 剖切面索引 // 返回: 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] a, b := u3dPlaneDistance(section.plane, line[0].point), u3dPlaneDistance(section.plane, line[1].point) if !finite(a) || !finite(b) { @@ -201,7 +201,7 @@ func (r *u3dRender) lineIntersection(line [2]u3dVertex, index int) error { // frustumPlanes 返回保留正侧的六个相机视锥面 // 返回: [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 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 { @@ -261,7 +261,7 @@ func u3dInterpolate(a, b u3dVertex, t float64) u3dVertex { // sectionIntersection 绘制原三角面与指定剖切面的交线,其他剖切面仍参与裁剪 // 入参: triangle 原三角面, index 剖切面索引 // 返回: 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 { return err } @@ -352,7 +352,7 @@ func u3dClipLine(a, b *u3dVertex, plane [4]float64) (bool, error) { // rasterSectionLine 绘制单像素不透明交线,透视深度使用倒数插值 // 入参: a 已裁剪起点, b 已裁剪终点, rgb 交线颜色, normal 原面法线, origin 原面上一点 // 返回: 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} b.color = a.color 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 绘制单像素线段,颜色与深度按透视插值,面深度偏移防止自身遮挡 // 入参: a 已裁剪起点, b 已裁剪终点, normal 面法线, origin 面上点, shader 着色器, pass 绘制轮次 // 返回: 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) if err != nil { return err diff --git a/ofdgo_u3d_lineset.go b/internal/base/threed/u3d_lineset.go similarity index 99% rename from ofdgo_u3d_lineset.go rename to internal/base/threed/u3d_lineset.go index 4b83290..f3fa789 100644 --- a/ofdgo_u3d_lineset.go +++ b/internal/base/threed/u3d_lineset.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package ofdgo +package threed import ( "fmt" diff --git a/ofdgo_u3d_mapping.go b/internal/base/threed/u3d_mapping.go similarity index 99% rename from ofdgo_u3d_mapping.go rename to internal/base/threed/u3d_mapping.go index b2d4679..28e5a13 100644 --- a/ofdgo_u3d_mapping.go +++ b/internal/base/threed/u3d_mapping.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package ofdgo +package threed import ( "fmt" diff --git a/ofdgo_u3d_mesh.go b/internal/base/threed/u3d_mesh.go similarity index 99% rename from ofdgo_u3d_mesh.go rename to internal/base/threed/u3d_mesh.go index 4b13b77..869986d 100644 --- a/ofdgo_u3d_mesh.go +++ b/internal/base/threed/u3d_mesh.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package ofdgo +package threed import ( "fmt" diff --git a/ofdgo_u3d_mode.go b/internal/base/threed/u3d_mode.go similarity index 95% rename from ofdgo_u3d_mode.go rename to internal/base/threed/u3d_mode.go index de34e87..54f97e5 100644 --- a/ofdgo_u3d_mode.go +++ b/internal/base/threed/u3d_mode.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package ofdgo +package threed import ( "fmt" @@ -57,7 +57,7 @@ type u3dTexturedPrimitiveKey struct { // uniquePrimitive 在当前网格内去重着色边点,缓存按条目计入绘制预算 // 入参: a 起点或顶点, b 终点或相同顶点, shader 着色器 // 返回: 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 { return true, nil } @@ -81,7 +81,7 @@ func (r *u3dRender) uniquePrimitive(a, b u3dVertex, shader U3DShader) (bool, err if _, ok := r.texturedPrimitives[textured]; ok { return false, nil } - if err := r.reserve(1, 1024); err != nil { + if err := r.Reserve(1, 1024); err != nil { return false, err } 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 { return false, nil } - if err := r.reserve(1, 512); err != nil { + if err := r.Reserve(1, 512); err != nil { return false, err } r.primitives[key] = struct{}{} @@ -115,7 +115,7 @@ func (mode U3DRenderMode) silhouette() bool { // modeTriangle 根据当前模式绘制原三角面、原始边或顶点,不为裁剪边界生成网格边 // 入参: triangle 相机空间顶点, shader 着色器, pass 绘制轮次 // 返回: 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() { return r.sectionTriangle(triangle, shader, pass) } @@ -171,7 +171,7 @@ func (r *u3dRender) modeTriangle(triangle [3]u3dVertex, shader U3DShader, pass U // modeLine 对原始网格边应用剖切与视锥裁剪,再按像素深度绘制 // 入参: a 起点, b 终点, normal 面法线, origin 面上点, shader 着色器, pass 绘制轮次 // 返回: 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 { if i%256 == 0 { if err := r.ctx.Err(); err != nil { @@ -198,7 +198,7 @@ func (r *u3dRender) modeLine(a, b u3dVertex, normal, origin u3dVector, shader U3 // modePoint 绘制原始顶点,不在裁剪边界补充顶点 // 入参: vertex 顶点, shader 着色器, pass 绘制轮次 // 返回: 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 { return err } @@ -262,7 +262,7 @@ func (r *u3dRender) modePoint(vertex u3dVertex, shader U3DShader, pass U3DViewPa // drawBounds 在模型局部空间计算包围盒,再应用实例变换,不以世界坐标轴重建包围盒 // 入参: instance 场景实例, node 模型节点, mesh 网格, pass 绘制轮次, passIndex 轮次索引 // 返回: 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)} maximum := u3dVector{math.Inf(-1), math.Inf(-1), math.Inf(-1)} visible := false diff --git a/ofdgo_u3d_motion.go b/internal/base/threed/u3d_motion.go similarity index 99% rename from ofdgo_u3d_motion.go rename to internal/base/threed/u3d_motion.go index de67ef2..58737f9 100644 --- a/ofdgo_u3d_motion.go +++ b/internal/base/threed/u3d_motion.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package ofdgo +package threed import ( "fmt" diff --git a/ofdgo_u3d_outline.go b/internal/base/threed/u3d_outline.go similarity index 93% rename from ofdgo_u3d_outline.go rename to internal/base/threed/u3d_outline.go index 8f735df..9e622d6 100644 --- a/ofdgo_u3d_outline.go +++ b/internal/base/threed/u3d_outline.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package ofdgo +package threed import ( "fmt" @@ -39,7 +39,7 @@ type u3dOutlineEdge struct { // outlineFace 收集当前绘制轮次中的原始网格边,正面法线在同一半圆内取角度极值 // 入参: outline 共享边索引, world 世界坐标, vertices 相机顶点, front 是否正面, visible 是否绘制面, shaders 着色列表, passIndex 轮次 // 返回: 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 for i, name := range shaders { if i%128 == 0 { @@ -75,7 +75,7 @@ func (r *u3dRender) outlineFace(outline *u3dOutline, world [3]u3dVector, vertice key := [2]u3dVector{a, b} index, exists := outline.index[key] if !exists { - if err := r.reserve(1, 1024); err != nil { + if err := r.Reserve(1, 1024); err != nil { return err } index = len(outline.edges) @@ -116,7 +116,7 @@ func (r *u3dRender) outlineFace(outline *u3dOutline, world [3]u3dVector, vertice // drawOutline 绘制外缘、正背面交界和达到夹角的共享边,沿用像素深度去除遮挡 // 入参: outline 共享边索引, pass 绘制轮次 // 返回: error 数值、资源或取消错误 -func (r *u3dRender) drawOutline(outline *u3dOutline, pass U3DViewPass) error { +func (r *Renderer) drawOutline(outline *u3dOutline, pass U3DViewPass) error { shader := U3DShader{Blend: 0x606} color := [4]float64{r.auxiliaryColor[0], r.auxiliaryColor[1], r.auxiliaryColor[2], 1} for i, edge := range outline.edges { diff --git a/ofdgo_u3d_playback.go b/internal/base/threed/u3d_playback.go similarity index 90% rename from ofdgo_u3d_playback.go rename to internal/base/threed/u3d_playback.go index 90ad455..c87f13f 100644 --- a/ofdgo_u3d_playback.go +++ b/internal/base/threed/u3d_playback.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package ofdgo +package threed import ( "cmp" @@ -36,7 +36,7 @@ type u3dPlaybackKey struct { // u3dPlaybackCache 在一次动画求值中共享轨道时间范围和采样结果 type u3dPlaybackCache struct { - render *u3dRender + render *Renderer bounds map[*U3DMotionTrack][2]float64 samples map[u3dPlaybackKey]U3DKeyFrame 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)) } 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 } c.bytes += 1024 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 } c.bytes += 96 @@ -98,13 +98,13 @@ func (c *u3dPlaybackCache) sample(track *U3DMotionTrack, time float64) (U3DKeyFr return frame, err } 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 } c.bytes += 1024 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 } c.bytes += 128 @@ -115,7 +115,7 @@ func (c *u3dPlaybackCache) sample(track *U3DMotionTrack, time float64) (U3DKeyFr // prepareAnimations 按资源链、节点链顺序求值,不保存上一帧状态或推进实际时钟 // 入参: time 从初始时刻起算的秒数 // 返回: error 动作、骨架、修饰顺序、预算或取消错误 -func (r *u3dRender) prepareAnimations(time float64) error { +func (r *Renderer) prepareAnimations(time float64) error { cache := u3dPlaybackCache{render: r} defer func() { r.remaining += cache.bytes }() for i := range r.model.Nodes { @@ -136,7 +136,7 @@ func (r *u3dRender) prepareAnimations(time float64) error { for chain, group := range groups { ordered := group 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 } ordered = slices.Clone(group) @@ -167,12 +167,12 @@ func (r *u3dRender) prepareAnimations(time float64) error { } if used { if r.animated == nil { - if err := r.reserve(1, 1024); err != nil { + if err := r.Reserve(1, 1024); err != nil { return err } r.animated = make(map[string]u3dAnimationState) } - if err := r.reserve(1, 384); err != nil { + if err := r.Reserve(1, 384); err != nil { return err } r.animated[node.Name] = state @@ -184,7 +184,7 @@ func (r *u3dRender) prepareAnimations(time float64) error { // animationTime 将毫秒偏移和两级时间倍率换算为动作时间,已绑定轨道共用循环区间 // 入参: animation 动画修饰器, tracks 动作轨道, bindings 骨骼绑定,空值表示全部轨道, time 从初始时刻起算的秒数, cache 本次采样缓存 // 返回: 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] if animation.Attributes&1 == 0 { time = 0 @@ -225,7 +225,7 @@ func (r *u3dRender) animationTime(animation *U3DAnimation, tracks []U3DMotionTra // sampleAnimation 求值单个动作的节点或骨骼姿态,锁定根骨骼时保留其初始变换 // 入参: state 本节点累计状态, node 节点, mesh 模型几何, animation 修饰器, motion 动作, chain 链类别, time 时间, cache 本次采样缓存 // 返回: 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 chain == 0 { 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 { 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 } 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 { 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 } state.bones = make([]U3DBoneState, 0, len(mesh.Bones)) diff --git a/ofdgo_u3d_pointset.go b/internal/base/threed/u3d_pointset.go similarity index 99% rename from ofdgo_u3d_pointset.go rename to internal/base/threed/u3d_pointset.go index 47bd905..f070a59 100644 --- a/ofdgo_u3d_pointset.go +++ b/internal/base/threed/u3d_pointset.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package ofdgo +package threed import ( "fmt" diff --git a/ofdgo_u3d_render.go b/internal/base/threed/u3d_render.go similarity index 92% rename from ofdgo_u3d_render.go rename to internal/base/threed/u3d_render.go index 6656c1f..d5b8bd7 100644 --- a/ofdgo_u3d_render.go +++ b/internal/base/threed/u3d_render.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package ofdgo +package threed import ( "context" @@ -80,8 +80,8 @@ type u3dFragment struct { next int32 } -// u3dRender 保存一次绘制的有限预算,不修改调用方场景 -type u3dRender struct { +// Renderer 保存一次绘制的有限预算,不修改调用方场景 +type Renderer struct { ctx context.Context model *U3DModel remaining uint64 @@ -134,29 +134,29 @@ type u3dRender struct { // 入参: ctx 取消上下文, model 场景, options 视图、输出与资源限制 // 返回: *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 { return nil, err } - passes, err := r.selectView(options) + passes, err := r.SelectView(options) if err != nil { return nil, err } - return r.render(options, passes) + return r.Render(options, passes) } -// newU3DRender 建立有预算限制的场景实例与资源索引 +// NewRenderer 建立有预算限制的场景实例与资源索引 // 入参: ctx 取消上下文, model 场景, options 输出与资源限制, states 后续节点覆盖分组 -// 返回: *u3dRender 绘制状态, error 参数、场景、预算或取消错误 -func newU3DRender(ctx context.Context, model *U3DModel, options U3DRenderOptions, states ...[]U3DNodeState) (*u3dRender, error) { +// 返回: *Renderer 绘制状态, 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) { return nil, fmt.Errorf("invalid U3D render options") } limit := options.MaxMemoryBytes 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 { 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") } 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 } 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 { 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 } r.members = make([]bool, len(r.scene)) return r, nil } -// render 在已选择的相机和轮次下生成图像 +// Render 在已选择的相机和轮次下生成图像 // 入参: options 输出与灯光背景, passes 绘制轮次 // 返回: *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 err := r.prepareSections(options.Sections); err != nil { + if err := r.PrepareSections(options.Sections); err != nil { return nil, err } } @@ -292,10 +292,10 @@ func (r *u3dRender) render(options U3DRenderOptions, passes []U3DViewPass) (*ima return r.output, nil } -// reserve 在分配绘制缓存前扣减预算并检查取消状态 +// Reserve 在分配绘制缓存前扣减预算并检查取消状态 // 入参: count 元素数量, size 元素预算 // 返回: error 预算或取消错误 -func (r *u3dRender) reserve(count, size uint64) error { +func (r *Renderer) Reserve(count, size uint64) error { if err := r.ctx.Err(); err != nil { return err } @@ -306,10 +306,10 @@ func (r *u3dRender) reserve(count, size uint64) error { return nil } -// selectView 解析视图与相机覆盖,内部视口使用左上角坐标 +// SelectView 解析视图与相机覆盖,内部视口使用左上角坐标 // 入参: options 绘制选项 // 返回: []U3DViewPass 绘制轮次, error 视图或投影错误 -func (r *u3dRender) selectView(options U3DRenderOptions) ([]U3DViewPass, error) { +func (r *Renderer) SelectView(options U3DRenderOptions) ([]U3DViewPass, error) { r.view = nil r.viewport = [4]float64{0, 0, float64(options.Width), float64(options.Height)} selected := -1 @@ -413,9 +413,9 @@ func (r *u3dRender) selectView(options U3DRenderOptions) ([]U3DViewPass, error) // resources 建立只读资源索引,空名称保留给标准默认资源 // 返回: 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)) - if err := r.reserve(count, 96); err != nil { + if err := r.Reserve(count, 96); err != nil { return err } r.meshes = make(map[string]int, len(r.model.Meshes)) @@ -481,7 +481,7 @@ func (r *u3dRender) resources() error { // renderPass 分别解析遮挡与混合,绘制轮次之间保留颜色并重置深度 // 入参: pass 轮次参数, passIndex 轮次索引, last 是否为最后一轮 // 返回: 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 { return fmt.Errorf("invalid U3D render pass") } @@ -540,7 +540,7 @@ func (r *u3dRender) renderPass(pass U3DViewPass, passIndex int, last bool) error // drawScene 绘制当前轮次中的可见模型,不重置深度及片元缓存 // 入参: pass 绘制轮次, passIndex 轮次索引 // 返回: 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 { if err := r.ctx.Err(); err != nil { return err diff --git a/ofdgo_u3d_resource_reader.go b/internal/base/threed/u3d_resource_reader.go similarity index 99% rename from ofdgo_u3d_resource_reader.go rename to internal/base/threed/u3d_resource_reader.go index 9aa114a..ecd2ed7 100644 --- a/ofdgo_u3d_resource_reader.go +++ b/internal/base/threed/u3d_resource_reader.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package ofdgo +package threed import ( "fmt" diff --git a/ofdgo_u3d_sample.go b/internal/base/threed/u3d_sample.go similarity index 98% rename from ofdgo_u3d_sample.go rename to internal/base/threed/u3d_sample.go index 6c9c23d..b3e4653 100644 --- a/ofdgo_u3d_sample.go +++ b/internal/base/threed/u3d_sample.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package ofdgo +package threed import ( "context" @@ -183,7 +183,7 @@ func (frame U3DKeyFrame) Matrix() ([16]float64, error) { // trackMatrix 按资源和轨道名称采样节点变换,不推测未命名轨道的绑定关系 // 入参: state 动作轨道状态 // 返回: [16]float64 节点变换, error 引用、数值或取消错误 -func (r *u3dRender) trackMatrix(state U3DTrackState) ([16]float64, error) { +func (r *Renderer) trackMatrix(state U3DTrackState) ([16]float64, error) { var track *U3DMotionTrack if err := r.ctx.Err(); err != nil { return [16]float64{}, err diff --git a/ofdgo_u3d_scene.go b/internal/base/threed/u3d_scene.go similarity index 96% rename from ofdgo_u3d_scene.go rename to internal/base/threed/u3d_scene.go index 61d633e..7b2d3d9 100644 --- a/ofdgo_u3d_scene.go +++ b/internal/base/threed/u3d_scene.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package ofdgo +package threed import ( "fmt" @@ -176,9 +176,9 @@ func u3dTriangleNormal(points [3]u3dVector) u3dVector { // sceneGraph 校验父子关系并确定拓扑顺序,只有默认根的后代接入场景 // 返回: []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 - if err := r.reserve(uint64(len(nodes)), 161); err != nil { + if err := r.Reserve(uint64(len(nodes)), 161); err != nil { return nil, nil, err } 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.connected = make([]bool, len(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 } for k, parent := range node.Parents { @@ -270,7 +270,7 @@ func (r *u3dRender) sceneGraph() ([]int, map[string]int, error) { // instances 按拓扑顺序展开接入场景的父路径,不将未解析的父节点视为默认根 // 入参: order 拓扑顺序, index 节点索引, limit 最大实例数 // 返回: 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 for _, i := range order { 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 { 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 } world := local diff --git a/ofdgo_u3d_section.go b/internal/base/threed/u3d_section.go similarity index 94% rename from ofdgo_u3d_section.go rename to internal/base/threed/u3d_section.go index 1274191..929a518 100644 --- a/ofdgo_u3d_section.go +++ b/internal/base/threed/u3d_section.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package ofdgo +package threed import ( "fmt" @@ -37,17 +37,17 @@ type u3dSection struct { opacity float64 } -// prepareSections 准备剖切缓存,不复制网格或修改调用方参数 +// PrepareSections 准备剖切缓存,不复制网格或修改调用方参数 // 入参: sections 世界坐标剖切面 // 返回: error 参数、场景、预算或取消错误 -func (r *u3dRender) prepareSections(sections []U3DCrossSection) error { +func (r *Renderer) PrepareSections(sections []U3DCrossSection) error { if len(sections) == 0 { return nil } - if err := r.reserve(uint64(len(sections)), 1152); err != nil { + if err := r.Reserve(uint64(len(sections)), 1152); err != nil { 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 } r.sections = make([]u3dSection, len(sections)) @@ -132,7 +132,7 @@ func (r *u3dRender) prepareSections(sections []U3DCrossSection) error { // sceneDiagonal 计算静态场景世界包围盒的对角线,包含隐藏节点的几何 // 返回: 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)} maximum := u3dVector{math.Inf(-1), math.Inf(-1), math.Inf(-1)} for _, instance := range r.scene { @@ -180,7 +180,7 @@ func (r *u3dRender) sceneDiagonal() (float64, error) { // sectionTriangle 用复用的凸多边形缓存应用全部剖切面,再进行视锥裁剪 // 入参: triangle 三角面, shader 着色器, pass 绘制轮次 // 返回: 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 { return r.clipTriangle(triangle, shader, pass) } @@ -264,7 +264,7 @@ func u3dAppendSectionVertex(buffer []u3dVertex, count int, vertex u3dVertex) (in // drawSectionPlanes 在最后一轮绘制平面方形,不把平面本身当作待剖切几何 // 返回: error 数值、资源或取消错误 -func (r *u3dRender) drawSectionPlanes() error { +func (r *Renderer) drawSectionPlanes() error { for _, section := range r.sections { if err := r.ctx.Err(); err != nil { return err diff --git a/ofdgo_u3d_shader.go b/internal/base/threed/u3d_shader.go similarity index 96% rename from ofdgo_u3d_shader.go rename to internal/base/threed/u3d_shader.go index 3e81469..5e63378 100644 --- a/ofdgo_u3d_shader.go +++ b/internal/base/threed/u3d_shader.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package ofdgo +package threed import ( "fmt" @@ -48,7 +48,7 @@ type u3dTextureSet struct { // shaderTextures 校验并缓存启用的纹理,未使用的着色器不会触发图像解码 // 入参: shader 着色器 // 返回: *u3dTextureSet 纹理组合,空值表示无纹理, error 参数、能力、预算或取消错误 -func (r *u3dRender) shaderTextures(shader U3DShader) (*u3dTextureSet, error) { +func (r *Renderer) shaderTextures(shader U3DShader) (*u3dTextureSet, error) { if shader.Textures == ([8]*U3DTextureLayer{}) { return nil, nil } @@ -58,7 +58,7 @@ func (r *u3dRender) shaderTextures(shader U3DShader) (*u3dTextureSet, error) { if cached := r.textureSets[shader.Textures]; cached != nil { return cached, nil } - if err := r.reserve(1, 512); err != nil { + if err := r.Reserve(1, 512); err != nil { return nil, err } 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 err := r.reserve(1, 256); err != nil { + if err := r.Reserve(1, 256); err != nil { return nil, err } inverse, err := u3dTextureInverse(layer.WrapTransform) @@ -175,7 +175,7 @@ func (s *u3dTextureSet) coordinates(vertex *u3dVertex, mesh *U3DMesh, shading U3 // textureColor 合成纹理后恢复节点指定的透明度,再叠加视图透明度 // 入参: textures 纹理组合, color 顶点插值颜色, coordinates 纹理输入, position 相机空间位置, override 是否指定节点透明度 // 返回: 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 { return nil } diff --git a/ofdgo_u3d_skeleton.go b/internal/base/threed/u3d_skeleton.go similarity index 99% rename from ofdgo_u3d_skeleton.go rename to internal/base/threed/u3d_skeleton.go index 5141cf9..165c61f 100644 --- a/ofdgo_u3d_skeleton.go +++ b/internal/base/threed/u3d_skeleton.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package ofdgo +package threed import ( "fmt" diff --git a/ofdgo_u3d_skin.go b/internal/base/threed/u3d_skin.go similarity index 90% rename from ofdgo_u3d_skin.go rename to internal/base/threed/u3d_skin.go index 3077c7f..af4d8aa 100644 --- a/ofdgo_u3d_skin.go +++ b/internal/base/threed/u3d_skin.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package ofdgo +package threed import ( "fmt" @@ -44,7 +44,7 @@ type u3dSkinPose struct { // nodeMesh 返回节点本次绘制使用的几何,未变形节点直接引用原资源 // 入参: node 模型节点 // 返回: *U3DMesh 几何,资源缺失时为空 -func (r *u3dRender) nodeMesh(node *U3DNode) *U3DMesh { +func (r *Renderer) nodeMesh(node *U3DNode) *U3DMesh { if mesh := r.posed[node.Name]; mesh != nil { return mesh } @@ -56,7 +56,7 @@ func (r *u3dRender) nodeMesh(node *U3DNode) *U3DMesh { // poseNodes 应用动画和显式骨骼姿态,同源同姿态的节点及父路径共用变形几何 // 返回: error 骨架、权重、预算或取消错误 -func (r *u3dRender) poseNodes() error { +func (r *Renderer) poseNodes() error { if len(r.states) == 0 && len(r.animated) == 0 { 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) } if r.posed == nil { - if err := r.reserve(1, 1024); err != nil { + if err := r.Reserve(1, 1024); err != nil { return err } r.posed = make(map[string]*U3DMesh) } - if err := r.reserve(1, 128); err != nil { + if err := r.Reserve(1, 128); err != nil { return err } var temporary uint64 @@ -95,7 +95,7 @@ func (r *u3dRender) poseNodes() error { states = animation.bones } else { 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 } temporary = count * 64 @@ -148,19 +148,19 @@ func (r *u3dRender) poseNodes() error { r.posed[node.Name] = posed if len(r.model.Nodes) > 1 { if cache == nil { - if err := r.reserve(1, 1024); err != nil { + if err := r.Reserve(1, 1024); err != nil { return err } cacheBytes += 1024 cache = make(map[*U3DMesh]u3dSkinPose) } if !exists { - if err := r.reserve(1, 128); err != nil { + if err := r.Reserve(1, 128); err != nil { return err } cacheBytes += 128 } - if err := r.reserve(uint64(len(states)), 64); err != nil { + if err := r.Reserve(uint64(len(states)), 64); err != nil { return err } cacheBytes += uint64(len(states)) * 64 @@ -179,7 +179,7 @@ func (r *u3dRender) poseNodes() error { // boneTransforms 连接骨骼局部姿态,以初始姿态的逆变换将作者坐标映射到当前姿态 // 入参: bones 按父项在前排列的骨架, states 显式姿态 // 返回: []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)) for i, bone := range bones { if i%256 == 0 { @@ -260,14 +260,14 @@ func (r *u3dRender) boneTransforms(bones []U3DBone, states []U3DBoneState) ([]u3 // skinWeights 选取与几何类型相符的末项权重,节点链覆盖资源链 // 入参: mesh 原始几何, sets 节点权重 // 返回: *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) } // skinWeightSource 按修饰链截止位置选取权重,不使用动画后面的权重输出 // 入参: mesh 原始几何, sets 节点权重, resourceEnd、nodeEnd 两条链中首个不参与的索引 // 返回: *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) for i, count := range []int{len(mesh.Faces), len(mesh.Lines), len(mesh.Points)} { if count != 0 { @@ -307,7 +307,7 @@ func (r *u3dRender) skinWeightSource(mesh *U3DMesh, sets []U3DBoneWeights, resou // skinWeightSet 选取并校验指定修饰链位置之前的权重 // 入参: mesh 原始几何, sets 节点权重, resourceEnd、nodeEnd 两条链中首个不参与的索引 // 返回: *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) if err != nil { return nil, err @@ -355,7 +355,7 @@ func (weights *U3DBoneWeights) influence(position uint32) []U3DBoneWeight { // skinVector 对位置或法线应用加权骨骼变换,法线在累加后统一归一化 // 入参: value 原向量, weights 位置权重, bones 骨骼变换, normal 是否处理法线 // 返回: [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])} for _, component := range input { if !finite(component) { @@ -408,15 +408,15 @@ func (r *u3dRender) skinVector(value [3]float32, weights []U3DBoneWeight, bones // skinMesh 复制受变形影响的属性,按位置与法线的组合保留独立法线索引 // 入参: mesh 原始几何, sets 节点权重, states 显式骨骼姿态 // 返回: *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) if err != nil { 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 } - 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 return nil, err } @@ -426,10 +426,10 @@ func (r *u3dRender) skinMesh(mesh *U3DMesh, sets []U3DBoneWeights, states []U3DB if err != nil { return nil, err } - if err := r.reserve(1, 768); err != nil { + if err := r.Reserve(1, 768); err != nil { 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 } result := *mesh @@ -449,12 +449,12 @@ func (r *u3dRender) skinMesh(mesh *U3DMesh, sets []U3DBoneWeights, states []U3DB return &result, r.ctx.Err() } 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 } result.Faces, result.Lines, result.Points = slices.Clone(mesh.Faces), slices.Clone(mesh.Lines), slices.Clone(mesh.Points) result.Normals = nil - if err := r.reserve(1, 1024); err != nil { + if err := r.Reserve(1, 1024); err != nil { return nil, err } 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} normal, exists := index[key] if !exists { - if err := r.reserve(1, 96); err != nil { + if err := r.Reserve(1, 96); err != nil { return nil, err } 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 { 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 } grown := make([][3]float32, len(result.Normals), int(capacity)) diff --git a/ofdgo_u3d_state.go b/internal/base/threed/u3d_state.go similarity index 93% rename from ofdgo_u3d_state.go rename to internal/base/threed/u3d_state.go index 4ca03e0..781176b 100644 --- a/ofdgo_u3d_state.go +++ b/internal/base/threed/u3d_state.go @@ -12,9 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -package ofdgo +package threed -import "fmt" +import ( + "fmt" +) // U3DNodeState 按名称覆盖节点,未指定透明度或可见性时沿父路径继承 // Opacity范围为0到1,采用标准透明混合;Matrix为相对父节点的列式仿射矩阵 @@ -34,7 +36,7 @@ type U3DNodeState struct { // nodeStates 建立有预算限制的覆盖索引,重复名称整体替换而不合并字段 // 入参: groups 按优先级递增排列的节点覆盖分组 // 返回: error 数值、预算或取消错误 -func (r *u3dRender) nodeStates(groups ...[]U3DNodeState) error { +func (r *Renderer) nodeStates(groups ...[]U3DNodeState) error { var count uint64 for _, states := range groups { if uint64(len(states)) > ^uint64(0)-count { @@ -45,7 +47,7 @@ func (r *u3dRender) nodeStates(groups ...[]U3DNodeState) error { if count == 0 { return nil } - if err := r.reserve(count, 160); err != nil { + if err := r.Reserve(count, 160); err != nil { return err } 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") } if samples == nil { - if err := r.reserve(1, 1024); err != nil { + if err := r.Reserve(1, 1024); err != nil { return err } samples = make(map[U3DTrackState]*[16]float64) } matrix := samples[*state.Motion] if matrix == nil { - if err := r.reserve(1, 320); err != nil { + if err := r.Reserve(1, 320); err != nil { return err } value, err := r.trackMatrix(*state.Motion) diff --git a/ofdgo_u3d_texture.go b/internal/base/threed/u3d_texture.go similarity index 99% rename from ofdgo_u3d_texture.go rename to internal/base/threed/u3d_texture.go index a0b1215..b2a6d5a 100644 --- a/ofdgo_u3d_texture.go +++ b/internal/base/threed/u3d_texture.go @@ -12,9 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -package ofdgo +package threed -import "fmt" +import ( + "fmt" +) // U3DTexture 保存纹理尺寸、图像类型及各通道的图像数据 type U3DTexture struct { diff --git a/ofdgo_u3d_texture_decode.go b/internal/base/threed/u3d_texture_decode.go similarity index 92% rename from ofdgo_u3d_texture_decode.go rename to internal/base/threed/u3d_texture_decode.go index 0b6541e..d25d2f6 100644 --- a/ofdgo_u3d_texture_decode.go +++ b/internal/base/threed/u3d_texture_decode.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package ofdgo +package threed import ( "bytes" @@ -64,19 +64,19 @@ func (r *u3dImageReader) ReadAt(p []byte, offset int64) (int, error) { // texture 按需合成纹理并缓存,空名称使用ECMA-363第8.4.1节规定的默认纹理 // 入参: name 纹理名称 // 返回: *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 { return nil, err } if cached := r.textures[name]; cached != nil { return cached, nil } - if err := r.reserve(1, 256); err != nil { + if err := r.Reserve(1, 256); err != nil { return nil, err } var pixels *image.NRGBA64 if name == "" { - if err := r.reserve(64, 8); err != nil { + if err := r.Reserve(64, 8); err != nil { return nil, err } pixels = image.NewNRGBA64(image.Rect(0, 0, 8, 8)) @@ -110,7 +110,7 @@ func (r *u3dRender) texture(name string) (*image.NRGBA64, error) { // decodeTexture 合成各续图像指定的通道,保留十六位颜色与非预乘透明度 // 入参: texture 纹理资源 // 返回: *image.NRGBA64 合成图像, error 格式、预算或取消错误 -func (r *u3dRender) decodeTexture(texture *U3DTexture) (*image.NRGBA64, error) { +func (r *Renderer) decodeTexture(texture *U3DTexture) (*image.NRGBA64, error) { switch texture.ImageType { case 1, 14, 15, 16, 17: default: @@ -139,7 +139,7 @@ func (r *u3dRender) decodeTexture(texture *U3DTexture) (*image.NRGBA64, error) { if covered != u3dTextureChannels(texture.ImageType) { 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 } 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 读取内置通道或按顺序尝试外部地址,不修改模型中的图像声明 // 入参: pixels 目标纹理, source 通道图像 // 返回: error 读取、解码、预算或取消错误 -func (r *u3dRender) textureSource(pixels *image.NRGBA64, source U3DTextureImage) error { +func (r *Renderer) textureSource(pixels *image.NRGBA64, source U3DTextureImage) error { if !source.External { return r.textureChannels(pixels, source) } @@ -202,7 +202,7 @@ func (r *u3dRender) textureSource(pixels *image.NRGBA64, source U3DTextureImage) // textureChannels 解码并合成一幅续图像,临时预算包括像素、分块及压缩数据开销 // 入参: pixels 目标纹理, source 通道图像 // 返回: 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 decode func(io.Reader) (image.Image, error) switch source.Compression { @@ -217,10 +217,10 @@ func (r *u3dRender) textureChannels(pixels *image.NRGBA64, source U3DTextureImag } budget := r.remaining 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 } - if err := r.reserve(1, 1<<20); err != nil { + if err := r.Reserve(1, 1<<20); err != nil { return err } 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 { 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 } reader.data.Reset(source.Data) diff --git a/ofdgo_u3d_triangle.go b/internal/base/threed/u3d_triangle.go similarity index 95% rename from ofdgo_u3d_triangle.go rename to internal/base/threed/u3d_triangle.go index cd0883c..c006f56 100644 --- a/ofdgo_u3d_triangle.go +++ b/internal/base/threed/u3d_triangle.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package ofdgo +package threed import ( "fmt" @@ -32,7 +32,7 @@ type u3dVertex struct { // drawMesh 按实例复用顶点变换,着色列表按指定顺序绘制 // 入参: instance 场景实例, node 模型节点, mesh 网格, pass 绘制轮次, passIndex 轮次索引 // 返回: 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 { 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 var outline *u3dOutline if r.mode.silhouette() { - if err := r.reserve(1, 1024); err != nil { + if err := r.Reserve(1, 1024); err != nil { return err } outline = &u3dOutline{index: make(map[[2]u3dVector]int)} defer func() { r.remaining += 1024 + uint64(len(outline.edges))*1024 }() } 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 } r.primitives = make(map[u3dPrimitiveKey]struct{}) @@ -64,7 +64,7 @@ func (r *u3dRender) drawMesh(instance u3dInstance, node *U3DNode, mesh *U3DMesh, normalCount = len(mesh.Normals) } 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 } defer func() { r.remaining += count * 24 }() @@ -244,7 +244,7 @@ func (r *u3dRender) drawMesh(instance u3dInstance, node *U3DNode, mesh *U3DMesh, // clipTriangle 在透视除法前裁剪六个视锥面,避免近面穿越或屏外巨大坐标 // 入参: triangle 三角面, shader 着色器, pass 绘制轮次 // 返回: 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 { return r.ctx.Err() } @@ -301,7 +301,7 @@ func (r *u3dRender) clipTriangle(triangle [3]u3dVertex, shader U3DShader, pass U // rasterTriangle 使用像素中心和左上边规则光栅化,透视插值保持颜色与深度一致 // 入参: triangle 已裁剪三角面, shader 着色器, pass 绘制轮次 // 返回: 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) if err != nil { return err @@ -432,7 +432,7 @@ func (r *u3dRender) rasterTriangle(triangle [3]u3dVertex, shader U3DShader, pass // fragment 直接保存不透明覆盖,透明及其他混合方式保留逐像素深度 // 入参: pixel 像素索引, depth 深度, color 颜色, blend 混合方式 // 返回: 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) { 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 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 } 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 按每个像素的远近排序,正确处理相交透明面和着色器叠加 // 返回: error 预算或取消错误 -func (r *u3dRender) resolveFragments() error { +func (r *Renderer) resolveFragments() error { if r.fragments == 0 { return r.ctx.Err() } @@ -491,7 +491,7 @@ func (r *u3dRender) resolveFragments() error { } if len(r.scratch) == cap(r.scratch) { 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 } values := make([]int, len(r.scratch), capacity) diff --git a/ofdgo_u3d_view.go b/internal/base/threed/u3d_view.go similarity index 99% rename from ofdgo_u3d_view.go rename to internal/base/threed/u3d_view.go index d23c9ff..142d6b3 100644 --- a/ofdgo_u3d_view.go +++ b/internal/base/threed/u3d_view.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package ofdgo +package threed import ( "fmt" diff --git a/internal/base/threed/u3d_view_bounds.go b/internal/base/threed/u3d_view_bounds.go new file mode 100644 index 0000000..2cb5fbd --- /dev/null +++ b/internal/base/threed/u3d_view_bounds.go @@ -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)) +} diff --git a/internal/base/threed/u3d_view_camera.go b/internal/base/threed/u3d_view_camera.go new file mode 100644 index 0000000..c0dea49 --- /dev/null +++ b/internal/base/threed/u3d_view_camera.go @@ -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 +} diff --git a/ofdgo_u3d_view_render.go b/internal/base/threed/u3d_view_render.go similarity index 93% rename from ofdgo_u3d_view_render.go rename to internal/base/threed/u3d_view_render.go index c6f2e0b..cba2959 100644 --- a/ofdgo_u3d_view_render.go +++ b/internal/base/threed/u3d_view_render.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package ofdgo +package threed import ( "fmt" @@ -23,7 +23,7 @@ import ( // viewLayers 按ECMA-363第9.5.4节依次绘制背景层或覆盖层 // 入参: layers 按后至前排列的纹理层 // 返回: error 参数、资源、预算或取消错误 -func (r *u3dRender) viewLayers(layers []U3DViewLayer) error { +func (r *Renderer) viewLayers(layers []U3DViewLayer) error { for _, layer := range layers { if err := r.ctx.Err(); err != nil { return err @@ -49,8 +49,8 @@ func (r *u3dRender) viewLayers(layers []U3DViewLayer) error { } sx, sy := float64(layer.Scale[0]), float64(layer.Scale[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} - bounds := rasterImageBounds(texture.Bounds(), m, r.output.Bounds()) + 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 := viewImageBounds(texture.Bounds(), m, r.output.Bounds()) for py := bounds.Min.Y; py < bounds.Max.Y; py++ { for px := bounds.Min.X; px < bounds.Max.X; px++ { if px%4096 == 0 || px == bounds.Min.X { diff --git a/ofdgo_u3d_weights.go b/internal/base/threed/u3d_weights.go similarity index 99% rename from ofdgo_u3d_weights.go rename to internal/base/threed/u3d_weights.go index 6af44df..ec21774 100644 --- a/ofdgo_u3d_weights.go +++ b/internal/base/threed/u3d_weights.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package ofdgo +package threed import ( "fmt" diff --git a/ofdgo_action.go b/ofdgo_action.go index 06816a3..cdae52d 100644 --- a/ofdgo_action.go +++ b/ofdgo_action.go @@ -17,6 +17,7 @@ package ofdgo import ( "encoding/xml" "fmt" + basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom" "net/url" "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 { source.Box.W, source.Box.H = box.W, box.H } - if box.W > 0 && box.H > 0 && !axisAlignedMatrix(placement) { - path := geometryRectangle(Box{W: box.W, H: box.H}) + if box.W > 0 && box.H > 0 && !basegeom.AxisAligned(basegeom.MatrixFromValues(placement.Values())) { + path := GeometryPath(basegeom.Rectangle(Box{W: box.W, H: box.H})) for i := range path { path[i].End.X, path[i].End.Y = placement.Transform(path[i].End.X, path[i].End.Y) } diff --git a/ofdgo_editor_action.go b/ofdgo_editor_action.go index 2212c0c..9757c75 100644 --- a/ofdgo_editor_action.go +++ b/ofdgo_editor_action.go @@ -178,7 +178,7 @@ func validateActionRegion(region *Region) error { if err != nil { return err } - current := Point{values[0], values[1]} + current := Point{X: values[0], Y: values[1]} start := current if len(area.Command) == 0 { return fmt.Errorf("action area requires a path command") @@ -222,7 +222,7 @@ func validateActionRegion(region *Region) error { if err != nil { return err } - end = Point{values[0], values[1]} + end = Point{X: values[0], Y: values[1]} } if command.Type == "Arc" && end == current { return fmt.Errorf("arc endpoint equals current point") diff --git a/ofdgo_editor_batch.go b/ofdgo_editor_batch.go index 554f910..98f1f1c 100644 --- a/ofdgo_editor_batch.go +++ b/ofdgo_editor_batch.go @@ -18,6 +18,7 @@ import ( "cmp" "context" "fmt" + basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom" "reflect" "slices" "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) for i, index := range order[:len(order)-1] { step := sizes[index] + gap - if geometryEqual(step, 0) { + if basegeom.Equal(step, 0) { step = 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] { position := next next += sizes[index] + gap - if geometryEqual(position, positions[index]) { + if basegeom.Equal(position, positions[index]) { continue } dx, dy := position-positions[index], 0.0 @@ -791,10 +792,10 @@ func editorAlignment(box, target Box, alignment string) Matrix { case "bottom": matrix.f = target.Y + target.H - box.H - box.Y } - if geometryEqual(matrix.e, 0) { + if basegeom.Equal(matrix.e, 0) { matrix.e = 0 } - if geometryEqual(matrix.f, 0) { + if basegeom.Equal(matrix.f, 0) { matrix.f = 0 } return matrix diff --git a/ofdgo_editor_composite_copy.go b/ofdgo_editor_composite_copy.go index 333a96c..6b61137 100644 --- a/ofdgo_editor_composite_copy.go +++ b/ofdgo_editor_composite_copy.go @@ -18,6 +18,7 @@ import ( "bytes" "encoding/xml" "fmt" + basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom" "maps" "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) { return false, nil } - if axisAlignedMatrix(matrix) { + if basegeom.AxisAligned(basegeom.MatrixFromValues(matrix.Values())) { return true, nil } var targets []*editorXML diff --git a/ofdgo_editor_composite_ops.go b/ofdgo_editor_composite_ops.go index efbfed8..822d271 100644 --- a/ofdgo_editor_composite_ops.go +++ b/ofdgo_editor_composite_ops.go @@ -18,6 +18,7 @@ import ( "bytes" "context" "fmt" + basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom" "maps" "slices" @@ -165,7 +166,7 @@ func (m CompositeMember) Shape() (ShapeKind, Box) { if lineShapeKind(kind) { return kind, transformLineBox(box, parent) } - if !axisAlignedMatrix(parent) { + if !basegeom.AxisAligned(basegeom.MatrixFromValues(parent.Values())) { return "", Box{} } return kind, parent.TransformBox(box) @@ -303,7 +304,7 @@ func (m CompositeMember) shapeParent() (Matrix, bool) { // 返回: Box 变换后的端点 func transformLineBox(box Box, matrix Matrix) Box { 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} } diff --git a/ofdgo_editor_erase.go b/ofdgo_editor_erase.go index d77bb40..68b6297 100644 --- a/ofdgo_editor_erase.go +++ b/ofdgo_editor_erase.go @@ -16,6 +16,7 @@ package ofdgo import ( "fmt" + basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom" "math" "reflect" "slices" @@ -58,7 +59,7 @@ func (e *Editor) EraseCompositeObjects(page int, path ObjectPath, indexes []int, if err := validateCreationBox(box); err != nil { return err } - return e.eraseCompositeObjects(page, path, indexes, geometryRectangle(box)) + return e.eraseCompositeObjects(page, path, indexes, GeometryPath(basegeom.Rectangle(box))) } // EraseCompositeObjectsPath 按闭合折线擦除内部成员,末点自动连接起点 @@ -113,7 +114,7 @@ func (e *Editor) eraseCompositeObjects(page int, path ObjectPath, indexes []int, if box.W <= 0 || box.H <= 0 { continue } - bounds := geometryRectangle(box) + bounds := GeometryPath(basegeom.Rectangle(box)) intersection, err := geometry.Combine(bounds, region, GeometryIntersect) if err != nil { return err @@ -137,7 +138,7 @@ func (e *Editor) eraseCompositeObjects(page int, path ObjectPath, indexes []int, if !ok { 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) if err != nil { 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 if region != nil { - path := geometryRectangle(bounds) + path := GeometryPath(basegeom.Rectangle(bounds)) intersection, err := e.backends.Geometry.Combine(path, region, GeometryIntersect) if err != nil { return err diff --git a/ofdgo_editor_transform.go b/ofdgo_editor_transform.go index 28f4bea..5fc944a 100644 --- a/ofdgo_editor_transform.go +++ b/ofdgo_editor_transform.go @@ -18,6 +18,7 @@ import ( "bytes" "encoding/xml" "fmt" + basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom" "math" "slices" ) @@ -206,7 +207,7 @@ func (obj TextObject) ResizeTextFrame(offset, width float64) (TextObject, error) if err != nil { 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") } 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() { *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) if *clips == nil { *clips = &Clips{} diff --git a/ofdgo_font_metrics.go b/ofdgo_font_metrics.go index caf46b5..eee4164 100644 --- a/ofdgo_font_metrics.go +++ b/ofdgo_font_metrics.go @@ -50,25 +50,25 @@ type glyphGeometry struct{ path GeometryPath } // MoveTo 开始子路径 // 入参: x 横坐标, y 纵坐标 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 添加直线 // 入参: x 横坐标, y 纵坐标 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 添加二次贝塞尔曲线 // 入参: cx 控制点横坐标, cy 控制点纵坐标, x 终点横坐标, y 终点纵坐标 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 添加三次贝塞尔曲线 // 入参: ax 第一控制点横坐标, ay 第一控制点纵坐标, bx 第二控制点横坐标, by 第二控制点纵坐标, x 终点横坐标, y 终点纵坐标 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 闭合当前子路径 diff --git a/ofdgo_geom.go b/ofdgo_geom.go index 5d07c02..279348b 100644 --- a/ofdgo_geom.go +++ b/ofdgo_geom.go @@ -16,24 +16,21 @@ package ofdgo import ( "fmt" - "math" "slices" "strconv" "strings" + + basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom" ) // IdentityMatrix 单位矩阵 var IdentityMatrix = Matrix{1, 0, 0, 1, 0, 0} // Box 矩形区域 -type Box struct { - X, Y, W, H float64 -} +type Box = basegeom.Box // Point 平面坐标点 -type Point struct { - X, Y float64 -} +type Point = basegeom.Point // Matrix 2D仿射变换矩阵 type Matrix struct { @@ -95,36 +92,21 @@ func NewMatrix(s string) Matrix { // 入参: 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, - } + return MatrixFromValues(basegeom.MatrixFromValues(m.Values()).Multiply(basegeom.MatrixFromValues(o.Values())).Values()) } // 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 + return basegeom.MatrixFromValues(m.Values()).Transform(x, y) } // 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} + return basegeom.MatrixFromValues(m.Values()).TransformBox(box) } // String 返回标准CTM属性值 @@ -136,109 +118,24 @@ func (m Matrix) String() string { // 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 { + result, ok := basegeom.MatrixFromValues(m.Values()).Invert() + if !ok { 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 + return MatrixFromValues(result.Values()), true } // TranslationMatrix 构造平移矩阵 // 入参: x X轴偏移, y Y轴偏移 // 返回: 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轴缩放比例 // 返回: float64 缩放比例 func (m Matrix) YScale() float64 { - return math.Sqrt(m.c*m.c + m.d*m.d) -} - -// 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 + return basegeom.MatrixFromValues(m.Values()).YScale() } // parseFloats 解析浮点数数组 diff --git a/ofdgo_geom_backend.go b/ofdgo_geom_backend.go index d07e771..e62be1a 100644 --- a/ofdgo_geom_backend.go +++ b/ofdgo_geom_backend.go @@ -16,7 +16,8 @@ package ofdgo import ( "fmt" - "math" + + basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom" ) // 路径填充区域的交集、并集、差集和异或运算 @@ -35,15 +36,7 @@ type GeometryOperation uint8 // MiterLimit为尖角长度与半线宽的比值上限,MiterLimitSet区分显式零值与默认值 // DashOffset为虚线起始偏移,允许负值;Dashes交替指定实线段和间隔长度,奇数项按重复一遍处理 // Tolerance为曲线展开的误差目标,0采用后端默认精度 -type StrokeOptions struct { - Width float64 - Cap, Join string - MiterLimit float64 - MiterLimitSet bool - DashOffset float64 - Tolerance float64 - Dashes []float64 -} +type StrokeOptions = basegeom.StrokeOptions // GeometryBackend 操作库自有路径,坐标以页面左上角为原点,不修改任何输入 // Clip返回nil表示不裁剪,空路径指针表示完全裁去,矩阵将裁剪坐标映射到页面坐标 @@ -97,72 +90,6 @@ func (r *Renderer) PathOutline(object PathObject) (string, error) { 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 将非零填充路径与页面裁剪相交 // 入参: geometry 几何后端, path 页面路径, clip 页面裁剪 // 返回: GeometryPath 裁剪路径, error 几何错误 @@ -173,14 +100,14 @@ func clipGeometry(geometry GeometryBackend, path GeometryPath, clip *GeometryPat if len(*clip) == 0 { return nil, nil } - if rect, ok := geometryRectangleBounds(*clip); ok { - if bounds, ok := geometryRectangleBounds(path); ok { + if rect, ok := basegeom.RectangleBounds(basegeom.Path(*clip)); ok { + if bounds, ok := basegeom.RectangleBounds(basegeom.Path(path)); ok { 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) if right <= left || bottom <= top { 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 { return nil, err @@ -214,7 +141,7 @@ func clipGeometry(geometry GeometryBackend, path GeometryPath, clip *GeometryPat return path, nil } } - if rect, ok := geometryRectangleBounds(path); ok { + if rect, ok := basegeom.RectangleBounds(basegeom.Path(path)); ok { bounds, err := geometry.Bounds(*clip) if err != nil { return nil, err diff --git a/ofdgo_geom_path.go b/ofdgo_geom_path.go index f643fa6..fdd8860 100644 --- a/ofdgo_geom_path.go +++ b/ofdgo_geom_path.go @@ -18,16 +18,18 @@ import ( "fmt" "strconv" "strings" + + basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom" ) // 几何路径指令,包括直线、曲线、椭圆弧和闭合 const ( - GeometryMove GeometryVerb = iota - GeometryLine - GeometryQuad - GeometryCubic - GeometryArc - GeometryClose + GeometryMove = basegeom.Move + GeometryLine = basegeom.Line + GeometryQuad = basegeom.Quad + GeometryCubic = basegeom.Cubic + GeometryArc = basegeom.Arc + GeometryClose = basegeom.ClosePath ) // GeometryPath 保存独立于绘图库的几何路径,不扁平化曲线或舍入坐标 @@ -35,18 +37,11 @@ const ( type GeometryPath []GeometrySegment // GeometryVerb 表示几何路径指令 -type GeometryVerb uint8 +type GeometryVerb = basegeom.Verb // GeometrySegment 保存路径端点、贝塞尔控制点或端点式椭圆弧参数 // 弧线Rotation单位为度,Sweep为true表示顺时针,Large表示大弧 -type GeometrySegment struct { - Verb GeometryVerb - End Point - Control1, Control2 Point - RadiusX, RadiusY float64 - Rotation float64 - Large, Sweep bool -} +type GeometrySegment = basegeom.Segment // ParseGeometryPath 解析标准OFD紧缩路径,保留弧线和原始数值精度 // 入参: data 紧缩路径 @@ -137,18 +132,18 @@ func geometryPathSegment(command string, args []string) (GeometrySegment, error) segment := GeometrySegment{} switch command { 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": - segment.Verb, segment.End = GeometryLine, Point{values[0], values[1]} + segment.Verb, segment.End = GeometryLine, Point{X: values[0], Y: values[1]} 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": - 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": 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") } - 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": 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))) } -// 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 // 入参: svg 是否使用SVG指令 // 返回: 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) 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() +} diff --git a/ofdgo_paint_mesh.go b/ofdgo_paint_mesh.go index 8fbfdd8..0d79434 100644 --- a/ofdgo_paint_mesh.go +++ b/ofdgo_paint_mesh.go @@ -239,7 +239,7 @@ func (r *Renderer) resolveMesh(fill *FillColor) (*MeshShading, error) { if value := mergeAlpha(point.Color.Alpha, fill.Alpha); value != nil { 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{} if extend == 1 && back != nil { diff --git a/ofdgo_paint_model.go b/ofdgo_paint_model.go index 6b5181d..03d9be0 100644 --- a/ofdgo_paint_model.go +++ b/ofdgo_paint_model.go @@ -14,7 +14,10 @@ package ofdgo -import "image/color" +import ( + basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom" + "image/color" +) // 画刷类型,区分纯色、渐变和图案填充 const ( @@ -94,7 +97,7 @@ func (r *Renderer) ResolvePaint(fill *FillColor) (Paint, error) { if err != nil { 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.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 diff --git a/ofdgo_pdf.go b/ofdgo_pdf.go index c08b956..e3afcbf 100644 --- a/ofdgo_pdf.go +++ b/ofdgo_pdf.go @@ -431,7 +431,7 @@ func (b *pdfPointBounds) box() Box { if !b.found { 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 逐段计算变换后的控制点边界,不创建临时点集合 @@ -465,7 +465,7 @@ func (p *pdfImporter) pathBoundsContext(ctx context.Context, path pdfgo.Path) (B if !found { 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 保留设备色彩分量并复用文档颜色空间 @@ -662,7 +662,7 @@ func (p *pdfImporter) appendPath(mark pdfgo.PathMark) error { } if mark.Stroke { 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{}) { origin = box diff --git a/ofdgo_pdf_3d.go b/ofdgo_pdf_3d.go index c9920c0..0bb6055 100644 --- a/ofdgo_pdf_3d.go +++ b/ofdgo_pdf_3d.go @@ -22,6 +22,7 @@ import ( "math" "strings" + basethreed "github.com/xiaoqidun/ofdgo/internal/base/threed" "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 { return nil, err } - r, err := newU3DRender(ctx, model, render, options.Nodes) + r, err := basethreed.NewRenderer(ctx, model, render, options.Nodes) if err != nil { return nil, err } - camera, view, err := r.pdfCamera(source.Camera) + camera, view, err := pdfThreeDCamera(ctx, model, r, source.Camera) if err != nil { return nil, err } render.View = view camera.ScaleX, camera.ScaleY, camera.Near = 1, 1, 1 render.Camera = &camera - passes, err := r.selectView(render) + passes, err := r.SelectView(render) if err != nil { return nil, err } - if err := r.prepareSections(render.Sections); err != nil { + if err := r.PrepareSections(render.Sections); err != nil { return nil, err } scale, err := projection.ScaleFactor(source.ViewBox, source.AnnotationBox) @@ -268,7 +269,7 @@ func RenderPDFThreeD(ctx context.Context, model *U3DModel, source pdfgo.ThreeD, } } case "ANF": - minimum, maximum, found, err := r.depthRange() + minimum, maximum, found, err := r.DepthRange() if err != nil { return nil, err } @@ -292,11 +293,11 @@ func RenderPDFThreeD(ctx context.Context, model *U3DModel, source pdfgo.ThreeD, default: 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 } 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 } 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} } } - return r.render(render, passes) + return r.Render(render, passes) } -// pdfCamera 解析PDF相机来源并保留模型视图的绘制轮次 -// 入参: source 相机来源 +// pdfThreeDCamera 解析PDF相机来源并保留模型视图的绘制轮次 +// 入参: ctx 取消上下文, model 三维模型, renderer 场景实例, source 相机来源 // 返回: 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 selected := -1 - for i, node := range r.model.Nodes { + for i, node := range model.Nodes { if i%256 == 0 { - if err := r.ctx.Err(); err != nil { + if err := ctx.Err(); err != nil { return camera, "", err } } @@ -342,9 +343,9 @@ func (r *u3dRender) pdfCamera(source pdfgo.ThreeDCamera) (U3DCamera, string, err } if source.Source == "U3D" { selected = -1 - for i, node := range r.model.Nodes { + for i, node := range model.Nodes { if i%256 == 0 { - if err := r.ctx.Err(); err != nil { + if err := ctx.Err(); err != nil { 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") } - found := false - for _, index := range r.byNode[selected] { - if err := r.ctx.Err(); err != nil { - 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"} - } - camera.ToWorld, found = r.scene[index].world, true + var err error + camera.ToWorld, err = renderer.ViewCamera(selected, source.Path, source.Source == "U3D") + if errors.Is(err, basethreed.ErrAmbiguousCamera) { + return camera, "", &pdfgo.UnsupportedError{Feature: "ambiguous 3D camera instance"} } - if !found { + if errors.Is(err, basethreed.ErrCameraNotFound) { return camera, "", fmt.Errorf("missing PDF 3D camera path") } + if err != nil { + return camera, "", err + } } view := "" if selected >= 0 { - view = r.model.Nodes[selected].Name + view = model.Nodes[selected].Name } 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节点参数转为场景覆盖,转换缓存计入绘制预算 // 入参: ctx 取消上下文, nodes 节点参数, options 绘制选项 // 返回: error 预算或取消错误 @@ -461,7 +386,7 @@ func pdfThreeDNodeStates(ctx context.Context, nodes []pdfgo.ThreeDNode, options } limit := options.MaxMemoryBytes if limit == 0 { - limit = u3dDefaultBytes + limit = basethreed.DefaultBytes } if limit <= 0 || uint64(len(nodes)) >= uint64(limit)/168 { return fmt.Errorf("PDF 3D node memory exceeds limit") diff --git a/ofdgo_pdf_3d_section.go b/ofdgo_pdf_3d_section.go index 1d40fc6..89e027d 100644 --- a/ofdgo_pdf_3d_section.go +++ b/ofdgo_pdf_3d_section.go @@ -19,6 +19,7 @@ import ( "fmt" "math" + basethreed "github.com/xiaoqidun/ofdgo/internal/base/threed" "github.com/xiaoqidun/pdfgo" ) @@ -31,7 +32,7 @@ func pdfThreeDSections(ctx context.Context, sections []pdfgo.ThreeDCrossSection, } limit := options.MaxMemoryBytes if limit == 0 { - limit = u3dDefaultBytes + limit = basethreed.DefaultBytes } if limit <= 0 || uint64(len(sections)) >= uint64(limit)/136 { return fmt.Errorf("PDF 3D section memory exceeds limit") diff --git a/ofdgo_pdf_geometry.go b/ofdgo_pdf_geometry.go index 873cafe..b6b0841 100644 --- a/ofdgo_pdf_geometry.go +++ b/ofdgo_pdf_geometry.go @@ -19,6 +19,7 @@ import ( "crypto/sha256" "encoding/binary" "fmt" + basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom" "image/color" "math" @@ -110,7 +111,7 @@ func (p *pdfImporter) compileCoverage(node pdfCompositeNode, stroke bool) (*Rast path, err = pdfGeometryPath(p.ctx, node.path.Path, p.matrix) evenOdd = node.path.Path.EvenOdd } 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 { return nil, Box{}, err @@ -118,7 +119,7 @@ func (p *pdfImporter) compileCoverage(node pdfCompositeNode, stroke bool) (*Rast if len(path) == 0 { 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) if err != nil || len(clip) == 0 { 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}) } } - boundary := geometryRectangle(bounds.box()) + boundary := GeometryPath(basegeom.Rectangle(bounds.box())) clip, err = clipGeometry(geometry, clip, &boundary) if err != nil { return nil, Box{}, err @@ -149,7 +150,7 @@ func (p *pdfImporter) compileCoverage(node pdfCompositeNode, stroke bool) (*Rast if err != nil { 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 evenOdd { 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 { 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 { return clip, nil } @@ -216,8 +217,8 @@ func (p *pdfImporter) coverageClip(paths []pdfgo.Path, geometry GeometryBackend) if err != nil { return nil, err } - part = closedGeometry(part) - if _, rectangle := geometryRectangleBounds(part); source.EvenOdd && !rectangle { + part = GeometryPath(basegeom.Close(basegeom.Path(part))) + if _, rectangle := basegeom.RectangleBounds(basegeom.Path(part)); source.EvenOdd && !rectangle { part, err = geometry.Normalize(part, true) if err != nil { return nil, err diff --git a/ofdgo_pdf_object.go b/ofdgo_pdf_object.go index 6882407..e17fed4 100644 --- a/ofdgo_pdf_object.go +++ b/ofdgo_pdf_object.go @@ -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 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.LineWidthSet = object.LineWidth == 0 object.Cap = []string{"Butt", "Round", "Square"}[mark.Style.Cap] diff --git a/ofdgo_pdf_shading.go b/ofdgo_pdf_shading.go index af89767..9753e03 100644 --- a/ofdgo_pdf_shading.go +++ b/ofdgo_pdf_shading.go @@ -16,6 +16,7 @@ package ofdgo import ( "fmt" + basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom" "image" "math" @@ -373,7 +374,7 @@ func pdfRadialBackground(gradient *pdfgo.RadialGradient, matrix pdfgo.Matrix, bo } var circles, path GeometryPath 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) } if !extend[0] && r0 > 0 { diff --git a/ofdgo_pdf_text_path.go b/ofdgo_pdf_text_path.go index 07eb060..aa1ae04 100644 --- a/ofdgo_pdf_text_path.go +++ b/ofdgo_pdf_text_path.go @@ -35,8 +35,8 @@ func pdfGlyphPath(outline GeometryPath, matrix pdfgo.Matrix) (pdfgo.Path, error) case GeometryLine: path.Segments = append(path.Segments, pdfgo.Segment{Operator: "L", Points: []pdfgo.Point{point(segment.End)}}) case GeometryQuad: - c1 := Point{current.X + (segment.Control1.X-current.X)*2/3, 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} + c1 := Point{X: current.X + (segment.Control1.X-current.X)*2/3, Y: current.Y + (segment.Control1.Y-current.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)}}) case GeometryCubic: path.Segments = append(path.Segments, pdfgo.Segment{Operator: "B", Points: []pdfgo.Point{point(segment.Control1), point(segment.Control2), point(segment.End)}}) diff --git a/ofdgo_raster_bounds.go b/ofdgo_raster_bounds.go index df03508..62600a8 100644 --- a/ofdgo_raster_bounds.go +++ b/ofdgo_raster_bounds.go @@ -17,6 +17,7 @@ package ofdgo import ( "context" "fmt" + basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom" ) // Bounds 度量已编译指令的页面坐标范围,不重新定位文字或读取图片像素 @@ -74,7 +75,7 @@ func rasterCommandGeometry(geometry GeometryBackend, command RasterCommand) (Geo if bounds.Empty() { 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 { if len(command.Path) == 0 || command.Paint.Gradient == nil && command.Paint.Color.A == 0 { return nil, nil @@ -92,7 +93,7 @@ func rasterCommandGeometry(geometry GeometryBackend, command RasterCommand) (Geo return nil, err } 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 } path, err = geometry.Stroke(path, *command.Stroke) diff --git a/ofdgo_render_compile.go b/ofdgo_render_compile.go index 92f5adc..0c89d35 100644 --- a/ofdgo_render_compile.go +++ b/ofdgo_render_compile.go @@ -16,6 +16,7 @@ package ofdgo import ( "fmt" + basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom" "math" "strconv" ) @@ -127,7 +128,7 @@ func (c *semanticCompiler) addBounds(path GeometryPath, evenOdd bool) error { } c.measurement.Bounds = unionTextBox(c.measurement.Bounds, box) if c.contours { - svg, err := closedGeometry(path).SVG() + svg, err := GeometryPath(basegeom.Close(basegeom.Path(path))).SVG() if err != nil { 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 { return nil } - path = closedGeometry(path) + path = GeometryPath(basegeom.Close(basegeom.Path(path))) if !c.measure && paint.Kind != PaintPattern { return c.command(path, paint, evenOdd, clip, shadingMatrix, nil) } diff --git a/ofdgo_render_compile_image.go b/ofdgo_render_compile_image.go index b6434cc..35d8456 100644 --- a/ofdgo_render_compile_image.go +++ b/ofdgo_render_compile_image.go @@ -18,6 +18,7 @@ import ( "bytes" "context" "fmt" + basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom" "image" "math" "slices" @@ -50,7 +51,7 @@ func (c *semanticCompiler) imageObject(object ImageObject, state RenderState) er return err } 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 { 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 { command := RasterCommand{Image: img, Transform: RasterMatrix(matrix.Values())} if clip != nil { - if box, ok := geometryRectangleBounds(*clip); ok { + if box, ok := basegeom.RectangleBounds(basegeom.Path(*clip)); ok { 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())}) 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 { 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 { return err } - if geometryConvexContains(*clip, outline) { + if basegeom.ConvexContains(basegeom.Path(*clip), basegeom.Path(outline)) { clip = nil } } @@ -184,7 +185,7 @@ func (c *semanticCompiler) drawStampContent(stamp Stamp) error { var clip GeometryPath var segments []RasterSegment 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) if err != nil { return err @@ -248,7 +249,7 @@ func (c *semanticCompiler) stampImage(img image.Image, stamp Stamp) error { box := stamp.Box var clip *GeometryPath 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 } bounds := img.Bounds() diff --git a/ofdgo_render_pattern.go b/ofdgo_render_pattern.go index f82d98f..8ecbf77 100644 --- a/ofdgo_render_pattern.go +++ b/ofdgo_render_pattern.go @@ -242,7 +242,7 @@ func geometryFromRaster(path []RasterSegment) GeometryPath { if s.Verb == RasterClose { 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 } diff --git a/ofdgo_render_shading.go b/ofdgo_render_shading.go index d41d14b..8495182 100644 --- a/ofdgo_render_shading.go +++ b/ofdgo_render_shading.go @@ -14,7 +14,10 @@ package ofdgo -import "math" +import ( + basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom" + "math" +) // reflectShdPosition 获取镜像周期内的位置 // 入参: position 渐变位置 @@ -68,7 +71,7 @@ func (c *semanticCompiler) shadingClip(path GeometryPath, paint Paint, matrix Ma if box.W <= 0 { return nil, nil } - clip, err := c.geometry.Transform(geometryRectangle(box), axis) + clip, err := c.geometry.Transform(GeometryPath(basegeom.Rectangle(box)), axis) if err != nil { return nil, err } diff --git a/ofdgo_render_text.go b/ofdgo_render_text.go index ea2b5ae..850e169 100644 --- a/ofdgo_render_text.go +++ b/ofdgo_render_text.go @@ -16,6 +16,7 @@ package ofdgo import ( "fmt" + basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom" "math" "strings" ) @@ -101,7 +102,7 @@ func (r *Renderer) positionText(object TextObject, state RenderState, contentOnl } box, _ := ParseBox(object.Boundary) 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 { return nil, err } @@ -430,7 +431,7 @@ func (run *TextRun) addGeometrySpan(geometry GeometryBackend, start, end int, bo return nil } if clip != nil { - path, err := geometry.Transform(geometryRectangle(bounds), matrix) + path, err := geometry.Transform(GeometryPath(basegeom.Rectangle(bounds)), matrix) if err != nil { return err } diff --git a/ofdgo_render_walk.go b/ofdgo_render_walk.go index a75593c..0023dac 100644 --- a/ofdgo_render_walk.go +++ b/ofdgo_render_walk.go @@ -14,7 +14,10 @@ package ofdgo -import "fmt" +import ( + "fmt" + basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom" +) // RenderState 保存对象继承的样式、父变换和页面坐标裁剪,所有引用只读 // Clip为nil表示未提供裁剪,空路径表示完全裁去,单位为毫米 @@ -344,7 +347,7 @@ func (r *Renderer) geometryExtentClip(width, height float64, matrix Matrix, pare if err != nil { 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 { return nil, err } diff --git a/ofdgo_renderer_canvas.go b/ofdgo_renderer_canvas.go index d169065..3df72c8 100644 --- a/ofdgo_renderer_canvas.go +++ b/ofdgo_renderer_canvas.go @@ -16,6 +16,7 @@ package ofdgo import ( "fmt" + basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom" "image" "image/color" "image/draw" @@ -165,7 +166,7 @@ func (b CanvasBackend) renderImage(page *RasterPage, img *image.RGBA) (image.Ima m := cmd.Transform if cmd.Stroke != nil { options := *cmd.Stroke - if err := validateStroke(options); err != nil { + if err := basegeom.ValidateStroke(options); err != nil { return nil, err } p = p.Transform(canvas.Matrix{{m[0], m[2], m[4]}, {m[1], m[3], m[5]}}) diff --git a/ofdgo_renderer_clip_convex_canvas.go b/ofdgo_renderer_clip_convex_canvas.go index ab80470..b5411d0 100644 --- a/ofdgo_renderer_clip_convex_canvas.go +++ b/ofdgo_renderer_clip_convex_canvas.go @@ -15,6 +15,7 @@ package ofdgo import ( + basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom" "math" "github.com/tdewolff/canvas" @@ -133,7 +134,7 @@ func intersectCanvasPolygons(a, b []canvas.Point) (*canvas.Path, bool) { if (d >= 0) != (previousDistance >= 0) { scale := math.Max(math.Abs(d), math.Abs(previousDistance)) 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 { intersection.X = start.X } diff --git a/ofdgo_renderer_compile.go b/ofdgo_renderer_compile.go index 1e336f5..7be5bf9 100644 --- a/ofdgo_renderer_compile.go +++ b/ofdgo_renderer_compile.go @@ -14,7 +14,10 @@ package ofdgo -import "image/color" +import ( + basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom" + "image/color" +) // OFDCompiler 使用库自有页面语义,字体和几何操作交给当前后端配置 type OFDCompiler struct{} @@ -40,7 +43,7 @@ func (OFDCompiler) CompilePage(r *Renderer, page *PageContent) (*RasterPage, err return nil, err } 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 } } diff --git a/ofdgo_renderer_geom_canvas.go b/ofdgo_renderer_geom_canvas.go index 7282414..533d288 100644 --- a/ofdgo_renderer_geom_canvas.go +++ b/ofdgo_renderer_geom_canvas.go @@ -16,6 +16,7 @@ package ofdgo import ( "fmt" + basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom" "math" "slices" "strconv" @@ -88,7 +89,7 @@ func (CanvasBackend) Transform(path GeometryPath, matrix Matrix) (GeometryPath, 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.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 描边样式 // 返回: GeometryPath 描边区域, 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 } if slices.Contains(options.Dashes, 0) { @@ -248,7 +249,7 @@ func canvasStrokeFillPath(geometry GeometryBackend, path *canvas.Path, options S if options.Tolerance == 0 { options.Tolerance = canvas.Tolerance } - if err := validateStroke(options); err != nil { + if err := basegeom.ValidateStroke(options); err != nil { return nil, err } if len(path.Dash(options.DashOffset, options.Dashes...).Data()) > 256 { diff --git a/ofdgo_renderer_image_canvas.go b/ofdgo_renderer_image_canvas.go index 4766d04..d188c5c 100644 --- a/ofdgo_renderer_image_canvas.go +++ b/ofdgo_renderer_image_canvas.go @@ -18,6 +18,7 @@ import ( "bytes" "context" "fmt" + basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom" "image" "math" @@ -177,7 +178,7 @@ func imageClipContains(img image.Image, clipPath *canvas.Path, m canvas.Matrix) if rect, ok := rectangularPath(clipPath); ok { 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 clipPath.Contains(outline) diff --git a/ofdgo_renderer_pdf_action.go b/ofdgo_renderer_pdf_action.go index d3da017..6ae81c7 100644 --- a/ofdgo_renderer_pdf_action.go +++ b/ofdgo_renderer_pdf_action.go @@ -17,6 +17,7 @@ package ofdgo import ( "context" "fmt" + basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom" "slices" "github.com/tdewolff/canvas" @@ -56,7 +57,7 @@ func (n *pdfNavigation) addActions(renderer *Renderer, page int, source actionSo if value == nil { continue } - path := geometryRectangle(box) + path := GeometryPath(basegeom.Rectangle(box)) if outline != "" { parsed, err := canvas.ParseSVGPath(outline) if err != nil { diff --git a/ofdgo_renderer_pdf_link.go b/ofdgo_renderer_pdf_link.go index 7086b1c..f6ff1ce 100644 --- a/ofdgo_renderer_pdf_link.go +++ b/ofdgo_renderer_pdf_link.go @@ -17,6 +17,7 @@ package ofdgo import ( "context" "fmt" + basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom" "math" "slices" @@ -60,7 +61,7 @@ func pdfActionRegion(ctx context.Context, outline string, pageH float64) (*pdfgo if path.Len() > pdfLinkEdgeLimit { 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 } 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-- { left[len(points)-n], right[n-1] = work[0], work[n-1] for i := 0; i < n-1; i++ { - work[i] = geometryLerp(work[i], work[i+1], .5) + work[i] = basegeom.Lerp(work[i], work[i+1], .5) } } 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() current = start default: - curves := GeometryPath{segment} + curves := basegeom.Path{segment} if segment.Verb == GeometryArc { var err error - curves, err = arcCurves(current, segment, pdfLinkTolerance/2) + curves, err = basegeom.ArcCurves(current, segment, pdfLinkTolerance/2) if err != nil { return nil, err } diff --git a/ofdgo_shape.go b/ofdgo_shape.go index ef5842c..e254fe6 100644 --- a/ofdgo_shape.go +++ b/ofdgo_shape.go @@ -16,6 +16,7 @@ package ofdgo import ( "fmt" + basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom" "math" "strconv" "strings" @@ -157,12 +158,12 @@ func (p PathObject) Shape() (ShapeKind, Box) { boundary, err := ParseBox(p.Boundary) m := NewMatrix(p.CTM) _, invertible := m.Invert() - if err != nil || !axisAlignedMatrix(m) && (!line || !invertible) { + if err != nil || !basegeom.AxisAligned(basegeom.MatrixFromValues(m.Values())) && (!line || !invertible) { return "", Box{} } if line { 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} } else { 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 } else { 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 { w, h = math.Abs(w), math.Abs(h) diff --git a/ofdgo_u3d.go b/ofdgo_u3d.go index b01ffdc..f7daa6b 100644 --- a/ofdgo_u3d.go +++ b/ofdgo_u3d.go @@ -15,108 +15,185 @@ package ofdgo import ( - "bytes" "context" - "encoding/binary" - "fmt" - "io" - "math" + "image" + + basethreed "github.com/xiaoqidun/ofdgo/internal/base/threed" ) -// u3dDefaultBytes 限制默认解码数据规模,不包括调用方已有输入及运行时开销 -const u3dDefaultBytes = 256 << 20 +const ( + 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上限 -type U3DOptions struct { - MaxDecodedBytes int64 -} +type U3DOptions = basethreed.U3DOptions // 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 -} +type U3DModel = basethreed.U3DModel // U3DMetadata 保留块所属名称与原始元数据,数据不与输入共享存储 -type U3DMetadata struct { - Block uint32 - Name string - Data []byte -} +type U3DMetadata = basethreed.U3DMetadata // U3DNode 保存组、模型、光源或视图节点,父节点矩阵按列存储 -type U3DNode struct { - Name, Kind, Resource string - Parents []U3DParent - Visibility uint32 - Shaders [][]string - LineShaders [][]string - PointShaders [][]string - View *U3DView - BoneWeights []U3DBoneWeights - Animations []U3DAnimation -} +type U3DNode = basethreed.U3DNode // U3DParent 保存父节点名称及局部到父节点的变换,空名称引用默认节点 -type U3DParent struct { - Name string - Transform [16]float32 -} +type U3DParent = basethreed.U3DParent // U3DMaterial 保存材质参数,Attributes各位决定相应参数是否生效 -type U3DMaterial struct { - Name string - Attributes uint32 - Ambient, Diffuse, Specular, Emissive [3]float32 - Reflectivity, Opacity float32 -} +type U3DMaterial = basethreed.U3DMaterial // U3DShader 保存材质与八个纹理通道,Textures中的空项表示未启用 -type U3DShader struct { - Name, Material string - Attributes, AlphaFunction, Blend uint32 - RenderPass uint32 - AlphaReference float32 - Textures [8]*U3DTextureLayer -} +type U3DShader = basethreed.U3DShader // U3DLight 保存环境、方向、点或聚光资源,Kind依次为0至3 -type U3DLight struct { - Name string - Attributes uint32 - Kind byte - Color, Attenuation [3]float32 - SpotAngle, Intensity float32 -} +type U3DLight = basethreed.U3DLight -// u3dBlock 保存校验后的块边界,数据切片仅在解码期间引用输入 -type u3dBlock struct { - kind uint32 - data, meta []byte -} +// U3DAnimation 保存ECMA-363第9.7.3节的动画修饰器,Index为链内位置 +// Attributes的1、2、4、8分别表示启动、锁定根骨骼、单轨播放和动作间平滑过渡 +// TimeScale为整体时间倍率,BlendTime以毫秒计;Motions为空时使用无轨道的默认动作 +type U3DAnimation = basethreed.U3DAnimation -// 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 -} +// U3DAnimationMotion 保存动画中的动作引用,空名称引用默认动作 +// Attributes的1、2分别表示循环和同步结束,TimeOffset以毫秒计,TimeScale为动作时间倍率 +type U3DAnimationMotion = basethreed.U3DAnimationMotion + +// U3DDirectionalLight 定义无限远光源,Direction为发射方向,CameraRelative使用相机坐标 +type U3DDirectionalLight = basethreed.U3DDirectionalLight + +// U3DLighting 替换场景灯光,空灯光列表表示无光源,DiffuseOffset加到受光材质的漫反射颜色 +// 颜色及漫反射增量的各分量取值为0到1;不修改材质自发光或不受光着色器 +type U3DLighting = basethreed.U3DLighting + +// 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解码静态几何、骨架、场景节点及材质、纹理图像等资源 // 保留动作轨道和动画修饰器,不在解码时求值;绘制能力由RenderU3D另行校验 @@ -125,556 +202,21 @@ type u3dDecoder struct { // 入参: 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 = 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 + return basethreed.DecodeU3D(ctx, data, options) } -// 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< 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 +// RenderU3D 绘制几何及视图纹理层,返回指定时刻的不透明图像,不执行脚本 +// 可通过节点覆盖指定动作轨道与采样时间,逐次生成独立姿态,不修改原模型 +// 显式骨骼姿态按权重变换位置和法线,暂不生成骨骼链接或推导缺失权重 +// 动画修饰器支持单动作的节点和骨骼求值,尚未实现的动作衔接返回明确错误 +// 几何纹理支持模型坐标与程序化映射,参考形状约定见U3DTextureLayer +// 纹理按需解码并复用,不自动访问外部图像 +// 采用逐顶点光照及逐像素深度混合,空名称引用标准默认资源 +// 缺失几何、材质和着色器采用默认资源,缺失的非空光源引用不产生光照 +// 缺失纹理返回错误,空纹理名称使用标准默认图像 +// 仅绘制默认根的后代,非空父节点引用缺失时不展开该父路径 +// 入参: ctx 取消上下文, model 场景, options 视图、输出与资源限制 +// 返回: *image.RGBA 图像, error 参数、能力、预算或取消错误 +func RenderU3D(ctx context.Context, model *U3DModel, options U3DRenderOptions) (*image.RGBA, error) { + return basethreed.RenderU3D(ctx, model, options) }