Skip to content

Commit 6d95a27

Browse files
committed
Fix PDTiles ContentWrapping: SVG as full-width background with float spacer for text wrapping
1 parent 2d05a57 commit 6d95a27

6 files changed

Lines changed: 140 additions & 48 deletions

File tree

PanoramicData.Blazor.Demo/Pages/PDTilesPage.razor

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666
<PDToolbarDropdown Text="@($"MaxW:{_options.MaxGridWidthPercent?.ToString() ?? "-"}%")" Items="@_maxSizeItems" Click="OnMaxWidthSelected" ToolTip="Max Grid Width %" />
6767
<PDToolbarDropdown Text="@($"MaxH:{_options.MaxGridHeightPercent?.ToString() ?? "-"}%")" Items="@_maxSizeItems" Click="OnMaxHeightSelected" ToolTip="Max Grid Height %" />
6868
<PDToggleSwitch @bind-Value="_showChildContent" @bind-Value:after="OnOptionsChanged" Label="Content" />
69+
<PDToggleSwitch @bind-Value="_options.ContentWrapping" @bind-Value:after="OnOptionsChanged" Label="Wrap" />
6970
</PDToolbar>
7071

7172
<h3 class="mt-3">Connector Options</h3>
@@ -143,13 +144,7 @@
143144

144145
<style>
145146
.pd-tiles-child-content {
146-
position: absolute;
147-
top: 0;
148-
left: 0;
149-
width: 50%;
150-
height: 100%;
151147
padding: 2rem;
152-
overflow: auto;
153148
color: white;
154149
text-shadow: 0 2px 4px rgba(0,0,0,0.8);
155150
pointer-events: auto;

PanoramicData.Blazor.Demo/Pages/PDTilesPage.razor.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ public partial class PDTilesPage
3636
ReflectionDepth = 150,
3737
Scale = 100,
3838
Padding = 5,
39-
Alignment = GridAlignment.MiddleRight
39+
Alignment = GridAlignment.MiddleRight,
40+
ContentWrapping = true
4041
};
4142

4243
private readonly TileConnectorOptions _connectorOptions = new()
@@ -277,6 +278,7 @@ private void ParseQueryParameters()
277278
TryParseNullableInt(query, "maxW", v => _options.MaxGridWidthPercent = v);
278279
TryParseNullableInt(query, "maxH", v => _options.MaxGridHeightPercent = v);
279280
TryParseBool(query, "content", v => _showChildContent = v);
281+
TryParseBool(query, "wrap", v => _options.ContentWrapping = v);
280282

281283
// Connector options
282284
TryParseEnum<ConnectorFillPattern>(query, "cPat", v => _connectorOptions.FillPattern = v);
@@ -362,6 +364,7 @@ private void UpdateUrl()
362364
["maxW"] = _options.MaxGridWidthPercent?.ToString() ?? "",
363365
["maxH"] = _options.MaxGridHeightPercent?.ToString() ?? "",
364366
["content"] = _showChildContent ? "true" : "false",
367+
["wrap"] = _options.ContentWrapping ? "true" : "false",
365368

366369
// Connector options
367370
["cPat"] = _connectorOptions.FillPattern.ToString(),

PanoramicData.Blazor/Models/Tiles/TileGridOptions.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,4 +134,11 @@ public class TileGridOptions
134134
/// Maximum grid height as percentage of container height (1-100). Null means no limit.
135135
/// </summary>
136136
public int? MaxGridHeightPercent { get; set; }
137+
138+
/// <summary>
139+
/// When true and ChildContent is provided, text wraps around the tile grid
140+
/// in a newspaper-style layout using CSS float, instead of overlaying the grid.
141+
/// Requires MaxGridWidthPercent to be set so the grid doesn't fill the full width.
142+
/// </summary>
143+
public bool ContentWrapping { get; set; }
137144
}

PanoramicData.Blazor/PDTiles.razor

Lines changed: 51 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
viewBox="@ViewBox"
1212
preserveAspectRatio="@PreserveAspectRatio"
1313
overflow="visible"
14-
style="width: 100%; height: 100%;@(Options.ShowBackground ? $" background-color: {Options.BackgroundColor};" : "")">
14+
style="@GetSvgElementStyle()">
1515

1616
<defs>
1717
@* Gradients for tile faces *@
@@ -111,9 +111,9 @@
111111

112112
@foreach (var depth in allDepths)
113113
{
114-
@if (UsesBezierCurves)
114+
@if (UsesBezierCurves && ConnectorOptions.ConnectionMode != ConnectionMode.ColumnCurves)
115115
{
116-
@* For curve modes: render tiles first, then connectors (curves go on top of their source row) *@
116+
@* For row curve modes: render tiles first, then connectors (curves go on top of their source row) *@
117117
@foreach (var tile in tiles.Where(t => t.Depth == depth))
118118
{
119119
@RenderTile(tile)
@@ -129,7 +129,7 @@
129129
}
130130
else
131131
{
132-
@* For straight-line mode: render connectors first (behind tiles at this depth) *@
132+
@* For straight-line mode and column curves: render connectors first (behind tiles at this depth) *@
133133
@if (connectorsByDepth.TryGetValue(depth, out var depthConnectors))
134134
{
135135
@foreach (var conn in depthConnectors)
@@ -149,12 +149,20 @@
149149
</div>
150150

151151
@* Child content - overlaid on top of the tile grid *@
152-
@if (ChildContent != null)
152+
@if (ChildContent != null && !Options.ContentWrapping)
153153
{
154154
<div class="pd-tiles-content" style="@GetChildContentStyle()">
155155
@ChildContent
156156
</div>
157157
}
158+
else if (ChildContent != null && Options.ContentWrapping)
159+
{
160+
<div class="pd-tiles-content-wrap" style="@GetChildContentWrapStyle()">
161+
@* Invisible float spacer matching the grid area *@
162+
<div class="pd-tiles-float-spacer" style="@GetFloatSpacerStyle()"></div>
163+
@ChildContent
164+
</div>
165+
}
158166
</div>
159167

160168
@code {
@@ -308,13 +316,13 @@
308316
@if (pattern == ConnectorFillPattern.Bars)
309317
{
310318
<g clip-path="url(#@clipId)">
311-
@RenderBarsPattern(t0, t1, b0, b1, color, opacity)
319+
@RenderBarsPattern(t0, t1, b0, b1, color, opacity, conn.Connector.Reversed)
312320
</g>
313321
}
314322
else if (pattern == ConnectorFillPattern.Chevrons)
315323
{
316324
<g clip-path="url(#@clipId)">
317-
@RenderChevronsPattern(t0, t1, b0, b1, color, opacity)
325+
@RenderChevronsPattern(t0, t1, b0, b1, color, opacity, conn.Connector.Reversed)
318326
</g>
319327
}
320328
else if (pattern == ConnectorFillPattern.Solid)
@@ -372,14 +380,14 @@
372380
{
373381
@* Bars pattern - follow the bezier curve *@
374382
<g clip-path="url(#@clipId)">
375-
@RenderBezierBarsPattern(bezierData, color, opacity)
383+
@RenderBezierBarsPattern(bezierData, color, opacity, conn.Connector.Reversed)
376384
</g>
377385
}
378386
else if (pattern == ConnectorFillPattern.Chevrons)
379387
{
380388
@* Chevrons pattern - follow the bezier curve *@
381389
<g clip-path="url(#@clipId)">
382-
@RenderBezierChevronsPattern(bezierData, color, opacity)
390+
@RenderBezierChevronsPattern(bezierData, color, opacity, conn.Connector.Reversed)
383391
</g>
384392
}
385393

@@ -398,11 +406,11 @@
398406
};
399407

400408
private RenderFragment RenderBarsPattern((double X, double Y) t0, (double X, double Y) t1,
401-
(double X, double Y) b0, (double X, double Y) b1, string color, double opacity) => __builder =>
409+
(double X, double Y) b0, (double X, double Y) b1, string color, double opacity, bool reversed) => __builder =>
402410
{
403411
var length = Math.Sqrt(Math.Pow(t1.X - t0.X, 2) + Math.Pow(t1.Y - t0.Y, 2));
404412
var barCount = Math.Max(3, (int)(length / 30));
405-
var offset = _animationOffset;
413+
var offset = reversed ? 1.0 - _animationOffset : _animationOffset;
406414

407415
for (var i = -1; i <= barCount; i++)
408416
{
@@ -422,14 +430,16 @@
422430
};
423431

424432
private RenderFragment RenderChevronsPattern((double X, double Y) t0, (double X, double Y) t1,
425-
(double X, double Y) b0, (double X, double Y) b1, string color, double opacity) => __builder =>
433+
(double X, double Y) b0, (double X, double Y) b1, string color, double opacity, bool reversed) => __builder =>
426434
{
427-
var length = Math.Sqrt(Math.Pow(t1.X - t0.X, 2) + Math.Pow(t1.Y - t0.Y, 2));
435+
// When reversed, swap start/end so chevrons point the opposite direction
436+
var (at0, at1, ab0, ab1) = reversed ? (t1, t0, b1, b0) : (t0, t1, b0, b1);
437+
var length = Math.Sqrt(Math.Pow(at1.X - at0.X, 2) + Math.Pow(at1.Y - at0.Y, 2));
428438
var chevronCount = Math.Max(2, (int)(length / 45));
429439
var chevronWidth = 0.6;
430440
var pointDepth = 0.3;
431441
var notchDepth = 0.3;
432-
var offset = _animationOffset;
442+
var offset = reversed ? 1.0 - _animationOffset : _animationOffset;
433443

434444
for (var i = -1; i <= chevronCount; i++)
435445
{
@@ -438,12 +448,12 @@
438448
var fBody = (i + offset + chevronWidth) / chevronCount;
439449
var fPoint = (i + offset + chevronWidth + pointDepth) / chevronCount;
440450

441-
var bl = Lerp(b0, b1, fTrail);
442-
var tl = Lerp(t0, t1, fTrail);
443-
var nm = LerpMid(t0, t1, b0, b1, fNotch);
444-
var br = Lerp(b0, b1, fBody);
445-
var tr = Lerp(t0, t1, fBody);
446-
var pm = LerpMid(t0, t1, b0, b1, fPoint);
451+
var bl = Lerp(ab0, ab1, fTrail);
452+
var tl = Lerp(at0, at1, fTrail);
453+
var nm = LerpMid(at0, at1, ab0, ab1, fNotch);
454+
var br = Lerp(ab0, ab1, fBody);
455+
var tr = Lerp(at0, at1, fBody);
456+
var pm = LerpMid(at0, at1, ab0, ab1, fPoint);
447457

448458
<polygon points="@($"{F(bl.X)},{F(bl.Y)} {F(br.X)},{F(br.Y)} {F(pm.X)},{F(pm.Y)} {F(tr.X)},{F(tr.Y)} {F(tl.X)},{F(tl.Y)} {F(nm.X)},{F(nm.Y)}")"
449459
fill="@color"
@@ -485,12 +495,12 @@
485495
);
486496
}
487497

488-
private RenderFragment RenderBezierBarsPattern(BezierConnectorData data, string color, double opacity) => __builder =>
498+
private RenderFragment RenderBezierBarsPattern(BezierConnectorData data, string color, double opacity, bool reversed) => __builder =>
489499
{
490500
// Estimate arc length from the top curve for consistent bar spacing
491501
var chordLen = Math.Sqrt(Math.Pow(data.EndTop.X - data.StartTop.X, 2) + Math.Pow(data.EndTop.Y - data.StartTop.Y, 2));
492502
var barCount = Math.Max(3, (int)(chordLen / 30));
493-
var offset = _animationOffset;
503+
var offset = reversed ? 1.0 - _animationOffset : _animationOffset;
494504

495505
for (var i = -1; i <= barCount; i++)
496506
{
@@ -509,14 +519,22 @@
509519
}
510520
};
511521

512-
private RenderFragment RenderBezierChevronsPattern(BezierConnectorData data, string color, double opacity) => __builder =>
522+
private RenderFragment RenderBezierChevronsPattern(BezierConnectorData data, string color, double opacity, bool reversed) => __builder =>
513523
{
514-
var chordLen = Math.Sqrt(Math.Pow(data.EndTop.X - data.StartTop.X, 2) + Math.Pow(data.EndTop.Y - data.StartTop.Y, 2));
524+
// When reversed, evaluate bezier from end?start to flip chevron direction
525+
var (sTop, sTopCtrl, eTopCtrl, eTop) = reversed
526+
? (data.EndTop, data.EndTopCtrl, data.StartTopCtrl, data.StartTop)
527+
: (data.StartTop, data.StartTopCtrl, data.EndTopCtrl, data.EndTop);
528+
var (sBot, sBotCtrl, eBotCtrl, eBot) = reversed
529+
? (data.EndBottom, data.EndBottomCtrl, data.StartBottomCtrl, data.StartBottom)
530+
: (data.StartBottom, data.StartBottomCtrl, data.EndBottomCtrl, data.EndBottom);
531+
532+
var chordLen = Math.Sqrt(Math.Pow(eTop.X - sTop.X, 2) + Math.Pow(eTop.Y - sTop.Y, 2));
515533
var chevronCount = Math.Max(2, (int)(chordLen / 45));
516534
var chevronWidth = 0.6;
517535
var pointDepth = 0.3;
518536
var notchDepth = 0.3;
519-
var offset = _animationOffset;
537+
var offset = reversed ? 1.0 - _animationOffset : _animationOffset;
520538

521539
for (var i = -1; i <= chevronCount; i++)
522540
{
@@ -525,18 +543,18 @@
525543
var fBody = (i + offset + chevronWidth) / chevronCount;
526544
var fPoint = (i + offset + chevronWidth + pointDepth) / chevronCount;
527545

528-
var tl = CubicBezier(data.StartTop, data.StartTopCtrl, data.EndTopCtrl, data.EndTop, fTrail);
529-
var bl = CubicBezier(data.StartBottom, data.StartBottomCtrl, data.EndBottomCtrl, data.EndBottom, fTrail);
530-
var tr = CubicBezier(data.StartTop, data.StartTopCtrl, data.EndTopCtrl, data.EndTop, fBody);
531-
var br = CubicBezier(data.StartBottom, data.StartBottomCtrl, data.EndBottomCtrl, data.EndBottom, fBody);
546+
var tl = CubicBezier(sTop, sTopCtrl, eTopCtrl, eTop, fTrail);
547+
var bl = CubicBezier(sBot, sBotCtrl, eBotCtrl, eBot, fTrail);
548+
var tr = CubicBezier(sTop, sTopCtrl, eTopCtrl, eTop, fBody);
549+
var br = CubicBezier(sBot, sBotCtrl, eBotCtrl, eBot, fBody);
532550

533551
// Notch and point are midpoints between the top and bottom curves at their respective t values
534-
var nTop = CubicBezier(data.StartTop, data.StartTopCtrl, data.EndTopCtrl, data.EndTop, fNotch);
535-
var nBot = CubicBezier(data.StartBottom, data.StartBottomCtrl, data.EndBottomCtrl, data.EndBottom, fNotch);
552+
var nTop = CubicBezier(sTop, sTopCtrl, eTopCtrl, eTop, fNotch);
553+
var nBot = CubicBezier(sBot, sBotCtrl, eBotCtrl, eBot, fNotch);
536554
var nm = (X: (nTop.X + nBot.X) / 2, Y: (nTop.Y + nBot.Y) / 2);
537555

538-
var pTop = CubicBezier(data.StartTop, data.StartTopCtrl, data.EndTopCtrl, data.EndTop, fPoint);
539-
var pBot = CubicBezier(data.StartBottom, data.StartBottomCtrl, data.EndBottomCtrl, data.EndBottom, fPoint);
556+
var pTop = CubicBezier(sTop, sTopCtrl, eTopCtrl, eTop, fPoint);
557+
var pBot = CubicBezier(sBot, sBotCtrl, eBotCtrl, eBot, fPoint);
540558
var pm = (X: (pTop.X + pBot.X) / 2, Y: (pTop.Y + pBot.Y) / 2);
541559

542560
<polygon points="@($"{F(bl.X)},{F(bl.Y)} {F(br.X)},{F(br.Y)} {F(pm.X)},{F(pm.Y)} {F(tr.X)},{F(tr.Y)} {F(tl.X)},{F(tl.Y)} {F(nm.X)},{F(nm.Y)}")"

PanoramicData.Blazor/PDTiles.razor.cs

Lines changed: 55 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -189,18 +189,58 @@ private string GetSvgContainerStyle()
189189
{
190190
if (ChildContent != null)
191191
{
192-
// Position absolutely but allow pointer events on SVG elements (tiles/connectors handle their own events)
192+
// Position absolutely so the SVG fills the entire container as a background layer.
193+
// In wrapping mode, grid lines/glow/background show through behind the content text.
193194
return "position: absolute; top: 0; left: 0; width: 100%; height: 100%;";
194195
}
195196

196197
return "width: 100%; height: 100%;";
197198
}
198199

200+
/// <summary>
201+
/// Gets the inline style for the SVG element itself.
202+
/// In wrapping mode the height is omitted so the SVG auto-sizes from its viewBox aspect ratio.
203+
/// </summary>
204+
private string GetSvgElementStyle()
205+
{
206+
var style = "width: 100%; height: 100%;";
207+
208+
if (Options.ShowBackground)
209+
{
210+
style += $" background-color: {Options.BackgroundColor};";
211+
}
212+
213+
return style;
214+
}
215+
199216
/// <summary>
200217
/// Gets the style for the child content container.
201218
/// </summary>
202219
private static string GetChildContentStyle() => "position: absolute; top: 0; left: 0; width: 100%; height: 100%; overflow: auto; z-index: 1; pointer-events: none;";
203220

221+
/// <summary>
222+
/// Gets the style for the wrapping child content container (newspaper-style).
223+
/// </summary>
224+
private static string GetChildContentWrapStyle() => "position: absolute; top: 0; left: 0; width: 100%; height: 100%; overflow: auto; z-index: 1; pointer-events: none;";
225+
226+
/// <summary>
227+
/// Gets the style for the invisible float spacer that forces content to wrap around the grid.
228+
/// The spacer fills the full container height to ensure content never overlaps the tile area.
229+
/// </summary>
230+
private string GetFloatSpacerStyle()
231+
{
232+
var floatSide = Options.Alignment switch
233+
{
234+
GridAlignment.TopLeft or GridAlignment.MiddleLeft or GridAlignment.BottomLeft => "left",
235+
_ => "right"
236+
};
237+
238+
// Use MaxGridWidthPercent if set, otherwise default to 50%
239+
var widthPercent = Options.MaxGridWidthPercent ?? 50;
240+
241+
return $"float: {floatSide}; width: {widthPercent}%; height: 100%;";
242+
}
243+
204244
protected override void OnInitialized()
205245
{
206246
base.OnInitialized();
@@ -477,10 +517,17 @@ private LayoutInfo CalculateLayout()
477517

478518
// Expand the viewBox to respect MaxGridWidthPercent and MaxGridHeightPercent
479519
// If MaxGridWidthPercent is 50%, the viewBox should be twice as wide so the grid
480-
// occupies only 50% of the container width (positioned by alignment)
481-
if (Options.MaxGridWidthPercent.HasValue && Options.MaxGridWidthPercent.Value > 0 && Options.MaxGridWidthPercent.Value < 100)
520+
// occupies only 50% of the container width (positioned by alignment).
521+
// When ContentWrapping is active, auto-constrain to 50% so tiles stay on one side.
522+
var effectiveMaxWidthPercent = Options.MaxGridWidthPercent;
523+
if (!effectiveMaxWidthPercent.HasValue && Options.ContentWrapping && ChildContent != null)
524+
{
525+
effectiveMaxWidthPercent = 50;
526+
}
527+
528+
if (effectiveMaxWidthPercent.HasValue && effectiveMaxWidthPercent.Value > 0 && effectiveMaxWidthPercent.Value < 100)
482529
{
483-
viewBoxWidth = constrainedWidth * (100.0 / Options.MaxGridWidthPercent.Value);
530+
viewBoxWidth = constrainedWidth * (100.0 / effectiveMaxWidthPercent.Value);
484531
}
485532

486533
if (Options.MaxGridHeightPercent.HasValue && Options.MaxGridHeightPercent.Value > 0 && Options.MaxGridHeightPercent.Value < 100)
@@ -1259,17 +1306,17 @@ private static (double X, double Y) GetEdgeOutwardPerpendicular(string direction
12591306
{
12601307
return direction switch
12611308
{
1262-
"right" or "up" => (0.447, 0.894),
1263-
"left" or "down" => (-0.447, 0.894),
1309+
"right" or "up" => (0.707, 0.707),
1310+
"left" or "down" => (-0.707, 0.707),
12641311
_ => (0.0, 1.0)
12651312
};
12661313
}
12671314
else
12681315
{
12691316
return direction switch
12701317
{
1271-
"left" or "down" => (-0.484, -0.875),
1272-
"right" or "up" => (0.484, -0.875),
1318+
"left" or "down" => (-0.707, -0.707),
1319+
"right" or "up" => (0.707, -0.707),
12731320
_ => (0.0, -1.0)
12741321
};
12751322
}

PanoramicData.Blazor/PDTiles.razor.css

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,25 @@
4949
pointer-events: auto;
5050
}
5151

52+
/* Newspaper-style wrapping content container */
53+
.pd-tiles-content-wrap {
54+
position: absolute;
55+
top: 0;
56+
left: 0;
57+
width: 100%;
58+
height: 100%;
59+
overflow: auto;
60+
z-index: 1;
61+
pointer-events: none;
62+
}
63+
64+
/* Allow pointer events on wrapping child content elements */
65+
.pd-tiles-content-wrap > * {
66+
pointer-events: auto;
67+
}
68+
69+
/* Invisible float spacer - no pointer events so clicks pass through to SVG */
70+
.pd-tiles-float-spacer {
71+
pointer-events: none;
72+
}
73+

0 commit comments

Comments
 (0)