@welldot/render 0.2.3 → 0.3.0-next.20261015.d7c7365

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,7 +1,12 @@
1
+ import {
2
+ importFgdcTextures
3
+ } from "./chunk-ODRL222Y.js";
4
+
1
5
  // src/configs/render.configs.ts
2
6
  import { easeCubic } from "d3";
3
7
 
4
8
  // src/utils/format.utils.ts
9
+ import { resolveLanguageText } from "@welldot/core";
5
10
  import { formatNumber } from "@welldot/utils";
6
11
  var MM_TO_INCHES = 0.0393701;
7
12
  var M_TO_FT = 3.28084;
@@ -19,8 +24,7 @@ function getDiameterUnit(units, locale) {
19
24
  return locale === "en" ? "in." : '"';
20
25
  }
21
26
  function resolveRenderLabel(value, locale) {
22
- if (typeof value === "string") return value;
23
- return locale === "en" && value.en ? value.en : value.pt;
27
+ return resolveLanguageText(value, locale, "pt");
24
28
  }
25
29
 
26
30
  // src/configs/render.configs.ts
@@ -45,6 +49,8 @@ var RENDER_LABELS = {
45
49
  wellCase: { pt: "Revestimento", en: "Casing" },
46
50
  wellScreen: { pt: "Filtro", en: "Screen" },
47
51
  reduction: { pt: "Redu\xE7\xE3o", en: "Reduction" },
52
+ centralizer: { pt: "Centralizador", en: "Centralizer" },
53
+ pump: { pt: "Bomba e edutor", en: "Pump and riser" },
48
54
  cementPad: { pt: "Laje de cimento", en: "Cement pad" },
49
55
  conflict: { pt: "Conflito", en: "Conflict" }
50
56
  }
@@ -68,9 +74,38 @@ var RENDER_LABELS = {
68
74
  wellCase: { title: { pt: "REVESTIMENTO", en: "CASING" } },
69
75
  wellScreen: {
70
76
  title: { pt: "FILTROS", en: "SCREENS" },
71
- slot: { pt: "Ranhura:", en: "Slot:" }
77
+ slot: { pt: "Ranhura:", en: "Slot:" },
78
+ material: { pt: "Material:", en: "Material:" },
79
+ slotType: { pt: "Tipo:", en: "Type:" }
72
80
  },
73
81
  reduction: { title: { pt: "REDU\xC7\xC3O", en: "REDUCTION" } },
82
+ centralizer: {
83
+ title: { pt: "CENTRALIZADOR", en: "CENTRALIZER" },
84
+ depth: { pt: "Profundidade:", en: "Depth:" },
85
+ spacing: { pt: "Espa\xE7amento:", en: "Spacing:" }
86
+ },
87
+ pump: {
88
+ title: { pt: "BOMBA", en: "PUMP" },
89
+ intakeDepth: { pt: "Profundidade do crivo:", en: "Intake depth:" },
90
+ model: { pt: "Modelo:", en: "Model:" },
91
+ power: { pt: "Pot\xEAncia:", en: "Power:" },
92
+ riser: { pt: "Edutor:", en: "Riser:" },
93
+ type_submersible: { pt: "Bomba submersa", en: "Submersible pump" },
94
+ type_vertical_turbine: {
95
+ pt: "Bomba de eixo vertical",
96
+ en: "Vertical turbine pump"
97
+ },
98
+ type_jet: { pt: "Bomba injetora", en: "Jet pump" },
99
+ type_progressive_cavity: {
100
+ pt: "Bomba helicoidal",
101
+ en: "Progressive cavity pump"
102
+ },
103
+ type_hand_pump: { pt: "Bomba manual", en: "Hand pump" },
104
+ type_compressor_airlift: {
105
+ pt: "Compressor (air-lift)",
106
+ en: "Compressor (air-lift)"
107
+ }
108
+ },
74
109
  conflict: { title: { pt: "CONFLITO", en: "CONFLICT" } },
75
110
  fracture: {
76
111
  title: { pt: "FRATURA", en: "FRACTURE" },
@@ -96,6 +131,19 @@ var RENDER_LABELS = {
96
131
  fractureWater: { pt: "fratura aberta", en: "open fracture" },
97
132
  cave: { pt: "caverna", en: "cave" },
98
133
  caveWater: { pt: "caverna \xFAmida", en: "wet cave" }
134
+ },
135
+ columnHeaders: {
136
+ depth: { pt: "PROF.", en: "DEPTH" },
137
+ lithology: { pt: "LIT.", en: "LITH." },
138
+ construction: { pt: "CONSTRU\xC7\xC3O", en: "CONSTRUCTION" },
139
+ description: { pt: "DESCRI\xC7\xC3O GEOL\xD3GICA", en: "GEOLOGICAL DESCRIPTION" }
140
+ },
141
+ continuation: { prefix: { pt: "continua\xE7\xE3o", en: "continued" } },
142
+ overlap: {
143
+ label: {
144
+ pt: "{range} repetido da p\xE1g. anterior",
145
+ en: "{range} repeated from previous page"
146
+ }
99
147
  }
100
148
  };
101
149
  function resolveGroup(group, locale) {
@@ -116,6 +164,8 @@ function resolveTooltipLabels(locale) {
116
164
  wellCase: resolveGroup(t.wellCase, locale),
117
165
  wellScreen: resolveGroup(t.wellScreen, locale),
118
166
  reduction: resolveGroup(t.reduction, locale),
167
+ centralizer: resolveGroup(t.centralizer, locale),
168
+ pump: resolveGroup(t.pump, locale),
119
169
  conflict: resolveGroup(t.conflict, locale),
120
170
  fracture: resolveGroup(t.fracture, locale),
121
171
  cementPad: resolveGroup(t.cementPad, locale),
@@ -137,7 +187,19 @@ function applyRenderLocale(config, locale) {
137
187
  tooltipLabels: resolveTooltipLabels(locale),
138
188
  labels: {
139
189
  ...config.labels,
140
- typeLabels: resolveGroup(RENDER_LABELS.typeLabels, locale)
190
+ typeLabels: resolveGroup(RENDER_LABELS.typeLabels, locale),
191
+ continuation: {
192
+ ...config.labels.continuation,
193
+ prefix: resolveRenderLabel(RENDER_LABELS.continuation.prefix, locale)
194
+ }
195
+ },
196
+ columnHeaders: {
197
+ ...config.columnHeaders,
198
+ labels: resolveGroup(RENDER_LABELS.columnHeaders, locale)
199
+ },
200
+ overlap: {
201
+ ...config.overlap,
202
+ label: resolveRenderLabel(RENDER_LABELS.overlap.label, locale)
141
203
  }
142
204
  };
143
205
  }
@@ -163,7 +225,7 @@ var DEFAULT_WELL_THEME = {
163
225
  crackStrokeWidth: 0.7
164
226
  }
165
227
  },
166
- cementPad: { stroke: "#303030", strokeWidth: 2 },
228
+ cementPad: { stroke: "#303030", strokeWidth: 1 },
167
229
  boreHole: {
168
230
  fill: "#ffffff",
169
231
  stroke: "#191919",
@@ -172,10 +234,24 @@ var DEFAULT_WELL_THEME = {
172
234
  strokeWidth: 1.2
173
235
  },
174
236
  surfaceCase: { stroke: "#000000", strokeWidth: 4 },
175
- holeFill: { stroke: "#303030", strokeWidth: 2 },
237
+ holeFill: { stroke: "#303030", strokeWidth: 1.2 },
176
238
  wellCase: { fill: "#ffffff", stroke: "#303030", strokeWidth: 2 },
177
- wellScreen: { stroke: "#303030", strokeWidth: 2 },
239
+ wellScreen: {
240
+ stroke: "#303030",
241
+ strokeWidth: 2,
242
+ endStrokeWidth: 0.75,
243
+ endDasharray: "3, 2"
244
+ },
178
245
  reduction: { fill: "#ffffff", stroke: "#303030", strokeWidth: 2 },
246
+ centralizer: { fill: "none", stroke: "#303030", strokeWidth: 1.4 },
247
+ pump: {
248
+ fill: "#5b6b7a",
249
+ stroke: "#202020",
250
+ strokeWidth: 1.2,
251
+ riserFill: "#a3adb7",
252
+ riserStroke: "#2f3a44",
253
+ riserStrokeWidth: 1
254
+ },
179
255
  conflict: { stroke: "#e52117", strokeWidth: 4 },
180
256
  unitLabels: {
181
257
  geologicFill: "#f0f0f0",
@@ -211,6 +287,39 @@ var DEFAULT_WELL_THEME = {
211
287
  headerFontWeight: 600,
212
288
  bodyFontWeight: 400
213
289
  },
290
+ overview: {
291
+ stroke: "#303030",
292
+ strokeWidth: 0.5,
293
+ emptyFill: "#d9d9d9",
294
+ windowStroke: "#c0392b",
295
+ windowStrokeWidth: 1.2
296
+ },
297
+ columnHeaders: {
298
+ fontSize: 6.5,
299
+ color: "#303030",
300
+ fontFamily: "sans-serif",
301
+ fontWeight: 600,
302
+ ruleStroke: "#101010",
303
+ ruleStrokeWidth: 1
304
+ },
305
+ markers: {
306
+ fill: "#ffffff",
307
+ stroke: "#101010",
308
+ strokeWidth: 0.6,
309
+ color: "#101010",
310
+ fontSize: 6,
311
+ fontFamily: "sans-serif",
312
+ fontWeight: 600
313
+ },
314
+ overlap: {
315
+ stroke: "#9a9a9a",
316
+ strokeWidth: 0.6,
317
+ size: 6,
318
+ ruleStroke: "#9a9a9a",
319
+ labelColor: "#555555",
320
+ fontSize: 6.5,
321
+ fontFamily: "monospace"
322
+ },
214
323
  legend: {
215
324
  borderStrokeWidth: 0.8,
216
325
  fractureStrokeWidth: 1.5,
@@ -268,8 +377,17 @@ var STATIC_RENDER_CONFIG = {
268
377
  }
269
378
  },
270
379
  construction: {
271
- cementPad: { widthMultiplier: 0.9, thicknessMultiplier: 1.3 },
272
- surfaceCase: { diameterPaddingRatio: 0.1 }
380
+ cementPad: { widthRatio: 1.4, height: 6 },
381
+ casingShoe: { active: true, size: 4 },
382
+ screenEnds: { active: true },
383
+ surfaceCase: { diameterPaddingRatio: 0.1 },
384
+ centralizer: { markerHalfHeight: 4 },
385
+ pump: {
386
+ active: true,
387
+ bodyHeight: 22,
388
+ bodyWidthRatio: 0.6,
389
+ riserWidthRatio: 0.3
390
+ }
273
391
  },
274
392
  tooltipLabels: resolveTooltipLabels("pt"),
275
393
  labels: {
@@ -282,7 +400,11 @@ var STATIC_RENDER_CONFIG = {
282
400
  stackingLineHeight: 10,
283
401
  stackingGap: 0,
284
402
  fractureLabelLeaderGap: 5,
285
- lithology: true
403
+ lithology: true,
404
+ continuation: {
405
+ active: false,
406
+ prefix: resolveRenderLabel(RENDER_LABELS.continuation.prefix, "pt")
407
+ }
286
408
  },
287
409
  unitLabels: {
288
410
  active: true,
@@ -340,10 +462,22 @@ var STATIC_RENDER_CONFIG = {
340
462
  wellCase: "Revestimento",
341
463
  wellScreen: "Filtro",
342
464
  reduction: "Redu\xE7\xE3o",
465
+ centralizer: "Centralizador",
466
+ pump: "Bomba e edutor",
343
467
  cementPad: "Laje de cimento",
344
468
  conflict: "Conflito"
345
469
  }
346
470
  },
471
+ overview: { active: false, x: 4, width: 8, height: 200 },
472
+ columnHeaders: {
473
+ active: false,
474
+ height: 14,
475
+ labels: resolveGroup(RENDER_LABELS.columnHeaders, "pt")
476
+ },
477
+ markers: { lithology: false, fractures: false, radius: 4.5 },
478
+ overlap: {
479
+ label: resolveRenderLabel(RENDER_LABELS.overlap.label, "pt")
480
+ },
347
481
  highlights: DEFAULT_HIGHLIGHT_CONFIG
348
482
  };
349
483
  var INTERACTIVE_RENDER_CONFIG = {
@@ -379,8 +513,17 @@ var INTERACTIVE_RENDER_CONFIG = {
379
513
  }
380
514
  },
381
515
  construction: {
382
- cementPad: { widthMultiplier: 0.9, thicknessMultiplier: 1.3 },
383
- surfaceCase: { diameterPaddingRatio: 0.1 }
516
+ cementPad: { widthRatio: 1.4, height: 6 },
517
+ casingShoe: { active: true, size: 4 },
518
+ screenEnds: { active: true },
519
+ surfaceCase: { diameterPaddingRatio: 0.1 },
520
+ centralizer: { markerHalfHeight: 4 },
521
+ pump: {
522
+ active: true,
523
+ bodyHeight: 22,
524
+ bodyWidthRatio: 0.6,
525
+ riserWidthRatio: 0.3
526
+ }
384
527
  },
385
528
  tooltipLabels: resolveTooltipLabels("pt"),
386
529
  labels: {
@@ -391,7 +534,11 @@ var INTERACTIVE_RENDER_CONFIG = {
391
534
  descriptionXOffset: 78,
392
535
  descriptionMaxWidth: 226,
393
536
  stackingLineHeight: 11,
394
- stackingGap: 0
537
+ stackingGap: 0,
538
+ continuation: {
539
+ active: false,
540
+ prefix: resolveRenderLabel(RENDER_LABELS.continuation.prefix, "pt")
541
+ }
395
542
  },
396
543
  unitLabels: {
397
544
  active: false,
@@ -448,19 +595,25 @@ var INTERACTIVE_RENDER_CONFIG = {
448
595
  wellCase: "Revestimento",
449
596
  wellScreen: "Filtro",
450
597
  reduction: "Redu\xE7\xE3o",
598
+ centralizer: "Centralizador",
599
+ pump: "Bomba e edutor",
451
600
  cementPad: "Laje de cimento",
452
601
  conflict: "Conflito"
453
602
  }
454
603
  },
604
+ overview: { active: false, x: 4, width: 8, height: 200 },
605
+ columnHeaders: {
606
+ active: false,
607
+ height: 14,
608
+ labels: resolveGroup(RENDER_LABELS.columnHeaders, "pt")
609
+ },
610
+ markers: { lithology: false, fractures: false, radius: 4.5 },
611
+ overlap: {
612
+ label: resolveRenderLabel(RENDER_LABELS.overlap.label, "pt")
613
+ },
455
614
  highlights: DEFAULT_HIGHLIGHT_CONFIG
456
615
  };
457
616
 
458
- // src/utils/fgdcTextures.ts
459
- async function importFgdcTextures() {
460
- const fgdcTextures = await import("./fgdcTextures-P5DTFNPT.js");
461
- return fgdcTextures.default;
462
- }
463
-
464
617
  // src/Renderer.ts
465
618
  import * as d3module2 from "d3";
466
619
  import d3tip2 from "d3-tip";
@@ -468,6 +621,8 @@ import { defu as defu2 } from "defu";
468
621
 
469
622
  // src/configs/render.textures.ts
470
623
  import textures from "textures";
624
+ var dot = (x, y, r) => `M${x - r},${y}a${r},${r} 0 1,0 ${2 * r},0a${r},${r} 0 1,0 ${-2 * r},0Z`;
625
+ var concreteAggregate = (s) => `M${s * 0.08},${s * 0.42}l${s * 0.3},0l${-s * 0.12},${-s * 0.26}ZM${s * 0.55},${s * 0.92}l${s * 0.26},${-s * 0.06}l${-s * 0.16},${-s * 0.2}Z${dot(s * 0.72, s * 0.24, s * 0.06)}${dot(s * 0.22, s * 0.78, s * 0.05)}`;
471
626
  function createWellTextures(cfg) {
472
627
  const pad = {
473
628
  background: "#ffffff",
@@ -515,7 +670,9 @@ function createWellTextures(cfg) {
515
670
  ...cfg?.surface_case
516
671
  };
517
672
  return {
518
- pad: textures.lines().heavier(10).thinner(1.5).stroke(pad.stroke).background(pad.background),
673
+ // Concrete: aggregate (small triangles and dots) on a light ground, so it
674
+ // reads apart from the seal's diagonals and the gravel pack's circles.
675
+ pad: textures.paths().d(concreteAggregate).size(6).strokeWidth(0.4).stroke(pad.stroke).fill(pad.stroke).background(pad.background),
519
676
  conflict: textures.lines().heavier().stroke(conflict.stroke),
520
677
  cave_dry: textures.lines().size(cave_dry.size).orientation("6/8").heavier(0.3).thinner(0.8).background(cave_dry.background).stroke(cave_dry.stroke),
521
678
  cave_wet: textures.lines().size(cave_wet.size).orientation("6/8").heavier(0.3).thinner(0.8).background(cave_wet.background).stroke(cave_wet.stroke),
@@ -531,8 +688,8 @@ import {
531
688
  isWellEmpty
532
689
  } from "@welldot/core";
533
690
  import {
534
- formatNumber as formatNumber3,
535
- getProfileDiamValues as getProfileDiamValues4,
691
+ formatNumber as formatNumber4,
692
+ getCurrentPump as getCurrentPump2,
536
693
  getProfileLastItemsDepths
537
694
  } from "@welldot/utils";
538
695
 
@@ -592,6 +749,14 @@ var DEFAULT_COMPONENTS_CLASS_NAMES = {
592
749
  group: "well-screen",
593
750
  rect: "well-screen-rect"
594
751
  },
752
+ centralizer: {
753
+ group: "centralizer",
754
+ item: "centralizer-marker"
755
+ },
756
+ pump: {
757
+ group: "pump",
758
+ item: "pump-item"
759
+ },
595
760
  reduction: {
596
761
  group: "reduction",
597
762
  item: "reduction-poly"
@@ -623,6 +788,26 @@ var DEFAULT_COMPONENTS_CLASS_NAMES = {
623
788
  caveFill: "cave-fill",
624
789
  constructionRect: "construction-rect"
625
790
  },
791
+ overview: {
792
+ group: "wp-overview",
793
+ item: "wp-overview-item",
794
+ window: "wp-overview-window"
795
+ },
796
+ columnHeaders: {
797
+ group: "wp-column-headers",
798
+ text: "wp-column-header",
799
+ rule: "wp-column-headers-rule"
800
+ },
801
+ markers: {
802
+ group: "wp-markers",
803
+ lithology: "wp-marker-lithology",
804
+ fracture: "wp-marker-fracture"
805
+ },
806
+ overlap: {
807
+ group: "wp-overlap",
808
+ band: "wp-overlap-band",
809
+ label: "wp-overlap-label"
810
+ },
626
811
  highlights: {
627
812
  geologicGroup: "wp-highlights-geologic",
628
813
  constructionGroup: "wp-highlights-construction",
@@ -637,6 +822,156 @@ var DEFAULT_COMPONENTS_CLASS_NAMES = {
637
822
  // src/utils/render.utils.ts
638
823
  import textures2 from "textures";
639
824
 
825
+ // src/utils/construction.geometry.ts
826
+ var JOINT_EPS = 1e-3;
827
+ function diameterAt(items, depth) {
828
+ return items.find((i) => depth >= i.from && depth <= i.to)?.diameter;
829
+ }
830
+ function getIntervalEnds(item, string, depthFrom, depthTo) {
831
+ return {
832
+ cutTop: item.from < depthFrom - JOINT_EPS,
833
+ cutBottom: item.to > depthTo + JOINT_EPS,
834
+ joinedTop: string.some(
835
+ (o) => o !== item && Math.abs(o.to - item.from) < JOINT_EPS
836
+ ),
837
+ joinedBottom: string.some(
838
+ (o) => o !== item && Math.abs(o.from - item.to) < JOINT_EPS
839
+ )
840
+ };
841
+ }
842
+ function pipeWallsPath(xLeft, xRight, yTop, yBottom) {
843
+ return `M${xLeft},${yTop}V${yBottom}M${xRight},${yTop}V${yBottom}`;
844
+ }
845
+ function shoePath(xLeft, xRight, y, size) {
846
+ return `M${xLeft},${y - size}L${xLeft},${y}L${xLeft - size},${y}ZM${xRight},${y - size}L${xRight},${y}L${xRight + size},${y}Z`;
847
+ }
848
+ function screenEndPath(xLeft, xRight, y) {
849
+ return `M${xLeft},${y}H${xRight}`;
850
+ }
851
+ var clampDepth = (s, depth) => Math.min(Math.max(depth, s.depthFrom), s.depthTo);
852
+ var inPanel = (s, depth) => depth >= s.depthFrom - JOINT_EPS && depth <= s.depthTo + JOINT_EPS;
853
+ var shoulders = (s, a, b, y) => `M${s.left(a)},${y}H${s.left(b)}M${s.right(a)},${y}H${s.right(b)}`;
854
+ function boreHoleOutlinePath(item, holes, s) {
855
+ const yTop = s.y(clampDepth(s, item.from));
856
+ const yBottom = s.y(clampDepth(s, item.to));
857
+ let d = pipeWallsPath(
858
+ s.left(item.diameter),
859
+ s.right(item.diameter),
860
+ yTop,
861
+ yBottom
862
+ );
863
+ if (item.from > JOINT_EPS && inPanel(s, item.from)) {
864
+ const above = holes.find(
865
+ (o) => o !== item && o.from < item.from && o.to >= item.from - JOINT_EPS
866
+ );
867
+ if (above && above.diameter < item.diameter)
868
+ d += shoulders(s, above.diameter, item.diameter, yTop);
869
+ }
870
+ if (inPanel(s, item.to)) {
871
+ const below = holes.find(
872
+ (o) => o !== item && o.from <= item.to + JOINT_EPS && o.to > item.to
873
+ );
874
+ if (!below) {
875
+ d += `M${s.left(item.diameter)},${yBottom}H${s.right(item.diameter)}`;
876
+ } else if (below.diameter < item.diameter) {
877
+ d += shoulders(s, item.diameter, below.diameter, yBottom);
878
+ }
879
+ }
880
+ return d;
881
+ }
882
+ function holeFillOutlinePath(item, pipes, s) {
883
+ const yTop = s.y(clampDepth(s, item.from));
884
+ const yBottom = s.y(clampDepth(s, item.to));
885
+ const xL = s.left(item.diameter);
886
+ const xR = s.right(item.diameter);
887
+ let d = pipeWallsPath(xL, xR, yTop, yBottom);
888
+ const interfaces = [item.from, item.to].filter(
889
+ (depth) => depth > JOINT_EPS && inPanel(s, depth)
890
+ );
891
+ for (const depth of interfaces) {
892
+ const y = s.y(depth);
893
+ const pipe = diameterAt(pipes, depth);
894
+ if (pipe !== void 0 && pipe < item.diameter) {
895
+ d += `M${xL},${y}H${s.left(pipe)}M${s.right(pipe)},${y}H${xR}`;
896
+ } else {
897
+ d += `M${xL},${y}H${xR}`;
898
+ }
899
+ }
900
+ return d;
901
+ }
902
+ function pipeSlices(pipes, from, to) {
903
+ const cuts = [.../* @__PURE__ */ new Set([from, to, ...pipes.flatMap((p) => [p.from, p.to])])].filter((d) => d >= from && d <= to).sort((a, b) => a - b);
904
+ const slices = [];
905
+ for (let i = 1; i < cuts.length; i++) {
906
+ const a = cuts[i - 1];
907
+ const b = cuts[i];
908
+ const mid = (a + b) / 2;
909
+ const widest = Math.max(
910
+ 0,
911
+ ...pipes.filter((p) => p.from <= mid && p.to >= mid).map((p) => p.diameter)
912
+ );
913
+ if (widest === 0) continue;
914
+ const last = slices[slices.length - 1];
915
+ if (last && last.to === a && last.diameter === widest) last.to = b;
916
+ else slices.push({ from: a, to: b, diameter: widest });
917
+ }
918
+ return slices;
919
+ }
920
+ function annulusFillPath(item, pipes, s) {
921
+ const from = clampDepth(s, item.from);
922
+ const to = clampDepth(s, item.to);
923
+ const box = (dia, a, b) => `M${s.left(dia)},${s.y(a)}H${s.right(dia)}V${s.y(b)}H${s.left(dia)}Z`;
924
+ const inner = pipes.filter((p) => p.diameter < item.diameter);
925
+ return box(item.diameter, from, to) + pipeSlices(inner, from, to).map((p) => box(p.diameter, p.from, p.to)).join("");
926
+ }
927
+ function getPadDiameters(data, surfaceCasePaddingRatio) {
928
+ const surfaceCase = diameterAt(data.surface_case, 0);
929
+ const paddedSurfaceCase = surfaceCase === void 0 ? void 0 : surfaceCase * (1 + surfaceCasePaddingRatio);
930
+ const casing = diameterAt([...data.well_case, ...data.well_screen], 0);
931
+ const hole = diameterAt(data.bore_hole, 0);
932
+ const opening = casing ?? paddedSurfaceCase ?? hole ?? 0;
933
+ const outer = Math.max(
934
+ opening,
935
+ paddedSurfaceCase ?? 0,
936
+ hole ?? 0,
937
+ diameterAt(data.hole_fill, 0) ?? 0
938
+ );
939
+ return { opening, outer };
940
+ }
941
+ function getCementPadGeometry(pocoWidth, pocoCenter, surfaceY, config, openingWidth = 0, outerWidth = openingWidth) {
942
+ const h = config.height;
943
+ const opening = Math.max(0, openingWidth);
944
+ const width = Math.max(
945
+ pocoWidth * config.widthRatio,
946
+ opening + 3 * h,
947
+ outerWidth + 2 * h
948
+ );
949
+ const xLeft = (pocoCenter - width) / 2;
950
+ const xRight = xLeft + width;
951
+ const oLeft = (pocoCenter - opening) / 2;
952
+ const oRight = oLeft + opening;
953
+ const yTop = surfaceY - h;
954
+ const a = h * 0.3;
955
+ const breakLine = (x, y0, y1, dir) => `L${x + dir * a},${y0 + (y1 - y0) * 0.4}L${x - dir * a},${y0 + (y1 - y0) * 0.6}L${x},${y1}`;
956
+ const bbox = { x: xLeft, y: yTop, width, height: h };
957
+ if (opening === 0) {
958
+ return {
959
+ bbox,
960
+ slabs: [
961
+ `M${xLeft},${yTop}H${xRight}${breakLine(xRight, yTop, surfaceY, 1)}H${xLeft}${breakLine(xLeft, surfaceY, yTop, -1)}Z`,
962
+ ""
963
+ ]
964
+ };
965
+ }
966
+ return {
967
+ bbox,
968
+ slabs: [
969
+ `M${oLeft},${yTop}H${xLeft}${breakLine(xLeft, yTop, surfaceY, -1)}H${oLeft}Z`,
970
+ `M${oRight},${yTop}H${xRight}${breakLine(xRight, yTop, surfaceY, 1)}H${oRight}Z`
971
+ ]
972
+ };
973
+ }
974
+
640
975
  // src/utils/d3.utils.ts
641
976
  function mergeEnter(enter, update) {
642
977
  return enter.merge(update);
@@ -707,6 +1042,14 @@ var makeSeededPrng = (seed, scramble = 6271) => {
707
1042
  // src/utils/tooltips.utils.ts
708
1043
  import d3tip from "d3-tip";
709
1044
  import sanitizeHtml from "sanitize-html";
1045
+ import {
1046
+ CEMENT_PAD_TYPES,
1047
+ CENTRALIZER_TYPES,
1048
+ CONSTRUCTION_MATERIALS,
1049
+ SCREEN_SLOT_TYPES,
1050
+ getVocabLabel,
1051
+ powerFromCanonical
1052
+ } from "@welldot/core";
710
1053
  import { formatNumber as formatNumber2 } from "@welldot/utils";
711
1054
  var esc = (v) => sanitizeHtml(String(v ?? ""), { allowedTags: [], allowedAttributes: {} });
712
1055
  var _tooltipCache = /* @__PURE__ */ new WeakMap();
@@ -717,6 +1060,7 @@ var populateTooltips = (svg, customClasses, units, tooltipConfig, labels, locale
717
1060
  if (cached && cached.signature === signature) {
718
1061
  return cached.tooltips;
719
1062
  }
1063
+ const material = (value) => getVocabLabel(CONSTRUCTION_MATERIALS, value, locale);
720
1064
  const tipsText = {
721
1065
  geology: (_, d) => `
722
1066
  <span class="${customClasses.tooltip.title}">${labels.geology.title}</span>
@@ -749,14 +1093,15 @@ var populateTooltips = (svg, customClasses, units, tooltipConfig, labels, locale
749
1093
  <span class="${customClasses.tooltip.secondaryInfo}">
750
1094
  <strong>${labels.common.diameter}</strong> ${esc(formatDiameter(d.diameter, units.diameter, locale))} ${esc(getDiameterUnit(units.diameter, locale))}
751
1095
  </span>
752
- <span class="${customClasses.tooltip.secondaryInfo}"><strong>${labels.common.type}</strong> ${esc(d.type)}</span>
1096
+ <span class="${customClasses.tooltip.secondaryInfo}"><strong>${labels.common.type}</strong> ${esc(material(d.type))}</span>
753
1097
  `,
754
1098
  wellScreen: (_, d) => `
755
1099
  <span class="${customClasses.tooltip.title}">${labels.wellScreen.title}</span>
756
1100
  <span class="${customClasses.tooltip.primaryInfo}">${labels.common.from} ${esc(formatLength(d.from, units.length, locale))} ${esc(getLengthUnit(units.length))} ${labels.common.to} ${esc(formatLength(d.to, units.length, locale))} ${esc(getLengthUnit(units.length))}</span>
757
1101
  <span class="${customClasses.tooltip.secondaryInfo}">
758
1102
  <strong>${labels.common.diameter}</strong> ${esc(formatDiameter(d.diameter, units.diameter, locale))} ${esc(getDiameterUnit(units.diameter, locale))}</span>
759
- <span class="${customClasses.tooltip.secondaryInfo}"><strong>${labels.common.type}</strong> ${esc(d.type)}</span>
1103
+ <span class="${customClasses.tooltip.secondaryInfo}"><strong>${labels.wellScreen.material}</strong> ${esc(material(d.type))}</span>
1104
+ ${d.slot_type ? `<span class="${customClasses.tooltip.secondaryInfo}"><strong>${labels.wellScreen.slotType}</strong> ${esc(getVocabLabel(SCREEN_SLOT_TYPES, d.slot_type, locale))}</span>` : ""}
760
1105
  <span class="${customClasses.tooltip.secondaryInfo}">
761
1106
  <strong>${labels.wellScreen.slot}</strong> ${esc(formatNumber2(d.screen_slot, { maximumFractionDigits: 2, locale }))} mm
762
1107
  </span>
@@ -767,8 +1112,29 @@ var populateTooltips = (svg, customClasses, units, tooltipConfig, labels, locale
767
1112
  <span class="${customClasses.tooltip.secondaryInfo}">
768
1113
  <strong>${labels.common.diameter}</strong> ${esc(formatDiameter(d.diam_from, units.diameter, locale))} \u2192 ${esc(formatDiameter(d.diam_to, units.diameter, locale))} ${esc(getDiameterUnit(units.diameter, locale))}
769
1114
  </span>
770
- <span class="${customClasses.tooltip.secondaryInfo}"><strong>${labels.common.type}</strong> ${esc(d.type)}</span>
1115
+ <span class="${customClasses.tooltip.secondaryInfo}"><strong>${labels.common.type}</strong> ${esc(material(d.type))}</span>
771
1116
  `,
1117
+ centralizer: (_, d) => `
1118
+ <span class="${customClasses.tooltip.title}">${labels.centralizer.title}</span>
1119
+ <span class="${customClasses.tooltip.primaryInfo}"><strong>${labels.centralizer.depth}</strong> ${esc(formatLength(d.depth, units.length, locale))} ${esc(getLengthUnit(units.length))}</span>
1120
+ ${d.from !== d.to ? `<span class="${customClasses.tooltip.secondaryInfo}">${labels.common.from} ${esc(formatLength(d.from, units.length, locale))} ${esc(getLengthUnit(units.length))} ${labels.common.to} ${esc(formatLength(d.to, units.length, locale))} ${esc(getLengthUnit(units.length))}</span>` : ""}
1121
+ ${d.spacing ? `<span class="${customClasses.tooltip.secondaryInfo}"><strong>${labels.centralizer.spacing}</strong> ${esc(formatLength(d.spacing, units.length, locale))} ${esc(getLengthUnit(units.length))}</span>` : ""}
1122
+ <span class="${customClasses.tooltip.secondaryInfo}"><strong>${labels.common.type}</strong> ${esc(getVocabLabel(CENTRALIZER_TYPES, d.type, locale))}</span>
1123
+ ${d.diameter ? `<span class="${customClasses.tooltip.secondaryInfo}"><strong>${labels.common.diameter}</strong> ${esc(formatDiameter(d.diameter, units.diameter, locale))} ${esc(getDiameterUnit(units.diameter, locale))}</span>` : ""}
1124
+ ${d.description ? `<span class="${customClasses.tooltip.secondaryInfo}"><strong>${labels.common.description}</strong> ${esc(d.description)}</span>` : ""}
1125
+ `,
1126
+ pump: (_, d) => {
1127
+ const typeLabel = labels.pump[`type_${d.type}`] ?? d.type;
1128
+ const model = [d.manufacturer, d.model].filter(Boolean).join(" ");
1129
+ return `
1130
+ <span class="${customClasses.tooltip.title}">${labels.pump.title}</span>
1131
+ <span class="${customClasses.tooltip.primaryInfo}">${esc(typeLabel)}</span>
1132
+ ${d.intake_depth !== void 0 ? `<span class="${customClasses.tooltip.secondaryInfo}"><strong>${labels.pump.intakeDepth}</strong> ${esc(formatLength(d.intake_depth, units.length, locale))} ${esc(getLengthUnit(units.length))}</span>` : ""}
1133
+ ${model ? `<span class="${customClasses.tooltip.secondaryInfo}"><strong>${labels.pump.model}</strong> ${esc(model)}</span>` : ""}
1134
+ ${d.rated_power !== void 0 ? `<span class="${customClasses.tooltip.secondaryInfo}"><strong>${labels.pump.power}</strong> ${esc(formatNumber2(powerFromCanonical(d.rated_power, units.power ?? "kW"), { maximumFractionDigits: 2, locale }))} ${esc(units.power ?? "kW")}</span>` : ""}
1135
+ ${d.riser_diameter !== void 0 ? `<span class="${customClasses.tooltip.secondaryInfo}"><strong>${labels.pump.riser}</strong> ${esc(formatDiameter(d.riser_diameter, units.diameter, locale))} ${esc(getDiameterUnit(units.diameter, locale))}${d.riser_material ? ` \xB7 ${esc(material(d.riser_material))}` : ""}</span>` : ""}
1136
+ `;
1137
+ },
772
1138
  conflict: (_, d) => `
773
1139
  <span class="${customClasses.tooltip.title}">${labels.conflict.title}</span>
774
1140
  <span class="${customClasses.tooltip.primaryInfo}">${labels.common.from} ${esc(formatLength(d.from, units.length, locale))} ${esc(getLengthUnit(units.length))} ${labels.common.to} ${esc(formatLength(d.to, units.length, locale))} ${esc(getLengthUnit(units.length))}</span>
@@ -779,14 +1145,14 @@ var populateTooltips = (svg, customClasses, units, tooltipConfig, labels, locale
779
1145
  <span class="${customClasses.tooltip.title}">${title}</span>
780
1146
  <span class="${customClasses.tooltip.primaryInfo}"><strong>${labels.fracture.depth}</strong> ${esc(formatLength(d.depth, units.length, locale))} ${esc(getLengthUnit(units.length))}</span>
781
1147
  ${d.water_intake ? `<span class="${customClasses.tooltip.secondaryInfo}"><strong>${labels.fracture.waterIntake}</strong> ${esc(formatLength(d.depth, units.length, locale))} ${esc(getLengthUnit(units.length))}</span>` : ""}
782
- <span class="${customClasses.tooltip.secondaryInfo}"><strong>${labels.fracture.dip}</strong> ${esc(d.dip)}\xB0</span>
783
- <span class="${customClasses.tooltip.secondaryInfo}"><strong>${labels.fracture.azimuth}</strong> ${esc(d.azimuth)}\xB0</span>
1148
+ ${typeof d.dip === "number" ? `<span class="${customClasses.tooltip.secondaryInfo}"><strong>${labels.fracture.dip}</strong> ${esc(d.dip)}\xB0</span>` : ""}
1149
+ ${typeof d.azimuth === "number" ? `<span class="${customClasses.tooltip.secondaryInfo}"><strong>${labels.fracture.azimuth}</strong> ${esc(d.azimuth)}\xB0</span>` : ""}
784
1150
  ${d.description ? `<span class="${customClasses.tooltip.secondaryInfo}"><strong>${labels.common.description}</strong> ${esc(d.description)}</span>` : ""}
785
1151
  `;
786
1152
  },
787
1153
  cementPad: (_, d) => `
788
1154
  <span class="${customClasses.tooltip.title}">${labels.cementPad.title}</span>
789
- <span class="${customClasses.tooltip.primaryInfo}">${esc(d.type)}</span>
1155
+ <span class="${customClasses.tooltip.primaryInfo}">${esc(getVocabLabel(CEMENT_PAD_TYPES, d.type, locale))}</span>
790
1156
  <span class="${customClasses.tooltip.secondaryInfo}"><strong>${labels.cementPad.thickness}</strong>
791
1157
  ${esc(formatLength(d.thickness, units.length, locale))} ${esc(getLengthUnit(units.length))}</span>
792
1158
  <span class="${customClasses.tooltip.secondaryInfo}">
@@ -824,6 +1190,17 @@ var populateTooltips = (svg, customClasses, units, tooltipConfig, labels, locale
824
1190
  };
825
1191
 
826
1192
  // src/utils/render.utils.ts
1193
+ var fractureDip = (dip) => typeof dip === "number" && Number.isFinite(dip) ? dip : 0;
1194
+ function getCementPadRect(pocoWidth, pocoCenter, surfaceY, config, openingWidth = 0, outerWidth = openingWidth) {
1195
+ return getCementPadGeometry(
1196
+ pocoWidth,
1197
+ pocoCenter,
1198
+ surfaceY,
1199
+ config,
1200
+ openingWidth,
1201
+ outerWidth
1202
+ ).bbox;
1203
+ }
827
1204
  var _fgdcTextures = {};
828
1205
  var _fgdcTexturesPromise = null;
829
1206
  var preloadFgdcTextures = () => {
@@ -932,14 +1309,23 @@ function drawAnnotationLabels(ctx, data) {
932
1309
  }
933
1310
  const items = [];
934
1311
  if (active.has("lithology")) {
1312
+ const continuation = s.continuation;
935
1313
  data.lithology.filter((d) => d.from < ctx.depthTo).forEach((d) => {
1314
+ const range = `${formatLength(d.from, ctx.units.length, ctx.locale)}\u2013${formatLength(d.to, ctx.units.length, ctx.locale)}${getLengthUnit(ctx.units.length)}`;
1315
+ const continues = continuation?.active && d.from < ctx.depthFrom;
936
1316
  items.push({
937
1317
  sortDepth: d.from,
938
1318
  baseY: ctx.yScale(Math.max(d.from, ctx.depthFrom)),
939
1319
  originX: ctx.geoXRight,
940
1320
  originY: ctx.yScale(Math.max(d.from, ctx.depthFrom)),
941
- header: `${formatLength(d.from, ctx.units.length, ctx.locale)}\u2013${formatLength(d.to, ctx.units.length, ctx.locale)}${getLengthUnit(ctx.units.length)}`,
942
- description: d.description || ""
1321
+ header: continues ? `\u2191 ${continuation.prefix} \xB7 ${range}` : range,
1322
+ description: continues ? "" : d.description || "",
1323
+ ...show.has("units") ? {
1324
+ unit: d.geologic_unit || void 0,
1325
+ aquifer: continues ? void 0 : d.aquifer_unit || void 0
1326
+ } : {},
1327
+ ...show.has("swatch") && !continues ? { swatch: d } : {},
1328
+ ...show.has("rules") ? { rule: continues ? "dashed" : "solid" } : {}
943
1329
  });
944
1330
  });
945
1331
  }
@@ -952,7 +1338,8 @@ function drawAnnotationLabels(ctx, data) {
952
1338
  originX: ctx.pocoCenterX,
953
1339
  originY: ctx.yScale(d.depth),
954
1340
  header: `${formatLength(d.depth, ctx.units.length, ctx.locale)}${getLengthUnit(ctx.units.length)} \xB7 ${typeLabel}`,
955
- description: d.description || ""
1341
+ description: d.description || "",
1342
+ ...show.has("rules") ? { rule: "dashed" } : {}
956
1343
  });
957
1344
  });
958
1345
  }
@@ -972,14 +1359,15 @@ function drawAnnotationLabels(ctx, data) {
972
1359
  }
973
1360
  items.sort((a, b) => a.sortDepth - b.sortDepth);
974
1361
  const labelPositions = [];
975
- let currY = 0;
1362
+ let currY = ctx.repeatedTo != null ? s.stackingLineHeight + 2 : 0;
976
1363
  const charsPerLine = Math.max(
977
1364
  1,
978
1365
  Math.floor(s.descriptionMaxWidth / (st.fontSize * 0.6))
979
1366
  );
980
1367
  items.forEach((item) => {
981
- const descLines = Math.ceil((item.description.length || 1) / charsPerLine);
982
- const estH = (1 + descLines) * s.stackingLineHeight + 4;
1368
+ const descLines = show.has("units") ? item.description ? wrapText(item.description, charsPerLine).length : 0 : Math.ceil((item.description.length || 1) / charsPerLine);
1369
+ const extraLines = (item.unit ? 1 : 0) + (item.aquifer ? 1 : 0);
1370
+ const estH = (1 + descLines + extraLines) * s.stackingLineHeight + 4;
983
1371
  const yLabel = Math.max(item.baseY, currY + s.stackingGap);
984
1372
  currY = yLabel + estH;
985
1373
  labelPositions.push({ item, yLabel, estH });
@@ -996,16 +1384,41 @@ function drawAnnotationLabels(ctx, data) {
996
1384
  if (show.has("description")) {
997
1385
  const lineH = s.stackingLineHeight;
998
1386
  const bgFill = st.annotationBg ?? "white";
1387
+ const swatchFill = labelPositions.some((p) => p.item.swatch) ? getLithologyFill(data.lithology, ctx.svg, ctx.theme.lithologyTexture) : null;
999
1388
  labelPositions.forEach(({ item, yLabel, estH }) => {
1000
1389
  const labelG = ctx.groups.lithologyLabelsGroup.append("g").attr("class", ctx.classes.labels.lithology.label).attr("transform", `translate(${labelX},${yLabel})`);
1001
1390
  labelG.append("rect").attr("x", 0).attr("y", 0).attr("width", s.descriptionMaxWidth).attr("height", estH).attr("rx", st.annotationRadius ?? 2).attr("fill", bgFill).attr("fill-opacity", st.annotationBgOpacity ?? 0.85).attr("stroke", st.annotationBorderColor ?? "none");
1002
- labelG.append("text").attr("class", "wp-annotation-header").attr("fill", ctx.theme.labels.color).attr("font-weight", st.headerFontWeight ?? 600).attr("font-family", ctx.theme.labels.headerFont).attr("x", 0).attr("y", lineH * 0.85).attr("font-size", st.fontSize).text(item.header);
1391
+ if (item.rule) {
1392
+ labelG.append("line").attr("x1", 0).attr("x2", s.descriptionMaxWidth).attr("y1", 0).attr("y2", 0).attr("stroke", ctx.theme.labels.color).attr(
1393
+ "stroke-width",
1394
+ item.rule === "solid" ? 0.8 : ctx.theme.labels.dividerStrokeWidth
1395
+ ).attr(
1396
+ "stroke-dasharray",
1397
+ item.rule === "solid" ? null : ctx.theme.labels.dividerStrokeDasharray
1398
+ );
1399
+ }
1400
+ let headerX = 0;
1401
+ if (item.swatch && swatchFill) {
1402
+ const size = lineH * 0.75;
1403
+ labelG.append("rect").attr("x", 0).attr("y", lineH * 0.85 - size + 1).attr("width", size).attr("height", size).attr("fill", swatchFill(item.swatch)).attr("stroke", ctx.theme.lithology.stroke).attr("stroke-width", 0.5);
1404
+ headerX = size + 3;
1405
+ }
1406
+ labelG.append("text").attr("class", "wp-annotation-header").attr("fill", ctx.theme.labels.color).attr("font-weight", st.headerFontWeight ?? 600).attr("font-family", ctx.theme.labels.headerFont).attr("x", headerX).attr("y", lineH * 0.85).attr("font-size", st.fontSize).text(item.header);
1407
+ let lineY = lineH * 0.85 + lineH;
1408
+ if (item.unit) {
1409
+ labelG.append("text").attr("class", "wp-annotation-unit").attr("fill", ctx.theme.labels.color).attr("font-weight", st.headerFontWeight ?? 600).attr("font-family", ctx.theme.labels.bodyFont ?? "sans-serif").attr("x", 0).attr("y", lineY).attr("font-size", st.fontSize).text(item.unit);
1410
+ lineY += lineH;
1411
+ }
1003
1412
  if (item.description) {
1004
1413
  const lines = wrapText(item.description, charsPerLine);
1005
- const bodyEl = labelG.append("text").attr("class", "wp-annotation-body").attr("fill", ctx.theme.labels.bodyColor).attr("font-weight", st.bodyFontWeight ?? 400).attr("font-family", ctx.theme.labels.bodyFont ?? "sans-serif").attr("x", 0).attr("y", lineH * 0.85 + lineH).attr("font-size", st.fontSize);
1414
+ const bodyEl = labelG.append("text").attr("class", "wp-annotation-body").attr("fill", ctx.theme.labels.bodyColor).attr("font-weight", st.bodyFontWeight ?? 400).attr("font-family", ctx.theme.labels.bodyFont ?? "sans-serif").attr("x", 0).attr("y", lineY).attr("font-size", st.fontSize);
1006
1415
  lines.forEach((line, i) => {
1007
1416
  bodyEl.append("tspan").attr("x", 0).attr("dy", i === 0 ? 0 : lineH).text(line);
1008
1417
  });
1418
+ lineY += lines.length * lineH;
1419
+ }
1420
+ if (item.aquifer) {
1421
+ labelG.append("text").attr("class", "wp-annotation-aquifer").attr("fill", ctx.theme.labels.bodyColor).attr("font-style", "italic").attr("font-family", ctx.theme.labels.bodyFont ?? "sans-serif").attr("x", 0).attr("y", lineY).attr("font-size", st.fontSize).text(item.aquifer);
1009
1422
  }
1010
1423
  });
1011
1424
  }
@@ -1026,7 +1439,6 @@ var makePointKey = (type) => (d, i) => {
1026
1439
 
1027
1440
  // src/renderers/caves.renderer.ts
1028
1441
  function drawCaves(ctx, data) {
1029
- if (!data.length) return;
1030
1442
  const {
1031
1443
  groups: { cavesGroup },
1032
1444
  geoXLeft,
@@ -1036,7 +1448,7 @@ function drawCaves(ctx, data) {
1036
1448
  yScale,
1037
1449
  renderConfig,
1038
1450
  theme,
1039
- textures: textures3,
1451
+ textures: textures4,
1040
1452
  classes,
1041
1453
  tooltips
1042
1454
  } = ctx;
@@ -1073,7 +1485,7 @@ function drawCaves(ctx, data) {
1073
1485
  const botPath = botCut ? `M ${geoXLeft.toFixed(1)},${yBot.toFixed(1)} L ${geoXRight.toFixed(1)},${yBot.toFixed(1)}` : ptsToSmoothPath(botPts);
1074
1486
  const botReversed = botCut ? `L ${geoXLeft.toFixed(1)},${yBot.toFixed(1)}` : ptsToSmoothPath([...botPts].reverse(), { omitMoveTo: true });
1075
1487
  const closedPath = `${topPath} L ${geoXRight.toFixed(1)},${yBot.toFixed(1)}${botReversed} L ${geoXLeft.toFixed(1)},${yTop.toFixed(1)} Z`;
1076
- const caveTexture = cave.water_intake ? textures3.cave_wet : textures3.cave_dry;
1488
+ const caveTexture = cave.water_intake ? textures4.cave_wet : textures4.cave_dry;
1077
1489
  const caveStroke = cave.water_intake ? theme.cave.wetStroke : theme.cave.dryStroke;
1078
1490
  g.append("path").attr("class", classes.caves.fill).attr("d", closedPath).attr("fill", caveTexture.url()).attr("fill-opacity", theme.cave.fillOpacity).attr("stroke", "none");
1079
1491
  g.append("path").attr("class", classes.caves.contact).attr("d", topPath).attr("fill", "none").attr("stroke", caveStroke).attr("stroke-width", theme.cave.contactStrokeWidth).attr("stroke-linecap", "round").attr("stroke-linejoin", "round");
@@ -1082,9 +1494,148 @@ function drawCaves(ctx, data) {
1082
1494
  merged.transition(ctx.transition).style("opacity", 1);
1083
1495
  }
1084
1496
 
1085
- // src/renderers/construction-labels.renderer.ts
1497
+ // src/renderers/centralizers.renderer.ts
1086
1498
  import * as d32 from "d3";
1087
- import { getProfileDiamValues } from "@welldot/utils";
1499
+ import { getCentralizerDepths, getProfileDiamValues } from "@welldot/utils";
1500
+ function drawCentralizers(ctx, data) {
1501
+ const group = ctx.groups.centralizerGroup;
1502
+ const cls = ctx.classes.centralizer;
1503
+ const maxXValue = d32.max(getProfileDiamValues(ctx.constructionData)) || 0;
1504
+ const xScale = d32.scaleLinear().domain([0, maxXValue]).range([0, ctx.POCO_WIDTH]);
1505
+ const pipes = [
1506
+ ...ctx.constructionData.well_case,
1507
+ ...ctx.constructionData.well_screen
1508
+ ];
1509
+ const holes = ctx.constructionData.bore_hole;
1510
+ const markers = (data.centralizers ?? []).flatMap(
1511
+ (c) => getCentralizerDepths(c).filter((depth) => depth >= ctx.depthFrom && depth < ctx.depthTo).map((depth) => ({ ...c, depth }))
1512
+ );
1513
+ const halfH = ctx.renderConfig.construction.centralizer.markerHalfHeight;
1514
+ const bowPoints = (m, side) => {
1515
+ const outerD = m.diameter ?? diameterAt(holes, m.depth) ?? maxXValue;
1516
+ const pipeD = diameterAt(pipes, m.depth) ?? outerD * 0.75;
1517
+ const pipeX = (ctx.POCO_CENTER + side * xScale(pipeD)) / 2;
1518
+ const outerX = (ctx.POCO_CENTER + side * xScale(outerD)) / 2;
1519
+ const y = ctx.yScale(m.depth);
1520
+ return `${pipeX},${y - halfH} ${outerX},${y} ${pipeX},${y + halfH}`;
1521
+ };
1522
+ const selection = group.selectAll(`g.${cls.item}`).data(markers, (d) => `${d.from}:${d.to}:${d.depth}`);
1523
+ selection.exit().remove();
1524
+ const entered = selection.enter().append("g").attr("class", cls.item).on("mouseover", ctx.tooltips.centralizer.show).on("mouseout", ctx.tooltips.centralizer.hide);
1525
+ entered.append("polyline").attr("class", `${cls.item}-left`);
1526
+ entered.append("polyline").attr("class", `${cls.item}-right`);
1527
+ const merged = entered.merge(selection);
1528
+ merged.selectAll("polyline").attr("fill", ctx.theme.centralizer.fill).attr("stroke", ctx.theme.centralizer.stroke).attr("stroke-width", ctx.theme.centralizer.strokeWidth).attr("stroke-linejoin", "round");
1529
+ merged.each((d, i, nodes) => {
1530
+ const g = d32.select(nodes[i]);
1531
+ g.select(`.${cls.item}-left`).attr("points", bowPoints(d, -1));
1532
+ g.select(`.${cls.item}-right`).attr("points", bowPoints(d, 1));
1533
+ });
1534
+ }
1535
+
1536
+ // src/utils/layout.utils.ts
1537
+ function getProfileTopDepth(profile) {
1538
+ return Math.min(
1539
+ 0,
1540
+ ...[
1541
+ ...profile.surface_case ?? [],
1542
+ ...profile.well_case ?? [],
1543
+ ...profile.well_screen ?? []
1544
+ ].map((i) => i.from).filter(Number.isFinite)
1545
+ );
1546
+ }
1547
+ function resolvePanelWindows(panels, totalDepth, topDepth = 0) {
1548
+ if (panels.length > 0 && panels.every((p) => p.depthRange)) {
1549
+ return panels.map((p) => [p.depthRange[0], p.depthRange[1]]);
1550
+ }
1551
+ if (panels.length === 1) return [[topDepth, totalDepth]];
1552
+ const totalHeight = panels.reduce((s, p) => s + p.height, 0) || 1;
1553
+ const metersPerPx = (totalDepth - topDepth) / totalHeight;
1554
+ let current = topDepth;
1555
+ return panels.map((p) => {
1556
+ const from = current;
1557
+ current = from + p.height * metersPerPx;
1558
+ return [from, current];
1559
+ });
1560
+ }
1561
+ function getColumnHeaderPositions(geometry) {
1562
+ const { marginLeft, geoXLeft, geoXRight, pocoCenterX, descriptionX } = geometry;
1563
+ return {
1564
+ depth: marginLeft - 2,
1565
+ lithology: marginLeft + (geoXLeft + geoXRight) / 2,
1566
+ construction: marginLeft + pocoCenterX,
1567
+ ...descriptionX != null ? { description: marginLeft + descriptionX } : {}
1568
+ };
1569
+ }
1570
+ function getLithologyMarkers(lithology, depthFrom, depthTo) {
1571
+ const markers = [];
1572
+ lithology.forEach((layer, i) => {
1573
+ const from = Math.max(layer.from, depthFrom);
1574
+ const to = Math.min(layer.to, depthTo);
1575
+ if (to > from) markers.push({ index: i + 1, from, to });
1576
+ });
1577
+ return markers;
1578
+ }
1579
+ function getFractureMarkers(fractures, depthFrom, depthTo) {
1580
+ const markers = [];
1581
+ fractures.forEach((f, i) => {
1582
+ if (f.depth >= depthFrom && f.depth < depthTo) {
1583
+ markers.push({ label: `F${i + 1}`, depth: f.depth });
1584
+ }
1585
+ });
1586
+ return markers;
1587
+ }
1588
+ function getOverviewGeometry(totalDepth, height, depthFrom, depthTo) {
1589
+ const ratio = totalDepth > 0 ? height / totalDepth : 0;
1590
+ const y = (depth) => Math.min(height, Math.max(0, depth * ratio));
1591
+ return {
1592
+ y,
1593
+ window: { y: y(depthFrom), height: Math.max(1, y(depthTo) - y(depthFrom)) }
1594
+ };
1595
+ }
1596
+ function formatOverlapLabel(template, range) {
1597
+ return template.replace("{range}", range);
1598
+ }
1599
+
1600
+ // src/renderers/column-headers.renderer.ts
1601
+ function hasDescriptionColumn(ctx) {
1602
+ const { active, lithology } = ctx.renderConfig.labels;
1603
+ if (active === false) return false;
1604
+ if (Array.isArray(active) && !active.includes("lithology")) return false;
1605
+ if (lithology === false) return false;
1606
+ return !Array.isArray(lithology) || lithology.includes("description");
1607
+ }
1608
+ function drawColumnHeaders(ctx) {
1609
+ const group = ctx.groups.columnHeadersGroup;
1610
+ group.selectAll("*").remove();
1611
+ const cfg = ctx.renderConfig.columnHeaders;
1612
+ if (!cfg.active) return;
1613
+ const theme = ctx.theme.columnHeaders;
1614
+ const { margins } = ctx.state;
1615
+ const positions = getColumnHeaderPositions({
1616
+ marginLeft: margins.left,
1617
+ geoXLeft: ctx.geoXLeft,
1618
+ geoXRight: ctx.geoXRight,
1619
+ pocoCenterX: ctx.pocoCenterX,
1620
+ descriptionX: hasDescriptionColumn(ctx) ? ctx.geoXRight + ctx.renderConfig.labels.descriptionXOffset : void 0
1621
+ });
1622
+ const ruleY = margins.top - 2 - ctx.renderConfig.construction.cementPad.height;
1623
+ const textY = ruleY - 4;
1624
+ const addText = (x, text, anchor) => group.append("text").attr("class", ctx.classes.columnHeaders.text).attr("x", x).attr("y", textY).attr("text-anchor", anchor).attr("font-size", theme.fontSize).attr("font-family", theme.fontFamily ?? "sans-serif").attr("font-weight", theme.fontWeight ?? 600).attr("fill", theme.color).text(text);
1625
+ addText(positions.depth, cfg.labels.depth, "end");
1626
+ addText(positions.lithology, cfg.labels.lithology, "middle");
1627
+ addText(positions.construction, cfg.labels.construction, "middle");
1628
+ if (positions.description != null) {
1629
+ addText(positions.description, cfg.labels.description, "start");
1630
+ }
1631
+ const overview = ctx.renderConfig.overview;
1632
+ group.append("line").attr("class", ctx.classes.columnHeaders.rule).attr("x1", overview.active ? overview.x : 0).attr("x2", ctx.svgWidth).attr("y1", ruleY).attr("y2", ruleY).attr("stroke", theme.ruleStroke).attr("stroke-width", theme.ruleStrokeWidth);
1633
+ }
1634
+
1635
+ // src/renderers/construction-labels.renderer.ts
1636
+ import * as d33 from "d3";
1637
+ import { CONSTRUCTION_MATERIALS as CONSTRUCTION_MATERIALS2, getVocabLabel as getVocabLabel2 } from "@welldot/core";
1638
+ import { getProfileDiamValues as getProfileDiamValues2 } from "@welldot/utils";
1088
1639
  function drawConstructionLabels(ctx, data) {
1089
1640
  if (!ctx.renderConfig.constructionLabels.active) return;
1090
1641
  ctx.groups.constructionLabelsGroup.selectAll("*").remove();
@@ -1092,16 +1643,18 @@ function drawConstructionLabels(ctx, data) {
1092
1643
  const clct = ctx.theme.constructionLabels;
1093
1644
  const fmtD = (mm) => formatDiameter(mm, ctx.units.diameter, ctx.locale);
1094
1645
  const dUnit = getDiameterUnit(ctx.units.diameter, ctx.locale);
1095
- const maxXValues = getProfileDiamValues(ctx.constructionData);
1096
- const xScale = d32.scaleLinear().domain([0, d32.max(maxXValues) || 0]).range([0, ctx.POCO_WIDTH]);
1646
+ const material = (value) => getVocabLabel2(CONSTRUCTION_MATERIALS2, value, ctx.locale);
1647
+ const maxXValues = getProfileDiamValues2(ctx.constructionData);
1648
+ const xScale = d33.scaleLinear().domain([0, d33.max(maxXValues) || 0]).range([0, ctx.POCO_WIDTH]);
1097
1649
  const occupied = [];
1098
- const placeLabel = (d, text) => {
1650
+ const onRight = clc.side === "right";
1651
+ const continuation = ctx.renderConfig.labels.continuation;
1652
+ const placeLabel = (d, label) => {
1099
1653
  const clampedFrom = Math.max(d.from, ctx.depthFrom);
1100
1654
  const clampedTo = Math.min(d.to, ctx.depthTo);
1101
1655
  if (clampedFrom >= clampedTo) return;
1656
+ const text = continuation?.active && d.from < ctx.depthFrom ? `\u2191 ${label}` : label;
1102
1657
  const midY = ctx.yScale((clampedFrom + clampedTo) / 2);
1103
- const leftEdgeX = (ctx.POCO_CENTER - xScale(d.diameter)) / 2;
1104
- const labelRightX = leftEdgeX - clc.xOffset;
1105
1658
  const charW = clct.fontSize * 0.52;
1106
1659
  const lineH = clct.fontSize * 1.35;
1107
1660
  const vertPad = 6;
@@ -1118,16 +1671,17 @@ function drawConstructionLabels(ctx, data) {
1118
1671
  lines = [text];
1119
1672
  boxW = text.length * charW + 8;
1120
1673
  }
1674
+ const labelRightX = onRight ? (ctx.POCO_CENTER + ctx.POCO_WIDTH) / 2 + clc.xOffset + boxW : (ctx.POCO_CENTER - xScale(d.diameter)) / 2 - clc.xOffset;
1121
1675
  const labelH = lines.length * lineH + vertPad;
1122
- let labelY = midY - labelH / 2;
1676
+ let labelY = onRight ? ctx.yScale(clampedFrom) + 1 : midY - labelH / 2;
1123
1677
  for (const pos of occupied) {
1124
1678
  if (labelY < pos.bot && labelY + labelH > pos.top) labelY = pos.bot + 2;
1125
1679
  }
1126
1680
  occupied.push({ top: labelY, bot: labelY + labelH });
1127
1681
  ctx.groups.constructionLabelsGroup.append("rect").attr("x", labelRightX - boxW).attr("y", labelY).attr("width", boxW).attr("height", labelH).attr("rx", clc.labelRadius).attr("fill", clct.labelFill ?? "white").attr("stroke", clct.labelColor ?? "#303030").attr("stroke-width", 0.5);
1128
- const textEl = ctx.groups.constructionLabelsGroup.append("text").attr("x", labelRightX - 4).attr("y", labelY + lineH * 0.85).attr("font-size", clct.fontSize).attr("font-family", clct.fontFamily ?? "sans-serif").attr("font-weight", clct.fontWeight ?? 400).attr("text-anchor", "end").attr("fill", clct.labelColor ?? "#303030");
1682
+ const textEl = ctx.groups.constructionLabelsGroup.append("text").attr("x", onRight ? labelRightX - boxW + 4 : labelRightX - 4).attr("y", labelY + lineH * 0.85).attr("font-size", clct.fontSize).attr("font-family", clct.fontFamily ?? "sans-serif").attr("font-weight", clct.fontWeight ?? 400).attr("text-anchor", onRight ? "start" : "end").attr("fill", clct.labelColor ?? "#303030");
1129
1683
  lines.forEach((line, i) => {
1130
- textEl.append("tspan").attr("x", labelRightX - 4).attr("dy", i === 0 ? 0 : lineH).text(line);
1684
+ textEl.append("tspan").attr("x", onRight ? labelRightX - boxW + 4 : labelRightX - 4).attr("dy", i === 0 ? 0 : lineH).text(line);
1131
1685
  });
1132
1686
  };
1133
1687
  let lastDiam = 0;
@@ -1140,7 +1694,7 @@ function drawConstructionLabels(ctx, data) {
1140
1694
  }).forEach(
1141
1695
  (d) => placeLabel(
1142
1696
  d,
1143
- `${clc.labels.wellCasePrefix} ${fmtD(d.diameter)}${dUnit} ${d.type}`
1697
+ `${clc.labels.wellCasePrefix} ${fmtD(d.diameter)}${dUnit} ${material(d.type)}`
1144
1698
  )
1145
1699
  );
1146
1700
  lastDiam = 0;
@@ -1153,122 +1707,319 @@ function drawConstructionLabels(ctx, data) {
1153
1707
  }).forEach(
1154
1708
  (d) => placeLabel(
1155
1709
  d,
1156
- `${clc.labels.wellScreenPrefix} ${fmtD(d.diameter)}${dUnit} ${d.type} ${clc.labels.wellScreenSlotPrefix} ${fmtD(d.screen_slot)}${dUnit}`
1710
+ `${clc.labels.wellScreenPrefix} ${fmtD(d.diameter)}${dUnit} ${material(d.type)} ${clc.labels.wellScreenSlotPrefix} ${fmtD(d.screen_slot)}${dUnit}`
1157
1711
  )
1158
1712
  );
1159
1713
  }
1160
1714
 
1161
1715
  // src/renderers/construction.renderer.ts
1162
- import * as d33 from "d3";
1163
- import { getProfileDiamValues as getProfileDiamValues2 } from "@welldot/utils";
1716
+ import * as d34 from "d3";
1717
+ import { getProfileDiamValues as getProfileDiamValues3 } from "@welldot/utils";
1718
+ var entering = /* @__PURE__ */ new WeakSet();
1719
+ function constructionXScale(ctx) {
1720
+ const max6 = d34.max(getProfileDiamValues3(ctx.constructionData)) || 0;
1721
+ return d34.scaleLinear().domain([0, max6]).range([0, ctx.POCO_WIDTH]);
1722
+ }
1723
+ function outlineScales(ctx) {
1724
+ const x = constructionXScale(ctx);
1725
+ return {
1726
+ y: ctx.yScale,
1727
+ left: (d) => (ctx.POCO_CENTER - x(d)) / 2,
1728
+ right: (d) => (ctx.POCO_CENTER + x(d)) / 2,
1729
+ depthFrom: ctx.depthFrom,
1730
+ depthTo: ctx.depthTo
1731
+ };
1732
+ }
1733
+ var surfaceCaseDiameter = (ctx, diameter) => diameter * (1 + ctx.renderConfig.construction.surfaceCase.diameterPaddingRatio);
1734
+ function joinIntervalLayer(ctx, group, cls, data, key, tooltip, { shoe = false, pathFill = false, ends = false } = {}) {
1735
+ const sel = group.selectAll(`g.${cls}`).data(data, makeIntervalKey(key));
1736
+ sel.exit().transition(ctx.transition).style("opacity", 0).remove();
1737
+ const top = (d) => ctx.yScale(Math.max(d.from, ctx.depthFrom));
1738
+ const entered = sel.enter().append("g").attr("class", cls).each((_d, i, nodes) => entering.add(nodes[i])).on("mouseover", tooltip.show).on("mouseout", tooltip.hide);
1739
+ if (pathFill)
1740
+ entered.append("path").attr("class", `${cls}-fill`).attr("stroke", "none").attr("fill-rule", "evenodd").attr("d", "");
1741
+ else
1742
+ entered.append("rect").attr("class", `${cls}-fill`).attr("stroke", "none").attr("y", top).attr("height", 0);
1743
+ entered.append("path").attr("class", `${cls}-outline`).attr("fill", "none").attr("d", "");
1744
+ if (shoe)
1745
+ entered.append("path").attr("class", `${cls}-shoe`).attr("stroke-linejoin", "round").attr("d", "");
1746
+ if (ends)
1747
+ entered.append("path").attr("class", `${cls}-ends`).attr("fill", "none").attr("d", "");
1748
+ return entered.merge(sel);
1749
+ }
1750
+ function drawCementPad(ctx, pad) {
1751
+ const cls = ctx.classes.cementPad.item;
1752
+ const visible = !!pad?.thickness && ctx.depthFrom <= 0 && ctx.depthTo > 0;
1753
+ const sel = ctx.groups.cementPadGroup.selectAll(`g.${cls}`).data(visible ? [pad] : [], () => "cement_pad");
1754
+ sel.exit().remove();
1755
+ if (!visible) return;
1756
+ const entered = sel.enter().append("g").attr("class", cls).on("mouseover", ctx.tooltips.cementPad.show).on("mouseout", ctx.tooltips.cementPad.hide);
1757
+ entered.append("path").attr("class", `${cls}-slab`);
1758
+ entered.append("path").attr("class", `${cls}-slab`);
1759
+ const x = constructionXScale(ctx);
1760
+ const { opening, outer } = getPadDiameters(
1761
+ ctx.constructionData,
1762
+ ctx.renderConfig.construction.surfaceCase.diameterPaddingRatio
1763
+ );
1764
+ const { slabs } = getCementPadGeometry(
1765
+ ctx.POCO_WIDTH,
1766
+ ctx.POCO_CENTER,
1767
+ ctx.yScale(0),
1768
+ ctx.renderConfig.construction.cementPad,
1769
+ x(opening),
1770
+ x(outer)
1771
+ );
1772
+ entered.merge(sel).selectAll("path").attr("d", (_d, i) => slabs[i] ?? "").attr("fill", ctx.textures.pad.url()).attr("stroke", ctx.theme.cementPad.stroke).attr("stroke-width", ctx.theme.cementPad.strokeWidth).attr("stroke-linejoin", "round");
1773
+ }
1774
+ function layoutConstruction(ctx, animate = false) {
1775
+ const c = ctx.classes;
1776
+ const scales = outlineScales(ctx);
1777
+ const data = ctx.constructionData;
1778
+ const motion = (sel) => animate ? withTransition(sel, ctx.transition) : sel;
1779
+ const yTop = (d) => ctx.yScale(Math.max(d.from, ctx.depthFrom));
1780
+ const yBottom = (d) => ctx.yScale(Math.min(d.to, ctx.depthTo));
1781
+ const collapsed = (d) => ({
1782
+ ...scales,
1783
+ y: () => yTop(d)
1784
+ });
1785
+ const shapeParts = (items, part, attr, shape) => {
1786
+ const parts = items.select(part);
1787
+ if (animate)
1788
+ parts.filter(function() {
1789
+ return entering.has(this.parentNode);
1790
+ }).attr(attr, (d) => shape(d, collapsed(d)));
1791
+ motion(parts).attr(attr, (d) => shape(d, scales));
1792
+ };
1793
+ const layoutLayer = (group, cls, diameter, outline, {
1794
+ shoe: shoe2,
1795
+ ends,
1796
+ fill
1797
+ } = {}) => {
1798
+ const items = group.selectAll(`g.${cls}`);
1799
+ shapeParts(items, `.${cls}-outline`, "d", outline);
1800
+ if (shoe2) shapeParts(items, `.${cls}-shoe`, "d", shoe2);
1801
+ if (ends) shapeParts(items, `.${cls}-ends`, "d", ends);
1802
+ if (fill) {
1803
+ shapeParts(items, `.${cls}-fill`, "d", fill);
1804
+ return;
1805
+ }
1806
+ motion(
1807
+ items.select(`.${cls}-fill`).attr("x", (d) => scales.left(diameter(d))).attr(
1808
+ "width",
1809
+ (d) => scales.right(diameter(d)) - scales.left(diameter(d))
1810
+ )
1811
+ ).attr("y", yTop).attr("height", (d) => Math.max(0, yBottom(d) - yTop(d)));
1812
+ };
1813
+ const walls = (d, s, dia = d.diameter) => pipeWallsPath(
1814
+ s.left(dia),
1815
+ s.right(dia),
1816
+ s.y(Math.max(d.from, s.depthFrom)),
1817
+ s.y(Math.min(d.to, s.depthTo))
1818
+ );
1819
+ layoutLayer(
1820
+ ctx.groups.holeGroup,
1821
+ c.boreHole.rect,
1822
+ (d) => d.diameter,
1823
+ (d, s) => boreHoleOutlinePath(d, data.bore_hole, s)
1824
+ );
1825
+ const pipes = [...data.well_case, ...data.well_screen];
1826
+ const insideSurface = [...data.bore_hole, ...data.hole_fill, ...pipes];
1827
+ const scDiameter = (d) => surfaceCaseDiameter(ctx, d.diameter);
1828
+ layoutLayer(
1829
+ ctx.groups.surfaceCaseGroup,
1830
+ c.surfaceCase.rect,
1831
+ scDiameter,
1832
+ (d, s) => walls(d, s, scDiameter(d)),
1833
+ // Only the ring outside the hole: the texture never shows under the casing.
1834
+ {
1835
+ fill: (d, s) => annulusFillPath({ ...d, diameter: scDiameter(d) }, insideSurface, s)
1836
+ }
1837
+ );
1838
+ layoutLayer(
1839
+ ctx.groups.holeFillGroup,
1840
+ c.holeFill.rect,
1841
+ (d) => d.diameter,
1842
+ (d, s) => holeFillOutlinePath(d, pipes, s),
1843
+ { fill: (d, s) => annulusFillPath(d, pipes, s) }
1844
+ );
1845
+ const string = [...pipes, ...data.reduction ?? []];
1846
+ const shoeCfg = ctx.renderConfig.construction.casingShoe;
1847
+ const endsCfg = ctx.renderConfig.construction.screenEnds;
1848
+ const room = (d, depth, size) => {
1849
+ const hole = diameterAt(data.bore_hole, depth);
1850
+ const annulus = hole !== void 0 && hole > d.diameter ? scales.left(d.diameter) - scales.left(hole) - 1 : size;
1851
+ return Math.max(2, Math.min(size, annulus));
1852
+ };
1853
+ const shoe = (d, s) => {
1854
+ const ends = getIntervalEnds(d, string, ctx.depthFrom, ctx.depthTo);
1855
+ if (!shoeCfg.active || ends.cutBottom || ends.joinedBottom) return "";
1856
+ return shoePath(
1857
+ s.left(d.diameter),
1858
+ s.right(d.diameter),
1859
+ s.y(Math.min(d.to, s.depthTo)),
1860
+ room(d, d.to, shoeCfg.size)
1861
+ );
1862
+ };
1863
+ const screenEnds = (d, s) => {
1864
+ if (!endsCfg.active) return "";
1865
+ const ends = getIntervalEnds(d, string, ctx.depthFrom, ctx.depthTo);
1866
+ const mark = (depth) => screenEndPath(s.left(d.diameter), s.right(d.diameter), s.y(depth));
1867
+ return (ends.cutTop ? "" : mark(d.from)) + (ends.cutBottom ? "" : mark(d.to));
1868
+ };
1869
+ layoutLayer(
1870
+ ctx.groups.wellCaseGroup,
1871
+ c.wellCase.rect,
1872
+ (d) => d.diameter,
1873
+ walls,
1874
+ { shoe }
1875
+ );
1876
+ layoutLayer(
1877
+ ctx.groups.wellScreenGroup,
1878
+ c.wellScreen.rect,
1879
+ (d) => d.diameter,
1880
+ walls,
1881
+ { shoe, ends: screenEnds }
1882
+ );
1883
+ const reductions = ctx.groups.reductionGroup.selectAll(`g.${c.reduction.item}`);
1884
+ const corners = (d, s) => {
1885
+ const t = s.y(Math.max(d.from, s.depthFrom));
1886
+ const b = s.y(Math.min(d.to, s.depthTo));
1887
+ return {
1888
+ tl: `${s.left(d.diam_from)},${t}`,
1889
+ tr: `${s.right(d.diam_from)},${t}`,
1890
+ bl: `${s.left(d.diam_to)},${b}`,
1891
+ br: `${s.right(d.diam_to)},${b}`
1892
+ };
1893
+ };
1894
+ shapeParts(reductions, `.${c.reduction.item}-fill`, "points", (d, s) => {
1895
+ const p = corners(d, s);
1896
+ return `${p.tl} ${p.tr} ${p.br} ${p.bl}`;
1897
+ });
1898
+ shapeParts(reductions, `.${c.reduction.item}-outline`, "d", (d, s) => {
1899
+ const p = corners(d, s);
1900
+ return `M${p.tl}L${p.bl}M${p.tr}L${p.br}`;
1901
+ });
1902
+ if (animate) {
1903
+ for (const group of [
1904
+ ctx.groups.holeGroup,
1905
+ ctx.groups.surfaceCaseGroup,
1906
+ ctx.groups.holeFillGroup,
1907
+ ctx.groups.wellCaseGroup,
1908
+ ctx.groups.wellScreenGroup,
1909
+ ctx.groups.reductionGroup
1910
+ ])
1911
+ group.selectAll("g").each(function() {
1912
+ entering.delete(this);
1913
+ });
1914
+ }
1915
+ }
1164
1916
  function drawConstructive(ctx, data) {
1165
1917
  const { getHeight, getYPos } = getYAxisFunctions(ctx.yScale, {
1166
1918
  from: ctx.depthFrom,
1167
1919
  to: ctx.depthTo
1168
1920
  });
1169
- const maxXValues = getProfileDiamValues2(ctx.constructionData);
1170
- const maxXValueConstruction = d33.max(maxXValues) || 0;
1171
- const xScale = d33.scaleLinear().domain([0, maxXValueConstruction]).range([0, ctx.POCO_WIDTH]);
1172
- const rcc = ctx.renderConfig.construction;
1173
- const yPos = getYPos;
1174
- ctx.groups.constructionGroup.selectAll(`.${ctx.classes.cementPad.item}`).remove();
1175
- if (data.cement_pad && data.cement_pad.thickness && ctx.depthFrom === 0) {
1176
- const cementPad = ctx.groups.cementPadGroup.selectAll("rect").data([data.cement_pad], () => "cement_pad");
1177
- cementPad.exit().remove();
1178
- const newCementPad = cementPad.enter().append("rect").attr("class", ctx.classes.cementPad.item).attr(
1179
- "x",
1180
- (d) => (ctx.POCO_CENTER - xScale(d.width * rcc.cementPad.widthMultiplier * 1e3 / 2)) / 2
1181
- ).attr("y", (d) => {
1182
- return ctx.yScale(0) - ctx.yScale(d.thickness * rcc.cementPad.thicknessMultiplier);
1183
- }).attr(
1184
- "width",
1185
- (d) => xScale(d.width * rcc.cementPad.widthMultiplier * 1e3 / 2)
1186
- ).attr("height", (d) => {
1187
- return ctx.yScale(d.thickness * rcc.cementPad.thicknessMultiplier);
1188
- }).attr("fill", ctx.textures.pad.url()).attr("stroke", ctx.theme.cementPad.stroke).attr("stroke-width", ctx.theme.cementPad.strokeWidth);
1189
- newCementPad.on("mouseover", ctx.tooltips.cementPad.show).on("mouseout", ctx.tooltips.cementPad.hide);
1190
- }
1191
- const hole = ctx.groups.holeGroup.selectAll(`.${ctx.classes.boreHole.rect}`).data(data.bore_hole, makeIntervalKey("bore_hole"));
1192
- hole.exit().transition(ctx.transition).attr("height", 0).remove();
1193
- const newHole = hole.enter().append("rect").attr("class", ctx.classes.boreHole.rect).attr("fill", ctx.theme.boreHole.fill).attr("stroke", ctx.theme.boreHole.stroke).attr("opacity", ctx.theme.boreHole.opacity).attr("stroke-width", ctx.theme.boreHole.strokeWidth).attr("stroke-dasharray", ctx.theme.boreHole.strokeDasharray).on("mouseover", ctx.tooltips.hole.show).on("mouseout", ctx.tooltips.hole.hide).attr("y", yPos).attr("height", 0);
1194
- const mergedHole = mergeEnter(newHole, hole).attr("x", (d) => (ctx.POCO_CENTER - xScale(d.diameter)) / 2).attr("width", (d) => xScale(d.diameter));
1195
- withTransition(mergedHole, ctx.transition).attr("y", getYPos).attr("height", getHeight);
1196
- const surfaceCaseGs = ctx.groups.surfaceCaseGroup.selectAll(`g.${ctx.classes.surfaceCase.rect}`).data(data.surface_case, makeIntervalKey("surface_case"));
1197
- surfaceCaseGs.exit().transition(ctx.transition).style("opacity", 0).remove();
1198
- const newSC = surfaceCaseGs.enter().append("g").attr("class", ctx.classes.surfaceCase.rect).on("mouseover", ctx.tooltips.surfaceCase.show).on("mouseout", ctx.tooltips.surfaceCase.hide);
1199
- newSC.append("rect").attr("class", "surface-case-fill").attr("stroke", "none").attr("y", yPos).attr("height", 0);
1200
- newSC.append("line").attr("class", "surface-case-side").attr("stroke", ctx.theme.surfaceCase.stroke).attr("stroke-width", ctx.theme.surfaceCase.strokeWidth);
1201
- newSC.append("line").attr("class", "surface-case-side").attr("stroke", ctx.theme.surfaceCase.stroke).attr("stroke-width", ctx.theme.surfaceCase.strokeWidth);
1202
- const mergedSC = mergeEnter(newSC, surfaceCaseGs);
1203
- mergedSC.each((d, i, nodes) => {
1204
- const x = (ctx.POCO_CENTER - xScale(
1205
- d.diameter + d.diameter * rcc.surfaceCase.diameterPaddingRatio
1206
- )) / 2;
1207
- const w = xScale(
1208
- d.diameter + d.diameter * rcc.surfaceCase.diameterPaddingRatio
1921
+ const c = ctx.classes;
1922
+ const theme = ctx.theme;
1923
+ drawCementPad(ctx, data.cement_pad);
1924
+ joinIntervalLayer(
1925
+ ctx,
1926
+ ctx.groups.holeGroup,
1927
+ c.boreHole.rect,
1928
+ data.bore_hole,
1929
+ "bore_hole",
1930
+ ctx.tooltips.hole
1931
+ ).attr("opacity", theme.boreHole.opacity).call((g) => {
1932
+ g.select(`.${c.boreHole.rect}-fill`).attr("fill", theme.boreHole.fill);
1933
+ g.select(`.${c.boreHole.rect}-outline`).attr("stroke", theme.boreHole.stroke).attr("stroke-width", theme.boreHole.strokeWidth).attr("stroke-dasharray", theme.boreHole.strokeDasharray);
1934
+ });
1935
+ joinIntervalLayer(
1936
+ ctx,
1937
+ ctx.groups.surfaceCaseGroup,
1938
+ c.surfaceCase.rect,
1939
+ data.surface_case,
1940
+ "surface_case",
1941
+ ctx.tooltips.surfaceCase,
1942
+ { pathFill: true }
1943
+ ).call((g) => {
1944
+ g.select(`.${c.surfaceCase.rect}-fill`).attr(
1945
+ "fill",
1946
+ ctx.textures.surface_case.url()
1209
1947
  );
1210
- const g = d33.select(nodes[i]);
1211
- g.select(".surface-case-fill").attr("x", x).attr("width", w).attr("fill", ctx.textures.surface_case.url());
1212
- const sideNodes = g.selectAll(".surface-case-side").nodes();
1213
- d33.select(sideNodes[0]).attr("x1", x).attr("x2", x);
1214
- d33.select(sideNodes[1]).attr("x1", x + w).attr("x2", x + w);
1948
+ g.select(`.${c.surfaceCase.rect}-outline`).attr("stroke", theme.surfaceCase.stroke).attr("stroke-width", theme.surfaceCase.strokeWidth);
1215
1949
  });
1216
- withTransition(
1217
- mergedSC.select(".surface-case-fill"),
1218
- ctx.transition
1219
- ).attr("y", (d) => getYPos(d)).attr("height", (d) => getHeight(d));
1220
- mergedSC.selectAll(".surface-case-side").attr("y1", (d) => getYPos(d)).attr(
1221
- "y2",
1222
- (d) => getYPos(d) + getHeight(d)
1223
- );
1224
- const holeFill = ctx.groups.holeFillGroup.selectAll(`.${ctx.classes.holeFill.rect}`).data(data.hole_fill, makeIntervalKey("hole_fill"));
1225
- holeFill.exit().transition(ctx.transition).attr("height", 0).remove();
1226
- const newHoleFill = holeFill.enter().append("rect").attr("class", ctx.classes.holeFill.rect).attr("stroke", ctx.theme.holeFill.stroke).attr("stroke-width", ctx.theme.holeFill.strokeWidth).on("mouseover", ctx.tooltips.holeFill.show).on("mouseout", ctx.tooltips.holeFill.hide).attr("y", yPos).attr("height", 0);
1227
- const mergedHoleFill = mergeEnter(newHoleFill, holeFill).attr("x", (d) => (ctx.POCO_CENTER - xScale(d.diameter)) / 2).attr("width", (d) => xScale(d.diameter));
1228
- withTransition(mergedHoleFill, ctx.transition).attr("y", getYPos).attr("height", getHeight).attr("fill", (d) => ctx.textures[d.type].url());
1229
- const wellCase = ctx.groups.wellCaseGroup.selectAll(`.${ctx.classes.wellCase.rect}`).data(data.well_case, makeIntervalKey("well_case"));
1230
- wellCase.exit().transition(ctx.transition).attr("height", 0).remove();
1231
- const newWellCase = wellCase.enter().append("rect").attr("class", ctx.classes.wellCase.rect).attr("fill", ctx.theme.wellCase.fill).attr("stroke", ctx.theme.wellCase.stroke).attr("stroke-width", ctx.theme.wellCase.strokeWidth).on("mouseover", ctx.tooltips.wellCase.show).on("mouseout", ctx.tooltips.wellCase.hide).attr("y", yPos).attr("height", 0);
1232
- const mergedWellCase = mergeEnter(newWellCase, wellCase).attr("x", (d) => (ctx.POCO_CENTER - xScale(d.diameter)) / 2).attr("width", (d) => xScale(d.diameter));
1233
- withTransition(mergedWellCase, ctx.transition).attr("y", getYPos).attr("height", getHeight);
1234
- const wellScreen = ctx.groups.wellScreenGroup.selectAll(`.${ctx.classes.wellScreen.rect}`).data(data.well_screen, makeIntervalKey("well_screen"));
1235
- wellScreen.exit().transition(ctx.transition).attr("height", 0).remove();
1236
- const newWellScreen = wellScreen.enter().append("rect").attr("class", ctx.classes.wellScreen.rect).attr("fill", ctx.textures.well_screen.url()).attr("stroke", ctx.theme.wellScreen.stroke).attr("stroke-width", ctx.theme.wellScreen.strokeWidth).on("mouseover", ctx.tooltips.wellScreen.show).on("mouseout", ctx.tooltips.wellScreen.hide).attr("y", yPos).attr("height", 0);
1237
- const mergedWellScreen = mergeEnter(newWellScreen, wellScreen).attr("x", (d) => (ctx.POCO_CENTER - xScale(d.diameter)) / 2).attr("width", (d) => xScale(d.diameter));
1238
- withTransition(mergedWellScreen, ctx.transition).attr("y", getYPos).attr("height", getHeight);
1239
- const reductionPoints = (d, yTop, yBot) => {
1240
- const xTopLeft = (ctx.POCO_CENTER - xScale(d.diam_from)) / 2;
1241
- const xTopRight = (ctx.POCO_CENTER + xScale(d.diam_from)) / 2;
1242
- const xBotLeft = (ctx.POCO_CENTER - xScale(d.diam_to)) / 2;
1243
- const xBotRight = (ctx.POCO_CENTER + xScale(d.diam_to)) / 2;
1244
- return `${xTopLeft},${yTop} ${xTopRight},${yTop} ${xBotRight},${yBot} ${xBotLeft},${yBot}`;
1245
- };
1246
- const reduction = ctx.groups.reductionGroup.selectAll(`.${ctx.classes.reduction.item}`).data(data.reduction, makeIntervalKey("reduction"));
1950
+ joinIntervalLayer(
1951
+ ctx,
1952
+ ctx.groups.holeFillGroup,
1953
+ c.holeFill.rect,
1954
+ data.hole_fill,
1955
+ "hole_fill",
1956
+ ctx.tooltips.holeFill,
1957
+ { pathFill: true }
1958
+ ).call((g) => {
1959
+ g.select(`.${c.holeFill.rect}-fill`).attr(
1960
+ "fill",
1961
+ (d) => ctx.textures[d.type].url()
1962
+ );
1963
+ g.select(`.${c.holeFill.rect}-outline`).attr("stroke", theme.holeFill.stroke).attr("stroke-width", theme.holeFill.strokeWidth);
1964
+ });
1965
+ for (const [group, cls, items, key, tooltip, fill, stroke] of [
1966
+ [
1967
+ ctx.groups.wellCaseGroup,
1968
+ c.wellCase.rect,
1969
+ data.well_case,
1970
+ "well_case",
1971
+ ctx.tooltips.wellCase,
1972
+ theme.wellCase.fill,
1973
+ theme.wellCase
1974
+ ],
1975
+ [
1976
+ ctx.groups.wellScreenGroup,
1977
+ c.wellScreen.rect,
1978
+ data.well_screen,
1979
+ "well_screen",
1980
+ ctx.tooltips.wellScreen,
1981
+ ctx.textures.well_screen.url(),
1982
+ theme.wellScreen
1983
+ ]
1984
+ ]) {
1985
+ joinIntervalLayer(ctx, group, cls, items, key, tooltip, {
1986
+ shoe: true,
1987
+ ends: cls === c.wellScreen.rect
1988
+ }).call((g) => {
1989
+ g.select(`.${cls}-fill`).attr("fill", fill);
1990
+ g.select(`.${cls}-outline`).attr("stroke", stroke.stroke).attr("stroke-width", stroke.strokeWidth);
1991
+ g.select(`.${cls}-shoe`).attr("fill", stroke.stroke).attr("stroke", stroke.stroke).attr("stroke-width", 0.5);
1992
+ g.select(`.${cls}-ends`).attr("stroke", stroke.stroke).attr("stroke-width", theme.wellScreen.endStrokeWidth).attr("stroke-dasharray", theme.wellScreen.endDasharray);
1993
+ });
1994
+ }
1995
+ const reduction = ctx.groups.reductionGroup.selectAll(`g.${c.reduction.item}`).data(data.reduction ?? [], makeIntervalKey("reduction"));
1247
1996
  reduction.exit().transition(ctx.transition).style("opacity", 0).remove();
1248
- const collapsedPoints = (d) => {
1249
- const r = d;
1250
- const y = getYPos(r);
1251
- return reductionPoints(r, y, y);
1252
- };
1253
- const newReduction = reduction.enter().append("polygon").attr("class", ctx.classes.reduction.item).attr("fill", ctx.theme.reduction.fill).attr("stroke", ctx.theme.reduction.stroke).attr("stroke-width", ctx.theme.reduction.strokeWidth).on("mouseover", ctx.tooltips.reduction.show).on("mouseout", ctx.tooltips.reduction.hide).attr("points", collapsedPoints);
1254
- const mergedReduction = mergeEnter(newReduction, reduction);
1255
- withTransition(mergedReduction, ctx.transition).attr(
1256
- "points",
1257
- (d) => reductionPoints(d, getYPos(d), getYPos(d) + getHeight(d))
1997
+ const newReduction = reduction.enter().append("g").attr("class", c.reduction.item).each((_d, i, nodes) => entering.add(nodes[i])).on("mouseover", ctx.tooltips.reduction.show).on("mouseout", ctx.tooltips.reduction.hide);
1998
+ newReduction.append("polygon").attr("class", `${c.reduction.item}-fill`).attr("stroke", "none");
1999
+ newReduction.append("path").attr("class", `${c.reduction.item}-outline`).attr("fill", "none");
2000
+ newReduction.merge(reduction).call((g) => {
2001
+ g.select(`.${c.reduction.item}-fill`).attr("fill", theme.reduction.fill);
2002
+ g.select(`.${c.reduction.item}-outline`).attr("stroke", theme.reduction.stroke).attr("stroke-width", theme.reduction.strokeWidth);
2003
+ });
2004
+ const mergedConflicts = mergeConflicts(
2005
+ [
2006
+ ...getConflictAreas(data.well_case, data.well_screen),
2007
+ ...getConflictAreas(data.well_screen, data.well_case)
2008
+ ],
2009
+ 1
1258
2010
  );
1259
- const conflictAreas = [];
1260
- conflictAreas.push(...getConflictAreas(data.well_case, data.well_screen));
1261
- conflictAreas.push(...getConflictAreas(data.well_screen, data.well_case));
1262
- const mergedConflicts = mergeConflicts(conflictAreas, 1);
1263
- const conflict = ctx.groups.conflictGroup.selectAll(`.${ctx.classes.conflict.rect}`).data(mergedConflicts, makeIntervalKey("conflict"));
2011
+ const xScale = constructionXScale(ctx);
2012
+ const yPos = getYPos;
2013
+ const conflict = ctx.groups.conflictGroup.selectAll(`.${c.conflict.rect}`).data(mergedConflicts, makeIntervalKey("conflict"));
1264
2014
  conflict.exit().transition(ctx.transition).attr("height", 0).remove();
1265
- const newConflict = conflict.enter().append("rect").attr("class", ctx.classes.conflict.rect).attr("fill", ctx.textures.conflict.url()).attr("stroke", ctx.theme.conflict.stroke).attr("stroke-width", ctx.theme.conflict.strokeWidth).on("mouseover", ctx.tooltips.conflict.show).on("mouseout", ctx.tooltips.conflict.hide).attr("y", ctx.yScale(0)).attr("height", 0);
2015
+ const newConflict = conflict.enter().append("rect").attr("class", c.conflict.rect).attr("fill", ctx.textures.conflict.url()).attr("stroke", theme.conflict.stroke).attr("stroke-width", theme.conflict.strokeWidth).on("mouseover", ctx.tooltips.conflict.show).on("mouseout", ctx.tooltips.conflict.hide).attr("y", yPos).attr("height", 0);
1266
2016
  const mergedConflict = mergeEnter(newConflict, conflict).attr("x", (d) => (ctx.POCO_CENTER - xScale(d.diameter)) / 2).attr("width", (d) => xScale(d.diameter));
1267
- withTransition(mergedConflict, ctx.transition).attr("y", ctx.yScale(0)).attr("height", getHeight);
2017
+ withTransition(mergedConflict, ctx.transition).attr("y", getYPos).attr("height", getHeight);
2018
+ layoutConstruction(ctx, true);
1268
2019
  }
1269
2020
 
1270
2021
  // src/renderers/fractures.renderer.ts
1271
- import * as d34 from "d3";
2022
+ import * as d35 from "d3";
1272
2023
  function wavyLine(rng, steps, baseY, jitter, startInset, endInset) {
1273
2024
  const pts = [];
1274
2025
  for (let i = 0; i < steps; i++) {
@@ -1280,7 +2031,6 @@ function wavyLine(rng, steps, baseY, jitter, startInset, endInset) {
1280
2031
  return pts;
1281
2032
  }
1282
2033
  function drawFractures(ctx, data) {
1283
- if (!data.length) return;
1284
2034
  const {
1285
2035
  groups: { fracturesGroup },
1286
2036
  POCO_WIDTH,
@@ -1302,10 +2052,10 @@ function drawFractures(ctx, data) {
1302
2052
  const merged = mergeEnter(entered, groups);
1303
2053
  merged.attr(
1304
2054
  "transform",
1305
- (fracture) => `translate(0,${yScale(fracture.depth)}) rotate(${fracture.dip},${pocoCenterX},0)`
2055
+ (fracture) => `translate(0,${yScale(fracture.depth)}) rotate(${fractureDip(fracture.dip)},${pocoCenterX},0)`
1306
2056
  );
1307
2057
  merged.each(function(fracture) {
1308
- const g = d34.select(this);
2058
+ const g = d35.select(this);
1309
2059
  g.selectAll("*").remove();
1310
2060
  const rng = makeSeededPrng(fracture.depth, 7919);
1311
2061
  const hitBuffer = fracture.swarm ? rf.hitBuffer.swarm : rf.hitBuffer.single;
@@ -1384,8 +2134,8 @@ function drawFractures(ctx, data) {
1384
2134
  }
1385
2135
 
1386
2136
  // src/renderers/highlights.renderer.ts
1387
- import * as d35 from "d3";
1388
- import { getProfileDiamValues as getProfileDiamValues3 } from "@welldot/utils";
2137
+ import * as d36 from "d3";
2138
+ import { getProfileDiamValues as getProfileDiamValues4 } from "@welldot/utils";
1389
2139
  function drawHighlightItem(ctx, group, x, y, w, h, label) {
1390
2140
  if (h <= 0 || w <= 0) return;
1391
2141
  const hlConfig = ctx.renderConfig.highlights;
@@ -1444,27 +2194,29 @@ function drawHighlights(ctx, hl) {
1444
2194
  h.label
1445
2195
  );
1446
2196
  });
1447
- const maxXVals = getProfileDiamValues3(ctx.constructionData);
1448
- const xScaleConstr = d35.scaleLinear().domain([0, d35.max(maxXVals) || 0]).range([0, ctx.POCO_WIDTH]);
2197
+ const maxXVals = getProfileDiamValues4(ctx.constructionData);
2198
+ const xScaleConstr = d36.scaleLinear().domain([0, d36.max(maxXVals) || 0]).range([0, ctx.POCO_WIDTH]);
1449
2199
  const constructionConfig = ctx.renderConfig.construction;
1450
- if (hl.cement_pad && ctx.depthFrom === 0 && ctx.constructionData.cement_pad?.thickness) {
1451
- const cp = ctx.constructionData.cement_pad;
1452
- const cpPadWidth = cp.width * constructionConfig.cementPad.widthMultiplier * 1e3 / 2;
1453
- const cpX = (ctx.POCO_CENTER - xScaleConstr(cpPadWidth)) / 2;
1454
- const cpY = ctx.yScale(0) - ctx.yScale(
1455
- cp.thickness * constructionConfig.cementPad.thicknessMultiplier
2200
+ if (hl.cement_pad && ctx.depthFrom <= 0 && ctx.depthTo > 0 && ctx.constructionData.cement_pad?.thickness) {
2201
+ const { opening, outer } = getPadDiameters(
2202
+ ctx.constructionData,
2203
+ constructionConfig.surfaceCase.diameterPaddingRatio
1456
2204
  );
1457
- const cpW = xScaleConstr(cpPadWidth);
1458
- const cpH = ctx.yScale(
1459
- cp.thickness * constructionConfig.cementPad.thicknessMultiplier
2205
+ const pad = getCementPadRect(
2206
+ ctx.POCO_WIDTH,
2207
+ ctx.POCO_CENTER,
2208
+ ctx.yScale(0),
2209
+ constructionConfig.cementPad,
2210
+ xScaleConstr(opening),
2211
+ xScaleConstr(outer)
1460
2212
  );
1461
2213
  drawHighlightItem(
1462
2214
  ctx,
1463
2215
  ctx.groups.highlightsConstructionGroup,
1464
- cpX,
1465
- cpY,
1466
- cpW,
1467
- cpH,
2216
+ pad.x,
2217
+ pad.y,
2218
+ pad.width,
2219
+ pad.height,
1468
2220
  hl.cement_pad.label
1469
2221
  );
1470
2222
  }
@@ -1540,8 +2292,12 @@ function drawHighlights(ctx, hl) {
1540
2292
  // src/renderers/legend.renderer.ts
1541
2293
  import * as d3module from "d3";
1542
2294
  import { defu } from "defu";
1543
- var d36 = { ...d3module };
1544
- function drawWellLegend(selector, profile, options = {}) {
2295
+ import { getCurrentPump } from "@welldot/utils";
2296
+ var d37 = { ...d3module };
2297
+ var SYMBOL_WIDTH = 24;
2298
+ var SYMBOL_HEIGHT = 12;
2299
+ var SYMBOL_MARGIN = 2;
2300
+ function getWellLegendItems(profile, labels = INTERACTIVE_RENDER_CONFIG.legend.labels) {
1545
2301
  const fractures = profile.fractures ?? [];
1546
2302
  const caves = profile.caves ?? [];
1547
2303
  const boreHoles = profile.bore_hole ?? [];
@@ -1561,6 +2317,8 @@ function drawWellLegend(selector, profile, options = {}) {
1561
2317
  const hasWellCase = wellCases.length > 0;
1562
2318
  const hasWellScreen = wellScreens.length > 0;
1563
2319
  const hasReduction = reductions.length > 0;
2320
+ const hasCentralizer = (profile.centralizers ?? []).length > 0;
2321
+ const hasPump = getCurrentPump(profile)?.intake_depth !== void 0;
1564
2322
  const hasCementPad = !!profile.cement_pad?.thickness;
1565
2323
  const hasConflict = mergeConflicts(
1566
2324
  [
@@ -1569,197 +2327,314 @@ function drawWellLegend(selector, profile, options = {}) {
1569
2327
  ],
1570
2328
  1
1571
2329
  ).length > 0;
1572
- const cfg = defu(
1573
- options.config ?? {},
1574
- INTERACTIVE_RENDER_CONFIG.legend
1575
- );
1576
- const theme = defu(options.theme ?? {}, DEFAULT_WELL_THEME);
1577
- const cls = options.classNames ? { ...DEFAULT_COMPONENTS_CLASS_NAMES.legend, ...options.classNames } : DEFAULT_COMPONENTS_CLASS_NAMES.legend;
1578
2330
  const items = [];
1579
2331
  if (hasSimple)
1580
2332
  items.push({
1581
2333
  kind: "fracture",
1582
- label: cfg.labels.fractureSingle,
2334
+ key: "fractureSingle",
2335
+ label: labels.fractureSingle,
1583
2336
  swarm: false,
1584
2337
  water_intake: false
1585
2338
  });
1586
2339
  if (hasSwarm)
1587
2340
  items.push({
1588
2341
  kind: "fracture",
1589
- label: cfg.labels.fractureSwarm,
2342
+ key: "fractureSwarm",
2343
+ label: labels.fractureSwarm,
1590
2344
  swarm: true,
1591
2345
  water_intake: false
1592
2346
  });
1593
2347
  if (hasWaterFx)
1594
2348
  items.push({
1595
2349
  kind: "fracture",
1596
- label: cfg.labels.fractureWater,
2350
+ key: "fractureWater",
2351
+ label: labels.fractureWater,
1597
2352
  swarm: false,
1598
2353
  water_intake: true
1599
2354
  });
1600
2355
  if (hasCaveDry)
1601
2356
  items.push({
1602
2357
  kind: "cave",
1603
- label: cfg.labels.caveDry,
2358
+ key: "caveDry",
2359
+ label: labels.caveDry,
1604
2360
  water_intake: false
1605
2361
  });
1606
2362
  if (hasCaveWet)
1607
- items.push({ kind: "cave", label: cfg.labels.caveWet, water_intake: true });
2363
+ items.push({
2364
+ kind: "cave",
2365
+ key: "caveWet",
2366
+ label: labels.caveWet,
2367
+ water_intake: true
2368
+ });
1608
2369
  if (hasCementPad)
1609
2370
  items.push({
1610
2371
  kind: "construction",
1611
- label: cfg.labels.cementPad,
2372
+ key: "cementPad",
2373
+ label: labels.cementPad,
1612
2374
  subKind: "cementPad"
1613
2375
  });
1614
2376
  if (hasBoreHole)
1615
2377
  items.push({
1616
2378
  kind: "construction",
1617
- label: cfg.labels.boreHole,
2379
+ key: "boreHole",
2380
+ label: labels.boreHole,
1618
2381
  subKind: "boreHole"
1619
2382
  });
1620
2383
  if (hasSurfaceCase)
1621
2384
  items.push({
1622
2385
  kind: "construction",
1623
- label: cfg.labels.surfaceCase,
2386
+ key: "surfaceCase",
2387
+ label: labels.surfaceCase,
1624
2388
  subKind: "surfaceCase"
1625
2389
  });
1626
2390
  if (hasHoleFillGravel)
1627
2391
  items.push({
1628
2392
  kind: "construction",
1629
- label: cfg.labels.holeFillGravel,
2393
+ key: "holeFillGravel",
2394
+ label: labels.holeFillGravel,
1630
2395
  subKind: "holeFillGravel"
1631
2396
  });
1632
2397
  if (hasHoleFillSeal)
1633
2398
  items.push({
1634
2399
  kind: "construction",
1635
- label: cfg.labels.holeFillSeal,
2400
+ key: "holeFillSeal",
2401
+ label: labels.holeFillSeal,
1636
2402
  subKind: "holeFillSeal"
1637
2403
  });
1638
2404
  if (hasWellCase)
1639
2405
  items.push({
1640
2406
  kind: "construction",
1641
- label: cfg.labels.wellCase,
2407
+ key: "wellCase",
2408
+ label: labels.wellCase,
1642
2409
  subKind: "wellCase"
1643
2410
  });
1644
2411
  if (hasWellScreen)
1645
2412
  items.push({
1646
2413
  kind: "construction",
1647
- label: cfg.labels.wellScreen,
2414
+ key: "wellScreen",
2415
+ label: labels.wellScreen,
1648
2416
  subKind: "wellScreen"
1649
2417
  });
1650
2418
  if (hasReduction)
1651
2419
  items.push({
1652
2420
  kind: "construction",
1653
- label: cfg.labels.reduction,
2421
+ key: "reduction",
2422
+ label: labels.reduction,
1654
2423
  subKind: "reduction"
1655
2424
  });
2425
+ if (hasCentralizer)
2426
+ items.push({
2427
+ kind: "construction",
2428
+ key: "centralizer",
2429
+ label: labels.centralizer,
2430
+ subKind: "centralizer"
2431
+ });
2432
+ if (hasPump)
2433
+ items.push({
2434
+ kind: "construction",
2435
+ key: "pump",
2436
+ label: labels.pump,
2437
+ subKind: "pump"
2438
+ });
1656
2439
  if (hasConflict)
1657
2440
  items.push({
1658
2441
  kind: "construction",
1659
- label: cfg.labels.conflict,
2442
+ key: "conflict",
2443
+ label: labels.conflict,
1660
2444
  subKind: "conflict"
1661
2445
  });
1662
- const svgEl = d36.select(selector);
2446
+ return items;
2447
+ }
2448
+ function drawLegendSymbol(symG, item, cx, rowSymY, { theme, cls, textures: legendTextures }) {
2449
+ const RC = "round";
2450
+ const rw = SYMBOL_WIDTH;
2451
+ const rh = SYMBOL_HEIGHT;
2452
+ if (item.kind === "fracture") {
2453
+ const color = item.water_intake ? theme.fracture.wetStroke : theme.fracture.dryStroke;
2454
+ if (item.swarm) {
2455
+ [-4, 0, 4].forEach((offset) => {
2456
+ symG.append("polyline").attr("class", cls.fracturePoly).attr(
2457
+ "points",
2458
+ `${cx},${rowSymY + offset} ${cx + 6},${rowSymY + offset - 1} ${cx + 12},${rowSymY + offset + 1} ${cx + 18},${rowSymY + offset - 0.5} ${cx + 24},${rowSymY + offset}`
2459
+ ).attr("stroke", color).attr(
2460
+ "stroke-width",
2461
+ offset !== 0 ? theme.legend.fractureSideStrokeWidth : theme.legend.fractureStrokeWidth
2462
+ ).attr("fill", "none").attr("stroke-linecap", RC).attr("stroke-linejoin", RC);
2463
+ });
2464
+ } else {
2465
+ symG.append("polyline").attr("class", cls.fracturePoly).attr(
2466
+ "points",
2467
+ `${cx},${rowSymY} ${cx + 5},${rowSymY - 1.5} ${cx + 11},${rowSymY + 1} ${cx + 17},${rowSymY - 0.5} ${cx + 24},${rowSymY}`
2468
+ ).attr("stroke", color).attr("stroke-width", theme.legend.fractureStrokeWidth).attr("fill", "none").attr("stroke-linecap", RC).attr("stroke-linejoin", RC);
2469
+ }
2470
+ } else if (item.kind === "cave") {
2471
+ const color = item.water_intake ? theme.cave.wetStroke : theme.cave.dryStroke;
2472
+ const texture = item.water_intake ? legendTextures.cave_wet : legendTextures.cave_dry;
2473
+ symG.append("rect").attr("class", cls.caveFill).attr("x", cx).attr("y", rowSymY - rh / 2).attr("width", rw).attr("height", rh).attr("fill", texture.url()).attr("stroke", color).attr("stroke-width", theme.legend.itemStrokeWidth);
2474
+ } else if (item.subKind === "surfaceCase") {
2475
+ symG.append("rect").attr("class", cls.constructionRect).attr("x", cx).attr("y", rowSymY - rh / 2).attr("width", rw).attr("height", rh).attr("fill", legendTextures.surface_case.url()).attr("stroke", "none");
2476
+ symG.append("line").attr("class", cls.constructionRect).attr("x1", cx).attr("x2", cx).attr("y1", rowSymY - rh / 2).attr("y2", rowSymY + rh / 2).attr("stroke", theme.surfaceCase.stroke).attr("stroke-width", 2);
2477
+ symG.append("line").attr("class", cls.constructionRect).attr("x1", cx + rw).attr("x2", cx + rw).attr("y1", rowSymY - rh / 2).attr("y2", rowSymY + rh / 2).attr("stroke", theme.surfaceCase.stroke).attr("stroke-width", 2);
2478
+ } else if (item.subKind === "centralizer") {
2479
+ const pipeW = rw * 0.4;
2480
+ const pipeL = cx + (rw - pipeW) / 2;
2481
+ const pipeR = pipeL + pipeW;
2482
+ const y1 = rowSymY - rh / 2;
2483
+ const y2 = rowSymY + rh / 2;
2484
+ const bowH = rh * 0.3;
2485
+ for (const x of [pipeL, pipeR]) {
2486
+ symG.append("line").attr("class", cls.constructionRect).attr("x1", x).attr("x2", x).attr("y1", y1).attr("y2", y2).attr("stroke", theme.wellCase.stroke).attr("stroke-width", theme.legend.itemStrokeWidth);
2487
+ }
2488
+ for (const [from, to] of [
2489
+ [pipeL, cx],
2490
+ [pipeR, cx + rw]
2491
+ ]) {
2492
+ symG.append("polyline").attr("class", cls.constructionRect).attr(
2493
+ "points",
2494
+ `${from},${rowSymY - bowH} ${to},${rowSymY} ${from},${rowSymY + bowH}`
2495
+ ).attr("fill", theme.centralizer.fill).attr("stroke", theme.centralizer.stroke).attr("stroke-width", theme.centralizer.strokeWidth);
2496
+ }
2497
+ } else if (item.subKind === "pump") {
2498
+ const y1 = rowSymY - rh / 2;
2499
+ const y2 = rowSymY + rh / 2;
2500
+ const midX = cx + rw / 2;
2501
+ const riserHalf = rw * 0.12;
2502
+ const bodyHalf = rw * 0.28;
2503
+ const bodyTop = y1 + rh * 0.4;
2504
+ symG.append("rect").attr("class", cls.constructionRect).attr("x", midX - riserHalf).attr("y", y1).attr("width", 2 * riserHalf).attr("height", bodyTop - y1).attr("fill", theme.pump.riserFill).attr("stroke", "none");
2505
+ symG.append("path").attr("class", cls.constructionRect).attr("d", pipeWallsPath(midX - riserHalf, midX + riserHalf, y1, bodyTop)).attr("fill", "none").attr("stroke", theme.pump.riserStroke).attr("stroke-width", theme.pump.riserStrokeWidth);
2506
+ symG.append("path").attr("class", cls.constructionRect).attr(
2507
+ "d",
2508
+ `M${midX - bodyHalf},${bodyTop}H${midX + bodyHalf}V${y2}H${midX - bodyHalf}Z`
2509
+ ).attr("fill", theme.pump.fill).attr("stroke", theme.pump.stroke).attr("stroke-width", theme.pump.strokeWidth);
2510
+ } else if (item.subKind === "reduction") {
2511
+ const topW = rw * 0.5;
2512
+ const y1 = rowSymY - rh / 2;
2513
+ const y2 = rowSymY + rh / 2;
2514
+ const midX = cx + rw / 2;
2515
+ const tl = `${midX - topW / 2},${y1}`;
2516
+ const tr = `${midX + topW / 2},${y1}`;
2517
+ symG.append("polygon").attr("class", cls.constructionRect).attr("points", `${tl} ${tr} ${cx + rw},${y2} ${cx},${y2}`).attr("fill", theme.reduction.fill).attr("stroke", "none");
2518
+ symG.append("path").attr("class", cls.constructionRect).attr("d", `M${tl}L${cx},${y2}M${tr}L${cx + rw},${y2}`).attr("fill", "none").attr("stroke", theme.reduction.stroke).attr("stroke-width", theme.legend.itemStrokeWidth);
2519
+ } else if (item.subKind === "wellCase" || item.subKind === "wellScreen" || item.subKind === "boreHole") {
2520
+ const y1 = rowSymY - rh / 2;
2521
+ const y2 = rowSymY + rh / 2;
2522
+ const shoe = 3;
2523
+ const xL = cx + shoe;
2524
+ const xR = cx + rw - shoe;
2525
+ const style = {
2526
+ wellCase: { fill: theme.wellCase.fill, stroke: theme.wellCase.stroke },
2527
+ wellScreen: {
2528
+ fill: legendTextures.well_screen.url(),
2529
+ stroke: theme.wellScreen.stroke
2530
+ },
2531
+ boreHole: { fill: theme.boreHole.fill, stroke: theme.boreHole.stroke }
2532
+ }[item.subKind];
2533
+ symG.append("rect").attr("class", cls.constructionRect).attr("x", xL).attr("y", y1).attr("width", xR - xL).attr("height", rh).attr("fill", style.fill).attr("stroke", "none");
2534
+ const walls = symG.append("path").attr("class", cls.constructionRect).attr(
2535
+ "d",
2536
+ pipeWallsPath(xL, xR, y1, y2) + (item.subKind === "boreHole" ? `M${xL},${y2}H${xR}` : "")
2537
+ ).attr("fill", "none").attr("stroke", style.stroke).attr("stroke-width", theme.legend.itemStrokeWidth);
2538
+ if (item.subKind === "boreHole")
2539
+ walls.attr("stroke-dasharray", theme.boreHole.strokeDasharray);
2540
+ if (item.subKind === "wellCase")
2541
+ symG.append("path").attr("class", cls.constructionRect).attr("d", shoePath(xL, xR, y2, shoe)).attr("fill", style.stroke).attr("stroke", "none");
2542
+ if (item.subKind === "wellScreen")
2543
+ symG.append("path").attr("class", cls.constructionRect).attr(
2544
+ "d",
2545
+ screenEndPath(xL, xR, y1 + 0.5) + screenEndPath(xL, xR, y2 - 0.5)
2546
+ ).attr("fill", "none").attr("stroke", style.stroke).attr("stroke-width", theme.wellScreen.endStrokeWidth).attr("stroke-dasharray", "2, 1.5");
2547
+ } else if (item.subKind === "cementPad") {
2548
+ const h = rh * 0.6;
2549
+ const { slabs } = getCementPadGeometry(
2550
+ rw,
2551
+ 2 * cx + rw,
2552
+ rowSymY + h / 2,
2553
+ { widthRatio: 1, height: h },
2554
+ rw * 0.3
2555
+ );
2556
+ for (const d of slabs)
2557
+ symG.append("path").attr("class", cls.constructionRect).attr("d", d).attr("fill", legendTextures.pad.url()).attr("stroke", theme.cementPad.stroke).attr("stroke-width", theme.legend.itemStrokeWidth).attr("stroke-linejoin", RC);
2558
+ } else {
2559
+ const symMap = {
2560
+ holeFillGravel: {
2561
+ fill: legendTextures.gravel_pack.url(),
2562
+ stroke: theme.holeFill.stroke
2563
+ },
2564
+ holeFillSeal: {
2565
+ fill: legendTextures.seal.url(),
2566
+ stroke: theme.holeFill.stroke
2567
+ },
2568
+ conflict: {
2569
+ fill: legendTextures.conflict.url(),
2570
+ stroke: theme.conflict.stroke
2571
+ }
2572
+ };
2573
+ const sym = symMap[item.subKind];
2574
+ const symRect = symG.append("rect").attr("class", cls.constructionRect).attr("x", cx).attr("y", rowSymY - rh / 2).attr("width", rw).attr("height", rh).attr("fill", sym.fill).attr("stroke", sym.stroke).attr("stroke-width", theme.legend.itemStrokeWidth);
2575
+ if (sym.dasharray) symRect.attr("stroke-dasharray", sym.dasharray);
2576
+ }
2577
+ }
2578
+ function resolveStyle(options) {
2579
+ return {
2580
+ theme: defu(options.theme ?? {}, DEFAULT_WELL_THEME),
2581
+ cls: options.classNames ? { ...DEFAULT_COMPONENTS_CLASS_NAMES.legend, ...options.classNames } : DEFAULT_COMPONENTS_CLASS_NAMES.legend,
2582
+ textures: createWellTextures(options.textures)
2583
+ };
2584
+ }
2585
+ function drawWellLegendSymbol(selector, item, options = {}) {
2586
+ const style = resolveStyle(options);
2587
+ const svgEl = d37.select(selector);
2588
+ svgEl.selectAll("*").remove();
2589
+ svgEl.attr("width", SYMBOL_WIDTH + SYMBOL_MARGIN * 2).attr("height", SYMBOL_HEIGHT + SYMBOL_MARGIN * 2);
2590
+ Object.values(style.textures).forEach((t) => svgEl.call(t));
2591
+ const symG = svgEl.append("g").attr("class", style.cls.item);
2592
+ drawLegendSymbol(
2593
+ symG,
2594
+ item,
2595
+ SYMBOL_MARGIN,
2596
+ SYMBOL_MARGIN + SYMBOL_HEIGHT / 2,
2597
+ style
2598
+ );
2599
+ }
2600
+ function drawWellLegend(selector, profile, options = {}) {
2601
+ const cfg = defu(
2602
+ options.config ?? {},
2603
+ INTERACTIVE_RENDER_CONFIG.legend
2604
+ );
2605
+ const items = getWellLegendItems(profile, cfg.labels);
2606
+ const svgEl = d37.select(selector);
1663
2607
  svgEl.selectAll("*").remove();
1664
2608
  if (items.length === 0) {
1665
2609
  svgEl.attr("width", 0).attr("height", 0);
1666
2610
  return;
1667
2611
  }
1668
- const legendTextures = createWellTextures(options.textures);
1669
- Object.values(legendTextures).forEach((t) => svgEl.call(t));
2612
+ const style = resolveStyle(options);
2613
+ const { theme, cls } = style;
2614
+ Object.values(style.textures).forEach((t) => svgEl.call(t));
1670
2615
  const itemsPerRow = cfg.maxWidth ? Math.max(1, Math.floor((cfg.maxWidth - cfg.padding * 2) / cfg.itemWidth)) : items.length;
1671
2616
  const numRows = Math.ceil(items.length / itemsPerRow);
1672
2617
  const rowWidth = Math.min(items.length, itemsPerRow) * cfg.itemWidth;
1673
2618
  const totalWidth = cfg.padding * 2 + rowWidth;
1674
2619
  const totalHeight = numRows * cfg.height;
1675
2620
  svgEl.attr("width", totalWidth).attr("height", totalHeight);
1676
- const RC = "round";
1677
2621
  const g = svgEl.append("g").attr("transform", `translate(${cfg.padding}, 2)`);
1678
2622
  g.append("rect").attr("class", cls.border).attr("x", 0).attr("y", 0).attr("width", totalWidth - cfg.padding).attr("height", totalHeight - 4).attr("fill", "white").attr("rx", cfg.borderRadius).attr("stroke-width", theme.legend.borderStrokeWidth);
1679
2623
  g.append("text").attr("class", cls.title).attr("x", 4).attr("y", 11).attr("font-size", theme.legend.fontSize).attr("font-family", theme.legend.fontFamily ?? "sans-serif").attr("font-weight", theme.legend.titleFontWeight ?? "bold").text(`${cfg.title}:`);
1680
- const rw = 24;
1681
- const rh = 12;
1682
2624
  items.forEach((item, i) => {
1683
2625
  const col = i % itemsPerRow;
1684
2626
  const row = Math.floor(i / itemsPerRow);
1685
2627
  const rowSymY = row * cfg.height + (cfg.height - 4) / 2;
1686
2628
  const cx = col * cfg.itemWidth;
1687
2629
  const symG = g.append("g").attr("class", cls.item);
1688
- if (item.kind === "fracture") {
1689
- const color = item.water_intake ? theme.fracture.wetStroke : theme.fracture.dryStroke;
1690
- if (item.swarm) {
1691
- [-4, 0, 4].forEach((offset) => {
1692
- symG.append("polyline").attr("class", cls.fracturePoly).attr(
1693
- "points",
1694
- `${cx},${rowSymY + offset} ${cx + 6},${rowSymY + offset - 1} ${cx + 12},${rowSymY + offset + 1} ${cx + 18},${rowSymY + offset - 0.5} ${cx + 24},${rowSymY + offset}`
1695
- ).attr("stroke", color).attr(
1696
- "stroke-width",
1697
- offset !== 0 ? theme.legend.fractureSideStrokeWidth : theme.legend.fractureStrokeWidth
1698
- ).attr("fill", "none").attr("stroke-linecap", RC).attr("stroke-linejoin", RC);
1699
- });
1700
- } else {
1701
- symG.append("polyline").attr("class", cls.fracturePoly).attr(
1702
- "points",
1703
- `${cx},${rowSymY} ${cx + 5},${rowSymY - 1.5} ${cx + 11},${rowSymY + 1} ${cx + 17},${rowSymY - 0.5} ${cx + 24},${rowSymY}`
1704
- ).attr("stroke", color).attr("stroke-width", theme.legend.fractureStrokeWidth).attr("fill", "none").attr("stroke-linecap", RC).attr("stroke-linejoin", RC);
1705
- }
1706
- } else if (item.kind === "cave") {
1707
- const color = item.water_intake ? theme.cave.wetStroke : theme.cave.dryStroke;
1708
- const texture = item.water_intake ? legendTextures.cave_wet : legendTextures.cave_dry;
1709
- symG.append("rect").attr("class", cls.caveFill).attr("x", cx).attr("y", rowSymY - rh / 2).attr("width", rw).attr("height", rh).attr("fill", texture.url()).attr("stroke", color).attr("stroke-width", theme.legend.itemStrokeWidth);
1710
- } else if (item.subKind === "surfaceCase") {
1711
- symG.append("rect").attr("class", cls.constructionRect).attr("x", cx).attr("y", rowSymY - rh / 2).attr("width", rw).attr("height", rh).attr("fill", legendTextures.surface_case.url()).attr("stroke", "none");
1712
- symG.append("line").attr("class", cls.constructionRect).attr("x1", cx).attr("x2", cx).attr("y1", rowSymY - rh / 2).attr("y2", rowSymY + rh / 2).attr("stroke", theme.surfaceCase.stroke).attr("stroke-width", 2);
1713
- symG.append("line").attr("class", cls.constructionRect).attr("x1", cx + rw).attr("x2", cx + rw).attr("y1", rowSymY - rh / 2).attr("y2", rowSymY + rh / 2).attr("stroke", theme.surfaceCase.stroke).attr("stroke-width", 2);
1714
- } else if (item.subKind === "reduction") {
1715
- const topW = rw * 0.5;
1716
- const y1 = rowSymY - rh / 2;
1717
- const y2 = rowSymY + rh / 2;
1718
- const midX = cx + rw / 2;
1719
- symG.append("polygon").attr("class", cls.constructionRect).attr(
1720
- "points",
1721
- `${midX - topW / 2},${y1} ${midX + topW / 2},${y1} ${cx + rw},${y2} ${cx},${y2}`
1722
- ).attr("fill", theme.reduction.fill).attr("stroke", theme.reduction.stroke).attr("stroke-width", theme.legend.itemStrokeWidth);
1723
- } else {
1724
- const symMap = {
1725
- boreHole: {
1726
- fill: theme.boreHole.fill,
1727
- stroke: theme.boreHole.stroke,
1728
- dasharray: theme.boreHole.strokeDasharray
1729
- },
1730
- holeFillGravel: {
1731
- fill: legendTextures.gravel_pack.url(),
1732
- stroke: theme.holeFill.stroke
1733
- },
1734
- holeFillSeal: {
1735
- fill: legendTextures.seal.url(),
1736
- stroke: theme.holeFill.stroke
1737
- },
1738
- wellCase: { fill: theme.wellCase.fill, stroke: theme.wellCase.stroke },
1739
- wellScreen: {
1740
- fill: legendTextures.well_screen.url(),
1741
- stroke: theme.wellScreen.stroke
1742
- },
1743
- cementPad: {
1744
- fill: legendTextures.pad.url(),
1745
- stroke: theme.cementPad.stroke
1746
- },
1747
- conflict: {
1748
- fill: legendTextures.conflict.url(),
1749
- stroke: theme.conflict.stroke
1750
- }
1751
- };
1752
- const sym = symMap[item.subKind];
1753
- const symRect = symG.append("rect").attr("class", cls.constructionRect).attr("x", cx).attr("y", rowSymY - rh / 2).attr("width", rw).attr("height", rh).attr("fill", sym.fill).attr("stroke", sym.stroke).attr("stroke-width", theme.legend.itemStrokeWidth);
1754
- if (sym.dasharray) symRect.attr("stroke-dasharray", sym.dasharray);
1755
- }
2630
+ drawLegendSymbol(symG, item, cx, rowSymY, style);
1756
2631
  g.append("text").attr("class", cls.label).attr("x", cx + 28).attr("y", rowSymY + 3).attr("font-size", theme.legend.fontSize).attr("font-family", theme.legend.fontFamily ?? "sans-serif").attr("font-weight", theme.legend.labelFontWeight ?? 400).text(item.label);
1757
2632
  });
1758
2633
  }
1759
2634
 
1760
2635
  // src/renderers/lithology.renderer.ts
2636
+ import * as d38 from "d3";
1761
2637
  function drawLithology(ctx, data) {
1762
- if (!data.length) return;
1763
2638
  const {
1764
2639
  groups: { lithologyGroup },
1765
2640
  svg,
@@ -1783,10 +2658,219 @@ function drawLithology(ctx, data) {
1783
2658
  const merged = mergeEnter(newLayers, rects);
1784
2659
  withTransition(merged, transition).attr("y", getYPos).attr("height", getHeight).attr("fill", getLithologyFill(data, svg, ctx.theme.lithologyTexture));
1785
2660
  }
2661
+ function drawLithologySwatch(selector, layer, options) {
2662
+ const { size, theme } = options;
2663
+ const svg = d38.select(selector);
2664
+ svg.selectAll("*").remove();
2665
+ svg.attr("width", size).attr("height", size);
2666
+ const fill = getLithologyFill([layer], svg, {
2667
+ ...theme.lithologyTexture,
2668
+ // Denser pattern so a few strokes show inside the small square.
2669
+ size: Math.max(12, size * 3)
2670
+ });
2671
+ svg.append("rect").attr("x", 0.5).attr("y", 0.5).attr("width", size - 1).attr("height", size - 1).attr("fill", fill(layer)).attr("stroke", theme.lithology.stroke).attr("stroke-width", 0.5);
2672
+ }
2673
+
2674
+ // src/renderers/markers.renderer.ts
2675
+ function drawMarkers(ctx, data) {
2676
+ const group = ctx.groups.markersGroup;
2677
+ group.selectAll("*").remove();
2678
+ const cfg = ctx.renderConfig.markers;
2679
+ const theme = ctx.theme.markers;
2680
+ const r = cfg.radius;
2681
+ if (cfg.lithology) {
2682
+ const cx = (ctx.geoXLeft + ctx.geoXRight) / 2;
2683
+ getLithologyMarkers(data.lithology, ctx.depthFrom, ctx.depthTo).forEach(
2684
+ (marker) => {
2685
+ const top = ctx.yScale(marker.from);
2686
+ const bottom = ctx.yScale(marker.to);
2687
+ if (bottom - top < r) return;
2688
+ const cy = bottom - top >= 2 * r + 4 ? top + r + 2 : (top + bottom) / 2;
2689
+ const g = group.append("g").attr("class", ctx.classes.markers.lithology);
2690
+ g.append("circle").attr("cx", cx).attr("cy", cy).attr("r", r).attr("fill", theme.fill).attr("stroke", theme.stroke).attr("stroke-width", theme.strokeWidth);
2691
+ g.append("text").attr("x", cx).attr("y", cy).attr("text-anchor", "middle").attr("dominant-baseline", "central").attr("font-size", theme.fontSize).attr("font-family", theme.fontFamily ?? "sans-serif").attr("font-weight", theme.fontWeight ?? 600).attr("fill", theme.color).text(String(marker.index));
2692
+ }
2693
+ );
2694
+ }
2695
+ if (cfg.fractures) {
2696
+ const x = ctx.pocoCenterX + ctx.POCO_WIDTH * ctx.renderConfig.fractures.widthMultiplier / 2 + 3;
2697
+ getFractureMarkers(data.fractures, ctx.depthFrom, ctx.depthTo).forEach(
2698
+ (marker) => {
2699
+ group.append("text").attr("class", ctx.classes.markers.fracture).attr("x", x).attr("y", ctx.yScale(marker.depth)).attr("dominant-baseline", "central").attr("font-size", theme.fontSize).attr("font-family", theme.fontFamily ?? "sans-serif").attr("font-weight", theme.fontWeight ?? 600).attr("fill", theme.color).text(marker.label);
2700
+ }
2701
+ );
2702
+ }
2703
+ }
2704
+
2705
+ // src/renderers/overlap.renderer.ts
2706
+ import textures3 from "textures";
2707
+ import { formatNumber as formatNumber3 } from "@welldot/utils";
2708
+ function drawOverlap(ctx) {
2709
+ const group = ctx.groups.overlapGroup;
2710
+ group.selectAll("*").remove();
2711
+ const { repeatedTo, depthFrom } = ctx;
2712
+ if (repeatedTo == null || repeatedTo <= depthFrom) return;
2713
+ const theme = ctx.theme.overlap;
2714
+ const { margins } = ctx.state;
2715
+ const overview = ctx.renderConfig.overview;
2716
+ const showsDescription = hasDescriptionColumn(ctx);
2717
+ const x0 = -margins.left + (overview.active ? overview.x + overview.width + 4 : 0);
2718
+ const x1 = showsDescription ? ctx.geoXRight + ctx.renderConfig.labels.descriptionXOffset - 4 : ctx.svgWidth - margins.left;
2719
+ const y0 = ctx.yScale(depthFrom);
2720
+ const y1 = ctx.yScale(repeatedTo);
2721
+ const hatch = textures3.lines().size(theme.size).strokeWidth(theme.strokeWidth).stroke(theme.stroke);
2722
+ ctx.svg.call(hatch);
2723
+ group.append("rect").attr("class", ctx.classes.overlap.band).attr("x", x0).attr("y", y0).attr("width", Math.max(0, x1 - x0)).attr("height", Math.max(0, y1 - y0)).attr("fill", hatch.url()).style("pointer-events", "none");
2724
+ group.append("line").attr("x1", x0).attr("x2", ctx.svgWidth - margins.left).attr("y1", y1).attr("y2", y1).attr("stroke", theme.ruleStroke).attr("stroke-width", 0.6).attr("stroke-dasharray", "3, 2");
2725
+ const whole = (m) => formatNumber3(ctx.units.length === "ft" ? m * 3.28084 : m, {
2726
+ maximumFractionDigits: 0,
2727
+ locale: ctx.locale
2728
+ });
2729
+ const unit = getLengthUnit(ctx.units.length);
2730
+ const range = `${whole(depthFrom)}\u2013${whole(repeatedTo)} ${unit}`;
2731
+ group.append("text").attr("class", ctx.classes.overlap.label).attr("x", ctx.svgWidth - margins.left - 2).attr("y", y0 + theme.fontSize + 1).attr("text-anchor", "end").attr("font-size", theme.fontSize).attr("font-family", theme.fontFamily ?? "monospace").attr("fill", theme.labelColor).text(formatOverlapLabel(ctx.renderConfig.overlap.label, range));
2732
+ }
2733
+
2734
+ // src/renderers/overview.renderer.ts
2735
+ function drawOverview(ctx, lithology) {
2736
+ const group = ctx.groups.overviewGroup;
2737
+ group.selectAll("*").remove();
2738
+ const cfg = ctx.renderConfig.overview;
2739
+ if (!cfg.active || lithology.length === 0 || ctx.totalDepth <= 0) return;
2740
+ const theme = ctx.theme.overview;
2741
+ const height = Math.min(cfg.height, ctx.state.height);
2742
+ const { y, window } = getOverviewGeometry(
2743
+ ctx.totalDepth,
2744
+ height,
2745
+ ctx.depthFrom,
2746
+ ctx.depthTo
2747
+ );
2748
+ lithology.forEach((layer) => {
2749
+ group.append("rect").attr("class", ctx.classes.overview.item).attr("x", cfg.x).attr("y", y(layer.from)).attr("width", cfg.width).attr("height", Math.max(0, y(layer.to) - y(layer.from))).attr("fill", layer.color || theme.emptyFill);
2750
+ });
2751
+ group.append("rect").attr("x", cfg.x).attr("y", 0).attr("width", cfg.width).attr("height", y(ctx.totalDepth)).attr("fill", "none").attr("stroke", theme.stroke).attr("stroke-width", theme.strokeWidth);
2752
+ group.append("rect").attr("class", ctx.classes.overview.window).attr("x", cfg.x - 2).attr("y", window.y).attr("width", cfg.width + 4).attr("height", window.height).attr("fill", "none").attr("stroke", theme.windowStroke).attr("stroke-width", theme.windowStrokeWidth);
2753
+ }
2754
+
2755
+ // src/renderers/pump.renderer.ts
2756
+ import * as d39 from "d3";
2757
+ import { getProfileDiamValues as getProfileDiamValues5 } from "@welldot/utils";
2758
+ function getPumpSpans(panelTop, panelBottom, bodyTop, intakeY) {
2759
+ const riserBottom = Math.min(Math.max(panelTop, bodyTop), panelBottom);
2760
+ const top = Math.max(bodyTop, panelTop);
2761
+ const bottom = Math.min(intakeY, panelBottom);
2762
+ return {
2763
+ riserBottom,
2764
+ body: bottom > top ? { top, bottom, cutTop: top > bodyTop, cutBottom: bottom < intakeY } : null
2765
+ };
2766
+ }
2767
+ var riserBreakAmplitude = (width) => Math.min(2, width / 3);
2768
+ function getRiserTop(xLeft, xRight, y, broken) {
2769
+ if (!broken) return { edge: `H${xRight}`, breakLine: "" };
2770
+ const a = riserBreakAmplitude(xRight - xLeft);
2771
+ const w = xRight - xLeft;
2772
+ const zigzag = `L${xLeft + w / 3},${y - a}L${xLeft + 2 * w / 3},${y + a}L${xRight},${y}`;
2773
+ return { edge: zigzag, breakLine: `M${xLeft},${y}${zigzag}` };
2774
+ }
2775
+ var BODY_MAX_FILL = 0.85;
2776
+ var RISER_MAX_FILL = 0.7;
2777
+ function getPumpDiameters(intakeD, narrowestD, riserDiameter, cfg) {
2778
+ const maxBody = intakeD * BODY_MAX_FILL;
2779
+ const riser = Math.min(
2780
+ riserDiameter ?? intakeD * cfg.riserWidthRatio,
2781
+ narrowestD * RISER_MAX_FILL,
2782
+ maxBody * RISER_MAX_FILL
2783
+ );
2784
+ const body = Math.min(
2785
+ Math.max(intakeD * cfg.bodyWidthRatio, riser * 1.4),
2786
+ maxBody
2787
+ );
2788
+ return { riser, body };
2789
+ }
2790
+ function drawPump(ctx, pump) {
2791
+ const group = ctx.groups.pumpGroup;
2792
+ const cls = ctx.classes.pump;
2793
+ const cfg = ctx.renderConfig.construction.pump;
2794
+ const theme = ctx.theme.pump;
2795
+ const intake = pump?.intake_depth;
2796
+ const visible = !!pump && cfg.active && intake !== void 0 && intake > 0 && // the riser crosses this panel when the panel starts above the intake
2797
+ ctx.depthFrom < intake;
2798
+ const data = visible ? [pump] : [];
2799
+ const selection = group.selectAll(`g.${cls.item}`).data(data, (d) => d.id);
2800
+ selection.exit().remove();
2801
+ if (!visible) return;
2802
+ const maxXValue = d39.max(getProfileDiamValues5(ctx.constructionData)) || 0;
2803
+ const xScale = d39.scaleLinear().domain([0, maxXValue]).range([0, ctx.POCO_WIDTH]);
2804
+ const pipes = [
2805
+ ...ctx.constructionData.well_case,
2806
+ ...ctx.constructionData.well_screen
2807
+ ];
2808
+ const casingD = diameterAt(pipes, intake) ?? diameterAt(ctx.constructionData.bore_hole, intake) ?? maxXValue;
2809
+ const narrowestD = Math.min(
2810
+ casingD,
2811
+ ...[...pipes, ...ctx.constructionData.bore_hole].filter((i) => i.from < intake && i.to > 0).map((i) => i.diameter)
2812
+ );
2813
+ const { riser: riserD, body: bodyD } = getPumpDiameters(
2814
+ casingD,
2815
+ narrowestD,
2816
+ pump.riser_diameter,
2817
+ cfg
2818
+ );
2819
+ const riserLeft = (ctx.POCO_CENTER - xScale(riserD)) / 2;
2820
+ const riserRight = (ctx.POCO_CENTER + xScale(riserD)) / 2;
2821
+ const bodyLeft = (ctx.POCO_CENTER - xScale(bodyD)) / 2;
2822
+ const bodyRight = (ctx.POCO_CENTER + xScale(bodyD)) / 2;
2823
+ const yIntake = ctx.yScale(intake);
2824
+ const yBodyTop = yIntake - cfg.bodyHeight;
2825
+ const headDepth = Math.min(
2826
+ 0,
2827
+ ...pipes.filter((p) => p.from < 0).map((p) => p.from)
2828
+ );
2829
+ const headInPanel = ctx.depthFrom <= headDepth;
2830
+ const padOnHead = headDepth === 0 && !!ctx.constructionData.cement_pad?.thickness;
2831
+ const yRiserTop = headInPanel ? ctx.yScale(headDepth) - (padOnHead ? ctx.renderConfig.construction.cementPad.height - riserBreakAmplitude(xScale(riserD)) : 0) : ctx.yScale(ctx.depthFrom);
2832
+ const { riserBottom: yRiserBottom, body } = getPumpSpans(
2833
+ yRiserTop,
2834
+ ctx.yScale(ctx.depthTo),
2835
+ yBodyTop,
2836
+ yIntake
2837
+ );
2838
+ const r = Math.min(3, (bodyRight - bodyLeft) / 4);
2839
+ const entered = selection.enter().append("g").attr("class", cls.item).on("mouseover", ctx.tooltips.pump.show).on("mouseout", ctx.tooltips.pump.hide);
2840
+ entered.append("path").attr("class", `${cls.item}-riser-fill`);
2841
+ entered.append("path").attr("class", `${cls.item}-riser`);
2842
+ entered.append("path").attr("class", `${cls.item}-body`);
2843
+ entered.append("path").attr("class", `${cls.item}-intake`);
2844
+ const merged = entered.merge(selection);
2845
+ const top = getRiserTop(riserLeft, riserRight, yRiserTop, headInPanel);
2846
+ merged.select(`.${cls.item}-riser-fill`).attr(
2847
+ "d",
2848
+ yRiserBottom > yRiserTop ? `M${riserLeft},${yRiserTop}${top.edge}V${yRiserBottom}H${riserLeft}Z` : ""
2849
+ ).attr("fill", theme.riserFill).attr("stroke", "none");
2850
+ merged.select(`.${cls.item}-riser`).attr(
2851
+ "d",
2852
+ yRiserBottom > yRiserTop ? `M${riserLeft},${yRiserTop}V${yRiserBottom}M${riserRight},${yRiserTop}V${yRiserBottom}${top.breakLine}` : ""
2853
+ ).attr("stroke-linejoin", "round").attr("fill", "none").attr("stroke", theme.riserStroke).attr("stroke-width", theme.riserStrokeWidth);
2854
+ let bodyPath = "";
2855
+ if (body) {
2856
+ const rt = body.cutTop ? 0 : Math.min(r, (body.bottom - body.top) / 2);
2857
+ const rb = body.cutBottom ? 0 : Math.min(r, (body.bottom - body.top) / 2);
2858
+ const { top: top2, bottom } = body;
2859
+ bodyPath = `M${bodyLeft + rt},${top2}H${bodyRight - rt}Q${bodyRight},${top2} ${bodyRight},${top2 + rt}V${bottom - rb}Q${bodyRight},${bottom} ${bodyRight - rb},${bottom}H${bodyLeft + rb}Q${bodyLeft},${bottom} ${bodyLeft},${bottom - rb}V${top2 + rt}Q${bodyLeft},${top2} ${bodyLeft + rt},${top2}Z`;
2860
+ }
2861
+ merged.select(`.${cls.item}-body`).attr("d", bodyPath).attr("fill", theme.fill).attr("stroke", theme.stroke).attr("stroke-width", theme.strokeWidth);
2862
+ const hatchY = [
2863
+ yIntake - cfg.bodyHeight * 0.3,
2864
+ yIntake - cfg.bodyHeight * 0.15
2865
+ ].filter((y) => body && y > body.top && y < body.bottom);
2866
+ merged.select(`.${cls.item}-intake`).attr(
2867
+ "d",
2868
+ hatchY.map((y) => `M${bodyLeft + 1.5},${y}H${bodyRight - 1.5}`).join("")
2869
+ ).attr("fill", "none").attr("stroke", theme.stroke).attr("stroke-width", 0.8);
2870
+ }
1786
2871
 
1787
2872
  // src/renderers/unit-labels.renderer.ts
1788
2873
  function drawUnitLabels(ctx, data) {
1789
- if (!ctx.renderConfig.unitLabels.active || !data.length) return;
1790
2874
  const {
1791
2875
  groups: { unitLabelsGroup },
1792
2876
  depthFrom,
@@ -1797,6 +2881,7 @@ function drawUnitLabels(ctx, data) {
1797
2881
  classes
1798
2882
  } = ctx;
1799
2883
  unitLabelsGroup.selectAll("*").remove();
2884
+ if (!renderConfig.unitLabels.active || !data.length) return;
1800
2885
  const ru = renderConfig.unitLabels;
1801
2886
  const rut = theme.unitLabels;
1802
2887
  const sw = ru.stripWidth;
@@ -1877,7 +2962,7 @@ function buildSvgStyleBlock(theme) {
1877
2962
  }
1878
2963
 
1879
2964
  // src/Renderer.ts
1880
- var d37 = { ...d3module2, tip: d3tip2 };
2965
+ var d310 = { ...d3module2, tip: d3tip2 };
1881
2966
  var WellRenderer = class {
1882
2967
  /**
1883
2968
  * @param svgs - One entry per SVG panel; multi-entry for split-panel layouts.
@@ -1937,12 +3022,14 @@ var WellRenderer = class {
1937
3022
  const { selector, height, width, margins } = inst;
1938
3023
  const svgWidth = width + margins.left + margins.right;
1939
3024
  const svgHeight = height + margins.top + margins.bottom;
1940
- const svg = d37.select(selector);
3025
+ const svg = d310.select(selector);
1941
3026
  svg.selectAll("*").remove();
1942
3027
  svg.attr("height", svgHeight).attr("width", svgWidth);
1943
3028
  const defs = svg.append("defs");
1944
3029
  defs.append("style").text(buildSvgStyleBlock(this.theme));
1945
3030
  defs.append("clipPath").attr("id", clipId).append("rect").attr("id", clipRectId).attr("x", 0).attr("y", 0).attr("width", 1e5).attr("height", 1e4);
3031
+ svg.append("g").attr("class", this.classes.overview.group).attr("transform", `translate(0, ${margins.top})`);
3032
+ svg.append("g").attr("class", this.classes.columnHeaders.group);
1946
3033
  const poco = svg.append("g").attr("class", this.classes.wellGroup);
1947
3034
  const geologic = poco.append("g").attr("class", this.classes.geologicGroup).attr("transform", `translate(${margins.left}, ${margins.top})`);
1948
3035
  geologic.append("g").attr("class", this.classes.yAxis);
@@ -1957,6 +3044,7 @@ var WellRenderer = class {
1957
3044
  );
1958
3045
  const fracturesGroup = poco.append("g").attr("class", this.classes.fractures.group).attr("transform", `translate(${margins.left}, ${margins.top})`).attr("clip-path", `url(#${clipId})`);
1959
3046
  fracturesGroup.append("g").attr("class", this.classes.highlights.fracturesGroup);
3047
+ poco.append("g").attr("class", this.classes.markers.group).attr("transform", `translate(${margins.left}, ${margins.top})`);
1960
3048
  construction.append("g").attr("class", this.classes.cementPad.group);
1961
3049
  construction.append("g").attr("class", this.classes.boreHole.group);
1962
3050
  construction.append("g").attr("class", this.classes.surfaceCase.group);
@@ -1964,11 +3052,23 @@ var WellRenderer = class {
1964
3052
  construction.append("g").attr("class", this.classes.wellCase.group);
1965
3053
  construction.append("g").attr("class", this.classes.wellScreen.group);
1966
3054
  construction.append("g").attr("class", this.classes.reduction.group);
3055
+ construction.append("g").attr("class", this.classes.pump.group);
3056
+ construction.append("g").attr("class", this.classes.centralizer.group);
1967
3057
  construction.append("g").attr("class", this.classes.conflict.group);
1968
3058
  construction.append("g").attr("class", this.classes.constructionLabels.group);
1969
3059
  construction.append("g").attr("class", this.classes.highlights.constructionGroup);
3060
+ poco.append("g").attr("class", this.classes.overlap.group).attr("transform", `translate(${margins.left}, ${margins.top})`);
1970
3061
  Object.values(this.textures).forEach((texture) => svg.call(texture));
1971
- return { svg, height, width, margins, clipId, clipRectId };
3062
+ return {
3063
+ svg,
3064
+ height,
3065
+ width,
3066
+ margins,
3067
+ clipId,
3068
+ clipRectId,
3069
+ depthRange: inst.depthRange,
3070
+ repeatedTo: inst.repeatedTo
3071
+ };
1972
3072
  }
1973
3073
  /** Renders a standalone legend into the element matched by `selector`. */
1974
3074
  renderLegend(selector, profile) {
@@ -1979,6 +3079,30 @@ var WellRenderer = class {
1979
3079
  textures: this.renderConfig.textures
1980
3080
  });
1981
3081
  }
3082
+ /** Lists the legend entries `profile` needs, labeled from `renderConfig.legend.labels`. */
3083
+ getLegendItems(profile) {
3084
+ return getWellLegendItems(profile, this.renderConfig.legend.labels);
3085
+ }
3086
+ /**
3087
+ * Renders one legend entry's symbol (see {@link getLegendItems}) into its
3088
+ * own SVG matched by `selector`, with the textures it needs defined inside.
3089
+ */
3090
+ renderLegendSymbol(selector, item) {
3091
+ drawWellLegendSymbol(selector, item, {
3092
+ theme: this.theme,
3093
+ classNames: this.classes.legend,
3094
+ textures: this.renderConfig.textures
3095
+ });
3096
+ }
3097
+ /**
3098
+ * Renders one lithology layer's fill as a `size`×`size` square into its
3099
+ * own SVG matched by `selector`, with its texture defined inside (e.g. for
3100
+ * a swatch in a PDF table). Call after `prepareSvg()`, which loads the
3101
+ * FGDC textures.
3102
+ */
3103
+ renderLithologySwatch(selector, layer, size = 10) {
3104
+ drawLithologySwatch(selector, layer, { size, theme: this.theme });
3105
+ }
1982
3106
  /**
1983
3107
  * Multiplies the current zoom scale by `factor` (e.g. `1.25` to zoom in,
1984
3108
  * `1 / 1.25` to zoom out) on every panel. No-op if both `renderConfig.zoom`
@@ -1995,7 +3119,7 @@ var WellRenderer = class {
1995
3119
  * (defaults to `1`, i.e. the initial fit-to-container view).
1996
3120
  */
1997
3121
  resetZoom() {
1998
- const target = d37.zoomIdentity.scale(this.renderConfig.zoomLevel ?? 1);
3122
+ const target = d310.zoomIdentity.scale(this.renderConfig.zoomLevel ?? 1);
1999
3123
  for (const state of this.instanceStates) {
2000
3124
  if (!state.zoomNode) continue;
2001
3125
  asSvgElement(state.svg).transition().call(state.zoomNode.transform, target);
@@ -2007,7 +3131,46 @@ var WellRenderer = class {
2007
3131
  */
2008
3132
  getZoomScale() {
2009
3133
  const node = this.instanceStates[0]?.svg.node();
2010
- return node ? d37.zoomTransform(node).k : 1;
3134
+ return node ? d310.zoomTransform(node).k : 1;
3135
+ }
3136
+ /**
3137
+ * Removes everything drawn on every panel (layers, labels, axis,
3138
+ * highlights) and keeps the structure `prepareSvg()` built, so the next
3139
+ * `draw()` needs no new `prepareSvg()`. `draw()` calls it for an empty well.
3140
+ */
3141
+ clear() {
3142
+ const c = this.classes;
3143
+ const layerGroups = [
3144
+ c.overview.group,
3145
+ c.columnHeaders.group,
3146
+ c.yAxis,
3147
+ c.unitLabels.group,
3148
+ c.lithology.group,
3149
+ c.caves.group,
3150
+ c.labels.lithology.group,
3151
+ c.highlights.geologicGroup,
3152
+ c.highlights.constructionGroup,
3153
+ c.highlights.fracturesGroup,
3154
+ c.markers.group,
3155
+ c.cementPad.group,
3156
+ c.boreHole.group,
3157
+ c.surfaceCase.group,
3158
+ c.holeFill.group,
3159
+ c.wellCase.group,
3160
+ c.wellScreen.group,
3161
+ c.reduction.group,
3162
+ c.pump.group,
3163
+ c.centralizer.group,
3164
+ c.conflict.group,
3165
+ c.constructionLabels.group,
3166
+ c.overlap.group
3167
+ ];
3168
+ for (const { svg } of this.instanceStates) {
3169
+ for (const cls of layerGroups) {
3170
+ svg.select(`.${cls}`).selectAll("*").interrupt().remove();
3171
+ }
3172
+ svg.select(`.${c.fractures.group}`).selectAll(`g.${c.fractures.item}`).interrupt().remove();
3173
+ }
2011
3174
  }
2012
3175
  /**
2013
3176
  * Initialises the SVG DOM structure and preloads FGDC textures.
@@ -2020,14 +3183,19 @@ var WellRenderer = class {
2020
3183
  );
2021
3184
  }
2022
3185
  /**
2023
- * Renders the well profile into all configured SVG panels.
3186
+ * Renders the well profile into all configured SVG panels. Layers the
3187
+ * profile no longer has are removed; an empty well (`isWellEmpty`) clears
3188
+ * the panels (see {@link clear}).
2024
3189
  * @param profile - The well data to render.
2025
3190
  * @param options.units - Override the instance-level measurement units for this render.
2026
3191
  * @param options.locale - Override the instance-level locale for this render.
2027
3192
  * @param options.highlights - Highlight overlays to display on top of the profile.
2028
3193
  */
2029
3194
  draw(profile, options = {}) {
2030
- if (isWellEmpty(profile)) return;
3195
+ if (isWellEmpty(profile)) {
3196
+ this.clear();
3197
+ return;
3198
+ }
2031
3199
  if (profile.version !== 2) {
2032
3200
  console.warn(
2033
3201
  `[WellRenderer] Received a .well file with version ${profile.version ?? "unknown"}. Only v2 is supported. Normalize with deserializeWell() from @welldot/core before rendering.`
@@ -2039,35 +3207,18 @@ var WellRenderer = class {
2039
3207
  }
2040
3208
  const highlights = options.highlights ?? {};
2041
3209
  const maxValues = getProfileLastItemsDepths(profile);
2042
- const maxYValues = d37.max(maxValues) || 0;
2043
- if (this.instanceStates.length === 1) {
2044
- this.drawLogToInstance(
2045
- this.instanceStates[0],
2046
- profile,
2047
- 0,
2048
- maxYValues,
2049
- highlights
2050
- );
2051
- return;
2052
- }
2053
- const totalHeight = this.instanceStates.reduce((s, st) => s + st.height, 0) || 1;
2054
- const yScaleGlobal = d37.scaleLinear().domain([0, maxYValues]).range([0, totalHeight]);
2055
- let currentDepth = 0;
2056
- for (const state of this.instanceStates) {
2057
- const maxSvgDepth = yScaleGlobal.invert(
2058
- yScaleGlobal(currentDepth) + state.height
2059
- );
2060
- this.drawLogToInstance(
2061
- state,
2062
- profile,
2063
- currentDepth,
2064
- maxSvgDepth,
2065
- highlights
2066
- );
2067
- currentDepth = maxSvgDepth;
2068
- }
3210
+ const maxYValues = d310.max(maxValues) || 0;
3211
+ const windows = resolvePanelWindows(
3212
+ this.instanceStates,
3213
+ maxYValues,
3214
+ getProfileTopDepth(profile)
3215
+ );
3216
+ this.instanceStates.forEach((state, i) => {
3217
+ const [from, to] = windows[i];
3218
+ this.drawLogToInstance(state, profile, from, to, maxYValues, highlights);
3219
+ });
2069
3220
  }
2070
- drawLogToInstance(state, profile, depthFrom, depthTo, highlights = {}) {
3221
+ drawLogToInstance(state, profile, depthFrom, depthTo, totalDepth, highlights = {}) {
2071
3222
  const svg = state.svg;
2072
3223
  const pocoGroup = svg.select(`.${this.classes.wellGroup}`);
2073
3224
  const lithologyGroup = svg.select(`.${this.classes.lithology.group}`);
@@ -2084,6 +3235,8 @@ var WellRenderer = class {
2084
3235
  const wellCaseGroup = svg.select(`.${this.classes.wellCase.group}`);
2085
3236
  const wellScreenGroup = svg.select(`.${this.classes.wellScreen.group}`);
2086
3237
  const reductionGroup = svg.select(`.${this.classes.reduction.group}`);
3238
+ const centralizerGroup = svg.select(`.${this.classes.centralizer.group}`);
3239
+ const pumpGroup = svg.select(`.${this.classes.pump.group}`);
2087
3240
  const conflictGroup = svg.select(`.${this.classes.conflict.group}`);
2088
3241
  const constructionLabelsGroup = svg.select(
2089
3242
  `.${this.classes.constructionLabels.group}`
@@ -2098,13 +3251,19 @@ var WellRenderer = class {
2098
3251
  `.${this.classes.highlights.fracturesGroup}`
2099
3252
  );
2100
3253
  const unitLabelsGroup = svg.select(`.${this.classes.unitLabels.group}`);
3254
+ const overviewGroup = svg.select(`.${this.classes.overview.group}`);
3255
+ const columnHeadersGroup = svg.select(
3256
+ `.${this.classes.columnHeaders.group}`
3257
+ );
3258
+ const markersGroup = svg.select(`.${this.classes.markers.group}`);
3259
+ const overlapGroup = svg.select(`.${this.classes.overlap.group}`);
2101
3260
  const svgWidth = state.width + state.margins.left + state.margins.right;
2102
3261
  const svgHeight = state.height + state.margins.top + state.margins.bottom;
2103
3262
  const { pocoWidthRatio, pocoCenterRatio } = this.renderConfig.layout;
2104
3263
  const POCO_WIDTH = svgWidth * pocoWidthRatio;
2105
3264
  const POCO_CENTER = svgWidth * pocoCenterRatio;
2106
3265
  const { duration, ease } = this.renderConfig.animation;
2107
- const transition = d37.transition().duration(duration).ease(ease);
3266
+ const transition = d310.transition().duration(duration).ease(ease);
2108
3267
  const tooltips = populateTooltips(
2109
3268
  svg,
2110
3269
  this.classes,
@@ -2129,14 +3288,15 @@ var WellRenderer = class {
2129
3288
  well_case: profile.well_case,
2130
3289
  well_screen: profile.well_screen,
2131
3290
  reduction: profile.reduction,
3291
+ centralizers: profile.centralizers ?? [],
2132
3292
  fractures: profile.fractures
2133
3293
  };
2134
3294
  const contentHeight = svgHeight - state.margins.top - state.margins.bottom;
2135
- const yScaleLocal = d37.scaleLinear().domain([depthFrom, depthTo]).range([0, contentHeight]);
3295
+ const yScaleLocal = d310.scaleLinear().domain([depthFrom, depthTo]).range([0, contentHeight]);
2136
3296
  const depthFactor = this.units.length === "ft" ? 3.28084 : 1;
2137
- const yScaleAxis = d37.scaleLinear().domain([depthFrom * depthFactor, depthTo * depthFactor]).range([0, contentHeight]);
2138
- const yAxis = d37.axisLeft(yScaleAxis).tickFormat(
2139
- (d) => `${formatNumber3(Number(d), {
3297
+ const yScaleAxis = d310.scaleLinear().domain([depthFrom * depthFactor, depthTo * depthFactor]).range([0, contentHeight]);
3298
+ const yAxis = d310.axisLeft(yScaleAxis).tickFormat(
3299
+ (d) => `${formatNumber4(Number(d), {
2140
3300
  maximumFractionDigits: this.units.length === "ft" ? 1 : 2,
2141
3301
  locale: this.locale
2142
3302
  })}${getLengthUnit(this.units.length)}`
@@ -2177,10 +3337,16 @@ var WellRenderer = class {
2177
3337
  wellCaseGroup,
2178
3338
  wellScreenGroup,
2179
3339
  reductionGroup,
3340
+ centralizerGroup,
3341
+ pumpGroup,
2180
3342
  conflictGroup,
2181
3343
  highlightsGeologicGroup,
2182
3344
  highlightsConstructionGroup,
2183
- highlightsFracturesGroup
3345
+ highlightsFracturesGroup,
3346
+ overviewGroup,
3347
+ columnHeadersGroup,
3348
+ markersGroup,
3349
+ overlapGroup
2184
3350
  };
2185
3351
  const ctx = {
2186
3352
  svg,
@@ -2194,6 +3360,8 @@ var WellRenderer = class {
2194
3360
  geoXRight,
2195
3361
  depthFrom,
2196
3362
  depthTo,
3363
+ totalDepth,
3364
+ repeatedTo: state.repeatedTo,
2197
3365
  yScale: yScaleLocal,
2198
3366
  transition,
2199
3367
  tooltips,
@@ -2207,6 +3375,7 @@ var WellRenderer = class {
2207
3375
  groups
2208
3376
  };
2209
3377
  const inDepth = filterByDepth(depthFrom, depthTo);
3378
+ const currentPump = getCurrentPump2(profile);
2210
3379
  const filteredConstruction = {
2211
3380
  ...constructionData,
2212
3381
  bore_hole: constructionData.bore_hole.filter(inDepth),
@@ -2214,7 +3383,8 @@ var WellRenderer = class {
2214
3383
  surface_case: constructionData.surface_case.filter(inDepth),
2215
3384
  well_case: constructionData.well_case.filter(inDepth),
2216
3385
  well_screen: constructionData.well_screen.filter(inDepth),
2217
- reduction: constructionData.reduction?.filter(inDepth) ?? []
3386
+ reduction: constructionData.reduction?.filter(inDepth) ?? [],
3387
+ centralizers: constructionData.centralizers?.filter(inDepth) ?? []
2218
3388
  };
2219
3389
  const zooming = (e) => {
2220
3390
  const transform = e.transform;
@@ -2230,33 +3400,14 @@ var WellRenderer = class {
2230
3400
  if (!d) return null;
2231
3401
  return transform.k * spanH(d);
2232
3402
  });
2233
- surfaceCaseGroup.selectAll(`g.${this.classes.surfaceCase.rect}`).each((d, i, nodes) => {
2234
- if (!d) return;
2235
- const sc = d;
2236
- const g = d37.select(nodes[i]);
2237
- const y = transform.applyY(spanY(sc));
2238
- const h = transform.k * spanH(sc);
2239
- g.select(".surface-case-fill").attr("y", y).attr("height", h);
2240
- g.selectAll(".surface-case-side").attr("y1", y).attr("y2", y + h);
2241
- });
2242
- const maxXValsReduction = getProfileDiamValues4(constructionData);
2243
- const xScaleReduction = d37.scaleLinear().domain([0, d37.max(maxXValsReduction) || 0]).range([0, POCO_WIDTH]);
2244
- reductionGroup.selectAll(`polygon.${this.classes.reduction.item}`).attr("points", (d) => {
2245
- const r = d;
2246
- const yTop = transform.applyY(spanY(r));
2247
- const yBot = yTop + transform.k * spanH(r);
2248
- const xTopLeft = (POCO_CENTER - xScaleReduction(r.diam_from)) / 2;
2249
- const xTopRight = (POCO_CENTER + xScaleReduction(r.diam_from)) / 2;
2250
- const xBotLeft = (POCO_CENTER - xScaleReduction(r.diam_to)) / 2;
2251
- const xBotRight = (POCO_CENTER + xScaleReduction(r.diam_to)) / 2;
2252
- return `${xTopLeft},${yTop} ${xTopRight},${yTop} ${xBotRight},${yBot} ${xBotLeft},${yBot}`;
2253
- });
3403
+ layoutConstruction(zoomedCtx);
3404
+ drawCementPad(zoomedCtx, constructionData.cement_pad);
2254
3405
  fracturesGroup.selectAll(`g.${this.classes.fractures.item}`).attr("transform", (d) => {
2255
3406
  if (!d) return null;
2256
3407
  const f = d;
2257
3408
  const cx = pocoCenterX;
2258
3409
  const cy = transform.applyY(yScaleLocal(f.depth));
2259
- return `translate(0,${cy}) rotate(${f.dip},${cx},0)`;
3410
+ return `translate(0,${cy}) rotate(${fractureDip(f.dip)},${cx},0)`;
2260
3411
  });
2261
3412
  drawCaves(zoomedCtx, geologicData.caves?.filter(inDepth) ?? []);
2262
3413
  drawUnitLabels(zoomedCtx, profile.lithology?.filter(inDepth) ?? []);
@@ -2267,10 +3418,17 @@ var WellRenderer = class {
2267
3418
  ) ?? [],
2268
3419
  caves: profile.caves?.filter(inDepth) ?? []
2269
3420
  });
3421
+ drawPump(zoomedCtx, currentPump);
3422
+ drawCentralizers(zoomedCtx, filteredConstruction);
2270
3423
  drawConstructionLabels(zoomedCtx, {
2271
3424
  well_case: constructionData.well_case.filter(inDepth),
2272
3425
  well_screen: constructionData.well_screen.filter(inDepth)
2273
3426
  });
3427
+ drawMarkers(zoomedCtx, {
3428
+ lithology: profile.lithology ?? [],
3429
+ fractures: profile.fractures ?? []
3430
+ });
3431
+ drawOverlap(zoomedCtx);
2274
3432
  drawHighlights(zoomedCtx, highlights);
2275
3433
  this.onZoom?.(transform.k);
2276
3434
  };
@@ -2293,10 +3451,19 @@ var WellRenderer = class {
2293
3451
  ) ?? []
2294
3452
  );
2295
3453
  drawConstructive(ctx, filteredConstruction);
3454
+ drawPump(ctx, currentPump);
3455
+ drawCentralizers(ctx, filteredConstruction);
2296
3456
  drawConstructionLabels(ctx, {
2297
3457
  well_case: filteredConstruction.well_case,
2298
3458
  well_screen: filteredConstruction.well_screen
2299
3459
  });
3460
+ drawMarkers(ctx, {
3461
+ lithology: profile.lithology ?? [],
3462
+ fractures: profile.fractures ?? []
3463
+ });
3464
+ drawOverview(ctx, profile.lithology ?? []);
3465
+ drawColumnHeaders(ctx);
3466
+ drawOverlap(ctx);
2300
3467
  svg.select(`#${state.clipRectId}`).attr("y", 0).attr("height", svgHeight);
2301
3468
  drawHighlights(ctx, highlights);
2302
3469
  };
@@ -2312,7 +3479,7 @@ var WellRenderer = class {
2312
3479
  }
2313
3480
  const { zoom: zoomEnabled, pan: panEnabled } = this.renderConfig;
2314
3481
  if (zoomEnabled || panEnabled) {
2315
- const zoomNode = d37.zoom();
3482
+ const zoomNode = d310.zoom();
2316
3483
  if (zoomEnabled || panEnabled) zoomNode.on("zoom", zooming);
2317
3484
  zoomNode.scaleExtent(
2318
3485
  zoomEnabled ? [
@@ -2328,7 +3495,7 @@ var WellRenderer = class {
2328
3495
  if (initialZoom !== 1) {
2329
3496
  asSvgElement(svg).call(
2330
3497
  zoomNode.transform,
2331
- d37.zoomIdentity.scale(initialZoom)
3498
+ d310.zoomIdentity.scale(initialZoom)
2332
3499
  );
2333
3500
  }
2334
3501
  }
@@ -2342,10 +3509,13 @@ export {
2342
3509
  WellRenderer,
2343
3510
  applyRenderLocale,
2344
3511
  drawWellLegend,
3512
+ drawWellLegendSymbol,
2345
3513
  formatDiameter,
2346
3514
  formatLength,
2347
3515
  getDiameterUnit,
2348
3516
  getLengthUnit,
3517
+ getProfileTopDepth,
3518
+ getWellLegendItems,
2349
3519
  importFgdcTextures,
2350
3520
  resolveRenderLabel
2351
3521
  };