mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-30 01:27:26 +08:00
feat(编辑渲染): 完善三维转换,优化编辑渲染压缩
This commit is contained in:
1 parent
e8344e6387
commit
839f65e3d7
47 files changed
+4887
-112
No files matched your search
+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
|
||||
}
|
||||
Reference in new issue
Block a user