transform

package
v0.2.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Dec 23, 2025 License: Apache-2.0 Imports: 6 Imported by: 0

Documentation

Overview

Package transform provides post-layout transformations for tower rendering.

Overview

After computing block positions with layout.Build, this package provides transformations that modify the layout for improved visual output:

  • MergeSubdividers: Combines subdivider blocks into continuous vertical columns
  • Randomize: Applies random width variation for a natural, hand-drawn look

These transformations are applied after layout computation but before rendering to SVG/JSON output.

Merging Subdividers

When long edges are subdivided by dag/transform.Subdivide, each segment becomes a separate block. MergeSubdividers combines these into single continuous vertical blocks for cleaner visualization:

layout := layout.Build(g, width, height, opts...)
layout = transform.MergeSubdividers(layout, g)

Blocks are grouped by their MasterID (the original node they were split from) and their horizontal position. Multiple groups can exist for the same master if the subdivider chain splits across different horizontal positions.

Randomization

Randomize applies controlled random variation to block widths, creating a checkerboard pattern that mimics hand-drawn diagrams:

layout = transform.Randomize(layout, g, seed, nil)

The randomization:

  • Shrinks alternating rows to create visual rhythm
  • Uses a seed for reproducible "randomness"
  • Ensures minimum overlap between connected blocks
  • Respects minimum block width and gap constraints

Options

Options configures randomization behavior:

  • WidthShrink: Maximum shrink factor (0-1, default 0.85)
  • MinBlockWidth: Minimum allowed width (default 30px)
  • MinGap: Minimum gap between blocks (default 5px)
  • MinOverlap: Minimum overlap for connected blocks (default 10px)

Pipeline Position

These transformations fit in the rendering pipeline:

DAG → dag/transform.Normalize → ordering → layout.Build → [this package] → sink.RenderSVG

layout.Build: github.com/matzehuels/stacktower/pkg/render/tower/layout.Build dag/transform.Subdivide: github.com/matzehuels/stacktower/pkg/dag/transform.Subdivide

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func MergeSubdividers

func MergeSubdividers(l layout.Layout, g *dag.DAG) layout.Layout

MergeSubdividers combines subdivider blocks into continuous vertical columns. Subdivider nodes (created by dag/transform.Subdivide to break long edges) are grouped by their MasterID and horizontal position, then merged into single blocks spanning from top to bottom.

This creates cleaner visuals where a package's vertical "column" is rendered as one continuous block rather than separate segments per row.

The returned layout has subdivider nodes removed from RowOrders and replaced with merged blocks keyed by their master ID.

Example
package main

import (
	"fmt"

	"github.com/matzehuels/stacktower/pkg/dag"
	"github.com/matzehuels/stacktower/pkg/render/tower/layout"
	"github.com/matzehuels/stacktower/pkg/render/tower/transform"
)

func main() {
	// Create a graph with subdivider nodes (from edge subdivision)
	g := dag.New(nil)
	_ = g.AddNode(dag.Node{ID: "app", Row: 0, Kind: dag.NodeKindRegular})
	_ = g.AddNode(dag.Node{ID: "lib", Row: 3, Kind: dag.NodeKindRegular})

	// Subdivider nodes break the long edge into segments
	_ = g.AddNode(dag.Node{
		ID:       "lib#1",
		Row:      1,
		Kind:     dag.NodeKindSubdivider,
		MasterID: "lib",
	})
	_ = g.AddNode(dag.Node{
		ID:       "lib#2",
		Row:      2,
		Kind:     dag.NodeKindSubdivider,
		MasterID: "lib",
	})

	_ = g.AddEdge(dag.Edge{From: "app", To: "lib#1"})
	_ = g.AddEdge(dag.Edge{From: "lib#1", To: "lib#2"})
	_ = g.AddEdge(dag.Edge{From: "lib#2", To: "lib"})

	// Create a layout with separate blocks for each subdivider
	l := layout.Layout{
		FrameWidth:  800,
		FrameHeight: 600,
		Blocks: map[string]layout.Block{
			"app":   {NodeID: "app", Left: 100, Right: 200, Top: 50, Bottom: 0},
			"lib#1": {NodeID: "lib#1", Left: 100, Right: 200, Top: 150, Bottom: 100},
			"lib#2": {NodeID: "lib#2", Left: 100, Right: 200, Top: 250, Bottom: 200},
			"lib":   {NodeID: "lib", Left: 100, Right: 200, Top: 350, Bottom: 300},
		},
		RowOrders: map[int][]string{
			0: {"app"},
			1: {"lib#1"},
			2: {"lib#2"},
			3: {"lib"},
		},
	}

	// Merge subdividers into a single continuous block
	merged := transform.MergeSubdividers(l, g)

	// The subdivider segments are now merged
	fmt.Printf("Original blocks: %d\n", len(l.Blocks))
	fmt.Printf("Merged blocks: %d\n", len(merged.Blocks))
	fmt.Printf("Subdividers removed from row orders: %v\n", len(merged.RowOrders[1]) == 0)
}
Output:

Original blocks: 4
Merged blocks: 2
Subdividers removed from row orders: true

func Randomize

func Randomize(l layout.Layout, g *dag.DAG, seed uint64, opts *Options) layout.Layout

Randomize applies controlled random variation to block widths. It creates a checkerboard pattern by shrinking alternating rows, which mimics hand-drawn diagrams and adds visual interest.

The seed ensures reproducible randomness—the same seed produces identical layouts. Pass nil for opts to use defaults.

After shrinking, the function ensures connected blocks maintain minimum overlap so dependency edges remain visually clear.

Example
package main

import (
	"fmt"

	"github.com/matzehuels/stacktower/pkg/dag"
	"github.com/matzehuels/stacktower/pkg/render/tower/layout"
	"github.com/matzehuels/stacktower/pkg/render/tower/transform"
)

func main() {
	// Create a simple layout
	g := dag.New(nil)
	_ = g.AddNode(dag.Node{ID: "app", Row: 0})
	_ = g.AddNode(dag.Node{ID: "lib", Row: 1})
	_ = g.AddEdge(dag.Edge{From: "app", To: "lib"})

	l := layout.Layout{
		FrameWidth:  800,
		FrameHeight: 400,
		Blocks: map[string]layout.Block{
			"app": {NodeID: "app", Left: 100, Right: 300, Top: 100, Bottom: 0},
			"lib": {NodeID: "lib", Left: 100, Right: 300, Top: 300, Bottom: 200},
		},
		RowOrders: map[int][]string{
			0: {"app"},
			1: {"lib"},
		},
	}

	// Apply randomization with a fixed seed for reproducibility
	randomized := transform.Randomize(l, g, 42, nil)

	// Block widths are now varied (but deterministic with same seed)
	// Note: Row 0 is not randomized (the root), only subsequent rows are
	appWidth := randomized.Blocks["app"].Right - randomized.Blocks["app"].Left
	libWidth := randomized.Blocks["lib"].Right - randomized.Blocks["lib"].Left

	fmt.Printf("Randomization applied\n")
	fmt.Printf("App width changed: %v\n", appWidth != 200)
	fmt.Printf("Lib width changed: %v\n", libWidth != 200)
}
Output:

Randomization applied
App width changed: false
Lib width changed: true
Example (CustomOptions)
package main

import (
	"fmt"

	"github.com/matzehuels/stacktower/pkg/dag"
	"github.com/matzehuels/stacktower/pkg/render/tower/layout"
	"github.com/matzehuels/stacktower/pkg/render/tower/transform"
)

func main() {
	// Create a layout
	g := dag.New(nil)
	_ = g.AddNode(dag.Node{ID: "service", Row: 0})
	_ = g.AddNode(dag.Node{ID: "database", Row: 1})
	_ = g.AddEdge(dag.Edge{From: "service", To: "database"})

	l := layout.Layout{
		FrameWidth:  800,
		FrameHeight: 400,
		Blocks: map[string]layout.Block{
			"service":  {NodeID: "service", Left: 100, Right: 400, Top: 100, Bottom: 0},
			"database": {NodeID: "database", Left: 100, Right: 400, Top: 300, Bottom: 200},
		},
		RowOrders: map[int][]string{
			0: {"service"},
			1: {"database"},
		},
	}

	// Use custom options for more aggressive randomization
	opts := &transform.Options{
		WidthShrink:   0.7,  // More shrinking (default 0.85)
		MinBlockWidth: 50.0, // Larger minimum (default 30)
		MinGap:        10.0, // Larger gaps (default 5)
		MinOverlap:    20.0, // More overlap required (default 10)
	}

	randomized := transform.Randomize(l, g, 123, opts)

	fmt.Printf("Custom randomization applied\n")
	fmt.Printf("Blocks modified: %d\n", len(randomized.Blocks))
}
Output:

Custom randomization applied
Blocks modified: 2
Example (DeterministicWithSeed)
package main

import (
	"fmt"

	"github.com/matzehuels/stacktower/pkg/dag"
	"github.com/matzehuels/stacktower/pkg/render/tower/layout"
	"github.com/matzehuels/stacktower/pkg/render/tower/transform"
)

func main() {
	// Demonstrate that the same seed produces identical results
	g := dag.New(nil)
	_ = g.AddNode(dag.Node{ID: "a", Row: 0})
	_ = g.AddNode(dag.Node{ID: "b", Row: 1})
	_ = g.AddEdge(dag.Edge{From: "a", To: "b"})

	l := layout.Layout{
		FrameWidth:  800,
		FrameHeight: 400,
		Blocks: map[string]layout.Block{
			"a": {NodeID: "a", Left: 100, Right: 300, Top: 100, Bottom: 0},
			"b": {NodeID: "b", Left: 100, Right: 300, Top: 300, Bottom: 200},
		},
		RowOrders: map[int][]string{
			0: {"a"},
			1: {"b"},
		},
	}

	// Apply randomization twice with the same seed
	result1 := transform.Randomize(l, g, 999, nil)
	result2 := transform.Randomize(l, g, 999, nil)

	// Results are identical
	width1 := result1.Blocks["a"].Right - result1.Blocks["a"].Left
	width2 := result2.Blocks["a"].Right - result2.Blocks["a"].Left

	fmt.Printf("Same seed produces identical results: %v\n", width1 == width2)
}
Output:

Same seed produces identical results: true

Types

type Options

type Options struct {
	// WidthShrink is the maximum shrink factor applied to blocks (0-1).
	// Higher values create more width variation. Default: 0.85.
	WidthShrink float64

	// MinBlockWidth is the minimum allowed block width in pixels.
	// Blocks will not shrink below this size. Default: 30.
	MinBlockWidth float64

	// MinGap is the minimum gap between adjacent blocks in pixels. Default: 5.
	MinGap float64

	// MinOverlap is the minimum horizontal overlap required between connected
	// blocks (parent-child pairs). Blocks are expanded if needed. Default: 10.
	MinOverlap float64
}

Options configures the randomization behavior for Randomize.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL