package d2isometricimg
import (
"fmt"
"image"
"image/draw"
"math"
"github.com/pancsta/d2/d2renderers/d2isometric"
"github.com/pancsta/d2/d2renderers/d2raster"
"github.com/pancsta/d2/d2renderers/d2scene"
"github.com/pancsta/d2/d2renderers/d2scenebuild"
"github.com/pancsta/d2/d2target"
"github.com/pancsta/d2/lib/geo"
"github.com/pancsta/d2/lib/label"
"github.com/pancsta/d2/lib/shape"
"github.com/pancsta/d2/lib/svg"
)
type nativeAnimatedNode struct { first, last int }
func nativeCanonicalNode(n d2isometric .Node ) bool {
return !nativeSolidNode (n )
}
func nativeReliefSymbol(n d2isometric .Node ) bool {
switch n .Type {
case d2target .ShapeCloud , d2target .ShapePerson , d2target .ShapeC4Person :
return true
}
return false
}
func nativeCanonicalHeight(n d2isometric .Node , scale float64 ) float64 {
height := math .Max (.10 , n .Size .Y *1.15 )
switch n .Type {
case d2target .ShapeClass , d2target .ShapeSQLTable :
height = .46
case d2target .ShapeCloud :
height = .24 * math .Min (n .Size .X , n .Size .Z )
case d2target .ShapePerson , d2target .ShapeC4Person :
height = .19 * math .Min (n .Size .X , n .Size .Z )
case d2target .ShapeText :
height = .018
if nativeMarkdownCard (n ) {
height = .115
}
}
if n .Metadata .Original .ThreeDee {
if scale <= 0 {
scale = .01
if n .Metadata .Original .Width > 0 {
scale = n .Size .X / float64 (n .Metadata .Original .Width )
}
}
height += d2target .THREE_DEE_OFFSET * scale
}
return height
}
func nativeFaceSource(n d2isometric .Node , fill string ) d2target .Shape {
s := n .Metadata .Original
s .ID , s .Pos = "native-face" , d2target .NewPoint (0 , 0 )
if s .Width <= 0 {
s .Width = max (1 , int (math .Ceil (n .Size .X /.01 )))
}
if s .Height <= 0 {
s .Height = max (1 , int (math .Ceil (n .Size .Z /.01 )))
}
s .Text = d2target .Text {}
s .Icon , s .Link , s .Tooltip = nil , "" , ""
s .PrettyLink , s .TooltipPosition , s .LabelPosition = "" , "" , ""
if label .FromString (n .Metadata .Original .LabelPosition ).IsBorder () && n .Metadata .Original .Label != "" {
s .Text = d2target .Text {Label : " " , LabelWidth : n .Metadata .Original .LabelWidth , LabelHeight : n .Metadata .Original .LabelHeight , FontSize : max (1 , n .Metadata .Original .FontSize ), Color : "transparent" , LabelFill : "transparent" }
s .LabelPosition = n .Metadata .Original .LabelPosition
}
s .Classes = nil
s .Class , s .SQLTable = d2target .Class {}, d2target .SQLTable {}
s .Multiple , s .Shadow , s .Animated , s .ThreeDee , s .Blend = false , false , false , false , false
s .Opacity , s .Fill , s .Stroke = 1 , fill , n .Stroke
s .StrokeWidth , s .StrokeDash = n .StrokeWidth , n .StrokeDash
if n .Type == d2target .ShapeClass || n .Type == d2target .ShapeSQLTable {
s .Fill , s .Stroke = "transparent" , n .Fill
}
if s .Type == d2target .ShapeClass || s .Type == d2target .ShapeSQLTable || s .Type == d2target .ShapeText || s .Type == d2target .ShapeCode || s .Type == d2target .ShapeImage {
s .Type = d2target .ShapeRectangle
}
return s
}
func (b *meshBuilder ) nativeFace (s d2target .Shape ) (*image .RGBA , labelSurface ) {
return b .nativeFaceLayer (s , nativeFaceCombined )
}
type nativeFaceLayer uint8
const (
nativeFaceCombined nativeFaceLayer = iota
nativeFaceFill
nativeFaceInk
)
func (b *meshBuilder ) nativeFaceLayers (s d2target .Shape , opacity ...float64 ) (fill , ink *image .RGBA , area labelSurface ) {
budget := b .faceMaxPixels
if budget > 0 {
b .faceMaxPixels = max (1 , budget /2 )
}
defer func () { b .faceMaxPixels = budget }()
fill , area = b .nativeFaceLayer (s , nativeFaceFill )
if b .err == nil {
ink , _ = b .nativeFaceLayer (s , nativeFaceInk )
}
if b .err == nil && len (opacity ) > 0 && opacity [0 ] < 1 && fill != nil && ink != nil {
b .nativeFaceOpacity (fill , ink , max (0 , opacity [0 ]))
}
return
}
func (b *meshBuilder ) nativeFaceOpacity (fill , ink *image .RGBA , opacity float64 ) {
if surface := nativeVectorForTexture (b .ctx , fill ); surface != nil {
surface .inkCoverage = nativeVectorForTexture (b .ctx , ink )
surface .coverageOpacity = opacity
}
for y := fill .Bounds ().Min .Y ; y < fill .Bounds ().Max .Y ; y ++ {
if (y -fill .Bounds ().Min .Y )%64 == 0 {
if err := b .ctx .Err (); err != nil {
b .err = err
return
}
}
for x := fill .Bounds ().Min .X ; x < fill .Bounds ().Max .X ; x ++ {
a := float64 (ink .Pix [ink .PixOffset (x , y )+3 ]) / 255
factor := (1 - a ) / (1 - a *opacity )
i := fill .PixOffset (x , y )
for channel := 0 ; channel < 4 ; channel ++ {
fill .Pix [i +channel ] = uint8 (math .Round (float64 (fill .Pix [i +channel ]) * factor ))
}
}
}
}
func nativeSelectFaceLayer(n *d2scene .Node , layer nativeFaceLayer ) {
if n == nil || layer == nativeFaceCombined {
return
}
selectPaint := func (fill *d2scene .Paint , stroke **d2scene .Stroke ) {
if layer == nativeFaceFill {
*stroke = nil
} else {
*fill = nil
}
}
switch p := n .Primitive .(type ) {
case d2scene .Rect :
selectPaint (&p .Fill , &p .Stroke )
n .Primitive = p
case d2scene .Ellipse :
selectPaint (&p .Fill , &p .Stroke )
n .Primitive = p
case d2scene .Path :
selectPaint (&p .Fill , &p .Stroke )
n .Primitive = p
case d2scene .TextRun :
n .Primitive = nil
}
for _ , child := range n .Children {
nativeSelectFaceLayer (child , layer )
}
}
func (b *meshBuilder ) nativeFaceLayer (s d2target .Shape , layer nativeFaceLayer ) (*image .RGBA , labelSurface ) {
d := d2target .NewDiagram ()
d .Root .Fill , d .Root .Stroke = "transparent" , "none"
d .Shapes = []d2target .Shape {s }
pad := int64 (0 )
doc , err := d2scenebuild .Build (b .ctx , d , d2scenebuild .Options {Pad : &pad , MaxNodes : 20000 , MaxPathCommands : 250000 ,
LinkBudget : d2scenebuild .LinkBudget {MaxRegions : 2 , MaxStringBytes : 4096 }})
if err != nil {
b .err = fmt .Errorf ("native shape face: %w" , err )
return nil , labelSurface {}
}
if s .Type == d2target .ShapePage && layer == nativeFaceInk && nativePaint (s .Fill , "white" ).A > 0 {
nativeRemovePageGlyph (doc .Root )
}
nativeSelectFaceLayer (doc .Root , layer )
opts := richRasterOptions ()
opts .Scale = math .Min (2 , 1024 /math .Max (doc .ViewBox .Width , doc .ViewBox .Height ))
if b .faceMaxPixels > 0 {
opts .Scale = math .Min (opts .Scale , math .Sqrt (float64 (b .faceMaxPixels )/(doc .ViewBox .Width *doc .ViewBox .Height ))) * .999
}
opts .MaxDecodedAssetBytes = 32 * 1024 * 1024
pixels := int64 (math .Ceil (doc .ViewBox .Width *opts .Scale )) * int64 (math .Ceil (doc .ViewBox .Height *opts .Scale ))
maxPixels := int64 (1024 * 1024 )
if b .faceMaxPixels > 0 && pixels > b .faceMaxPixels {
maximum := max (1 , int (math .Ceil (max (doc .ViewBox .Width , doc .ViewBox .Height )*opts .Scale )))
w , h := surfaceTextureDimensions (doc .ViewBox .Width , doc .ViewBox .Height , maximum , int (b .faceMaxPixels ))
doc .LogicalWidth , doc .LogicalHeight = float64 (w ), float64 (h )
doc .ViewportFit , opts .Scale = d2scene .ViewportStretch , 1
pixels = int64 (w ) * int64 (h )
}
if b .outputDensity > 0 && captionFinite (b .outputDensity ) {
scale := b .scale
if scale <= 0 {
scale = .01
}
maxPixels = 4 * 1024 * 1024
budget := maxPixels
if b .faceMaxPixels > 0 {
budget = min (budget , b .faceMaxPixels )
}
w , h := surfaceTextureDimensionsAtDensity (doc .ViewBox .Width *scale , doc .ViewBox .Height *scale , 4096 , int (budget ), b .outputDensity )
doc .LogicalWidth , doc .LogicalHeight = float64 (w ), float64 (h )
doc .ViewportFit = d2scene .ViewportStretch
opts .Scale = 1
pixels = int64 (w ) * int64 (h )
}
if pixels <= 0 || pixels > maxPixels || b .facePixels > 24 *1024 *1024 -pixels {
b .err = fmt .Errorf ("native shape faces exceed texture pixel budget" )
return nil , labelSurface {}
}
b .facePixels += pixels
frame , err := d2raster .Render (b .ctx , doc , opts )
if err != nil {
b .err = fmt .Errorf ("native shape face raster: %w" , err )
return nil , labelSurface {}
}
tex := image .NewRGBA (frame .Bounds ())
draw .Draw (tex , tex .Bounds (), frame , frame .Bounds ().Min , draw .Src )
if err := retainNativeVectorSurface (b .ctx , tex , doc ); err != nil {
b .err = err
return nil , labelSurface {}
}
area := labelSurface {center : nv (doc .ViewBox .X +doc .ViewBox .Width /2 , 0 , doc .ViewBox .Y +doc .ViewBox .Height /2 ), width : doc .ViewBox .Width , depth : doc .ViewBox .Height }
if layer != nativeFaceFill {
b .nativeFaceBorderAperture (tex , area , s )
}
visible := false
for i := 3 ; i < len (tex .Pix ); i += 4 {
if tex .Pix [i ] != 0 {
visible = true
break
}
}
if !visible {
tex = nil
}
return tex , area
}
func (b *meshBuilder ) nativeFaceBorderAperture (tex *image .RGBA , area labelSurface , s d2target .Shape ) {
position := label .FromString (s .LabelPosition )
if s .Label == "" || !position .IsBorder () || s .StrokeWidth <= 0 {
return
}
box := geo .NewBox (geo .NewPoint (float64 (s .Pos .X ), float64 (s .Pos .Y )), float64 (s .Width ), float64 (s .Height ))
p := position .GetPointOnBox (box , label .PADDING , float64 (s .LabelWidth ), float64 (s .LabelHeight ))
stroke := float64 (s .StrokeWidth )
x , y , w , h := 0. , 0. , 0. , 0.
switch position {
case label .BorderTopLeft , label .BorderTopCenter , label .BorderTopRight :
x , y , w , h = p .X -2 , box .TopLeft .Y -stroke /2 , float64 (s .LabelWidth +4 ), stroke
case label .BorderBottomLeft , label .BorderBottomCenter , label .BorderBottomRight :
x , y , w , h = p .X -2 , box .TopLeft .Y +box .Height -stroke /2 , float64 (s .LabelWidth +4 ), stroke
case label .BorderLeftTop , label .BorderLeftMiddle , label .BorderLeftBottom :
x , y , w , h = box .TopLeft .X -stroke /2 , p .Y -2 , stroke , float64 (s .LabelHeight +4 )
case label .BorderRightTop , label .BorderRightMiddle , label .BorderRightBottom :
x , y , w , h = box .TopLeft .X +box .Width -stroke /2 , p .Y -2 , stroke , float64 (s .LabelHeight +4 )
}
if surface := nativeVectorForTexture (b .ctx , tex ); surface != nil {
nativeVectorAperture (surface , d2scene .Box {X : x , Y : y , Width : w , Height : h })
}
sx , sy := float64 (tex .Bounds ().Dx ())/area .width , float64 (tex .Bounds ().Dy ())/area .depth
x0 , y0 := (x -area .center .X +area .width /2 )*sx , (y -area .center .Z +area .depth /2 )*sy
x1 , y1 := x0 +w *sx , y0 +h *sy
for py := max (0 , int (math .Floor (y0 ))); py < min (tex .Bounds ().Dy (), int (math .Ceil (y1 ))); py ++ {
for px := max (0 , int (math .Floor (x0 ))); px < min (tex .Bounds ().Dx (), int (math .Ceil (x1 ))); px ++ {
coverage := (math .Min (float64 (px +1 ), x1 ) - math .Max (float64 (px ), x0 )) * (math .Min (float64 (py +1 ), y1 ) - math .Max (float64 (py ), y0 ))
i := tex .PixOffset (px +tex .Bounds ().Min .X , py +tex .Bounds ().Min .Y )
for c := 0 ; c < 4 ; c ++ {
tex .Pix [i +c ] = uint8 (math .Round (float64 (tex .Pix [i +c ]) * (1 - coverage )))
}
}
}
}
func (b *meshBuilder ) canonicalNode (n d2isometric .Node , tint string ) {
scale := b .scale
if scale <= 0 {
scale = .01
}
fill := n .Fill
if nativeToken (n .Metadata .Original .Fill ) && !n .FillExplicit {
fill = "#dce5eb"
}
if n .Type == d2target .ShapeClass || n .Type == d2target .ShapeSQLTable {
fill = n .Stroke
}
s := nativeFaceSource (n , fill )
profiles , err := nativeShapeProfiles (s )
if err != nil {
b .err = err
return
}
paintOpacity := n .Opacity
if nativeClassicRim (n ) {
paintOpacity = 1
}
tex , ink , face := b .nativeFaceLayers (s , paintOpacity )
if b .err != nil {
return
}
w , d := n .Size .X , n .Size .Z
sx , sz := w /float64 (s .Width ), d /float64 (s .Height )
base := n .Position .Y - n .Size .Y /2
height := nativeCanonicalHeight (n , scale )
top := base + height
mat := nativeMaterial (fill , .68 , 0 , n .Opacity )
side := *mat
for copyIndex := 1 ; copyIndex >= 0 ; copyIndex -- {
if copyIndex == 1 && !n .Metadata .Original .Multiple {
continue
}
dx , dz , dy := float64 (copyIndex )*d2target .MULTIPLE_OFFSET *scale , -float64 (copyIndex )*d2target .MULTIPLE_OFFSET *scale , -float64 (copyIndex )*math .Min (.045 , height *.25 )
var pageProfile []Vec
for _ , profile := range profiles {
world := make ([]Vec , len (profile ))
for i , p := range profile {
world [i ] = nv (n .Position .X -w /2 +float64 (p .X *sx )+dx , top +dy , n .Position .Z -d /2 +float64 (p .Z *sz )+dz )
}
if n .Type == d2target .ShapePage {
pageProfile = world
}
var cap *Material
if b .options .vector && n .Opacity == 1 && nativePaint (fill , "transparent" ).A == 255 {
cap = mat
}
start := len (b .triangles )
b .extrudedProfile (world , base +dy +b .nodeSupportDrop , cap , &side )
if cap != nil {
for i := start ; i < len (b .triangles ); i ++ {
if b .triangles [i ].Material == cap {
b .triangles [i ].svgCoverageOnly = true
b .triangles [i ].CastShadow = false
}
}
}
}
paint := face
paint .center = nv (n .Position .X -w /2 +float64 (face .center .X *sx )+dx , top +dy +.0005 , n .Position .Z -d /2 +float64 (face .center .Z *sz )+dz )
paint .width , paint .depth = face .width *sx , face .depth *sz
before := len (b .triangles )
b .surfaceTexture (tex , paint , n .Opacity )
for i := before ; i < len (b .triangles ); i ++ {
b .triangles [i ].Material .Unlit = false
b .triangles [i ].Material .Roughness = .68
b .triangles [i ].Material .Metalness = 0
b .triangles [i ].NoDepthWrite = false
b .triangles [i ].CastShadow = true
}
if !nativeClassicRim (n ) {
b .surfaceTexture (ink , paint , n .Opacity )
}
if n .Type == d2target .ShapePage {
b .nativePageFold (n , paint , tex , mat , dx , dz , top +dy , pageProfile )
}
}
b .canonicalNodeContent (n , s , fill , top )
}
func (b *meshBuilder ) canonicalNodeContent (n d2isometric .Node , s d2target .Shape , fill string , top float64 ) {
surface := nativeNodeLabelSurface (n , s , top +.0015 )
ink := readableSurfaceInk (fill , n .Opacity )
if n .FontExplicit {
ink = n .FontColor
}
if n .Metadata .Original .Icon != nil {
iconFace := nativeNodeIconSurface (n , s , top +.0015 )
iconSource := n .Metadata .Original
if n .Type == d2target .ShapeImage {
iconSource .Label = ""
}
insideLabel := b .shapeIcon (iconSource , iconFace , n .Opacity , "node" )
position := label .FromString (n .Metadata .Original .LabelPosition )
if n .Type != d2target .ShapeImage && !position .IsOutside () && !position .IsBorder () {
surface = insideLabel
}
}
b .shapeLabel (n .Metadata .Original , surface , ink , n .Opacity , "node" )
}
func nativeNodeIconSurface(n d2isometric .Node , s d2target .Shape , y float64 ) labelSurface {
if n .Type == d2target .ShapeImage || n .Type == d2target .ShapeClass || n .Type == d2target .ShapeSQLTable {
return labelSurface {center : nv (n .Position .X , y , n .Position .Z ), width : n .Size .X , depth : n .Size .Z }
}
geometry := shape .NewShape (d2target .DSL_SHAPE_TO_SHAPE_TYPE [s .Type ], geo .NewBox (geo .NewPoint (0 , 0 ), float64 (s .Width ), float64 (s .Height )))
box := geometry .GetInnerBox ()
sx , sz := n .Size .X /float64 (s .Width ), n .Size .Z /float64 (s .Height )
return labelSurface {center : nv (n .Position .X -n .Size .X /2 +(box .TopLeft .X +box .Width /2 )*sx , y , n .Position .Z -n .Size .Z /2 +(box .TopLeft .Y +box .Height /2 )*sz ), width : box .Width * sx , depth : box .Height * sz }
}
func (b *meshBuilder ) clearOutsideCaption (n d2isometric .Node , first int , relief float64 ) {
if b .err != nil || n .Metadata .Original .Label == "" || len (b .triangles ) < first +2 {
return
}
caption := b .triangles [len (b .triangles )-2 :]
for _ , t := range caption {
if !t .NoDepthWrite || t .Material == nil || t .Material .Texture == nil {
return
}
}
direction := nativeViewDirection ()
height := math .Max (0 , caption [0 ].V [0 ].Position .Y -(n .Position .Y -n .Size .Y /2 )) * relief
horizontal := direction .X *direction .X + direction .Z *direction .Z
dx , dz := 0. , 0.
switch label .FromString (n .Metadata .Original .LabelPosition ) {
case label .OutsideBottomLeft , label .OutsideBottomCenter , label .OutsideBottomRight :
dz = height * horizontal / (direction .Y * direction .Z )
case label .OutsideRightTop , label .OutsideRightMiddle , label .OutsideRightBottom :
dx = height * horizontal / (direction .Y * direction .X )
default :
return
}
for i := range caption {
for j := range caption [i ].V {
caption [i ].V [j ].Position .X += dx
caption [i ].V [j ].Position .Z += dz
}
}
}
func nativeNodeLabelSurface(n d2isometric .Node , s d2target .Shape , y float64 ) labelSurface {
sx , sz := n .Size .X /float64 (s .Width ), n .Size .Z /float64 (s .Height )
if n .Type == d2target .ShapeClass || n .Type == d2target .ShapeSQLTable {
return labelSurface {center : nv (n .Position .X , y , n .Position .Z ), width : n .Size .X , depth : n .Size .Z }
}
geometry := shape .NewShape (d2target .DSL_SHAPE_TO_SHAPE_TYPE [s .Type ], geo .NewBox (geo .NewPoint (0 , 0 ), float64 (s .Width ), float64 (s .Height )))
position := label .FromString (n .Metadata .Original .LabelPosition )
if position == label .Unset {
position = label .InsideMiddleCenter
}
box := geometry .GetInnerBox ()
if position .IsOutside () || position .IsBorder () {
box = geometry .GetBox ().Copy ()
if n .Metadata .Original .ThreeDee {
offsetY := float64 (d2target .THREE_DEE_OFFSET )
if n .Type == d2target .ShapeHexagon {
offsetY /= 2
}
box .TopLeft .Y -= offsetY
box .Height += offsetY
box .Width += d2target .THREE_DEE_OFFSET
} else if n .Metadata .Original .Multiple {
box .TopLeft .Y -= d2target .MULTIPLE_OFFSET
box .Height += d2target .MULTIPLE_OFFSET
box .Width += d2target .MULTIPLE_OFFSET
}
}
w , h := float64 (n .Metadata .Original .LabelWidth ), float64 (n .Metadata .Original .LabelHeight )
if w <= 0 || h <= 0 {
w , h = box .Width , box .Height
}
tl := position .GetPointOnBox (box , label .PADDING , w , h )
return labelSurface {center : nv (n .Position .X -n .Size .X /2 +(tl .X +w /2 )*sx , y , n .Position .Z -n .Size .Z /2 +(tl .Y +h /2 )*sz ), width : w * sx , depth : h * sz }
}
func nativeShapeProfile(s d2target .Shape , contours ...*[][]Vec ) ([]Vec , error ) {
w , h := float64 (max (1 , s .Width )), float64 (max (1 , s .Height ))
obj := shape .NewShape (d2target .DSL_SHAPE_TO_SHAPE_TYPE [s .Type ], geo .NewBox (geo .NewPoint (0 , 0 ), w , h ))
paths := shape .GetPathCommands (obj )
if len (paths ) == 0 {
if s .Type == d2target .ShapeCircle || s .Type == d2target .ShapeOval {
p := make ([]Vec , 96 )
for i := range p {
a := 2 * math .Pi * float64 (i ) / float64 (len (p ))
p [i ] = nv (w /2 +w /2 *math .Cos (a ), 0 , h /2 +h /2 *math .Sin (a ))
}
return p , nil
}
r := math .Min (float64 (s .BorderRadius ), math .Min (w , h )/2 )
if r <= 0 {
return []Vec {nv (0 , 0 , 0 ), nv (w , 0 , 0 ), nv (w , 0 , h ), nv (0 , 0 , h )}, nil
}
p := make ([]Vec , 0 , 36 )
for i , c := range []Vec {nv (w -r , 0 , r ), nv (w -r , 0 , h -r ), nv (r , 0 , h -r ), nv (r , 0 , r )} {
for k := 0 ; k <= 8 ; k ++ {
a := -math .Pi /2 + float64 (i )*math .Pi /2 + float64 (k )*math .Pi /16
p = append (p , nv (c .X +r *math .Cos (a ), 0 , c .Z +r *math .Sin (a )))
}
}
return p , nil
}
var best []Vec
bestArea := 0.
for _ , commands := range paths {
var p []Vec
var at Vec
closed := false
add := func (v Vec ) error {
if len (p ) >= 2048 {
return fmt .Errorf ("native shape profile exceeds 2048 vertices" )
}
if len (p ) == 0 || nlen (nsub (p [len (p )-1 ], v )) > 1e-8 {
p = append (p , v )
}
return nil
}
for _ , c := range commands {
end := nv (c .End .X , 0 , c .End .Y )
switch c .Kind {
case svg .PathCommandClose :
closed = true
case svg .PathCommandMove , svg .PathCommandLine :
if err := add (end ); err != nil {
return nil , err
}
at = end
case svg .PathCommandCubic :
c1 , c2 := nv (c .Control1 .X , 0 , c .Control1 .Y ), nv (c .Control2 .X , 0 , c .Control2 .Y )
steps := max (4 , min (64 , int (math .Ceil ((nlen (nsub (at , c1 ))+nlen (nsub (c1 , c2 ))+nlen (nsub (c2 , end )))/4 ))))
for j := 1 ; j <= steps ; j ++ {
t := float64 (j ) / float64 (steps )
u := 1 - t
v := nadd (nadd (nmul (at , u *u *u ), nmul (c1 , 3 *u *u *t )), nadd (nmul (c2 , 3 *u *t *t ), nmul (end , t *t *t )))
if err := add (v ); err != nil {
return nil , err
}
}
at = end
}
}
if len (p ) > 1 && nlen (nsub (p [0 ], p [len (p )-1 ])) < 1e-7 {
closed = true
p = p [:len (p )-1 ]
}
a := math .Abs (nativePolygonArea (p ))
if closed && len (p ) >= 3 && len (contours ) > 0 {
*contours [0 ] = append (*contours [0 ], p )
}
if a > bestArea {
best , bestArea = p , a
}
}
if len (best ) < 3 {
return nil , fmt .Errorf ("native shape %q has no closed profile" , s .Type )
}
return best , nil
}
func nativeShapeProfiles(s d2target .Shape ) ([][]Vec , error ) {
var contours [][]Vec
outer , err := nativeShapeProfile (s , &contours )
if err != nil {
return nil , err
}
if len (contours ) == 0 {
return [][]Vec {outer }, nil
}
inside := func (p Vec , polygon []Vec ) bool {
hit := false
for i , a := range polygon {
c := polygon [(i +1 )%len (polygon )]
dx , dz := c .X -a .X , c .Z -a .Z
if math .Abs ((p .X -a .X )*dz -(p .Z -a .Z )*dx ) < 1e-6 && p .X >= min (a .X , c .X )-1e-6 && p .X <= max (a .X , c .X )+1e-6 && p .Z >= min (a .Z , c .Z )-1e-6 && p .Z <= max (a .Z , c .Z )+1e-6 {
return true
}
if (a .Z > p .Z ) != (c .Z > p .Z ) && p .X < (c .X -a .X )*(p .Z -a .Z )/(c .Z -a .Z )+a .X {
hit = !hit
}
}
return hit
}
var result [][]Vec
for i , p := range contours {
contained := false
for j , q := range contours {
if i == j || math .Abs (nativePolygonArea (q )) <= math .Abs (nativePolygonArea (p )) {
continue
}
all := true
for _ , v := range p {
if !inside (v , q ) {
all = false
break
}
}
if all {
contained = true
break
}
}
if !contained {
result = append (result , p )
}
}
return result , nil
}
func nativePolygonArea(p []Vec ) float64 {
a := 0.
for i := range p {
j := (i + 1 ) % len (p )
a += p [i ].X *p [j ].Z - p [j ].X *p [i ].Z
}
return a / 2
}
func (b *meshBuilder ) extrudedProfile (p []Vec , base float64 , top , side *Material ) {
if len (p ) < 3 {
return
}
area := nativePolygonArea (p )
sign := 1.
if area < 0 {
sign = -1
}
edges , corners := make ([]Vec , len (p )), make ([]Vec , len (p ))
smooth := make ([]bool , len (p ))
for i , a := range p {
c := p [(i +1 )%len (p )]
edges [i ] = nunit (nv ((c .Z -a .Z )*sign , 0 , (a .X -c .X )*sign ))
}
for i , at := range p {
previous , next := (i +len (p )-1 )%len (p ), (i +1 )%len (p )
incoming , outgoing := nsub (at , p [previous ]), nsub (p [next ], at )
turn := (incoming .X *outgoing .Z - incoming .Z *outgoing .X ) * sign
if turn >= 0 && ndot (edges [previous ], edges [i ]) > math .Cos (math .Pi /6 ) {
smooth [i ] = true
corners [i ] = nunit (nadd (edges [previous ], edges [i ]))
}
}
normal := func (vertex , edge int ) Vec {
if smooth [vertex ] {
return corners [vertex ]
}
return edges [edge ]
}
for i , a := range p {
next := (i + 1 ) % len (p )
c := p [next ]
na , nc := normal (i , i ), normal (next , i )
if area < 0 {
a , c = c , a
na , nc = nc , na
}
d , e := a , c
d .Y , e .Y = base , base
va , vc := Vertex {Position : a , Normal : na }, Vertex {Position : c , Normal : nc }
vd , ve := Vertex {Position : d , Normal : na }, Vertex {Position : e , Normal : nc }
b .triangle (va , vc , vd , side , true )
b .triangle (vc , ve , vd , side , true )
}
if top == nil {
return
}
indices := make ([]int , len (p ))
for i := range indices {
indices [i ] = i
}
cross := func (a , c , d Vec ) float64 { return ((c .X -a .X )*(d .Z -a .Z ) - (c .Z -a .Z )*(d .X -a .X )) * sign }
work := 0
for len (indices ) > 2 {
found := false
for j , k := range indices {
a , c , d := p [indices [(j +len (indices )-1 )%len (indices )]], p [k ], p [indices [(j +1 )%len (indices )]]
if cross (a , c , d ) <= 1e-12 {
continue
}
empty := true
for _ , q := range indices {
work ++
if work %4096 == 0 {
if err := b .ctx .Err (); err != nil {
b .err = err
return
}
}
if work > 4_000_000 {
b .err = fmt .Errorf ("native shape triangulation exceeds work budget" )
return
}
if q == indices [(j +len (indices )-1 )%len (indices )] || q == k || q == indices [(j +1 )%len (indices )] {
continue
}
v := p [q ]
if cross (a , c , v ) > 1e-10 && cross (c , d , v ) > 1e-10 && cross (d , a , v ) > 1e-10 {
empty = false
break
}
}
if empty {
b .flat (a , d , c , top , true )
indices = append (indices [:j ], indices [j +1 :]...)
found = true
break
}
}
if !found {
return
}
}
}
The pages are generated with Golds v0.8.7 . (GOOS=linux GOARCH=amd64)
Golds is a Go 101 project developed by Tapir Liu .
PR and bug reports are welcome and can be submitted to the issue list .
Please follow @zigo_101 (reachable from the left QR code) to get the latest news of Golds .