@oicl/openbridge-webcomponents-full-bundle 2.0.0-next.101 → 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 +580 -143
- package/bundle/openbridge-webcomponents.bundle.js.map +1 -1
- package/custom-elements.json +629 -407
- package/dist/bars-graphs/donut-chart/donut-chart.d.ts +18 -16
- package/dist/bars-graphs/donut-chart/donut-chart.d.ts.map +1 -1
- package/dist/bars-graphs/donut-chart/donut-chart.js.map +1 -1
- package/dist/bars-graphs/pie-chart/pie-chart.d.ts +14 -12
- package/dist/bars-graphs/pie-chart/pie-chart.d.ts.map +1 -1
- package/dist/bars-graphs/pie-chart/pie-chart.js.map +1 -1
- package/dist/bars-graphs/polar-chart/polar-chart.d.ts +19 -16
- package/dist/bars-graphs/polar-chart/polar-chart.d.ts.map +1 -1
- package/dist/bars-graphs/polar-chart/polar-chart.js.map +1 -1
- package/dist/bars-graphs/radial-bar-chart/radial-bar-chart.d.ts +8 -8
- package/dist/bars-graphs/radial-bar-chart/radial-bar-chart.d.ts.map +1 -1
- package/dist/bars-graphs/radial-bar-chart/radial-bar-chart.js.map +1 -1
- package/dist/building-blocks/chart-line/chart-line-base.d.ts +8 -25
- package/dist/building-blocks/chart-line/chart-line-base.d.ts.map +1 -1
- package/dist/building-blocks/chart-line/chart-line-base.js.map +1 -1
- 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/integration-systems/integration-bar/integration-bar.d.ts +20 -24
- package/dist/integration-systems/integration-bar/integration-bar.d.ts.map +1 -1
- package/dist/integration-systems/integration-bar/integration-bar.js.map +1 -1
- package/dist/integration-systems/integration-button/integration-button.d.ts +15 -13
- package/dist/integration-systems/integration-button/integration-button.d.ts.map +1 -1
- package/dist/integration-systems/integration-button/integration-button.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 +68 -31
- 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-flat/compass-flat.d.ts +8 -9
- package/dist/navigation-instruments/compass-flat/compass-flat.d.ts.map +1 -1
- package/dist/navigation-instruments/compass-flat/compass-flat.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/gauge-trend/gauge-trend.d.ts +0 -5
- package/dist/navigation-instruments/gauge-trend/gauge-trend.d.ts.map +1 -1
- package/dist/navigation-instruments/gauge-trend/gauge-trend.js.map +1 -1
- package/dist/navigation-instruments/graph-mini/graph-mini.d.ts +1 -1
- package/dist/navigation-instruments/graph-mini/graph-mini.d.ts.map +1 -1
- package/dist/navigation-instruments/graph-mini/graph-mini.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/indicator-graph/indicator-graph.d.ts +1 -1
- package/dist/navigation-instruments/indicator-graph/indicator-graph.d.ts.map +1 -1
- package/dist/navigation-instruments/indicator-graph/indicator-graph.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 +45 -17
- package/dist/navigation-instruments/watch/watch.d.ts.map +1 -1
- package/dist/navigation-instruments/watch/watch.js +53 -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/script/check-slot-event-docs.ts +35 -0
- package/src/bars-graphs/donut-chart/donut-chart.ts +18 -16
- package/src/bars-graphs/pie-chart/pie-chart.ts +14 -12
- package/src/bars-graphs/polar-chart/polar-chart.ts +19 -16
- package/src/bars-graphs/radial-bar-chart/radial-bar-chart.ts +8 -8
- package/src/building-blocks/chart-line/chart-line-base.ts +8 -25
- 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/integration-systems/integration-bar/integration-bar.ts +20 -24
- package/src/integration-systems/integration-button/integration-button.ts +15 -13
- 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 +99 -46
- package/src/navigation-instruments/compass-flat/compass-flat.ts +8 -9
- 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/gauge-trend/gauge-trend.ts +0 -5
- package/src/navigation-instruments/graph-mini/graph-mini.ts +1 -1
- package/src/navigation-instruments/heading/heading.stories.ts +92 -1
- package/src/navigation-instruments/heading/heading.ts +50 -16
- package/src/navigation-instruments/indicator-graph/indicator-graph.ts +1 -1
- 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 +100 -60
- package/src/storybook-util.ts +59 -0
- package/src/svghelpers/radial-frame.spec.ts +307 -0
- package/src/svghelpers/radial-frame.ts +508 -0
|
@@ -6,7 +6,12 @@ import {
|
|
|
6
6
|
WatchCircleType,
|
|
7
7
|
} from './watch.js';
|
|
8
8
|
import './watch.js';
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
playgroundColumn,
|
|
11
|
+
resizableStoryBox,
|
|
12
|
+
storyHint,
|
|
13
|
+
widthDecorator,
|
|
14
|
+
} from '../../storybook-util.js';
|
|
10
15
|
import {AdviceState, AdviceType} from './advice.js';
|
|
11
16
|
import {InstrumentState, Priority} from '../types.js';
|
|
12
17
|
import {TickmarkType} from './tickmark.js';
|
|
@@ -70,6 +75,11 @@ Source of truth: \`packages/openbridge-webcomponents/src/navigation-instruments/
|
|
|
70
75
|
},
|
|
71
76
|
areas: {control: {type: 'object'}},
|
|
72
77
|
padding: {control: {type: 'range', min: 0, max: 100, step: 1}},
|
|
78
|
+
faceDiameter: {
|
|
79
|
+
control: {type: 'range', min: 100, max: 600, step: 10},
|
|
80
|
+
description:
|
|
81
|
+
'Pins the outer-ring diameter in px (fixed intrinsic size, equal circumference across instruments). Clear to return to fill-the-container sizing.',
|
|
82
|
+
},
|
|
73
83
|
vessels: {control: {type: 'object'}},
|
|
74
84
|
windKnots: {control: {type: 'range', min: 0, max: 100, step: 1}},
|
|
75
85
|
windFromDirectionDeg: {control: {type: 'range', min: 0, max: 360, step: 1}},
|
|
@@ -729,6 +739,135 @@ export const TickmarksTestInsideRotation: Story = {
|
|
|
729
739
|
},
|
|
730
740
|
};
|
|
731
741
|
|
|
742
|
+
/**
|
|
743
|
+
* Regression test for label counter-scaling after a container resize: the
|
|
744
|
+
* wrapper starts at 400px and `play` shrinks it to 240px BEFORE the snapshot.
|
|
745
|
+
* The standalone watch host renders inline (no box), so its ResizeObserver
|
|
746
|
+
* only fires via the internal `<svg>` (`observeInnerBox`) — if that
|
|
747
|
+
* observation is lost, `--scale` stays at the 400px value and the labels
|
|
748
|
+
* shrink with the box (~7px on screen) instead of holding ~12px.
|
|
749
|
+
*/
|
|
750
|
+
export const CounterScaleAfterResize: Story = {
|
|
751
|
+
args: {
|
|
752
|
+
tickmarks: [
|
|
753
|
+
...Array.from({length: 24}, (_, i) => ({
|
|
754
|
+
angle: i * 15,
|
|
755
|
+
type: TickmarkType.secondary,
|
|
756
|
+
text: `${i * 15}`,
|
|
757
|
+
})),
|
|
758
|
+
],
|
|
759
|
+
},
|
|
760
|
+
parameters: {widthDecorator: false},
|
|
761
|
+
render: (args) => html`
|
|
762
|
+
<div id="counter-scale-wrap" style="width: 400px; height: 400px;">
|
|
763
|
+
<obc-watch .tickmarks=${args.tickmarks}></obc-watch>
|
|
764
|
+
</div>
|
|
765
|
+
`,
|
|
766
|
+
play: async ({canvasElement}) => {
|
|
767
|
+
const wrap = canvasElement.querySelector(
|
|
768
|
+
'#counter-scale-wrap'
|
|
769
|
+
) as HTMLElement;
|
|
770
|
+
wrap.style.width = '240px';
|
|
771
|
+
wrap.style.height = '240px';
|
|
772
|
+
await new Promise((resolve) =>
|
|
773
|
+
requestAnimationFrame(() => requestAnimationFrame(resolve))
|
|
774
|
+
);
|
|
775
|
+
await wrap.querySelector('obc-watch')?.updateComplete;
|
|
776
|
+
},
|
|
777
|
+
};
|
|
778
|
+
|
|
779
|
+
type SizingPlaygroundArgs = Partial<ObcWatch> & {
|
|
780
|
+
lockFaceDiameter?: boolean;
|
|
781
|
+
};
|
|
782
|
+
|
|
783
|
+
/**
|
|
784
|
+
* Interactive sizing playground for the core renderer: drag the dashed box's
|
|
785
|
+
* bottom-right corner to resize it. The first watch (2-digit tick labels) is
|
|
786
|
+
* pinned to a fixed intrinsic size by the `faceDiameter` control, while the
|
|
787
|
+
* 4-digit watch and the 180° arc adapt to the remaining flex space, reserving
|
|
788
|
+
* label room adaptively (and hiding labels past the reserve cap in
|
|
789
|
+
* `svghelpers/radial-frame.ts`, issue #1021). Enable `lockFaceDiameter` to
|
|
790
|
+
* pin all three to the same circumference. Related: *Sizing Playground*
|
|
791
|
+
* stories under Building Blocks/Instrument Radial and Instruments/Gauge
|
|
792
|
+
* Radial.
|
|
793
|
+
*/
|
|
794
|
+
export const SizingPlayground: StoryObj<SizingPlaygroundArgs> = {
|
|
795
|
+
name: 'Sizing Playground — FaceDiameter + Resizable (Manual)',
|
|
796
|
+
tags: ['skip-test'],
|
|
797
|
+
parameters: {widthDecorator: false},
|
|
798
|
+
args: {
|
|
799
|
+
faceDiameter: 200,
|
|
800
|
+
lockFaceDiameter: false,
|
|
801
|
+
},
|
|
802
|
+
argTypes: {
|
|
803
|
+
lockFaceDiameter: {
|
|
804
|
+
control: 'boolean',
|
|
805
|
+
description:
|
|
806
|
+
'Apply faceDiameter to every instance (equal circumference) instead of only the first.',
|
|
807
|
+
},
|
|
808
|
+
},
|
|
809
|
+
render: (args) => {
|
|
810
|
+
const fullCircleTicks = (factor: number) =>
|
|
811
|
+
Array.from({length: 12}, (_, i) => ({
|
|
812
|
+
angle: i * 30,
|
|
813
|
+
type: TickmarkType.secondary,
|
|
814
|
+
text: `${i * 30 * factor}`,
|
|
815
|
+
}));
|
|
816
|
+
const instances = [
|
|
817
|
+
{label: '2-digit ticks', ticks: fullCircleTicks(1), areas: undefined},
|
|
818
|
+
{label: '4-digit ticks', ticks: fullCircleTicks(10), areas: undefined},
|
|
819
|
+
{
|
|
820
|
+
label: '180° arc',
|
|
821
|
+
ticks: Array.from({length: 7}, (_, i) => ({
|
|
822
|
+
angle: i * 30 - 90,
|
|
823
|
+
type: TickmarkType.secondary,
|
|
824
|
+
text: `${i * 30}`,
|
|
825
|
+
})),
|
|
826
|
+
areas: [
|
|
827
|
+
{
|
|
828
|
+
startAngle: -90,
|
|
829
|
+
endAngle: 90,
|
|
830
|
+
roundInsideCut: true,
|
|
831
|
+
roundOutsideCut: true,
|
|
832
|
+
},
|
|
833
|
+
],
|
|
834
|
+
},
|
|
835
|
+
];
|
|
836
|
+
const fd = (index: number) =>
|
|
837
|
+
index === 0 || args.lockFaceDiameter ? args.faceDiameter : undefined;
|
|
838
|
+
const caption = (index: number, label: string) =>
|
|
839
|
+
fd(index) !== undefined
|
|
840
|
+
? `${label} — pinned ${fd(index)}px`
|
|
841
|
+
: `${label} — adaptive (flex)`;
|
|
842
|
+
return html`
|
|
843
|
+
${storyHint(
|
|
844
|
+
'Drag the bottom-right corner of the dashed box to resize it. The first watch is pinned by the faceDiameter control; the 4-digit watch and the 180° arc adapt to the remaining flex space. Enable lockFaceDiameter to pin all three to the same circumference.'
|
|
845
|
+
)}
|
|
846
|
+
${resizableStoryBox(
|
|
847
|
+
html`
|
|
848
|
+
${instances.map((g, index) =>
|
|
849
|
+
playgroundColumn(
|
|
850
|
+
caption(index, g.label),
|
|
851
|
+
html`
|
|
852
|
+
<obc-watch
|
|
853
|
+
.faceDiameter=${fd(index)}
|
|
854
|
+
.tickmarks=${g.ticks}
|
|
855
|
+
.areas=${g.areas ?? []}
|
|
856
|
+
.watchCircleType=${g.areas
|
|
857
|
+
? WatchCircleType.double
|
|
858
|
+
: WatchCircleType.single}
|
|
859
|
+
></obc-watch>
|
|
860
|
+
`,
|
|
861
|
+
{pinned: fd(index) !== undefined}
|
|
862
|
+
)
|
|
863
|
+
)}
|
|
864
|
+
`,
|
|
865
|
+
{width: 760, height: 320}
|
|
866
|
+
)}
|
|
867
|
+
`;
|
|
868
|
+
},
|
|
869
|
+
};
|
|
870
|
+
|
|
732
871
|
/**
|
|
733
872
|
* Side-by-side comparison of setpoint visual states for radial instruments.
|
|
734
873
|
*
|
|
@@ -52,10 +52,16 @@ import {
|
|
|
52
52
|
import {VesselImage, VesselImageSize, vesselImages} from './vessel.js';
|
|
53
53
|
import {renderCurrent, renderWind} from './environment.js';
|
|
54
54
|
import {customElement} from '../../decorator.js';
|
|
55
|
+
import {type ZoomToFitArcFrame} from '../../svghelpers/arc-frame.js';
|
|
55
56
|
import {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
applyPinnedHostSize,
|
|
58
|
+
computeRadialFrame,
|
|
59
|
+
estimateLabelWidthPx,
|
|
60
|
+
measureContainerPx,
|
|
61
|
+
NSWE_LABEL_WIDTH_PX,
|
|
62
|
+
observeInnerBox,
|
|
63
|
+
type RadialFrame,
|
|
64
|
+
} from '../../svghelpers/radial-frame.js';
|
|
59
65
|
export {VesselImage, VesselImageSize, vesselImages};
|
|
60
66
|
|
|
61
67
|
export enum WatchCircleType {
|
|
@@ -161,22 +167,6 @@ const RADIAL_SETPOINT_INWARD_ADJUST = 4;
|
|
|
161
167
|
* `_setpointCssAngle` tracks the accumulated CSS angle to avoid long-way-around
|
|
162
168
|
* transitions across the 0°/360° boundary.
|
|
163
169
|
*
|
|
164
|
-
* @property {InstrumentState} state - Instrument state (active, loading, off)
|
|
165
|
-
* @property {Priority} priority - Color priority (enhanced = blue palette, regular = gray palette)
|
|
166
|
-
* @property {number|undefined} angleSetpoint - Setpoint angle in degrees (0° = 12 o'clock)
|
|
167
|
-
* @property {number|undefined} newAngleSetpoint - New setpoint being adjusted (focus mode)
|
|
168
|
-
* @property {boolean} atAngleSetpoint - Whether value matches setpoint (within deadband)
|
|
169
|
-
* @property {number} angleSetpointAtZeroDeadband - Deadband for zero detection (default 0.5°)
|
|
170
|
-
* @property {boolean} setpointOverride - Override to derive setpoint color from priority regardless of state
|
|
171
|
-
* @property {RotType|undefined} rotType - ROT visualization type: `'dots'` (spinning dots) or `'bar'` (arc bar with clipped dots). Undefined hides the ROT layer.
|
|
172
|
-
* @property {RotPosition} rotPosition - Track on which ROT elements are placed: `'scale'` (on the outer ring) or `'innerCircle'` (default, inside the inner ring)
|
|
173
|
-
* @property {number} rotStartAngle - Start angle of the ROT bar arc in degrees (0° = 12 o'clock, clockwise). Only used when `rotType` is `'bar'`.
|
|
174
|
-
* @property {number} rotEndAngle - End angle of the ROT bar arc in degrees. The bar is hidden when the difference from `rotStartAngle` is less than 0.1°.
|
|
175
|
-
* @property {Priority|undefined} rotPriority - Override priority for ROT color derivation. When set, ROT colors use this instead of the main `priority`. Useful when the ROT element has independent priority (e.g. compass per-element priority).
|
|
176
|
-
* @property {number|undefined} rateOfTurnDegreesPerMinute - Measured rate of turn in degrees per minute (the maritime/AIS convention, see ES-TRIN 2025/1 Art. 3.02 and ITU-R M.1371). Sign controls direction (positive = starboard/clockwise). When defined, this drives both the dot animation (multiplied by `rotDotAnimationFactor`) and the port/starboard direction sign.
|
|
177
|
-
* @property {number} rotDotAnimationFactor - Visual amplification factor applied only to the spinning-dot animation (not to bar extent). Default `18` keeps the legacy visual feel (≈1 rpm at 20°/min).
|
|
178
|
-
* @property {number} rotationsPerMinute - **Deprecated.** Spin speed of the ROT dot ring in rotations per minute. Sign controls direction (positive = clockwise). Use `rateOfTurnDegreesPerMinute` instead.
|
|
179
|
-
* @property {ZoomToFitArcFrame|undefined} arcFrame - Pre-computed zoom-to-fit arc frame. When set, the watch skips its own `computeZoomToFitArcFrame()` call and uses these values directly. Consumer instruments (e.g. rudder, instrument-radial) should compute the frame once and pass it here to avoid redundant computation. If you pass `arcFrame`, you own keeping it in sync with `areas` / `watchCircleType` — obc-watch will NOT recompute it, so a stale frame renders stale geometry.
|
|
180
170
|
* @experimental
|
|
181
171
|
*/
|
|
182
172
|
@customElement('obc-watch')
|
|
@@ -184,16 +174,23 @@ export class ObcWatch extends LitElement {
|
|
|
184
174
|
private _setpointId = `watch-setpoint-${Math.random().toString(36).slice(2, 9)}`;
|
|
185
175
|
private _newSetpointId = `watch-new-setpoint-${Math.random().toString(36).slice(2, 9)}`;
|
|
186
176
|
|
|
177
|
+
/** Instrument state (active, loading, off) */
|
|
187
178
|
@property({type: String}) state: InstrumentState = InstrumentState.active;
|
|
179
|
+
/** Color priority (enhanced = blue palette, regular = gray palette) */
|
|
188
180
|
@property({type: String}) priority: Priority = Priority.regular;
|
|
189
181
|
@property({type: String}) watchCircleType: WatchCircleType =
|
|
190
182
|
WatchCircleType.single;
|
|
191
183
|
@property({type: Boolean}) northArrow: boolean = false;
|
|
192
184
|
@property({type: Boolean}) northArrowInside: boolean | undefined;
|
|
185
|
+
/** Setpoint angle in degrees (0° = 12 o'clock) */
|
|
193
186
|
@property({type: Number}) angleSetpoint: number | undefined;
|
|
187
|
+
/** New setpoint being adjusted (focus mode) */
|
|
194
188
|
@property({type: Number}) newAngleSetpoint: number | undefined;
|
|
189
|
+
/** Whether value matches setpoint (within deadband) */
|
|
195
190
|
@property({type: Boolean}) atAngleSetpoint: boolean = false;
|
|
191
|
+
/** Deadband for zero detection (default 0.5°) */
|
|
196
192
|
@property({type: Number}) angleSetpointAtZeroDeadband: number = 0.5;
|
|
193
|
+
/** Override to derive setpoint color from priority regardless of state */
|
|
197
194
|
@property({type: Boolean}) setpointOverride: boolean = false;
|
|
198
195
|
@property({type: Boolean}) touching: boolean = false;
|
|
199
196
|
|
|
@@ -211,7 +208,21 @@ export class ObcWatch extends LitElement {
|
|
|
211
208
|
|
|
212
209
|
/** Whether the setpoint CSS angle has been initialised (to skip transition on first render). */
|
|
213
210
|
private _setpointCssAngleInit = false;
|
|
211
|
+
/**
|
|
212
|
+
* Explicit padding override in SVG units: the un-zoomed viewBox becomes
|
|
213
|
+
* exactly `(176 + padding) * 2`. Setting it disables the automatic
|
|
214
|
+
* width-aware label reserve (issue #1021) — the caller owns label room.
|
|
215
|
+
*/
|
|
214
216
|
@property({type: Number}) padding: number | undefined;
|
|
217
|
+
/**
|
|
218
|
+
* Outer-ring diameter in CSS pixels. When set, the instrument renders at a
|
|
219
|
+
* fixed intrinsic size derived from the ring, arc shape and label reserve —
|
|
220
|
+
* so instruments sharing the same value have identical ring circumference
|
|
221
|
+
* regardless of label width or arc extent (like obc-donut-chart's
|
|
222
|
+
* fixedHeight). When unset (default), the instrument fills its container.
|
|
223
|
+
*/
|
|
224
|
+
@property({type: Number, attribute: 'face-diameter'})
|
|
225
|
+
faceDiameter: number | undefined;
|
|
215
226
|
@property({type: Array, attribute: false}) areas: WatchArea[] = [];
|
|
216
227
|
@property({type: Array, attribute: false}) barAreas: WatchBarArea[] = [];
|
|
217
228
|
@property({type: Array, attribute: false}) needles: WatchNeedle[] = [];
|
|
@@ -250,17 +261,25 @@ export class ObcWatch extends LitElement {
|
|
|
250
261
|
@property({type: Number}) scaleWindIcon: number = 1;
|
|
251
262
|
@property({type: Number}) rotation: number | undefined;
|
|
252
263
|
@property({type: Boolean}) zoomToFitArc: boolean = false;
|
|
264
|
+
/** Pre-computed zoom-to-fit arc frame. When set, the watch skips its own `computeZoomToFitArcFrame()` call and uses these values directly. Consumer instruments (e.g. rudder, instrument-radial) should compute the frame once and pass it here to avoid redundant computation. If you pass `arcFrame`, you own keeping it in sync with `areas` / `watchCircleType` — obc-watch will NOT recompute it, so a stale frame renders stale geometry. */
|
|
253
265
|
@property({attribute: false}) arcFrame: ZoomToFitArcFrame | undefined;
|
|
254
266
|
@property({type: Number}) tickFadeAngle: number = 0;
|
|
255
267
|
|
|
268
|
+
/** ROT visualization type: `'dots'` (spinning dots) or `'bar'` (arc bar with clipped dots). Undefined hides the ROT layer. */
|
|
256
269
|
@property({type: String}) rotType: RotType | undefined;
|
|
270
|
+
/** Track on which ROT elements are placed: `'scale'` (on the outer ring) or `'innerCircle'` (default, inside the inner ring) */
|
|
257
271
|
@property({type: String}) rotPosition: RotPosition = RotPosition.innerCircle;
|
|
272
|
+
/** Start angle of the ROT bar arc in degrees (0° = 12 o'clock, clockwise). Only used when `rotType` is `'bar'`. */
|
|
258
273
|
@property({type: Number}) rotStartAngle: number = 0;
|
|
274
|
+
/** End angle of the ROT bar arc in degrees. The bar is hidden when the difference from `rotStartAngle` is less than 0.1°. */
|
|
259
275
|
@property({type: Number}) rotEndAngle: number = 0;
|
|
276
|
+
/** Override priority for ROT color derivation. When set, ROT colors use this instead of the main `priority`. Useful when the ROT element has independent priority (e.g. compass per-element priority). */
|
|
260
277
|
@property({type: String}) rotPriority: Priority | undefined;
|
|
261
278
|
@property({type: Boolean}) rotPortStarboard: boolean = false;
|
|
262
279
|
@property({type: Number}) rotAtZeroDeadband: number = ROT_ZERO_DEADBAND_DEG;
|
|
280
|
+
/** Measured rate of turn in degrees per minute (the maritime/AIS convention, see ES-TRIN 2025/1 Art. 3.02 and ITU-R M.1371). Sign controls direction (positive = starboard/clockwise). When defined, this drives both the dot animation (multiplied by `rotDotAnimationFactor`) and the port/starboard direction sign. */
|
|
263
281
|
@property({type: Number}) rateOfTurnDegreesPerMinute: number | undefined;
|
|
282
|
+
/** Visual amplification factor applied only to the spinning-dot animation (not to bar extent). Default `18` keeps the legacy visual feel (≈1 rpm at 20°/min). */
|
|
264
283
|
@property({type: Number}) rotDotAnimationFactor: number = 18;
|
|
265
284
|
/**
|
|
266
285
|
* @deprecated Use `rateOfTurnDegreesPerMinute` (and optionally `rotDotAnimationFactor`) instead.
|
|
@@ -271,6 +290,7 @@ export class ObcWatch extends LitElement {
|
|
|
271
290
|
set rotationsPerMinute(value: number) {
|
|
272
291
|
this._legacyRotationsPerMinute = value;
|
|
273
292
|
}
|
|
293
|
+
/** Legacy spin speed of the ROT dot ring, in rotations per minute (sign controls direction; positive = clockwise). */
|
|
274
294
|
get rotationsPerMinute() {
|
|
275
295
|
return this._legacyRotationsPerMinute;
|
|
276
296
|
}
|
|
@@ -293,10 +313,13 @@ export class ObcWatch extends LitElement {
|
|
|
293
313
|
return this._legacyRotationsPerMinute;
|
|
294
314
|
}
|
|
295
315
|
|
|
296
|
-
// @ts-expect-error TS6133: The controller ensures that the render
|
|
297
|
-
// function is called on resize of the element
|
|
298
316
|
private _resizeController = new ResizeController(this, {});
|
|
299
317
|
|
|
318
|
+
override firstUpdated(changed: PropertyValues): void {
|
|
319
|
+
super.firstUpdated(changed);
|
|
320
|
+
observeInnerBox(this._resizeController, this.renderRoot);
|
|
321
|
+
}
|
|
322
|
+
|
|
300
323
|
override willUpdate(changed: PropertyValues): void {
|
|
301
324
|
super.willUpdate(changed);
|
|
302
325
|
|
|
@@ -333,6 +356,11 @@ export class ObcWatch extends LitElement {
|
|
|
333
356
|
|
|
334
357
|
override updated(changed: PropertyValues): void {
|
|
335
358
|
super.updated(changed);
|
|
359
|
+
this._hostSizePinned = applyPinnedHostSize(
|
|
360
|
+
this,
|
|
361
|
+
this.arcFrame ? undefined : this._ownFrame,
|
|
362
|
+
this._hostSizePinned
|
|
363
|
+
);
|
|
336
364
|
const el = this.rotType
|
|
337
365
|
? this.renderRoot.querySelector('#rot-spinner')
|
|
338
366
|
: null;
|
|
@@ -356,6 +384,18 @@ export class ObcWatch extends LitElement {
|
|
|
356
384
|
|
|
357
385
|
private _rOff = 0;
|
|
358
386
|
|
|
387
|
+
/**
|
|
388
|
+
* Set by the frame when the label reserve exceeded its cap: tick label
|
|
389
|
+
* texts are dropped instead of clipped (see radial-frame.ts).
|
|
390
|
+
*/
|
|
391
|
+
private _labelsHidden = false;
|
|
392
|
+
|
|
393
|
+
/** The frame of the last render, when computed internally (no arcFrame). */
|
|
394
|
+
private _ownFrame: RadialFrame | undefined;
|
|
395
|
+
|
|
396
|
+
/** Whether the host size styles were set by applyPinnedHostSize. */
|
|
397
|
+
private _hostSizePinned = false;
|
|
398
|
+
|
|
359
399
|
/**
|
|
360
400
|
* Radius for a dial-band edge under zoom: additive (`base + _rOff`), keeping
|
|
361
401
|
* band thickness constant in SVG units. INVARIANT: every band-edge radius
|
|
@@ -680,16 +720,8 @@ export class ObcWatch extends LitElement {
|
|
|
680
720
|
}
|
|
681
721
|
|
|
682
722
|
private getScale({width, height}: {width: number; height: number}): number {
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
if (clientWidth === 0 || clientHeight === 0) {
|
|
686
|
-
const box = this.parentElement?.getBoundingClientRect();
|
|
687
|
-
if (box) {
|
|
688
|
-
clientWidth = box.width;
|
|
689
|
-
clientHeight = box.height;
|
|
690
|
-
}
|
|
691
|
-
}
|
|
692
|
-
const scale = Math.min(clientWidth / width, clientHeight / height);
|
|
723
|
+
const container = measureContainerPx(this);
|
|
724
|
+
const scale = Math.min(container.width / width, container.height / height);
|
|
693
725
|
// On first paint the element and its parent can both still be zero-sized, so
|
|
694
726
|
// the scale is 0 (or non-finite). That value flows into `px / scale` label
|
|
695
727
|
// math and yields ±Infinity coordinates the browser rejects. Fall back to a
|
|
@@ -701,17 +733,19 @@ export class ObcWatch extends LitElement {
|
|
|
701
733
|
return scale;
|
|
702
734
|
}
|
|
703
735
|
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
736
|
+
/**
|
|
737
|
+
* Pixel width of the widest outside label, feeding the frame's label
|
|
738
|
+
* reserve (issue #1021). Explicit `padding` is a hard geometry override
|
|
739
|
+
* and disables the reserve, preserving legacy consumer output.
|
|
740
|
+
*/
|
|
741
|
+
private getLabelWidthPx(): number {
|
|
742
|
+
if (this.padding !== undefined || this.tickmarksInside) {
|
|
743
|
+
return 0;
|
|
707
744
|
}
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
this.tickmarks.some((t) => t.text !== undefined);
|
|
711
|
-
if (hasTickmarksWithText && !this.tickmarksInside) {
|
|
712
|
-
return 24 * 2.5;
|
|
745
|
+
if (this.showLabels) {
|
|
746
|
+
return NSWE_LABEL_WIDTH_PX;
|
|
713
747
|
}
|
|
714
|
-
return
|
|
748
|
+
return estimateLabelWidthPx(this.tickmarks.map((t) => t.text));
|
|
715
749
|
}
|
|
716
750
|
|
|
717
751
|
override render() {
|
|
@@ -721,31 +755,33 @@ export class ObcWatch extends LitElement {
|
|
|
721
755
|
|
|
722
756
|
if (this.arcFrame) {
|
|
723
757
|
this._rOff = this.arcFrame.radiusOffset;
|
|
758
|
+
this._labelsHidden = false;
|
|
759
|
+
this._ownFrame = undefined;
|
|
724
760
|
width = this.arcFrame.width;
|
|
725
761
|
height = this.arcFrame.height;
|
|
726
762
|
viewBox = this.arcFrame.viewBox;
|
|
727
|
-
} else
|
|
728
|
-
const
|
|
729
|
-
|
|
730
|
-
|
|
763
|
+
} else {
|
|
764
|
+
const frame = computeRadialFrame({
|
|
765
|
+
basePadding: this.padding ?? 24,
|
|
766
|
+
labelWidthPx: this.getLabelWidthPx(),
|
|
767
|
+
clips: {
|
|
768
|
+
top: this.clipTop,
|
|
769
|
+
bottom: this.clipBottom,
|
|
770
|
+
left: this.clipLeft,
|
|
771
|
+
right: this.clipRight,
|
|
772
|
+
},
|
|
773
|
+
containerPx: measureContainerPx(this),
|
|
774
|
+
faceDiameter: this.faceDiameter,
|
|
775
|
+
zoomToFitArc: this.zoomToFitArc,
|
|
731
776
|
areas: this.areas,
|
|
732
|
-
outerRadius: OUTER_RING_RADIUS,
|
|
733
777
|
innerRadius: this.innerRingRadius,
|
|
734
|
-
extension: ext,
|
|
735
|
-
targetSize,
|
|
736
778
|
});
|
|
737
779
|
this._rOff = frame.radiusOffset;
|
|
780
|
+
this._labelsHidden = frame.labelsHidden;
|
|
781
|
+
this._ownFrame = frame;
|
|
738
782
|
width = frame.width;
|
|
739
783
|
height = frame.height;
|
|
740
784
|
viewBox = frame.viewBox;
|
|
741
|
-
} else {
|
|
742
|
-
this._rOff = 0;
|
|
743
|
-
const full = (176 + this.getPadding()) * 2;
|
|
744
|
-
width = full * (1 - this.clipLeft / 100 - this.clipRight / 100);
|
|
745
|
-
height = full * (1 - this.clipTop / 100 - this.clipBottom / 100);
|
|
746
|
-
const left = -full / 2 + (full * this.clipLeft) / 100;
|
|
747
|
-
const top = -full / 2 + (full * this.clipTop) / 100;
|
|
748
|
-
viewBox = `${left} ${top} ${width} ${height}`;
|
|
749
785
|
}
|
|
750
786
|
|
|
751
787
|
const rOff = this._rOff;
|
|
@@ -764,7 +800,7 @@ export class ObcWatch extends LitElement {
|
|
|
764
800
|
size: t.type,
|
|
765
801
|
style: this.tickmarkStyle,
|
|
766
802
|
scale,
|
|
767
|
-
text: this.showLabels ? undefined : t.text,
|
|
803
|
+
text: this.showLabels || this._labelsHidden ? undefined : t.text,
|
|
768
804
|
inside: this.tickmarksInside,
|
|
769
805
|
textRadius,
|
|
770
806
|
rotation: this.rotation,
|
|
@@ -778,10 +814,14 @@ export class ObcWatch extends LitElement {
|
|
|
778
814
|
? this.advices.map((a) => renderAdvice(a, rOff))
|
|
779
815
|
: nothing;
|
|
780
816
|
|
|
781
|
-
// Compute label positions once – used for both rendering and crosshair
|
|
782
|
-
|
|
817
|
+
// Compute label positions once – used for both rendering and crosshair
|
|
818
|
+
// knockout. NSWE labels and the north arrow are px-fixed outside decor,
|
|
819
|
+
// so they follow the same labelsHidden degradation as tick label texts.
|
|
820
|
+
const showNsweLabels = this.showLabels && !this._labelsHidden;
|
|
821
|
+
const showNorthArrow = this.northArrow && !this._labelsHidden;
|
|
822
|
+
const insideLabels = this.tickmarksInside && showNsweLabels;
|
|
783
823
|
const includeNorth = !this.northArrow;
|
|
784
|
-
const labelPositions =
|
|
824
|
+
const labelPositions = showNsweLabels
|
|
785
825
|
? getLabelPositions({
|
|
786
826
|
scale,
|
|
787
827
|
inside: this.tickmarksInside,
|
|
@@ -799,7 +839,7 @@ export class ObcWatch extends LitElement {
|
|
|
799
839
|
includeNorth,
|
|
800
840
|
})
|
|
801
841
|
: nothing;
|
|
802
|
-
const northArrowEl =
|
|
842
|
+
const northArrowEl = showNorthArrow
|
|
803
843
|
? renderNorthArrow({
|
|
804
844
|
scale,
|
|
805
845
|
rotation: this.rotation,
|
package/src/storybook-util.ts
CHANGED
|
@@ -98,6 +98,16 @@ export function widthDecorator(
|
|
|
98
98
|
story: () => unknown,
|
|
99
99
|
context: {args: {width?: number; height?: number}}
|
|
100
100
|
): HTMLTemplateResult {
|
|
101
|
+
// Stories that manage their own container (e.g. user-resizable sizing
|
|
102
|
+
// playgrounds) opt out with `parameters: {widthDecorator: false}` — the
|
|
103
|
+
// fixed-size overflow:auto wrapper would fence in their resize handle.
|
|
104
|
+
// (Read via a cast so the signature stays assignable to Storybook's
|
|
105
|
+
// DecoratorFunction for every Meta typing in the repo.)
|
|
106
|
+
const parameters = (context as {parameters?: {widthDecorator?: boolean}})
|
|
107
|
+
.parameters;
|
|
108
|
+
if (parameters?.widthDecorator === false) {
|
|
109
|
+
return html`${story()}`;
|
|
110
|
+
}
|
|
101
111
|
const width = context.args.width ?? 300;
|
|
102
112
|
const height = context.args.height ?? width;
|
|
103
113
|
return html` <div
|
|
@@ -107,3 +117,52 @@ export function widthDecorator(
|
|
|
107
117
|
${story()}
|
|
108
118
|
</div>`;
|
|
109
119
|
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* User-resizable container for sizing-playground stories: drag the
|
|
123
|
+
* bottom-right corner and watch the content adapt. CSS `resize` only works
|
|
124
|
+
* with a non-visible `overflow`, hence `overflow: auto` on the box itself.
|
|
125
|
+
*/
|
|
126
|
+
export function resizableStoryBox(
|
|
127
|
+
content: unknown,
|
|
128
|
+
{width = 560, height = 320}: {width?: number; height?: number} = {}
|
|
129
|
+
): HTMLTemplateResult {
|
|
130
|
+
return html`<div
|
|
131
|
+
style="resize: both; overflow: auto; border: 1px dashed var(--instrument-frame-tertiary-color, gray); width: ${width}px; height: ${height}px; display: flex; gap: 8px; align-items: stretch;"
|
|
132
|
+
>
|
|
133
|
+
${content}
|
|
134
|
+
</div>`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* One captioned column inside a sizing-playground flex row. A `pinned`
|
|
139
|
+
* instrument sets its own intrinsic size (faceDiameter), so its column
|
|
140
|
+
* shrink-wraps; an adaptive one gets an equal share of the remaining space.
|
|
141
|
+
*/
|
|
142
|
+
export function playgroundColumn(
|
|
143
|
+
caption: string,
|
|
144
|
+
content: unknown,
|
|
145
|
+
{pinned = false}: {pinned?: boolean} = {}
|
|
146
|
+
): HTMLTemplateResult {
|
|
147
|
+
return html`<div
|
|
148
|
+
style="flex: ${pinned
|
|
149
|
+
? '0 0 auto'
|
|
150
|
+
: '1 1 0'}; min-width: 0; display: flex; flex-direction: column; gap: 2px;"
|
|
151
|
+
>
|
|
152
|
+
<span
|
|
153
|
+
style="flex: 0 0 auto; font-family: var(--font-family-main, sans-serif); font-size: 11px; color: var(--instrument-frame-secondary-color, gray); white-space: nowrap;"
|
|
154
|
+
>
|
|
155
|
+
${caption}
|
|
156
|
+
</span>
|
|
157
|
+
<div style="flex: 1 1 0; min-height: 0;">${content}</div>
|
|
158
|
+
</div>`;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Short explanatory line rendered above a sizing-playground box. */
|
|
162
|
+
export function storyHint(text: string): HTMLTemplateResult {
|
|
163
|
+
return html`<p
|
|
164
|
+
style="max-width: 70ch; margin: 0 0 8px; font-family: var(--font-family-main, sans-serif); font-size: 12px; color: var(--instrument-frame-secondary-color, gray);"
|
|
165
|
+
>
|
|
166
|
+
${text}
|
|
167
|
+
</p>`;
|
|
168
|
+
}
|