@oicl/openbridge-webcomponents-full-bundle 2.0.0-next.56 → 2.0.0-next.58

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.
Files changed (65) hide show
  1. package/bundle/openbridge-webcomponents.bundle.js +987 -362
  2. package/bundle/openbridge-webcomponents.bundle.js.map +1 -1
  3. package/custom-elements.json +576 -10
  4. package/dist/building-blocks/instrument-radial/instrument-radial.d.ts +10 -0
  5. package/dist/building-blocks/instrument-radial/instrument-radial.d.ts.map +1 -1
  6. package/dist/building-blocks/instrument-radial/instrument-radial.js +86 -21
  7. package/dist/building-blocks/instrument-radial/instrument-radial.js.map +1 -1
  8. package/dist/navigation-instruments/compass-sector/compass-sector.css.js +12 -0
  9. package/dist/navigation-instruments/compass-sector/compass-sector.css.js.map +1 -1
  10. package/dist/navigation-instruments/compass-sector/compass-sector.d.ts +23 -0
  11. package/dist/navigation-instruments/compass-sector/compass-sector.d.ts.map +1 -1
  12. package/dist/navigation-instruments/compass-sector/compass-sector.js +47 -0
  13. package/dist/navigation-instruments/compass-sector/compass-sector.js.map +1 -1
  14. package/dist/navigation-instruments/gauge-radial/gauge-radial.css.js +99 -0
  15. package/dist/navigation-instruments/gauge-radial/gauge-radial.css.js.map +1 -0
  16. package/dist/navigation-instruments/gauge-radial/gauge-radial.d.ts +42 -7
  17. package/dist/navigation-instruments/gauge-radial/gauge-radial.d.ts.map +1 -1
  18. package/dist/navigation-instruments/gauge-radial/gauge-radial.js +178 -31
  19. package/dist/navigation-instruments/gauge-radial/gauge-radial.js.map +1 -1
  20. package/dist/navigation-instruments/pitch/pitch.d.ts +37 -0
  21. package/dist/navigation-instruments/pitch/pitch.d.ts.map +1 -1
  22. package/dist/navigation-instruments/pitch/pitch.js +130 -62
  23. package/dist/navigation-instruments/pitch/pitch.js.map +1 -1
  24. package/dist/navigation-instruments/pitch-roll/pitch-roll.d.ts +7 -0
  25. package/dist/navigation-instruments/pitch-roll/pitch-roll.d.ts.map +1 -1
  26. package/dist/navigation-instruments/pitch-roll/pitch-roll.js +58 -2
  27. package/dist/navigation-instruments/pitch-roll/pitch-roll.js.map +1 -1
  28. package/dist/navigation-instruments/readout/readout.css.js +4 -0
  29. package/dist/navigation-instruments/readout/readout.css.js.map +1 -1
  30. package/dist/navigation-instruments/roll/roll.d.ts +37 -0
  31. package/dist/navigation-instruments/roll/roll.d.ts.map +1 -1
  32. package/dist/navigation-instruments/roll/roll.js +119 -63
  33. package/dist/navigation-instruments/roll/roll.js.map +1 -1
  34. package/dist/navigation-instruments/rot-sector/rot-sector.d.ts +15 -0
  35. package/dist/navigation-instruments/rot-sector/rot-sector.d.ts.map +1 -1
  36. package/dist/navigation-instruments/rot-sector/rot-sector.js +53 -1
  37. package/dist/navigation-instruments/rot-sector/rot-sector.js.map +1 -1
  38. package/dist/navigation-instruments/watch/tickmark.d.ts +2 -1
  39. package/dist/navigation-instruments/watch/tickmark.d.ts.map +1 -1
  40. package/dist/navigation-instruments/watch/tickmark.js +24 -4
  41. package/dist/navigation-instruments/watch/tickmark.js.map +1 -1
  42. package/dist/navigation-instruments/watch/watch.d.ts +23 -1
  43. package/dist/navigation-instruments/watch/watch.d.ts.map +1 -1
  44. package/dist/navigation-instruments/watch/watch.js +48 -20
  45. package/dist/navigation-instruments/watch/watch.js.map +1 -1
  46. package/package.json +1 -1
  47. package/src/building-blocks/instrument-radial/instrument-radial.stories.ts +21 -0
  48. package/src/building-blocks/instrument-radial/instrument-radial.ts +118 -21
  49. package/src/navigation-instruments/compass-sector/compass-sector.css +12 -0
  50. package/src/navigation-instruments/compass-sector/compass-sector.stories.ts +7 -0
  51. package/src/navigation-instruments/compass-sector/compass-sector.ts +52 -0
  52. package/src/navigation-instruments/gauge-radial/gauge-radial.css +85 -0
  53. package/src/navigation-instruments/gauge-radial/gauge-radial.stories.ts +149 -7
  54. package/src/navigation-instruments/gauge-radial/gauge-radial.ts +212 -33
  55. package/src/navigation-instruments/pitch/pitch.stories.ts +23 -1
  56. package/src/navigation-instruments/pitch/pitch.ts +184 -86
  57. package/src/navigation-instruments/pitch-roll/pitch-roll.stories.ts +13 -0
  58. package/src/navigation-instruments/pitch-roll/pitch-roll.ts +80 -13
  59. package/src/navigation-instruments/readout/readout.css +4 -0
  60. package/src/navigation-instruments/roll/roll.stories.ts +23 -1
  61. package/src/navigation-instruments/roll/roll.ts +171 -87
  62. package/src/navigation-instruments/rot-sector/rot-sector.stories.ts +15 -0
  63. package/src/navigation-instruments/rot-sector/rot-sector.ts +59 -1
  64. package/src/navigation-instruments/watch/tickmark.ts +33 -6
  65. package/src/navigation-instruments/watch/watch.ts +60 -25
@@ -2,6 +2,8 @@ import {LitElement, PropertyValues, html, svg, unsafeCSS, nothing} from 'lit';
2
2
  import {property} from 'lit/decorators.js';
3
3
  import componentStyle from './compass-sector.css?inline';
4
4
  import '../watch/watch.js';
5
+ import '../readout/readout.js';
6
+ import {ReadoutDirection, ReadoutVariant} from '../readout/readout.js';
5
7
  import {Tickmark, TickmarkType, TickmarkStyle} from '../watch/tickmark.js';
6
8
  import {arrow, ArrowStyle} from '../compass/arrow.js';
7
9
  import {AdviceState, AngleAdvice, AngleAdviceRaw} from '../watch/advice.js';
@@ -155,6 +157,12 @@ export class ObcCompassSector extends LitElement {
155
157
  ];
156
158
  @property({type: Boolean}) tickmarksInside: boolean = false;
157
159
  @property({type: Boolean}) zoomToFitArc: boolean = false;
160
+ /**
161
+ * When `true`, shows a centered `<obc-readout>` under the arc displaying the
162
+ * heading (label `HDG`, unit `DEG`). The value color follows the HDG entry in
163
+ * `priorityElements`, matching the HDG arrow.
164
+ */
165
+ @property({type: Boolean}) hasReadout: boolean = false;
158
166
 
159
167
  private _headingSp = new SetpointBundle({
160
168
  angularWraparound: true,
@@ -396,6 +404,33 @@ export class ObcCompassSector extends LitElement {
396
404
  return selected.includes(element) ? this.priority : Priority.regular;
397
405
  }
398
406
 
407
+ /**
408
+ * Vertical placement of the readout as a percentage of the host height.
409
+ *
410
+ * With `zoomToFitArc` the arc is reframed by `computeZoomToFitArcFrame`, whose
411
+ * `radiusOffset`/`viewBox` depend on the arc's *absolute* orientation (which
412
+ * cardinal axes its bounding box crosses), not just its bend. The arc itself
413
+ * is always rotated back to the top, so it stays put on screen — but a
414
+ * frame-derived offset would swing wildly as `heading` rotates the bbox around
415
+ * the circle. So in zoom we use a fixed offset, which keeps the readout steady
416
+ * under the (stationary) arc regardless of heading.
417
+ *
418
+ * Without zoom the viewBox is the fixed, origin-symmetric 120° framing, so the
419
+ * geometry is orientation-independent: place the readout halfway down the inner
420
+ * radius from the watch center toward the arc's inner edge.
421
+ */
422
+ private get _readoutTopPercent(): number {
423
+ if (this.zoomToFitArc) {
424
+ return 70;
425
+ }
426
+ const [, vy, , vh] = this._cachedViewBox.split(' ').map(Number);
427
+ if (!vh || Number.isNaN(vy)) {
428
+ return 50;
429
+ }
430
+ const anchorY = -INNER_RADIUS * 0.5;
431
+ return Math.round(((anchorY - vy) / vh) * 1000) / 10;
432
+ }
433
+
399
434
  // ---------------------------------------------------------------------------
400
435
  // Render
401
436
  // ---------------------------------------------------------------------------
@@ -477,6 +512,23 @@ export class ObcCompassSector extends LitElement {
477
512
  rOff
478
513
  )}
479
514
  </svg>
515
+ ${this.hasReadout
516
+ ? html`<div class="readout" style="top: ${this._readoutTopPercent}%">
517
+ <obc-readout
518
+ .variant=${ReadoutVariant.enhanced}
519
+ .direction=${ReadoutDirection.vertical}
520
+ .hasSetpoint=${false}
521
+ .hasAdvice=${false}
522
+ .value=${this.heading}
523
+ .fractionDigits=${0}
524
+ .valuePriority=${this.priorityFor(
525
+ CompassSectorPriorityElement.hdg
526
+ )}
527
+ label="HDG"
528
+ unit="DEG"
529
+ ></obc-readout>
530
+ </div>`
531
+ : nothing}
480
532
  </div>
481
533
  `;
482
534
  }
@@ -0,0 +1,85 @@
1
+ :host {
2
+ display: block;
3
+ width: 100%;
4
+ height: 100%;
5
+ }
6
+
7
+ /* Host takes each sector's crop aspect (see `sectorClips`): 270/90 square, 180
8
+ wide — so the dial is the same size at any width. */
9
+ :host([sector="270"]),
10
+ :host([sector="90-left"]),
11
+ :host([sector="90-right"]) {
12
+ height: auto;
13
+ aspect-ratio: 1;
14
+ }
15
+
16
+ /* 448 / 251 = the 180° crop's aspect (448 wide × 448·(1 − 44%) tall); keep in
17
+ sync with `sectorClips` bottom=44 in gauge-radial.ts. */
18
+ :host([sector="180"]) {
19
+ height: auto;
20
+ aspect-ratio: 448 / 251;
21
+ }
22
+
23
+ .gauge-radial-root {
24
+ /* Readout vertical placement per layout (% of the cropped host), tuned to the
25
+ watch geometry — re-check if the ring radii or the 448 box change. Which
26
+ layout uses which is in renderReadouts(). */
27
+ --readout-meta-top: 72%;
28
+ --readout-meta-top-needle: 63%;
29
+ --readout-meta-top-180: 60%;
30
+
31
+ position: relative;
32
+ width: 100%;
33
+ height: 100%;
34
+
35
+ .gauge-readout-value {
36
+ position: absolute;
37
+ left: 50%;
38
+ top: 50%;
39
+ transform: translate(-50%, -50%);
40
+ z-index: 1;
41
+ }
42
+
43
+ .gauge-readout-meta {
44
+ position: absolute;
45
+ left: 50%;
46
+ top: var(--readout-meta-top);
47
+ transform: translateX(-50%);
48
+ z-index: 1;
49
+ }
50
+
51
+ &.type-needle .gauge-readout-meta {
52
+ top: var(--readout-meta-top-needle);
53
+ }
54
+
55
+ &.sector-180 .gauge-readout-meta {
56
+ top: var(--readout-meta-top-180);
57
+ }
58
+
59
+ /* Nudge the corner readout inward (up + toward center) so it sits inside the
60
+ open quadrant rather than hugging the box corner. Symmetric for both
61
+ orientations — only the horizontal edge differs. */
62
+ &.sector-90-left .gauge-readout-meta,
63
+ &.sector-90-right .gauge-readout-meta {
64
+ --readout-90-inset: 6%;
65
+ bottom: var(--readout-90-inset);
66
+ top: auto;
67
+ transform: none;
68
+ }
69
+
70
+ &.sector-90-left .gauge-readout-meta {
71
+ left: auto;
72
+ right: var(--readout-90-inset);
73
+ }
74
+
75
+ &.sector-90-right .gauge-readout-meta {
76
+ left: var(--readout-90-inset);
77
+ right: auto;
78
+ }
79
+
80
+ /* 90-right readout sits in the bottom-left corner, so left-align the value
81
+ under the label (enhanced readouts right-align the value by default). */
82
+ &.sector-90-right .gauge-readout-meta::part(value-wrapper) {
83
+ justify-self: start;
84
+ }
85
+ }
@@ -1,33 +1,57 @@
1
1
  import type {Meta, StoryObj} from '@storybook/web-components-vite';
2
- import {ObcGaugeRadial, ObcGaugeRadialType} from './gauge-radial.js';
2
+ import {
3
+ GaugeRadialSector,
4
+ ObcGaugeRadial,
5
+ ObcGaugeRadialType,
6
+ } from './gauge-radial.js';
3
7
  import './gauge-radial.js';
4
8
  import {widthDecorator} from '../../storybook-util.js';
5
9
  import {AdviceType} from '../watch/advice.js';
6
10
  import {TickmarkStyle} from '../watch/tickmark.js';
7
11
  import {InstrumentState, Priority} from '../types.js';
8
12
 
9
- const meta: Meta<typeof ObcGaugeRadial> = {
13
+ type GaugeRadialStoryArgs = Partial<ObcGaugeRadial> & {
14
+ width?: number;
15
+ height?: number;
16
+ };
17
+
18
+ const meta = {
10
19
  title: 'Instruments/Gauge Radial',
11
- tags: ['6.0'],
20
+ tags: ['autodocs', '6.0'],
12
21
  component: 'obc-gauge-radial',
13
22
  decorators: [widthDecorator],
14
23
  args: {
15
24
  width: 400,
16
25
  },
17
26
  argTypes: {
18
- state: {control: 'select', options: Object.values(InstrumentState)},
27
+ width: {control: {type: 'range', min: 100, max: 1000, step: 1}},
28
+ height: {control: {type: 'range', min: 100, max: 1000, step: 1}},
29
+ type: {control: 'select', options: Object.values(ObcGaugeRadialType)},
30
+ sector: {control: 'select', options: Object.values(GaugeRadialSector)},
19
31
  priority: {control: 'select', options: Object.values(Priority)},
20
- tickmarksInside: {control: 'boolean'},
32
+ state: {control: 'select', options: Object.values(InstrumentState)},
21
33
  tickmarkStyle: {
22
34
  control: 'select',
23
35
  options: Object.values(TickmarkStyle),
24
36
  },
25
37
  showLabels: {control: 'boolean'},
38
+ tickmarksInside: {control: 'boolean'},
39
+ showReadout: {control: 'boolean'},
40
+ value: {control: 'number'},
41
+ minValue: {control: 'number'},
42
+ maxValue: {control: 'number'},
43
+ primaryTickmarkInterval: {control: 'number'},
44
+ secondaryTickmarkInterval: {control: 'number'},
45
+ tertiaryTickmarkInterval: {control: 'number'},
46
+ fractionDigits: {control: 'number'},
47
+ label: {control: 'text'},
48
+ unit: {control: 'text'},
49
+ advices: {control: 'object'},
26
50
  },
27
- } satisfies Meta<ObcGaugeRadial>;
51
+ } satisfies Meta<GaugeRadialStoryArgs>;
28
52
 
29
53
  export default meta;
30
- type Story = StoryObj<ObcGaugeRadial>;
54
+ type Story = StoryObj<GaugeRadialStoryArgs>;
31
55
 
32
56
  export const Positive: Story = {
33
57
  args: {
@@ -128,6 +152,124 @@ export const IrregularRange: Story = {
128
152
  },
129
153
  };
130
154
 
155
+ const sectorStoryArgs = {
156
+ value: 50,
157
+ minValue: 0,
158
+ maxValue: 100,
159
+ type: ObcGaugeRadialType.filled,
160
+ showLabels: true,
161
+ } as const;
162
+
163
+ export const Sector270: Story = {
164
+ args: {
165
+ ...sectorStoryArgs,
166
+ sector: GaugeRadialSector.deg270,
167
+ },
168
+ };
169
+
170
+ export const Sector180: Story = {
171
+ args: {
172
+ ...sectorStoryArgs,
173
+ sector: GaugeRadialSector.deg180,
174
+ },
175
+ };
176
+
177
+ export const Sector180WithReadout: Story = {
178
+ args: {
179
+ ...sectorStoryArgs,
180
+ sector: GaugeRadialSector.deg180,
181
+ showReadout: true,
182
+ label: 'Label',
183
+ unit: 'unit',
184
+ value: 123,
185
+ },
186
+ };
187
+
188
+ // Regression for the width-derived "Max-min" end-label inset: wide labels on
189
+ // both ends (-250 / 1000). The old per-side literals (11/6) broke for these;
190
+ // min/max are interval multiples so the end labels sit on the primary grid.
191
+ export const Sector180WideEndLabels: Story = {
192
+ args: {
193
+ ...sectorStoryArgs,
194
+ sector: GaugeRadialSector.deg180,
195
+ tickmarksInside: true,
196
+ minValue: -250,
197
+ maxValue: 1000,
198
+ value: 500,
199
+ primaryTickmarkInterval: 250,
200
+ secondaryTickmarkInterval: 50,
201
+ },
202
+ };
203
+
204
+ export const Sector90Left: Story = {
205
+ args: {
206
+ ...sectorStoryArgs,
207
+ sector: GaugeRadialSector.deg90Left,
208
+ width: 200,
209
+ },
210
+ };
211
+
212
+ export const Sector90Right: Story = {
213
+ args: {
214
+ ...sectorStoryArgs,
215
+ sector: GaugeRadialSector.deg90Right,
216
+ width: 200,
217
+ },
218
+ };
219
+
220
+ export const Sector90LeftWithReadout: Story = {
221
+ args: {
222
+ ...sectorStoryArgs,
223
+ sector: GaugeRadialSector.deg90Left,
224
+ showReadout: true,
225
+ label: 'Label',
226
+ unit: 'unit',
227
+ value: 123,
228
+ width: 200,
229
+ },
230
+ };
231
+
232
+ export const Sector90RightWithReadout: Story = {
233
+ args: {
234
+ ...sectorStoryArgs,
235
+ sector: GaugeRadialSector.deg90Right,
236
+ showReadout: true,
237
+ label: 'Label',
238
+ unit: 'unit',
239
+ value: 123,
240
+ width: 200,
241
+ },
242
+ };
243
+
244
+ const readoutStoryArgs = {
245
+ value: 123,
246
+ minValue: 0,
247
+ maxValue: 100,
248
+ type: ObcGaugeRadialType.filled,
249
+ sector: GaugeRadialSector.deg270,
250
+ showReadout: true,
251
+ label: 'Label',
252
+ unit: 'unit',
253
+ } as const;
254
+
255
+ export const WithReadout: Story = {
256
+ args: readoutStoryArgs,
257
+ };
258
+
259
+ export const WithReadoutEnhanced: Story = {
260
+ args: {
261
+ ...readoutStoryArgs,
262
+ priority: Priority.enhanced,
263
+ },
264
+ };
265
+
266
+ export const WithReadoutNeedle: Story = {
267
+ args: {
268
+ ...readoutStoryArgs,
269
+ type: ObcGaugeRadialType.needle,
270
+ },
271
+ };
272
+
131
273
  export const WideAdviceSpan: Story = {
132
274
  args: {
133
275
  value: 320,
@@ -1,4 +1,6 @@
1
- import {LitElement, html} from 'lit';
1
+ import {LitElement, html, nothing, unsafeCSS, type TemplateResult} from 'lit';
2
+ import {classMap} from 'lit/directives/class-map.js';
3
+ import componentStyle from './gauge-radial.css?inline';
2
4
  import {customElement} from '../../decorator.js';
3
5
  import {property} from 'lit/decorators.js';
4
6
  import {AdviceType} from '../watch/advice.js';
@@ -6,6 +8,11 @@ import {InstrumentState, Priority} from '../types.js';
6
8
  import {SetpointMixin} from '../../svghelpers/setpoint-mixin.js';
7
9
  import '../../building-blocks/instrument-radial/instrument-radial.js';
8
10
  import {TickmarkStyle} from '../watch/tickmark.js';
11
+ import '../readout/readout.js';
12
+ import {
13
+ ReadoutStackVerticalAlignment,
14
+ ReadoutVariant,
15
+ } from '../readout/readout.js';
9
16
 
10
17
  export enum ObcGaugeRadialType {
11
18
  filled = 'filled',
@@ -13,6 +20,13 @@ export enum ObcGaugeRadialType {
13
20
  needle = 'needle',
14
21
  }
15
22
 
23
+ export enum GaugeRadialSector {
24
+ deg270 = '270',
25
+ deg180 = '180',
26
+ deg90Left = '90-left',
27
+ deg90Right = '90-right',
28
+ }
29
+
16
30
  export interface GaugeRadialAdvice {
17
31
  minValue: number;
18
32
  maxValue: number;
@@ -34,14 +48,17 @@ export interface GaugeRadialAdvice {
34
48
  *
35
49
  * - **Three display types**: `filled` (solid arc), `bar` (thinner arc), and
36
50
  * `needle` (pointer indicator) via the `type` property.
37
- * - **Full-range mapping**: The configured `minValue..maxValue` always spans
38
- * the full 270° sweep. Symmetric ranges still place `0` at 12 o'clock.
51
+ * - **Sector sweep**: `sector` selects the arc span (`270`, `180`, `90-left`, or `90-right`).
52
+ * The configured `minValue..maxValue` always spans the full sector. For the
53
+ * centered sectors (`270`, `180`) a symmetric range places `0` at 12 o'clock;
54
+ * for `90-left`/`90-right` the range midpoint sits at the middle of the quadrant.
39
55
  * - **Setpoint via mixin**: `setpoint`, `newSetpoint`, `touching`,
40
56
  * `autoAtSetpointDeadband`, `setpointOverride`, and all other setpoint
41
57
  * properties are provided by `SetpointMixin` and forwarded to the inner
42
58
  * `<obc-instrument-radial>`.
43
59
  * - **Advice zones**: Pass an array of {@link GaugeRadialAdvice} objects to
44
- * render caution/alert arcs on the gauge.
60
+ * render caution/alert arcs on the gauge. Not shown on the `90-left` /
61
+ * `90-right` sectors.
45
62
  *
46
63
  * ## Usage Guidelines
47
64
  *
@@ -50,6 +67,12 @@ export interface GaugeRadialAdvice {
50
67
  * - Provide `primaryTickmarkInterval` and `secondaryTickmarkInterval` to
51
68
  * control tickmark density.
52
69
  * - Enable `showLabels` to show numeric labels at primary tickmarks.
70
+ * - Enable `showReadout` with optional `label` and `unit`. Layout depends on `sector`
71
+ * and `type`: **270** filled/bar — value at center + a label-only `meta` row in
72
+ * the bottom gap (two separate readouts); **180** filled/bar — a single centered
73
+ * stack (value + label/unit) at the bottom; **270** needle — bottom stack;
74
+ * **180** needle — no readout; **90-left** / **90-right** filled/bar — corner
75
+ * readout in a square host; **90** needle — no readout.
53
76
  *
54
77
  * ## Best Practices
55
78
  *
@@ -66,7 +89,7 @@ export interface GaugeRadialAdvice {
66
89
  * minValue="0"
67
90
  * maxValue="100"
68
91
  * type="filled"
69
- * enhanced
92
+ * priority="enhanced"
70
93
  * showLabels
71
94
  * primaryTickmarkInterval="25"
72
95
  * secondaryTickmarkInterval="5"
@@ -98,47 +121,203 @@ export class ObcGaugeRadial extends SetpointMixin(LitElement) {
98
121
  @property({type: Boolean}) tickmarksInside: boolean = false;
99
122
  @property({type: String}) tickmarkStyle: TickmarkStyle =
100
123
  TickmarkStyle.regular;
124
+ /** Caution/alert arcs. Ignored on `sector: 90-left` / `90-right`. */
101
125
  @property({type: Array, attribute: false}) advices: GaugeRadialAdvice[] = [];
126
+ @property({type: String, reflect: true}) sector: GaugeRadialSector =
127
+ GaugeRadialSector.deg270;
128
+ @property({type: Boolean}) showReadout = false;
129
+ @property({type: String}) label = '';
130
+ @property({type: String}) unit = '';
131
+ @property({type: Number}) fractionDigits = 0;
132
+
133
+ private get sectorAngles(): {sweep: number; start: number} {
134
+ switch (this.sector) {
135
+ case GaugeRadialSector.deg180:
136
+ return {sweep: 180, start: -90};
137
+ case GaugeRadialSector.deg90Left:
138
+ return {sweep: 90, start: -90};
139
+ case GaugeRadialSector.deg90Right:
140
+ return {sweep: 90, start: 0};
141
+ case GaugeRadialSector.deg270:
142
+ default:
143
+ return {sweep: 270, start: -135};
144
+ }
145
+ }
102
146
 
103
- getAngle(v: number): number {
147
+ /**
148
+ * Per-edge crop (%) of the shared, origin-centered 448 SVG box each sector
149
+ * shows. All sectors render at the same natural scale, so the dial stays the
150
+ * same size; the sector only windows a different part (270 whole, 180 wide,
151
+ * 90 a quadrant).
152
+ */
153
+ private get sectorClips(): {
154
+ top: number;
155
+ bottom: number;
156
+ left: number;
157
+ right: number;
158
+ } {
159
+ switch (this.sector) {
160
+ case GaugeRadialSector.deg180:
161
+ return {top: 0, bottom: 44, left: 0, right: 0};
162
+ case GaugeRadialSector.deg90Left:
163
+ return {top: 0, bottom: 45, left: 0, right: 45};
164
+ case GaugeRadialSector.deg90Right:
165
+ return {top: 0, bottom: 45, left: 45, right: 0};
166
+ case GaugeRadialSector.deg270:
167
+ default:
168
+ return {top: 0, bottom: 0, left: 0, right: 0};
169
+ }
170
+ }
171
+
172
+ private get isSector90(): boolean {
173
+ return (
174
+ this.sector === GaugeRadialSector.deg90Left ||
175
+ this.sector === GaugeRadialSector.deg90Right
176
+ );
177
+ }
178
+
179
+ // Arrow form so `this` binds when passed as `.getAngle=${this.getAngle}`
180
+ // to <obc-instrument-radial>. Do not convert to a method.
181
+ getAngle = (v: number): number => {
182
+ const {sweep, start} = this.sectorAngles;
104
183
  const span = this.maxValue - this.minValue;
105
184
  if (!Number.isFinite(span) || span <= 0) {
106
- return -135;
185
+ return start;
107
186
  }
108
187
 
109
- return ((v - this.minValue) / span) * 270 - 135;
188
+ return ((v - this.minValue) / span) * sweep + start;
189
+ };
190
+
191
+ /** Renders one gauge readout; `withMeta`/`labelOnly` pick parts. */
192
+ private renderReadout({
193
+ className,
194
+ variant,
195
+ alignment = ReadoutStackVerticalAlignment.vertical,
196
+ withMeta = true,
197
+ labelOnly = false,
198
+ }: {
199
+ className: string;
200
+ variant: ReadoutVariant;
201
+ alignment?: ReadoutStackVerticalAlignment;
202
+ withMeta?: boolean;
203
+ labelOnly?: boolean;
204
+ }): TemplateResult {
205
+ // `labelOnly` already means "no value", so derive it instead of passing both.
206
+ const withValue = !labelOnly;
207
+ return html`
208
+ <obc-readout
209
+ class=${className}
210
+ direction="vertical"
211
+ ?labelOnly=${labelOnly}
212
+ .variant=${variant}
213
+ .alignment=${alignment}
214
+ .valuePriority=${withValue ? this.priority : undefined}
215
+ .value=${withValue ? this.value : undefined}
216
+ .fractionDigits=${this.fractionDigits}
217
+ .label=${withMeta ? this.label : ''}
218
+ .unit=${withMeta ? this.unit : ''}
219
+ ></obc-readout>
220
+ `;
221
+ }
222
+
223
+ private renderReadouts() {
224
+ if (!this.showReadout) {
225
+ return nothing;
226
+ }
227
+
228
+ const isNeedle = this.type === ObcGaugeRadialType.needle;
229
+ const is90 = this.isSector90;
230
+ const is180 = this.sector === GaugeRadialSector.deg180;
231
+
232
+ if (isNeedle && (is180 || is90)) {
233
+ return nothing;
234
+ }
235
+
236
+ // 90 filled/bar: enhanced corner readout (bold value + label/unit).
237
+ if (is90) {
238
+ return this.renderReadout({
239
+ className: 'gauge-readout-meta',
240
+ variant: ReadoutVariant.enhanced,
241
+ });
242
+ }
243
+
244
+ // 270 needle and 180 filled/bar: centered stack (value weight follows `priority`).
245
+ if (isNeedle || is180) {
246
+ return this.renderReadout({
247
+ className: 'gauge-readout-meta',
248
+ variant: ReadoutVariant.stack,
249
+ alignment: ReadoutStackVerticalAlignment.center,
250
+ });
251
+ }
252
+
253
+ // 270 filled/bar: the value sits at the circle center and the label/unit row
254
+ // lower in the bottom gap, so they are two separately-positioned readouts.
255
+ return html`
256
+ ${this.renderReadout({
257
+ className: 'gauge-readout-value',
258
+ variant: ReadoutVariant.enhanced,
259
+ withMeta: false,
260
+ })}
261
+ ${this.label || this.unit
262
+ ? this.renderReadout({
263
+ className: 'gauge-readout-meta',
264
+ variant: ReadoutVariant.stack,
265
+ alignment: ReadoutStackVerticalAlignment.center,
266
+ labelOnly: true,
267
+ })
268
+ : nothing}
269
+ `;
110
270
  }
111
271
 
112
272
  override render() {
273
+ const clips = this.sectorClips;
113
274
  return html`
114
- <obc-instrument-radial
115
- .value=${this.value}
116
- .state=${this.state}
117
- .priority=${this.priority}
118
- .setpoint=${this.setpoint}
119
- .newSetpoint=${this.newSetpoint}
120
- .setpointAtZeroDeadband=${this.setpointAtZeroDeadband}
121
- .setpointOverride=${this.setpointOverride}
122
- .touching=${this.touching}
123
- .autoAtSetpoint=${this.autoAtSetpoint}
124
- .autoAtSetpointDeadband=${this.autoAtSetpointDeadband}
125
- .animateSetpoint=${this.animateSetpoint}
126
- .maxValue=${this.maxValue}
127
- .minValue=${this.minValue}
128
- .getAngle=${this.getAngle}
129
- .showLabels=${this.showLabels}
130
- .primaryTickmarkInterval=${this.primaryTickmarkInterval}
131
- .secondaryTickmarkInterval=${this.secondaryTickmarkInterval}
132
- .tertiaryTickmarkInterval=${this.tertiaryTickmarkInterval}
133
- .type=${this.type}
134
- .needleType=${this.type}
135
- .tickmarksInside=${this.tickmarksInside}
136
- .tickmarkStyle=${this.tickmarkStyle}
137
- .advices=${this.advices}
275
+ <div
276
+ class=${classMap({
277
+ 'gauge-radial-root': true,
278
+ 'type-needle': this.type === ObcGaugeRadialType.needle,
279
+ 'sector-180': this.sector === GaugeRadialSector.deg180,
280
+ 'sector-90-left': this.sector === GaugeRadialSector.deg90Left,
281
+ 'sector-90-right': this.sector === GaugeRadialSector.deg90Right,
282
+ })}
138
283
  >
139
- </obc-instrument-radial>
284
+ <obc-instrument-radial
285
+ .value=${this.value}
286
+ .state=${this.state}
287
+ .priority=${this.priority}
288
+ .setpoint=${this.setpoint}
289
+ .newSetpoint=${this.newSetpoint}
290
+ .setpointAtZeroDeadband=${this.setpointAtZeroDeadband}
291
+ .setpointOverride=${this.setpointOverride}
292
+ .touching=${this.touching}
293
+ .autoAtSetpoint=${this.autoAtSetpoint}
294
+ .autoAtSetpointDeadband=${this.autoAtSetpointDeadband}
295
+ .animateSetpoint=${this.animateSetpoint}
296
+ .maxValue=${this.maxValue}
297
+ .minValue=${this.minValue}
298
+ .getAngle=${this.getAngle}
299
+ .showLabels=${this.showLabels}
300
+ .primaryTickmarkInterval=${this.primaryTickmarkInterval}
301
+ .secondaryTickmarkInterval=${this.secondaryTickmarkInterval}
302
+ .tertiaryTickmarkInterval=${this.tertiaryTickmarkInterval}
303
+ .type=${this.type}
304
+ .needleType=${this.type}
305
+ .tickmarksInside=${this.tickmarksInside}
306
+ .tickmarkStyle=${this.tickmarkStyle}
307
+ .advices=${this.isSector90 ? [] : this.advices}
308
+ .clipTop=${clips.top}
309
+ .clipBottom=${clips.bottom}
310
+ .clipLeft=${clips.left}
311
+ .clipRight=${clips.right}
312
+ .endLabelsMaxMin=${this.sector === GaugeRadialSector.deg180}
313
+ >
314
+ </obc-instrument-radial>
315
+ ${this.renderReadouts()}
316
+ </div>
140
317
  `;
141
318
  }
319
+
320
+ static override styles = unsafeCSS(componentStyle);
142
321
  }
143
322
 
144
323
  declare global {
@@ -1,6 +1,7 @@
1
1
  import type {Meta, StoryObj} from '@storybook/web-components-vite';
2
- import {ObcPitch} from './pitch.js';
2
+ import {ObcPitch, ObcPitchType} from './pitch.js';
3
3
  import './pitch.js';
4
+ import {Priority} from '../types.js';
4
5
  import {widthDecorator} from '../../storybook-util.js';
5
6
  import {sideVessels} from '../watch/vessels/storybook-helper.js';
6
7
 
@@ -23,6 +24,8 @@ const meta: Meta<typeof ObcPitch> = {
23
24
  control: 'select',
24
25
  options: sideVessels,
25
26
  },
27
+ type: {control: 'select', options: Object.values(ObcPitchType)},
28
+ priority: {control: 'select', options: Object.values(Priority)},
26
29
  },
27
30
  decorators: [widthDecorator],
28
31
  } satisfies Meta<ObcPitch>;
@@ -50,3 +53,22 @@ export const ZoomedInNarrow: Story = {
50
53
  maxPitchAdvice: 10,
51
54
  },
52
55
  };
56
+
57
+ export const WithReadout: Story = {
58
+ args: {
59
+ hasReadout: true,
60
+ },
61
+ };
62
+
63
+ export const DualScale: Story = {
64
+ args: {
65
+ type: ObcPitchType.dualScale,
66
+ hasReadout: true,
67
+ },
68
+ };
69
+
70
+ export const Enhanced: Story = {
71
+ args: {
72
+ priority: Priority.enhanced,
73
+ },
74
+ };