@oicl/openbridge-webcomponents-full-bundle 2.0.0-next.102 → 2.0.0-next.103
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/bundle/openbridge-webcomponents.bundle.js +579 -143
- package/bundle/openbridge-webcomponents.bundle.js.map +1 -1
- package/custom-elements.json +338 -8
- package/dist/building-blocks/instrument-radial/instrument-radial.d.ts +22 -2
- package/dist/building-blocks/instrument-radial/instrument-radial.d.ts.map +1 -1
- package/dist/building-blocks/instrument-radial/instrument-radial.js +61 -37
- package/dist/building-blocks/instrument-radial/instrument-radial.js.map +1 -1
- package/dist/navigation-instruments/azimuth-thruster/azimuth-thruster.d.ts +14 -0
- package/dist/navigation-instruments/azimuth-thruster/azimuth-thruster.d.ts.map +1 -1
- package/dist/navigation-instruments/azimuth-thruster/azimuth-thruster.js +30 -11
- package/dist/navigation-instruments/azimuth-thruster/azimuth-thruster.js.map +1 -1
- package/dist/navigation-instruments/compass/compass.d.ts +21 -1
- package/dist/navigation-instruments/compass/compass.d.ts.map +1 -1
- package/dist/navigation-instruments/compass/compass.js +36 -16
- package/dist/navigation-instruments/compass/compass.js.map +1 -1
- package/dist/navigation-instruments/compass-sector/compass-sector.d.ts.map +1 -1
- package/dist/navigation-instruments/compass-sector/compass-sector.js.map +1 -1
- package/dist/navigation-instruments/gauge-radial/gauge-radial.css.js +34 -2
- package/dist/navigation-instruments/gauge-radial/gauge-radial.css.js.map +1 -1
- package/dist/navigation-instruments/gauge-radial/gauge-radial.d.ts +15 -0
- package/dist/navigation-instruments/gauge-radial/gauge-radial.d.ts.map +1 -1
- package/dist/navigation-instruments/gauge-radial/gauge-radial.js +35 -1
- package/dist/navigation-instruments/gauge-radial/gauge-radial.js.map +1 -1
- package/dist/navigation-instruments/heading/heading.d.ts +19 -1
- package/dist/navigation-instruments/heading/heading.d.ts.map +1 -1
- package/dist/navigation-instruments/heading/heading.js +34 -16
- package/dist/navigation-instruments/heading/heading.js.map +1 -1
- package/dist/navigation-instruments/rudder/rudder.d.ts +15 -2
- package/dist/navigation-instruments/rudder/rudder.d.ts.map +1 -1
- package/dist/navigation-instruments/rudder/rudder.js +36 -26
- package/dist/navigation-instruments/rudder/rudder.js.map +1 -1
- package/dist/navigation-instruments/speed-gauge/speed-gauge.d.ts +15 -1
- package/dist/navigation-instruments/speed-gauge/speed-gauge.d.ts.map +1 -1
- package/dist/navigation-instruments/speed-gauge/speed-gauge.js +31 -3
- package/dist/navigation-instruments/speed-gauge/speed-gauge.js.map +1 -1
- package/dist/navigation-instruments/watch/watch.d.ts +29 -1
- package/dist/navigation-instruments/watch/watch.d.ts.map +1 -1
- package/dist/navigation-instruments/watch/watch.js +52 -38
- package/dist/navigation-instruments/watch/watch.js.map +1 -1
- package/dist/svghelpers/radial-frame.d.ts +122 -0
- package/dist/svghelpers/radial-frame.d.ts.map +1 -0
- package/dist/svghelpers/radial-frame.js +259 -0
- package/dist/svghelpers/radial-frame.js.map +1 -0
- package/package.json +1 -1
- package/src/building-blocks/instrument-radial/instrument-radial.stories.ts +187 -6
- package/src/building-blocks/instrument-radial/instrument-radial.ts +98 -47
- package/src/navigation-instruments/azimuth-thruster/azimuth-thruster.stories.ts +78 -1
- package/src/navigation-instruments/azimuth-thruster/azimuth-thruster.ts +55 -11
- package/src/navigation-instruments/compass/compass.stories.ts +88 -1
- package/src/navigation-instruments/compass/compass.ts +52 -16
- package/src/navigation-instruments/compass-sector/compass-sector.ts +7 -0
- package/src/navigation-instruments/gauge-radial/gauge-radial.css +32 -2
- package/src/navigation-instruments/gauge-radial/gauge-radial.stories.ts +265 -1
- package/src/navigation-instruments/gauge-radial/gauge-radial.ts +59 -1
- package/src/navigation-instruments/heading/heading.stories.ts +92 -1
- package/src/navigation-instruments/heading/heading.ts +50 -16
- package/src/navigation-instruments/rudder/rudder.stories.ts +83 -1
- package/src/navigation-instruments/rudder/rudder.ts +61 -34
- package/src/navigation-instruments/speed-gauge/speed-gauge.stories.ts +94 -1
- package/src/navigation-instruments/speed-gauge/speed-gauge.ts +57 -4
- package/src/navigation-instruments/watch/watch.stories.ts +140 -1
- package/src/navigation-instruments/watch/watch.ts +84 -44
- package/src/storybook-util.ts +59 -0
- package/src/svghelpers/radial-frame.spec.ts +307 -0
- package/src/svghelpers/radial-frame.ts +508 -0
|
@@ -4,6 +4,13 @@
|
|
|
4
4
|
height: 100%;
|
|
5
5
|
}
|
|
6
6
|
|
|
7
|
+
/* With a pinned face the host shrink-wraps the fixed-size dial, so rows of
|
|
8
|
+
pinned instruments can sit side by side in flex layouts. */
|
|
9
|
+
:host([face-diameter]) {
|
|
10
|
+
width: fit-content;
|
|
11
|
+
height: fit-content;
|
|
12
|
+
}
|
|
13
|
+
|
|
7
14
|
.gauge-radial-root {
|
|
8
15
|
/* Readout vertical placement per layout (% of the cropped box), tuned to the
|
|
9
16
|
watch geometry — re-check if the ring radii or the 448 box change. Which
|
|
@@ -24,7 +31,10 @@
|
|
|
24
31
|
height: auto;
|
|
25
32
|
max-width: 100%;
|
|
26
33
|
max-height: 100%;
|
|
27
|
-
aspect
|
|
34
|
+
/* --gauge-radial-aspect is set from TS only when the frame lowered a crop
|
|
35
|
+
for the label drop (frame.clipsAdjusted); otherwise the static sector
|
|
36
|
+
aspect below applies. */
|
|
37
|
+
aspect-ratio: var(--gauge-radial-aspect, 1);
|
|
28
38
|
|
|
29
39
|
/* Defaults (centered both ways); overridden by the halign-* / valign-*
|
|
30
40
|
classes. Horizontal alignment uses auto margins (which reference the host
|
|
@@ -38,7 +48,7 @@
|
|
|
38
48
|
/* 448 / 251 = the 180° crop's aspect (448 wide × 448·(1 − 44%) tall); keep in
|
|
39
49
|
sync with `sectorClips` bottom=44 in gauge-radial.ts. */
|
|
40
50
|
&.sector-180 {
|
|
41
|
-
aspect-ratio: 448 / 251;
|
|
51
|
+
aspect-ratio: var(--gauge-radial-aspect, 448 / 251);
|
|
42
52
|
}
|
|
43
53
|
|
|
44
54
|
&.halign-left {
|
|
@@ -67,6 +77,26 @@
|
|
|
67
77
|
transform: translateY(-100%);
|
|
68
78
|
}
|
|
69
79
|
|
|
80
|
+
/* faceDiameter pins the dial to a fixed intrinsic size: the inner
|
|
81
|
+
instrument sets its own px box and the root shrink-wraps it. The host
|
|
82
|
+
shrink-wraps too, so there is no slack to align within — neutralize the
|
|
83
|
+
alignment offsets (declared after the valign rules to win the cascade;
|
|
84
|
+
`top: 50%` would resolve to 0 in the auto-height host anyway while the
|
|
85
|
+
translateY still applied, shifting the dial up by half its height). */
|
|
86
|
+
&.face-pinned {
|
|
87
|
+
width: fit-content;
|
|
88
|
+
height: fit-content;
|
|
89
|
+
max-width: none;
|
|
90
|
+
max-height: none;
|
|
91
|
+
aspect-ratio: auto;
|
|
92
|
+
top: 0;
|
|
93
|
+
transform: none;
|
|
94
|
+
|
|
95
|
+
&.sector-180 {
|
|
96
|
+
aspect-ratio: auto;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
70
100
|
.gauge-readout-value {
|
|
71
101
|
position: absolute;
|
|
72
102
|
left: 50%;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type {Meta, StoryObj} from '@storybook/web-components-vite';
|
|
2
|
+
import {html} from 'lit';
|
|
2
3
|
import {
|
|
3
4
|
GaugeRadialHorizontalAlignment,
|
|
4
5
|
GaugeRadialSector,
|
|
@@ -7,7 +8,12 @@ import {
|
|
|
7
8
|
ObcGaugeRadialType,
|
|
8
9
|
} from './gauge-radial.js';
|
|
9
10
|
import './gauge-radial.js';
|
|
10
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
playgroundColumn,
|
|
13
|
+
resizableStoryBox,
|
|
14
|
+
storyHint,
|
|
15
|
+
widthDecorator,
|
|
16
|
+
} from '../../storybook-util.js';
|
|
11
17
|
import {AdviceType} from '../watch/advice.js';
|
|
12
18
|
import {TickmarkStyle} from '../watch/tickmark.js';
|
|
13
19
|
import {InstrumentState, Priority} from '../types.js';
|
|
@@ -15,6 +21,8 @@ import {InstrumentState, Priority} from '../types.js';
|
|
|
15
21
|
type GaugeRadialStoryArgs = Partial<ObcGaugeRadial> & {
|
|
16
22
|
width?: number;
|
|
17
23
|
height?: number;
|
|
24
|
+
/** Story-only: apply faceDiameter to every playground instance. */
|
|
25
|
+
lockFaceDiameter?: boolean;
|
|
18
26
|
};
|
|
19
27
|
|
|
20
28
|
const meta = {
|
|
@@ -47,6 +55,11 @@ const meta = {
|
|
|
47
55
|
showLabels: {control: 'boolean'},
|
|
48
56
|
tickmarksInside: {control: 'boolean'},
|
|
49
57
|
hasReadout: {control: 'boolean'},
|
|
58
|
+
faceDiameter: {
|
|
59
|
+
control: {type: 'range', min: 100, max: 600, step: 10},
|
|
60
|
+
description:
|
|
61
|
+
'Pins the outer-ring diameter in px (fixed intrinsic size, equal circumference across instruments). Clear to return to fill-the-container sizing.',
|
|
62
|
+
},
|
|
50
63
|
value: {control: 'number'},
|
|
51
64
|
minValue: {control: 'number'},
|
|
52
65
|
maxValue: {control: 'number'},
|
|
@@ -357,3 +370,254 @@ export const WideAdviceSpan: Story = {
|
|
|
357
370
|
],
|
|
358
371
|
},
|
|
359
372
|
};
|
|
373
|
+
|
|
374
|
+
const labelRoomRow = html`
|
|
375
|
+
<div style="display: flex; gap: 8px; width: 100%; height: 100%;">
|
|
376
|
+
${[
|
|
377
|
+
{label: 'Load', max: 100},
|
|
378
|
+
{label: 'Torque', max: 100},
|
|
379
|
+
{label: 'Throttle', max: 100},
|
|
380
|
+
{label: 'RPM', max: 3600},
|
|
381
|
+
].map(
|
|
382
|
+
(g) => html`
|
|
383
|
+
<div style="flex: 1; min-width: 0;">
|
|
384
|
+
<obc-gauge-radial
|
|
385
|
+
.value=${g.max * 0.6}
|
|
386
|
+
.maxValue=${g.max}
|
|
387
|
+
.showLabels=${true}
|
|
388
|
+
.primaryTickmarkInterval=${g.max / 4}
|
|
389
|
+
.secondaryTickmarkInterval=${g.max / 20}
|
|
390
|
+
.hasReadout=${true}
|
|
391
|
+
.label=${g.label}
|
|
392
|
+
></obc-gauge-radial>
|
|
393
|
+
</div>
|
|
394
|
+
`
|
|
395
|
+
)}
|
|
396
|
+
</div>
|
|
397
|
+
`;
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* The issue #1021 repro: four gauges squeezed in a flex dashboard row. Each
|
|
401
|
+
* dial automatically reserves room for its own labels, so the 4-digit RPM
|
|
402
|
+
* scale gets a wider margin (and a slightly smaller ring) than its 2-digit
|
|
403
|
+
* siblings — nothing is clipped. To keep the rings the same size instead,
|
|
404
|
+
* give every gauge the same `faceDiameter` — see the *Equal Circumference*
|
|
405
|
+
* stories. Try the *Sizing Playground* story to explore interactively.
|
|
406
|
+
*/
|
|
407
|
+
export const LabelRoomFourDigitRow: Story = {
|
|
408
|
+
name: 'Label Room — Mixed 2/4-Digit Flex Row (#1021)',
|
|
409
|
+
args: {
|
|
410
|
+
width: 800,
|
|
411
|
+
height: 200,
|
|
412
|
+
},
|
|
413
|
+
render: () => labelRoomRow,
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* The same row squeezed below the label-reserve cap
|
|
418
|
+
* (`LABEL_RESERVE_MAX_FRACTION` in `svghelpers/radial-frame.ts`): when labels
|
|
419
|
+
* would eat more than ~45% of the box, they are hidden entirely instead of
|
|
420
|
+
* clipped — matching the canvas charts' graceful degradation. The threshold
|
|
421
|
+
* is content-aware: 2-digit labels survive smaller cells than 4-digit ones.
|
|
422
|
+
*/
|
|
423
|
+
export const LabelRoomDegradationRow: Story = {
|
|
424
|
+
name: 'Label Room — Hide Past Reserve Cap (#1021)',
|
|
425
|
+
args: {
|
|
426
|
+
width: 800,
|
|
427
|
+
height: 140,
|
|
428
|
+
},
|
|
429
|
+
render: () => labelRoomRow,
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* `faceDiameter` pins the outer-ring diameter in px, so all four sectors
|
|
434
|
+
* (270°, 180°, 90°-left, 90°-right) render with identical ring circumference
|
|
435
|
+
* while their boxes differ per arc shape — the radial counterpart of
|
|
436
|
+
* `obc-donut-chart`'s fixed-height ⇒ fixed-circumference contract. Use the
|
|
437
|
+
* `faceDiameter` control to scale all of them together.
|
|
438
|
+
*/
|
|
439
|
+
export const EqualCircumferenceMixedArcs: Story = {
|
|
440
|
+
name: 'Equal Circumference — Same FaceDiameter Across Sectors',
|
|
441
|
+
args: {
|
|
442
|
+
width: 900,
|
|
443
|
+
height: 320,
|
|
444
|
+
faceDiameter: 180,
|
|
445
|
+
},
|
|
446
|
+
render: (args) => html`
|
|
447
|
+
<div style="display: flex; gap: 8px; align-items: flex-start;">
|
|
448
|
+
${[
|
|
449
|
+
GaugeRadialSector.deg270,
|
|
450
|
+
GaugeRadialSector.deg180,
|
|
451
|
+
GaugeRadialSector.deg90Left,
|
|
452
|
+
GaugeRadialSector.deg90Right,
|
|
453
|
+
].map(
|
|
454
|
+
(sector) => html`
|
|
455
|
+
<obc-gauge-radial
|
|
456
|
+
.sector=${sector}
|
|
457
|
+
.value=${60}
|
|
458
|
+
.maxValue=${100}
|
|
459
|
+
.faceDiameter=${args.faceDiameter ?? 180}
|
|
460
|
+
.showLabels=${true}
|
|
461
|
+
.primaryTickmarkInterval=${25}
|
|
462
|
+
.secondaryTickmarkInterval=${5}
|
|
463
|
+
></obc-gauge-radial>
|
|
464
|
+
`
|
|
465
|
+
)}
|
|
466
|
+
</div>
|
|
467
|
+
`,
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Two gauges with the same `faceDiameter` but different label widths: the
|
|
472
|
+
* rings are pixel-identical while the 4-digit gauge's box grows wider to hold
|
|
473
|
+
* its labels — the "equal circumference" answer to the unequal rings visible
|
|
474
|
+
* in the *Label Room* stories. Use the `faceDiameter` control to resize both.
|
|
475
|
+
*/
|
|
476
|
+
export const EqualCircumferenceMixedDigits: Story = {
|
|
477
|
+
name: 'Equal Circumference — 2-Digit Vs 4-Digit, Same FaceDiameter',
|
|
478
|
+
args: {
|
|
479
|
+
width: 700,
|
|
480
|
+
height: 300,
|
|
481
|
+
faceDiameter: 200,
|
|
482
|
+
},
|
|
483
|
+
render: (args) => html`
|
|
484
|
+
<div style="display: flex; gap: 8px; align-items: center;">
|
|
485
|
+
${[
|
|
486
|
+
{label: 'Load', max: 99, interval: 33},
|
|
487
|
+
{label: 'RPM', max: 3600, interval: 900},
|
|
488
|
+
].map(
|
|
489
|
+
(g) => html`
|
|
490
|
+
<obc-gauge-radial
|
|
491
|
+
.value=${g.max * 0.6}
|
|
492
|
+
.maxValue=${g.max}
|
|
493
|
+
.faceDiameter=${args.faceDiameter ?? 200}
|
|
494
|
+
.showLabels=${true}
|
|
495
|
+
.primaryTickmarkInterval=${g.interval}
|
|
496
|
+
.hasReadout=${true}
|
|
497
|
+
.label=${g.label}
|
|
498
|
+
></obc-gauge-radial>
|
|
499
|
+
`
|
|
500
|
+
)}
|
|
501
|
+
</div>
|
|
502
|
+
`,
|
|
503
|
+
};
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Interactive sizing playground: drag the dashed box's bottom-right corner to
|
|
507
|
+
* resize it. The first gauge is pinned to a fixed intrinsic size by the
|
|
508
|
+
* `faceDiameter` control (donut-chart `fixedHeight` behavior), while the
|
|
509
|
+
* 4-digit gauge and the 180° sector adapt to the remaining flex space,
|
|
510
|
+
* reserving label room adaptively (the *Label Room* stories). Enable
|
|
511
|
+
* `lockFaceDiameter` to pin all three to the same circumference — full circle
|
|
512
|
+
* and partial arcs alike (the *Equal Circumference* stories). The same
|
|
513
|
+
* behavior is available on every radial instrument — see the *Sizing
|
|
514
|
+
* Playground* stories under Building Blocks/Watch, Building Blocks/Instrument
|
|
515
|
+
* Radial, Instruments/Speed Gauge, Instruments/Compass, etc.
|
|
516
|
+
*/
|
|
517
|
+
export const SizingPlayground: Story = {
|
|
518
|
+
name: 'Sizing Playground — FaceDiameter + Resizable (Manual)',
|
|
519
|
+
tags: ['skip-test'],
|
|
520
|
+
parameters: {widthDecorator: false},
|
|
521
|
+
args: {
|
|
522
|
+
faceDiameter: 180,
|
|
523
|
+
lockFaceDiameter: false,
|
|
524
|
+
},
|
|
525
|
+
argTypes: {
|
|
526
|
+
lockFaceDiameter: {
|
|
527
|
+
control: 'boolean',
|
|
528
|
+
description:
|
|
529
|
+
'Apply faceDiameter to every instance (equal circumference) instead of only the first.',
|
|
530
|
+
},
|
|
531
|
+
},
|
|
532
|
+
render: (args) => {
|
|
533
|
+
const instances = [
|
|
534
|
+
{
|
|
535
|
+
label: '2-digit, 270°',
|
|
536
|
+
sector: GaugeRadialSector.deg270,
|
|
537
|
+
max: 100,
|
|
538
|
+
interval: 25,
|
|
539
|
+
},
|
|
540
|
+
{
|
|
541
|
+
label: '4-digit, 270°',
|
|
542
|
+
sector: GaugeRadialSector.deg270,
|
|
543
|
+
max: 3600,
|
|
544
|
+
interval: 900,
|
|
545
|
+
},
|
|
546
|
+
{
|
|
547
|
+
label: '2-digit, 180° sector',
|
|
548
|
+
sector: GaugeRadialSector.deg180,
|
|
549
|
+
max: 100,
|
|
550
|
+
interval: 25,
|
|
551
|
+
},
|
|
552
|
+
];
|
|
553
|
+
const fd = (index: number) =>
|
|
554
|
+
index === 0 || args.lockFaceDiameter ? args.faceDiameter : undefined;
|
|
555
|
+
const caption = (index: number, label: string) =>
|
|
556
|
+
fd(index) !== undefined
|
|
557
|
+
? `${label} — pinned ${fd(index)}px`
|
|
558
|
+
: `${label} — adaptive (flex)`;
|
|
559
|
+
return html`
|
|
560
|
+
${storyHint(
|
|
561
|
+
'Drag the bottom-right corner of the dashed box to resize it. The first gauge is pinned by the faceDiameter control; the 4-digit gauge and the 180° sector adapt to the remaining flex space. Enable lockFaceDiameter to pin all three to the same circumference.'
|
|
562
|
+
)}
|
|
563
|
+
${resizableStoryBox(
|
|
564
|
+
html`
|
|
565
|
+
${instances.map((g, index) =>
|
|
566
|
+
playgroundColumn(
|
|
567
|
+
caption(index, g.label),
|
|
568
|
+
html`
|
|
569
|
+
<obc-gauge-radial
|
|
570
|
+
.sector=${g.sector}
|
|
571
|
+
.value=${g.max * 0.6}
|
|
572
|
+
.maxValue=${g.max}
|
|
573
|
+
.faceDiameter=${fd(index)}
|
|
574
|
+
.showLabels=${true}
|
|
575
|
+
.primaryTickmarkInterval=${g.interval}
|
|
576
|
+
></obc-gauge-radial>
|
|
577
|
+
`,
|
|
578
|
+
{pinned: fd(index) !== undefined}
|
|
579
|
+
)
|
|
580
|
+
)}
|
|
581
|
+
`,
|
|
582
|
+
{width: 760, height: 300}
|
|
583
|
+
)}
|
|
584
|
+
`;
|
|
585
|
+
},
|
|
586
|
+
};
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* A 4-digit scale in a narrow column: the reserve is driven by the binding
|
|
590
|
+
* axis (here the width), so the labels stay readable while the ring shrinks.
|
|
591
|
+
*/
|
|
592
|
+
export const LabelRoomTallNarrow: Story = {
|
|
593
|
+
name: 'Label Room — Tall Narrow 4-Digit (#1021)',
|
|
594
|
+
args: {
|
|
595
|
+
value: 2400,
|
|
596
|
+
minValue: 0,
|
|
597
|
+
maxValue: 3600,
|
|
598
|
+
showLabels: true,
|
|
599
|
+
primaryTickmarkInterval: 900,
|
|
600
|
+
secondaryTickmarkInterval: 300,
|
|
601
|
+
width: 160,
|
|
602
|
+
height: 400,
|
|
603
|
+
},
|
|
604
|
+
};
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* With `tickmarksInside` the labels live inside the ring, so no outside
|
|
608
|
+
* reserve is needed — the dial keeps the full legacy box at any label width.
|
|
609
|
+
*/
|
|
610
|
+
export const LabelRoomInsideLabels: Story = {
|
|
611
|
+
name: 'Label Room — Inside Labels Keep The Base Box (#1021)',
|
|
612
|
+
args: {
|
|
613
|
+
value: 2400,
|
|
614
|
+
minValue: 0,
|
|
615
|
+
maxValue: 3600,
|
|
616
|
+
showLabels: true,
|
|
617
|
+
tickmarksInside: true,
|
|
618
|
+
primaryTickmarkInterval: 900,
|
|
619
|
+
secondaryTickmarkInterval: 300,
|
|
620
|
+
width: 200,
|
|
621
|
+
height: 200,
|
|
622
|
+
},
|
|
623
|
+
};
|
|
@@ -2,7 +2,8 @@ import {LitElement, html, nothing, unsafeCSS, type TemplateResult} from 'lit';
|
|
|
2
2
|
import {classMap} from 'lit/directives/class-map.js';
|
|
3
3
|
import componentStyle from './gauge-radial.css?inline';
|
|
4
4
|
import {customElement} from '../../decorator.js';
|
|
5
|
-
import {property} from 'lit/decorators.js';
|
|
5
|
+
import {property, query} from 'lit/decorators.js';
|
|
6
|
+
import type {RadialFrame} from '../../svghelpers/radial-frame.js';
|
|
6
7
|
import {AdviceType} from '../watch/advice.js';
|
|
7
8
|
import {InstrumentState, Priority} from '../types.js';
|
|
8
9
|
import {SetpointMixin} from '../../svghelpers/setpoint-mixin.js';
|
|
@@ -43,6 +44,17 @@ export interface GaugeRadialAdvice {
|
|
|
43
44
|
hinted: boolean;
|
|
44
45
|
}
|
|
45
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Readout meta-row anchors in SVG units, derived from the legacy CSS
|
|
49
|
+
* percentages against the default 448 box (72% → 98.56, 63% → 58.24; the 180°
|
|
50
|
+
* sector's 60% of its 250.88-high crop → −73.472). Converted back to
|
|
51
|
+
* percentages against the actual frame so readouts stay put when the box
|
|
52
|
+
* grows for the label reserve (issue #1021).
|
|
53
|
+
*/
|
|
54
|
+
const READOUT_META_Y = 98.56;
|
|
55
|
+
const READOUT_META_Y_NEEDLE = 58.24;
|
|
56
|
+
const READOUT_META_Y_180 = -73.472;
|
|
57
|
+
|
|
46
58
|
/**
|
|
47
59
|
* `<obc-gauge-radial>` — Configurable radial gauge for generic numeric values.
|
|
48
60
|
*
|
|
@@ -165,6 +177,15 @@ export class ObcGaugeRadial extends SetpointMixin(LitElement) {
|
|
|
165
177
|
@property({type: String}) label = '';
|
|
166
178
|
@property({type: String}) unit = '';
|
|
167
179
|
@property({type: Number}) fractionDigits = 0;
|
|
180
|
+
/**
|
|
181
|
+
* Outer-ring diameter in CSS pixels. When set, the instrument renders at a
|
|
182
|
+
* fixed intrinsic size derived from the ring, arc shape and label reserve —
|
|
183
|
+
* so instruments sharing the same value have identical ring circumference
|
|
184
|
+
* regardless of label width or arc extent (like obc-donut-chart's
|
|
185
|
+
* fixedHeight). When unset (default), the instrument fills its container.
|
|
186
|
+
*/
|
|
187
|
+
@property({type: Number, attribute: 'face-diameter', reflect: true})
|
|
188
|
+
faceDiameter: number | undefined;
|
|
168
189
|
|
|
169
190
|
private get sectorAngles(): {sweep: number; start: number} {
|
|
170
191
|
switch (this.sector) {
|
|
@@ -212,6 +233,40 @@ export class ObcGaugeRadial extends SetpointMixin(LitElement) {
|
|
|
212
233
|
);
|
|
213
234
|
}
|
|
214
235
|
|
|
236
|
+
/** @internal */
|
|
237
|
+
@query('.gauge-radial-root') private _rootEl?: HTMLElement;
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Re-anchor the %-positioned readouts against the frame the inner
|
|
241
|
+
* instrument actually rendered (the box grows when labels need room).
|
|
242
|
+
*/
|
|
243
|
+
private _onFrameChanged = (e: Event): void => {
|
|
244
|
+
const frame = (e as CustomEvent<RadialFrame>).detail;
|
|
245
|
+
const root = this._rootEl;
|
|
246
|
+
if (!root || !(frame.height > 0)) {
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
const pct = (anchorY: number) =>
|
|
250
|
+
`${(((anchorY - frame.y) / frame.height) * 100).toFixed(4)}%`;
|
|
251
|
+
root.style.setProperty('--readout-meta-top', pct(READOUT_META_Y));
|
|
252
|
+
root.style.setProperty(
|
|
253
|
+
'--readout-meta-top-needle',
|
|
254
|
+
pct(READOUT_META_Y_NEEDLE)
|
|
255
|
+
);
|
|
256
|
+
root.style.setProperty('--readout-meta-top-180', pct(READOUT_META_Y_180));
|
|
257
|
+
// When the frame lowered a crop for the label drop, the sector's static
|
|
258
|
+
// aspect-ratio no longer matches the viewBox — follow the frame so the
|
|
259
|
+
// dial keeps hugging the root box (contain behavior, issue #992).
|
|
260
|
+
if (frame.clipsAdjusted) {
|
|
261
|
+
root.style.setProperty(
|
|
262
|
+
'--gauge-radial-aspect',
|
|
263
|
+
`${frame.width} / ${frame.height}`
|
|
264
|
+
);
|
|
265
|
+
} else {
|
|
266
|
+
root.style.removeProperty('--gauge-radial-aspect');
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
|
|
215
270
|
// Arrow form so `this` binds when passed as `.getAngle=${this.getAngle}`
|
|
216
271
|
// to <obc-instrument-radial>. Do not convert to a method.
|
|
217
272
|
getAngle = (v: number): number => {
|
|
@@ -304,6 +359,7 @@ export class ObcGaugeRadial extends SetpointMixin(LitElement) {
|
|
|
304
359
|
<div
|
|
305
360
|
class=${classMap({
|
|
306
361
|
'gauge-radial-root': true,
|
|
362
|
+
'face-pinned': this.faceDiameter !== undefined,
|
|
307
363
|
'type-needle': this.type === ObcGaugeRadialType.needle,
|
|
308
364
|
'sector-180': this.sector === GaugeRadialSector.deg180,
|
|
309
365
|
'sector-90-left': this.sector === GaugeRadialSector.deg90Left,
|
|
@@ -341,6 +397,8 @@ export class ObcGaugeRadial extends SetpointMixin(LitElement) {
|
|
|
341
397
|
.clipLeft=${clips.left}
|
|
342
398
|
.clipRight=${clips.right}
|
|
343
399
|
.endLabelsMaxMin=${this.sector === GaugeRadialSector.deg180}
|
|
400
|
+
.faceDiameter=${this.faceDiameter}
|
|
401
|
+
@frame-changed=${this._onFrameChanged}
|
|
344
402
|
>
|
|
345
403
|
</obc-instrument-radial>
|
|
346
404
|
${this.renderReadouts()}
|
|
@@ -5,7 +5,13 @@ import {
|
|
|
5
5
|
ObcHeading,
|
|
6
6
|
} from './heading.js';
|
|
7
7
|
import './heading.js';
|
|
8
|
-
import {
|
|
8
|
+
import {html} from 'lit';
|
|
9
|
+
import {
|
|
10
|
+
playgroundColumn,
|
|
11
|
+
resizableStoryBox,
|
|
12
|
+
storyHint,
|
|
13
|
+
widthDecorator,
|
|
14
|
+
} from '../../storybook-util.js';
|
|
9
15
|
import {AdviceType} from '../watch/advice.js';
|
|
10
16
|
import {Priority} from '../types.js';
|
|
11
17
|
|
|
@@ -33,6 +39,11 @@ const meta: Meta<typeof ObcHeading> = {
|
|
|
33
39
|
},
|
|
34
40
|
argTypes: {
|
|
35
41
|
width: {control: {type: 'range', min: 32, max: 1028, step: 1}},
|
|
42
|
+
faceDiameter: {
|
|
43
|
+
control: {type: 'range', min: 100, max: 600, step: 10},
|
|
44
|
+
description:
|
|
45
|
+
'Pins the outer-ring diameter in px (fixed intrinsic size, equal circumference across instruments). Clear to return to fill-the-container sizing.',
|
|
46
|
+
},
|
|
36
47
|
heading: {control: {type: 'range', min: 0, max: 360, step: 1}},
|
|
37
48
|
courseOverGround: {control: {type: 'range', min: 0, max: 360, step: 1}},
|
|
38
49
|
headingSetpoint: {control: {type: 'range', min: 0, max: 360, step: 1}},
|
|
@@ -67,3 +78,83 @@ export const Enhanced: Story = {
|
|
|
67
78
|
headingSetpoint: 311,
|
|
68
79
|
},
|
|
69
80
|
};
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Below the label-reserve cap (~200px for the NSWE + north-arrow decor) the
|
|
84
|
+
* frame reports `labelsHidden` and the instrument drops the labels and the
|
|
85
|
+
* north arrow instead of letting them clip — the same graceful degradation
|
|
86
|
+
* the tick-label instruments use (issue #1021).
|
|
87
|
+
*/
|
|
88
|
+
export const SmallContainerDegraded: Story = {
|
|
89
|
+
name: 'Small Container (160px, Labels + Arrow Hidden Past Cap)',
|
|
90
|
+
args: {
|
|
91
|
+
width: 160,
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
type SizingPlaygroundArgs = Partial<ObcHeading> & {
|
|
96
|
+
lockFaceDiameter?: boolean;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Interactive sizing playground: drag the dashed box's bottom-right corner to
|
|
101
|
+
* resize it. The first instrument is pinned to a fixed intrinsic size by the
|
|
102
|
+
* `faceDiameter` control, while the second adapts to the remaining flex
|
|
103
|
+
* space, reserving room for the NSWE labels and north arrow adaptively
|
|
104
|
+
* (issue #1021). Enable `lockFaceDiameter` to pin both to the same
|
|
105
|
+
* circumference. Related: *Sizing Playground* stories under Building
|
|
106
|
+
* Blocks/Watch, Building Blocks/Instrument Radial and Instruments/Gauge
|
|
107
|
+
* Radial.
|
|
108
|
+
*/
|
|
109
|
+
export const SizingPlayground: StoryObj<SizingPlaygroundArgs> = {
|
|
110
|
+
name: 'Sizing Playground — FaceDiameter + Resizable (Manual)',
|
|
111
|
+
tags: ['skip-test'],
|
|
112
|
+
parameters: {widthDecorator: false},
|
|
113
|
+
args: {
|
|
114
|
+
faceDiameter: 240,
|
|
115
|
+
lockFaceDiameter: false,
|
|
116
|
+
},
|
|
117
|
+
argTypes: {
|
|
118
|
+
lockFaceDiameter: {
|
|
119
|
+
control: 'boolean',
|
|
120
|
+
description:
|
|
121
|
+
'Apply faceDiameter to every instance (equal circumference) instead of only the first.',
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
render: (args) => {
|
|
125
|
+
const instances = [
|
|
126
|
+
{label: 'heading A', heading: 311},
|
|
127
|
+
{label: 'heading B', heading: 45},
|
|
128
|
+
];
|
|
129
|
+
const fd = (index: number) =>
|
|
130
|
+
index === 0 || args.lockFaceDiameter ? args.faceDiameter : undefined;
|
|
131
|
+
const caption = (index: number, label: string) =>
|
|
132
|
+
fd(index) !== undefined
|
|
133
|
+
? `${label} — pinned ${fd(index)}px`
|
|
134
|
+
: `${label} — adaptive (flex)`;
|
|
135
|
+
return html`
|
|
136
|
+
${storyHint(
|
|
137
|
+
'Drag the bottom-right corner of the dashed box to resize it. The first instrument is pinned by the faceDiameter control; the second adapts to the remaining flex space. Enable lockFaceDiameter to pin both to the same circumference.'
|
|
138
|
+
)}
|
|
139
|
+
${resizableStoryBox(
|
|
140
|
+
html`
|
|
141
|
+
${instances.map((g, index) =>
|
|
142
|
+
playgroundColumn(
|
|
143
|
+
caption(index, g.label),
|
|
144
|
+
html`
|
|
145
|
+
<obc-heading
|
|
146
|
+
.heading=${g.heading}
|
|
147
|
+
.courseOverGround=${g.heading + 27}
|
|
148
|
+
.showLabels=${true}
|
|
149
|
+
.faceDiameter=${fd(index)}
|
|
150
|
+
></obc-heading>
|
|
151
|
+
`,
|
|
152
|
+
{pinned: fd(index) !== undefined}
|
|
153
|
+
)
|
|
154
|
+
)}
|
|
155
|
+
`,
|
|
156
|
+
{width: 680, height: 400}
|
|
157
|
+
)}
|
|
158
|
+
`;
|
|
159
|
+
},
|
|
160
|
+
};
|
|
@@ -5,6 +5,14 @@ import {Tickmark, TickmarkType} from '../watch/tickmark.js';
|
|
|
5
5
|
import {arrow, ArrowStyle} from './arrow.js';
|
|
6
6
|
import {AdviceState, AngleAdvice, AngleAdviceRaw} from '../watch/advice.js';
|
|
7
7
|
import {ResizeController} from '@lit-labs/observers/resize-controller.js';
|
|
8
|
+
import {
|
|
9
|
+
applyPinnedHostSize,
|
|
10
|
+
computeRadialFrame,
|
|
11
|
+
measureContainerPx,
|
|
12
|
+
NORTH_ARROW_WIDTH_PX,
|
|
13
|
+
NSWE_LABEL_WIDTH_PX,
|
|
14
|
+
type RadialFrame,
|
|
15
|
+
} from '../../svghelpers/radial-frame.js';
|
|
8
16
|
import {WatchCircleType} from '../watch/watch.js';
|
|
9
17
|
import {SetpointBundle} from '../../svghelpers/setpoint-bundle.js';
|
|
10
18
|
import {Priority} from '../types.js';
|
|
@@ -58,6 +66,15 @@ export class ObcHeading extends LitElement {
|
|
|
58
66
|
@property({type: Boolean}) showLabels: boolean = false;
|
|
59
67
|
/** When true, labels and north arrow are placed inside the outer ring. */
|
|
60
68
|
@property({type: Boolean}) tickmarksInside: boolean = false;
|
|
69
|
+
/**
|
|
70
|
+
* Outer-ring diameter in CSS pixels. When set, the instrument renders at a
|
|
71
|
+
* fixed intrinsic size derived from the ring, arc shape and label reserve —
|
|
72
|
+
* so instruments sharing the same value have identical ring circumference
|
|
73
|
+
* regardless of label width or arc extent (like obc-donut-chart's
|
|
74
|
+
* fixedHeight). When unset (default), the instrument fills its container.
|
|
75
|
+
*/
|
|
76
|
+
@property({type: Number, attribute: 'face-diameter'})
|
|
77
|
+
faceDiameter: number | undefined;
|
|
61
78
|
|
|
62
79
|
private _headingSp = new SetpointBundle({
|
|
63
80
|
angularWraparound: true,
|
|
@@ -88,18 +105,30 @@ export class ObcHeading extends LitElement {
|
|
|
88
105
|
// function is called on resize of the element
|
|
89
106
|
private _resizeController = new ResizeController(this, {});
|
|
90
107
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
108
|
+
/**
|
|
109
|
+
* Pixel cost of the outside decor: the always-rendered north-arrow glyph
|
|
110
|
+
* plus, only while shown, the NSWE labels (both keep a constant on-screen
|
|
111
|
+
* size via `1/scale` terms). Feeds the frame's width-aware reserve,
|
|
112
|
+
* replacing the former empirical `72 + delta(clientSize)` padding.
|
|
113
|
+
*/
|
|
114
|
+
private getOutsideDecorPx(): number {
|
|
115
|
+
if (this.tickmarksInside) {
|
|
116
|
+
return 0;
|
|
100
117
|
}
|
|
118
|
+
return NORTH_ARROW_WIDTH_PX + (this.showLabels ? NSWE_LABEL_WIDTH_PX : 0);
|
|
119
|
+
}
|
|
101
120
|
|
|
102
|
-
|
|
121
|
+
/** Whether the host size styles were set by applyPinnedHostSize. */
|
|
122
|
+
private _hostSizePinned = false;
|
|
123
|
+
private _frame: RadialFrame | undefined;
|
|
124
|
+
|
|
125
|
+
override updated(changed: PropertyValues): void {
|
|
126
|
+
super.updated(changed);
|
|
127
|
+
this._hostSizePinned = applyPinnedHostSize(
|
|
128
|
+
this,
|
|
129
|
+
this._frame,
|
|
130
|
+
this._hostSizePinned
|
|
131
|
+
);
|
|
103
132
|
}
|
|
104
133
|
|
|
105
134
|
private get angleAdviceRaw(): AngleAdviceRaw[] {
|
|
@@ -140,22 +169,27 @@ export class ObcHeading extends LitElement {
|
|
|
140
169
|
{angle: 270, type: TickmarkType.main},
|
|
141
170
|
];
|
|
142
171
|
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
|
|
172
|
+
const frame = computeRadialFrame({
|
|
173
|
+
basePadding: 72,
|
|
174
|
+
labelWidthPx: this.getOutsideDecorPx(),
|
|
175
|
+
containerPx: measureContainerPx(this),
|
|
176
|
+
faceDiameter: this.faceDiameter,
|
|
177
|
+
});
|
|
178
|
+
this._frame = frame;
|
|
179
|
+
const viewBox = frame.viewBox;
|
|
146
180
|
|
|
147
181
|
return html`
|
|
148
182
|
<div class="container">
|
|
149
183
|
<obc-watch
|
|
150
184
|
.touching=${this.touching}
|
|
151
|
-
.
|
|
185
|
+
.arcFrame=${frame}
|
|
152
186
|
.advices=${this.angleAdviceRaw}
|
|
153
187
|
.tickmarks=${tickmarks}
|
|
154
188
|
.watchCircleType=${WatchCircleType.single}
|
|
155
|
-
.showLabels=${this.showLabels}
|
|
189
|
+
.showLabels=${this.showLabels && !frame.labelsHidden}
|
|
156
190
|
.tickmarksInside=${this.tickmarksInside}
|
|
157
191
|
.crosshairEnabled=${true}
|
|
158
|
-
.northArrow=${
|
|
192
|
+
.northArrow=${!frame.labelsHidden}
|
|
159
193
|
.angleSetpoint=${this.headingSetpoint ?? undefined}
|
|
160
194
|
.newAngleSetpoint=${this.newHeadingSetpoint}
|
|
161
195
|
.atAngleSetpoint=${this._headingSp.computeAtSetpoint(this.heading)}
|