-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathlayout.go
More file actions
294 lines (251 loc) · 8.63 KB
/
layout.go
File metadata and controls
294 lines (251 loc) · 8.63 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
package main
import (
"fmt"
"strings"
)
// Layout dimension constants
const (
CHAR_WIDTH_PIXELS = 16.0 // Approximate width per character for text wrapping
MIN_CHARS_PER_LINE = 1 // Minimum characters per line in wrapped text
MAX_TEXT_LINES = 3 // Maximum lines for wrapped text
)
// Coordinate conversion helpers
// gridToPixelX converts grid X coordinate to pixel X coordinate
func gridToPixelX(gridX int, dims Dimensions, config DiagramConfig) int {
return dims.LeftMargin + int(float64(gridX-1)*dims.CellUnits*float64(config.GridUnit))
}
// gridToPixelY converts grid Y coordinate to pixel Y coordinate
func gridToPixelY(gridY int, dims Dimensions, config DiagramConfig) int {
return dims.TopMargin + int(float64(gridY-1)*dims.VerticalCellUnits*float64(config.GridUnit))
}
// calculateBoxWidth calculates the pixel width of a box based on its grid width
func calculateBoxWidth(gridWidth float64, dims Dimensions, config DiagramConfig) int {
return int((gridWidth*dims.CellUnits - config.GapUnits*config.Stretch) * float64(config.GridUnit))
}
// calculateBoxHeight calculates the pixel height of a box based on its grid height
func calculateBoxHeight(gridHeight int, config DiagramConfig) int {
return gridHeight * config.GridUnit
}
// calculateTouchExtension calculates the extension amount for TouchLeft boxes
func calculateTouchExtension(config DiagramConfig) int {
return int((config.GapUnits / 2.0) * float64(config.GridUnit))
}
// groupPadding is the padding (in pixels) around boxes within a group rectangle
const groupPadding = 15
// Layout converts a DiagramSpec into a concrete Diagram with pixel coordinates
func Layout(spec *DiagramSpec, config DiagramConfig, legend []LegendEntry, groups []GroupDef, arrowFlow string) (*Diagram, map[string]BoxData) {
// Find maximum grid positions
maxGridX := 0
maxGridY := 0
for _, box := range spec.Boxes {
if box.GridX > maxGridX {
maxGridX = box.GridX
}
if box.GridY > maxGridY {
maxGridY = box.GridY
}
}
// Calculate dimensions
dims := CalculateDimensions(maxGridX, maxGridY, config)
// Extend width for legend area if needed
legendWidth := EstimateLegendWidth(legend)
dims.Width += legendWidth
// Create diagram
diagram := NewDiagram(dims.Width, dims.Height)
// Map box IDs to their data for arrow routing
boxData := make(map[string]BoxData)
// Create boxes
var previousBoxSpecID string // Track previous box ID for touch-left boxData updates
for _, boxSpec := range spec.Boxes {
// Convert grid coordinates to pixel coordinates
pixelX := gridToPixelX(boxSpec.GridX, dims, config)
pixelY := gridToPixelY(boxSpec.GridY, dims, config)
// Calculate per-box dimensions based on GridWidth and GridHeight
boxWidth := calculateBoxWidth(boxSpec.GridWidth, dims, config)
boxHeight := calculateBoxHeight(boxSpec.GridHeight, config)
// Handle touch-left connector: extend both boxes into the gap
if boxSpec.TouchLeft {
touchExtension := calculateTouchExtension(config)
boxWidth += touchExtension // Extend current box to the left
pixelX -= touchExtension // Shift position left by extension amount
// Extend previous box to the right (both diagram and boxData)
if len(diagram.Boxes) > 0 {
prevIdx := len(diagram.Boxes) - 1
diagram.Boxes[prevIdx].Width += touchExtension
// Update boxData for accurate arrow routing/collision detection
if prev, ok := boxData[previousBoxSpecID]; ok {
prev.Width += touchExtension
prev.CenterX = prev.PixelX + prev.Width/2
boxData[previousBoxSpecID] = prev
}
}
}
color := boxSpec.Color
if color == "" {
// Apply gradient: bottom row uses #FFCE33, top rows lighten to #FFFFE0
color = calculateGradientColor(boxSpec.GridY, maxGridY)
}
borderColor := boxSpec.BorderColor
borderWidth := boxSpec.BorderWidth
fontSize := boxSpec.FontSize
textColor := boxSpec.TextColor
// Wrap text to fit in box
maxCharsPerLine := int(float64(boxWidth) / CHAR_WIDTH_PIXELS)
if maxCharsPerLine < MIN_CHARS_PER_LINE {
maxCharsPerLine = MIN_CHARS_PER_LINE
}
wrappedLines := WrapText(boxSpec.Label, maxCharsPerLine, MAX_TEXT_LINES)
wrappedLabel := strings.Join(wrappedLines, "\n")
diagram.AddBox(pixelX, pixelY, boxWidth, boxHeight, wrappedLabel, color, borderColor, borderWidth, fontSize, textColor)
// Store box data for arrow routing
boxData[boxSpec.ID] = BoxData{
ID: boxSpec.ID,
GridX: boxSpec.GridX,
GridY: boxSpec.GridY,
PixelX: pixelX,
PixelY: pixelY,
CenterX: pixelX + boxWidth/2,
CenterY: pixelY + boxHeight/2,
Width: boxWidth,
Height: boxHeight,
}
previousBoxSpecID = boxSpec.ID
}
// Extend diagram width if any box (with custom GridWidth) exceeds it
for _, bd := range boxData {
rightEdge := bd.PixelX + bd.Width + 20 // 20px right margin
if rightEdge+legendWidth > diagram.Width {
diagram.Width = rightEdge + legendWidth
}
}
// Create arrows
for _, arrowSpec := range spec.Arrows {
fromBox := boxData[arrowSpec.FromID]
toBox := boxData[arrowSpec.ToID]
// Convert boxData map to slice for collision detection
allBoxes := make([]BoxData, 0, len(boxData))
for _, box := range boxData {
allBoxes = append(allBoxes, box)
}
// Create box coordinates
box1 := BoxCoords{
X1: fromBox.PixelX,
Y1: fromBox.PixelY,
X2: fromBox.PixelX + fromBox.Width,
Y2: fromBox.PixelY + fromBox.Height,
}
box2 := BoxCoords{
X1: toBox.PixelX,
Y1: toBox.PixelY,
X2: toBox.PixelX + toBox.Width,
Y2: toBox.PixelY + toBox.Height,
}
// Resolve per-arrow flow vs global flow
flow := arrowFlow
if arrowSpec.Flow != "" {
flow = arrowSpec.Flow
}
plan, err := RouteArrow(
box1, box2,
fromBox.GridX, fromBox.GridY,
toBox.GridX, toBox.GridY,
allBoxes,
arrowSpec.FromID, arrowSpec.ToID,
flow,
)
if err != nil {
fmt.Printf("Error routing arrow from %s to %s: %v\n", arrowSpec.FromID, arrowSpec.ToID, err)
continue
}
diagram.AddArrow(plan.StartX, plan.StartY, plan.EndX, plan.EndY, plan.VerticalFirst, plan.NumSegments, arrowSpec.FromID, arrowSpec.ToID, plan.Strategy, plan.AllCandidates)
}
// Resolve groups to pixel coordinates
for _, g := range groups {
if len(g.BoxIDs) == 0 {
continue
}
// Compute bounding box of all member boxes
first := true
var minX, minY, maxX, maxY int
for _, boxID := range g.BoxIDs {
bd, ok := boxData[boxID]
if !ok {
continue
}
bx1 := bd.PixelX
by1 := bd.PixelY
bx2 := bd.PixelX + bd.Width
by2 := bd.PixelY + bd.Height
if first {
minX, minY, maxX, maxY = bx1, by1, bx2, by2
first = false
} else {
if bx1 < minX {
minX = bx1
}
if by1 < minY {
minY = by1
}
if bx2 > maxX {
maxX = bx2
}
if by2 > maxY {
maxY = by2
}
}
}
if first {
continue // no valid boxes found
}
diagram.Groups = append(diagram.Groups, Group{
X: minX - groupPadding,
Y: minY - groupPadding - 30, // extra space for label
Width: (maxX - minX) + 2*groupPadding,
Height: (maxY - minY) + 2*groupPadding + 30, // extra space for label
Label: g.Label,
BoxIDs: g.BoxIDs,
})
}
return diagram, boxData
}
// parseHexColor converts hex color string to RGB values (0-255 range)
func parseHexColor(hex string) (r, g, b int) {
// Remove # if present
if len(hex) > 0 && hex[0] == '#' {
hex = hex[1:]
}
// Parse hex values
if len(hex) == 6 {
_, err := fmt.Sscanf(hex, "%02x%02x%02x", &r, &g, &b)
if err != nil {
return 0, 0, 0 // Return black on invalid hex
}
}
return
}
// rgbToHex converts RGB values (0-255) to hex color string
func rgbToHex(r, g, b int) string {
return fmt.Sprintf("#%02X%02X%02X", r, g, b)
}
// interpolateColor blends two hex colors based on factor (0.0 to 1.0)
// factor=0.0 returns color1, factor=1.0 returns color2
func interpolateColor(color1, color2 string, factor float64) string {
r1, g1, b1 := parseHexColor(color1)
r2, g2, b2 := parseHexColor(color2)
r := int(float64(r1) + float64(r2-r1)*factor)
g := int(float64(g1) + float64(g2-g1)*factor)
b := int(float64(b1) + float64(b2-b1)*factor)
return rgbToHex(r, g, b)
}
// calculateGradientColor returns a yellow gradient color based on row position
// Bottom row (highest GridY) gets #FFCE33, top rows get progressively lighter
func calculateGradientColor(gridY, maxGridY int) string {
if maxGridY <= 1 {
return "#FFCE33" // Single row, use default yellow
}
// Calculate factor: 0.0 for bottom row, increasing towards 1.0 for top
// Apply 0.5 multiplier for subtle gradient (use only 50% of color range)
factor := float64(maxGridY-gridY) / float64(maxGridY-1) * 0.5
// Interpolate between dark yellow (#FFCE33) and very light yellow (#FFFEF0)
return interpolateColor("#FFCE33", "#FFFEF0", factor)
}