lib/zen/src/diagram/ego/layout.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

  1 const std = @import("std");
  2 
  3 const root = @import("root.zig");
  4 
  5 const model = root.model;
  6 
  7 pub const margin_width_max: u16 = 256;
  8 pub const margin_height_min: u16 = 268;
  9 pub const margin_height_max: u16 = 520;
 10 
 11 const wide_leaf_width: u16 = 640;
 12 const wide_leaf_height: u16 = 120;
 13 const wide_leaf_loop_height: u16 = 152;
 14 const wide_leaf_node_width: f32 = 150;
 15 const wide_header_y: f32 = 26;
 16 const wide_content_top: f32 = 50;
 17 const wide_content_bottom: f32 = 408;
 18 const vertical_heading_extent: f32 = 18;
 19 const vertical_heading_gap: f32 = 16;
 20 const vertical_empty_extent: f32 = 32;
 21 const vertical_item_gap: f32 = 10;
 22 const vertical_flow_gap: f32 = 28;
 23 
 24 pub const Point = struct {
 25     x: f32,
 26     y: f32,
 27 };
 28 
 29 pub const Rect = struct {
 30     x: f32,
 31     y: f32,
 32     width: f32,
 33     height: f32,
 34 
 35     pub fn centerX(self: Rect) f32 {
 36         return self.x + self.width / 2;
 37     }
 38 
 39     pub fn centerY(self: Rect) f32 {
 40         return self.y + self.height / 2;
 41     }
 42 };
 43 
 44 pub const More = struct {
 45     count: u32,
 46     href: []const u8,
 47 };
 48 
 49 pub const Content = union(enum) {
 50     node: model.Node,
 51     more: More,
 52 };
 53 
 54 pub const Item = struct {
 55     box: Rect,
 56     content: Content,
 57 };
 58 
 59 pub const Band = struct {
 60     items: [model.max_slots_per_band]Item = undefined,
 61     count: u8 = 0,
 62     heading: Point,
 63     empty: Point,
 64 
 65     pub fn slice(self: *const Band) []const Item {
 66         std.debug.assert(self.count <= model.max_slots_per_band);
 67         return self.items[0..self.count];
 68     }
 69 };
 70 
 71 pub const Plan = struct {
 72     geometry: model.Geometry,
 73     subject: Rect,
 74     callers: Band,
 75     callees: Band,
 76 
 77     pub fn inspect(network: model.Network, surface: model.Surface) model.Error!Plan {
 78         try model.validate(network);
 79         const slot_limit = model.slotsPerBand(surface);
 80         const callers = try select(network.callers, slot_limit);
 81         const callees = try select(network.callees, slot_limit);
 82         return switch (surface) {
 83             .margin => verticalPlan(network, callers, callees),
 84             .wide => horizontalPlan(network, callers, callees),
 85         };
 86     }
 87 };
 88 
 89 const Columns = struct {
 90     callers: Rect,
 91     subject: Rect,
 92     callees: Rect,
 93 };
 94 
 95 const Selection = struct {
 96     node_count: u8,
 97     more_count: u32,
 98 
 99     fn visibleCount(self: Selection) u8 {
100         return self.node_count + @intFromBool(self.more_count > 0);
101     }
102 };
103 
104 fn horizontalPlan(
105     network: model.Network,
106     callers: Selection,
107     callees: Selection,
108 ) Plan {
109     const empty = network.callers.total == 0 and network.callees.total == 0;
110     const geometry = wideGeometry(empty, network.recursive);
111     const columns = horizontalColumns(geometry);
112     var plan = Plan{
113         .geometry = geometry,
114         .subject = columns.subject,
115         .callers = horizontalBand(columns.callers, geometry),
116         .callees = horizontalBand(columns.callees, geometry),
117     };
118     placeBand(
119         &plan.callers,
120         network.callers,
121         callers,
122         columns.callers,
123         horizontalFirstY(geometry, callers.visibleCount()),
124         horizontalStep(geometry, callers.visibleCount()),
125     );
126     placeBand(
127         &plan.callees,
128         network.callees,
129         callees,
130         columns.callees,
131         horizontalFirstY(geometry, callees.visibleCount()),
132         horizontalStep(geometry, callees.visibleCount()),
133     );
134     return plan;
135 }
136 
137 fn verticalPlan(
138     network: model.Network,
139     callers: Selection,
140     callees: Selection,
141 ) Plan {
142     var geometry = marginGeometry();
143     const center_x = @as(f32, @floatFromInt(geometry.width)) / 2;
144     const callers_extent = verticalBandExtent(geometry, callers.visibleCount());
145     const callees_extent = verticalBandExtent(geometry, callees.visibleCount());
146     const callers_top = geometry.outer_inset + vertical_heading_extent +
147         vertical_heading_gap;
148     const subject_y = callers_top + callers_extent + vertical_flow_gap;
149     const calls_header_top = subject_y + geometry.subject_height + vertical_flow_gap;
150     const callees_top = calls_header_top + vertical_heading_extent +
151         vertical_heading_gap;
152     const height = callees_top + callees_extent + geometry.outer_inset;
153     geometry.height = @intFromFloat(height);
154     std.debug.assert(geometry.height >= margin_height_min);
155     std.debug.assert(geometry.height <= margin_height_max);
156     var plan = Plan{
157         .geometry = geometry,
158         .subject = .{
159             .x = center_x - geometry.subject_width / 2,
160             .y = subject_y,
161             .width = geometry.subject_width,
162             .height = geometry.subject_height,
163         },
164         .callers = verticalBand(geometry, callers_top, geometry.outer_inset),
165         .callees = verticalBand(geometry, callees_top, calls_header_top),
166     };
167     const node_box = Rect{
168         .x = center_x - geometry.node_width / 2,
169         .y = 0,
170         .width = geometry.node_width,
171         .height = geometry.node_height,
172     };
173     const step = geometry.node_height + vertical_item_gap;
174     placeBand(&plan.callers, network.callers, callers, node_box, callers_top, step);
175     placeBand(&plan.callees, network.callees, callees, node_box, callees_top, step);
176     return plan;
177 }
178 
179 fn horizontalBand(column: Rect, geometry: model.Geometry) Band {
180     return .{
181         .heading = .{ .x = column.centerX(), .y = wide_header_y },
182         .empty = .{
183             .x = column.centerX(),
184             .y = @as(f32, @floatFromInt(geometry.height)) / 2,
185         },
186     };
187 }
188 
189 fn verticalBand(geometry: model.Geometry, content_y: f32, heading_top: f32) Band {
190     const center_x = @as(f32, @floatFromInt(geometry.width)) / 2;
191     return .{
192         .heading = .{ .x = center_x, .y = heading_top + geometry.heading_size },
193         .empty = .{ .x = center_x, .y = content_y + vertical_empty_extent / 2 },
194     };
195 }
196 
197 fn placeBand(
198     result: *Band,
199     source: model.Band,
200     selection: Selection,
201     template: Rect,
202     first_y: f32,
203     step: f32,
204 ) void {
205     result.count = selection.visibleCount();
206     std.debug.assert(result.count <= model.max_slots_per_band);
207     var index: u8 = 0;
208     while (index < selection.node_count) : (index += 1) {
209         result.items[index] = .{
210             .box = placedBox(template, first_y, step, index),
211             .content = .{ .node = source.nodes[index] },
212         };
213     }
214     if (selection.more_count == 0) return;
215     result.items[index] = .{
216         .box = placedBox(template, first_y, step, index),
217         .content = .{ .more = .{
218             .count = selection.more_count,
219             .href = source.more_href,
220         } },
221     };
222 }
223 
224 fn placedBox(template: Rect, first_y: f32, step: f32, index: u8) Rect {
225     var result = template;
226     result.y = first_y + step * @as(f32, @floatFromInt(index));
227     return result;
228 }
229 
230 fn select(source: model.Band, slot_limit: u8) model.Error!Selection {
231     std.debug.assert(slot_limit > 1);
232     std.debug.assert(slot_limit <= model.max_slots_per_band);
233     var node_count: u8 = @intCast(@min(source.nodes.len, slot_limit));
234     if (source.total > node_count and node_count == slot_limit) node_count -= 1;
235     const result = Selection{
236         .node_count = node_count,
237         .more_count = source.total - node_count,
238     };
239     if (result.more_count > 0 and source.more_href.len == 0) {
240         return error.MissingOverflowLink;
241     }
242     return result;
243 }
244 
245 fn horizontalColumns(geometry: model.Geometry) Columns {
246     const width: f32 = @floatFromInt(geometry.width);
247     const subject_x = (width - geometry.subject_width) / 2;
248     return .{
249         .callers = .{
250             .x = geometry.outer_inset,
251             .y = 0,
252             .width = geometry.node_width,
253             .height = geometry.node_height,
254         },
255         .subject = .{
256             .x = subject_x,
257             .y = (@as(f32, @floatFromInt(geometry.height)) -
258                 geometry.subject_height) / 2,
259             .width = geometry.subject_width,
260             .height = geometry.subject_height,
261         },
262         .callees = .{
263             .x = width - geometry.outer_inset - geometry.node_width,
264             .y = 0,
265             .width = geometry.node_width,
266             .height = geometry.node_height,
267         },
268     };
269 }
270 
271 fn horizontalFirstY(geometry: model.Geometry, count: u8) f32 {
272     std.debug.assert(count <= model.wide_slots_per_band);
273     if (count == 1) {
274         return (@as(f32, @floatFromInt(geometry.height)) - geometry.node_height) / 2;
275     }
276     return wide_content_top;
277 }
278 
279 fn horizontalStep(geometry: model.Geometry, count: u8) f32 {
280     std.debug.assert(count <= model.wide_slots_per_band);
281     if (count <= 1) return 0;
282     const span = wide_content_bottom - wide_content_top - geometry.node_height;
283     return span / @as(f32, @floatFromInt(count - 1));
284 }
285 
286 fn verticalBandExtent(geometry: model.Geometry, count: u8) f32 {
287     std.debug.assert(count <= model.margin_slots_per_band);
288     if (count == 0) return vertical_empty_extent;
289     return geometry.node_height * @as(f32, @floatFromInt(count)) +
290         vertical_item_gap * @as(f32, @floatFromInt(count - 1));
291 }
292 
293 fn marginGeometry() model.Geometry {
294     return .{
295         .arrangement = .vertical,
296         .width = margin_width_max,
297         .height = margin_height_min,
298         .slots_per_band = model.margin_slots_per_band,
299         .outer_inset = 8,
300         .node_width = 208,
301         .subject_width = 232,
302         .node_height = 46,
303         .subject_height = 64,
304         .heading_size = 13,
305         .empty_size = 12.5,
306         .label_size = 13,
307         .context_size = 10.5,
308         .badge_size = 8.5,
309         .subject_label_size = 16,
310     };
311 }
312 
313 fn wideGeometry(empty: bool, recursive: bool) model.Geometry {
314     const leaf_height = if (recursive) wide_leaf_loop_height else wide_leaf_height;
315     return .{
316         .arrangement = .horizontal,
317         .width = if (empty) wide_leaf_width else 920,
318         .height = if (empty) leaf_height else 420,
319         .slots_per_band = model.wide_slots_per_band,
320         .outer_inset = 20,
321         .node_width = if (empty) wide_leaf_node_width else 240,
322         .subject_width = 260,
323         .node_height = 48,
324         .subject_height = 72,
325         .heading_size = 11,
326         .empty_size = 12,
327         .label_size = 14,
328         .context_size = 10.5,
329         .badge_size = 8.5,
330         .subject_label_size = 18,
331     };
332 }