Skip to content

Convert recursive functions to iterative traversal #76

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Apr 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 43 additions & 30 deletions SwiftDraw/LayerTree.Builder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -91,47 +91,60 @@ extension LayerTree {
return transform
}

func makeLayer(from element: DOM.GraphicsElement, inheriting previousState: State) -> Layer {
func makeLayer(from root: DOM.GraphicsElement, inheriting previousState: State) -> Layer {
var stack: [(DOM.GraphicsElement, State, Layer?)] = [(root, previousState, nil)]
var resultLayer: Layer? = nil

while let (currentElement, currentState, parentLayer) = stack.popLast() {
let (layer, newState) = makeBaseLayer(from: currentElement, inheriting: currentState)

if let contents = makeContents(from: currentElement, with: newState) {
layer.appendContents(contents)
} else if let container = currentElement as? ContainerElement {
// Push children in reverse so they are processed in the original order
for child in container.childElements.reversed() {
stack.append((child, newState, layer))
}
}

if let parent = parentLayer {
parent.appendContents(.layer(layer))

if let svg = currentElement as? DOM.SVG {
let viewBox = svg.viewBox ?? DOM.SVG.ViewBox(x: 0, y: 0, width: .init(svg.width), height: .init(svg.height))
let bounds = LayerTree.Rect(x: viewBox.x, y: viewBox.y, width: viewBox.width, height: viewBox.height)
layer.clip = [ClipShape(shape: .rect(within: bounds, radii: .zero), transform: .identity)]
layer.transform = Builder.makeTransform(
x: svg.x,
y: svg.y,
viewBox: svg.viewBox,
width: svg.width,
height: svg.height
)
}
} else {
// This must be the top-level root layer
resultLayer = layer
}
}

return resultLayer!
}

func makeBaseLayer(from element: DOM.GraphicsElement, inheriting previousState: State) -> (Layer, State) {
let state = createState(for: element, inheriting: previousState)
let attributes = element.attributes
let l = Layer()
l.class = element.class
guard state.display != .none else { return l }
guard state.display != .none else { return (l, state) }

l.transform = Builder.createTransforms(from: attributes.transform ?? [])
l.clip = makeClipShapes(for: element)
l.clipRule = attributes.clipRule
l.mask = createMaskLayer(for: element)
l.opacity = state.opacity
l.contents = makeAllContents(from: element, with: state)
l.filters = makeFilters(for: state)
return l
}

func makeChildLayer(from element: DOM.GraphicsElement, inheriting previousState: State) -> Layer {
if let svg = element as? DOM.SVG {
let layer = makeLayer(svg: svg, inheriting: previousState)
let viewBox = svg.viewBox ?? DOM.SVG.ViewBox(x: 0, y: 0, width: .init(svg.width), height: .init(svg.height))
let bounds = LayerTree.Rect(x: viewBox.x, y: viewBox.y, width: viewBox.width, height: viewBox.height)
layer.clip = [ClipShape(shape: .rect(within: bounds, radii: .zero), transform: .identity)]
return layer
} else {
return makeLayer(from: element, inheriting: previousState)
}
}

func makeAllContents(from element: DOM.GraphicsElement, with state: State) -> [Layer.Contents] {
var all = [Layer.Contents]()
if let contents = makeContents(from: element, with: state) {
all.append(contents)
}
else if let container = element as? ContainerElement {
container.childElements.forEach{
let contents = Layer.Contents.layer(makeChildLayer(from: $0, inheriting: state))
all.append(contents)
}
}
return all
return (l, state)
}

func makeContents(from element: DOM.GraphicsElement, with state: State) -> Layer.Contents? {
Expand Down
98 changes: 59 additions & 39 deletions SwiftDraw/LayerTree.CommandGenerator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,56 +52,76 @@ extension LayerTree {
self.options = options
}

func renderCommands(for layer: Layer, colorConverter: any ColorConverter) -> [RendererCommand<P.Types>] {
func renderCommands(for l: Layer, colorConverter c: any ColorConverter) -> [RendererCommand<P.Types>] {
var commands = [RendererCommand<P.Types>]()

let state = makeCommandState(for: layer, colorConverter: colorConverter)

guard state.hasContents else { return commands }

if state.hasFilters {
logUnsupportedFilters(layer.filters)
}

if state.hasOpacity || state.hasTransform || state.hasClip || state.hasMask {
commands.append(.pushState)
}

commands.append(contentsOf: renderCommands(forTransforms: layer.transform))
commands.append(contentsOf: renderCommands(forOpacity: layer.opacity))
commands.append(contentsOf: renderCommands(forClip: layer.clip, using: layer.clipRule))

if state.hasMask {
commands.append(.pushTransparencyLayer)
}

//render all of the layer contents
for contents in layer.contents {
switch makeRenderContents(for: contents, colorConverter: state.colorConverter) {
case let .simple(cmd):
var stack: [RenderStep] = [
.beginLayer(l, c)
]

while let step = stack.popLast() {
switch step {
case let .beginLayer(layer, colorConverter):
let state = makeCommandState(for: layer, colorConverter: colorConverter)

//guard state.hasContents else { continue }
stack.append(.endLayer(layer, state))

if state.hasFilters {
logUnsupportedFilters(layer.filters)
}

if state.hasOpacity || state.hasTransform || state.hasClip || state.hasMask {
commands.append(.pushState)
}

commands.append(contentsOf: renderCommands(forTransforms: layer.transform))
commands.append(contentsOf: renderCommands(forOpacity: layer.opacity))
commands.append(contentsOf: renderCommands(forClip: layer.clip, using: layer.clipRule))

if state.hasMask {
commands.append(.pushTransparencyLayer)
}

//push render of all of the layer contents in reverse order
for contents in layer.contents.reversed() {
switch makeRenderContents(for: contents, colorConverter: colorConverter) {
case let .simple(cmd):
stack.append(.contents(cmd))
case let .layer(layer):
stack.append(.beginLayer(layer, colorConverter))
}
}

case let .contents(cmd):
commands.append(contentsOf: cmd)
case let .layer(layer):
commands.append(contentsOf: renderCommands(for: layer, colorConverter: colorConverter))
}
}

//render apply mask
if state.hasMask {
commands.append(contentsOf: renderCommands(forMask: layer.mask))
commands.append(.popTransparencyLayer)
}
case let .endLayer(layer, state):
//render apply mask
if state.hasMask {
commands.append(contentsOf: renderCommands(forMask: layer.mask))
commands.append(.popTransparencyLayer)
}

if state.hasOpacity {
commands.append(.popTransparencyLayer)
}
if state.hasOpacity {
commands.append(.popTransparencyLayer)
}

if state.hasOpacity || state.hasTransform || state.hasClip || state.hasMask {
commands.append(.popState)
if state.hasOpacity || state.hasTransform || state.hasClip || state.hasMask {
commands.append(.popState)
}
}
}

return commands
}

enum RenderStep {
case beginLayer(LayerTree.Layer, any ColorConverter)
case contents([RendererCommand<P.Types>])
case endLayer(LayerTree.Layer, CommandState)
}

struct CommandState {
var hasOpacity: Bool
var hasTransform: Bool
Expand Down
32 changes: 30 additions & 2 deletions SwiftDraw/LayerTree.Layer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,6 @@ extension LayerTree {
func appendContents(_ contents: Contents) {
switch contents {
case .layer(let l):
guard l.contents.isEmpty == false else { return }

//if layer is simple, we can ignore all other properties
if let simple = l.simpleContents {
self.contents.append(simple)
Expand All @@ -63,6 +61,7 @@ extension LayerTree {
var simpleContents: Contents? {
guard self.contents.count == 1,
let first = self.contents.first,
`class` == nil,
opacity == 1.0,
transform == [],
clip == [],
Expand Down Expand Up @@ -154,3 +153,32 @@ extension LayerTree {
var anchor: DOM.TextAnchor
}
}

extension LayerTree.Layer.Contents: CustomDebugStringConvertible {

var debugDescription: String {
switch self {
case .image:
return "image"
case .layer(let l):
return "layer-\(l.contents.map(\.debugDescription).joined(separator: ", "))"
case .shape(let s, _, _):
return "shape-\(s.debugDescription)"
case .text:
return "text"
}
}
}

extension LayerTree.Shape: CustomDebugStringConvertible {

var debugDescription: String {
switch self {
case .ellipse: return "ellipse"
case .rect: return "rect"
case .line: return "line"
case .path: return "path"
case .polygon: return "polygon"
}
}
}
51 changes: 22 additions & 29 deletions SwiftDraw/Parser.XML.Element.swift
Original file line number Diff line number Diff line change
Expand Up @@ -111,33 +111,30 @@ extension XMLParser {
return ge
}

func parseContainerChildren(_ e: XML.Element) throws -> [DOM.GraphicsElement] {
guard e.name == "svg" ||
e.name == "clipPath" ||
e.name == "pattern" ||
e.name == "mask" ||
e.name == "defs" ||
e.name == "switch" ||
e.name == "g" ||
e.name == "a" else {
throw Error.invalid
}
func parseGraphicsElements(_ elements: [XML.Element]) throws -> [DOM.GraphicsElement] {
var result = [DOM.GraphicsElement]()
var stack: [(XML.Element, parent: ContainerElement?)] = elements
.reversed()
.map { ($0, parent: nil) }

while let (element, parent) = stack.popLast() {
guard let ge = try parseGraphicsElement(element) else {
continue
}

var children = [DOM.GraphicsElement]()

for n in e.children {
do {
if let ge = try parseGraphicsElement(n) {
children.append(ge)
}
} catch let error {
if let parseError = parseError(for: error, parsing: n, with: options) {
throw parseError
}
if var parent {
parent.childElements.append(ge)
} else {
result.append(ge)
}

if let container = ge as? ContainerElement {
stack.append(contentsOf: element.children.reversed().map { ($0, container) })
}

}

return children
return result
}

func parseError(for error: Swift.Error, parsing element: XML.Element, with options: Options) -> XMLParser.Error? {
Expand Down Expand Up @@ -165,19 +162,15 @@ extension XMLParser {
throw Error.invalid
}

let group = DOM.Group()
group.childElements = try parseContainerChildren(e)
return group
return DOM.Group()
}

func parseSwitch(_ e: XML.Element) throws -> DOM.Switch {
guard e.name == "switch" else {
throw Error.invalid
}

let node = DOM.Switch()
node.childElements = try parseContainerChildren(e)
return node
return DOM.Switch()
}

func parseAttributes(_ e: XML.Element) throws -> Attributes {
Expand Down
10 changes: 5 additions & 5 deletions SwiftDraw/Parser.XML.SVG.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ extension XMLParser {
let svg = DOM.SVG(width: DOM.Length(w), height: DOM.Length(h))
svg.x = try att.parseCoordinate("x")
svg.y = try att.parseCoordinate("y")
svg.childElements = try parseContainerChildren(e)
svg.childElements = try parseGraphicsElements(e.children)
svg.viewBox = try parseViewBox(try att.parseString("viewBox"))

svg.defs = try parseSVGDefs(e)
Expand Down Expand Up @@ -121,7 +121,7 @@ extension XMLParser {
}

var defs = Dictionary<String, DOM.GraphicsElement>()
let elements = try parseContainerChildren(e)
let elements = try parseGraphicsElements(e.children)

for e in elements {
guard let id = e.id else {
Expand Down Expand Up @@ -153,7 +153,7 @@ extension XMLParser {
let att = try parseAttributes(e)
let id: String = try att.parseString("id")

let children = try parseContainerChildren(e)
let children = try parseGraphicsElements(e.children)
return DOM.ClipPath(id: id, childElements: children)
}

Expand All @@ -176,7 +176,7 @@ extension XMLParser {
let att = try parseAttributes(e)
let id: String = try att.parseString("id")

let children = try parseContainerChildren(e)
let children = try parseGraphicsElements(e.children)
return DOM.Mask(id: id, childElements: children)
}

Expand All @@ -198,7 +198,7 @@ extension XMLParser {

let att = try parseAttributes(e)
var pattern = try parsePattern(att)
pattern.childElements = try parseContainerChildren(e)
pattern.childElements = try parseGraphicsElements(e.children)
return pattern
}
}
1 change: 0 additions & 1 deletion SwiftDraw/Parser.XML.Text.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ extension XMLParser {
func parseAnchor(_ att: AttributeParser, element: XML.Element) throws -> DOM.Anchor? {
let anchor = DOM.Anchor()
anchor.href = try att.parseUrl("href")
anchor.childElements = try parseContainerChildren(element)
return anchor
}

Expand Down
Loading