mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 08:46:55 +08:00
feat(编辑渲染): 完善三维转换,优化编辑渲染压缩
This commit is contained in:
1 parent
e8344e6387
commit
839f65e3d7
47 files changed
+4887
-112
No files matched your search
@@ -43,6 +43,14 @@ type RasterBackend interface {
|
|||||||
Render(*RasterPage) (image.Image, error)
|
Render(*RasterPage) (image.Image, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RasterBufferBackend 可选的目标缓冲绘制能力,不改变后端和采样精度
|
||||||
|
// 目标须为原点为零、尺寸与页面一致的紧密RGBA缓冲,不得与输入图像共享数据
|
||||||
|
// 绘制前清空目标像素,不保留目标引用;失败时目标可能包含部分结果
|
||||||
|
type RasterBufferBackend interface {
|
||||||
|
RasterBackend
|
||||||
|
RenderInto(*RasterPage, *image.RGBA) error
|
||||||
|
}
|
||||||
|
|
||||||
// PageCompiler 统一页面编译、文字提取和对象度量,不得修改源数据
|
// PageCompiler 统一页面编译、文字提取和对象度量,不得修改源数据
|
||||||
// 三项能力需采用相同的字体和几何语义,避免绘制、搜索与选区不一致
|
// 三项能力需采用相同的字体和几何语义,避免绘制、搜索与选区不一致
|
||||||
// renderer提供文档、字体配置和文字回调,实现中不得递归调用对应的Renderer方法
|
// renderer提供文档、字体配置和文字回调,实现中不得递归调用对应的Renderer方法
|
||||||
|
|||||||
@@ -164,6 +164,9 @@ func (e *Editor) outputSnapshot(ctx context.Context, options WriteOptions) (*Edi
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if err := snapshot.compressPatternPaints(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
return &snapshot, nil
|
return &snapshot, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,164 @@
|
|||||||
|
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package ofdgo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"reflect"
|
||||||
|
"slices"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
compressionPatternMinimum = 4096
|
||||||
|
compressionPatternLimit = 4096
|
||||||
|
compressionPatternUses = 65536
|
||||||
|
)
|
||||||
|
|
||||||
|
// compressionPatternPaint 保存可共享的底纹颜色,不修改输入对象
|
||||||
|
type compressionPatternPaint struct {
|
||||||
|
fill *FillColor
|
||||||
|
stroke *StrokeColor
|
||||||
|
count int
|
||||||
|
id string
|
||||||
|
}
|
||||||
|
|
||||||
|
// compressionPatternUse 记录输出快照内需要替换的直接对象
|
||||||
|
type compressionPatternUse struct {
|
||||||
|
page, layer, object int
|
||||||
|
paint *compressionPatternPaint
|
||||||
|
}
|
||||||
|
|
||||||
|
// compressPatternPaints 将新文档中重复的大底纹颜色提取为绘制参数,仅修改输出快照
|
||||||
|
// 返回: error 编码或取消错误
|
||||||
|
func (e *Editor) compressPatternPaints() error {
|
||||||
|
if e.output == nil || e.output.options.Mode == CompressionUnchanged || e.output.protected || e.source != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
paints := make(map[[32]byte]*compressionPatternPaint)
|
||||||
|
var uses []compressionPatternUse
|
||||||
|
for pi, page := range e.pages {
|
||||||
|
for li, layer := range page.Content.Layer {
|
||||||
|
if layer.DrawParam != "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for oi, object := range layer.Objects {
|
||||||
|
if err := e.output.ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(uses) == compressionPatternUses {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
fill, stroke := compressionObjectPattern(object)
|
||||||
|
if fill == nil && stroke == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
hash := sha256.New()
|
||||||
|
var size int64
|
||||||
|
x := newOFDXML(convertWriter{writer: hash, count: &size, context: e.output.ctx})
|
||||||
|
x.ctx = e.output.ctx
|
||||||
|
x.color("FillColor", fill)
|
||||||
|
x.color("StrokeColor", (*FillColor)(stroke))
|
||||||
|
if err := x.finish(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if size < compressionPatternMinimum {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
key := [32]byte(hash.Sum(nil))
|
||||||
|
paint := paints[key]
|
||||||
|
if paint == nil {
|
||||||
|
if len(paints) == compressionPatternLimit {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
paint = &compressionPatternPaint{fill: fill, stroke: stroke}
|
||||||
|
paints[key] = paint
|
||||||
|
} else if !reflect.DeepEqual(paint.fill, fill) || !reflect.DeepEqual(paint.stroke, stroke) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
paint.count++
|
||||||
|
uses = append(uses, compressionPatternUse{pi, li, oi, paint})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var copied bool
|
||||||
|
lastPage, lastLayer := -1, -1
|
||||||
|
for _, use := range uses {
|
||||||
|
if err := e.output.ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if use.paint.count < 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !copied {
|
||||||
|
e.pages = slices.Clone(e.pages)
|
||||||
|
e.resources = slices.Clone(e.resources)
|
||||||
|
copied = true
|
||||||
|
}
|
||||||
|
if use.paint.id == "" {
|
||||||
|
id, err := e.addEditorDrawParam(DrawParam{FillColor: use.paint.fill, StrokeColor: use.paint.stroke})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
use.paint.id = id
|
||||||
|
}
|
||||||
|
if use.page != lastPage {
|
||||||
|
e.pages[use.page].Content.Layer = slices.Clone(e.pages[use.page].Content.Layer)
|
||||||
|
lastPage, lastLayer = use.page, -1
|
||||||
|
}
|
||||||
|
layer := &e.pages[use.page].Content.Layer[use.layer]
|
||||||
|
if use.layer != lastLayer {
|
||||||
|
layer.Objects = slices.Clone(layer.Objects)
|
||||||
|
lastLayer = use.layer
|
||||||
|
}
|
||||||
|
object := &layer.Objects[use.object]
|
||||||
|
if object.Type == "PathObject" {
|
||||||
|
object.PathObject.DrawParam = use.paint.id
|
||||||
|
object.PathObject.FillColor, object.PathObject.StrokeColor = nil, nil
|
||||||
|
} else {
|
||||||
|
object.TextObject.DrawParam = use.paint.id
|
||||||
|
object.TextObject.FillColor, object.TextObject.StrokeColor = nil, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// compressionObjectPattern 筛选没有原文及绘制参数继承的底纹对象
|
||||||
|
// 入参: object 图形对象
|
||||||
|
// 返回: *FillColor 填充颜色, *StrokeColor 描边颜色,不适用时均为空
|
||||||
|
func compressionObjectPattern(object GraphicObject) (*FillColor, *StrokeColor) {
|
||||||
|
if object.origin != nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
var fill *FillColor
|
||||||
|
var stroke *StrokeColor
|
||||||
|
switch object.Type {
|
||||||
|
case "PathObject":
|
||||||
|
if object.PathObject.DrawParam != "" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
fill, stroke = object.PathObject.FillColor, object.PathObject.StrokeColor
|
||||||
|
case "TextObject":
|
||||||
|
if object.TextObject.DrawParam != "" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
fill, stroke = object.TextObject.FillColor, object.TextObject.StrokeColor
|
||||||
|
default:
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
if fill != nil && fill.Pattern != nil || stroke != nil && stroke.Pattern != nil {
|
||||||
|
return fill, stroke
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
@@ -50,6 +50,7 @@ func (e *Editor) compressResourceReferences(parts map[string][]byte, reader *Rea
|
|||||||
if e.output == nil || e.output.options.Mode == CompressionUnchanged || e.output.protected {
|
if e.output == nil || e.output.options.Mode == CompressionUnchanged || e.output.protected {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
generated = e.generatedVectorReferences(parts, generated)
|
||||||
names := make(map[string]bool)
|
names := make(map[string]bool)
|
||||||
if reader != nil {
|
if reader != nil {
|
||||||
for name := range reader.fileIndex {
|
for name := range reader.fileIndex {
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ type editorResource struct {
|
|||||||
vectorKey [32]byte
|
vectorKey [32]byte
|
||||||
vectorStates map[string]editorCompositeState
|
vectorStates map[string]editorCompositeState
|
||||||
references []string
|
references []string
|
||||||
|
usage *editorVectorUsage
|
||||||
subset *editorFontSubset
|
subset *editorFontSubset
|
||||||
states map[string]editorCompositeState
|
states map[string]editorCompositeState
|
||||||
draw *DrawParam
|
draw *DrawParam
|
||||||
|
|||||||
@@ -170,6 +170,14 @@ func (e *Editor) CopyObjectsContext(ctx context.Context, page int, objects []Gra
|
|||||||
// 入参: ctx 取消上下文, page 目标页, objects 无原文和标识且调用方不再修改的对象
|
// 入参: ctx 取消上下文, page 目标页, objects 无原文和标识且调用方不再修改的对象
|
||||||
// 返回: error 校验或取消错误,失败时不提交页面内容
|
// 返回: error 校验或取消错误,失败时不提交页面内容
|
||||||
func (e *Editor) appendOwnedObjects(ctx context.Context, page int, objects []GraphicObject) error {
|
func (e *Editor) appendOwnedObjects(ctx context.Context, page int, objects []GraphicObject) error {
|
||||||
|
return (&editorValidation{Editor: e}).appendOwnedObjects(ctx, page, objects)
|
||||||
|
}
|
||||||
|
|
||||||
|
// appendOwnedObjects 在资源不变的构建会话内复用校验结果,接收对象所有权
|
||||||
|
// 入参: ctx 取消上下文, page 目标页, objects 无原文和标识的独立对象
|
||||||
|
// 返回: error 校验或取消错误
|
||||||
|
func (v *editorValidation) appendOwnedObjects(ctx context.Context, page int, objects []GraphicObject) error {
|
||||||
|
e := v.Editor
|
||||||
if err := ctx.Err(); err != nil {
|
if err := ctx.Err(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -182,7 +190,6 @@ func (e *Editor) appendOwnedObjects(ctx context.Context, page int, objects []Gra
|
|||||||
if err := e.prepareSourceIDs(); err != nil {
|
if err := e.prepareSourceIDs(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
validation := &editorValidation{Editor: e}
|
|
||||||
for i, object := range objects {
|
for i, object := range objects {
|
||||||
if err := ctx.Err(); err != nil {
|
if err := ctx.Err(); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -190,7 +197,7 @@ func (e *Editor) appendOwnedObjects(ctx context.Context, page int, objects []Gra
|
|||||||
if object.origin != nil || editorObjectID(object) != "" {
|
if object.origin != nil || editorObjectID(object) != "" {
|
||||||
return fmt.Errorf("owned objects must not have identifiers or original content")
|
return fmt.Errorf("owned objects must not have identifiers or original content")
|
||||||
}
|
}
|
||||||
prepared, err := validation.prepareObject("", object)
|
prepared, err := v.prepareObject("", object)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
+36
-2
@@ -39,7 +39,7 @@ type editorValidation struct {
|
|||||||
*Editor
|
*Editor
|
||||||
patterns map[*Pattern]bool
|
patterns map[*Pattern]bool
|
||||||
composites map[string]bool
|
composites map[string]bool
|
||||||
vectors map[string]*editorCompositeNode
|
vectors map[string]bool
|
||||||
paths map[string]bool
|
paths map[string]bool
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,7 +142,7 @@ func (e *Editor) StylePatterns(page int, ids []string, stroke bool, style Patter
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
objects, _, err := e.selectedObjects(page, ids)
|
objects, indexes, err := e.selectedObjects(page, ids)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -150,17 +150,51 @@ func (e *Editor) StylePatterns(page int, ids []string, stroke bool, style Patter
|
|||||||
objects[i] = cloneEditorData(objects[i])
|
objects[i] = cloneEditorData(objects[i])
|
||||||
object := &objects[i]
|
object := &objects[i]
|
||||||
var fill *FillColor
|
var fill *FillColor
|
||||||
|
var draw string
|
||||||
switch object.Type {
|
switch object.Type {
|
||||||
case "TextObject":
|
case "TextObject":
|
||||||
|
draw = object.TextObject.DrawParam
|
||||||
fill = object.TextObject.FillColor
|
fill = object.TextObject.FillColor
|
||||||
if stroke {
|
if stroke {
|
||||||
fill = (*FillColor)(object.TextObject.StrokeColor)
|
fill = (*FillColor)(object.TextObject.StrokeColor)
|
||||||
}
|
}
|
||||||
case "PathObject", "Path":
|
case "PathObject", "Path":
|
||||||
|
draw = object.PathObject.DrawParam
|
||||||
fill = object.PathObject.FillColor
|
fill = object.PathObject.FillColor
|
||||||
if stroke {
|
if stroke {
|
||||||
fill = (*FillColor)(object.PathObject.StrokeColor)
|
fill = (*FillColor)(object.PathObject.StrokeColor)
|
||||||
}
|
}
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("object %q does not use a pattern", ids[i])
|
||||||
|
}
|
||||||
|
if fill == nil {
|
||||||
|
base, err := e.editorDrawParam(e.pages[page].Content.Layer[indexes[i].layer].DrawParam, make(map[string]bool))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
param, err := e.editorDrawParam(draw, make(map[string]bool))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
paint := mergeDrawParam(*base, param)
|
||||||
|
fill = paint.FillColor
|
||||||
|
if stroke {
|
||||||
|
fill = (*FillColor)(paint.StrokeColor)
|
||||||
|
}
|
||||||
|
fill = cloneEditorData(fill)
|
||||||
|
if object.Type == "TextObject" {
|
||||||
|
if stroke {
|
||||||
|
object.TextObject.StrokeColor = (*StrokeColor)(fill)
|
||||||
|
} else {
|
||||||
|
object.TextObject.FillColor = fill
|
||||||
|
}
|
||||||
|
} else if object.Type == "PathObject" || object.Type == "Path" {
|
||||||
|
if stroke {
|
||||||
|
object.PathObject.StrokeColor = (*StrokeColor)(fill)
|
||||||
|
} else {
|
||||||
|
object.PathObject.FillColor = fill
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if fill == nil || fill.Pattern == nil {
|
if fill == nil || fill.Pattern == nil {
|
||||||
return fmt.Errorf("object %q does not use a pattern", ids[i])
|
return fmt.Errorf("object %q does not use a pattern", ids[i])
|
||||||
|
|||||||
@@ -1097,20 +1097,17 @@ func (v *editorValidation) validateVector(id string) error {
|
|||||||
if v.composites[id] {
|
if v.composites[id] {
|
||||||
return fmt.Errorf("cyclic composite resource %q", id)
|
return fmt.Errorf("cyclic composite resource %q", id)
|
||||||
}
|
}
|
||||||
|
if v.vectors[id] {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
if v.composites == nil {
|
if v.composites == nil {
|
||||||
v.composites = make(map[string]bool)
|
v.composites = make(map[string]bool)
|
||||||
v.vectors = make(map[string]*editorCompositeNode)
|
|
||||||
}
|
}
|
||||||
v.composites[id] = true
|
v.composites[id] = true
|
||||||
defer delete(v.composites, id)
|
defer delete(v.composites, id)
|
||||||
node := v.vectors[id]
|
node, err := v.compositeDefinition(id)
|
||||||
if node == nil {
|
if err != nil {
|
||||||
var err error
|
return err
|
||||||
node, err = v.compositeDefinition(id)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
v.vectors[id] = node
|
|
||||||
}
|
}
|
||||||
unit := node.object.CompositeGraphicUnit
|
unit := node.object.CompositeGraphicUnit
|
||||||
if node.node.attr("Width") == "" || node.node.attr("Height") == "" || !finite(unit.Width) || !finite(unit.Height) || unit.Width < 0 || unit.Height < 0 {
|
if node.node.attr("Width") == "" || node.node.attr("Height") == "" || !finite(unit.Width) || !finite(unit.Height) || unit.Width < 0 || unit.Height < 0 {
|
||||||
@@ -1131,5 +1128,9 @@ func (v *editorValidation) validateVector(id string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if v.vectors == nil {
|
||||||
|
v.vectors = make(map[string]bool)
|
||||||
|
}
|
||||||
|
v.vectors[id] = true
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -249,7 +249,7 @@ func (e *Editor) addEditorDrawParam(draw DrawParam) (string, error) {
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
draw.ID = e.nextID()
|
draw.ID = e.nextID()
|
||||||
data, err := encodeOFDXML(func(x *ofdXML) {
|
data, err := e.encodeXML(func(x *ofdXML) {
|
||||||
x.root("Res", ofdAttrs{{Name: xml.Name{Local: "BaseLoc"}, Value: "."}})
|
x.root("Res", ofdAttrs{{Name: xml.Name{Local: "BaseLoc"}, Value: "."}})
|
||||||
x.start("DrawParams", nil)
|
x.start("DrawParams", nil)
|
||||||
attrs := ofdAttrs{}
|
attrs := ofdAttrs{}
|
||||||
|
|||||||
+22
-7
@@ -202,12 +202,15 @@ func (e *Editor) subsetFonts(progress editorProgress) (map[string][]byte, error)
|
|||||||
if object.Type == "CompositeObject" || object.Type == "CompositeGraphicUnit" {
|
if object.Type == "CompositeObject" || object.Type == "CompositeGraphicUnit" {
|
||||||
composite = true
|
composite = true
|
||||||
origin := e.objectOrigin(editorObjectID(object))
|
origin := e.objectOrigin(editorObjectID(object))
|
||||||
|
var data []byte
|
||||||
|
var err error
|
||||||
if origin == nil {
|
if origin == nil {
|
||||||
return nil, nil
|
data, err = e.encodeXML(func(x *ofdXML) { x.object(object, true) })
|
||||||
}
|
} else {
|
||||||
data, err := editorXMLObject(origin.data, origin.node, origin.object, object)
|
data, err = editorXMLObject(origin.data, origin.node, origin.object, object)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
data, err = editorXMLStandalone(data, origin.node)
|
data, err = editorXMLStandalone(data, origin.node)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -252,18 +255,30 @@ func (e *Editor) subsetFonts(progress editorProgress) (map[string][]byte, error)
|
|||||||
if err := progress.report("fonts", len(e.pages)+i, total); err != nil {
|
if err := progress.report("fonts", len(e.pages)+i, total); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if _, safe := refs.scan(bytes.NewReader(resource.data), resource.name); !safe {
|
if resource.usage.matches(resource.name, resource.data) {
|
||||||
|
refs.merge(resource.usage.refs)
|
||||||
|
} else if _, safe := refs.scan(bytes.NewReader(resource.data), resource.name); !safe {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for id, usage := range refs.fonts {
|
for id, usage := range refs.fonts {
|
||||||
glyphs := used[id]
|
glyphs := used[id]
|
||||||
if usage.unsafe || len(usage.glyphs) != 0 {
|
if usage.unsafe {
|
||||||
delete(used, id)
|
delete(used, id)
|
||||||
} else if glyphs != nil {
|
} else if glyphs != nil {
|
||||||
for char := range usage.chars {
|
for char := range usage.chars {
|
||||||
glyphs[e.fonts[id].GlyphIndex(char)] = true
|
glyphs[e.fonts[id].GlyphIndex(char)] = true
|
||||||
}
|
}
|
||||||
|
if len(usage.glyphs) != 0 {
|
||||||
|
mapped[id] = true
|
||||||
|
for glyph := range usage.glyphs {
|
||||||
|
if glyph >= e.fonts[id].NumGlyphs() {
|
||||||
|
delete(used, id)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
glyphs[glyph] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,13 +16,17 @@ package ofdgo
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"crypto/sha256"
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"maps"
|
"maps"
|
||||||
"strings"
|
"strings"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
)
|
)
|
||||||
|
|
||||||
// editorGeneratedReferences 保存本次编码页面及其引用,不跨写出复用
|
// editorVectorUsageLimit 限制单个矢量引用快照的记录数,不限制资源内容
|
||||||
|
const editorVectorUsageLimit = 4096
|
||||||
|
|
||||||
|
// editorGeneratedReferences 保存本次输出条目及其引用,不跨写出复用
|
||||||
type editorGeneratedReferences struct {
|
type editorGeneratedReferences struct {
|
||||||
data []byte
|
data []byte
|
||||||
refs *editorResourceRefs
|
refs *editorResourceRefs
|
||||||
@@ -39,9 +43,49 @@ func (r editorGeneratedReferences) matches(data []byte) bool {
|
|||||||
return bytes.Equal(data, r.data)
|
return bytes.Equal(data, r.data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// editorVectorUsage 保存自产矢量资源的引用快照,内容和路径变化时失效
|
||||||
|
type editorVectorUsage struct {
|
||||||
|
name string
|
||||||
|
digest [32]byte
|
||||||
|
refs *editorResourceRefs
|
||||||
|
}
|
||||||
|
|
||||||
|
// matches 核对资源路径和完整内容摘要,不以切片地址判断内容
|
||||||
|
// 入参: name 当前资源路径, data 当前资源内容
|
||||||
|
// 返回: bool 是否仍可使用编码时的引用
|
||||||
|
func (u *editorVectorUsage) matches(name string, data []byte) bool {
|
||||||
|
return u != nil && u.name == name && u.digest == sha256.Sum256(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// generatedVectorReferences 合并未改变的自产矢量引用,不修改调用方集合
|
||||||
|
// 入参: parts 输出条目, generated 本次已知引用
|
||||||
|
// 返回: map[string]editorGeneratedReferences 本次可复用引用
|
||||||
|
func (e *Editor) generatedVectorReferences(parts map[string][]byte, generated map[string]editorGeneratedReferences) map[string]editorGeneratedReferences {
|
||||||
|
copied := false
|
||||||
|
for _, resource := range e.resources {
|
||||||
|
if e.output != nil && e.output.ctx.Err() != nil {
|
||||||
|
return generated
|
||||||
|
}
|
||||||
|
data, ok := parts[resource.name]
|
||||||
|
if !ok || !resource.usage.matches(resource.name, data) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !copied {
|
||||||
|
generated = maps.Clone(generated)
|
||||||
|
if generated == nil {
|
||||||
|
generated = make(map[string]editorGeneratedReferences)
|
||||||
|
}
|
||||||
|
copied = true
|
||||||
|
}
|
||||||
|
generated[resource.name] = editorGeneratedReferences{data: data, refs: resource.usage.refs}
|
||||||
|
}
|
||||||
|
return generated
|
||||||
|
}
|
||||||
|
|
||||||
// editorReferenceScan 按XML事件收集资源和字体用字,未知内容保持完整扫描
|
// editorReferenceScan 按XML事件收集资源和字体用字,未知内容保持完整扫描
|
||||||
type editorReferenceScan struct {
|
type editorReferenceScan struct {
|
||||||
refs *editorResourceRefs
|
refs *editorResourceRefs
|
||||||
|
objects map[string]bool
|
||||||
name string
|
name string
|
||||||
base string
|
base string
|
||||||
stack []string
|
stack []string
|
||||||
@@ -53,6 +97,26 @@ type editorReferenceScan struct {
|
|||||||
seen bool
|
seen bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// vectorUsage 保留完整且限额内的自产矢量引用,超限时保存流程重新扫描
|
||||||
|
// 入参: data 本次编码内容
|
||||||
|
// 返回: *editorVectorUsage 引用快照,无法复用时为空
|
||||||
|
func (s *editorReferenceScan) vectorUsage(data []byte) *editorVectorUsage {
|
||||||
|
if !s.safe || !s.seen || len(s.stack) != 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
count := len(s.refs.ids) + len(s.refs.files) + len(s.refs.fonts)
|
||||||
|
for _, usage := range s.refs.fonts {
|
||||||
|
count += len(usage.chars) + len(usage.glyphs)
|
||||||
|
if count > editorVectorUsageLimit {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if count > editorVectorUsageLimit {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &editorVectorUsage{name: s.name, digest: sha256.Sum256(data), refs: s.refs}
|
||||||
|
}
|
||||||
|
|
||||||
// accept 共用读取和自产页面的引用判断,不将对象自身标识视为引用
|
// accept 共用读取和自产页面的引用判断,不将对象自身标识视为引用
|
||||||
// 入参: token 已解析或已编码的XML事件
|
// 入参: token 已解析或已编码的XML事件
|
||||||
// 返回: bool 是否仍可完整判断引用
|
// 返回: bool 是否仍可完整判断引用
|
||||||
@@ -99,6 +163,9 @@ func (s *editorReferenceScan) accept(token xml.Token) bool {
|
|||||||
if attr.Name.Local != "ID" {
|
if attr.Name.Local != "ID" {
|
||||||
s.refs.reference(s.name, s.base, attr.Name.Local, attr.Value, s.resource)
|
s.refs.reference(s.name, s.base, attr.Name.Local, attr.Value, s.resource)
|
||||||
}
|
}
|
||||||
|
if s.objects != nil && editorObjectReference(attr.Name.Local) {
|
||||||
|
s.objects[attr.Value] = true
|
||||||
|
}
|
||||||
if attr.Name.Local == "Font" {
|
if attr.Name.Local == "Font" {
|
||||||
usage = s.refs.fontUsage(attr.Value)
|
usage = s.refs.fontUsage(attr.Value)
|
||||||
if token.Name.Local != "TextObject" && token.Name.Local != "Text" {
|
if token.Name.Local != "TextObject" && token.Name.Local != "Text" {
|
||||||
@@ -122,6 +189,11 @@ func (s *editorReferenceScan) accept(token xml.Token) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
s.refs.reference(s.name, s.base, token.Name.Local, s.text.String(), s.resource)
|
s.refs.reference(s.name, s.base, token.Name.Local, s.text.String(), s.resource)
|
||||||
|
if s.objects != nil && (token.Name.Local == "Thumbnail" || token.Name.Local == "Substitution") {
|
||||||
|
if id := editorResourceID(s.text.String()); id != "" {
|
||||||
|
s.objects[id] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
if (token.Name.Local == "Substitution" || token.Name.Local == "Font") && strings.TrimSpace(s.text.String()) != "" {
|
if (token.Name.Local == "Substitution" || token.Name.Local == "Font") && strings.TrimSpace(s.text.String()) != "" {
|
||||||
s.refs.fontUsage(s.text.String()).unsafe = true
|
s.refs.fontUsage(s.text.String()).unsafe = true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ func (e *Editor) compactSourceReferences(parts map[string][]byte, progress edito
|
|||||||
if len(parts) == 0 && e.revision == 0 && (e.output == nil || e.output.options.Mode == CompressionUnchanged) {
|
if len(parts) == 0 && e.revision == 0 && (e.output == nil || e.output.options.Mode == CompressionUnchanged) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
generated = e.generatedVectorReferences(parts, generated)
|
||||||
reader := e.source.reader
|
reader := e.source.reader
|
||||||
names := make(map[string]bool)
|
names := make(map[string]bool)
|
||||||
for name := range reader.fileIndex {
|
for name := range reader.fileIndex {
|
||||||
|
|||||||
@@ -91,7 +91,11 @@ func (e *Editor) ApplyAppearance(page int, ids []string, appearance ObjectAppear
|
|||||||
// 返回: *DrawParam 绘制参数, error 错误信息
|
// 返回: *DrawParam 绘制参数, error 错误信息
|
||||||
func (e *Editor) DrawParam(id string, resolved bool) (*DrawParam, error) {
|
func (e *Editor) DrawParam(id string, resolved bool) (*DrawParam, error) {
|
||||||
if resolved {
|
if resolved {
|
||||||
return e.editorDrawParam(id, make(map[string]bool))
|
draw, err := e.editorDrawParam(id, make(map[string]bool))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return cloneEditorData(draw), nil
|
||||||
}
|
}
|
||||||
for _, resource := range e.resources {
|
for _, resource := range e.resources {
|
||||||
if resource.draw != nil && resource.draw.ID == id {
|
if resource.draw != nil && resource.draw.ID == id {
|
||||||
|
|||||||
+31
-5
@@ -17,25 +17,41 @@ package ofdgo
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
|
"fmt"
|
||||||
|
"maps"
|
||||||
|
"slices"
|
||||||
)
|
)
|
||||||
|
|
||||||
// addOwnedVector 将本库生成的独立对象注册为标准矢量资源,不保留内联复合内容
|
// addOwnedVector 将本库生成的独立对象注册为标准矢量资源,不保留内联复合内容
|
||||||
// 入参: ctx 取消上下文, objects 无原文和标识的对象, width 资源宽度, height 资源高度
|
// 入参: ctx 取消上下文, objects 无原文和标识的对象, width 资源宽度, height 资源高度
|
||||||
// 返回: string 资源编号, error 校验、编码或取消错误
|
// 返回: string 资源编号, error 校验、编码或取消错误
|
||||||
func (e *Editor) addOwnedVector(ctx context.Context, objects []GraphicObject, width, height float64) (string, error) {
|
func (e *Editor) addOwnedVector(ctx context.Context, objects []GraphicObject, width, height float64) (string, error) {
|
||||||
validation := &editorValidation{Editor: e}
|
return (&editorValidation{Editor: e}).addOwnedVector(ctx, objects, width, height)
|
||||||
|
}
|
||||||
|
|
||||||
|
// addOwnedVector 在资源不变的构建会话内登记矢量资源,复用资源校验结果
|
||||||
|
// 入参: ctx 取消上下文, objects 无原文和标识的对象, width 资源宽度, height 资源高度
|
||||||
|
// 返回: string 资源编号, error 校验、编码或取消错误
|
||||||
|
func (v *editorValidation) addOwnedVector(ctx context.Context, objects []GraphicObject, width, height float64) (string, error) {
|
||||||
|
e := v.Editor
|
||||||
|
if !finite(width) || !finite(height) || width < 0 || height < 0 {
|
||||||
|
return "", fmt.Errorf("invalid vector dimensions")
|
||||||
|
}
|
||||||
for index, object := range objects {
|
for index, object := range objects {
|
||||||
if err := ctx.Err(); err != nil {
|
if err := ctx.Err(); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
prepared, err := validation.prepareObject(e.nextID(), object)
|
prepared, err := v.prepareObject(e.nextID(), object)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
objects[index] = prepared
|
objects[index] = prepared
|
||||||
}
|
}
|
||||||
id := e.nextID()
|
id := e.nextID()
|
||||||
|
name := e.packageName("Res/Composites/Composite_" + id + ".xml")
|
||||||
|
scan := &editorReferenceScan{refs: &editorResourceRefs{ids: make(map[string]bool), files: make(map[string]bool), fonts: make(map[string]*editorFontUsage)}, objects: make(map[string]bool), name: name, safe: true}
|
||||||
data, err := encodeOFDXMLContext(ctx, func(x *ofdXML) {
|
data, err := encodeOFDXMLContext(ctx, func(x *ofdXML) {
|
||||||
|
x.references = scan
|
||||||
x.root("Res", ofdAttrs{{Name: xml.Name{Local: "BaseLoc"}, Value: "."}})
|
x.root("Res", ofdAttrs{{Name: xml.Name{Local: "BaseLoc"}, Value: "."}})
|
||||||
x.start("CompositeGraphicUnits", nil)
|
x.start("CompositeGraphicUnits", nil)
|
||||||
x.start("CompositeGraphicUnit", ofdAttrs{{Name: xml.Name{Local: "ID"}, Value: id},
|
x.start("CompositeGraphicUnit", ofdAttrs{{Name: xml.Name{Local: "ID"}, Value: id},
|
||||||
@@ -56,10 +72,20 @@ func (e *Editor) addOwnedVector(ctx context.Context, objects []GraphicObject, wi
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
resource, err := e.compositeResource(id, data)
|
resource := editorResource{name: name, data: data, composite: id}
|
||||||
if err != nil {
|
if scan.safe && scan.seen && len(scan.stack) == 0 {
|
||||||
return "", err
|
resource.references = slices.Sorted(maps.Keys(scan.objects))
|
||||||
|
resource.usage = scan.vectorUsage(data)
|
||||||
|
} else {
|
||||||
|
resource, err = e.compositeResource(id, data)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
e.resources = append(e.resources, resource)
|
e.resources = append(e.resources, resource)
|
||||||
|
if v.vectors == nil {
|
||||||
|
v.vectors = make(map[string]bool)
|
||||||
|
}
|
||||||
|
v.vectors[id] = true
|
||||||
return id, ctx.Err()
|
return id, ctx.Err()
|
||||||
}
|
}
|
||||||
@@ -431,6 +431,17 @@ func imageNRGBAAt(img image.Image, x, y int) color.NRGBA {
|
|||||||
return color.NRGBAModel.Convert(img.At(x, y)).(color.NRGBA)
|
return color.NRGBAModel.Convert(img.At(x, y)).(color.NRGBA)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// imageAlphaAt 读取十六位透明度,原生图像避免颜色装箱
|
||||||
|
// 入参: img 图像, x 横坐标, y 纵坐标
|
||||||
|
// 返回: uint32 十六位透明度
|
||||||
|
func imageAlphaAt(img image.Image, x, y int) uint32 {
|
||||||
|
if source, ok := img.(image.RGBA64Image); ok {
|
||||||
|
return uint32(source.RGBA64At(x, y).A)
|
||||||
|
}
|
||||||
|
_, _, _, alpha := img.At(x, y).RGBA()
|
||||||
|
return alpha
|
||||||
|
}
|
||||||
|
|
||||||
// imageNRGBA64At 读取十六位非预乘像素,原生图像避免颜色装箱
|
// imageNRGBA64At 读取十六位非预乘像素,原生图像避免颜色装箱
|
||||||
// 入参: img 图片对象, x X坐标, y Y坐标
|
// 入参: img 图片对象, x X坐标, y Y坐标
|
||||||
// 返回: color.NRGBA64 NRGBA64像素
|
// 返回: color.NRGBA64 NRGBA64像素
|
||||||
|
|||||||
+6
-7
@@ -32,7 +32,7 @@ var ErrPDFPassword = pdfgo.ErrPassword
|
|||||||
// Password默认使用PDF安全处理器编码,PasswordUTF8启用UTF-8输入及标准密码准备,空值尝试空密码
|
// Password默认使用PDF安全处理器编码,PasswordUTF8启用UTF-8输入及标准密码准备,空值尝试空密码
|
||||||
// Strict禁止恢复缺失资源或丢失内容语义,默认在报告中记录警告
|
// Strict禁止恢复缺失资源或丢失内容语义,默认在报告中记录警告
|
||||||
// RendererOptions复用渲染器字体来源和后端配置,Backends优先于其中的后端设置
|
// RendererOptions复用渲染器字体来源和后端配置,Backends优先于其中的后端设置
|
||||||
// RasterDPI指定局部透明效果合成精度,0沿用渲染器DPI,默认300dpi,Strict禁用局部合成
|
// RasterDPI指定局部合成与三维绘制精度,0沿用渲染器DPI,默认300dpi,Strict禁用局部合成
|
||||||
// OnProgress按open、pages、convert及write.*阶段报告进度,total为0表示总量未知
|
// OnProgress按open、pages、convert及write.*阶段报告进度,total为0表示总量未知
|
||||||
// ResolveFile读取PDF引用的外部媒体、附件和页面,默认不访问网络或本地路径
|
// ResolveFile读取PDF引用的外部媒体、附件和页面,默认不访问网络或本地路径
|
||||||
// ResolveReference可提供已解密的引用页面,目标阅读器由调用方维护
|
// ResolveReference可提供已解密的引用页面,目标阅读器由调用方维护
|
||||||
@@ -86,12 +86,15 @@ type pdfImporter struct {
|
|||||||
cmykSpace string
|
cmykSpace string
|
||||||
iccSpaces map[pdfICCKey]string
|
iccSpaces map[pdfICCKey]string
|
||||||
objects *pdfObjectBuffer
|
objects *pdfObjectBuffer
|
||||||
|
formResources *pdfFormCache
|
||||||
|
validation *editorValidation
|
||||||
pages map[pdfgo.Reference]*pdfgo.Page
|
pages map[pdfgo.Reference]*pdfgo.Page
|
||||||
pageIDs map[pdfgo.Reference]string
|
pageIDs map[pdfgo.Reference]string
|
||||||
pageIndexes map[pdfgo.Reference]int
|
pageIndexes map[pdfgo.Reference]int
|
||||||
imageIDs map[pdfImageKey][]pdfImageResource
|
imageIDs map[pdfImageKey][]pdfImageResource
|
||||||
imageContents *renderCache[pdfImageContentKey, pdfImageContent]
|
imageContents *renderCache[pdfImageContentKey, pdfImageContent]
|
||||||
attachmentIDs map[pdfAttachmentKey]string
|
attachmentIDs map[pdfAttachmentKey]string
|
||||||
|
modelAttachments map[*pdfgo.Stream]string
|
||||||
maskClips map[*pdfgo.SoftMask]pdfgo.Path
|
maskClips map[*pdfgo.SoftMask]pdfgo.Path
|
||||||
pageBox pdfgo.Rectangle
|
pageBox pdfgo.Rectangle
|
||||||
pageWidth float64
|
pageWidth float64
|
||||||
@@ -308,7 +311,7 @@ func (p *pdfImporter) commitObjects() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := p.editor.appendOwnedObjects(p.ctx, p.page, objects); err != nil {
|
if err := p.objectValidation().appendOwnedObjects(p.ctx, p.page, objects); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
p.objects = nil
|
p.objects = nil
|
||||||
@@ -778,11 +781,7 @@ func (p *pdfImporter) buildTilingPattern(source *pdfgo.TilingPattern, base pdfgo
|
|||||||
return nil, fmt.Errorf("invalid PDF tiling pattern raster scale")
|
return nil, fmt.Errorf("invalid PDF tiling pattern raster scale")
|
||||||
}
|
}
|
||||||
cell.objects, cell.pendingPath = nil, nil
|
cell.objects, cell.pendingPath = nil, nil
|
||||||
visitor := pdfgo.Visitor{Path: cell.path, Text: cell.text, Image: cell.image, Reference: p.referencePage}
|
if err := source.Walk(p.ctx, base, cell.visitor()); err != nil {
|
||||||
visitor.Group = func(mark pdfgo.GroupMark, walk func(pdfgo.Visitor) error) error {
|
|
||||||
return cell.group(mark, walk, visitor)
|
|
||||||
}
|
|
||||||
if err := source.Walk(p.ctx, base, visitor); err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if err := cell.flushPath(); err != nil {
|
if err := cell.flushPath(); err != nil {
|
||||||
|
|||||||
+501
@@ -0,0 +1,501 @@
|
|||||||
|
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package ofdgo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
"math"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/xiaoqidun/pdfgo"
|
||||||
|
)
|
||||||
|
|
||||||
|
// pdfModelAttachmentLimit 限制单次转换保留的共享模型索引数量
|
||||||
|
const pdfModelAttachmentLimit = 1024
|
||||||
|
|
||||||
|
// PDFThreeDRenderOptions 控制PDF三维视口的静态绘制,不改变注解激活状态
|
||||||
|
// 缓存与实例上限为0时采用U3DRenderOptions的默认值
|
||||||
|
type PDFThreeDRenderOptions struct {
|
||||||
|
Width, Height int
|
||||||
|
MaxMemoryBytes int64
|
||||||
|
MaxInstances int
|
||||||
|
}
|
||||||
|
|
||||||
|
// threeDAnnotation 按非交互呈现规则处理激活状态,保留原始模型与附件动作
|
||||||
|
// 入参: ctx 取消上下文, page PDF页面, annotation 三维注解
|
||||||
|
// 返回: error 外观、资源或取消错误
|
||||||
|
func (p *pdfImporter) threeDAnnotation(ctx context.Context, page *pdfgo.Page, annotation pdfgo.Annotation) error {
|
||||||
|
source, err := p.reader.ReadThreeD(annotation)
|
||||||
|
if err != nil {
|
||||||
|
return p.interactiveAppearanceFallback(ctx, page, annotation, "metadata", err)
|
||||||
|
}
|
||||||
|
var data []byte
|
||||||
|
id := p.modelAttachments[source.Stream]
|
||||||
|
if id == "" {
|
||||||
|
data, err = source.Stream.DecodeContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return p.interactiveAppearanceFallback(ctx, page, annotation, "model", err)
|
||||||
|
}
|
||||||
|
id, err = p.editor.AddAttachment(fmt.Sprintf("Model_%d.%s", p.page+1, strings.ToLower(string(source.Format))), data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if p.modelAttachments == nil {
|
||||||
|
p.modelAttachments = make(map[*pdfgo.Stream]string)
|
||||||
|
}
|
||||||
|
if len(p.modelAttachments) < pdfModelAttachmentLimit {
|
||||||
|
p.modelAttachments[source.Stream] = id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
action := Action{Event: "CLICK", GotoA: &GotoA{AttachID: id}}
|
||||||
|
var paint func(*pdfImporter) error
|
||||||
|
if source.ActivationPolicy.Activation != "XA" {
|
||||||
|
appearance, err := p.reader.ReadAnnotationAppearance(annotation)
|
||||||
|
if err != nil {
|
||||||
|
return p.interactiveAppearanceFallback(ctx, page, annotation, "appearance", err)
|
||||||
|
}
|
||||||
|
if appearance == nil {
|
||||||
|
return fmt.Errorf("missing PDF 3D normal appearance")
|
||||||
|
}
|
||||||
|
if data == nil {
|
||||||
|
data, err = p.editor.source.reader.AttachmentData(id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
paint, err = p.threeDPainter(ctx, source, annotation, data)
|
||||||
|
if err != nil {
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
p.warning(pdfgo.Diagnostic{Page: p.page + 1, Message: "PDF 3D rendering unavailable; normal appearance retained: " + err.Error()})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := p.paintedAnnotation(ctx, page, annotation, paint, action); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
message := "PDF 3D appearance and original model retained; advanced interaction not transferred to OFD"
|
||||||
|
if paint != nil {
|
||||||
|
message = "PDF 3D static view and original model retained; interactive rendering not transferred to OFD"
|
||||||
|
}
|
||||||
|
p.warning(pdfgo.Diagnostic{Page: p.page + 1, Message: message})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// threeDPainter 准备三维静态图像,复用库层压缩,失败时不提交注解对象
|
||||||
|
// 入参: ctx 取消上下文, source 三维参数, annotation 注解, data 原始模型
|
||||||
|
// 返回: func(*pdfImporter) error 绘制回调, error 模型、绘制或资源错误
|
||||||
|
func (p *pdfImporter) threeDPainter(ctx context.Context, source pdfgo.ThreeD, annotation pdfgo.Annotation, data []byte) (func(*pdfImporter) error, error) {
|
||||||
|
box := source.ViewBox
|
||||||
|
width, height := box.XMax-box.XMin, box.YMax-box.YMin
|
||||||
|
x, y := annotation.Rect.XMin/2+annotation.Rect.XMax/2, annotation.Rect.YMin/2+annotation.Rect.YMax/2
|
||||||
|
var nodes []pdfCompositeNode
|
||||||
|
if source.Background.RGB == nil {
|
||||||
|
return nil, &pdfgo.UnsupportedError{Feature: "3D background color space"}
|
||||||
|
}
|
||||||
|
if source.Background.EntireAnnotation {
|
||||||
|
r := annotation.Rect
|
||||||
|
path := pdfgo.Path{Segments: []pdfgo.Segment{
|
||||||
|
{Operator: "M", Points: []pdfgo.Point{{X: r.XMin, Y: r.YMin}}},
|
||||||
|
{Operator: "L", Points: []pdfgo.Point{{X: r.XMax, Y: r.YMin}}},
|
||||||
|
{Operator: "L", Points: []pdfgo.Point{{X: r.XMax, Y: r.YMax}}},
|
||||||
|
{Operator: "L", Points: []pdfgo.Point{{X: r.XMin, Y: r.YMax}}},
|
||||||
|
{Operator: "C"},
|
||||||
|
}}
|
||||||
|
nodes = append(nodes, pdfCompositeNode{path: &pdfgo.PathMark{Path: path, Fill: true, Style: pdfgo.Style{Fill: pdfgo.Paint{RGB: *source.Background.RGB, Alpha: 1}}}})
|
||||||
|
}
|
||||||
|
if width > 0 && height > 0 {
|
||||||
|
if source.Format != "U3D" {
|
||||||
|
return nil, &pdfgo.UnsupportedError{Feature: "3D model format " + string(source.Format)}
|
||||||
|
}
|
||||||
|
model, err := DecodeU3D(ctx, data, U3DOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
widthMM := width * math.Hypot(p.matrix[0], p.matrix[1])
|
||||||
|
heightMM := height * math.Hypot(p.matrix[2], p.matrix[3])
|
||||||
|
w, h := math.Ceil(widthMM*p.rasterDPI/25.4), math.Ceil(heightMM*p.rasterDPI/25.4)
|
||||||
|
if !finite(w) || !finite(h) || w <= 0 || h <= 0 || w > 1<<30 || h > 1<<30 {
|
||||||
|
return nil, fmt.Errorf("invalid PDF 3D raster dimensions")
|
||||||
|
}
|
||||||
|
img, err := RenderPDFThreeD(ctx, model, source, PDFThreeDRenderOptions{Width: int(w), Height: int(h)})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
resource, err := p.reader.CreateImage(ctx, img)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
nodes = append(nodes, pdfCompositeNode{image: &pdfgo.ImageMark{Image: resource, Matrix: pdfgo.Matrix{width, 0, 0, height, x + box.XMin, y + box.YMin}, Style: pdfgo.Style{Fill: pdfgo.Paint{Alpha: 1}}}})
|
||||||
|
}
|
||||||
|
return func(stamp *pdfImporter) error {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return stamp.compositeObjects(nodes)
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RenderPDFThreeD 按PDF相机、投影、背景与灯光绘制3DB区域,不执行脚本或动画
|
||||||
|
// 返回图像不含3DB以外的注解背景,未支持的视图效果返回明确错误
|
||||||
|
// 每次从传入场景建立独立节点状态,不继承此前绘制的运行时状态
|
||||||
|
// 入参: ctx 取消上下文, model 解码后的U3D模型, source PDF三维参数, options 输出与资源限制
|
||||||
|
// 返回: *image.RGBA 不透明图像, error 参数、能力、预算或取消错误
|
||||||
|
func RenderPDFThreeD(ctx context.Context, model *U3DModel, source pdfgo.ThreeD, options PDFThreeDRenderOptions) (*image.RGBA, error) {
|
||||||
|
if ctx == nil {
|
||||||
|
return nil, fmt.Errorf("invalid PDF 3D render context")
|
||||||
|
}
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if source.Format != "U3D" {
|
||||||
|
return nil, &pdfgo.UnsupportedError{Feature: "3D model format " + string(source.Format)}
|
||||||
|
}
|
||||||
|
presentation := source.Presentation
|
||||||
|
if presentation.RenderMode != "" && presentation.RenderMode != "Solid" || presentation.Overlay != nil {
|
||||||
|
return nil, &pdfgo.UnsupportedError{Feature: "3D view render mode or overlay"}
|
||||||
|
}
|
||||||
|
if source.Background.RGB == nil {
|
||||||
|
return nil, &pdfgo.UnsupportedError{Feature: "3D background color space"}
|
||||||
|
}
|
||||||
|
box := source.ViewBox
|
||||||
|
width, height := box.XMax-box.XMin, box.YMax-box.YMin
|
||||||
|
if !finite(width) || !finite(height) || width <= 0 || height <= 0 {
|
||||||
|
return nil, fmt.Errorf("invalid PDF 3D viewport")
|
||||||
|
}
|
||||||
|
projection := pdfgo.ThreeDProjection{Subtype: "P", Clipping: "ANF", FieldOfView: 90, PerspectiveBinding: "W"}
|
||||||
|
if source.Projection != nil {
|
||||||
|
projection = *source.Projection
|
||||||
|
}
|
||||||
|
render := U3DRenderOptions{Width: options.Width, Height: options.Height, MaxMemoryBytes: options.MaxMemoryBytes, MaxInstances: options.MaxInstances, Background: source.Background.RGB}
|
||||||
|
if err := pdfThreeDNodeStates(ctx, presentation.Nodes, &render); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := pdfThreeDSections(ctx, presentation.CrossSections, &render); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
r, err := newU3DRender(ctx, model, render)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
camera, view, err := r.pdfCamera(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)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := r.prepareSections(render.Sections); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
scale := 0.0
|
||||||
|
switch projection.Subtype {
|
||||||
|
case "P":
|
||||||
|
if !finite(projection.FieldOfView) || projection.FieldOfView <= 0 || projection.FieldOfView >= 180 {
|
||||||
|
return nil, &pdfgo.UnsupportedError{Feature: "degenerate 3D perspective field of view"}
|
||||||
|
}
|
||||||
|
diameter := projection.PerspectiveDiameter
|
||||||
|
if projection.PerspectiveBinding != "" {
|
||||||
|
diameter, err = pdfThreeDBinding(projection.PerspectiveBinding, width, height)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !finite(diameter) || diameter <= 0 {
|
||||||
|
return nil, fmt.Errorf("invalid PDF 3D perspective diameter")
|
||||||
|
}
|
||||||
|
scale = diameter / (2 * math.Tan(projection.FieldOfView*math.Pi/360))
|
||||||
|
camera.Perspective = true
|
||||||
|
case "O":
|
||||||
|
width := source.AnnotationBox.XMax - source.AnnotationBox.XMin
|
||||||
|
height := source.AnnotationBox.YMax - source.AnnotationBox.YMin
|
||||||
|
binding, err := pdfThreeDBinding(projection.OrthographicBinding, width, height)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
scale = projection.OrthographicScale * binding
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("invalid PDF 3D projection subtype")
|
||||||
|
}
|
||||||
|
camera.ScaleX = scale * float64(options.Width) / width
|
||||||
|
camera.ScaleY = scale * float64(options.Height) / height
|
||||||
|
switch projection.Clipping {
|
||||||
|
case "XNF":
|
||||||
|
camera.Near = 0
|
||||||
|
if projection.Near != nil {
|
||||||
|
camera.Near = *projection.Near
|
||||||
|
}
|
||||||
|
if !finite(camera.Near) || camera.Near < 0 || camera.Perspective && camera.Near == 0 {
|
||||||
|
return nil, fmt.Errorf("invalid PDF 3D near distance")
|
||||||
|
}
|
||||||
|
if projection.Far != nil {
|
||||||
|
camera.ClipFar = true
|
||||||
|
camera.Far = *projection.Far
|
||||||
|
if !finite(camera.Far) {
|
||||||
|
return nil, fmt.Errorf("invalid PDF 3D far distance")
|
||||||
|
}
|
||||||
|
if camera.Far < camera.Near {
|
||||||
|
passes, camera.Far = nil, 0
|
||||||
|
camera.ClipFar = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "ANF":
|
||||||
|
minimum, maximum, found, err := r.depthRange()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
passes = nil
|
||||||
|
}
|
||||||
|
camera.Near = minimum
|
||||||
|
if camera.Perspective {
|
||||||
|
if maximum <= 0 {
|
||||||
|
passes = nil
|
||||||
|
}
|
||||||
|
camera.Near = max(1e-300, maximum*1e-9)
|
||||||
|
if minimum > 0 {
|
||||||
|
camera.Near = max(1e-300, minimum/2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
camera.Far = math.Nextafter(maximum, math.Inf(1))
|
||||||
|
if !finite(camera.Far) || camera.Far <= camera.Near {
|
||||||
|
camera.Far = 0
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("invalid PDF 3D clipping style")
|
||||||
|
}
|
||||||
|
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 {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
render.Lighting = &U3DLighting{DiffuseOffset: source.Lighting.Ambient, Lights: make([]U3DDirectionalLight, len(source.Lighting.Lights))}
|
||||||
|
for i, light := range source.Lighting.Lights {
|
||||||
|
if i%256 == 0 {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
direction := light.Direction
|
||||||
|
if light.CameraAttached {
|
||||||
|
direction = [3]float64{0, 0, 1}
|
||||||
|
}
|
||||||
|
render.Lighting.Lights[i] = U3DDirectionalLight{Color: light.Color, Direction: direction, CameraRelative: light.CameraAttached}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return r.render(render, passes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// pdfThreeDBinding 计算目标区域的标准绑定倍率,不按模型包围盒重新适配
|
||||||
|
// 入参: binding 绑定方式, width 区域宽度, height 区域高度
|
||||||
|
// 返回: float64 倍率, error 方式或尺寸错误
|
||||||
|
func pdfThreeDBinding(binding pdfgo.Name, width, height float64) (float64, error) {
|
||||||
|
if binding == "Absolute" {
|
||||||
|
return 1, nil
|
||||||
|
}
|
||||||
|
if !finite(width) || !finite(height) || width <= 0 || height <= 0 {
|
||||||
|
return 0, fmt.Errorf("invalid PDF 3D binding dimensions")
|
||||||
|
}
|
||||||
|
switch binding {
|
||||||
|
case "W":
|
||||||
|
return width, nil
|
||||||
|
case "H":
|
||||||
|
return height, nil
|
||||||
|
case "Min":
|
||||||
|
return min(width, height), nil
|
||||||
|
case "Max":
|
||||||
|
return max(width, height), nil
|
||||||
|
}
|
||||||
|
return 0, fmt.Errorf("invalid PDF 3D binding")
|
||||||
|
}
|
||||||
|
|
||||||
|
// pdfCamera 解析PDF相机来源并保留模型视图的绘制轮次
|
||||||
|
// 入参: source 相机来源
|
||||||
|
// 返回: U3DCamera 相机变换, string 视图节点, error 路径或变换错误
|
||||||
|
func (r *u3dRender) pdfCamera(source pdfgo.ThreeDCamera) (U3DCamera, string, error) {
|
||||||
|
var camera U3DCamera
|
||||||
|
selected := -1
|
||||||
|
for i, node := range r.model.Nodes {
|
||||||
|
if i%256 == 0 {
|
||||||
|
if err := r.ctx.Err(); err != nil {
|
||||||
|
return camera, "", err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if node.Kind == "View" {
|
||||||
|
selected = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if source.Source == "M" {
|
||||||
|
m := source.Matrix
|
||||||
|
camera.ToWorld = [16]float64{m[0], m[1], m[2], 0, m[3], m[4], m[5], 0, m[6], m[7], m[8], 0, m[9], m[10], m[11], 1}
|
||||||
|
} else {
|
||||||
|
if source.Source != "" && source.Source != "U3D" || source.Source == "U3D" && len(source.Path) == 0 {
|
||||||
|
return camera, "", fmt.Errorf("invalid PDF 3D camera source")
|
||||||
|
}
|
||||||
|
if source.Source == "U3D" {
|
||||||
|
selected = -1
|
||||||
|
for i, node := range r.model.Nodes {
|
||||||
|
if i%256 == 0 {
|
||||||
|
if err := r.ctx.Err(); err != nil {
|
||||||
|
return camera, "", err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if node.Name == source.Path[len(source.Path)-1] {
|
||||||
|
selected = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if selected < 0 || r.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
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
return camera, "", fmt.Errorf("missing PDF 3D camera path")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
view := ""
|
||||||
|
if selected >= 0 {
|
||||||
|
view = r.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, face := range mesh.Faces {
|
||||||
|
if j%256 == 0 {
|
||||||
|
if err := r.ctx.Err(); err != nil {
|
||||||
|
return 0, 0, false, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, corner := range face.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 预算或取消错误
|
||||||
|
func pdfThreeDNodeStates(ctx context.Context, nodes []pdfgo.ThreeDNode, options *U3DRenderOptions) error {
|
||||||
|
if len(nodes) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
limit := options.MaxMemoryBytes
|
||||||
|
if limit == 0 {
|
||||||
|
limit = u3dDefaultBytes
|
||||||
|
}
|
||||||
|
if limit <= 0 || uint64(len(nodes)) >= uint64(limit)/168 {
|
||||||
|
return fmt.Errorf("PDF 3D node memory exceeds limit")
|
||||||
|
}
|
||||||
|
options.MaxMemoryBytes = limit - int64(len(nodes))*168
|
||||||
|
options.Nodes = make([]U3DNodeState, len(nodes))
|
||||||
|
for i, node := range nodes {
|
||||||
|
if i%256 == 0 {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
state := U3DNodeState{Name: node.Name, Opacity: node.Opacity, Visible: node.Visible}
|
||||||
|
if node.Matrix != nil {
|
||||||
|
m := node.Matrix
|
||||||
|
state.Matrix = &[16]float64{m[0], m[1], m[2], 0, m[3], m[4], m[5], 0, m[6], m[7], m[8], 0, m[9], m[10], m[11], 1}
|
||||||
|
}
|
||||||
|
options.Nodes[i] = state
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package ofdgo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
|
||||||
|
"github.com/xiaoqidun/pdfgo"
|
||||||
|
)
|
||||||
|
|
||||||
|
// pdfThreeDSections 按XYZ顺序构造剖切方向,中心保持为世界坐标固定点
|
||||||
|
// 入参: ctx 取消上下文, sections PDF剖切面, options 绘制选项
|
||||||
|
// 返回: error 方向、预算或取消错误
|
||||||
|
func pdfThreeDSections(ctx context.Context, sections []pdfgo.ThreeDCrossSection, options *U3DRenderOptions) error {
|
||||||
|
if len(sections) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
limit := options.MaxMemoryBytes
|
||||||
|
if limit == 0 {
|
||||||
|
limit = u3dDefaultBytes
|
||||||
|
}
|
||||||
|
if limit <= 0 || uint64(len(sections)) >= uint64(limit)/136 {
|
||||||
|
return fmt.Errorf("PDF 3D section memory exceeds limit")
|
||||||
|
}
|
||||||
|
options.MaxMemoryBytes = limit - int64(len(sections))*136
|
||||||
|
options.Sections = make([]U3DCrossSection, len(sections))
|
||||||
|
for i, source := range sections {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if source.Axis < 0 || source.Axis > 2 {
|
||||||
|
return fmt.Errorf("invalid PDF 3D section axis")
|
||||||
|
}
|
||||||
|
section := U3DCrossSection{Center: source.Center, Color: source.Color, Opacity: source.Opacity}
|
||||||
|
section.AxisU[(source.Axis+1)%3], section.AxisV[(source.Axis+2)%3] = 1, 1
|
||||||
|
for axis, angle := range source.Rotation {
|
||||||
|
if axis == source.Axis {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !finite(angle) {
|
||||||
|
return fmt.Errorf("invalid PDF 3D section rotation")
|
||||||
|
}
|
||||||
|
sin, cos := math.Sincos(math.Remainder(angle, 360) * math.Pi / 180)
|
||||||
|
a, b := (axis+1)%3, (axis+2)%3
|
||||||
|
for _, vector := range []*[3]float64{§ion.AxisU, §ion.AxisV} {
|
||||||
|
vector[a], vector[b] = cos*vector[a]-sin*vector[b], sin*vector[a]+cos*vector[b]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if source.IntersectionVisible {
|
||||||
|
color := source.IntersectionColor
|
||||||
|
section.Intersection = &color
|
||||||
|
}
|
||||||
|
options.Sections[i] = section
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
+32
-19
@@ -29,6 +29,13 @@ import (
|
|||||||
// 入参: ctx 取消上下文, page PDF页面, annotation PDF注解, actions 外观区域的动作
|
// 入参: ctx 取消上下文, page PDF页面, annotation PDF注解, actions 外观区域的动作
|
||||||
// 返回: error 外观或转换错误
|
// 返回: error 外观或转换错误
|
||||||
func (p *pdfImporter) appearanceAnnotation(ctx context.Context, page *pdfgo.Page, annotation pdfgo.Annotation, actions ...Action) error {
|
func (p *pdfImporter) appearanceAnnotation(ctx context.Context, page *pdfgo.Page, annotation pdfgo.Annotation, actions ...Action) error {
|
||||||
|
return p.paintedAnnotation(ctx, page, annotation, nil, actions...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// paintedAnnotation 复用注解坐标、标志与元数据处理,允许替换静态绘制内容
|
||||||
|
// 入参: ctx 取消上下文, page PDF页面, annotation PDF注解, paint 绘制回调,空值使用原外观, actions 外观区域的动作
|
||||||
|
// 返回: error 外观或转换错误
|
||||||
|
func (p *pdfImporter) paintedAnnotation(ctx context.Context, page *pdfgo.Page, annotation pdfgo.Annotation, paint func(*pdfImporter) error, actions ...Action) error {
|
||||||
for _, key := range []pdfgo.Name{"AA", "OC", "A"} {
|
for _, key := range []pdfgo.Name{"AA", "OC", "A"} {
|
||||||
if annotation.Subtype == "Widget" && (key == "A" || key == "AA") {
|
if annotation.Subtype == "Widget" && (key == "A" || key == "AA") {
|
||||||
continue
|
continue
|
||||||
@@ -74,29 +81,35 @@ func (p *pdfImporter) appearanceAnnotation(ctx context.Context, page *pdfgo.Page
|
|||||||
stamp.compositeCache = nil
|
stamp.compositeCache = nil
|
||||||
stamp.clipTexts = make(map[*pdfgo.TextClip][]TextObject)
|
stamp.clipTexts = make(map[*pdfgo.TextClip][]TextObject)
|
||||||
stamp.pageWidth, stamp.pageHeight = box.W, box.H
|
stamp.pageWidth, stamp.pageHeight = box.W, box.H
|
||||||
nodes, err := p.collectCompositeNodes(func(visitor pdfgo.Visitor) error {
|
if paint != nil {
|
||||||
if annotation.Subtype == "Widget" {
|
if err := paint(&stamp); err != nil {
|
||||||
visitor.MarkedContent = func(mark pdfgo.MarkedContentMark) error {
|
return err
|
||||||
if mark.Tag == "Tx" && len(mark.Properties) == 0 {
|
}
|
||||||
|
} else {
|
||||||
|
nodes, err := p.collectCompositeNodes(func(visitor pdfgo.Visitor) error {
|
||||||
|
if annotation.Subtype == "Widget" {
|
||||||
|
visitor.MarkedContent = func(mark pdfgo.MarkedContentMark) error {
|
||||||
|
if mark.Tag == "Tx" && len(mark.Properties) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if mark.Operator == "EMC" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if p.warning == nil {
|
||||||
|
return &pdfgo.UnsupportedError{Feature: "widget marked content " + string(mark.Tag)}
|
||||||
|
}
|
||||||
|
p.warning(pdfgo.Diagnostic{Message: "PDF widget marked content " + string(mark.Tag) + " not preserved"})
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if mark.Operator == "EMC" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if p.warning == nil {
|
|
||||||
return &pdfgo.UnsupportedError{Feature: "widget marked content " + string(mark.Tag)}
|
|
||||||
}
|
|
||||||
p.warning(pdfgo.Diagnostic{Message: "PDF widget marked content " + string(mark.Tag) + " not preserved"})
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
return p.reader.WalkAnnotationAppearance(ctx, page, annotation, visitor)
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := stamp.compositeObjects(nodes); err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
return p.reader.WalkAnnotationAppearance(ctx, page, annotation, visitor)
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := stamp.compositeObjects(nodes); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
if flags&(2|32) == 0 && !invisible {
|
if flags&(2|32) == 0 && !invisible {
|
||||||
p.compositeNodes = stamp.compositeNodes
|
p.compositeNodes = stamp.compositeNodes
|
||||||
|
|||||||
@@ -25,6 +25,15 @@ type pdfObjectBuffer struct {
|
|||||||
size int
|
size int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// objectValidation 在单次导入中共享不可变资源的校验结果,隔离局部编辑器
|
||||||
|
// 返回: *editorValidation 当前编辑器的构建校验会话
|
||||||
|
func (p *pdfImporter) objectValidation() *editorValidation {
|
||||||
|
if p.validation == nil || p.validation.Editor != p.editor {
|
||||||
|
p.validation = &editorValidation{Editor: p.editor}
|
||||||
|
}
|
||||||
|
return p.validation
|
||||||
|
}
|
||||||
|
|
||||||
// appendObject 追加本次导入对象,按需建立独立暂存缓冲
|
// appendObject 追加本次导入对象,按需建立独立暂存缓冲
|
||||||
// 入参: object 独立导入对象
|
// 入参: object 独立导入对象
|
||||||
func (p *pdfImporter) appendObject(object GraphicObject) {
|
func (p *pdfImporter) appendObject(object GraphicObject) {
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ func (p *pdfImporter) sharedClipObjects(objects []GraphicObject) ([]GraphicObjec
|
|||||||
for index := range members {
|
for index := range members {
|
||||||
*editorObjectClips(&members[index]) = nil
|
*editorObjectClips(&members[index]) = nil
|
||||||
}
|
}
|
||||||
id, err := p.editor.addOwnedVector(p.ctx, members, p.pageWidth, p.pageHeight)
|
id, err := p.objectValidation().addOwnedVector(p.ctx, members, p.pageWidth, p.pageHeight)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
+34
-2
@@ -33,6 +33,7 @@ type pdfCompositeNode struct {
|
|||||||
children []pdfCompositeNode
|
children []pdfCompositeNode
|
||||||
textObject *pdfgo.TextObject
|
textObject *pdfgo.TextObject
|
||||||
glyph *pdfgo.TextMark
|
glyph *pdfgo.TextMark
|
||||||
|
formCount int
|
||||||
}
|
}
|
||||||
|
|
||||||
// pdfCompositePixel 保存非预乘源空间分量及累计透明度
|
// pdfCompositePixel 保存非预乘源空间分量及累计透明度
|
||||||
@@ -160,7 +161,15 @@ func (c *pdfCompositor) coverage(node pdfCompositeNode, stroke bool) (image.Imag
|
|||||||
if geometry.scene == nil || b.X >= c.box.X+c.box.W || b.Y >= c.box.Y+c.box.H || b.X+b.W <= c.box.X || b.Y+b.H <= c.box.Y {
|
if geometry.scene == nil || b.X >= c.box.X+c.box.W || b.Y >= c.box.Y+c.box.H || b.X+b.W <= c.box.X || b.Y+b.H <= c.box.Y {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
return c.importer.renderGroupScene(geometry.scene, c.box)
|
target, err := c.acquireCoverage()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
coverage, err := c.importer.renderGroupSceneInto(geometry.scene, c.box, target)
|
||||||
|
if err != nil {
|
||||||
|
c.releaseCoverage(target)
|
||||||
|
}
|
||||||
|
return coverage, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// geometry 缓存图元或组的实际覆盖场景与像素对齐边界
|
// geometry 缓存图元或组的实际覆盖场景与像素对齐边界
|
||||||
@@ -431,6 +440,17 @@ func (p *pdfImporter) collectCompositeNodes(walk func(pdfgo.Visitor) error) ([]p
|
|||||||
nodes = append(nodes, pdfCompositeNode{group: &mark, children: children})
|
nodes = append(nodes, pdfCompositeNode{group: &mark, children: children})
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
v.Form = func(_ pdfgo.FormMark, walk func(pdfgo.Visitor) error) error {
|
||||||
|
children, err := collect(walk)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(children) != 0 {
|
||||||
|
children[0].formCount = len(children)
|
||||||
|
nodes = append(nodes, children...)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
err := walk(v)
|
err := walk(v)
|
||||||
return nodes, err
|
return nodes, err
|
||||||
}
|
}
|
||||||
@@ -503,7 +523,19 @@ func (p *pdfImporter) compositePage(space *pdfgo.ColorSpace, walk func(pdfgo.Vis
|
|||||||
// 返回: error 转换或合成错误
|
// 返回: error 转换或合成错误
|
||||||
func (p *pdfImporter) compositeObjects(nodes []pdfCompositeNode) error {
|
func (p *pdfImporter) compositeObjects(nodes []pdfCompositeNode) error {
|
||||||
space := p.compositeSpace
|
space := p.compositeSpace
|
||||||
for i, node := range nodes {
|
for i := 0; i < len(nodes); i++ {
|
||||||
|
node := nodes[i]
|
||||||
|
if count := node.formCount; count >= 16 && count <= len(nodes)-i && !p.transferBackdrop {
|
||||||
|
packed, err := p.compositeForm(nodes[i : i+count])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("convert form at graphic %d: %w", i+1, err)
|
||||||
|
}
|
||||||
|
if packed {
|
||||||
|
p.compositeNodes = append(p.compositeNodes, nodes[i:i+count]...)
|
||||||
|
i += count - 1
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
direct, err := p.directCompositeNode(node, space)
|
direct, err := p.directCompositeNode(node, space)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("inspect graphic %d: %w", i+1, err)
|
return fmt.Errorf("inspect graphic %d: %w", i+1, err)
|
||||||
|
|||||||
@@ -109,6 +109,7 @@ func (c *pdfCompositor) drawPaint(node pdfCompositeNode, outline bool, paint pdf
|
|||||||
if err != nil || coverage == nil {
|
if err != nil || coverage == nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
defer c.releaseCoverage(coverage)
|
||||||
style, _, _ := node.style()
|
style, _, _ := node.style()
|
||||||
var transfer *pdfgo.TransferFunction
|
var transfer *pdfgo.TransferFunction
|
||||||
imageShape := false
|
imageShape := false
|
||||||
@@ -198,7 +199,7 @@ func (c *pdfCompositor) drawPaint(node pdfCompositeNode, outline bool, paint pdf
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for x := region.Min.X; x < region.Max.X; x++ {
|
for x := region.Min.X; x < region.Max.X; x++ {
|
||||||
_, _, _, a := coverage.At(x, y).RGBA()
|
a := imageAlphaAt(coverage, x, y)
|
||||||
if a == 0 {
|
if a == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,8 @@
|
|||||||
|
|
||||||
package ofdgo
|
package ofdgo
|
||||||
|
|
||||||
|
import "image"
|
||||||
|
|
||||||
// pdfCompositeTileSize 限定单次合成块的像素边长
|
// pdfCompositeTileSize 限定单次合成块的像素边长
|
||||||
const pdfCompositeTileSize = 256
|
const pdfCompositeTileSize = 256
|
||||||
|
|
||||||
@@ -22,8 +24,51 @@ type pdfCompositeBuffers[T any] [8][]T
|
|||||||
|
|
||||||
// pdfCompositeScratch 复用像素及蒙版缓冲,不跨页持有活动内容
|
// pdfCompositeScratch 复用像素及蒙版缓冲,不跨页持有活动内容
|
||||||
type pdfCompositeScratch struct {
|
type pdfCompositeScratch struct {
|
||||||
pixels pdfCompositeBuffers[pdfCompositePixel]
|
pixels pdfCompositeBuffers[pdfCompositePixel]
|
||||||
masks pdfCompositeBuffers[float64]
|
masks pdfCompositeBuffers[float64]
|
||||||
|
rasters [8]*image.RGBA
|
||||||
|
}
|
||||||
|
|
||||||
|
// acquireCoverage 借用当前块的独占画布,保留活动画布供嵌套合成读取
|
||||||
|
// 返回: *image.RGBA 未清空画布,不支持缓冲绘制的后端返回nil, error 无效绘制区域
|
||||||
|
func (c *pdfCompositor) acquireCoverage() (*image.RGBA, error) {
|
||||||
|
if _, ok := c.importer.editor.Backends().Raster.(RasterBufferBackend); !ok {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
page := RasterPage{Width: c.box.W, Height: c.box.H, DPI: c.importer.rasterDPI}
|
||||||
|
w, h, err := page.PixelSize()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if c.scratch == nil {
|
||||||
|
c.scratch = &pdfCompositeScratch{}
|
||||||
|
}
|
||||||
|
rect := image.Rect(0, 0, w, h)
|
||||||
|
for i, stored := range c.scratch.rasters {
|
||||||
|
if stored != nil && stored.Rect == rect {
|
||||||
|
c.scratch.rasters[i] = nil
|
||||||
|
return stored, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return image.NewRGBA(rect), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// releaseCoverage 归还当前调用不再读取的画布,限制单次合成保留的内存
|
||||||
|
// 入参: coverage 当前调用的覆盖图像
|
||||||
|
func (c *pdfCompositor) releaseCoverage(coverage image.Image) {
|
||||||
|
if _, ok := c.importer.editor.Backends().Raster.(RasterBufferBackend); !ok || c.scratch == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
buffer, ok := coverage.(*image.RGBA)
|
||||||
|
if !ok || buffer == nil || cap(buffer.Pix) > 4*pdfCompositeTileSize*pdfCompositeTileSize {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for i, stored := range c.scratch.rasters {
|
||||||
|
if stored == nil {
|
||||||
|
c.scratch.rasters[i] = buffer
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// acquire 借用尚未初始化的独立缓冲,嵌套调用不复用活动缓冲
|
// acquire 借用尚未初始化的独立缓冲,嵌套调用不复用活动缓冲
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ func (c *pdfCompositor) shadingPattern(pattern *pdfgo.ShadingPattern, node pdfCo
|
|||||||
c.releasePixels(result)
|
c.releasePixels(result)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
defer c.releaseCoverage(coverage)
|
||||||
step := 25.4 / c.importer.rasterDPI
|
step := 25.4 / c.importer.rasterDPI
|
||||||
for i := range result {
|
for i := range result {
|
||||||
if i%pdfCompositeTileSize == 0 {
|
if i%pdfCompositeTileSize == 0 {
|
||||||
@@ -90,7 +91,7 @@ func (c *pdfCompositor) shadingPattern(pattern *pdfgo.ShadingPattern, node pdfCo
|
|||||||
}
|
}
|
||||||
shape := 0.0
|
shape := 0.0
|
||||||
if coverage != nil {
|
if coverage != nil {
|
||||||
_, _, _, a := coverage.At(i%c.width, i/c.width).RGBA()
|
a := imageAlphaAt(coverage, i%c.width, i/c.width)
|
||||||
shape = float64(a) / 65535
|
shape = float64(a) / 65535
|
||||||
}
|
}
|
||||||
if nodes[0].image != nil && shape != 0 {
|
if nodes[0].image != nil && shape != 0 {
|
||||||
|
|||||||
@@ -0,0 +1,142 @@
|
|||||||
|
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package ofdgo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/xml"
|
||||||
|
)
|
||||||
|
|
||||||
|
// pdfFormBufferLimit 限制表单编码暂存容量,不限制资源大小
|
||||||
|
const pdfFormBufferLimit = 8 << 20
|
||||||
|
|
||||||
|
// pdfFormResource 保存生成内容及资源编号,复用前核对完整内容
|
||||||
|
type pdfFormResource struct {
|
||||||
|
content []byte
|
||||||
|
id string
|
||||||
|
}
|
||||||
|
|
||||||
|
// pdfFormCache 隔离编辑器的表单资源,复用编码缓冲而不共享缓存内容
|
||||||
|
type pdfFormCache struct {
|
||||||
|
editor *Editor
|
||||||
|
entries renderCache[[32]byte, pdfFormResource]
|
||||||
|
buffer bytes.Buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
// encode 按原顺序编码对象分块,不为缓存检查整理连续对象数组
|
||||||
|
// 入参: ctx 取消上下文, objects 表单对象, width 页面宽度, height 页面高度
|
||||||
|
// 返回: []byte 下次编码或重置前有效的内容, error 编码或取消错误
|
||||||
|
func (c *pdfFormCache) encode(ctx context.Context, objects *pdfObjectBuffer, width, height float64) ([]byte, error) {
|
||||||
|
c.buffer.Reset()
|
||||||
|
x := newOFDXML(&c.buffer)
|
||||||
|
x.ctx = ctx
|
||||||
|
x.root("Content", ofdAttrs{{Name: xml.Name{Local: "Width"}, Value: ofdNumber(width)},
|
||||||
|
{Name: xml.Name{Local: "Height"}, Value: ofdNumber(height)}})
|
||||||
|
for _, part := range objects.parts {
|
||||||
|
for _, object := range part {
|
||||||
|
x.object(object, false)
|
||||||
|
if x.err != nil {
|
||||||
|
return nil, x.err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
x.end("Content")
|
||||||
|
if err := x.finish(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return c.buffer.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// reset 清空临时内容,释放超限缓冲,不修改已登记资源
|
||||||
|
func (c *pdfFormCache) reset() {
|
||||||
|
if c.buffer.Cap() > pdfFormBufferLimit {
|
||||||
|
c.buffer = bytes.Buffer{}
|
||||||
|
} else {
|
||||||
|
c.buffer.Reset()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// compositeForm 将无需背景合成的普通表单保存为矢量资源,保留图元的实际坐标和裁剪
|
||||||
|
// 入参: nodes 普通表单连续图元
|
||||||
|
// 返回: bool 是否已保存为复合对象, error 检查或转换错误
|
||||||
|
func (p *pdfImporter) compositeForm(nodes []pdfCompositeNode) (bool, error) {
|
||||||
|
if pdfCompositeHasTransfer(nodes) {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
for _, node := range nodes {
|
||||||
|
if err := p.ctx.Err(); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
direct, err := p.directCompositeNode(node, p.compositeSpace)
|
||||||
|
if err != nil || !direct {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := p.flushPath(); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
parent := p.objects
|
||||||
|
p.objects = nil
|
||||||
|
defer func() { p.objects = parent }()
|
||||||
|
visitor := p.visitor()
|
||||||
|
for _, node := range nodes {
|
||||||
|
if err := node.emit(visitor); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := p.flushPath(); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if err := p.ctx.Err(); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if p.objects.len() == 0 {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
if p.formResources == nil || p.formResources.editor != p.editor {
|
||||||
|
p.formResources = &pdfFormCache{editor: p.editor, entries: renderCache[[32]byte, pdfFormResource]{limit: 32 << 20}}
|
||||||
|
}
|
||||||
|
cache := p.formResources
|
||||||
|
defer cache.reset()
|
||||||
|
content, err := cache.encode(p.ctx, p.objects, p.pageWidth, p.pageHeight)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
key := sha256.Sum256(content)
|
||||||
|
resource, found := cache.entries.get(key)
|
||||||
|
if !found || !bytes.Equal(resource.content, content) {
|
||||||
|
objects, err := p.objects.data(p.ctx)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
id, err := p.objectValidation().addOwnedVector(p.ctx, objects, p.pageWidth, p.pageHeight)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
resource = pdfFormResource{id: id}
|
||||||
|
if len(content) <= cache.entries.limit-len(id)-128 {
|
||||||
|
resource.content = bytes.Clone(content)
|
||||||
|
cache.entries.put(key, resource, cap(resource.content)+len(id)+128)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p.objects = parent
|
||||||
|
p.appendObject(GraphicObject{Type: "CompositeObject", CompositeGraphicUnit: CompositeGraphicUnit{
|
||||||
|
Boundary: pdfBoundary(Box{W: p.pageWidth, H: p.pageHeight}), ResourceID: resource.id,
|
||||||
|
}})
|
||||||
|
parent = p.objects
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
+16
-3
@@ -158,9 +158,22 @@ func (p *pdfImporter) compileCoverage(node pdfCompositeNode, stroke bool) (*Rast
|
|||||||
}
|
}
|
||||||
evenOdd = false
|
evenOdd = false
|
||||||
}
|
}
|
||||||
path, err = clipGeometry(geometry, path, &clip)
|
var clipped GeometryPath
|
||||||
if err != nil {
|
var linear bool
|
||||||
return nil, Box{}, err
|
var clipErr error
|
||||||
|
if rectangle {
|
||||||
|
clipped, linear, clipErr = clipLinearFillGeometry(path, clip)
|
||||||
|
}
|
||||||
|
if clipErr != nil {
|
||||||
|
return nil, Box{}, clipErr
|
||||||
|
}
|
||||||
|
if linear {
|
||||||
|
path = clipped
|
||||||
|
} else {
|
||||||
|
path, err = clipGeometry(geometry, path, &clip)
|
||||||
|
if err != nil {
|
||||||
|
return nil, Box{}, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err := compiler.command(path, Paint{Color: color.RGBA{255, 255, 255, 255}}, evenOdd, nil, IdentityMatrix, nil); err != nil {
|
if err := compiler.command(path, Paint{Color: color.RGBA{255, 255, 255, 255}}, evenOdd, nil, IdentityMatrix, nil); err != nil {
|
||||||
|
|||||||
@@ -435,6 +435,13 @@ func (p *pdfImporter) groupSceneBounds(scene *RasterPage) (*RasterPage, Box, err
|
|||||||
// 入参: source 原始场景, box 页面裁取区域
|
// 入参: source 原始场景, box 页面裁取区域
|
||||||
// 返回: image.Image 透明图像, error 后端渲染错误
|
// 返回: image.Image 透明图像, error 后端渲染错误
|
||||||
func (p *pdfImporter) renderGroupScene(source *RasterPage, box Box) (image.Image, error) {
|
func (p *pdfImporter) renderGroupScene(source *RasterPage, box Box) (image.Image, error) {
|
||||||
|
return p.renderGroupSceneInto(source, box, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// renderGroupSceneInto 在指定区域绘制场景,按所选后端能力复用独占目标
|
||||||
|
// 入参: source 原始场景, box 页面裁取区域, target 可选目标缓冲
|
||||||
|
// 返回: image.Image 透明图像, error 后端渲染错误
|
||||||
|
func (p *pdfImporter) renderGroupSceneInto(source *RasterPage, box Box, target *image.RGBA) (image.Image, error) {
|
||||||
scene := *source
|
scene := *source
|
||||||
scene.DPI = p.rasterDPI
|
scene.DPI = p.rasterDPI
|
||||||
scene.Commands = append([]RasterCommand(nil), source.Commands...)
|
scene.Commands = append([]RasterCommand(nil), source.Commands...)
|
||||||
@@ -457,6 +464,14 @@ func (p *pdfImporter) renderGroupScene(source *RasterPage, box Box) (image.Image
|
|||||||
if backend == nil {
|
if backend == nil {
|
||||||
return nil, fmt.Errorf("PDF local compositing: %w", ErrBackendUnavailable)
|
return nil, fmt.Errorf("PDF local compositing: %w", ErrBackendUnavailable)
|
||||||
}
|
}
|
||||||
|
if target != nil {
|
||||||
|
if buffered, ok := backend.(RasterBufferBackend); ok {
|
||||||
|
if err := buffered.RenderInto(&scene, target); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return target, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
return backend.Render(&scene)
|
return backend.Render(&scene)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+23
-19
@@ -18,6 +18,7 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"net/url"
|
"net/url"
|
||||||
@@ -134,33 +135,19 @@ func (p *pdfImporter) movieAnnotation(ctx context.Context, page *pdfgo.Page, ann
|
|||||||
// 入参: ctx 取消上下文, page PDF页面, annotation 交互注解, strict 严格检查开关
|
// 入参: ctx 取消上下文, page PDF页面, annotation 交互注解, strict 严格检查开关
|
||||||
// 返回: error 外观、资源或不可等价转换错误
|
// 返回: error 外观、资源或不可等价转换错误
|
||||||
func (p *pdfImporter) interactiveAnnotation(ctx context.Context, page *pdfgo.Page, annotation pdfgo.Annotation, strict bool) error {
|
func (p *pdfImporter) interactiveAnnotation(ctx context.Context, page *pdfgo.Page, annotation pdfgo.Annotation, strict bool) error {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if strict {
|
if strict {
|
||||||
return &pdfgo.UnsupportedError{Feature: "interactive " + string(annotation.Subtype) + " conversion"}
|
return &pdfgo.UnsupportedError{Feature: "interactive " + string(annotation.Subtype) + " conversion"}
|
||||||
}
|
}
|
||||||
var actions []Action
|
var actions []Action
|
||||||
if annotation.Subtype == "3D" {
|
if annotation.Subtype == "3D" {
|
||||||
model, err := p.reader.ReadThreeD(annotation)
|
return p.threeDAnnotation(ctx, page, annotation)
|
||||||
if err != nil {
|
|
||||||
p.warning(pdfgo.Diagnostic{Page: p.page + 1, Message: "PDF 3D metadata unavailable; static appearance retained: " + err.Error()})
|
|
||||||
return p.appearanceAnnotation(ctx, page, annotation)
|
|
||||||
}
|
|
||||||
data, err := model.Stream.Decode()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
id, err := p.editor.AddAttachment(fmt.Sprintf("Model_%d.%s", p.page+1, strings.ToLower(string(model.Format))), data)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
actions = append(actions, Action{Event: "CLICK", GotoA: &GotoA{AttachID: id}})
|
|
||||||
} else {
|
} else {
|
||||||
media, err := p.reader.ReadRichMedia(ctx, annotation)
|
media, err := p.reader.ReadRichMedia(ctx, annotation)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if ctx.Err() != nil {
|
return p.interactiveAppearanceFallback(ctx, page, annotation, "metadata", err)
|
||||||
return ctx.Err()
|
|
||||||
}
|
|
||||||
p.warning(pdfgo.Diagnostic{Page: p.page + 1, Message: "PDF rich media metadata unavailable; static appearance retained: " + err.Error()})
|
|
||||||
return p.appearanceAnnotation(ctx, page, annotation)
|
|
||||||
}
|
}
|
||||||
config := media.Configurations[media.ActiveConfiguration]
|
config := media.Configurations[media.ActiveConfiguration]
|
||||||
var playing *pdfgo.FileSpecification
|
var playing *pdfgo.FileSpecification
|
||||||
@@ -245,6 +232,23 @@ func (p *pdfImporter) interactiveAnnotation(ctx context.Context, page *pdfgo.Pag
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// interactiveAppearanceFallback 保留交互注解的有效正常外观,取消时不继续转换
|
||||||
|
// 入参: ctx 取消上下文, page PDF页面, annotation 交互注解, kind 失败资源类型, cause 原始错误
|
||||||
|
// 返回: error 取消或外观转换错误
|
||||||
|
func (p *pdfImporter) interactiveAppearanceFallback(ctx context.Context, page *pdfgo.Page, annotation pdfgo.Annotation, kind string, cause error) error {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if errors.Is(cause, context.Canceled) || errors.Is(cause, context.DeadlineExceeded) {
|
||||||
|
return cause
|
||||||
|
}
|
||||||
|
if err := p.appearanceAnnotation(ctx, page, annotation); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
p.warning(pdfgo.Diagnostic{Page: p.page + 1, Message: fmt.Sprintf("PDF %s %s unavailable; normal appearance retained: %v", annotation.Subtype, kind, cause)})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// mediaData 读取媒体或附件,宽松模式缺少外部读取器时报告资源不可用
|
// mediaData 读取媒体或附件,宽松模式缺少外部读取器时报告资源不可用
|
||||||
// 入参: ctx 取消上下文, file PDF文件说明
|
// 入参: ctx 取消上下文, file PDF文件说明
|
||||||
// 返回: []byte 原始文件, bool 是否可用, error 解码、外部读取或严格模式错误
|
// 返回: []byte 原始文件, bool 是否可用, error 解码、外部读取或严格模式错误
|
||||||
|
|||||||
+1
-4
@@ -252,10 +252,7 @@ func (p *pdfImporter) text(mark pdfgo.TextMark) error {
|
|||||||
if err := p.flushPath(); err != nil {
|
if err := p.flushPath(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
visitor := pdfgo.Visitor{Path: p.path, Image: p.image, Warning: p.warning, Reference: p.referencePage}
|
visitor := p.visitor()
|
||||||
visitor.Group = func(group pdfgo.GroupMark, walk func(pdfgo.Visitor) error) error {
|
|
||||||
return p.group(group, walk, visitor)
|
|
||||||
}
|
|
||||||
for index := range mark.Glyphs {
|
for index := range mark.Glyphs {
|
||||||
if err := p.reader.WalkType3Glyph(p.ctx, mark, index, visitor); err != nil {
|
if err := p.reader.WalkType3Glyph(p.ctx, mark, index, visitor); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package ofdgo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/binary"
|
||||||
|
"math"
|
||||||
|
|
||||||
|
"github.com/tdewolff/canvas"
|
||||||
|
)
|
||||||
|
|
||||||
|
// canvasGeometryDigest 按完整坐标、路径顺序及几何容差计算摘要
|
||||||
|
// 入参: paths 非空路径序列
|
||||||
|
// 返回: [32]byte 几何摘要
|
||||||
|
func canvasGeometryDigest(paths ...*canvas.Path) [32]byte {
|
||||||
|
hash := sha256.New()
|
||||||
|
var data [512]byte
|
||||||
|
binary.LittleEndian.PutUint64(data[:8], math.Float64bits(canvas.Tolerance))
|
||||||
|
binary.LittleEndian.PutUint64(data[8:16], math.Float64bits(canvas.Epsilon))
|
||||||
|
binary.LittleEndian.PutUint64(data[16:24], math.Float64bits(canvas.BentleyOttmannEpsilon))
|
||||||
|
hash.Write(data[:24])
|
||||||
|
for _, path := range paths {
|
||||||
|
values := path.Data()
|
||||||
|
binary.LittleEndian.PutUint64(data[:8], uint64(len(values)))
|
||||||
|
hash.Write(data[:8])
|
||||||
|
for len(values) > 0 {
|
||||||
|
count := min(len(values), len(data)/8)
|
||||||
|
for i, value := range values[:count] {
|
||||||
|
binary.LittleEndian.PutUint64(data[i*8:], math.Float64bits(value))
|
||||||
|
}
|
||||||
|
hash.Write(data[:count*8])
|
||||||
|
values = values[count:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var key [32]byte
|
||||||
|
hash.Sum(key[:0])
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
|
||||||
|
// settleCanvasPath 复用复杂路径的填充规范化结果,返回独立副本
|
||||||
|
// 入参: path 页面坐标路径, rule 填充规则
|
||||||
|
// 返回: *canvas.Path 规范化填充轮廓
|
||||||
|
func (r *Renderer) settleCanvasPath(path *canvas.Path, rule canvas.FillRule) *canvas.Path {
|
||||||
|
if len(path.Data()) < 256 {
|
||||||
|
return path.Settle(rule)
|
||||||
|
}
|
||||||
|
key := canvasFillKey{digest: canvasGeometryDigest(path), rule: rule}
|
||||||
|
cache := r.canvasSettledPaths()
|
||||||
|
if result, ok := cache.get(key); ok {
|
||||||
|
return result.Copy()
|
||||||
|
}
|
||||||
|
result := path.Settle(rule)
|
||||||
|
if cost := len(result.Data())*8 + 256; cost <= cache.limit {
|
||||||
|
cache.put(key, result.Copy(), cost)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
@@ -55,6 +55,30 @@ func (b CanvasBackend) Render(page *RasterPage) (image.Image, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
img := image.NewRGBA(image.Rect(0, 0, w, h))
|
img := image.NewRGBA(image.Rect(0, 0, w, h))
|
||||||
|
return b.renderImage(page, img)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RenderInto 在调用方独占的紧密RGBA缓冲中重新绘制,不修改页面或保留缓冲
|
||||||
|
// 入参: page 只读页面, target 原点为零且尺寸匹配的目标,不与输入图像共享数据
|
||||||
|
// 返回: error 参数或绘制错误,绘制失败时目标可能包含部分结果
|
||||||
|
func (b CanvasBackend) RenderInto(page *RasterPage, target *image.RGBA) error {
|
||||||
|
w, h, err := page.PixelSize()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if target == nil || target.Rect != image.Rect(0, 0, w, h) || target.Stride != 4*w || len(target.Pix) < 4*w*h {
|
||||||
|
return fmt.Errorf("invalid canvas target buffer")
|
||||||
|
}
|
||||||
|
clear(target.Pix[:4*w*h])
|
||||||
|
_, err = b.renderImage(page, target)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// renderImage 将页面绘制到已清空且尺寸匹配的目标
|
||||||
|
// 入参: page 只读页面, img 独占目标
|
||||||
|
// 返回: image.Image 目标图像, error 绘制错误
|
||||||
|
func (b CanvasBackend) renderImage(page *RasterPage, img *image.RGBA) (image.Image, error) {
|
||||||
|
h := img.Rect.Dy()
|
||||||
space := b.ColorSpace
|
space := b.ColorSpace
|
||||||
if space == nil {
|
if space == nil {
|
||||||
space = canvas.DefaultColorSpace
|
space = canvas.DefaultColorSpace
|
||||||
|
|||||||
@@ -15,10 +15,7 @@
|
|||||||
package ofdgo
|
package ofdgo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/sha256"
|
|
||||||
"encoding/binary"
|
|
||||||
"image"
|
"image"
|
||||||
"math"
|
|
||||||
"slices"
|
"slices"
|
||||||
|
|
||||||
"github.com/tdewolff/canvas"
|
"github.com/tdewolff/canvas"
|
||||||
@@ -195,27 +192,7 @@ func (r *Renderer) intersectCachedClipPath(parent, current *canvas.Path) *canvas
|
|||||||
if parent == nil || current == nil || len(parent.Data())+len(current.Data()) < 256 {
|
if parent == nil || current == nil || len(parent.Data())+len(current.Data()) < 256 {
|
||||||
return intersectClipPath(parent, current)
|
return intersectClipPath(parent, current)
|
||||||
}
|
}
|
||||||
hash := sha256.New()
|
key := canvasGeometryDigest(parent, current)
|
||||||
var data [512]byte
|
|
||||||
binary.LittleEndian.PutUint64(data[:8], math.Float64bits(canvas.Tolerance))
|
|
||||||
binary.LittleEndian.PutUint64(data[8:16], math.Float64bits(canvas.Epsilon))
|
|
||||||
binary.LittleEndian.PutUint64(data[16:24], math.Float64bits(canvas.BentleyOttmannEpsilon))
|
|
||||||
hash.Write(data[:24])
|
|
||||||
for _, path := range []*canvas.Path{parent, current} {
|
|
||||||
values := path.Data()
|
|
||||||
binary.LittleEndian.PutUint64(data[:8], uint64(len(values)))
|
|
||||||
hash.Write(data[:8])
|
|
||||||
for len(values) > 0 {
|
|
||||||
count := min(len(values), len(data)/8)
|
|
||||||
for i, value := range values[:count] {
|
|
||||||
binary.LittleEndian.PutUint64(data[i*8:], math.Float64bits(value))
|
|
||||||
}
|
|
||||||
hash.Write(data[:count*8])
|
|
||||||
values = values[count:]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var key [32]byte
|
|
||||||
hash.Sum(key[:0])
|
|
||||||
cache := r.canvasClipIntersections()
|
cache := r.canvasClipIntersections()
|
||||||
if path, ok := cache.get(key); ok {
|
if path, ok := cache.get(key); ok {
|
||||||
return path.Copy()
|
return path.Copy()
|
||||||
@@ -310,6 +287,8 @@ func applyClipPath(path, clip *canvas.Path) *canvas.Path {
|
|||||||
if path == nil || clip == nil {
|
if path == nil || clip == nil {
|
||||||
return path
|
return path
|
||||||
}
|
}
|
||||||
|
path = reduceRepeatedNonZeroPath(path)
|
||||||
|
clip = reduceRepeatedNonZeroPath(clip)
|
||||||
if slices.Equal(path.Data(), clip.Data()) {
|
if slices.Equal(path.Data(), clip.Data()) {
|
||||||
return path
|
return path
|
||||||
}
|
}
|
||||||
@@ -340,6 +319,36 @@ func applyClipPath(path, clip *canvas.Path) *canvas.Path {
|
|||||||
return path.And(clip)
|
return path.And(clip)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// reduceRepeatedNonZeroPath 将完全相同的闭合轮廓约为一份,保持非零填充区域
|
||||||
|
// 入参: path 非零填充路径
|
||||||
|
// 返回: *canvas.Path 等价填充路径
|
||||||
|
func reduceRepeatedNonZeroPath(path *canvas.Path) *canvas.Path {
|
||||||
|
if path == nil {
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
data := path.Data()
|
||||||
|
if len(data) < 8 || data[0] != canvas.MoveToCmd {
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
end := 0
|
||||||
|
scanner := path.Scanner()
|
||||||
|
for scanner.Scan() {
|
||||||
|
if scanner.Cmd() == canvas.MoveToCmd && end != 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
end += len(scanner.Values()) + 2
|
||||||
|
}
|
||||||
|
if end == len(data) || data[end-1] != canvas.CloseCmd || len(data)%end != 0 {
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
for i := end; i < len(data); i += end {
|
||||||
|
if !slices.Equal(data[:end], data[i:i+end]) {
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return canvas.NewPathFromData(slices.Clone(data[:end]))
|
||||||
|
}
|
||||||
|
|
||||||
// applyFillClipPath 为多段描边直接填充裁剪,不将相消轮廓用于几何测量
|
// applyFillClipPath 为多段描边直接填充裁剪,不将相消轮廓用于几何测量
|
||||||
// 入参: path 非零填充描边, clip 裁剪路径
|
// 入参: path 非零填充描边, clip 裁剪路径
|
||||||
// 返回: *canvas.Path 绘制轮廓
|
// 返回: *canvas.Path 绘制轮廓
|
||||||
|
|||||||
@@ -400,6 +400,25 @@ func geometryFromCanvasPath(path *canvas.Path) *GeometryPath {
|
|||||||
return &result
|
return &result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// clipLinearFillGeometry 裁剪直线填充轮廓,保留原填充规则,不用于区域布尔运算
|
||||||
|
// 入参: path 闭合绘制轮廓, clip 凸裁剪窗口
|
||||||
|
// 返回: GeometryPath 填充轮廓, bool 是否适用, error 路径错误
|
||||||
|
func clipLinearFillGeometry(path, clip GeometryPath) (GeometryPath, bool, error) {
|
||||||
|
a, err := geometryToCanvasPath(&path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
b, err := geometryToCanvasPath(&clip)
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
result, ok := intersectLinearCanvasContours(a, b)
|
||||||
|
if !ok {
|
||||||
|
return nil, false, nil
|
||||||
|
}
|
||||||
|
return *geometryFromCanvasPath(result), true, nil
|
||||||
|
}
|
||||||
|
|
||||||
// actionCanvasMatrix 转换页面坐标矩阵,不翻转Y轴
|
// actionCanvasMatrix 转换页面坐标矩阵,不翻转Y轴
|
||||||
// 入参: matrix 页面变换
|
// 入参: matrix 页面变换
|
||||||
// 返回: canvas.Matrix 路径变换
|
// 返回: canvas.Matrix 路径变换
|
||||||
|
|||||||
@@ -253,13 +253,16 @@ func (r *Renderer) renderPath(ctx *canvas.Context, obj PathObject, pageH float64
|
|||||||
}
|
}
|
||||||
fp := paths.Merge()
|
fp := paths.Merge()
|
||||||
fillRule := canvas.NonZero
|
fillRule := canvas.NonZero
|
||||||
|
if fillClip != nil && !fp.Bounds().Overlaps(fillClip.Bounds()) {
|
||||||
|
fp = &canvas.Path{}
|
||||||
|
}
|
||||||
if obj.Rule == "Even-Odd" {
|
if obj.Rule == "Even-Odd" {
|
||||||
fillRule = canvas.EvenOdd
|
fillRule = canvas.EvenOdd
|
||||||
if bounds, ok := rectangularPath(fillClip); ok && bounds.Contains(fp.Bounds()) {
|
if bounds, ok := rectangularPath(fillClip); ok && bounds.Contains(fp.Bounds()) {
|
||||||
fillClip = nil
|
fillClip = nil
|
||||||
}
|
}
|
||||||
if fillClip != nil || style.fillPattern != nil {
|
if fillClip != nil || style.fillPattern != nil {
|
||||||
fp = fp.Settle(fillRule)
|
fp = r.settleCanvasPath(fp, fillRule)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if fillClip != nil {
|
if fillClip != nil {
|
||||||
|
|||||||
@@ -33,6 +33,15 @@ type canvasClipCacheKey struct{}
|
|||||||
// canvasPathCacheKey 区分不依赖文档资源的共享局部路径缓存
|
// canvasPathCacheKey 区分不依赖文档资源的共享局部路径缓存
|
||||||
type canvasPathCacheKey struct{}
|
type canvasPathCacheKey struct{}
|
||||||
|
|
||||||
|
// canvasFillCacheKey 区分按填充规则规范化的共享路径缓存
|
||||||
|
type canvasFillCacheKey struct{}
|
||||||
|
|
||||||
|
// canvasFillKey 保存完整几何摘要及填充规则
|
||||||
|
type canvasFillKey struct {
|
||||||
|
digest [32]byte
|
||||||
|
rule canvas.FillRule
|
||||||
|
}
|
||||||
|
|
||||||
// canvasPathKey 按完整路径文本与几何容差区分解析结果
|
// canvasPathKey 按完整路径文本与几何容差区分解析结果
|
||||||
type canvasPathKey struct {
|
type canvasPathKey struct {
|
||||||
data string
|
data string
|
||||||
@@ -78,6 +87,21 @@ func (r *Renderer) canvasParsedPaths() *renderCache[canvasPathKey, *canvas.Path]
|
|||||||
return cache
|
return cache
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// canvasSettledPaths 获取共享填充轮廓缓存,限制结果保留量
|
||||||
|
// 返回: *renderCache[canvasFillKey, *canvas.Path] 填充轮廓缓存
|
||||||
|
func (r *Renderer) canvasSettledPaths() *renderCache[canvasFillKey, *canvas.Path] {
|
||||||
|
key := canvasFillCacheKey{}
|
||||||
|
if state, ok := r.sharedBackendStates[key]; ok {
|
||||||
|
return state.(*renderCache[canvasFillKey, *canvas.Path])
|
||||||
|
}
|
||||||
|
if r.sharedBackendStates == nil {
|
||||||
|
r.sharedBackendStates = make(map[any]any)
|
||||||
|
}
|
||||||
|
cache := &renderCache[canvasFillKey, *canvas.Path]{limit: 16 << 20}
|
||||||
|
r.sharedBackendStates[key] = cache
|
||||||
|
return cache
|
||||||
|
}
|
||||||
|
|
||||||
// canvasFontFamilies 获取子渲染器共享的只读字体族缓存,限制解析数据保留量
|
// canvasFontFamilies 获取子渲染器共享的只读字体族缓存,限制解析数据保留量
|
||||||
// 返回: *renderCache[canvasFontKey, *canvas.FontFamily] 字体族缓存
|
// 返回: *renderCache[canvasFontKey, *canvas.FontFamily] 字体族缓存
|
||||||
func (r *Renderer) canvasFontFamilies() *renderCache[canvasFontKey, *canvas.FontFamily] {
|
func (r *Renderer) canvasFontFamilies() *renderCache[canvasFontKey, *canvas.FontFamily] {
|
||||||
|
|||||||
+589
@@ -0,0 +1,589 @@
|
|||||||
|
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package ofdgo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"math"
|
||||||
|
)
|
||||||
|
|
||||||
|
// u3dDefaultBytes 限制默认解码数据规模,不包括调用方已有输入及运行时开销
|
||||||
|
const u3dDefaultBytes = 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
|
||||||
|
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
|
||||||
|
View *U3DView
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 保存无纹理着色器参数,不提前应用混合、透明测试或光照
|
||||||
|
type U3DShader struct {
|
||||||
|
Name, Material string
|
||||||
|
Attributes, AlphaFunction, Blend uint32
|
||||||
|
RenderPass uint32
|
||||||
|
AlphaReference float32
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecodeU3D 按ECMA-363解码静态基础网格、组、材质、着色器和光源
|
||||||
|
// 暂不支持渐进网格、纹理和动画,遇到未支持内容返回错误而非部分场景
|
||||||
|
// 入参: 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
|
||||||
|
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.checkParents(); 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 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()
|
||||||
|
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 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:
|
||||||
|
if chain >= 0 {
|
||||||
|
return name, fmt.Errorf("invalid U3D palette resource location")
|
||||||
|
}
|
||||||
|
err = d.resource(&r, block.kind, 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")
|
||||||
|
}
|
||||||
|
if r.u32() != 1 {
|
||||||
|
return fmt.Errorf("unsupported U3D non-mesh shading modifier")
|
||||||
|
}
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
d.model.Nodes[index].Shaders = 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 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")
|
||||||
|
}
|
||||||
|
if channels != 0 {
|
||||||
|
return fmt.Errorf("unsupported U3D textured shader")
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
@@ -0,0 +1,301 @@
|
|||||||
|
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package ofdgo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"math"
|
||||||
|
"math/bits"
|
||||||
|
"unicode/utf8"
|
||||||
|
)
|
||||||
|
|
||||||
|
// u3dValues 读取U3D未压缩字段,不允许越过当前块
|
||||||
|
type u3dValues struct {
|
||||||
|
data []byte
|
||||||
|
pos int
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
// u3dBits 按ECMA-363第10章逆解16位算术区间,字节内按低位优先读取
|
||||||
|
type u3dBits struct {
|
||||||
|
data []byte
|
||||||
|
pos uint64
|
||||||
|
low, high, code uint32
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
// u3dHistogram 保存一个动态压缩上下文的频次及前缀和
|
||||||
|
type u3dHistogram struct {
|
||||||
|
counts []uint32
|
||||||
|
tree []uint32
|
||||||
|
total uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
// take 读取定长字段,失败后不继续推进位置
|
||||||
|
// 入参: size 字节数
|
||||||
|
// 返回: []byte 字段数据,失败时为空
|
||||||
|
func (r *u3dValues) take(size int) []byte {
|
||||||
|
if r.err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if size < 0 || size > len(r.data)-r.pos {
|
||||||
|
r.err = io.ErrUnexpectedEOF
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
value := r.data[r.pos : r.pos+size]
|
||||||
|
r.pos += size
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
// u8 读取一个未压缩字节
|
||||||
|
// 返回: byte 数值
|
||||||
|
func (r *u3dValues) u8() byte {
|
||||||
|
value := r.take(1)
|
||||||
|
if value == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return value[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
// u32 读取小端32位整数
|
||||||
|
// 返回: uint32 数值
|
||||||
|
func (r *u3dValues) u32() uint32 {
|
||||||
|
value := r.take(4)
|
||||||
|
if value == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return binary.LittleEndian.Uint32(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// f32 读取有限单精度数值
|
||||||
|
// 返回: float32 数值
|
||||||
|
func (r *u3dValues) f32() float32 {
|
||||||
|
value := math.Float32frombits(r.u32())
|
||||||
|
if !finite(float64(value)) && r.err == nil {
|
||||||
|
r.err = fmt.Errorf("invalid U3D floating-point value")
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
// text 读取带16位字节长度的UTF-8字符串
|
||||||
|
// 返回: string 文本
|
||||||
|
func (r *u3dValues) text() string {
|
||||||
|
length := r.take(2)
|
||||||
|
if length == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
data := r.take(int(binary.LittleEndian.Uint16(length)))
|
||||||
|
if r.err == nil && !utf8.Valid(data) {
|
||||||
|
r.err = fmt.Errorf("invalid U3D UTF-8 string")
|
||||||
|
}
|
||||||
|
return string(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// done 检查字段是否恰好覆盖当前块
|
||||||
|
// 返回: error 读取或多余字段错误
|
||||||
|
func (r *u3dValues) done() error {
|
||||||
|
if r.err != nil {
|
||||||
|
return r.err
|
||||||
|
}
|
||||||
|
if r.pos != len(r.data) {
|
||||||
|
return fmt.Errorf("unexpected U3D block data")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// newU3DBits 初始化当前块的独立算术解码状态
|
||||||
|
// 入参: data 块数据
|
||||||
|
// 返回: *u3dBits 解码器
|
||||||
|
func newU3DBits(data []byte) *u3dBits {
|
||||||
|
r := &u3dBits{data: data, high: 0xffff}
|
||||||
|
for range 16 {
|
||||||
|
r.code = r.code<<1 | r.bit()
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// bit 读取低位优先比特,区间预读最多允许16个末尾零位
|
||||||
|
// 返回: uint32 比特
|
||||||
|
func (r *u3dBits) bit() uint32 {
|
||||||
|
if r.err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
limit := uint64(len(r.data)) * 8
|
||||||
|
var value uint32
|
||||||
|
if r.pos < limit {
|
||||||
|
value = uint32(r.data[r.pos/8]>>(r.pos%8)) & 1
|
||||||
|
} else if r.pos >= limit+16 {
|
||||||
|
r.err = io.ErrUnexpectedEOF
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
r.pos++
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
// cumulative 求当前编码值在上下文累计频次中的位置
|
||||||
|
// 入参: total 总频次
|
||||||
|
// 返回: uint32 累计频次位置
|
||||||
|
func (r *u3dBits) cumulative(total uint32) uint32 {
|
||||||
|
if r.err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if total == 0 || r.code < r.low || r.code > r.high {
|
||||||
|
r.err = fmt.Errorf("invalid U3D arithmetic interval")
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return uint32((uint64(r.code-r.low+1)*uint64(total) - 1) / uint64(r.high-r.low+1))
|
||||||
|
}
|
||||||
|
|
||||||
|
// advance 收缩符号区间并执行同位及下溢归一化
|
||||||
|
// 入参: start 累计频次, count 符号频次, total 总频次
|
||||||
|
func (r *u3dBits) advance(start, count, total uint32) {
|
||||||
|
if r.err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if count == 0 || start >= total || count > total-start {
|
||||||
|
r.err = fmt.Errorf("invalid U3D symbol frequency")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
width := uint64(r.high-r.low) + 1
|
||||||
|
r.high = r.low + uint32(width*uint64(start+count)/uint64(total)) - 1
|
||||||
|
r.low += uint32(width * uint64(start) / uint64(total))
|
||||||
|
for r.err == nil {
|
||||||
|
var offset uint32
|
||||||
|
switch {
|
||||||
|
case r.high < 0x8000:
|
||||||
|
case r.low >= 0x8000:
|
||||||
|
offset = 0x8000
|
||||||
|
case r.low >= 0x4000 && r.high < 0xc000:
|
||||||
|
offset = 0x4000
|
||||||
|
default:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r.low = (r.low - offset) << 1
|
||||||
|
r.high = (r.high-offset)<<1 | 1
|
||||||
|
r.code = (r.code-offset)<<1 | r.bit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// byteValue 逆解静态256符号上下文中的反序字节
|
||||||
|
// 返回: byte 原始字节
|
||||||
|
func (r *u3dBits) byteValue() byte {
|
||||||
|
value := r.cumulative(256)
|
||||||
|
r.advance(value, 1, 256)
|
||||||
|
return bits.Reverse8(byte(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
// u32 逆解四个小端字节,不重置压缩区间
|
||||||
|
// 返回: uint32 原始整数
|
||||||
|
func (r *u3dBits) u32() uint32 {
|
||||||
|
var value uint32
|
||||||
|
for shift := range 4 {
|
||||||
|
value |= uint32(r.byteValue()) << (8 * shift)
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
// index 读取静态上下文的网格索引,大范围按标准使用未压缩整数
|
||||||
|
// 入参: count 索引范围
|
||||||
|
// 返回: uint32 索引
|
||||||
|
func (r *u3dBits) index(count uint32) uint32 {
|
||||||
|
if count > 0x3ffe {
|
||||||
|
return r.u32()
|
||||||
|
}
|
||||||
|
value := r.cumulative(count)
|
||||||
|
r.advance(value, 1, count)
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
// dynamic 读取动态上下文数值,逃逸后的字面量仍使用当前算术区间
|
||||||
|
// 入参: h 频次上下文
|
||||||
|
// 返回: uint32 原始整数
|
||||||
|
func (r *u3dBits) dynamic(h *u3dHistogram) uint32 {
|
||||||
|
if h.total == 0 {
|
||||||
|
h.counts = make([]uint32, 1)
|
||||||
|
h.tree = make([]uint32, 2)
|
||||||
|
h.add(0)
|
||||||
|
}
|
||||||
|
index := r.cumulative(h.total)
|
||||||
|
symbol, start := h.find(index)
|
||||||
|
if r.err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
r.advance(start, h.counts[symbol], h.total)
|
||||||
|
h.add(symbol)
|
||||||
|
if symbol != 0 {
|
||||||
|
return symbol - 1
|
||||||
|
}
|
||||||
|
value := r.u32()
|
||||||
|
if value < 0xffff && r.err == nil {
|
||||||
|
h.add(value + 1)
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
// find 以树状前缀和定位符号,避免按符号数量线性扫描
|
||||||
|
// 入参: position 累计频次位置
|
||||||
|
// 返回: uint32 符号, uint32 符号前缀频次
|
||||||
|
func (h *u3dHistogram) find(position uint32) (uint32, uint32) {
|
||||||
|
index, prefix := 0, uint32(0)
|
||||||
|
for step := 1 << (bits.Len(uint(len(h.tree))) - 1); step != 0; step >>= 1 {
|
||||||
|
next := index + step
|
||||||
|
if next < len(h.tree) && h.tree[next] <= position-prefix {
|
||||||
|
prefix += h.tree[next]
|
||||||
|
index = next
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return uint32(index), prefix
|
||||||
|
}
|
||||||
|
|
||||||
|
// add 更新动态频次,并按ECMA-363阈值缩减历史频次
|
||||||
|
// 入参: symbol 符号,超过16位的值不加入上下文
|
||||||
|
func (h *u3dHistogram) add(symbol uint32) {
|
||||||
|
if symbol > 0xffff {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if h.total >= 0x1fff {
|
||||||
|
for index := range h.counts {
|
||||||
|
h.counts[index] >>= 1
|
||||||
|
}
|
||||||
|
h.counts[0]++
|
||||||
|
h.rebuild()
|
||||||
|
}
|
||||||
|
if int(symbol) >= len(h.counts) {
|
||||||
|
length := min(65536, max(int(symbol)+1, len(h.counts)*2))
|
||||||
|
h.counts = append(h.counts, make([]uint32, length-len(h.counts))...)
|
||||||
|
h.rebuild()
|
||||||
|
}
|
||||||
|
h.counts[symbol]++
|
||||||
|
h.total++
|
||||||
|
for index := int(symbol) + 1; index < len(h.tree); index += index & -index {
|
||||||
|
h.tree[index]++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// rebuild 在线性时间内重建动态频次索引
|
||||||
|
func (h *u3dHistogram) rebuild() {
|
||||||
|
h.tree = make([]uint32, len(h.counts)+1)
|
||||||
|
h.total = 0
|
||||||
|
for index, count := range h.counts {
|
||||||
|
h.total += count
|
||||||
|
i := index + 1
|
||||||
|
h.tree[i] += count
|
||||||
|
if next := i + (i & -i); next < len(h.tree) {
|
||||||
|
h.tree[next] += h.tree[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,326 @@
|
|||||||
|
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package ofdgo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
)
|
||||||
|
|
||||||
|
// U3DDirectionalLight 定义无限远光源,Direction为发射方向,CameraRelative使用相机坐标
|
||||||
|
type U3DDirectionalLight struct {
|
||||||
|
Color, Direction [3]float64
|
||||||
|
CameraRelative bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// U3DLighting 替换场景灯光,空灯光列表表示无光源,DiffuseOffset加到受光材质的漫反射颜色
|
||||||
|
// 颜色及漫反射增量的各分量取值为0到1;不修改材质自发光或不受光着色器
|
||||||
|
type U3DLighting struct {
|
||||||
|
Lights []U3DDirectionalLight
|
||||||
|
DiffuseOffset [3]float64
|
||||||
|
}
|
||||||
|
|
||||||
|
// u3dRenderLight 保存世界空间的光源位置与正Z发射方向
|
||||||
|
type u3dRenderLight struct {
|
||||||
|
value U3DLight
|
||||||
|
position, direction u3dVector
|
||||||
|
}
|
||||||
|
|
||||||
|
// u3dRenderStyle 保存当前着色器的有效材质参数
|
||||||
|
type u3dRenderStyle struct {
|
||||||
|
shader U3DShader
|
||||||
|
ambient, diffuse, specular, emissive [3]float64
|
||||||
|
opacity, exponent float64
|
||||||
|
}
|
||||||
|
|
||||||
|
// collectLights 收集本轮根节点下启用的光源实例
|
||||||
|
// 返回: error 参数、预算或取消错误
|
||||||
|
func (r *u3dRender) collectLights() error {
|
||||||
|
if r.lighting != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
r.lights = r.lights[:0]
|
||||||
|
for i, instance := range r.scene {
|
||||||
|
if i%256 == 0 {
|
||||||
|
if err := r.ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
node := r.model.Nodes[instance.node]
|
||||||
|
if !r.members[i] || node.Kind != "Light" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
j, ok := r.lightIndex[node.Resource]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
light := r.model.Lights[j]
|
||||||
|
if light.Attributes&1 == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if light.Attributes > 7 || light.Kind > 3 {
|
||||||
|
return fmt.Errorf("invalid U3D light")
|
||||||
|
}
|
||||||
|
for _, v := range light.Color {
|
||||||
|
if !finite(float64(v)) {
|
||||||
|
return fmt.Errorf("invalid U3D light color")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if light.Kind != 0 && !finite(float64(light.Intensity)) {
|
||||||
|
return fmt.Errorf("invalid U3D light intensity")
|
||||||
|
}
|
||||||
|
if light.Kind >= 2 {
|
||||||
|
for _, v := range light.Attenuation {
|
||||||
|
if !finite(float64(v)) {
|
||||||
|
return fmt.Errorf("invalid U3D light attenuation")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if light.Kind == 3 && (!finite(float64(light.SpotAngle)) || light.SpotAngle <= 0 || light.SpotAngle > 180) {
|
||||||
|
return fmt.Errorf("invalid U3D spot angle")
|
||||||
|
}
|
||||||
|
world := instance.world
|
||||||
|
item := u3dRenderLight{value: light, position: u3dVector{world[12], world[13], world[14]}, direction: (u3dVector{world[8], world[9], world[10]}).unit()}
|
||||||
|
if (light.Kind == 1 || light.Kind == 3) && item.direction == (u3dVector{}) {
|
||||||
|
return fmt.Errorf("invalid U3D light direction")
|
||||||
|
}
|
||||||
|
if len(r.lights) == cap(r.lights) {
|
||||||
|
capacity := max(8, cap(r.lights)*2)
|
||||||
|
if err := r.reserve(uint64(capacity), 128); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
values := make([]u3dRenderLight, len(r.lights), capacity)
|
||||||
|
copy(values, r.lights)
|
||||||
|
r.remaining += uint64(cap(r.lights)) * 128
|
||||||
|
r.lights = values
|
||||||
|
}
|
||||||
|
r.lights = append(r.lights, item)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// overrideLighting 校验并建立替代光源,不依赖模型光源节点
|
||||||
|
// 入参: lighting 灯光覆盖,空值保留模型灯光
|
||||||
|
// 返回: error 参数、预算或取消错误
|
||||||
|
func (r *u3dRender) overrideLighting(lighting *U3DLighting) error {
|
||||||
|
if lighting == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
for _, v := range lighting.DiffuseOffset {
|
||||||
|
if !finite(v) || v < 0 || v > 1 {
|
||||||
|
return fmt.Errorf("invalid U3D diffuse offset")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := r.reserve(uint64(len(lighting.Lights)), 128); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
r.lights = make([]u3dRenderLight, len(lighting.Lights))
|
||||||
|
for i, light := range lighting.Lights {
|
||||||
|
if i%256 == 0 {
|
||||||
|
if err := r.ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for j, v := range light.Color {
|
||||||
|
if !finite(v) || v < 0 || v > 1 || !finite(light.Direction[j]) {
|
||||||
|
return fmt.Errorf("invalid U3D directional light")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
direction := u3dVector(light.Direction).unit()
|
||||||
|
if light.CameraRelative && r.camera.ToWorld != ([16]float64{}) {
|
||||||
|
m := r.camera.ToWorld
|
||||||
|
direction = u3dVector{m[0]*direction[0] + m[4]*direction[1] + m[8]*direction[2], m[1]*direction[0] + m[5]*direction[1] + m[9]*direction[2], m[2]*direction[0] + m[6]*direction[1] + m[10]*direction[2]}.unit()
|
||||||
|
}
|
||||||
|
if direction == (u3dVector{}) {
|
||||||
|
return fmt.Errorf("invalid U3D directional light direction")
|
||||||
|
}
|
||||||
|
r.lights[i] = u3dRenderLight{value: U3DLight{Attributes: 3, Kind: 1, Intensity: 1}, direction: direction}
|
||||||
|
}
|
||||||
|
r.lighting = lighting
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// style 解析着色器与材质,未启用的材质项使用固定默认值
|
||||||
|
// 入参: name 着色器名
|
||||||
|
// 返回: u3dRenderStyle 有效绘制参数, error 参数错误
|
||||||
|
func (r *u3dRender) style(name string) (u3dRenderStyle, error) {
|
||||||
|
style := u3dRenderStyle{shader: U3DShader{Blend: 0x606, AlphaFunction: 0x617, RenderPass: math.MaxUint32}, ambient: [3]float64{.2, .2, .2}, diffuse: [3]float64{.8, .8, .8}, opacity: 1}
|
||||||
|
if index, ok := r.shaders[name]; ok {
|
||||||
|
style.shader = r.model.Shaders[index]
|
||||||
|
}
|
||||||
|
shader := style.shader
|
||||||
|
if shader.Attributes > 7 || shader.Blend < 0x604 || shader.Blend > 0x607 || shader.AlphaFunction < 0x610 || shader.AlphaFunction > 0x617 || shader.Attributes&2 != 0 && !finite(float64(shader.AlphaReference)) {
|
||||||
|
return style, fmt.Errorf("invalid U3D render shader")
|
||||||
|
}
|
||||||
|
if index, ok := r.materials[shader.Material]; ok {
|
||||||
|
material := r.model.Materials[index]
|
||||||
|
if material.Attributes > 63 {
|
||||||
|
return style, fmt.Errorf("invalid U3D render material")
|
||||||
|
}
|
||||||
|
for i, item := range []struct {
|
||||||
|
source [3]float32
|
||||||
|
target *[3]float64
|
||||||
|
}{{material.Ambient, &style.ambient}, {material.Diffuse, &style.diffuse}, {material.Specular, &style.specular}, {material.Emissive, &style.emissive}} {
|
||||||
|
if material.Attributes&(1<<i) != 0 {
|
||||||
|
for j, v := range item.source {
|
||||||
|
if !finite(float64(v)) {
|
||||||
|
return style, fmt.Errorf("invalid U3D material color")
|
||||||
|
}
|
||||||
|
item.target[j] = float64(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if material.Attributes&16 != 0 {
|
||||||
|
if !finite(float64(material.Reflectivity)) {
|
||||||
|
return style, fmt.Errorf("invalid U3D reflectivity")
|
||||||
|
}
|
||||||
|
style.exponent = min(1, max(0, float64(material.Reflectivity))) * 128
|
||||||
|
}
|
||||||
|
if material.Attributes&32 != 0 {
|
||||||
|
if !finite(float64(material.Opacity)) {
|
||||||
|
return style, fmt.Errorf("invalid U3D opacity")
|
||||||
|
}
|
||||||
|
style.opacity = min(1, max(0, float64(material.Opacity)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return style, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
color := [4]float64{style.diffuse[0], style.diffuse[1], style.diffuse[2], style.opacity}
|
||||||
|
if style.shader.Attributes&4 != 0 {
|
||||||
|
if diffuse != nil {
|
||||||
|
for i, v := range diffuse {
|
||||||
|
if !finite(float64(v)) {
|
||||||
|
return color, fmt.Errorf("invalid U3D vertex color")
|
||||||
|
}
|
||||||
|
if i < 3 {
|
||||||
|
color[i] = float64(v)
|
||||||
|
style.diffuse[i] = float64(v)
|
||||||
|
style.ambient[i] = float64(v)
|
||||||
|
} else {
|
||||||
|
color[3] *= min(1, max(0, float64(v)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if specular != nil {
|
||||||
|
for i, v := range specular[:3] {
|
||||||
|
if !finite(float64(v)) {
|
||||||
|
return color, fmt.Errorf("invalid U3D vertex specular color")
|
||||||
|
}
|
||||||
|
style.specular[i] = float64(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if style.shader.Attributes&1 == 0 {
|
||||||
|
return color, nil
|
||||||
|
}
|
||||||
|
if r.lighting != nil {
|
||||||
|
for c := range 3 {
|
||||||
|
style.diffuse[c] += r.lighting.DiffuseOffset[c]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
copy(color[:3], style.emissive[:])
|
||||||
|
eye := (u3dVector{r.camera.ToWorld[12], r.camera.ToWorld[13], r.camera.ToWorld[14]}).sub(position).unit()
|
||||||
|
if !r.camera.Perspective {
|
||||||
|
eye = (u3dVector{-r.camera.ToWorld[8], -r.camera.ToWorld[9], -r.camera.ToWorld[10]}).unit()
|
||||||
|
if r.camera.ToWorld == ([16]float64{}) {
|
||||||
|
eye = u3dVector{0, 0, -1}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i, light := range r.lights {
|
||||||
|
if i%256 == 0 {
|
||||||
|
if err := r.ctx.Err(); err != nil {
|
||||||
|
return color, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
value := light.value
|
||||||
|
if value.Kind == 0 {
|
||||||
|
for c := range 3 {
|
||||||
|
color[c] += style.ambient[c] * float64(value.Color[c])
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
incoming := u3dVector{-light.direction[0], -light.direction[1], -light.direction[2]}
|
||||||
|
factor := float64(value.Intensity)
|
||||||
|
if value.Kind >= 2 {
|
||||||
|
incoming = light.position.sub(position)
|
||||||
|
distance := math.Hypot(math.Hypot(incoming[0], incoming[1]), incoming[2])
|
||||||
|
denominator := float64(value.Attenuation[0]) + float64(value.Attenuation[1])*distance + float64(value.Attenuation[2])*distance*distance
|
||||||
|
if !finite(denominator) || denominator == 0 {
|
||||||
|
return color, fmt.Errorf("invalid U3D point attenuation")
|
||||||
|
}
|
||||||
|
factor /= denominator
|
||||||
|
incoming = incoming.unit()
|
||||||
|
if value.Kind == 3 {
|
||||||
|
cosine := -incoming.dot(light.direction)
|
||||||
|
cutoff := math.Cos(float64(value.SpotAngle) * math.Pi / 360)
|
||||||
|
if cosine < cutoff {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if value.Attributes&4 != 0 && cutoff < 1 {
|
||||||
|
factor *= min(1, max(0, (cosine-cutoff)/(1-cutoff)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
diffuseAmount := max(0, normal.dot(incoming))
|
||||||
|
specularAmount := 0.0
|
||||||
|
if diffuseAmount > 0 && style.exponent > 0 && value.Attributes&2 != 0 {
|
||||||
|
reflection := u3dVector{2*diffuseAmount*normal[0] - incoming[0], 2*diffuseAmount*normal[1] - incoming[1], 2*diffuseAmount*normal[2] - incoming[2]}
|
||||||
|
specularAmount = math.Pow(max(0, reflection.dot(eye)), style.exponent)
|
||||||
|
}
|
||||||
|
for c := range 3 {
|
||||||
|
lightColor := float64(value.Color[c])
|
||||||
|
if r.lighting != nil {
|
||||||
|
lightColor = r.lighting.Lights[i].Color[c]
|
||||||
|
}
|
||||||
|
color[c] += lightColor * factor * (style.diffuse[c]*diffuseAmount + style.specular[c]*specularAmount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, v := range color {
|
||||||
|
if !finite(v) {
|
||||||
|
return color, fmt.Errorf("invalid U3D lighting result")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return color, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// u3dAlphaTest 按ECMA-363枚举比较透明度,不套用图形接口的枚举顺序
|
||||||
|
// 入参: function 比较函数, value 透明度, reference 参考值
|
||||||
|
// 返回: bool 是否通过
|
||||||
|
func u3dAlphaTest(function uint32, value, reference float64) bool {
|
||||||
|
switch function {
|
||||||
|
case 0x610:
|
||||||
|
return false
|
||||||
|
case 0x611:
|
||||||
|
return value < reference
|
||||||
|
case 0x612:
|
||||||
|
return value > reference
|
||||||
|
case 0x613:
|
||||||
|
return value == reference
|
||||||
|
case 0x614:
|
||||||
|
return value != reference
|
||||||
|
case 0x615:
|
||||||
|
return value <= reference
|
||||||
|
case 0x616:
|
||||||
|
return value >= reference
|
||||||
|
case 0x617:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
@@ -0,0 +1,231 @@
|
|||||||
|
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package ofdgo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
)
|
||||||
|
|
||||||
|
// frustumPlanes 返回保留正侧的六个相机视锥面
|
||||||
|
// 返回: [6][4]float64 近、远、左右及上下平面
|
||||||
|
func (r *u3dRender) 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 {
|
||||||
|
for i := 2; i < 6; i++ {
|
||||||
|
planes[i][2], planes[i][3] = planes[i][3], 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return planes
|
||||||
|
}
|
||||||
|
|
||||||
|
// u3dPlaneDistance 计算未归一化的平面有符号距离
|
||||||
|
// 入参: plane 平面方程, point 点坐标
|
||||||
|
// 返回: float64 有符号距离
|
||||||
|
func u3dPlaneDistance(plane [4]float64, point u3dVector) float64 {
|
||||||
|
return plane[0]*point[0] + plane[1]*point[1] + plane[2]*point[2] + plane[3]
|
||||||
|
}
|
||||||
|
|
||||||
|
// u3dClipFraction 计算异侧有限距离的交点比例,避免绝对值求和溢出
|
||||||
|
// 入参: a 起点距离, b 终点距离
|
||||||
|
// 返回: float64 插值比例
|
||||||
|
func u3dClipFraction(a, b float64) float64 {
|
||||||
|
a, b = math.Abs(a), math.Abs(b)
|
||||||
|
scale := max(a, b)
|
||||||
|
if scale == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
a, b = a/scale, b/scale
|
||||||
|
return a / (a + b)
|
||||||
|
}
|
||||||
|
|
||||||
|
// u3dInterpolate 在相机空间插值坐标与未预乘颜色
|
||||||
|
// 入参: a 起点, b 终点, t 比例
|
||||||
|
// 返回: u3dVertex 插值顶点
|
||||||
|
func u3dInterpolate(a, b u3dVertex, t float64) u3dVertex {
|
||||||
|
var result u3dVertex
|
||||||
|
for i := range result.point {
|
||||||
|
result.point[i] = a.point[i]*(1-t) + b.point[i]*t
|
||||||
|
}
|
||||||
|
for i := range result.color {
|
||||||
|
result.color[i] = a.color[i]*(1-t) + b.color[i]*t
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// sectionIntersection 绘制原三角面与指定剖切面的交线,其他剖切面仍参与裁剪
|
||||||
|
// 入参: triangle 原三角面, index 剖切面索引
|
||||||
|
// 返回: error 数值、资源或取消错误
|
||||||
|
func (r *u3dRender) sectionIntersection(triangle [3]u3dVertex, index int) error {
|
||||||
|
if err := r.ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
section := r.sections[index]
|
||||||
|
var distances [3]float64
|
||||||
|
for i, vertex := range triangle {
|
||||||
|
distances[i] = u3dPlaneDistance(section.plane, vertex.point)
|
||||||
|
if !finite(distances[i]) {
|
||||||
|
return fmt.Errorf("invalid U3D intersection distance")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if distances == ([3]float64{}) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var points [3]u3dVertex
|
||||||
|
count := 0
|
||||||
|
add := func(vertex u3dVertex) {
|
||||||
|
for i := range count {
|
||||||
|
if points[i].point == vertex.point {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if count < len(points) {
|
||||||
|
points[count], count = vertex, count+1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i, vertex := range triangle {
|
||||||
|
j := (i + 1) % 3
|
||||||
|
if distances[i] == 0 {
|
||||||
|
add(vertex)
|
||||||
|
}
|
||||||
|
if distances[i] < 0 && distances[j] > 0 || distances[i] > 0 && distances[j] < 0 {
|
||||||
|
add(u3dInterpolate(vertex, triangle[j], u3dClipFraction(distances[i], distances[j])))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if count < 2 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
a, b := points[0], points[1]
|
||||||
|
for i, other := range r.sections {
|
||||||
|
if i%256 == 0 {
|
||||||
|
if err := r.ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if i != index {
|
||||||
|
keep, err := u3dClipLine(&a, &b, other.plane)
|
||||||
|
if err != nil || !keep {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i, plane := range r.frustumPlanes() {
|
||||||
|
if i == 1 && r.camera.Far == 0 && !r.camera.ClipFar {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
keep, err := u3dClipLine(&a, &b, plane)
|
||||||
|
if err != nil || !keep {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
normal := u3dTriangleNormal([3]u3dVector{triangle[0].point, triangle[1].point, triangle[2].point})
|
||||||
|
return r.rasterSectionLine(a, b, *section.intersection, normal, triangle[0].point)
|
||||||
|
}
|
||||||
|
|
||||||
|
// u3dClipLine 将线段裁至平面正侧
|
||||||
|
// 入参: a 起点, b 终点, plane 平面方程
|
||||||
|
// 返回: bool 是否保留, error 数值错误
|
||||||
|
func u3dClipLine(a, b *u3dVertex, plane [4]float64) (bool, error) {
|
||||||
|
da, db := u3dPlaneDistance(plane, a.point), u3dPlaneDistance(plane, b.point)
|
||||||
|
if !finite(da) || !finite(db) {
|
||||||
|
return false, fmt.Errorf("invalid U3D line clip distance")
|
||||||
|
}
|
||||||
|
if da < 0 && db < 0 {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
if (da < 0) != (db < 0) {
|
||||||
|
intersection := u3dInterpolate(*a, *b, u3dClipFraction(da, db))
|
||||||
|
if da < 0 {
|
||||||
|
*a = intersection
|
||||||
|
} else {
|
||||||
|
*b = intersection
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// rasterSectionLine 绘制单像素不透明交线,透视深度使用倒数插值
|
||||||
|
// 入参: a 已裁剪起点, b 已裁剪终点, rgb 交线颜色, normal 原面法线, origin 原面上一点
|
||||||
|
// 返回: error 数值、资源或取消错误
|
||||||
|
func (r *u3dRender) rasterSectionLine(a, b u3dVertex, rgb [3]float64, normal, origin u3dVector) error {
|
||||||
|
cx := r.viewport[0] + r.viewport[2]/2 + r.camera.ShiftX
|
||||||
|
cy := r.viewport[1] + r.viewport[3]/2 + r.camera.ShiftY
|
||||||
|
planes := [4][4]float64{{r.camera.ScaleX, 0, 0, cx}, {-r.camera.ScaleX, 0, 0, float64(r.output.Rect.Dx()) - cx}, {0, -r.camera.ScaleY, 0, cy}, {0, r.camera.ScaleY, 0, float64(r.output.Rect.Dy()) - cy}}
|
||||||
|
for _, plane := range planes {
|
||||||
|
if r.camera.Perspective {
|
||||||
|
plane[2], plane[3] = plane[3], 0
|
||||||
|
}
|
||||||
|
keep, err := u3dClipLine(&a, &b, plane)
|
||||||
|
if err != nil || !keep {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var x, y, q [2]float64
|
||||||
|
for i, vertex := range [2]u3dVertex{a, b} {
|
||||||
|
q[i] = 1
|
||||||
|
if r.camera.Perspective {
|
||||||
|
if vertex.point[2] <= 0 {
|
||||||
|
return fmt.Errorf("invalid U3D line perspective depth")
|
||||||
|
}
|
||||||
|
q[i] = 1 / vertex.point[2]
|
||||||
|
}
|
||||||
|
x[i] = cx + vertex.point[0]*r.camera.ScaleX*q[i]
|
||||||
|
y[i] = cy - vertex.point[1]*r.camera.ScaleY*q[i]
|
||||||
|
if !finite(x[i]) || !finite(y[i]) || !finite(q[i]) {
|
||||||
|
return fmt.Errorf("invalid U3D projected line")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
steps := math.Ceil(max(math.Abs(x[1]-x[0]), math.Abs(y[1]-y[0])))
|
||||||
|
if !finite(steps) || steps > float64(max(r.output.Rect.Dx(), r.output.Rect.Dy()))+2 {
|
||||||
|
return fmt.Errorf("invalid U3D line span")
|
||||||
|
}
|
||||||
|
count := max(1, int(steps))
|
||||||
|
color := [4]float64{rgb[0], rgb[1], rgb[2], 1}
|
||||||
|
r.order++
|
||||||
|
for i := 0; i <= count; i++ {
|
||||||
|
if i%1024 == 0 {
|
||||||
|
if err := r.ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t := float64(i) / float64(count)
|
||||||
|
px, py := math.Floor(x[0]*(1-t)+x[1]*t), math.Floor(y[0]*(1-t)+y[1]*t)
|
||||||
|
if px < 0 || py < 0 || px >= float64(r.output.Rect.Dx()) || py >= float64(r.output.Rect.Dy()) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
depth := a.point[2]*(1-t) + b.point[2]*t
|
||||||
|
if r.camera.Perspective {
|
||||||
|
depth = 1 / (q[0]*(1-t) + q[1]*t)
|
||||||
|
}
|
||||||
|
ray := u3dVector{(px + .5 - cx) / r.camera.ScaleX, (cy - py - .5) / r.camera.ScaleY, 1}
|
||||||
|
denominator := normal[2]
|
||||||
|
numerator := normal.dot(origin) - normal[0]*ray[0] - normal[1]*ray[1]
|
||||||
|
if r.camera.Perspective {
|
||||||
|
denominator, numerator = normal.dot(ray), normal.dot(origin)
|
||||||
|
}
|
||||||
|
if sampled := numerator / denominator; finite(sampled) && sampled >= r.camera.Near && (r.camera.Far == 0 && !r.camera.ClipFar || sampled <= r.camera.Far) {
|
||||||
|
depth = sampled
|
||||||
|
}
|
||||||
|
depth -= 8 * math.Abs(depth-math.Nextafter(depth, math.Inf(-1)))
|
||||||
|
pixel := int(py)*r.output.Rect.Dx() + int(px)
|
||||||
|
if depth <= r.frame[pixel].depth {
|
||||||
|
if err := r.fragment(pixel, depth, color, 0x606); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,295 @@
|
|||||||
|
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package ofdgo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
// U3DMesh 保存完整基础网格的独立数组,索引对应原始作者网格
|
||||||
|
type U3DMesh struct {
|
||||||
|
Name string
|
||||||
|
ExcludeNormals bool
|
||||||
|
Positions, Normals [][3]float32
|
||||||
|
Diffuse, Specular, TextureCoordinates [][4]float32
|
||||||
|
Shadings []U3DShading
|
||||||
|
Faces []U3DFace
|
||||||
|
Quality [3]uint32
|
||||||
|
InverseQuantization [5]float32
|
||||||
|
NormalCrease, NormalUpdate, NormalTolerance float32
|
||||||
|
}
|
||||||
|
|
||||||
|
// U3DShading 描述各面使用的颜色及纹理坐标,纹理维数依层排列
|
||||||
|
type U3DShading struct {
|
||||||
|
Attributes uint32
|
||||||
|
TextureDimensions []uint32
|
||||||
|
OriginalID uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
// U3DFace 保存着色描述索引和依原始绕序排列的三个角点
|
||||||
|
type U3DFace struct {
|
||||||
|
Shading uint32
|
||||||
|
Corners [3]U3DCorner
|
||||||
|
}
|
||||||
|
|
||||||
|
// U3DCorner 保存各属性索引,未启用的属性不可用于访问数组
|
||||||
|
type U3DCorner struct {
|
||||||
|
Position, Normal, Diffuse, Specular uint32
|
||||||
|
Texture [8]uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
// u3dMeshDeclaration 保存基础网格声明及续块接收状态
|
||||||
|
type u3dMeshDeclaration struct {
|
||||||
|
mesh U3DMesh
|
||||||
|
counts [7]uint32
|
||||||
|
decoded bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// meshDeclaration 读取完整基础网格声明,不将渐进网格当作完整模型
|
||||||
|
// 入参: r 字段读取器, name 网格名
|
||||||
|
// 返回: error 结构、能力或预算错误
|
||||||
|
func (d *u3dDecoder) meshDeclaration(r *u3dValues, name string) error {
|
||||||
|
if _, exists := d.meshes[name]; exists {
|
||||||
|
return fmt.Errorf("duplicate U3D mesh %q", name)
|
||||||
|
}
|
||||||
|
index, attributes := r.u32(), r.u32()
|
||||||
|
if index != 0 || attributes > 1 {
|
||||||
|
return fmt.Errorf("invalid U3D mesh declaration")
|
||||||
|
}
|
||||||
|
decl := &u3dMeshDeclaration{mesh: U3DMesh{Name: name, ExcludeNormals: attributes == 1}}
|
||||||
|
for index := range decl.counts {
|
||||||
|
decl.counts[index] = r.u32()
|
||||||
|
}
|
||||||
|
count := decl.counts[6]
|
||||||
|
if uint64(count) > uint64(len(r.data)-r.pos)/12 {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
if err := d.reserve(uint64(count), 40); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
decl.mesh.Shadings = make([]U3DShading, int(count))
|
||||||
|
for index := range decl.mesh.Shadings {
|
||||||
|
shading := &decl.mesh.Shadings[index]
|
||||||
|
shading.Attributes = r.u32()
|
||||||
|
layers := r.u32()
|
||||||
|
if shading.Attributes > 3 {
|
||||||
|
return fmt.Errorf("invalid U3D shading attributes")
|
||||||
|
}
|
||||||
|
if layers > 8 {
|
||||||
|
return fmt.Errorf("unsupported U3D texture layer count")
|
||||||
|
}
|
||||||
|
if err := d.reserve(uint64(layers), 4); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
shading.TextureDimensions = make([]uint32, int(layers))
|
||||||
|
for layer := range shading.TextureDimensions {
|
||||||
|
dimension := r.u32()
|
||||||
|
if dimension < 1 || dimension > 4 {
|
||||||
|
return fmt.Errorf("invalid U3D texture coordinate dimension")
|
||||||
|
}
|
||||||
|
shading.TextureDimensions[layer] = dimension
|
||||||
|
}
|
||||||
|
shading.OriginalID = r.u32()
|
||||||
|
}
|
||||||
|
minimum, maximum := r.u32(), r.u32()
|
||||||
|
for index := range decl.mesh.Quality {
|
||||||
|
decl.mesh.Quality[index] = r.u32()
|
||||||
|
}
|
||||||
|
for index := range decl.mesh.InverseQuantization {
|
||||||
|
decl.mesh.InverseQuantization[index] = r.f32()
|
||||||
|
}
|
||||||
|
decl.mesh.NormalCrease, decl.mesh.NormalUpdate, decl.mesh.NormalTolerance = r.f32(), r.f32(), r.f32()
|
||||||
|
bones := r.u32()
|
||||||
|
if r.err != nil {
|
||||||
|
return r.err
|
||||||
|
}
|
||||||
|
if minimum > maximum || maximum != decl.counts[1] {
|
||||||
|
return fmt.Errorf("invalid U3D mesh resolution")
|
||||||
|
}
|
||||||
|
if minimum != maximum || bones != 0 {
|
||||||
|
return fmt.Errorf("unsupported U3D progressive or skeletal mesh")
|
||||||
|
}
|
||||||
|
if minimum == 0 {
|
||||||
|
for _, count := range decl.counts[:6] {
|
||||||
|
if count != 0 {
|
||||||
|
return fmt.Errorf("invalid U3D empty mesh")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
d.meshes[name] = decl
|
||||||
|
d.meshOrder = append(d.meshOrder, name)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// baseMesh 读取基础顶点与压缩面索引,保留各角点的独立属性
|
||||||
|
// 入参: r 字段读取器, name 网格名
|
||||||
|
// 返回: error 结构、能力、预算或取消错误
|
||||||
|
func (d *u3dDecoder) baseMesh(r *u3dValues, name string) error {
|
||||||
|
decl := d.meshes[name]
|
||||||
|
if decl == nil || decl.decoded || decl.counts[1] == 0 || r.u32() != 0 {
|
||||||
|
return fmt.Errorf("invalid U3D base mesh reference")
|
||||||
|
}
|
||||||
|
var counts [6]uint32
|
||||||
|
for index := range counts {
|
||||||
|
counts[index] = r.u32()
|
||||||
|
if counts[index] != decl.counts[index] {
|
||||||
|
return fmt.Errorf("U3D base mesh does not match full resolution declaration")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if r.err != nil {
|
||||||
|
return r.err
|
||||||
|
}
|
||||||
|
literalBytes := (uint64(counts[1])+uint64(counts[2]))*12 + (uint64(counts[3])+uint64(counts[4])+uint64(counts[5]))*16
|
||||||
|
if literalBytes > uint64(len(r.data)-r.pos) {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
if err := d.reserve(literalBytes, 1); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := d.reserve(uint64(counts[0]), 148); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
mesh := &decl.mesh
|
||||||
|
mesh.Positions, mesh.Normals = make([][3]float32, int(counts[1])), make([][3]float32, int(counts[2]))
|
||||||
|
mesh.Diffuse, mesh.Specular, mesh.TextureCoordinates = make([][4]float32, int(counts[3])), make([][4]float32, int(counts[4])), make([][4]float32, int(counts[5]))
|
||||||
|
for _, array := range [][][3]float32{mesh.Positions, mesh.Normals} {
|
||||||
|
for index := range array {
|
||||||
|
if index%1024 == 0 {
|
||||||
|
if r.err != nil {
|
||||||
|
return r.err
|
||||||
|
}
|
||||||
|
if err := d.ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for component := range array[index] {
|
||||||
|
array[index][component] = r.f32()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, array := range [][][4]float32{mesh.Diffuse, mesh.Specular, mesh.TextureCoordinates} {
|
||||||
|
for index := range array {
|
||||||
|
if index%1024 == 0 {
|
||||||
|
if r.err != nil {
|
||||||
|
return r.err
|
||||||
|
}
|
||||||
|
if err := d.ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for component := range array[index] {
|
||||||
|
array[index][component] = r.f32()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if r.err != nil {
|
||||||
|
return r.err
|
||||||
|
}
|
||||||
|
mesh.Faces = make([]U3DFace, int(counts[0]))
|
||||||
|
var histogram u3dHistogram
|
||||||
|
var compressed *u3dBits
|
||||||
|
if !d.plain && counts[0] != 0 {
|
||||||
|
if err := d.reserve(1, 1<<20); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
compressed = newU3DBits(r.data[r.pos:])
|
||||||
|
}
|
||||||
|
readIndex := func(count uint32) (uint32, error) {
|
||||||
|
var value uint32
|
||||||
|
if compressed == nil {
|
||||||
|
value = r.u32()
|
||||||
|
} else {
|
||||||
|
value = compressed.index(count)
|
||||||
|
if compressed.err != nil {
|
||||||
|
return 0, compressed.err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if r.err != nil {
|
||||||
|
return 0, r.err
|
||||||
|
}
|
||||||
|
if value >= count {
|
||||||
|
return 0, fmt.Errorf("invalid U3D corner index")
|
||||||
|
}
|
||||||
|
return value, nil
|
||||||
|
}
|
||||||
|
for index := range mesh.Faces {
|
||||||
|
if index%256 == 0 {
|
||||||
|
if err := d.ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
face := &mesh.Faces[index]
|
||||||
|
if compressed == nil {
|
||||||
|
face.Shading = r.u32()
|
||||||
|
} else {
|
||||||
|
face.Shading = compressed.dynamic(&histogram)
|
||||||
|
if compressed.err != nil {
|
||||||
|
return compressed.err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if r.err != nil {
|
||||||
|
return r.err
|
||||||
|
}
|
||||||
|
if uint64(face.Shading) >= uint64(len(mesh.Shadings)) {
|
||||||
|
return fmt.Errorf("invalid U3D face shading index")
|
||||||
|
}
|
||||||
|
shading := &mesh.Shadings[face.Shading]
|
||||||
|
for cornerIndex := range face.Corners {
|
||||||
|
corner := &face.Corners[cornerIndex]
|
||||||
|
var err error
|
||||||
|
if corner.Position, err = readIndex(counts[1]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !mesh.ExcludeNormals {
|
||||||
|
if corner.Normal, err = readIndex(counts[2]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if shading.Attributes&1 != 0 {
|
||||||
|
if corner.Diffuse, err = readIndex(counts[3]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if shading.Attributes&2 != 0 {
|
||||||
|
if corner.Specular, err = readIndex(counts[4]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for layer := range shading.TextureDimensions {
|
||||||
|
if corner.Texture[layer], err = readIndex(counts[5]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if compressed != nil {
|
||||||
|
if compressed.pos > uint64(len(compressed.data))*8 {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
if compressed.u32() != 0 {
|
||||||
|
return fmt.Errorf("invalid U3D compression flush")
|
||||||
|
}
|
||||||
|
if compressed.err != nil {
|
||||||
|
return compressed.err
|
||||||
|
}
|
||||||
|
if (compressed.pos-16+7)/8 != uint64(len(compressed.data)) {
|
||||||
|
return fmt.Errorf("unexpected U3D compressed block data: decoded %d bits, stored %d bytes", compressed.pos-16, len(compressed.data))
|
||||||
|
}
|
||||||
|
r.pos = len(r.data)
|
||||||
|
}
|
||||||
|
decl.decoded = true
|
||||||
|
return r.done()
|
||||||
|
}
|
||||||
@@ -0,0 +1,443 @@
|
|||||||
|
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package ofdgo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
"math"
|
||||||
|
)
|
||||||
|
|
||||||
|
// U3DCamera 定义沿正Z方向观察的相机,ScaleX和ScaleY为投影到像素的倍率
|
||||||
|
// ToWorld按列存储,全零表示单位矩阵;Far为0且ClipFar为假时不设置远裁剪面
|
||||||
|
// ShiftX和ShiftY为投影原点相对视口中心的像素偏移,向右及向下为正
|
||||||
|
type U3DCamera struct {
|
||||||
|
ToWorld [16]float64
|
||||||
|
Perspective bool
|
||||||
|
ScaleX, ScaleY, Near, Far float64
|
||||||
|
ShiftX, ShiftY float64
|
||||||
|
ClipFar bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// U3DRenderOptions 控制静态绘制,指定Camera时使用整个输出视口
|
||||||
|
// Camera为空时使用View指定的节点或首个视图节点,Instance指定其父路径实例
|
||||||
|
// Lighting为空时沿用模型灯光,非空时替换全部模型灯光
|
||||||
|
// Nodes按名称覆盖节点,重复名称使用最后一项,每次绘制均从原场景开始
|
||||||
|
// Background为空时使用白色;缓存与实例上限为0时分别使用256MiB和65536,不含输入及运行时开销
|
||||||
|
type U3DRenderOptions struct {
|
||||||
|
Width, Height int
|
||||||
|
View string
|
||||||
|
Instance int
|
||||||
|
Camera *U3DCamera
|
||||||
|
Background *[3]float64
|
||||||
|
Lighting *U3DLighting
|
||||||
|
Nodes []U3DNodeState
|
||||||
|
Sections []U3DCrossSection
|
||||||
|
MaxMemoryBytes int64
|
||||||
|
MaxInstances int
|
||||||
|
}
|
||||||
|
|
||||||
|
// u3dPixel 保存不透明底色、最近深度及透明片元链
|
||||||
|
type u3dPixel struct {
|
||||||
|
color [3]float64
|
||||||
|
depth float64
|
||||||
|
order uint64
|
||||||
|
head int32
|
||||||
|
}
|
||||||
|
|
||||||
|
// u3dFragment 保存等待按像素深度混合的透明或非覆盖片元
|
||||||
|
type u3dFragment struct {
|
||||||
|
color [4]float64
|
||||||
|
depth float64
|
||||||
|
order uint64
|
||||||
|
blend uint32
|
||||||
|
next int32
|
||||||
|
}
|
||||||
|
|
||||||
|
// u3dRender 保存一次绘制的有限预算,不修改调用方场景
|
||||||
|
type u3dRender struct {
|
||||||
|
ctx context.Context
|
||||||
|
model *U3DModel
|
||||||
|
remaining uint64
|
||||||
|
frame []u3dPixel
|
||||||
|
output *image.RGBA
|
||||||
|
viewport [4]float64
|
||||||
|
camera U3DCamera
|
||||||
|
toCamera u3dMatrix
|
||||||
|
scene []u3dInstance
|
||||||
|
byNode [][]int
|
||||||
|
members []bool
|
||||||
|
meshes, shaders, materials, lightIndex map[string]int
|
||||||
|
lights []u3dRenderLight
|
||||||
|
lighting *U3DLighting
|
||||||
|
states map[string]U3DNodeState
|
||||||
|
sections []u3dSection
|
||||||
|
sectionScratch [2][]u3dVertex
|
||||||
|
chunks [][]u3dFragment
|
||||||
|
fragments int
|
||||||
|
scratch []int
|
||||||
|
order uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
// RenderU3D 绘制静态无纹理三角面,返回不透明图像,不执行脚本或动画
|
||||||
|
// 采用逐顶点光照及逐像素深度混合,缺失资源回退为空网格、灰色材质或无光源
|
||||||
|
// 入参: ctx 取消上下文, model 场景, options 视图、输出与资源限制
|
||||||
|
// 返回: *image.RGBA 图像, error 参数、能力、预算或取消错误
|
||||||
|
func RenderU3D(ctx context.Context, model *U3DModel, options U3DRenderOptions) (*image.RGBA, error) {
|
||||||
|
r, err := newU3DRender(ctx, model, options)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
passes, err := r.selectView(options)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return r.render(options, passes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// newU3DRender 建立有预算限制的场景实例与资源索引
|
||||||
|
// 入参: ctx 取消上下文, model 场景, options 输出与资源限制
|
||||||
|
// 返回: *u3dRender 绘制状态, error 参数、场景、预算或取消错误
|
||||||
|
func newU3DRender(ctx context.Context, model *U3DModel, options U3DRenderOptions) (*u3dRender, error) {
|
||||||
|
if ctx == nil || model == nil || options.Width <= 0 || options.Height <= 0 || options.MaxMemoryBytes < 0 || options.MaxInstances < 0 || options.Instance < 0 {
|
||||||
|
return nil, fmt.Errorf("invalid U3D render options")
|
||||||
|
}
|
||||||
|
limit := options.MaxMemoryBytes
|
||||||
|
if limit == 0 {
|
||||||
|
limit = u3dDefaultBytes
|
||||||
|
}
|
||||||
|
r := &u3dRender{ctx: ctx, model: model, remaining: uint64(limit)}
|
||||||
|
if uint64(options.Width) > uint64(^uint(0)>>1)/uint64(options.Height) {
|
||||||
|
return nil, fmt.Errorf("invalid U3D render dimensions")
|
||||||
|
}
|
||||||
|
pixels := uint64(options.Width) * uint64(options.Height)
|
||||||
|
if err := r.reserve(pixels, 60); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
maxInstances := options.MaxInstances
|
||||||
|
if maxInstances == 0 {
|
||||||
|
maxInstances = 65536
|
||||||
|
}
|
||||||
|
if err := r.nodeStates(options.Nodes); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := r.instances(maxInstances); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := r.reserve(uint64(len(r.scene)), 1); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
r.members = make([]bool, len(r.scene))
|
||||||
|
if err := r.resources(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// render 在已选择的相机和轮次下生成图像
|
||||||
|
// 入参: options 输出与灯光背景, passes 绘制轮次
|
||||||
|
// 返回: *image.RGBA 图像, error 参数、绘制、预算或取消错误
|
||||||
|
func (r *u3dRender) render(options U3DRenderOptions, passes []U3DViewPass) (*image.RGBA, error) {
|
||||||
|
if r.sections == nil {
|
||||||
|
if err := r.prepareSections(options.Sections); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := r.overrideLighting(options.Lighting); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
background := [3]float64{1, 1, 1}
|
||||||
|
if options.Background != nil {
|
||||||
|
background = *options.Background
|
||||||
|
}
|
||||||
|
for i, v := range background {
|
||||||
|
if !finite(v) {
|
||||||
|
return nil, fmt.Errorf("invalid U3D background")
|
||||||
|
}
|
||||||
|
background[i] = min(1, max(0, v))
|
||||||
|
}
|
||||||
|
r.frame = make([]u3dPixel, options.Width*options.Height)
|
||||||
|
r.output = image.NewRGBA(image.Rect(0, 0, options.Width, options.Height))
|
||||||
|
for i := range r.frame {
|
||||||
|
if i%4096 == 0 {
|
||||||
|
if err := r.ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
r.frame[i].color = background
|
||||||
|
}
|
||||||
|
for passIndex, pass := range passes {
|
||||||
|
if err := r.renderPass(pass, passIndex, passIndex == len(passes)-1); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i, pixel := range r.frame {
|
||||||
|
if i%4096 == 0 {
|
||||||
|
if err := r.ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for component, v := range pixel.color {
|
||||||
|
r.output.Pix[i*4+component] = byte(math.Round(min(1, max(0, v)) * 255))
|
||||||
|
}
|
||||||
|
r.output.Pix[i*4+3] = 255
|
||||||
|
}
|
||||||
|
if err := r.ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return r.output, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// reserve 在分配绘制缓存前扣减预算并检查取消状态
|
||||||
|
// 入参: count 元素数量, size 元素预算
|
||||||
|
// 返回: error 预算或取消错误
|
||||||
|
func (r *u3dRender) reserve(count, size uint64) error {
|
||||||
|
if err := r.ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if size == 0 || count > r.remaining/size || count > uint64(^uint(0)>>1)/size {
|
||||||
|
return fmt.Errorf("U3D render memory exceeds limit")
|
||||||
|
}
|
||||||
|
r.remaining -= count * size
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// selectView 解析视图与相机覆盖,内部视口使用左上角坐标
|
||||||
|
// 入参: options 绘制选项
|
||||||
|
// 返回: []U3DViewPass 绘制轮次, error 视图或投影错误
|
||||||
|
func (r *u3dRender) selectView(options U3DRenderOptions) ([]U3DViewPass, error) {
|
||||||
|
r.viewport = [4]float64{0, 0, float64(options.Width), float64(options.Height)}
|
||||||
|
selected := -1
|
||||||
|
for i, node := range r.model.Nodes {
|
||||||
|
if i%256 == 0 {
|
||||||
|
if err := r.ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if options.View != "" && node.Name == options.View || options.View == "" && options.Camera == nil && node.Kind == "View" {
|
||||||
|
selected = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if options.View != "" && selected < 0 {
|
||||||
|
return nil, fmt.Errorf("missing U3D view %q", options.View)
|
||||||
|
}
|
||||||
|
passes := []U3DViewPass{{}}
|
||||||
|
if selected >= 0 {
|
||||||
|
node := r.model.Nodes[selected]
|
||||||
|
if node.Kind != "View" || node.View == nil {
|
||||||
|
return nil, fmt.Errorf("invalid U3D view node")
|
||||||
|
}
|
||||||
|
view := node.View
|
||||||
|
if len(view.Backdrops) != 0 || len(view.Overlays) != 0 {
|
||||||
|
return nil, fmt.Errorf("unsupported U3D textured view layers")
|
||||||
|
}
|
||||||
|
found := false
|
||||||
|
for i, resource := range r.model.Views {
|
||||||
|
if i%256 == 0 {
|
||||||
|
if err := r.ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if resource.Name == node.Resource {
|
||||||
|
if found {
|
||||||
|
return nil, fmt.Errorf("duplicate U3D view resource")
|
||||||
|
}
|
||||||
|
passes = resource.Passes
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if options.Camera == nil {
|
||||||
|
if options.Instance >= len(r.byNode[selected]) {
|
||||||
|
return nil, fmt.Errorf("missing U3D camera instance")
|
||||||
|
}
|
||||||
|
r.camera.ToWorld = r.scene[r.byNode[selected][options.Instance]].world
|
||||||
|
if view.Attributes & ^uint32(7) != 0 || view.Attributes&6 >= 4 {
|
||||||
|
return nil, fmt.Errorf("unsupported U3D view projection")
|
||||||
|
}
|
||||||
|
for i, v := range view.Viewport {
|
||||||
|
if !finite(float64(v)) {
|
||||||
|
return nil, fmt.Errorf("invalid U3D viewport")
|
||||||
|
}
|
||||||
|
r.viewport[(i+2)%4] = float64(v)
|
||||||
|
}
|
||||||
|
if view.Attributes&1 != 0 {
|
||||||
|
r.viewport[0] *= float64(options.Width)
|
||||||
|
r.viewport[1] *= float64(options.Height)
|
||||||
|
r.viewport[2] *= float64(options.Width)
|
||||||
|
r.viewport[3] *= float64(options.Height)
|
||||||
|
}
|
||||||
|
if r.viewport[2] <= 0 || r.viewport[3] <= 0 {
|
||||||
|
return nil, fmt.Errorf("invalid U3D viewport size")
|
||||||
|
}
|
||||||
|
r.camera.Near, r.camera.Far = float64(view.Near), float64(view.Far)
|
||||||
|
if r.camera.Far <= r.camera.Near {
|
||||||
|
return nil, fmt.Errorf("invalid U3D view clipping range")
|
||||||
|
}
|
||||||
|
r.camera.Perspective = view.Attributes&6 == 0
|
||||||
|
if r.camera.Perspective {
|
||||||
|
if view.Projection <= 0 || view.Projection >= 180 {
|
||||||
|
return nil, fmt.Errorf("invalid U3D perspective field of view")
|
||||||
|
}
|
||||||
|
r.camera.ScaleY = r.viewport[3] / (2 * math.Tan(float64(view.Projection)*math.Pi/360))
|
||||||
|
} else {
|
||||||
|
if view.Projection <= 0 {
|
||||||
|
return nil, fmt.Errorf("invalid U3D orthographic height")
|
||||||
|
}
|
||||||
|
r.camera.ScaleY = r.viewport[3] / float64(view.Projection)
|
||||||
|
}
|
||||||
|
r.camera.ScaleX = r.camera.ScaleY
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if options.Camera != nil {
|
||||||
|
r.camera = *options.Camera
|
||||||
|
} else if selected < 0 {
|
||||||
|
return nil, fmt.Errorf("missing U3D camera")
|
||||||
|
}
|
||||||
|
if len(passes) > 32 {
|
||||||
|
return nil, fmt.Errorf("unsupported U3D render pass count")
|
||||||
|
}
|
||||||
|
for _, v := range []float64{r.camera.ScaleX, r.camera.ScaleY, r.camera.Near, r.camera.Far, r.camera.ShiftX, r.camera.ShiftY} {
|
||||||
|
if !finite(v) {
|
||||||
|
return nil, fmt.Errorf("invalid U3D camera values")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if r.camera.ScaleX <= 0 || r.camera.ScaleY <= 0 || r.camera.Perspective && r.camera.Near <= 0 || (r.camera.ClipFar || r.camera.Far != 0) && r.camera.Far < r.camera.Near {
|
||||||
|
return nil, fmt.Errorf("invalid U3D camera projection")
|
||||||
|
}
|
||||||
|
matrix := u3dMatrix(r.camera.ToWorld)
|
||||||
|
if matrix == (u3dMatrix{}) {
|
||||||
|
matrix = u3dIdentity()
|
||||||
|
}
|
||||||
|
var ok bool
|
||||||
|
r.toCamera, ok = matrix.inverse()
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("invalid U3D camera transform")
|
||||||
|
}
|
||||||
|
return passes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// resources 建立只读资源索引,不按名称反复扫描场景
|
||||||
|
// 返回: error 重名或预算错误
|
||||||
|
func (r *u3dRender) resources() error {
|
||||||
|
count := uint64(len(r.model.Meshes)) + uint64(len(r.model.Shaders)) + uint64(len(r.model.Materials)) + uint64(len(r.model.Lights))
|
||||||
|
if err := r.reserve(count, 96); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
r.meshes = make(map[string]int, len(r.model.Meshes))
|
||||||
|
r.shaders = make(map[string]int, len(r.model.Shaders))
|
||||||
|
r.materials = make(map[string]int, len(r.model.Materials))
|
||||||
|
r.lightIndex = make(map[string]int, len(r.model.Lights))
|
||||||
|
add := func(index map[string]int, name string, i int) error {
|
||||||
|
if i%256 == 0 {
|
||||||
|
if err := r.ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _, ok := index[name]; ok {
|
||||||
|
return fmt.Errorf("duplicate U3D render resource %q", name)
|
||||||
|
}
|
||||||
|
index[name] = i
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
for i, v := range r.model.Meshes {
|
||||||
|
if err := add(r.meshes, v.Name, i); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i, v := range r.model.Shaders {
|
||||||
|
if err := add(r.shaders, v.Name, i); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i, v := range r.model.Materials {
|
||||||
|
if err := add(r.materials, v.Name, i); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i, v := range r.model.Lights {
|
||||||
|
if err := add(r.lightIndex, v.Name, i); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// renderPass 分别解析遮挡与混合,绘制轮次之间保留颜色并重置深度
|
||||||
|
// 入参: pass 轮次参数, passIndex 轮次索引, last 是否为最后一轮
|
||||||
|
// 返回: error 场景、预算或取消错误
|
||||||
|
func (r *u3dRender) renderPass(pass U3DViewPass, passIndex int, last bool) error {
|
||||||
|
if pass.Attributes > 1 || pass.FogMode > 2 {
|
||||||
|
return fmt.Errorf("invalid U3D render pass")
|
||||||
|
}
|
||||||
|
if pass.Attributes&1 != 0 {
|
||||||
|
for _, v := range append(pass.FogColor[:], pass.FogNear, pass.FogFar) {
|
||||||
|
if !finite(float64(v)) {
|
||||||
|
return fmt.Errorf("invalid U3D fog value")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if pass.FogMode == 0 && pass.FogFar <= pass.FogNear || pass.FogMode != 0 && pass.FogFar <= 0 {
|
||||||
|
return fmt.Errorf("invalid U3D fog range")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i, instance := range r.scene {
|
||||||
|
if i%256 == 0 {
|
||||||
|
if err := r.ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
r.members[i] = pass.Root == "" || r.model.Nodes[instance.node].Name == pass.Root || instance.parent >= 0 && r.members[instance.parent]
|
||||||
|
}
|
||||||
|
for i := range r.frame {
|
||||||
|
if i%4096 == 0 {
|
||||||
|
if err := r.ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
r.frame[i].depth = math.Inf(1)
|
||||||
|
r.frame[i].order = 0
|
||||||
|
r.frame[i].head = -1
|
||||||
|
}
|
||||||
|
r.fragments = 0
|
||||||
|
if err := r.collectLights(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for i, instance := range r.scene {
|
||||||
|
if err := r.ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
node := &r.model.Nodes[instance.node]
|
||||||
|
if !r.members[i] || node.Kind != "Model" || instance.modelVisibility(node) == 0 || instance.opacity == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if node.Visibility > 3 {
|
||||||
|
return fmt.Errorf("invalid U3D model visibility")
|
||||||
|
}
|
||||||
|
mesh, ok := r.meshes[node.Resource]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := r.drawMesh(instance, node, &r.model.Meshes[mesh], pass, passIndex); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if last {
|
||||||
|
if err := r.drawSectionPlanes(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return r.resolveFragments()
|
||||||
|
}
|
||||||
@@ -0,0 +1,303 @@
|
|||||||
|
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package ofdgo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
)
|
||||||
|
|
||||||
|
// u3dVector 保存计算过程中的三维双精度坐标
|
||||||
|
type u3dVector [3]float64
|
||||||
|
|
||||||
|
// u3dMatrix 保存按列排列的仿射矩阵
|
||||||
|
type u3dMatrix [16]float64
|
||||||
|
|
||||||
|
// u3dInstance 保存一个父路径产生的世界实例,parent为负值时连接默认根
|
||||||
|
type u3dInstance struct {
|
||||||
|
node, parent int
|
||||||
|
world u3dMatrix
|
||||||
|
opacity float64
|
||||||
|
visibility int8
|
||||||
|
}
|
||||||
|
|
||||||
|
// u3dIdentity 创建单位矩阵
|
||||||
|
// 返回: u3dMatrix 单位矩阵
|
||||||
|
func u3dIdentity() u3dMatrix { return u3dMatrix{1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1} }
|
||||||
|
|
||||||
|
// multiply 连接子节点到父节点的变换
|
||||||
|
// 入参: other 子变换
|
||||||
|
// 返回: u3dMatrix 组合变换
|
||||||
|
func (m u3dMatrix) multiply(other u3dMatrix) u3dMatrix {
|
||||||
|
var result u3dMatrix
|
||||||
|
for column := range 4 {
|
||||||
|
for row := range 4 {
|
||||||
|
for k := range 4 {
|
||||||
|
result[column*4+row] += m[k*4+row] * other[column*4+k]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// valid 判断矩阵是否为有限仿射变换
|
||||||
|
// 返回: bool 是否可用于静态绘制
|
||||||
|
func (m u3dMatrix) valid() bool {
|
||||||
|
for _, v := range m {
|
||||||
|
if !finite(v) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return m[3] == 0 && m[7] == 0 && m[11] == 0 && m[15] == 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// point 将点从局部坐标映射到目标坐标
|
||||||
|
// 入参: p 局部点
|
||||||
|
// 返回: u3dVector 目标点
|
||||||
|
func (m u3dMatrix) point(p u3dVector) u3dVector {
|
||||||
|
return u3dVector{m[0]*p[0] + m[4]*p[1] + m[8]*p[2] + m[12], m[1]*p[0] + m[5]*p[1] + m[9]*p[2] + m[13], m[2]*p[0] + m[6]*p[1] + m[10]*p[2] + m[14]}
|
||||||
|
}
|
||||||
|
|
||||||
|
// inverse 通过带主元消元计算仿射逆矩阵
|
||||||
|
// 返回: u3dMatrix 逆矩阵, bool 是否存在有限逆矩阵
|
||||||
|
func (m u3dMatrix) inverse() (u3dMatrix, bool) {
|
||||||
|
if !m.valid() {
|
||||||
|
return u3dMatrix{}, false
|
||||||
|
}
|
||||||
|
var rows [3][6]float64
|
||||||
|
for i := range 3 {
|
||||||
|
for j := range 3 {
|
||||||
|
rows[i][j] = m[j*4+i]
|
||||||
|
}
|
||||||
|
rows[i][i+3] = 1
|
||||||
|
}
|
||||||
|
for column := range 3 {
|
||||||
|
pivot := column
|
||||||
|
for row := column + 1; row < 3; row++ {
|
||||||
|
if math.Abs(rows[row][column]) > math.Abs(rows[pivot][column]) {
|
||||||
|
pivot = row
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if rows[pivot][column] == 0 {
|
||||||
|
return u3dMatrix{}, false
|
||||||
|
}
|
||||||
|
rows[pivot], rows[column] = rows[column], rows[pivot]
|
||||||
|
divisor := rows[column][column]
|
||||||
|
for j := range 6 {
|
||||||
|
rows[column][j] /= divisor
|
||||||
|
}
|
||||||
|
for row := range 3 {
|
||||||
|
if row != column {
|
||||||
|
factor := rows[row][column]
|
||||||
|
for j := range 6 {
|
||||||
|
rows[row][j] -= factor * rows[column][j]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result := u3dIdentity()
|
||||||
|
for row := range 3 {
|
||||||
|
for col := range 3 {
|
||||||
|
result[col*4+row] = rows[row][col+3]
|
||||||
|
}
|
||||||
|
result[12+row] = -(result[row]*m[12] + result[4+row]*m[13] + result[8+row]*m[14])
|
||||||
|
}
|
||||||
|
return result, result.valid()
|
||||||
|
}
|
||||||
|
|
||||||
|
// normal 以逆矩阵的转置变换并归一化法线
|
||||||
|
// 入参: n 局部法线,接收者为局部变换的逆矩阵
|
||||||
|
// 返回: u3dVector 目标法线
|
||||||
|
func (m u3dMatrix) normal(n u3dVector) u3dVector {
|
||||||
|
return (u3dVector{m[0]*n[0] + m[1]*n[1] + m[2]*n[2], m[4]*n[0] + m[5]*n[1] + m[6]*n[2], m[8]*n[0] + m[9]*n[1] + m[10]*n[2]}).unit()
|
||||||
|
}
|
||||||
|
|
||||||
|
// unit 归一化向量,零向量保持为零
|
||||||
|
// 返回: u3dVector 单位向量
|
||||||
|
func (v u3dVector) unit() u3dVector {
|
||||||
|
scale := max(math.Abs(v[0]), math.Abs(v[1]), math.Abs(v[2]))
|
||||||
|
if scale == 0 || !finite(scale) {
|
||||||
|
return u3dVector{}
|
||||||
|
}
|
||||||
|
for i := range v {
|
||||||
|
v[i] /= scale
|
||||||
|
}
|
||||||
|
length := math.Sqrt(v.dot(v))
|
||||||
|
return u3dVector{v[0] / length, v[1] / length, v[2] / length}
|
||||||
|
}
|
||||||
|
|
||||||
|
// sub 求向量差
|
||||||
|
// 入参: other 被减向量
|
||||||
|
// 返回: u3dVector 差向量
|
||||||
|
func (v u3dVector) sub(other u3dVector) u3dVector {
|
||||||
|
return u3dVector{v[0] - other[0], v[1] - other[1], v[2] - other[2]}
|
||||||
|
}
|
||||||
|
|
||||||
|
// dot 求向量内积
|
||||||
|
// 入参: other 另一向量
|
||||||
|
// 返回: float64 内积
|
||||||
|
func (v u3dVector) dot(other u3dVector) float64 { return v[0]*other[0] + v[1]*other[1] + v[2]*other[2] }
|
||||||
|
|
||||||
|
// cross 求向量叉积
|
||||||
|
// 入参: other 另一向量
|
||||||
|
// 返回: u3dVector 叉积
|
||||||
|
func (v u3dVector) cross(other u3dVector) u3dVector {
|
||||||
|
return u3dVector{v[1]*other[2] - v[2]*other[1], v[2]*other[0] - v[0]*other[2], v[0]*other[1] - v[1]*other[0]}
|
||||||
|
}
|
||||||
|
|
||||||
|
// u3dTriangleNormal 选取夹角更稳定的顶点计算法线,避免长瘦三角面的相消
|
||||||
|
// 入参: points 按绕序排列的顶点
|
||||||
|
// 返回: u3dVector 单位法线,退化三角面返回零向量
|
||||||
|
func u3dTriangleNormal(points [3]u3dVector) u3dVector {
|
||||||
|
var normal u3dVector
|
||||||
|
score := 0.0
|
||||||
|
for i := range 3 {
|
||||||
|
a := points[(i+1)%3].sub(points[i]).unit()
|
||||||
|
b := points[(i+2)%3].sub(points[i]).unit()
|
||||||
|
candidate := a.cross(b)
|
||||||
|
if length := max(math.Abs(candidate[0]), math.Abs(candidate[1]), math.Abs(candidate[2])); length > score {
|
||||||
|
normal, score = candidate, length
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return normal.unit()
|
||||||
|
}
|
||||||
|
|
||||||
|
// instances 按拓扑顺序构建全部父路径实例,限制共享层级的指数展开
|
||||||
|
// 入参: limit 最大实例数
|
||||||
|
// 返回: error 结构、预算、实例上限或取消错误
|
||||||
|
func (r *u3dRender) instances(limit int) error {
|
||||||
|
nodes := r.model.Nodes
|
||||||
|
if err := r.reserve(uint64(len(nodes)), 160); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
index := make(map[string]int, len(nodes))
|
||||||
|
for i, node := range nodes {
|
||||||
|
if i%256 == 0 {
|
||||||
|
if err := r.ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _, exists := index[node.Name]; exists || node.Name == "" {
|
||||||
|
return fmt.Errorf("invalid U3D render node name")
|
||||||
|
}
|
||||||
|
switch node.Kind {
|
||||||
|
case "Group", "Model", "Light", "View":
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported U3D render node kind")
|
||||||
|
}
|
||||||
|
index[node.Name] = i
|
||||||
|
}
|
||||||
|
counts := make([]int, len(nodes))
|
||||||
|
children := make([][]int, len(nodes))
|
||||||
|
r.byNode = make([][]int, len(nodes))
|
||||||
|
for i, node := range nodes {
|
||||||
|
if err := r.reserve(uint64(len(node.Parents)), 16); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for k, parent := range node.Parents {
|
||||||
|
if k%256 == 0 {
|
||||||
|
if err := r.ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if j, ok := index[parent.Name]; ok {
|
||||||
|
counts[i]++
|
||||||
|
children[j] = append(children[j], i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
queue := make([]int, 0, len(nodes))
|
||||||
|
for i, n := range counts {
|
||||||
|
if i%256 == 0 {
|
||||||
|
if err := r.ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if n == 0 {
|
||||||
|
queue = append(queue, i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for cursor := 0; cursor < len(queue); cursor++ {
|
||||||
|
if err := r.ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
i := queue[cursor]
|
||||||
|
state := r.states[nodes[i].Name]
|
||||||
|
for parentIndex, parent := range nodes[i].Parents {
|
||||||
|
if parentIndex%256 == 0 {
|
||||||
|
if err := r.ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var local u3dMatrix
|
||||||
|
for k, v := range parent.Transform {
|
||||||
|
local[k] = float64(v)
|
||||||
|
}
|
||||||
|
if state.Matrix != nil {
|
||||||
|
local = *state.Matrix
|
||||||
|
}
|
||||||
|
if !local.valid() {
|
||||||
|
return fmt.Errorf("unsupported U3D non-affine node transform")
|
||||||
|
}
|
||||||
|
parents := []int{-1}
|
||||||
|
if j, ok := index[parent.Name]; ok {
|
||||||
|
parents = r.byNode[j]
|
||||||
|
}
|
||||||
|
for _, p := range parents {
|
||||||
|
if len(r.scene) >= limit {
|
||||||
|
return fmt.Errorf("U3D instance count exceeds limit")
|
||||||
|
}
|
||||||
|
if err := r.reserve(1, 384); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
world := local
|
||||||
|
opacity, visibility := -1.0, int8(-1)
|
||||||
|
if p >= 0 {
|
||||||
|
world = r.scene[p].world.multiply(local)
|
||||||
|
opacity, visibility = r.scene[p].opacity, r.scene[p].visibility
|
||||||
|
}
|
||||||
|
if state.Opacity != nil {
|
||||||
|
opacity = *state.Opacity
|
||||||
|
}
|
||||||
|
if state.Visible != nil {
|
||||||
|
visibility = 0
|
||||||
|
if *state.Visible {
|
||||||
|
visibility = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !world.valid() {
|
||||||
|
return fmt.Errorf("invalid U3D world transform")
|
||||||
|
}
|
||||||
|
r.byNode[i] = append(r.byNode[i], len(r.scene))
|
||||||
|
r.scene = append(r.scene, u3dInstance{node: i, parent: p, world: world, opacity: opacity, visibility: visibility})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for childIndex, child := range children[i] {
|
||||||
|
if childIndex%256 == 0 {
|
||||||
|
if err := r.ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
counts[child]--
|
||||||
|
if counts[child] == 0 {
|
||||||
|
queue = append(queue, child)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(queue) != len(nodes) {
|
||||||
|
return fmt.Errorf("cyclic U3D node parents")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,280 @@
|
|||||||
|
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package ofdgo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
)
|
||||||
|
|
||||||
|
// U3DCrossSection 定义世界坐标中的剖切面,AxisU与AxisV须为非零正交方向
|
||||||
|
// 两轴叉积指向被裁去的一侧;平面方形边长取静态场景包围盒对角线
|
||||||
|
// Opacity为0时不显示平面,Intersection为空时不显示交线,颜色分量范围为0到1
|
||||||
|
type U3DCrossSection struct {
|
||||||
|
Center, AxisU, AxisV [3]float64
|
||||||
|
Opacity float64
|
||||||
|
Color [3]float64
|
||||||
|
Intersection *[3]float64
|
||||||
|
}
|
||||||
|
|
||||||
|
// u3dSection 保存相机空间裁剪方程、平面四角与交线颜色
|
||||||
|
type u3dSection struct {
|
||||||
|
plane [4]float64
|
||||||
|
corners [4]u3dVertex
|
||||||
|
intersection *[3]float64
|
||||||
|
opacity float64
|
||||||
|
}
|
||||||
|
|
||||||
|
// prepareSections 准备剖切缓存,不复制网格或修改调用方参数
|
||||||
|
// 入参: sections 世界坐标剖切面
|
||||||
|
// 返回: error 参数、场景、预算或取消错误
|
||||||
|
func (r *u3dRender) prepareSections(sections []U3DCrossSection) error {
|
||||||
|
if len(sections) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := r.reserve(uint64(len(sections)), 352); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := r.reserve(uint64(len(sections))+3, 112); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
r.sections = make([]u3dSection, len(sections))
|
||||||
|
for i := range r.sectionScratch {
|
||||||
|
r.sectionScratch[i] = make([]u3dVertex, len(sections)+3)
|
||||||
|
}
|
||||||
|
visible := false
|
||||||
|
for i, section := range sections {
|
||||||
|
if i%256 == 0 {
|
||||||
|
if err := r.ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !finite(section.Opacity) || section.Opacity < 0 || section.Opacity > 1 {
|
||||||
|
return fmt.Errorf("invalid U3D section opacity")
|
||||||
|
}
|
||||||
|
visible = visible || section.Opacity > 0
|
||||||
|
}
|
||||||
|
diagonal := 0.0
|
||||||
|
if visible {
|
||||||
|
var err error
|
||||||
|
diagonal, err = r.sceneDiagonal()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
world := u3dMatrix(r.camera.ToWorld)
|
||||||
|
if world == (u3dMatrix{}) {
|
||||||
|
world = u3dIdentity()
|
||||||
|
}
|
||||||
|
for i, section := range sections {
|
||||||
|
if err := r.ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, vector := range [][3]float64{section.Center, section.AxisU, section.AxisV} {
|
||||||
|
for _, value := range vector {
|
||||||
|
if !finite(value) {
|
||||||
|
return fmt.Errorf("invalid U3D section coordinates")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, color := range []*[3]float64{§ion.Color, section.Intersection} {
|
||||||
|
if color != nil {
|
||||||
|
for _, v := range color {
|
||||||
|
if !finite(v) || v < 0 || v > 1 {
|
||||||
|
return fmt.Errorf("invalid U3D section color")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
u, v := u3dVector(section.AxisU).unit(), u3dVector(section.AxisV).unit()
|
||||||
|
if u == (u3dVector{}) || v == (u3dVector{}) || math.Abs(u.dot(v)) > 1e-12 {
|
||||||
|
return fmt.Errorf("invalid U3D section axes")
|
||||||
|
}
|
||||||
|
normal := u.cross(v).unit()
|
||||||
|
prepared := &r.sections[i]
|
||||||
|
for j := range 3 {
|
||||||
|
prepared.plane[j] = -(normal[0]*world[j*4] + normal[1]*world[j*4+1] + normal[2]*world[j*4+2])
|
||||||
|
}
|
||||||
|
prepared.plane[3] = normal.dot(u3dVector(section.Center).sub(u3dVector{world[12], world[13], world[14]}))
|
||||||
|
for _, coefficient := range prepared.plane {
|
||||||
|
if !finite(coefficient) {
|
||||||
|
return fmt.Errorf("invalid U3D section plane")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
prepared.intersection, prepared.opacity = section.Intersection, section.Opacity
|
||||||
|
for j, signs := range [4][2]float64{{-1, -1}, {1, -1}, {1, 1}, {-1, 1}} {
|
||||||
|
point := u3dVector(section.Center)
|
||||||
|
for k := range point {
|
||||||
|
point[k] += diagonal / 2 * (signs[0]*u[k] + signs[1]*v[k])
|
||||||
|
}
|
||||||
|
prepared.corners[j] = u3dVertex{point: r.toCamera.point(point), color: [4]float64{section.Color[0], section.Color[1], section.Color[2], section.Opacity}}
|
||||||
|
for _, coordinate := range prepared.corners[j].point {
|
||||||
|
if !finite(coordinate) {
|
||||||
|
return fmt.Errorf("invalid U3D section corner")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// sceneDiagonal 计算静态场景世界包围盒的对角线,包含隐藏节点的几何
|
||||||
|
// 返回: float64 对角线长度, error 几何、数值或取消错误
|
||||||
|
func (r *u3dRender) 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 {
|
||||||
|
if err := r.ctx.Err(); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
node := r.model.Nodes[instance.node]
|
||||||
|
index, exists := r.meshes[node.Resource]
|
||||||
|
if node.Kind != "Model" || !exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
mesh := &r.model.Meshes[index]
|
||||||
|
for i, face := range mesh.Faces {
|
||||||
|
if i%256 == 0 {
|
||||||
|
if err := r.ctx.Err(); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, corner := range face.Corners {
|
||||||
|
if uint64(corner.Position) >= uint64(len(mesh.Positions)) {
|
||||||
|
return 0, fmt.Errorf("invalid U3D position index")
|
||||||
|
}
|
||||||
|
p := mesh.Positions[corner.Position]
|
||||||
|
world := instance.world.point(u3dVector{float64(p[0]), float64(p[1]), float64(p[2])})
|
||||||
|
for j, v := range world {
|
||||||
|
if !finite(v) {
|
||||||
|
return 0, fmt.Errorf("invalid U3D scene bounds")
|
||||||
|
}
|
||||||
|
minimum[j], maximum[j] = min(minimum[j], v), max(maximum[j], v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if math.IsInf(minimum[0], 1) {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
delta := maximum.sub(minimum)
|
||||||
|
diagonal := math.Hypot(math.Hypot(delta[0], delta[1]), delta[2])
|
||||||
|
if !finite(diagonal) {
|
||||||
|
return 0, fmt.Errorf("invalid U3D scene diagonal")
|
||||||
|
}
|
||||||
|
return diagonal, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// sectionTriangle 用复用的凸多边形缓存应用全部剖切面,再进行视锥裁剪
|
||||||
|
// 入参: triangle 三角面, shader 着色器, pass 绘制轮次
|
||||||
|
// 返回: error 数值、资源或取消错误
|
||||||
|
func (r *u3dRender) sectionTriangle(triangle [3]u3dVertex, shader U3DShader, pass U3DViewPass) error {
|
||||||
|
if len(r.sections) == 0 {
|
||||||
|
return r.clipTriangle(triangle, shader, pass)
|
||||||
|
}
|
||||||
|
copy(r.sectionScratch[0], triangle[:])
|
||||||
|
size, current := 3, 0
|
||||||
|
for _, section := range r.sections {
|
||||||
|
if err := r.ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
next, count := 1-current, 0
|
||||||
|
previous := r.sectionScratch[current][size-1]
|
||||||
|
previousDistance := u3dPlaneDistance(section.plane, previous.point)
|
||||||
|
for i, vertex := range r.sectionScratch[current][:size] {
|
||||||
|
if i%256 == 0 {
|
||||||
|
if err := r.ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
distance := u3dPlaneDistance(section.plane, vertex.point)
|
||||||
|
if !finite(distance) || !finite(previousDistance) {
|
||||||
|
return fmt.Errorf("invalid U3D section distance")
|
||||||
|
}
|
||||||
|
if (distance >= 0) != (previousDistance >= 0) {
|
||||||
|
var err error
|
||||||
|
count, err = u3dAppendSectionVertex(r.sectionScratch[next], count, u3dInterpolate(previous, vertex, u3dClipFraction(previousDistance, distance)))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if distance >= 0 {
|
||||||
|
var err error
|
||||||
|
count, err = u3dAppendSectionVertex(r.sectionScratch[next], count, vertex)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
previous, previousDistance = vertex, distance
|
||||||
|
}
|
||||||
|
if count > 1 && r.sectionScratch[next][0].point == r.sectionScratch[next][count-1].point {
|
||||||
|
count--
|
||||||
|
}
|
||||||
|
size, current = count, next
|
||||||
|
if size < 3 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if size < 2 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
for i := 1; i+1 < size; i++ {
|
||||||
|
if err := r.clipTriangle([3]u3dVertex{r.sectionScratch[current][0], r.sectionScratch[current][i], r.sectionScratch[current][i+1]}, shader, pass); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i, section := range r.sections {
|
||||||
|
if section.intersection != nil {
|
||||||
|
if err := r.sectionIntersection(triangle, i); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// u3dAppendSectionVertex 合并连续重复交点,维持凸多边形缓存上限
|
||||||
|
// 入参: buffer 固定缓存, count 已有顶点数, vertex 新顶点
|
||||||
|
// 返回: int 顶点数, error 缓存越界错误
|
||||||
|
func u3dAppendSectionVertex(buffer []u3dVertex, count int, vertex u3dVertex) (int, error) {
|
||||||
|
if count > 0 && buffer[count-1].point == vertex.point {
|
||||||
|
return count, nil
|
||||||
|
}
|
||||||
|
if count >= len(buffer) {
|
||||||
|
return count, fmt.Errorf("invalid U3D section polygon")
|
||||||
|
}
|
||||||
|
buffer[count] = vertex
|
||||||
|
return count + 1, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// drawSectionPlanes 在最后一轮绘制平面方形,不把平面本身当作待剖切几何
|
||||||
|
// 返回: error 数值、资源或取消错误
|
||||||
|
func (r *u3dRender) drawSectionPlanes() error {
|
||||||
|
for _, section := range r.sections {
|
||||||
|
if err := r.ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if section.opacity == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for i := 1; i < 3; i++ {
|
||||||
|
r.order++
|
||||||
|
if err := r.clipTriangle([3]u3dVertex{section.corners[0], section.corners[i], section.corners[i+1]}, U3DShader{Blend: 0x606}, U3DViewPass{}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package ofdgo
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
// U3DNodeState 按名称覆盖节点,未指定透明度或可见性时沿父路径继承
|
||||||
|
// Opacity范围为0到1,采用标准透明混合;Matrix为相对父节点的列式仿射矩阵
|
||||||
|
// 未匹配的名称不影响场景,矩阵为空时保留每条父路径的原始变换
|
||||||
|
type U3DNodeState struct {
|
||||||
|
Name string
|
||||||
|
Opacity *float64
|
||||||
|
Visible *bool
|
||||||
|
Matrix *[16]float64
|
||||||
|
}
|
||||||
|
|
||||||
|
// nodeStates 建立有预算限制的覆盖索引,重复名称整体替换而不合并字段
|
||||||
|
// 入参: states 节点覆盖
|
||||||
|
// 返回: error 数值、预算或取消错误
|
||||||
|
func (r *u3dRender) nodeStates(states []U3DNodeState) error {
|
||||||
|
if len(states) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := r.reserve(uint64(len(states)), 128); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
r.states = make(map[string]U3DNodeState, len(states))
|
||||||
|
for i, state := range states {
|
||||||
|
if i%256 == 0 {
|
||||||
|
if err := r.ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
r.states[state.Name] = state
|
||||||
|
}
|
||||||
|
for _, state := range r.states {
|
||||||
|
if err := r.ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if state.Opacity != nil && (!finite(*state.Opacity) || *state.Opacity < 0 || *state.Opacity > 1) {
|
||||||
|
return fmt.Errorf("invalid U3D node opacity")
|
||||||
|
}
|
||||||
|
if state.Matrix != nil && !u3dMatrix(*state.Matrix).valid() {
|
||||||
|
return fmt.Errorf("invalid U3D node matrix")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// modelVisibility 在节点覆盖生效时恢复隐藏模型,保留原有正反面选择
|
||||||
|
// 入参: node 模型节点
|
||||||
|
// 返回: uint32 可见面标志
|
||||||
|
func (instance u3dInstance) modelVisibility(node *U3DNode) uint32 {
|
||||||
|
if instance.visibility == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if instance.visibility == 1 && node.Visibility == 0 {
|
||||||
|
return 3
|
||||||
|
}
|
||||||
|
return node.Visibility
|
||||||
|
}
|
||||||
@@ -0,0 +1,464 @@
|
|||||||
|
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package ofdgo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"slices"
|
||||||
|
)
|
||||||
|
|
||||||
|
// u3dVertex 保存裁剪前后的相机坐标及未预乘颜色
|
||||||
|
type u3dVertex struct {
|
||||||
|
point u3dVector
|
||||||
|
color [4]float64
|
||||||
|
}
|
||||||
|
|
||||||
|
// drawMesh 按实例复用顶点变换,着色列表按指定顺序绘制
|
||||||
|
// 入参: instance 场景实例, node 模型节点, mesh 网格, pass 绘制轮次, passIndex 轮次索引
|
||||||
|
// 返回: error 几何、资源或取消错误
|
||||||
|
func (r *u3dRender) drawMesh(instance u3dInstance, node *U3DNode, mesh *U3DMesh, pass U3DViewPass, passIndex int) error {
|
||||||
|
count := uint64(len(mesh.Positions)) + uint64(len(mesh.Normals))
|
||||||
|
if err := r.reserve(count, 24); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer func() { r.remaining += count * 24 }()
|
||||||
|
positions := make([]u3dVector, len(mesh.Positions))
|
||||||
|
normals := make([]u3dVector, len(mesh.Normals))
|
||||||
|
inverse, invertible := instance.world.inverse()
|
||||||
|
for i, p := range mesh.Positions {
|
||||||
|
if i%1024 == 0 {
|
||||||
|
if err := r.ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
positions[i] = instance.world.point(u3dVector{float64(p[0]), float64(p[1]), float64(p[2])})
|
||||||
|
for _, v := range positions[i] {
|
||||||
|
if !finite(v) {
|
||||||
|
return fmt.Errorf("invalid U3D transformed position")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i, n := range mesh.Normals {
|
||||||
|
if i%1024 == 0 {
|
||||||
|
if err := r.ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, v := range n {
|
||||||
|
if !finite(float64(v)) {
|
||||||
|
return fmt.Errorf("invalid U3D normal")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if invertible {
|
||||||
|
normals[i] = inverse.normal(u3dVector{float64(n[0]), float64(n[1]), float64(n[2])})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for index, face := range mesh.Faces {
|
||||||
|
if index%128 == 0 {
|
||||||
|
if err := r.ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if uint64(face.Shading) >= uint64(len(mesh.Shadings)) {
|
||||||
|
return fmt.Errorf("invalid U3D render shading index")
|
||||||
|
}
|
||||||
|
shading := mesh.Shadings[face.Shading]
|
||||||
|
if shading.Attributes > 3 || len(shading.TextureDimensions) > 8 {
|
||||||
|
return fmt.Errorf("invalid U3D render shading")
|
||||||
|
}
|
||||||
|
var world [3]u3dVector
|
||||||
|
var vertices [3]u3dVertex
|
||||||
|
for i, corner := range face.Corners {
|
||||||
|
if uint64(corner.Position) >= uint64(len(positions)) || !mesh.ExcludeNormals && uint64(corner.Normal) >= uint64(len(normals)) || shading.Attributes&1 != 0 && uint64(corner.Diffuse) >= uint64(len(mesh.Diffuse)) || shading.Attributes&2 != 0 && uint64(corner.Specular) >= uint64(len(mesh.Specular)) {
|
||||||
|
return fmt.Errorf("invalid U3D render corner index")
|
||||||
|
}
|
||||||
|
world[i] = positions[corner.Position]
|
||||||
|
vertices[i].point = r.toCamera.point(world[i])
|
||||||
|
for _, v := range vertices[i].point {
|
||||||
|
if !finite(v) {
|
||||||
|
return fmt.Errorf("invalid U3D camera position")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var faceNormal u3dVector
|
||||||
|
if mesh.ExcludeNormals || !invertible {
|
||||||
|
faceNormal = u3dTriangleNormal(world)
|
||||||
|
}
|
||||||
|
p0, p1, p2 := vertices[0].point, vertices[1].point, vertices[2].point
|
||||||
|
facing := (p1[0]-p0[0])*(p2[1]-p0[1]) - (p1[1]-p0[1])*(p2[0]-p0[0])
|
||||||
|
if r.camera.Perspective {
|
||||||
|
facing = p0.unit().dot(p1.unit().cross(p2.unit()))
|
||||||
|
}
|
||||||
|
if !finite(facing) {
|
||||||
|
return fmt.Errorf("invalid U3D face orientation")
|
||||||
|
}
|
||||||
|
front := facing < 0
|
||||||
|
visibility := instance.modelVisibility(node)
|
||||||
|
if facing == 0 || front && visibility&1 == 0 || !front && visibility&2 == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
shaderNames := []string{""}
|
||||||
|
if uint64(face.Shading) < uint64(len(node.Shaders)) {
|
||||||
|
shaderNames = node.Shaders[face.Shading]
|
||||||
|
}
|
||||||
|
for shaderIndex, name := range shaderNames {
|
||||||
|
if shaderIndex%128 == 0 {
|
||||||
|
if err := r.ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
style, err := r.style(name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if style.shader.RenderPass&(1<<passIndex) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if instance.opacity >= 0 {
|
||||||
|
style.shader.Blend = 0x606
|
||||||
|
}
|
||||||
|
for i, corner := range face.Corners {
|
||||||
|
normal := faceNormal
|
||||||
|
if !mesh.ExcludeNormals && invertible {
|
||||||
|
normal = normals[corner.Normal]
|
||||||
|
}
|
||||||
|
if !front {
|
||||||
|
for j := range normal {
|
||||||
|
normal[j] = -normal[j]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var diffuse, specular *[4]float32
|
||||||
|
if shading.Attributes&1 != 0 {
|
||||||
|
diffuse = &mesh.Diffuse[corner.Diffuse]
|
||||||
|
}
|
||||||
|
if shading.Attributes&2 != 0 {
|
||||||
|
specular = &mesh.Specular[corner.Specular]
|
||||||
|
}
|
||||||
|
vertices[i].color, err = r.shade(world[i], normal, style, diffuse, specular)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if instance.opacity >= 0 {
|
||||||
|
vertices[i].color[3] = instance.opacity
|
||||||
|
}
|
||||||
|
}
|
||||||
|
r.order++
|
||||||
|
if err := r.sectionTriangle(vertices, style.shader, pass); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// clipTriangle 在透视除法前裁剪六个视锥面,避免近面穿越或屏外巨大坐标
|
||||||
|
// 入参: triangle 三角面, shader 着色器, pass 绘制轮次
|
||||||
|
// 返回: error 数值、资源或取消错误
|
||||||
|
func (r *u3dRender) clipTriangle(triangle [3]u3dVertex, shader U3DShader, pass U3DViewPass) error {
|
||||||
|
var storage [2][12]u3dVertex
|
||||||
|
copy(storage[0][:], triangle[:])
|
||||||
|
size, current := 3, 0
|
||||||
|
planes := r.frustumPlanes()
|
||||||
|
for planeIndex, plane := range planes {
|
||||||
|
if planeIndex == 1 && r.camera.Far == 0 && !r.camera.ClipFar {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
output, next := 0, 1-current
|
||||||
|
previous := storage[current][size-1]
|
||||||
|
distance := func(v u3dVertex) float64 {
|
||||||
|
return plane[0]*v.point[0] + plane[1]*v.point[1] + plane[2]*v.point[2] + plane[3]
|
||||||
|
}
|
||||||
|
previousDistance := distance(previous)
|
||||||
|
for _, vertex := range storage[current][:size] {
|
||||||
|
d := distance(vertex)
|
||||||
|
if !finite(d) || !finite(previousDistance) {
|
||||||
|
return fmt.Errorf("invalid U3D clip distance")
|
||||||
|
}
|
||||||
|
if (d >= 0) != (previousDistance >= 0) {
|
||||||
|
t := u3dClipFraction(previousDistance, d)
|
||||||
|
var intersection u3dVertex
|
||||||
|
for i := range intersection.point {
|
||||||
|
intersection.point[i] = previous.point[i]*(1-t) + vertex.point[i]*t
|
||||||
|
}
|
||||||
|
for i := range intersection.color {
|
||||||
|
intersection.color[i] = previous.color[i]*(1-t) + vertex.color[i]*t
|
||||||
|
}
|
||||||
|
if output == len(storage[next]) {
|
||||||
|
return fmt.Errorf("invalid U3D clipped polygon")
|
||||||
|
}
|
||||||
|
storage[next][output] = intersection
|
||||||
|
output++
|
||||||
|
}
|
||||||
|
if d >= 0 {
|
||||||
|
if output == len(storage[next]) {
|
||||||
|
return fmt.Errorf("invalid U3D clipped polygon")
|
||||||
|
}
|
||||||
|
storage[next][output] = vertex
|
||||||
|
output++
|
||||||
|
}
|
||||||
|
previous, previousDistance = vertex, d
|
||||||
|
}
|
||||||
|
if output < 3 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
current, size = next, output
|
||||||
|
}
|
||||||
|
for i := 1; i+1 < size; i++ {
|
||||||
|
if err := r.rasterTriangle([3]u3dVertex{storage[current][0], storage[current][i], storage[current][i+1]}, shader, pass); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// rasterTriangle 使用像素中心和左上边规则光栅化,透视插值保持颜色与深度一致
|
||||||
|
// 入参: triangle 已裁剪三角面, shader 着色器, pass 绘制轮次
|
||||||
|
// 返回: error 数值、预算或取消错误
|
||||||
|
func (r *u3dRender) rasterTriangle(triangle [3]u3dVertex, shader U3DShader, pass U3DViewPass) error {
|
||||||
|
var x, y, reciprocal [3]float64
|
||||||
|
for i, v := range triangle {
|
||||||
|
reciprocal[i] = 1
|
||||||
|
if r.camera.Perspective {
|
||||||
|
reciprocal[i] = 1 / v.point[2]
|
||||||
|
}
|
||||||
|
x[i] = r.viewport[0] + r.viewport[2]/2 + r.camera.ShiftX + v.point[0]*r.camera.ScaleX*reciprocal[i]
|
||||||
|
y[i] = r.viewport[1] + r.viewport[3]/2 + r.camera.ShiftY - v.point[1]*r.camera.ScaleY*reciprocal[i]
|
||||||
|
if !finite(x[i]) || !finite(y[i]) || !finite(reciprocal[i]) {
|
||||||
|
return fmt.Errorf("invalid U3D projected point")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
area := (x[1]-x[0])*(y[2]-y[0]) - (y[1]-y[0])*(x[2]-x[0])
|
||||||
|
if !finite(area) {
|
||||||
|
return fmt.Errorf("invalid U3D projected area")
|
||||||
|
}
|
||||||
|
if area == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if area < 0 {
|
||||||
|
triangle[1], triangle[2] = triangle[2], triangle[1]
|
||||||
|
x[1], x[2] = x[2], x[1]
|
||||||
|
y[1], y[2] = y[2], y[1]
|
||||||
|
reciprocal[1], reciprocal[2] = reciprocal[2], reciprocal[1]
|
||||||
|
area = -area
|
||||||
|
}
|
||||||
|
width, height := r.output.Rect.Dx(), r.output.Rect.Dy()
|
||||||
|
left := max(0, math.Ceil(min(x[0], x[1], x[2])-.5))
|
||||||
|
right := min(float64(width-1), math.Floor(max(x[0], x[1], x[2])-.5))
|
||||||
|
top := max(0, math.Ceil(min(y[0], y[1], y[2])-.5))
|
||||||
|
bottom := min(float64(height-1), math.Floor(max(y[0], y[1], y[2])-.5))
|
||||||
|
if left > right || top > bottom {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var inclusive [3]bool
|
||||||
|
for i := range 3 {
|
||||||
|
j := (i + 1) % 3
|
||||||
|
inclusive[i] = y[j] < y[i] || y[j] == y[i] && x[j] > x[i]
|
||||||
|
}
|
||||||
|
for row := int(top); row <= int(bottom); row++ {
|
||||||
|
if err := r.ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for column := int(left); column <= int(right); column++ {
|
||||||
|
if column%1024 == 0 {
|
||||||
|
if err := r.ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
px, py := float64(column)+.5, float64(row)+.5
|
||||||
|
var edge [3]float64
|
||||||
|
inside := true
|
||||||
|
for i := range 3 {
|
||||||
|
j := (i + 1) % 3
|
||||||
|
edge[i] = (x[j]-x[i])*(py-y[i]) - (y[j]-y[i])*(px-x[i])
|
||||||
|
if edge[i] < 0 || edge[i] == 0 && !inclusive[i] {
|
||||||
|
inside = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !inside {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
w1, w2 := edge[2]/area, edge[0]/area
|
||||||
|
depth := triangle[0].point[2] + w1*(triangle[1].point[2]-triangle[0].point[2]) + w2*(triangle[2].point[2]-triangle[0].point[2])
|
||||||
|
if r.camera.Perspective {
|
||||||
|
q := edge[1]/area*reciprocal[0] + w1*reciprocal[1] + w2*reciprocal[2]
|
||||||
|
if !finite(q) || q <= 0 {
|
||||||
|
return fmt.Errorf("invalid U3D perspective depth")
|
||||||
|
}
|
||||||
|
w1 = w1 * reciprocal[1] / q
|
||||||
|
w2 = w2 * reciprocal[2] / q
|
||||||
|
depth = 1 / q
|
||||||
|
}
|
||||||
|
pixel := row*width + column
|
||||||
|
if depth > r.frame[pixel].depth {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var color [4]float64
|
||||||
|
for c := range color {
|
||||||
|
color[c] = triangle[0].color[c] + w1*(triangle[1].color[c]-triangle[0].color[c]) + w2*(triangle[2].color[c]-triangle[0].color[c])
|
||||||
|
if !finite(color[c]) {
|
||||||
|
return fmt.Errorf("invalid U3D interpolated color")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
color[3] = min(1, max(0, color[3]))
|
||||||
|
if shader.Attributes&2 != 0 && !u3dAlphaTest(shader.AlphaFunction, color[3], float64(shader.AlphaReference)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if pass.Attributes&1 != 0 {
|
||||||
|
position := u3dVector{(px - r.viewport[0] - r.viewport[2]/2 - r.camera.ShiftX) / r.camera.ScaleX, (r.viewport[1] + r.viewport[3]/2 + r.camera.ShiftY - py) / r.camera.ScaleY, depth}
|
||||||
|
if r.camera.Perspective {
|
||||||
|
position[0] *= depth
|
||||||
|
position[1] *= depth
|
||||||
|
}
|
||||||
|
distance := math.Hypot(math.Hypot(position[0], position[1]), position[2])
|
||||||
|
factor := (float64(pass.FogFar) - distance) / (float64(pass.FogFar) - float64(pass.FogNear))
|
||||||
|
if pass.FogMode != 0 {
|
||||||
|
amount := distance * math.Log(100) / float64(pass.FogFar)
|
||||||
|
if pass.FogMode == 2 {
|
||||||
|
amount *= amount
|
||||||
|
}
|
||||||
|
factor = math.Exp(-amount)
|
||||||
|
}
|
||||||
|
factor = min(1, max(0, factor))
|
||||||
|
for c := range 3 {
|
||||||
|
color[c] = color[c]*factor + float64(pass.FogColor[c])*(1-factor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for c := range 3 {
|
||||||
|
color[c] = min(1, max(0, color[c]))
|
||||||
|
}
|
||||||
|
if err := r.fragment(pixel, depth, color, shader.Blend); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// fragment 直接保存不透明覆盖,透明及其他混合方式保留逐像素深度
|
||||||
|
// 入参: pixel 像素索引, depth 深度, color 颜色, blend 混合方式
|
||||||
|
// 返回: error 预算或取消错误
|
||||||
|
func (r *u3dRender) fragment(pixel int, depth float64, color [4]float64, blend uint32) error {
|
||||||
|
if !finite(depth) {
|
||||||
|
return fmt.Errorf("invalid U3D fragment depth")
|
||||||
|
}
|
||||||
|
p := &r.frame[pixel]
|
||||||
|
if blend == 0x606 && color[3] == 1 || blend == 0x607 && color[3] == 0 {
|
||||||
|
p.depth = depth
|
||||||
|
p.order = r.order
|
||||||
|
copy(p.color[:], color[:3])
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if blend == 0x606 && color[3] == 0 || blend == 0x607 && color[3] == 1 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if r.fragments >= math.MaxInt32 {
|
||||||
|
return fmt.Errorf("U3D fragment count exceeds limit")
|
||||||
|
}
|
||||||
|
chunk := r.fragments / 512
|
||||||
|
if chunk == len(r.chunks) {
|
||||||
|
if err := r.reserve(1, 512*56+48); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
r.chunks = append(r.chunks, make([]u3dFragment, 512))
|
||||||
|
}
|
||||||
|
r.chunks[chunk][r.fragments%512] = u3dFragment{color: color, depth: depth, order: r.order, blend: blend, next: p.head}
|
||||||
|
p.head = int32(r.fragments)
|
||||||
|
r.fragments++
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// resolveFragments 按每个像素的远近排序,正确处理相交透明面和着色器叠加
|
||||||
|
// 返回: error 预算或取消错误
|
||||||
|
func (r *u3dRender) resolveFragments() error {
|
||||||
|
if r.fragments == 0 {
|
||||||
|
return r.ctx.Err()
|
||||||
|
}
|
||||||
|
get := func(i int) *u3dFragment { return &r.chunks[i/512][i%512] }
|
||||||
|
for i := range r.frame {
|
||||||
|
if i%1024 == 0 {
|
||||||
|
if err := r.ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pixel := &r.frame[i]
|
||||||
|
r.scratch = r.scratch[:0]
|
||||||
|
for index := pixel.head; index >= 0; {
|
||||||
|
if len(r.scratch)%1024 == 0 {
|
||||||
|
if err := r.ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
current := int(index)
|
||||||
|
fragment := get(current)
|
||||||
|
index = fragment.next
|
||||||
|
if fragment.depth > pixel.depth || fragment.depth == pixel.depth && fragment.order < pixel.order {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if len(r.scratch) == cap(r.scratch) {
|
||||||
|
capacity := max(8, cap(r.scratch)*2)
|
||||||
|
if err := r.reserve(uint64(capacity), 8); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
values := make([]int, len(r.scratch), capacity)
|
||||||
|
copy(values, r.scratch)
|
||||||
|
r.remaining += uint64(cap(r.scratch)) * 8
|
||||||
|
r.scratch = values
|
||||||
|
}
|
||||||
|
r.scratch = append(r.scratch, current)
|
||||||
|
}
|
||||||
|
slices.SortFunc(r.scratch, func(a, b int) int {
|
||||||
|
x, y := get(a), get(b)
|
||||||
|
if x.depth > y.depth {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
if x.depth < y.depth {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
if x.order < y.order {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
if x.order > y.order {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
})
|
||||||
|
for layer, index := range r.scratch {
|
||||||
|
if layer%1024 == 0 {
|
||||||
|
if err := r.ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fragment := get(index)
|
||||||
|
alpha := fragment.color[3]
|
||||||
|
if fragment.blend == 0x607 {
|
||||||
|
alpha = 1 - alpha
|
||||||
|
}
|
||||||
|
for c := range 3 {
|
||||||
|
switch fragment.blend {
|
||||||
|
case 0x604:
|
||||||
|
pixel.color[c] += fragment.color[c]
|
||||||
|
case 0x605:
|
||||||
|
pixel.color[c] *= fragment.color[c]
|
||||||
|
default:
|
||||||
|
pixel.color[c] = fragment.color[c]*alpha + pixel.color[c]*(1-alpha)
|
||||||
|
}
|
||||||
|
pixel.color[c] = min(1, max(0, pixel.color[c]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return r.ctx.Err()
|
||||||
|
}
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package ofdgo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
// U3DView 保存视图节点的投影、裁剪、视口及叠加层参数
|
||||||
|
type U3DView struct {
|
||||||
|
Attributes uint32
|
||||||
|
Near, Far float32
|
||||||
|
Projection float32
|
||||||
|
ProjectionVector [3]float32
|
||||||
|
Viewport [4]float32
|
||||||
|
Backdrops, Overlays []U3DViewLayer
|
||||||
|
}
|
||||||
|
|
||||||
|
// U3DViewLayer 保存背景或覆盖纹理,注册点使用纹理像素,旋转使用弧度
|
||||||
|
type U3DViewLayer struct {
|
||||||
|
Texture string
|
||||||
|
Blend, Rotation float32
|
||||||
|
Location [2]float32
|
||||||
|
Registration [2]int32
|
||||||
|
Scale [2]float32
|
||||||
|
}
|
||||||
|
|
||||||
|
// U3DViewResource 保存视图的绘制轮次,各轮独立指定根节点及雾效
|
||||||
|
type U3DViewResource struct {
|
||||||
|
Name string
|
||||||
|
Passes []U3DViewPass
|
||||||
|
}
|
||||||
|
|
||||||
|
// U3DViewPass 保存单轮根节点与雾效,Attributes最低位启用雾效
|
||||||
|
type U3DViewPass struct {
|
||||||
|
Root string
|
||||||
|
Attributes, FogMode uint32
|
||||||
|
FogColor [4]float32
|
||||||
|
FogNear, FogFar float32
|
||||||
|
}
|
||||||
|
|
||||||
|
// view 按ECMA-363第9.5.4节读取视图节点的可变投影字段
|
||||||
|
// 入参: r 字段读取器
|
||||||
|
// 返回: *U3DView 视图, error 结构或预算错误
|
||||||
|
func (d *u3dDecoder) view(r *u3dValues) (*U3DView, error) {
|
||||||
|
view := &U3DView{Attributes: r.u32(), Near: r.f32(), Far: r.f32()}
|
||||||
|
if view.Attributes & ^uint32(7) != 0 {
|
||||||
|
return nil, fmt.Errorf("invalid U3D view attributes")
|
||||||
|
}
|
||||||
|
if view.Attributes&6 < 4 {
|
||||||
|
view.Projection = r.f32()
|
||||||
|
} else {
|
||||||
|
for i := range view.ProjectionVector {
|
||||||
|
view.ProjectionVector[i] = r.f32()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i := range view.Viewport {
|
||||||
|
view.Viewport[i] = r.f32()
|
||||||
|
}
|
||||||
|
for _, layers := range []*[]U3DViewLayer{&view.Backdrops, &view.Overlays} {
|
||||||
|
count := r.u32()
|
||||||
|
if uint64(count) > uint64(len(r.data)-r.pos)/34 {
|
||||||
|
return nil, io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
if err := d.reserve(uint64(count), 64); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
*layers = make([]U3DViewLayer, int(count))
|
||||||
|
for i := range *layers {
|
||||||
|
if err := d.ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
layer := &(*layers)[i]
|
||||||
|
layer.Texture = r.text()
|
||||||
|
layer.Blend, layer.Rotation = r.f32(), r.f32()
|
||||||
|
for j := range layer.Location {
|
||||||
|
layer.Location[j] = r.f32()
|
||||||
|
}
|
||||||
|
for j := range layer.Registration {
|
||||||
|
layer.Registration[j] = int32(r.u32())
|
||||||
|
}
|
||||||
|
for j := range layer.Scale {
|
||||||
|
layer.Scale[j] = r.f32()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return view, r.err
|
||||||
|
}
|
||||||
|
|
||||||
|
// viewResource 按ECMA-363第9.8.2节读取绘制轮次与雾效
|
||||||
|
// 入参: r 字段读取器, name 资源名
|
||||||
|
// 返回: error 结构或预算错误
|
||||||
|
func (d *u3dDecoder) viewResource(r *u3dValues, name string) error {
|
||||||
|
count := r.u32()
|
||||||
|
if uint64(count) > uint64(len(r.data)-r.pos)/34 {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
if err := d.reserve(uint64(count), 48); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
view := U3DViewResource{Name: name, Passes: make([]U3DViewPass, int(count))}
|
||||||
|
for i := range view.Passes {
|
||||||
|
if err := d.ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
pass := &view.Passes[i]
|
||||||
|
pass.Root, pass.Attributes, pass.FogMode = r.text(), r.u32(), r.u32()
|
||||||
|
if pass.Attributes > 1 || pass.FogMode > 2 {
|
||||||
|
return fmt.Errorf("invalid U3D view fog parameters")
|
||||||
|
}
|
||||||
|
for j := range pass.FogColor {
|
||||||
|
pass.FogColor[j] = r.f32()
|
||||||
|
}
|
||||||
|
pass.FogNear, pass.FogFar = r.f32(), r.f32()
|
||||||
|
}
|
||||||
|
d.model.Views = append(d.model.Views, view)
|
||||||
|
return r.err
|
||||||
|
}
|
||||||
Reference in new issue
Block a user