2023-11-21 01:55:09 +00:00
package shapes
import (
2023-12-02 01:15:21 +00:00
"git.gammaspectra.live/WeebDataHoarder/swf-go/subtypes"
"git.gammaspectra.live/WeebDataHoarder/swf-go/types"
2023-11-21 01:55:09 +00:00
"git.gammaspectra.live/WeebDataHoarder/swf2ass-go/types/math"
"golang.org/x/exp/maps"
"slices"
)
type ShapeConverter struct {
2023-11-24 04:02:08 +00:00
Collection ObjectCollection
Styles StyleList
2023-11-21 01:55:09 +00:00
FillStyle0 * ActivePath
FillStyle1 * ActivePath
LineStyle * ActivePath
Position math . Vector2 [ types . Twip ]
Fills , Strokes PendingPathMap
Commands DrawPathList
Finished bool
}
2023-11-26 09:38:47 +00:00
func NewShapeConverter ( collection ObjectCollection , styles StyleList ) * ShapeConverter {
2023-11-21 01:55:09 +00:00
return & ShapeConverter {
2023-11-26 09:38:47 +00:00
Collection : collection ,
Styles : styles ,
Position : math . NewVector2 [ types . Twip ] ( 0 , 0 ) ,
Fills : make ( PendingPathMap ) ,
Strokes : make ( PendingPathMap ) ,
2023-11-21 01:55:09 +00:00
}
}
2023-11-26 09:38:47 +00:00
func NewMorphShapeConverter ( collection ObjectCollection , styles StyleList ) * ShapeConverter {
2023-11-21 01:55:09 +00:00
return & ShapeConverter {
2023-11-26 09:38:47 +00:00
Collection : collection ,
Styles : styles ,
Position : math . NewVector2 [ types . Twip ] ( 0 , 0 ) ,
Fills : make ( PendingPathMap ) ,
Strokes : make ( PendingPathMap ) ,
2023-11-21 01:55:09 +00:00
}
}
2023-11-26 09:38:47 +00:00
func ( c * ShapeConverter ) Convert ( elements subtypes . SHAPERECORDS ) DrawPathList {
2023-11-21 01:55:09 +00:00
if c . Finished {
2023-11-26 09:38:47 +00:00
return nil
2023-11-21 01:55:09 +00:00
}
2023-11-26 09:38:47 +00:00
for _ , e := range elements {
c . HandleNode ( e )
}
2023-11-21 17:38:17 +00:00
2023-11-26 09:38:47 +00:00
c . FlushLayer ( )
2023-11-21 01:55:09 +00:00
2023-11-26 09:38:47 +00:00
return c . Commands
}
// ConvertMorph
// We step through both the start records and end records, interpolating edges pairwise.
// Fill style/line style changes should only appear in the start records.
// However, StyleChangeRecord move_to can appear it both start and end records,
// and not necessarily in matching pairs; therefore, we have to keep track of the pen position
// in case one side is missing a move_to; it will implicitly use the last pen position.
func ( c * ShapeConverter ) ConvertMorph ( start , end subtypes . SHAPERECORDS ) ( startList , endList subtypes . SHAPERECORDS ) {
if c . Finished {
return nil , nil
}
2023-11-21 01:55:09 +00:00
2023-11-26 09:38:47 +00:00
var startPos , endPos math . Vector2 [ types . Twip ]
updatePos := func ( v math . Vector2 [ types . Twip ] , s subtypes . SHAPERECORD ) math . Vector2 [ types . Twip ] {
switch s := s . ( type ) {
case * subtypes . StraightEdgeRecord :
v = v . AddVector ( math . NewVector2 ( s . DeltaX , s . DeltaY ) )
case * subtypes . CurvedEdgeRecord :
v = v . AddVector ( math . NewVector2 ( s . ControlDeltaX + s . AnchorDeltaX , s . ControlDeltaY + s . AnchorDeltaY ) )
case * subtypes . StyleChangeRecord :
if s . Flag . MoveTo {
v = math . NewVector2 ( s . MoveDeltaX , s . MoveDeltaY )
2023-11-21 01:55:09 +00:00
}
}
2023-11-26 09:38:47 +00:00
return v
}
2023-11-21 01:55:09 +00:00
2023-11-26 09:38:47 +00:00
for len ( start ) > 0 {
startPtr := start [ 0 ]
endPtr := end [ 0 ]
2023-11-21 01:55:09 +00:00
2023-11-26 09:38:47 +00:00
if startPtr . RecordType ( ) == endPtr . RecordType ( ) {
switch s := startPtr . ( type ) {
2023-11-21 01:55:09 +00:00
case * subtypes . StyleChangeRecord :
2023-11-26 09:38:47 +00:00
if s . Flag . MoveTo {
startPos = math . NewVector2 ( s . MoveDeltaX , s . MoveDeltaY )
2023-11-21 01:55:09 +00:00
}
2023-11-26 09:38:47 +00:00
e := endPtr . ( * subtypes . StyleChangeRecord )
endRecord := * s
endRecord . Flag . MoveTo = e . Flag . MoveTo
endRecord . MoveDeltaX = e . MoveDeltaX
endRecord . MoveDeltaY = e . MoveDeltaY
if e . Flag . MoveTo {
endPos = math . NewVector2 ( e . MoveDeltaX , e . MoveDeltaY )
2023-11-21 01:55:09 +00:00
}
2023-11-26 09:38:47 +00:00
startList = append ( startList , s )
endList = append ( endList , & endRecord )
2023-11-21 01:55:09 +00:00
2023-11-26 09:38:47 +00:00
start = start [ 1 : ]
end = end [ 1 : ]
default :
startList = append ( startList , startPtr )
endList = append ( endList , endPtr )
2023-11-21 01:55:09 +00:00
2023-11-26 09:38:47 +00:00
start = start [ 1 : ]
end = end [ 1 : ]
}
} else {
if s , ok := startPtr . ( * subtypes . StyleChangeRecord ) ; ok {
endRecord := * s
if s . Flag . MoveTo {
startPos = math . NewVector2 ( s . MoveDeltaX , s . MoveDeltaY )
endRecord . MoveDeltaX = endPos . X
endRecord . MoveDeltaY = endPos . Y
2023-11-21 01:55:09 +00:00
}
2023-11-26 09:38:47 +00:00
startList = append ( startList , startPtr )
endList = append ( endList , & endRecord )
startPos = updatePos ( startPos , startPtr )
start = start [ 1 : ]
} else if e , ok := endPtr . ( * subtypes . StyleChangeRecord ) ; ok {
startRecord := * e
if e . Flag . MoveTo {
endPos = math . NewVector2 ( e . MoveDeltaX , e . MoveDeltaY )
startRecord . MoveDeltaX = startPos . X
startRecord . MoveDeltaY = startPos . Y
2023-11-21 01:55:09 +00:00
}
2023-11-26 09:38:47 +00:00
startList = append ( startList , & startRecord )
endList = append ( endList , endPtr )
endPos = updatePos ( endPos , startPtr )
end = end [ 1 : ]
2023-11-21 01:55:09 +00:00
} else {
2023-11-26 09:38:47 +00:00
startList = append ( startList , startPtr )
endList = append ( endList , endPtr )
startPos = updatePos ( startPos , startPtr )
endPos = updatePos ( endPos , endPtr )
2023-11-21 01:55:09 +00:00
2023-11-26 09:38:47 +00:00
start = start [ 1 : ]
end = end [ 1 : ]
2023-11-21 01:55:09 +00:00
}
}
}
2023-11-26 09:38:47 +00:00
if len ( end ) > 0 {
panic ( "did not complete" )
}
return
2023-11-21 01:55:09 +00:00
}
func ( c * ShapeConverter ) HandleNode ( node subtypes . SHAPERECORD ) {
switch node := node . ( type ) {
case * subtypes . StyleChangeRecord :
if node . Flag . MoveTo {
moveTo := math . NewVector2 [ types . Twip ] ( node . MoveDeltaX , node . MoveDeltaY )
c . Position = moveTo
c . FlushPaths ( )
}
if node . Flag . NewStyles {
c . FlushLayer ( )
2023-11-24 04:02:08 +00:00
c . Styles = StyleListFromSWFItems ( c . Collection , node . FillStyles , node . LineStyles )
2023-11-21 01:55:09 +00:00
}
if node . Flag . FillStyle1 {
if c . FillStyle1 != nil {
c . Fills . MergePath ( c . FillStyle1 , true )
}
if node . FillStyle1 > 0 {
c . FillStyle1 = NewActivePath ( int ( node . FillStyle1 ) , c . Position )
} else {
c . FillStyle1 = nil
}
}
if node . Flag . FillStyle0 {
if c . FillStyle0 != nil {
if ! c . FillStyle0 . Segment . IsEmpty ( ) {
c . FillStyle0 . Flip ( )
c . Fills . MergePath ( c . FillStyle0 , true )
}
}
if node . FillStyle0 > 0 {
c . FillStyle0 = NewActivePath ( int ( node . FillStyle0 ) , c . Position )
} else {
c . FillStyle0 = nil
}
}
if node . Flag . LineStyle {
if c . LineStyle != nil {
c . Strokes . MergePath ( c . LineStyle , false )
}
if node . LineStyle > 0 {
c . LineStyle = NewActivePath ( int ( node . LineStyle ) , c . Position )
} else {
c . LineStyle = nil
}
}
case * subtypes . StraightEdgeRecord :
to := c . Position . AddVector ( math . NewVector2 [ types . Twip ] ( node . DeltaX , node . DeltaY ) )
c . VisitPoint ( to , false )
c . Position = to
case * subtypes . CurvedEdgeRecord :
control := c . Position . AddVector ( math . NewVector2 [ types . Twip ] ( node . ControlDeltaX , node . ControlDeltaY ) )
anchor := control . AddVector ( math . NewVector2 [ types . Twip ] ( node . AnchorDeltaX , node . AnchorDeltaY ) )
c . VisitPoint ( control , true )
c . VisitPoint ( anchor , false )
c . Position = anchor
case * subtypes . EndShapeRecord :
c . Finished = true
}
}
func ( c * ShapeConverter ) VisitPoint ( pos math . Vector2 [ types . Twip ] , isBezierControlPoint bool ) {
2023-11-24 08:59:01 +00:00
point := VisitedPoint [ types . Twip ] {
2023-11-21 01:55:09 +00:00
Pos : pos ,
IsBezierControl : isBezierControlPoint ,
}
if c . FillStyle0 != nil {
c . FillStyle0 . AddPoint ( point )
}
if c . FillStyle1 != nil {
c . FillStyle1 . AddPoint ( point )
}
if c . LineStyle != nil {
c . LineStyle . AddPoint ( point )
}
}
func ( c * ShapeConverter ) FlushPaths ( ) {
if c . FillStyle1 != nil {
c . Fills . MergePath ( c . FillStyle1 , true )
c . FillStyle1 = NewActivePath ( c . FillStyle1 . StyleId , c . Position )
}
if c . FillStyle0 != nil {
if ! c . FillStyle0 . Segment . IsEmpty ( ) {
c . FillStyle0 . Flip ( )
c . Fills . MergePath ( c . FillStyle0 , true )
}
c . FillStyle0 = NewActivePath ( c . FillStyle0 . StyleId , c . Position )
}
if c . LineStyle != nil {
c . Strokes . MergePath ( c . LineStyle , false )
c . LineStyle = NewActivePath ( c . LineStyle . StyleId , c . Position )
}
}
func ( c * ShapeConverter ) FlushLayer ( ) {
c . FlushPaths ( )
c . FillStyle0 = nil
c . FillStyle1 = nil
c . LineStyle = nil
fillsKeys := maps . Keys ( c . Fills )
slices . Sort ( fillsKeys )
for _ , styleId := range fillsKeys {
path := c . Fills [ styleId ]
if styleId <= 0 || styleId > len ( c . Styles . FillStyles ) {
panic ( "should not happen" )
}
style := c . Styles . GetFillStyle ( styleId - 1 )
if style == nil {
panic ( "should not happen" )
}
2023-11-26 21:45:07 +00:00
c . Commands = append ( c . Commands , DrawPathFill ( style , path . GetShape ( ) ) )
2023-11-21 01:55:09 +00:00
}
clear ( c . Fills )
strokesKeys := maps . Keys ( c . Strokes )
slices . Sort ( strokesKeys )
for _ , styleId := range strokesKeys {
path := c . Strokes [ styleId ]
if styleId <= 0 || styleId > len ( c . Styles . LineStyles ) {
panic ( "should not happen" )
}
style := c . Styles . GetLineStyle ( styleId - 1 )
if style == nil {
panic ( "should not happen" )
}
//wrap around all segments, even if closed. ASS does NOT like them otherwise. so we draw everything backwards to have border around the line, not just on one side
//TODO: using custom line borders later using fills this can be removed
2023-11-24 08:59:01 +00:00
var newSegments PendingPath [ types . Twip ]
2023-11-21 01:55:09 +00:00
for _ , segment := range * path {
other := slices . Clone ( * segment )
other . Flip ( )
segment . Merge ( other )
newSegments . MergePath ( segment , false )
}
if len ( newSegments ) > 0 {
//Reduce width of line style to account for double border
//TODO: using custom line borders later using fills this can be removed
fixedStyle := * style
fixedStyle . Width /= 2
2023-11-26 21:45:07 +00:00
c . Commands = append ( c . Commands , DrawPathStroke ( & fixedStyle , newSegments . GetShape ( ) ) )
2023-11-21 01:55:09 +00:00
}
2023-11-21 17:38:17 +00:00
//TODO: leave this as-is and create a fill in renderer
2023-11-21 01:55:09 +00:00
}
clear ( c . Strokes )
}