@oicl/openbridge-webcomponents 2.0.0-next.161 → 2.0.0-next.163

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 (33) hide show
  1. package/bundle/openbridge-webcomponents.bundle.js +439 -327
  2. package/bundle/openbridge-webcomponents.bundle.js.map +1 -1
  3. package/custom-elements.json +96 -29
  4. package/dist/components/keyboard-numeric/keyboard-numeric.css.js +2 -3
  5. package/dist/components/keyboard-numeric/keyboard-numeric.css.js.map +1 -1
  6. package/dist/navigation-instruments/compass-sector/compass-sector.css.js +20 -11
  7. package/dist/navigation-instruments/compass-sector/compass-sector.css.js.map +1 -1
  8. package/dist/navigation-instruments/compass-sector/compass-sector.d.ts +18 -17
  9. package/dist/navigation-instruments/compass-sector/compass-sector.d.ts.map +1 -1
  10. package/dist/navigation-instruments/compass-sector/compass-sector.js +85 -52
  11. package/dist/navigation-instruments/compass-sector/compass-sector.js.map +1 -1
  12. package/dist/navigation-instruments/gauge-radial/gauge-radial.css.js +2 -6
  13. package/dist/navigation-instruments/gauge-radial/gauge-radial.css.js.map +1 -1
  14. package/dist/navigation-instruments/rudder/rudder.css.js +40 -0
  15. package/dist/navigation-instruments/rudder/rudder.css.js.map +1 -0
  16. package/dist/navigation-instruments/rudder/rudder.d.ts +9 -0
  17. package/dist/navigation-instruments/rudder/rudder.d.ts.map +1 -1
  18. package/dist/navigation-instruments/rudder/rudder.js +40 -23
  19. package/dist/navigation-instruments/rudder/rudder.js.map +1 -1
  20. package/dist/navigation-instruments/watch/watch.css.js +6 -0
  21. package/dist/navigation-instruments/watch/watch.css.js.map +1 -1
  22. package/dist/navigation-instruments/watch/watch.d.ts.map +1 -1
  23. package/dist/navigation-instruments/watch/watch.js +11 -1
  24. package/dist/navigation-instruments/watch/watch.js.map +1 -1
  25. package/dist/svghelpers/arc-frame.d.ts +8 -0
  26. package/dist/svghelpers/arc-frame.d.ts.map +1 -1
  27. package/dist/svghelpers/arc-frame.js +16 -6
  28. package/dist/svghelpers/arc-frame.js.map +1 -1
  29. package/dist/svghelpers/radial-frame.d.ts +14 -1
  30. package/dist/svghelpers/radial-frame.d.ts.map +1 -1
  31. package/dist/svghelpers/radial-frame.js +3 -1
  32. package/dist/svghelpers/radial-frame.js.map +1 -1
  33. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"watch.js","sources":["../../../src/navigation-instruments/watch/watch.ts"],"sourcesContent":["import {\n LitElement,\n PropertyValues,\n SVGTemplateResult,\n html,\n nothing,\n svg,\n unsafeCSS,\n} from 'lit';\nimport {property, state} from 'lit/decorators.js';\nimport {circle} from '../../svghelpers/index.js';\nimport {roundedArch} from '../../svghelpers/roundedArch.js';\nimport {\n cssSafeAngle,\n deriveRadialSetpointConfig,\n drawSetpointMarker,\n getSetpointAnimationDurationMs,\n getSetpointOutwardOffset,\n RADIAL_SETPOINT_RADIUS,\n SetpointVisualState,\n SETPOINT_ANIMATION_CSS_VAR,\n SETPOINT_ANIMATION_DURATION_DEFAULT,\n} from '../../svghelpers/setpoint.js';\nimport {InstrumentState, Priority} from '../types.js';\nimport compentStyle from './watch.css?inline';\nimport {ResizeController} from '@lit-labs/observers/resize-controller.js';\nimport {adviceMask, AngleAdviceRaw, renderAdvice} from './advice.js';\nimport {Tickmark, TickmarkStyle, tickmark} from './tickmark.js';\nexport {TickmarkStyle};\nimport {\n RotType,\n RotPosition,\n renderRotDots,\n renderRotBarStatic,\n renderRotBarDots,\n shortestAngularDeltaDeg,\n renderRotZeroPill,\n rotBarThresholdAngle,\n ROT_ZERO_DEADBAND_DEG,\n} from '../rate-of-turn/rot-renderer.js';\nexport {RotType, RotPosition};\nimport {\n RateOfTurnController,\n disposeRotController,\n} from '../rate-of-turn/rate-of-turn.controller.js';\nimport {\n renderLabels,\n renderNorthArrow,\n renderNorthMarker,\n getLabelPositions,\n LabelPosition,\n} from './label.js';\nimport {VesselImage, VesselImageSize, vesselImages} from './vessel.js';\nimport {\n renderCurrent,\n renderCurrentCentered,\n renderWind,\n WIND_ICON_TIP_TO_BOX_INNER,\n} from './environment.js';\nimport {customElement} from '../../decorator.js';\nimport {type ZoomToFitArcFrame} from '../../svghelpers/arc-frame.js';\nimport {degToRad, normalizeAngle} from '../../svghelpers/math.js';\nimport {\n applyPinnedHostSize,\n computeRadialFrame,\n estimateLabelWidthPx,\n measureContainerPx,\n NSWE_LABEL_WIDTH_PX,\n observeInnerBox,\n type RadialFrame,\n} from '../../svghelpers/radial-frame.js';\nexport {VesselImage, VesselImageSize, vesselImages};\n\nexport enum WatchCircleType {\n single = 'single',\n double = 'double',\n doubleThin = 'doubleThin',\n triple = 'triple',\n}\n\nexport interface WatchArea {\n startAngle: number;\n endAngle: number;\n roundOutsideCut: boolean;\n roundInsideCut: boolean;\n /** Corner fillet radius of the rounded cuts; `roundedArch`'s default when unset. */\n roundRadius?: number;\n /** Outline the roundBandCuts band arch with the tertiary frame color instead of self-colored. */\n outlined?: boolean;\n}\n\nexport interface WatchBarArea {\n startAngle: number;\n endAngle: number;\n fillColor: string;\n /** Outer band radius override; defaults to the second ring (160). */\n outerRadius?: number;\n /** Inner band radius override; defaults to the third ring (112). */\n innerRadius?: number;\n}\n\nexport interface WatchNeedle {\n angle: number;\n fillColor: string;\n strokeColor: string;\n /** Radial length of the needle pill; defaults to 48 (the full band). */\n length?: number;\n}\n\nexport interface WatchVessel {\n size: VesselImageSize;\n transform: string;\n vesselImage: VesselImage;\n}\n\nexport const OUTER_RING_RADIUS = 368 / 2;\nconst RING2_RADIUS = 320 / 2;\nconst RING3_RADIUS = 224 / 2;\nconst RING3B_RADIUS = 272 / 2;\nconst RING4_RADIUS = 176 / 2;\n\n/**\n * Split-band lane geometry (the design's \"pitch-rpm\" subdivision of the\n * 112..160 band): thin secondary line, bare-face gap, narrowed primary\n * lane. Shared with the `secondary-lane.ts` helpers.\n */\nexport const BAND_INNER_RADIUS = RING3_RADIUS;\nexport const BAND_OUTER_RADIUS = RING2_RADIUS;\nexport const SECONDARY_LINE_WIDTH = 8;\nexport const LANE_DIVIDER_WIDTH = 8;\nexport const SECONDARY_LANE_RADIUS =\n BAND_INNER_RADIUS + SECONDARY_LINE_WIDTH / 2;\nexport const LANE_DIVIDER_RADIUS =\n BAND_INNER_RADIUS + SECONDARY_LINE_WIDTH + LANE_DIVIDER_WIDTH / 2;\nexport const PRIMARY_SUBBAND_INNER_RADIUS =\n BAND_INNER_RADIUS + SECONDARY_LINE_WIDTH + LANE_DIVIDER_WIDTH;\nexport const PRIMARY_SUBBAND_NEEDLE_LENGTH =\n BAND_OUTER_RADIUS - PRIMARY_SUBBAND_INNER_RADIUS;\n\nexport function innerRingRadiusFor(type: WatchCircleType): number {\n switch (type) {\n case WatchCircleType.single:\n return RING2_RADIUS;\n case WatchCircleType.double:\n return RING3_RADIUS;\n case WatchCircleType.doubleThin:\n return RING3B_RADIUS;\n case WatchCircleType.triple:\n return RING4_RADIUS;\n default:\n throw new Error(`Unknown WatchCircleType: ${type as string}`);\n }\n}\n\nconst RADIAL_SETPOINT_INWARD_ADJUST = 4;\n\n/**\n * Split-band bars whose end sits on a sector cut overshoot it by this much;\n * the lane-arch track mask then trims them flush with the cut's fillet.\n */\nconst SPLIT_BAR_CUT_OVERSHOOT_DEG = 4;\n\n/**\n * Default anchors for the wind/current icons outside the outer ring (the\n * compass layout): the 48-unit icon box sits 4 units off the ring, spanning\n * 188–236 (OpenBridge 6.1 compass, node 19846-157200). The current chevron\n * anchors by its tip at the box inner edge; the wind anchor adds the glyph's\n * inward overhang so its box starts at the same edge.\n */\nconst CURRENT_ICON_OUTSIDE_RADIUS = OUTER_RING_RADIUS + 4;\nconst WIND_ICON_OUTSIDE_RADIUS =\n CURRENT_ICON_OUTSIDE_RADIUS + WIND_ICON_TIP_TO_BOX_INNER;\n\n/**\n * `<obc-watch>` - Core SVG renderer for circular/radial watch-based instruments.\n *\n * This component renders all circular instrument elements including rings, tickmarks,\n * bar areas, needles, advices, setpoints, vessel images, and environmental indicators\n * (wind/current). It serves as the foundation for compass, heading, rudder, speed-gauge,\n * and other radial navigation instruments.\n *\n * ## Setpoint Behavior\n *\n * The setpoint marker visual state is derived from the combination of `atAngleSetpoint`,\n * `angleSetpoint`, and `angleSetpointAtZeroDeadband` properties:\n *\n * - **notEqual**: Value differs from setpoint (triangular marker, offset outward)\n * - **equal**: Value matches setpoint (line marker, sits on ring)\n * - **equalZero**: Value matches setpoint at zero angle (double-line marker, offset outward)\n * - **focus**: User is actively adjusting via `newAngleSetpoint` - shows focus visual state\n *\n * ## newAngleSetpoint Pattern\n *\n * When `newAngleSetpoint` is defined, TWO setpoint markers are rendered:\n * 1. Original marker at `angleSetpoint` - dimmed (0.75 opacity)\n * 2. New marker at `newAngleSetpoint` - focus visual state, full opacity\n *\n * This enables the \"adjustment preview\" UX where users can see both the current\n * and proposed setpoint positions simultaneously.\n *\n * The `RADIAL_SETPOINT_INWARD_ADJUST` constant (4px) fine-tunes radial setpoint positioning\n * to match Figma designs, applied on top of visual state offsets from setpoint.ts.\n *\n * The `colorMode` property allows overriding the derived color mode (enhanced for enhanced priority,\n * regular for other states).\n *\n * ## Setpoint Animation (`animateSetpoint`)\n *\n * When `animateSetpoint` is true and a confirm occurs (`newAngleSetpoint` → `undefined`):\n * - The original setpoint slides to the new position via CSS transition\n * - The departing new-setpoint marker fades out\n * - Angular transitions always take the shortest path via accumulated\n * CSS-safe angles (`cssSafeAngle()`), so even 350° → 10° animates +20°\n *\n * Duration: `var(--setpoint-animation-duration, 300ms)`\n *\n * Internally, `_departingNewAngleSetpoint` captures the departing angle during confirm\n * fade-out and `_animationTimer` auto-clears it after the animation duration.\n * `_setpointCssAngle` tracks the accumulated CSS angle to avoid long-way-around\n * transitions across the 0°/360° boundary.\n *\n * @property state - Instrument state (active, loading, off)\n * @property priority - Color priority (enhanced = blue palette, regular = gray palette)\n * @property hasBackgroundCircle - When `true`, the full watch-face circle is drawn behind the dial —\n * a frame-primary fill with a tertiary outline — so partial-sector\n * instruments still read as a complete circle. In the `off` state the fill\n * is omitted (only the outline remains), keeping the off skeleton hollow.\n * With sector `areas`, the per-area arch outline is dropped; the face\n * outline takes its place.\n * @property northMarker - Replace the north arrow with the compact heading-up north marker: a small\n * triangle on the outer ring with an upright \"N\" at the outside label\n * radius. Rotates with the card via `rotation`.\n * @availableWhen northMarker northArrow==true\n * @property angleSetpoint - Setpoint angle in degrees (0° = 12 o'clock)\n * @property newAngleSetpoint - New setpoint being adjusted (focus mode)\n * @property atAngleSetpoint - Whether value matches setpoint (within deadband)\n * @property angleSetpointAtZeroDeadband - Deadband for zero detection (default 0.5°)\n * @property setpointOverride - Override to derive setpoint color from priority regardless of state\n * @property padding - Explicit padding override in SVG units: the un-zoomed viewBox becomes\n * exactly `(176 + padding) * 2`. Setting it disables the automatic\n * width-aware label reserve (issue #1021) — the caller owns label room.\n * @property faceDiameter - Outer-ring diameter in CSS pixels. When set, the instrument renders at a\n * fixed intrinsic size derived from the ring, arc shape and label reserve —\n * so instruments sharing the same value have identical ring circumference\n * regardless of label width or arc extent (like obc-donut-chart's\n * fixedHeight). When unset (default), the instrument fills its container.\n * @property roundBandCuts - Rounds the band track's sector end cuts at the band's own radii and clips\n * bars to that rounded track shape (the design's track-mask model), instead\n * of the joint silhouette cut shared with the scale ring. No effect without\n * `areas`.\n * @property splitBand - With `watchCircleType: double`, render the band as the split\n * primary/secondary lane tracks (secondary-lane geometry) instead of the\n * joint band; bars clip to the primary lane. Takes precedence over\n * `roundBandCuts` for the band.\n * @property crosshairCenterCutout - Cuts the crosshair out inside the inner ring, so center content (e.g.\n * center readouts) sits on a clean face.\n * @availableWhen crosshairCenterCutout crosshairEnabled==true\n * @property insideLabelsFlush - With `tickmarksInside`, anchor the NSEW label boxes flush against the\n * inner ring (px-fixed) instead of the legacy gap that lets them drift\n * toward the centre as the instrument shrinks.\n * @availableWhen insideLabelsFlush tickmarksInside==true\n * @property currentIconCentered - Render the current icon centered on the face (direction-type layout)\n * instead of at `currentSymbolRadius` on the periphery.\n * @availableWhen currentIconCentered current!=null && currentFromDirectionDeg!=null\n * @property scaleCurrentIcon - Scale factor for the centered current icon.\n * @availableWhen scaleCurrentIcon currentIconCentered==true\n * @property clipTop - Top clip, % of height. Ignored when `zoomToFitArc` is true.\n * @property clipBottom - Bottom clip, % of height. Ignored when `zoomToFitArc` is true.\n * @property clipLeft - Left clip, % of width — horizontal counterpart of clipTop/bottom (90° sectors). Ignored when `zoomToFitArc`.\n * @property clipRight - Right clip, % of width — horizontal counterpart of clipTop/bottom (90° sectors). Ignored when `zoomToFitArc`.\n * @property endLabelsMaxMin - Place the horizontal end labels (±90°, e.g. min/max) below the tick instead\n * of beside it. This is the \"Max-min\" label placement from the radial label\n * model (External / Internal / Max-min) — see PR #903 / design discussion.\n * Currently only used by the 180° sector of `obc-gauge-radial`.\n * @property 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.\n * @property rotType - ROT visualization type: `'dots'` (spinning dots) or `'bar'` (arc bar with clipped dots). Undefined hides the ROT layer.\n * @property rotPosition - Track on which ROT elements are placed: `'scale'` (on the outer ring) or `'innerCircle'` (default, inside the inner ring)\n * @property rotStartAngle - Start angle of the ROT bar arc in degrees (0° = 12 o'clock, clockwise). Only used when `rotType` is `'bar'`.\n * @property rotEndAngle - End angle of the ROT bar arc in degrees. The bar is hidden when the difference from `rotStartAngle` is less than 0.1°.\n * @property 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).\n * @property 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.\n * @property 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).\n * @experimental\n */\n@customElement('obc-watch')\nexport class ObcWatch extends LitElement {\n private _setpointId = `watch-setpoint-${Math.random().toString(36).slice(2, 9)}`;\n private _newSetpointId = `watch-new-setpoint-${Math.random().toString(36).slice(2, 9)}`;\n\n @property({type: String}) state: InstrumentState = InstrumentState.active;\n @property({type: String}) priority: Priority = Priority.regular;\n @property({type: String}) watchCircleType: WatchCircleType =\n WatchCircleType.single;\n @property({type: Boolean}) hasBackgroundCircle: boolean = false;\n @property({type: Boolean}) northArrow: boolean = false;\n @property({type: Boolean}) northArrowInside: boolean | undefined;\n @property({type: Boolean}) northMarker: boolean = false;\n @property({type: Number}) angleSetpoint: number | undefined;\n @property({type: Number}) newAngleSetpoint: number | undefined;\n @property({type: Boolean}) atAngleSetpoint: boolean = false;\n @property({type: Number}) angleSetpointAtZeroDeadband: number = 0.5;\n @property({type: Boolean}) setpointOverride: boolean = false;\n @property({type: Boolean}) touching: boolean = false;\n\n @property({type: Boolean}) animateSetpoint: boolean = false;\n\n @state() private _departingNewAngleSetpoint: number | undefined;\n private _animationTimer?: ReturnType<typeof setTimeout>;\n\n /**\n * Accumulated CSS-safe angle for the original setpoint marker.\n * Ensures CSS rotate() transitions always take the shortest path,\n * even across the 0°/360° boundary.\n */\n private _setpointCssAngle: number = 0;\n\n /** Whether the setpoint CSS angle has been initialised (to skip transition on first render). */\n private _setpointCssAngleInit = false;\n @property({type: Number}) padding: number | undefined;\n @property({type: Number, attribute: 'face-diameter'})\n faceDiameter: number | undefined;\n @property({type: Array, attribute: false}) areas: WatchArea[] = [];\n @property({type: Array, attribute: false}) barAreas: WatchBarArea[] = [];\n @property({type: Boolean}) roundBandCuts: boolean = false;\n @property({type: Boolean}) splitBand: boolean = false;\n @property({type: Array, attribute: false}) needles: WatchNeedle[] = [];\n @property({type: Array, attribute: false}) tickmarks: Tickmark[] = [];\n @property({type: Boolean}) tickmarksInside: boolean = false;\n @property({type: String}) tickmarkStyle: TickmarkStyle =\n TickmarkStyle.regular;\n @property({type: Array, attribute: false}) advices: AngleAdviceRaw[] = [];\n @property({type: Boolean}) crosshairEnabled: boolean = false;\n @property({type: Boolean}) crosshairCenterCutout: boolean = false;\n @property({type: Boolean}) showLabels: boolean = false;\n @property({type: Boolean}) insideLabelsFlush: boolean = false;\n @property({type: Array, attribute: false}) vessels: WatchVessel[] = [];\n @property({type: Number}) windKnots: number | null = null;\n @property({type: Number}) windFromDirectionDeg: number | null = null;\n @property({type: Number}) windSymbolRadius: number | null = null;\n @property({type: String}) windColor: string | undefined;\n @property({type: Number}) current: number | null = null;\n @property({type: Number}) currentFromDirectionDeg: number | null = null;\n @property({type: Number}) currentSymbolRadius: number | null = null;\n @property({type: String}) currentColor: string | undefined;\n @property({type: Boolean}) currentIconCentered: boolean = false;\n @property({type: Number}) scaleCurrentIcon: number = 1;\n @property({type: Boolean}) starboardPortIndicator: boolean = false;\n @property({type: Number}) clipTop: number = 0;\n @property({type: Number}) clipBottom: number = 0;\n @property({type: Number}) clipLeft: number = 0;\n @property({type: Number}) clipRight: number = 0;\n @property({type: Boolean}) endLabelsMaxMin: boolean = false;\n @property({type: Number}) scaleWindIcon: number = 1;\n @property({type: Number}) rotation: number | undefined;\n @property({type: Boolean}) zoomToFitArc: boolean = false;\n @property({attribute: false}) arcFrame: ZoomToFitArcFrame | undefined;\n @property({type: Number}) tickFadeAngle: number = 0;\n\n @property({type: String}) rotType: RotType | undefined;\n @property({type: String}) rotPosition: RotPosition = RotPosition.innerCircle;\n @property({type: Number}) rotStartAngle: number = 0;\n @property({type: Number}) rotEndAngle: number = 0;\n @property({type: String}) rotPriority: Priority | undefined;\n @property({type: Boolean}) rotPortStarboard: boolean = false;\n @property({type: Number}) rotAtZeroDeadband: number = ROT_ZERO_DEADBAND_DEG;\n @property({type: Number}) rateOfTurnDegreesPerMinute: number | undefined;\n @property({type: Number}) rotDotAnimationFactor: number = 18;\n /**\n * @deprecated Use `rateOfTurnDegreesPerMinute` (and optionally `rotDotAnimationFactor`) instead.\n * Kept as a backward-compatible alias; takes effect only when\n * `rateOfTurnDegreesPerMinute` is `undefined`.\n */\n @property({type: Number})\n set rotationsPerMinute(value: number) {\n this._legacyRotationsPerMinute = value;\n }\n /** Legacy spin speed of the ROT dot ring, in rotations per minute (sign controls direction; positive = clockwise). */\n get rotationsPerMinute() {\n return this._legacyRotationsPerMinute;\n }\n private _legacyRotationsPerMinute = 0;\n private _rotController?: RateOfTurnController;\n\n /**\n * Effective rotations-per-minute for the spinning dot animation and\n * port/starboard direction sign. Resolves to:\n * • `(rateOfTurnDegreesPerMinute / 360) * rotDotAnimationFactor` when the\n * new physical API is in use, OR\n * • the legacy `rotationsPerMinute` value otherwise.\n */\n private get _effectiveRpm(): number {\n if (this.rateOfTurnDegreesPerMinute != null) {\n return (\n (this.rateOfTurnDegreesPerMinute / 360) * this.rotDotAnimationFactor\n );\n }\n return this._legacyRotationsPerMinute;\n }\n\n private _resizeController = new ResizeController(this, {});\n\n override firstUpdated(changed: PropertyValues): void {\n super.firstUpdated(changed);\n observeInnerBox(this._resizeController, this.renderRoot);\n }\n\n override willUpdate(changed: PropertyValues): void {\n super.willUpdate(changed);\n\n // Push the resolved effective RPM into the live controller whenever any\n // of the inputs that compose it change.\n if (\n this._rotController &&\n (changed.has('rateOfTurnDegreesPerMinute') ||\n changed.has('rotDotAnimationFactor') ||\n changed.has('rotationsPerMinute'))\n ) {\n this._rotController.rotationsPerMinute = this._effectiveRpm;\n }\n\n // Detect confirm: newAngleSetpoint was defined, now undefined\n if (changed.has('newAngleSetpoint') && this.animateSetpoint) {\n const prev = changed.get('newAngleSetpoint') as number | undefined;\n if (prev !== undefined && this.newAngleSetpoint === undefined) {\n this._departingNewAngleSetpoint = prev;\n clearTimeout(this._animationTimer);\n const duration = getSetpointAnimationDurationMs(this);\n this._animationTimer = setTimeout(() => {\n this._departingNewAngleSetpoint = undefined;\n }, duration);\n }\n }\n }\n\n override disconnectedCallback(): void {\n super.disconnectedCallback();\n clearTimeout(this._animationTimer);\n this._rotController = disposeRotController(this, this._rotController);\n }\n\n override updated(changed: PropertyValues): void {\n super.updated(changed);\n this._hostSizePinned = applyPinnedHostSize(\n this,\n this.arcFrame ? undefined : this._ownFrame,\n this._hostSizePinned\n );\n const el = this.rotType\n ? this.renderRoot.querySelector('#rot-spinner')\n : null;\n if (!el) {\n this._rotController = disposeRotController(this, this._rotController);\n return;\n }\n if (!this._rotController || this._rotController.el !== el) {\n this._rotController = disposeRotController(this, this._rotController);\n this._rotController = new RateOfTurnController(\n this,\n el,\n this._effectiveRpm\n );\n }\n }\n\n private get innerRingRadius(): number {\n return innerRingRadiusFor(this.watchCircleType);\n }\n\n private _rOff = 0;\n\n /**\n * Set by the frame when the label reserve exceeded its cap: tick label\n * texts are dropped instead of clipped (see radial-frame.ts).\n */\n private _labelsHidden = false;\n\n /** The frame of the last render, when computed internally (no arcFrame). */\n private _ownFrame: RadialFrame | undefined;\n\n /** Whether the host size styles were set by applyPinnedHostSize. */\n private _hostSizePinned = false;\n\n /**\n * Radius for a dial-band edge under zoom: additive (`base + _rOff`), keeping\n * band thickness constant in SVG units. INVARIANT: every band-edge radius\n * (rings, value arc, bars, needles, inside-label `textRadius`) must go through\n * this — mixing additive and multiplicative offsets misaligns ticks, labels,\n * advice masks and arcs. The proportional experiments were removed for this\n * reason (PR #903).\n */\n private _bandRadius(base: number): number {\n return base + this._rOff;\n }\n\n private watchCircle(): SVGTemplateResult | SVGTemplateResult[] {\n const rings = [];\n // The face circle is split: fill under the rings, outline over them. The\n // masked track band reaches exactly the face radius, so an outline drawn\n // underneath loses its inner half where the band overlaps and reads\n // thinner there than across the sector gap. The ring branch needs only\n // the fill — its outer ring already outlines the face.\n const faceFill =\n this.hasBackgroundCircle && this.state !== InstrumentState.off\n ? svg`\n <circle\n cx=\"0\"\n cy=\"0\"\n r=\"${this._bandRadius(OUTER_RING_RADIUS)}\"\n fill=\"var(--instrument-frame-primary-color)\"\n stroke=\"none\"\n />`\n : undefined;\n const faceOutline =\n this.hasBackgroundCircle && this.areas.length > 0\n ? svg`\n <circle\n cx=\"0\"\n cy=\"0\"\n r=\"${this._bandRadius(OUTER_RING_RADIUS)}\"\n fill=\"none\"\n stroke=\"var(--instrument-frame-tertiary-color)\"\n stroke-width=\"1\"\n vector-effect=\"non-scaling-stroke\"\n />`\n : undefined;\n if (this.state !== InstrumentState.off) {\n rings.push(svg`\n <circle\n cx=\"0\"\n cy=\"0\"\n r=\"${this._bandRadius(172)}\"\n stroke=\"var(--instrument-frame-primary-color)\"\n fill=\"none\"\n stroke-width=\"24\"\n />`);\n\n if (this.watchCircleType !== WatchCircleType.single) {\n const r1 = this._bandRadius(RING2_RADIUS);\n const r2 = this._bandRadius(\n this.watchCircleType === WatchCircleType.doubleThin\n ? RING3B_RADIUS\n : RING3_RADIUS\n );\n const r = (r1 + r2) / 2;\n const strokeWidth = r1 - r2;\n if (this.splitBand && this.watchCircleType === WatchCircleType.double) {\n rings.push(...this.splitBandTracks());\n } else if (this.roundBandCuts && this.areas.length > 0) {\n rings.push(\n ...this.areas.map(\n (area) =>\n svg`<path d=${roundedArch({\n startAngle: area.startAngle,\n endAngle: area.endAngle,\n R: r1,\n r: r2,\n roundOutsideCut: area.roundOutsideCut,\n roundInsideCut: area.roundInsideCut,\n roundRadius: area.roundRadius,\n })} fill=\"var(--instrument-frame-secondary-color)\" stroke=${\n area.outlined\n ? 'var(--instrument-frame-tertiary-color)'\n : 'var(--instrument-frame-secondary-color)'\n } stroke-width=\"1\" vector-effect=\"non-scaling-stroke\" />`\n )\n );\n } else {\n rings.push(\n svg`\n <circle cx=\"0\" cy=\"0\" r=${r} stroke=\"var(--instrument-frame-secondary-color)\" stroke-width=${strokeWidth} fill=\"none\" />\n <circle cx=\"0\" cy=\"0\" r=${r1} stroke=\"var(--instrument-frame-secondary-color)\" stroke-width=\"1\" fill=\"none\" vector-effect=\"non-scaling-stroke\" />\n <circle cx=\"0\" cy=\"0\" r=${r2} stroke=\"var(--instrument-frame-secondary-color)\" stroke-width=\"1\" fill=\"none\" vector-effect=\"non-scaling-stroke\" />\n `\n );\n }\n }\n if (this.watchCircleType === WatchCircleType.triple) {\n const r1 = this._bandRadius(RING3_RADIUS);\n const r2 = this._bandRadius(RING4_RADIUS);\n const r = (r1 + r2) / 2;\n const strokeWidth = r1 - r2;\n rings.push(\n svg`<circle cx=\"0\" cy=\"0\" r=${r} stroke=\"var(--instrument-frame-primary-color)\" stroke-width=${strokeWidth} fill=\"none\" />`\n );\n }\n }\n\n const maskSize = Math.max(200, OUTER_RING_RADIUS + this._rOff + 50);\n let result = rings;\n if (this.areas.length > 0) {\n const areas = this.areas.map((area) => {\n const svgPath = roundedArch({\n startAngle: area.startAngle,\n endAngle: area.endAngle,\n R: this._bandRadius(OUTER_RING_RADIUS),\n r: this._bandRadius(this.innerRingRadius),\n roundOutsideCut: area.roundOutsideCut,\n roundInsideCut: area.roundInsideCut,\n roundRadius: area.roundRadius,\n });\n return svgPath;\n });\n const mask = svg`<mask id=\"cutMask\">\n <rect x=\"${-maskSize}\" y=\"${-maskSize}\" width=\"${maskSize * 2}\" height=\"${maskSize * 2}\" fill=\"black\" />\n ${areas.map((area) => svg`<path d=${area} fill=\"white\" vector-effect=\"non-scaling-stroke\" stroke=\"white\" stroke-width=\"1\"/>`)}\n </mask>`;\n // clipPath for ROT uses r=0 so dots at any track radius stay visible\n const rotClip = svg`<clipPath id=\"rot-arc-clip\">${this.areas.map(\n (area) =>\n svg`<path d=${roundedArch({\n startAngle: area.startAngle,\n endAngle: area.endAngle,\n R: OUTER_RING_RADIUS + this._rOff + 20,\n r: 0,\n roundOutsideCut: area.roundOutsideCut,\n roundInsideCut: area.roundInsideCut,\n })} />`\n )}</clipPath>`;\n result = [\n mask,\n rotClip,\n ...(faceFill ? [faceFill] : []),\n svg`<g mask=\"url(#cutMask)\">${rings}</g>`,\n ...(faceOutline ? [faceOutline] : []),\n ];\n if (!this.hasBackgroundCircle) {\n areas.forEach((area) => {\n result.push(\n svg`<path d=${area} fill=\"none\" stroke=\"var(--instrument-frame-tertiary-color)\" vector-effect=\"non-scaling-stroke\"/>`\n );\n });\n }\n } else {\n if (faceFill) {\n result = [faceFill, ...rings];\n }\n if (this.state !== InstrumentState.off) {\n result.push(\n circle('outerRing', {\n radius: this._bandRadius(OUTER_RING_RADIUS),\n strokeWidth: 1,\n strokeColor: 'var(--instrument-frame-tertiary-color)',\n strokePosition: 'center',\n fillColor: 'none',\n })\n );\n\n result.push(svg`\n ${circle('innerRing', {\n radius: this._bandRadius(this.innerRingRadius),\n strokeWidth: 1,\n strokeColor: 'var(--instrument-frame-tertiary-color)',\n strokePosition: 'center',\n fillColor: 'none',\n })}\n `);\n } else {\n result.push(svg`\n ${circle('innerRing', {\n radius: this._bandRadius(OUTER_RING_RADIUS),\n strokeWidth: 1,\n strokeColor: 'var(--instrument-frame-tertiary-color)',\n strokePosition: 'center',\n fillColor: 'none',\n })}\n `);\n }\n }\n return result;\n }\n\n private _renderTickFadeDefs(): SVGTemplateResult | typeof nothing {\n if (this.tickFadeAngle <= 0 || this.areas.length === 0) return nothing;\n const area = this.areas[0];\n const arcSpan = area.endAngle - area.startAngle;\n const fade = Math.min(this.tickFadeAngle, arcSpan / 4);\n if (fade < 0.5) return nothing;\n\n const {startAngle, endAngle} = area;\n const R = OUTER_RING_RADIUS + this._rOff + 200;\n const px = (deg: number) => R * Math.sin(degToRad(deg));\n const py = (deg: number) => -R * Math.cos(degToRad(deg));\n\n const pieSlice = (a: number, b: number): string => {\n const x1 = px(a),\n y1 = py(a);\n const x2 = px(b),\n y2 = py(b);\n const largeArc = b - a > 180 ? 1 : 0;\n return `M 0 0 L ${x1} ${y1} A ${R} ${R} 0 ${largeArc} 1 ${x2} ${y2} Z`;\n };\n\n const Rm =\n (this._bandRadius(OUTER_RING_RADIUS) +\n this._bandRadius(this.innerRingRadius)) /\n 2;\n const gx = (deg: number) => Rm * Math.sin(degToRad(deg));\n const gy = (deg: number) => -Rm * Math.cos(degToRad(deg));\n\n return svg`\n <defs>\n <linearGradient id=\"tickFadeL\" gradientUnits=\"userSpaceOnUse\"\n x1=\"${gx(startAngle)}\" y1=\"${gy(startAngle)}\"\n x2=\"${gx(startAngle + fade)}\" y2=\"${gy(startAngle + fade)}\">\n <stop offset=\"0\" stop-color=\"black\" />\n <stop offset=\"1\" stop-color=\"white\" />\n </linearGradient>\n <linearGradient id=\"tickFadeR\" gradientUnits=\"userSpaceOnUse\"\n x1=\"${gx(endAngle - fade)}\" y1=\"${gy(endAngle - fade)}\"\n x2=\"${gx(endAngle)}\" y2=\"${gy(endAngle)}\">\n <stop offset=\"0\" stop-color=\"white\" />\n <stop offset=\"1\" stop-color=\"black\" />\n </linearGradient>\n <mask id=\"tickFadeMask\" maskUnits=\"userSpaceOnUse\"\n x=\"${-R}\" y=\"${-R}\" width=\"${R * 2}\" height=\"${R * 2}\">\n <path d=\"${pieSlice(startAngle + fade, endAngle - fade)}\" fill=\"white\" />\n <path d=\"${pieSlice(startAngle, startAngle + fade)}\" fill=\"url(#tickFadeL)\" />\n <path d=\"${pieSlice(endAngle - fade, endAngle)}\" fill=\"url(#tickFadeR)\" />\n </mask>\n </defs>\n `;\n }\n\n private renderCrosshair(\n radius: number,\n labelKnockouts?: {\n positions: LabelPosition[];\n rotation: number | undefined;\n scale: number;\n /** Inner ring radius – crosshair is hidden between labelRadius and this value. */\n innerRingRadius: number;\n },\n centerCutoutRadius?: number\n ): SVGTemplateResult {\n const hasLabelKnockouts =\n !!labelKnockouts && labelKnockouts.positions.length > 0;\n const hasMask = hasLabelKnockouts || centerCutoutRadius !== undefined;\n\n // Radius at which labels sit (distance from centre).\n // Any position is equally valid — they're all at the same radial distance.\n const labelRadius = hasLabelKnockouts\n ? Math.max(\n ...labelKnockouts!.positions.map((l) =>\n Math.abs(l.x !== 0 ? l.x : l.y)\n )\n )\n : 0;\n // Small extra padding so the crosshair doesn't start/end right at the\n // label edge — use the same visual pad as the letter knockouts.\n const ringGapPad = hasLabelKnockouts ? 3 / labelKnockouts!.scale : 0;\n\n return svg`\n ${\n hasMask\n ? svg`\n <defs>\n <mask\n id=\"crosshair-label-mask\"\n maskUnits=\"userSpaceOnUse\"\n x=\"-${radius}\" y=\"-${radius}\"\n width=\"${radius * 2}\" height=\"${radius * 2}\"\n >\n <rect x=\"-${radius}\" y=\"-${radius}\" width=\"${radius * 2}\" height=\"${radius * 2}\" fill=\"white\"/>\n ${\n hasLabelKnockouts\n ? svg`\n <!-- Annular ring knockout: hide crosshair between labels and inner ring -->\n <circle cx=\"0\" cy=\"0\" r=\"${labelKnockouts!.innerRingRadius}\" fill=\"black\"/>\n <circle cx=\"0\" cy=\"0\" r=\"${labelRadius - ringGapPad}\" fill=\"white\"/>\n <!-- Per-label rectangular knockouts -->\n ${labelKnockouts!.positions.map((l) => {\n const fontSize = 12 / labelKnockouts!.scale;\n const pad = 3 / labelKnockouts!.scale;\n const size = fontSize + pad * 2;\n return svg`\n <rect\n x=\"${l.x - size / 2}\" y=\"${l.y - size / 2}\"\n width=\"${size}\" height=\"${size}\"\n fill=\"black\"\n transform=\"rotate(${-(labelKnockouts!.rotation ?? 0)})\"\n transform-origin=\"${l.x} ${l.y}\"\n />\n `;\n })}`\n : nothing\n }\n ${\n centerCutoutRadius !== undefined\n ? svg`<circle cx=\"0\" cy=\"0\" r=\"${centerCutoutRadius}\" fill=\"black\"/>`\n : nothing\n }\n </mask>\n </defs>`\n : nothing\n }\n <g mask=${hasMask ? 'url(#crosshair-label-mask)' : nothing}>\n <line\n x1=\"-${radius}\"\n y1=\"0\"\n x2=\"${radius}\"\n y2=\"0\"\n stroke=\"var(--instrument-frame-tertiary-color)\"\n stroke-width=\"1\"\n vector-effect=\"non-scaling-stroke\"\n />\n <line\n x1=\"0\"\n y1=\"-${radius}\"\n x2=\"0\"\n y2=\"${radius}\"\n stroke=\"var(--instrument-frame-tertiary-color)\"\n stroke-width=\"1\"\n vector-effect=\"non-scaling-stroke\"\n />\n </g>\n `;\n }\n\n /**\n * The split band: the 112..160 annulus as two lane tracks with a bare-face\n * gap. Sector cuts collapse to flush fillets — half the line width on the\n * secondary lane, roundedArch's default on the primary lane.\n */\n private splitBandTracks(): SVGTemplateResult[] {\n const track = 'var(--instrument-frame-secondary-color)';\n if (this.areas.length === 0) {\n const priCenter =\n (this._bandRadius(PRIMARY_SUBBAND_INNER_RADIUS) +\n this._bandRadius(BAND_OUTER_RADIUS)) /\n 2;\n return [\n svg`<circle cx=\"0\" cy=\"0\" r=${this._bandRadius(SECONDARY_LANE_RADIUS)} stroke=${track} stroke-width=${SECONDARY_LINE_WIDTH} fill=\"none\" />`,\n svg`<circle cx=\"0\" cy=\"0\" r=${priCenter} stroke=${track} stroke-width=${BAND_OUTER_RADIUS - PRIMARY_SUBBAND_INNER_RADIUS} fill=\"none\" />`,\n ];\n }\n return this.areas.flatMap((area) => [\n svg`<path d=${roundedArch({\n startAngle: area.startAngle,\n endAngle: area.endAngle,\n R: this._bandRadius(BAND_INNER_RADIUS + SECONDARY_LINE_WIDTH),\n r: this._bandRadius(BAND_INNER_RADIUS),\n roundOutsideCut: true,\n roundInsideCut: true,\n roundRadius: SECONDARY_LINE_WIDTH / 2,\n })} fill=${track} stroke=${track} stroke-width=\"1\" vector-effect=\"non-scaling-stroke\" />`,\n svg`<path d=${roundedArch({\n startAngle: area.startAngle,\n endAngle: area.endAngle,\n R: this._bandRadius(BAND_OUTER_RADIUS),\n r: this._bandRadius(PRIMARY_SUBBAND_INNER_RADIUS),\n roundOutsideCut: true,\n roundInsideCut: true,\n })} fill=${track} stroke=${track} stroke-width=\"1\" vector-effect=\"non-scaling-stroke\" />`,\n ]);\n }\n\n private renderBars(): SVGTemplateResult[] | typeof nothing {\n if (this.barAreas.length === 0) {\n return nothing;\n }\n return this.barAreas.map((bar, index) => {\n let startAngle = Math.min(bar.startAngle, bar.endAngle);\n let endAngle = Math.max(bar.startAngle, bar.endAngle);\n if (this.splitBand && this.areas.length > 0) {\n for (const area of this.areas) {\n if (Math.abs(startAngle - area.startAngle) <= 0.01) {\n startAngle -= SPLIT_BAR_CUT_OVERSHOOT_DEG;\n }\n if (Math.abs(endAngle - area.endAngle) <= 0.01) {\n endAngle += SPLIT_BAR_CUT_OVERSHOOT_DEG;\n }\n }\n }\n const arc = roundedArch({\n r: this._bandRadius(bar.innerRadius ?? RING3_RADIUS),\n R: this._bandRadius(bar.outerRadius ?? RING2_RADIUS),\n startAngle: startAngle,\n endAngle: endAngle,\n roundInsideCut: false,\n roundOutsideCut: false,\n });\n const barMaskR = (bar.outerRadius ?? RING2_RADIUS) + this._rOff + 40;\n // The mask is a sector to cut out the stroke on the start and end of the bar\n const mask = svg`<mask id=\"barMask-${index}\">\n <rect x=\"${-barMaskR}\" y=\"${-barMaskR}\" width=\"${barMaskR * 2}\" height=\"${barMaskR * 2}\" fill=\"black\" />\n <path d=${roundedArch({\n r: 1,\n R: barMaskR,\n startAngle: startAngle,\n endAngle: endAngle,\n roundInsideCut: false,\n roundOutsideCut: false,\n })} fill=\"white\" />\n </mask>`;\n // With roundBandCuts the bar is additionally clipped to the rounded\n // track shape, so its ends round off where they reach the sector cuts\n // (the design's track-mask model) while mid-track ends stay square.\n const trackMask =\n (this.roundBandCuts || this.splitBand) && this.areas.length > 0\n ? svg`<mask id=\"barTrackMask-${index}\">\n <rect x=\"${-barMaskR}\" y=\"${-barMaskR}\" width=\"${barMaskR * 2}\" height=\"${barMaskR * 2}\" fill=\"black\" />\n ${this.areas.map(\n (area) =>\n svg`<path d=${roundedArch({\n startAngle: area.startAngle,\n endAngle: area.endAngle,\n R: this._bandRadius(\n this.splitBand\n ? BAND_OUTER_RADIUS\n : (bar.outerRadius ?? RING2_RADIUS)\n ),\n r: this._bandRadius(\n this.splitBand\n ? PRIMARY_SUBBAND_INNER_RADIUS\n : (bar.innerRadius ?? RING3_RADIUS)\n ),\n // splitBandTracks always rounds the lane cuts, so the\n // split mask must too — else a bar could paint into the\n // arch fillets of an area with square cut flags.\n roundOutsideCut: this.splitBand || area.roundOutsideCut,\n roundInsideCut: this.splitBand || area.roundInsideCut,\n roundRadius: this.splitBand ? undefined : area.roundRadius,\n })} fill=\"white\" stroke=\"white\" stroke-width=\"1\" vector-effect=\"non-scaling-stroke\" />`\n )}\n </mask>`\n : nothing;\n return svg`\n ${mask}\n ${trackMask}\n <g mask=${this.areas.length > 0 && !this.splitBand ? 'url(#cutMask)' : nothing}>\n <g mask=${trackMask !== nothing ? `url(#barTrackMask-${index})` : nothing}>\n <path\n d=${arc}\n fill=${bar.fillColor}\n stroke=${bar.fillColor}\n stroke-width=\"1\"\n vector-effect=\"non-scaling-stroke\"\n mask=\"url(#barMask-${index})\"\n />\n </g>\n </g>\n `;\n });\n }\n\n private renderNeedles(): SVGTemplateResult[] | typeof nothing {\n if (this.needles.length === 0) {\n return nothing;\n }\n return this.needles.map((needle) => {\n return svg`\n <rect\n transform=\"rotate(${needle.angle})\"\n x=\"-4\" y=\"${-this._bandRadius(RING2_RADIUS)}\" width=\"8\" height=\"${needle.length ?? 48}\" rx=\"4\"\n fill=${needle.fillColor}\n stroke=${needle.strokeColor}\n stroke-width=\"1\"\n vector-effect=\"non-scaling-stroke\"\n paint-order=\"stroke fill\"\n />\n `;\n });\n }\n\n private getScale({width, height}: {width: number; height: number}): number {\n const container = measureContainerPx(this);\n const scale = Math.min(container.width / width, container.height / height);\n // On first paint the element and its parent can both still be zero-sized, so\n // the scale is 0 (or non-finite). That value flows into `px / scale` label\n // math and yields ±Infinity coordinates the browser rejects. Fall back to a\n // 1:1 scale until a real size is available; the ResizeController re-renders\n // with the true scale once the element is laid out (issue #1032).\n if (!Number.isFinite(scale) || scale <= 0) {\n return 1;\n }\n return scale;\n }\n\n /**\n * Pixel width of the widest outside label, feeding the frame's label\n * reserve (issue #1021). Explicit `padding` is a hard geometry override\n * and disables the reserve, preserving legacy consumer output.\n */\n private getLabelWidthPx(): number {\n if (this.padding !== undefined || this.tickmarksInside) {\n return 0;\n }\n if (this.showLabels) {\n return NSWE_LABEL_WIDTH_PX;\n }\n return estimateLabelWidthPx(this.tickmarks.map((t) => t.text));\n }\n\n override render() {\n let width: number;\n let height: number;\n let viewBox: string;\n\n if (this.arcFrame) {\n this._rOff = this.arcFrame.radiusOffset;\n this._labelsHidden = false;\n this._ownFrame = undefined;\n width = this.arcFrame.width;\n height = this.arcFrame.height;\n viewBox = this.arcFrame.viewBox;\n } else {\n const frame = computeRadialFrame({\n basePadding: this.padding ?? 24,\n labelWidthPx: this.getLabelWidthPx(),\n clips: {\n top: this.clipTop,\n bottom: this.clipBottom,\n left: this.clipLeft,\n right: this.clipRight,\n },\n containerPx: measureContainerPx(this),\n faceDiameter: this.faceDiameter,\n zoomToFitArc: this.zoomToFitArc,\n areas: this.areas,\n innerRadius: this.innerRingRadius,\n });\n this._rOff = frame.radiusOffset;\n this._labelsHidden = frame.labelsHidden;\n this._ownFrame = frame;\n width = frame.width;\n height = frame.height;\n viewBox = frame.viewBox;\n }\n\n const rOff = this._rOff;\n const scale = this.getScale({width, height});\n const angleSetpoint = this.renderSetpoint();\n // Route through _bandRadius so inside labels track the (zoom-shifted) inner\n // band edge; the coupling is intentional (see _bandRadius INVARIANT).\n const textRadius = this.tickmarksInside\n ? this._bandRadius(this.innerRingRadius)\n : this._bandRadius(OUTER_RING_RADIUS);\n const maxDigits = Math.max(\n ...this.tickmarks.map((t) => t.text?.length ?? 0)\n );\n const tickmarks = this.tickmarks.map((t) =>\n tickmark(t.angle, {\n size: t.type,\n style: this.tickmarkStyle,\n scale,\n text: this.showLabels || this._labelsHidden ? undefined : t.text,\n inside: this.tickmarksInside,\n textRadius,\n rotation: this.rotation,\n maxDigits,\n color: t.color,\n radiusOffset: rOff,\n endLabelsMaxMin: this.endLabelsMaxMin,\n })\n );\n const advices = this.advices\n ? this.advices.map((a) => renderAdvice(a, rOff))\n : nothing;\n\n // NSWE labels are px-fixed outside decor, so they degrade with\n // `labelsHidden` like the tick label texts. The north arrow is exempt:\n // below the small-scale threshold it becomes a compact triangle at the\n // ring that scales with the face, so it survives the smallest faces\n // without clipping.\n const showNsweLabels = this.showLabels && !this._labelsHidden;\n const showNorthArrow = this.northArrow;\n const insideLabels = this.tickmarksInside && showNsweLabels;\n const includeNorth = !this.northArrow;\n const labelPositions = showNsweLabels\n ? getLabelPositions({\n scale,\n inside: this.tickmarksInside,\n innerRadius: this.innerRingRadius + rOff,\n includeNorth,\n insideFlush: this.insideLabelsFlush,\n })\n : undefined;\n\n const labels = labelPositions\n ? renderLabels({\n scale,\n rotation: this.rotation,\n inside: this.tickmarksInside,\n innerRadius: this.innerRingRadius + rOff,\n includeNorth,\n insideFlush: this.insideLabelsFlush,\n })\n : nothing;\n const northArrowEl = showNorthArrow\n ? this.northMarker\n ? renderNorthMarker({scale, rotation: this.rotation})\n : renderNorthArrow({\n scale,\n rotation: this.rotation,\n inside: this.northArrowInside ?? this.tickmarksInside,\n })\n : nothing;\n const wind =\n this.windKnots != null && this.windFromDirectionDeg != null\n ? svg`<g transform=\"scale(${this.scaleWindIcon})\">${renderWind({\n windKnots: this.windKnots,\n fromDirectionDeg: this.windFromDirectionDeg,\n radius: this.windSymbolRadius ?? WIND_ICON_OUTSIDE_RADIUS,\n color: this.windColor,\n })}</g>`\n : nothing;\n const current =\n this.current != null && this.currentFromDirectionDeg != null\n ? this.currentIconCentered\n ? renderCurrentCentered({\n current: this.current,\n fromDirectionDeg: this.currentFromDirectionDeg,\n scale: this.scaleCurrentIcon,\n color: this.currentColor,\n })\n : renderCurrent({\n current: this.current,\n fromDirectionDeg: this.currentFromDirectionDeg,\n radius: this.currentSymbolRadius ?? CURRENT_ICON_OUTSIDE_RADIUS,\n color: this.currentColor,\n })\n : nothing;\n return html`\n <svg\n width=\"100%\"\n height=\"100%\"\n viewBox=${viewBox}\n style=\"--scale: ${scale}\"\n transform=\"rotate(${this.rotation ?? 0})\"\n >\n ${this.watchCircle()} ${this.renderBars()}\n ${\n this.crosshairEnabled\n ? this.renderCrosshair(\n OUTER_RING_RADIUS + rOff,\n insideLabels && labelPositions\n ? {\n positions: labelPositions,\n rotation: this.rotation,\n scale,\n innerRingRadius: this.innerRingRadius + rOff,\n }\n : undefined,\n this.crosshairCenterCutout\n ? this.innerRingRadius + rOff\n : undefined\n )\n : nothing\n }\n ${northArrowEl} ${this.renderStarboardPortIndicator()} ${current}\n ${this._renderTickFadeDefs()} ${wind}\n ${\n this.tickFadeAngle > 0 && this.areas.length > 0\n ? svg`<g mask=\"url(#tickFadeMask)\">${tickmarks}</g>`\n : tickmarks\n }\n ${\n this.areas.length > 0\n ? svg`<g clip-path=\"url(#rot-arc-clip)\">${this.renderRot()}</g>`\n : this.renderRot()\n }\n ${advices} ${angleSetpoint}\n ${\n this.tickFadeAngle > 0 && this.areas.length > 0\n ? svg`<g mask=\"url(#tickFadeMask)\">${labels}</g>`\n : labels\n }\n ${this.renderVesselImage()} ${this.renderNeedles()}\n </svg>\n `;\n }\n\n private getRotColors(): {\n dotColor: string;\n barBgColor: string;\n } {\n const p = this.rotPriority ?? this.priority;\n const isEnhanced = p === Priority.enhanced;\n\n if (this.rotPortStarboard) {\n // For bar type, derive direction from bar angles (visual direction);\n // for dots, use spinner RPM.\n let direction: number;\n if (this.rotType === RotType.bar) {\n const cwSpan = normalizeAngle(this.rotEndAngle - this.rotStartAngle);\n direction = cwSpan <= 180 ? cwSpan : cwSpan - 360;\n } else {\n direction = this._effectiveRpm;\n }\n\n if (direction > 0) {\n return {\n dotColor: 'var(--instrument-starboard-secondary-color)',\n barBgColor: 'var(--instrument-starboard-primary-color)',\n };\n }\n if (direction < 0) {\n return {\n dotColor: 'var(--instrument-port-secondary-color)',\n barBgColor: 'var(--instrument-port-primary-color)',\n };\n }\n }\n\n return {\n dotColor: isEnhanced\n ? 'var(--instrument-enhanced-tertiary-color)'\n : 'var(--instrument-regular-tertiary-color)',\n barBgColor: isEnhanced\n ? 'var(--instrument-enhanced-secondary-color)'\n : 'var(--instrument-regular-secondary-color)',\n };\n }\n\n private renderRot(): SVGTemplateResult | typeof nothing {\n if (!this.rotType) return nothing;\n\n const {dotColor, barBgColor} = this.getRotColors();\n const rOff = this._rOff;\n\n if (this.rotType === RotType.bar) {\n const angularDelta = shortestAngularDeltaDeg(\n this.rotStartAngle,\n this.rotEndAngle\n );\n const threshold = rotBarThresholdAngle(this.rotPosition, rOff);\n const zeroDb = Number.isFinite(this.rotAtZeroDeadband)\n ? this.rotAtZeroDeadband\n : ROT_ZERO_DEADBAND_DEG;\n\n if (angularDelta < Math.max(zeroDb, threshold)) {\n return renderRotZeroPill(\n barBgColor,\n this.rotStartAngle,\n this.rotPosition,\n rOff\n );\n }\n\n return svg`\n ${renderRotBarStatic({\n startAngle: this.rotStartAngle,\n endAngle: this.rotEndAngle,\n barColor: barBgColor,\n position: this.rotPosition,\n maskId: 'rot-bar-mask',\n radiusOffset: rOff,\n })}\n ${svg`<g clip-path=\"url(#rot-bar-mask)\">\n <g id=\"rot-spinner\">\n ${renderRotBarDots(dotColor, this.rotPosition, rOff)}\n </g>\n </g>`}\n `;\n }\n\n const p = this.rotPriority ?? this.priority;\n const isEnhanced = p === Priority.enhanced;\n let dotsColor: string = isEnhanced\n ? 'var(--instrument-enhanced-secondary-color)'\n : 'var(--instrument-regular-secondary-color)';\n if (this.rotPortStarboard) {\n if (this._effectiveRpm > 0) {\n dotsColor = 'var(--instrument-starboard-secondary-color)';\n } else if (this._effectiveRpm < 0) {\n dotsColor = 'var(--instrument-port-secondary-color)';\n }\n }\n return svg`\n <g id=\"rot-spinner\">\n ${renderRotDots(dotsColor, this.rotPosition, rOff)}\n </g>\n `;\n }\n\n private renderSetpoint(): SVGTemplateResult | typeof nothing {\n if (this.angleSetpoint === undefined) {\n return nothing;\n }\n\n const derived = deriveRadialSetpointConfig({\n state: this.state,\n priority: this.priority,\n atSetpoint: this.atAngleSetpoint,\n angleSetpoint: this.angleSetpoint,\n setpointAtZeroDeadband: this.angleSetpointAtZeroDeadband,\n newAngleSetpoint: this.newAngleSetpoint,\n touching: this.touching,\n setpointOverride: this.setpointOverride,\n });\n\n const {visualState, colorMode, disabled, hasNewSetpoint} = derived;\n\n const outwardOffset = getSetpointOutwardOffset(visualState);\n const radius =\n RADIAL_SETPOINT_RADIUS +\n this._rOff +\n outwardOffset -\n RADIAL_SETPOINT_INWARD_ADJUST;\n\n // Render original setpoint marker (dimmed when newAngleSetpoint is active)\n const opacity = hasNewSetpoint ? 0.75 : 1;\n const originalMarker = drawSetpointMarker({\n visualState,\n colorMode,\n disabled,\n id: this._setpointId,\n });\n\n const animate = this.animateSetpoint;\n const hasDeparting = this._departingNewAngleSetpoint !== undefined;\n\n // Compute CSS-safe accumulated angle so transitions always take the short path\n const rawAngle = this.angleSetpoint + 90;\n if (!this._setpointCssAngleInit) {\n // First render: set angle without transition\n this._setpointCssAngle = rawAngle;\n this._setpointCssAngleInit = true;\n } else {\n this._setpointCssAngle = cssSafeAngle(this._setpointCssAngle, rawAngle);\n }\n\n // Use CSS style transform when animating for smooth transition\n const originalSetpoint = animate\n ? svg`\n <g style=\"transform: rotate(${this._setpointCssAngle}deg) translateX(${-radius}px) rotate(270deg); opacity: ${opacity}; transition: transform var(${SETPOINT_ANIMATION_CSS_VAR}, ${SETPOINT_ANIMATION_DURATION_DEFAULT}) ease-out, opacity var(${SETPOINT_ANIMATION_CSS_VAR}, ${SETPOINT_ANIMATION_DURATION_DEFAULT}) ease-out;\">\n ${originalMarker}\n </g>\n `\n : svg`\n <g transform=\"rotate(${this.angleSetpoint + 90}) translate(${-radius}, 0) rotate(270)\" opacity=\"${opacity}\">\n ${originalMarker}\n </g>\n `;\n\n // Render newAngleSetpoint in focus state (always on top)\n // OR render departing newAngleSetpoint during confirm fade-out\n if (hasNewSetpoint || hasDeparting) {\n const isActive = hasNewSetpoint;\n const newAngle = isActive\n ? this.newAngleSetpoint!\n : this._departingNewAngleSetpoint!;\n const targetOpacity = isActive ? 1 : 0;\n\n const focusOutwardOffset = getSetpointOutwardOffset(\n SetpointVisualState.focus\n );\n const focusRadius =\n RADIAL_SETPOINT_RADIUS +\n this._rOff +\n focusOutwardOffset -\n RADIAL_SETPOINT_INWARD_ADJUST;\n\n const newMarker = drawSetpointMarker({\n visualState: SetpointVisualState.focus,\n colorMode,\n disabled: false, // newSetpoint is never disabled\n id: this._newSetpointId,\n });\n\n if (animate) {\n const duration = `var(${SETPOINT_ANIMATION_CSS_VAR}, ${SETPOINT_ANIMATION_DURATION_DEFAULT})`;\n return svg`\n ${originalSetpoint}\n <g style=\"transform: rotate(${newAngle + 90}deg) translateX(${-focusRadius}px) rotate(270deg); opacity: ${targetOpacity}; transition: opacity ${duration} ease-out;\">\n ${newMarker}\n </g>\n `;\n }\n\n return svg`\n ${originalSetpoint}\n <g transform=\"rotate(${newAngle + 90}) translate(${-focusRadius}, 0) rotate(270)\" opacity=\"${targetOpacity}\">\n ${newMarker}\n </g>\n `;\n }\n\n return originalSetpoint;\n }\n\n private renderVesselImage(): SVGTemplateResult[] | typeof nothing {\n if (this.vessels.length === 0) {\n return nothing;\n }\n\n return this.vessels.map((v) => {\n let size;\n switch (v.size) {\n case VesselImageSize.large:\n size = 224;\n break;\n case VesselImageSize.medium:\n size = 160;\n break;\n default:\n size = 100;\n }\n\n const scale = size / 160;\n return svg`<g style=\"transform: ${v.transform} scale(${scale}) translate(-80px, -80px) \">${vesselImages[v.vesselImage]}</g>`;\n });\n }\n\n private renderStarboardPortIndicator(): SVGTemplateResult[] | typeof nothing {\n if (!this.starboardPortIndicator) {\n return nothing;\n }\n return [\n adviceMask(\n 0,\n 180,\n 'var(--instrument-starboard-secondary-color)',\n 'var(--instrument-starboard-secondary-color)'\n ),\n adviceMask(\n 180,\n 360,\n 'var(--instrument-port-secondary-color)',\n 'var(--instrument-port-secondary-color)'\n ),\n ] as SVGTemplateResult[];\n }\n\n static override styles = unsafeCSS(compentStyle);\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'obc-watch': ObcWatch;\n }\n}\n"],"names":["WatchCircleType"],"mappings":"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"}
1
+ {"version":3,"file":"watch.js","sources":["../../../src/navigation-instruments/watch/watch.ts"],"sourcesContent":["import {\n LitElement,\n PropertyValues,\n SVGTemplateResult,\n html,\n nothing,\n svg,\n unsafeCSS,\n} from 'lit';\nimport {property, state} from 'lit/decorators.js';\nimport {circle} from '../../svghelpers/index.js';\nimport {roundedArch} from '../../svghelpers/roundedArch.js';\nimport {\n cssSafeAngle,\n deriveRadialSetpointConfig,\n drawSetpointMarker,\n getSetpointAnimationDurationMs,\n getSetpointOutwardOffset,\n RADIAL_SETPOINT_RADIUS,\n SetpointVisualState,\n SETPOINT_ANIMATION_CSS_VAR,\n SETPOINT_ANIMATION_DURATION_DEFAULT,\n} from '../../svghelpers/setpoint.js';\nimport {InstrumentState, Priority} from '../types.js';\nimport compentStyle from './watch.css?inline';\nimport {ResizeController} from '@lit-labs/observers/resize-controller.js';\nimport {adviceMask, AngleAdviceRaw, renderAdvice} from './advice.js';\nimport {Tickmark, TickmarkStyle, tickmark} from './tickmark.js';\nexport {TickmarkStyle};\nimport {\n RotType,\n RotPosition,\n renderRotDots,\n renderRotBarStatic,\n renderRotBarDots,\n shortestAngularDeltaDeg,\n renderRotZeroPill,\n rotBarThresholdAngle,\n ROT_ZERO_DEADBAND_DEG,\n} from '../rate-of-turn/rot-renderer.js';\nexport {RotType, RotPosition};\nimport {\n RateOfTurnController,\n disposeRotController,\n} from '../rate-of-turn/rate-of-turn.controller.js';\nimport {\n renderLabels,\n renderNorthArrow,\n renderNorthMarker,\n getLabelPositions,\n LabelPosition,\n} from './label.js';\nimport {VesselImage, VesselImageSize, vesselImages} from './vessel.js';\nimport {\n renderCurrent,\n renderCurrentCentered,\n renderWind,\n WIND_ICON_TIP_TO_BOX_INNER,\n} from './environment.js';\nimport {customElement} from '../../decorator.js';\nimport {type ZoomToFitArcFrame} from '../../svghelpers/arc-frame.js';\nimport {degToRad, normalizeAngle} from '../../svghelpers/math.js';\nimport {\n applyPinnedHostSize,\n computeRadialFrame,\n estimateLabelWidthPx,\n measureContainerPx,\n NSWE_LABEL_WIDTH_PX,\n observeInnerBox,\n type RadialFrame,\n} from '../../svghelpers/radial-frame.js';\nexport {VesselImage, VesselImageSize, vesselImages};\n\nexport enum WatchCircleType {\n single = 'single',\n double = 'double',\n doubleThin = 'doubleThin',\n triple = 'triple',\n}\n\nexport interface WatchArea {\n startAngle: number;\n endAngle: number;\n roundOutsideCut: boolean;\n roundInsideCut: boolean;\n /** Corner fillet radius of the rounded cuts; `roundedArch`'s default when unset. */\n roundRadius?: number;\n /** Outline the roundBandCuts band arch with the tertiary frame color instead of self-colored. */\n outlined?: boolean;\n}\n\nexport interface WatchBarArea {\n startAngle: number;\n endAngle: number;\n fillColor: string;\n /** Outer band radius override; defaults to the second ring (160). */\n outerRadius?: number;\n /** Inner band radius override; defaults to the third ring (112). */\n innerRadius?: number;\n}\n\nexport interface WatchNeedle {\n angle: number;\n fillColor: string;\n strokeColor: string;\n /** Radial length of the needle pill; defaults to 48 (the full band). */\n length?: number;\n}\n\nexport interface WatchVessel {\n size: VesselImageSize;\n transform: string;\n vesselImage: VesselImage;\n}\n\nexport const OUTER_RING_RADIUS = 368 / 2;\nconst RING2_RADIUS = 320 / 2;\nconst RING3_RADIUS = 224 / 2;\nconst RING3B_RADIUS = 272 / 2;\nconst RING4_RADIUS = 176 / 2;\n\n/**\n * Split-band lane geometry (the design's \"pitch-rpm\" subdivision of the\n * 112..160 band): thin secondary line, bare-face gap, narrowed primary\n * lane. Shared with the `secondary-lane.ts` helpers.\n */\nexport const BAND_INNER_RADIUS = RING3_RADIUS;\nexport const BAND_OUTER_RADIUS = RING2_RADIUS;\nexport const SECONDARY_LINE_WIDTH = 8;\nexport const LANE_DIVIDER_WIDTH = 8;\nexport const SECONDARY_LANE_RADIUS =\n BAND_INNER_RADIUS + SECONDARY_LINE_WIDTH / 2;\nexport const LANE_DIVIDER_RADIUS =\n BAND_INNER_RADIUS + SECONDARY_LINE_WIDTH + LANE_DIVIDER_WIDTH / 2;\nexport const PRIMARY_SUBBAND_INNER_RADIUS =\n BAND_INNER_RADIUS + SECONDARY_LINE_WIDTH + LANE_DIVIDER_WIDTH;\nexport const PRIMARY_SUBBAND_NEEDLE_LENGTH =\n BAND_OUTER_RADIUS - PRIMARY_SUBBAND_INNER_RADIUS;\n\nexport function innerRingRadiusFor(type: WatchCircleType): number {\n switch (type) {\n case WatchCircleType.single:\n return RING2_RADIUS;\n case WatchCircleType.double:\n return RING3_RADIUS;\n case WatchCircleType.doubleThin:\n return RING3B_RADIUS;\n case WatchCircleType.triple:\n return RING4_RADIUS;\n default:\n throw new Error(`Unknown WatchCircleType: ${type as string}`);\n }\n}\n\nconst RADIAL_SETPOINT_INWARD_ADJUST = 4;\n\n/**\n * Split-band bars whose end sits on a sector cut overshoot it by this much;\n * the lane-arch track mask then trims them flush with the cut's fillet.\n */\nconst SPLIT_BAR_CUT_OVERSHOOT_DEG = 4;\n\n/**\n * Default anchors for the wind/current icons outside the outer ring (the\n * compass layout): the 48-unit icon box sits 4 units off the ring, spanning\n * 188–236 (OpenBridge 6.1 compass, node 19846-157200). The current chevron\n * anchors by its tip at the box inner edge; the wind anchor adds the glyph's\n * inward overhang so its box starts at the same edge.\n */\nconst CURRENT_ICON_OUTSIDE_RADIUS = OUTER_RING_RADIUS + 4;\nconst WIND_ICON_OUTSIDE_RADIUS =\n CURRENT_ICON_OUTSIDE_RADIUS + WIND_ICON_TIP_TO_BOX_INNER;\n\n/**\n * `<obc-watch>` - Core SVG renderer for circular/radial watch-based instruments.\n *\n * This component renders all circular instrument elements including rings, tickmarks,\n * bar areas, needles, advices, setpoints, vessel images, and environmental indicators\n * (wind/current). It serves as the foundation for compass, heading, rudder, speed-gauge,\n * and other radial navigation instruments.\n *\n * ## Setpoint Behavior\n *\n * The setpoint marker visual state is derived from the combination of `atAngleSetpoint`,\n * `angleSetpoint`, and `angleSetpointAtZeroDeadband` properties:\n *\n * - **notEqual**: Value differs from setpoint (triangular marker, offset outward)\n * - **equal**: Value matches setpoint (line marker, sits on ring)\n * - **equalZero**: Value matches setpoint at zero angle (double-line marker, offset outward)\n * - **focus**: User is actively adjusting via `newAngleSetpoint` - shows focus visual state\n *\n * ## newAngleSetpoint Pattern\n *\n * When `newAngleSetpoint` is defined, TWO setpoint markers are rendered:\n * 1. Original marker at `angleSetpoint` - dimmed (0.75 opacity)\n * 2. New marker at `newAngleSetpoint` - focus visual state, full opacity\n *\n * This enables the \"adjustment preview\" UX where users can see both the current\n * and proposed setpoint positions simultaneously.\n *\n * The `RADIAL_SETPOINT_INWARD_ADJUST` constant (4px) fine-tunes radial setpoint positioning\n * to match Figma designs, applied on top of visual state offsets from setpoint.ts.\n *\n * The `colorMode` property allows overriding the derived color mode (enhanced for enhanced priority,\n * regular for other states).\n *\n * ## Setpoint Animation (`animateSetpoint`)\n *\n * When `animateSetpoint` is true and a confirm occurs (`newAngleSetpoint` → `undefined`):\n * - The original setpoint slides to the new position via CSS transition\n * - The departing new-setpoint marker fades out\n * - Angular transitions always take the shortest path via accumulated\n * CSS-safe angles (`cssSafeAngle()`), so even 350° → 10° animates +20°\n *\n * Duration: `var(--setpoint-animation-duration, 300ms)`\n *\n * Internally, `_departingNewAngleSetpoint` captures the departing angle during confirm\n * fade-out and `_animationTimer` auto-clears it after the animation duration.\n * `_setpointCssAngle` tracks the accumulated CSS angle to avoid long-way-around\n * transitions across the 0°/360° boundary.\n *\n * @property state - Instrument state (active, loading, off)\n * @property priority - Color priority (enhanced = blue palette, regular = gray palette)\n * @property hasBackgroundCircle - When `true`, the full watch-face circle is drawn behind the dial —\n * a frame-primary fill with a tertiary outline — so partial-sector\n * instruments still read as a complete circle. In the `off` state the fill\n * is omitted (only the outline remains), keeping the off skeleton hollow.\n * With sector `areas`, the per-area arch outline is dropped; the face\n * outline takes its place.\n * @property northMarker - Replace the north arrow with the compact heading-up north marker: a small\n * triangle on the outer ring with an upright \"N\" at the outside label\n * radius. Rotates with the card via `rotation`.\n * @availableWhen northMarker northArrow==true\n * @property angleSetpoint - Setpoint angle in degrees (0° = 12 o'clock)\n * @property newAngleSetpoint - New setpoint being adjusted (focus mode)\n * @property atAngleSetpoint - Whether value matches setpoint (within deadband)\n * @property angleSetpointAtZeroDeadband - Deadband for zero detection (default 0.5°)\n * @property setpointOverride - Override to derive setpoint color from priority regardless of state\n * @property padding - Explicit padding override in SVG units: the un-zoomed viewBox becomes\n * exactly `(176 + padding) * 2`. Setting it disables the automatic\n * width-aware label reserve (issue #1021) — the caller owns label room.\n * @property faceDiameter - Outer-ring diameter in CSS pixels. When set, the instrument renders at a\n * fixed intrinsic size derived from the ring, arc shape and label reserve —\n * so instruments sharing the same value have identical ring circumference\n * regardless of label width or arc extent (like obc-donut-chart's\n * fixedHeight). When unset (default), the instrument fills its container.\n * @property roundBandCuts - Rounds the band track's sector end cuts at the band's own radii and clips\n * bars to that rounded track shape (the design's track-mask model), instead\n * of the joint silhouette cut shared with the scale ring. No effect without\n * `areas`.\n * @property splitBand - With `watchCircleType: double`, render the band as the split\n * primary/secondary lane tracks (secondary-lane geometry) instead of the\n * joint band; bars clip to the primary lane. Takes precedence over\n * `roundBandCuts` for the band.\n * @property crosshairCenterCutout - Cuts the crosshair out inside the inner ring, so center content (e.g.\n * center readouts) sits on a clean face.\n * @availableWhen crosshairCenterCutout crosshairEnabled==true\n * @property insideLabelsFlush - With `tickmarksInside`, anchor the NSEW label boxes flush against the\n * inner ring (px-fixed) instead of the legacy gap that lets them drift\n * toward the centre as the instrument shrinks.\n * @availableWhen insideLabelsFlush tickmarksInside==true\n * @property currentIconCentered - Render the current icon centered on the face (direction-type layout)\n * instead of at `currentSymbolRadius` on the periphery.\n * @availableWhen currentIconCentered current!=null && currentFromDirectionDeg!=null\n * @property scaleCurrentIcon - Scale factor for the centered current icon.\n * @availableWhen scaleCurrentIcon currentIconCentered==true\n * @property clipTop - Top clip, % of height. Ignored when `zoomToFitArc` is true.\n * @property clipBottom - Bottom clip, % of height. Ignored when `zoomToFitArc` is true.\n * @property clipLeft - Left clip, % of width — horizontal counterpart of clipTop/bottom (90° sectors). Ignored when `zoomToFitArc`.\n * @property clipRight - Right clip, % of width — horizontal counterpart of clipTop/bottom (90° sectors). Ignored when `zoomToFitArc`.\n * @property endLabelsMaxMin - Place the horizontal end labels (±90°, e.g. min/max) below the tick instead\n * of beside it. This is the \"Max-min\" label placement from the radial label\n * model (External / Internal / Max-min) — see PR #903 / design discussion.\n * Currently only used by the 180° sector of `obc-gauge-radial`.\n * @property 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.\n * @property rotType - ROT visualization type: `'dots'` (spinning dots) or `'bar'` (arc bar with clipped dots). Undefined hides the ROT layer.\n * @property rotPosition - Track on which ROT elements are placed: `'scale'` (on the outer ring) or `'innerCircle'` (default, inside the inner ring)\n * @property rotStartAngle - Start angle of the ROT bar arc in degrees (0° = 12 o'clock, clockwise). Only used when `rotType` is `'bar'`.\n * @property rotEndAngle - End angle of the ROT bar arc in degrees. The bar is hidden when the difference from `rotStartAngle` is less than 0.1°.\n * @property 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).\n * @property 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.\n * @property 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).\n * @experimental\n */\n@customElement('obc-watch')\nexport class ObcWatch extends LitElement {\n private _setpointId = `watch-setpoint-${Math.random().toString(36).slice(2, 9)}`;\n private _newSetpointId = `watch-new-setpoint-${Math.random().toString(36).slice(2, 9)}`;\n\n @property({type: String}) state: InstrumentState = InstrumentState.active;\n @property({type: String}) priority: Priority = Priority.regular;\n @property({type: String}) watchCircleType: WatchCircleType =\n WatchCircleType.single;\n @property({type: Boolean}) hasBackgroundCircle: boolean = false;\n @property({type: Boolean}) northArrow: boolean = false;\n @property({type: Boolean}) northArrowInside: boolean | undefined;\n @property({type: Boolean}) northMarker: boolean = false;\n @property({type: Number}) angleSetpoint: number | undefined;\n @property({type: Number}) newAngleSetpoint: number | undefined;\n @property({type: Boolean}) atAngleSetpoint: boolean = false;\n @property({type: Number}) angleSetpointAtZeroDeadband: number = 0.5;\n @property({type: Boolean}) setpointOverride: boolean = false;\n @property({type: Boolean}) touching: boolean = false;\n\n @property({type: Boolean}) animateSetpoint: boolean = false;\n\n @state() private _departingNewAngleSetpoint: number | undefined;\n private _animationTimer?: ReturnType<typeof setTimeout>;\n\n /**\n * Accumulated CSS-safe angle for the original setpoint marker.\n * Ensures CSS rotate() transitions always take the shortest path,\n * even across the 0°/360° boundary.\n */\n private _setpointCssAngle: number = 0;\n\n /** Whether the setpoint CSS angle has been initialised (to skip transition on first render). */\n private _setpointCssAngleInit = false;\n @property({type: Number}) padding: number | undefined;\n @property({type: Number, attribute: 'face-diameter'})\n faceDiameter: number | undefined;\n @property({type: Array, attribute: false}) areas: WatchArea[] = [];\n @property({type: Array, attribute: false}) barAreas: WatchBarArea[] = [];\n @property({type: Boolean}) roundBandCuts: boolean = false;\n @property({type: Boolean}) splitBand: boolean = false;\n @property({type: Array, attribute: false}) needles: WatchNeedle[] = [];\n @property({type: Array, attribute: false}) tickmarks: Tickmark[] = [];\n @property({type: Boolean}) tickmarksInside: boolean = false;\n @property({type: String}) tickmarkStyle: TickmarkStyle =\n TickmarkStyle.regular;\n @property({type: Array, attribute: false}) advices: AngleAdviceRaw[] = [];\n @property({type: Boolean}) crosshairEnabled: boolean = false;\n @property({type: Boolean}) crosshairCenterCutout: boolean = false;\n @property({type: Boolean}) showLabels: boolean = false;\n @property({type: Boolean}) insideLabelsFlush: boolean = false;\n @property({type: Array, attribute: false}) vessels: WatchVessel[] = [];\n @property({type: Number}) windKnots: number | null = null;\n @property({type: Number}) windFromDirectionDeg: number | null = null;\n @property({type: Number}) windSymbolRadius: number | null = null;\n @property({type: String}) windColor: string | undefined;\n @property({type: Number}) current: number | null = null;\n @property({type: Number}) currentFromDirectionDeg: number | null = null;\n @property({type: Number}) currentSymbolRadius: number | null = null;\n @property({type: String}) currentColor: string | undefined;\n @property({type: Boolean}) currentIconCentered: boolean = false;\n @property({type: Number}) scaleCurrentIcon: number = 1;\n @property({type: Boolean}) starboardPortIndicator: boolean = false;\n @property({type: Number}) clipTop: number = 0;\n @property({type: Number}) clipBottom: number = 0;\n @property({type: Number}) clipLeft: number = 0;\n @property({type: Number}) clipRight: number = 0;\n @property({type: Boolean}) endLabelsMaxMin: boolean = false;\n @property({type: Number}) scaleWindIcon: number = 1;\n @property({type: Number}) rotation: number | undefined;\n @property({type: Boolean}) zoomToFitArc: boolean = false;\n @property({attribute: false}) arcFrame: ZoomToFitArcFrame | undefined;\n @property({type: Number}) tickFadeAngle: number = 0;\n\n @property({type: String}) rotType: RotType | undefined;\n @property({type: String}) rotPosition: RotPosition = RotPosition.innerCircle;\n @property({type: Number}) rotStartAngle: number = 0;\n @property({type: Number}) rotEndAngle: number = 0;\n @property({type: String}) rotPriority: Priority | undefined;\n @property({type: Boolean}) rotPortStarboard: boolean = false;\n @property({type: Number}) rotAtZeroDeadband: number = ROT_ZERO_DEADBAND_DEG;\n @property({type: Number}) rateOfTurnDegreesPerMinute: number | undefined;\n @property({type: Number}) rotDotAnimationFactor: number = 18;\n /**\n * @deprecated Use `rateOfTurnDegreesPerMinute` (and optionally `rotDotAnimationFactor`) instead.\n * Kept as a backward-compatible alias; takes effect only when\n * `rateOfTurnDegreesPerMinute` is `undefined`.\n */\n @property({type: Number})\n set rotationsPerMinute(value: number) {\n this._legacyRotationsPerMinute = value;\n }\n /** Legacy spin speed of the ROT dot ring, in rotations per minute (sign controls direction; positive = clockwise). */\n get rotationsPerMinute() {\n return this._legacyRotationsPerMinute;\n }\n private _legacyRotationsPerMinute = 0;\n private _rotController?: RateOfTurnController;\n\n /**\n * Effective rotations-per-minute for the spinning dot animation and\n * port/starboard direction sign. Resolves to:\n * • `(rateOfTurnDegreesPerMinute / 360) * rotDotAnimationFactor` when the\n * new physical API is in use, OR\n * • the legacy `rotationsPerMinute` value otherwise.\n */\n private get _effectiveRpm(): number {\n if (this.rateOfTurnDegreesPerMinute != null) {\n return (\n (this.rateOfTurnDegreesPerMinute / 360) * this.rotDotAnimationFactor\n );\n }\n return this._legacyRotationsPerMinute;\n }\n\n private _resizeController = new ResizeController(this, {});\n\n override firstUpdated(changed: PropertyValues): void {\n super.firstUpdated(changed);\n observeInnerBox(this._resizeController, this.renderRoot);\n }\n\n override willUpdate(changed: PropertyValues): void {\n super.willUpdate(changed);\n\n // Push the resolved effective RPM into the live controller whenever any\n // of the inputs that compose it change.\n if (\n this._rotController &&\n (changed.has('rateOfTurnDegreesPerMinute') ||\n changed.has('rotDotAnimationFactor') ||\n changed.has('rotationsPerMinute'))\n ) {\n this._rotController.rotationsPerMinute = this._effectiveRpm;\n }\n\n // Detect confirm: newAngleSetpoint was defined, now undefined\n if (changed.has('newAngleSetpoint') && this.animateSetpoint) {\n const prev = changed.get('newAngleSetpoint') as number | undefined;\n if (prev !== undefined && this.newAngleSetpoint === undefined) {\n this._departingNewAngleSetpoint = prev;\n clearTimeout(this._animationTimer);\n const duration = getSetpointAnimationDurationMs(this);\n this._animationTimer = setTimeout(() => {\n this._departingNewAngleSetpoint = undefined;\n }, duration);\n }\n }\n }\n\n override disconnectedCallback(): void {\n super.disconnectedCallback();\n clearTimeout(this._animationTimer);\n this._rotController = disposeRotController(this, this._rotController);\n }\n\n override updated(changed: PropertyValues): void {\n super.updated(changed);\n this._hostSizePinned = applyPinnedHostSize(\n this,\n this.arcFrame ? undefined : this._ownFrame,\n this._hostSizePinned\n );\n const el = this.rotType\n ? this.renderRoot.querySelector('#rot-spinner')\n : null;\n if (!el) {\n this._rotController = disposeRotController(this, this._rotController);\n return;\n }\n if (!this._rotController || this._rotController.el !== el) {\n this._rotController = disposeRotController(this, this._rotController);\n this._rotController = new RateOfTurnController(\n this,\n el,\n this._effectiveRpm\n );\n }\n }\n\n private get innerRingRadius(): number {\n return innerRingRadiusFor(this.watchCircleType);\n }\n\n private _rOff = 0;\n\n /**\n * Set by the frame when the label reserve exceeded its cap: tick label\n * texts are dropped instead of clipped (see radial-frame.ts).\n */\n private _labelsHidden = false;\n\n /** The frame of the last render, when computed internally (no arcFrame). */\n private _ownFrame: RadialFrame | undefined;\n\n /** Whether the host size styles were set by applyPinnedHostSize. */\n private _hostSizePinned = false;\n\n /**\n * Radius for a dial-band edge under zoom: additive (`base + _rOff`), keeping\n * band thickness constant in SVG units. INVARIANT: every band-edge radius\n * (rings, value arc, bars, needles, inside-label `textRadius`) must go through\n * this — mixing additive and multiplicative offsets misaligns ticks, labels,\n * advice masks and arcs. The proportional experiments were removed for this\n * reason (PR #903).\n */\n private _bandRadius(base: number): number {\n return base + this._rOff;\n }\n\n private watchCircle(): SVGTemplateResult | SVGTemplateResult[] {\n const rings = [];\n // The face circle is split: fill under the rings, outline over them. The\n // masked track band reaches exactly the face radius, so an outline drawn\n // underneath loses its inner half where the band overlaps and reads\n // thinner there than across the sector gap. The ring branch needs only\n // the fill — its outer ring already outlines the face.\n const faceFill =\n this.hasBackgroundCircle && this.state !== InstrumentState.off\n ? svg`\n <circle\n cx=\"0\"\n cy=\"0\"\n r=\"${this._bandRadius(OUTER_RING_RADIUS)}\"\n fill=\"var(--instrument-frame-primary-color)\"\n stroke=\"none\"\n />`\n : undefined;\n const faceOutline =\n this.hasBackgroundCircle && this.areas.length > 0\n ? svg`\n <circle\n cx=\"0\"\n cy=\"0\"\n r=\"${this._bandRadius(OUTER_RING_RADIUS)}\"\n fill=\"none\"\n stroke=\"var(--instrument-frame-tertiary-color)\"\n stroke-width=\"1\"\n vector-effect=\"non-scaling-stroke\"\n />`\n : undefined;\n if (this.state !== InstrumentState.off) {\n rings.push(svg`\n <circle\n cx=\"0\"\n cy=\"0\"\n r=\"${this._bandRadius(172)}\"\n stroke=\"var(--instrument-frame-primary-color)\"\n fill=\"none\"\n stroke-width=\"24\"\n />`);\n\n if (this.watchCircleType !== WatchCircleType.single) {\n const r1 = this._bandRadius(RING2_RADIUS);\n const r2 = this._bandRadius(\n this.watchCircleType === WatchCircleType.doubleThin\n ? RING3B_RADIUS\n : RING3_RADIUS\n );\n const r = (r1 + r2) / 2;\n const strokeWidth = r1 - r2;\n if (this.splitBand && this.watchCircleType === WatchCircleType.double) {\n rings.push(...this.splitBandTracks());\n } else if (this.roundBandCuts && this.areas.length > 0) {\n rings.push(\n ...this.areas.map(\n (area) =>\n svg`<path d=${roundedArch({\n startAngle: area.startAngle,\n endAngle: area.endAngle,\n R: r1,\n r: r2,\n roundOutsideCut: area.roundOutsideCut,\n roundInsideCut: area.roundInsideCut,\n roundRadius: area.roundRadius,\n })} fill=\"var(--instrument-frame-secondary-color)\" stroke=${\n area.outlined\n ? 'var(--instrument-frame-tertiary-color)'\n : 'var(--instrument-frame-secondary-color)'\n } stroke-width=\"1\" vector-effect=\"non-scaling-stroke\" />`\n )\n );\n } else {\n rings.push(\n svg`\n <circle cx=\"0\" cy=\"0\" r=${r} stroke=\"var(--instrument-frame-secondary-color)\" stroke-width=${strokeWidth} fill=\"none\" />\n <circle cx=\"0\" cy=\"0\" r=${r1} stroke=\"var(--instrument-frame-secondary-color)\" stroke-width=\"1\" fill=\"none\" vector-effect=\"non-scaling-stroke\" />\n <circle cx=\"0\" cy=\"0\" r=${r2} stroke=\"var(--instrument-frame-secondary-color)\" stroke-width=\"1\" fill=\"none\" vector-effect=\"non-scaling-stroke\" />\n `\n );\n }\n }\n if (this.watchCircleType === WatchCircleType.triple) {\n const r1 = this._bandRadius(RING3_RADIUS);\n const r2 = this._bandRadius(RING4_RADIUS);\n const r = (r1 + r2) / 2;\n const strokeWidth = r1 - r2;\n rings.push(\n svg`<circle cx=\"0\" cy=\"0\" r=${r} stroke=\"var(--instrument-frame-primary-color)\" stroke-width=${strokeWidth} fill=\"none\" />`\n );\n }\n }\n\n const maskSize = Math.max(200, OUTER_RING_RADIUS + this._rOff + 50);\n let result = rings;\n if (this.areas.length > 0) {\n const areas = this.areas.map((area) => {\n const svgPath = roundedArch({\n startAngle: area.startAngle,\n endAngle: area.endAngle,\n R: this._bandRadius(OUTER_RING_RADIUS),\n r: this._bandRadius(this.innerRingRadius),\n roundOutsideCut: area.roundOutsideCut,\n roundInsideCut: area.roundInsideCut,\n roundRadius: area.roundRadius,\n });\n return svgPath;\n });\n const mask = svg`<mask id=\"cutMask\">\n <rect x=\"${-maskSize}\" y=\"${-maskSize}\" width=\"${maskSize * 2}\" height=\"${maskSize * 2}\" fill=\"black\" />\n ${areas.map((area) => svg`<path d=${area} fill=\"white\" vector-effect=\"non-scaling-stroke\" stroke=\"white\" stroke-width=\"1\"/>`)}\n </mask>`;\n // clipPath for ROT uses r=0 so dots at any track radius stay visible\n const rotClip = svg`<clipPath id=\"rot-arc-clip\">${this.areas.map(\n (area) =>\n svg`<path d=${roundedArch({\n startAngle: area.startAngle,\n endAngle: area.endAngle,\n R: OUTER_RING_RADIUS + this._rOff + 20,\n r: 0,\n roundOutsideCut: area.roundOutsideCut,\n roundInsideCut: area.roundInsideCut,\n })} />`\n )}</clipPath>`;\n result = [\n mask,\n rotClip,\n ...(faceFill ? [faceFill] : []),\n svg`<g mask=\"url(#cutMask)\">${rings}</g>`,\n ...(faceOutline ? [faceOutline] : []),\n ];\n if (!this.hasBackgroundCircle) {\n areas.forEach((area) => {\n result.push(\n svg`<path d=${area} fill=\"none\" stroke=\"var(--instrument-frame-tertiary-color)\" vector-effect=\"non-scaling-stroke\"/>`\n );\n });\n }\n } else {\n if (faceFill) {\n result = [faceFill, ...rings];\n }\n if (this.state !== InstrumentState.off) {\n result.push(\n circle('outerRing', {\n radius: this._bandRadius(OUTER_RING_RADIUS),\n strokeWidth: 1,\n strokeColor: 'var(--instrument-frame-tertiary-color)',\n strokePosition: 'center',\n fillColor: 'none',\n })\n );\n\n result.push(svg`\n ${circle('innerRing', {\n radius: this._bandRadius(this.innerRingRadius),\n strokeWidth: 1,\n strokeColor: 'var(--instrument-frame-tertiary-color)',\n strokePosition: 'center',\n fillColor: 'none',\n })}\n `);\n } else {\n result.push(svg`\n ${circle('innerRing', {\n radius: this._bandRadius(OUTER_RING_RADIUS),\n strokeWidth: 1,\n strokeColor: 'var(--instrument-frame-tertiary-color)',\n strokePosition: 'center',\n fillColor: 'none',\n })}\n `);\n }\n }\n return result;\n }\n\n private _renderTickFadeDefs(): SVGTemplateResult | typeof nothing {\n if (this.tickFadeAngle <= 0 || this.areas.length === 0) return nothing;\n const area = this.areas[0];\n const arcSpan = area.endAngle - area.startAngle;\n const fade = Math.min(this.tickFadeAngle, arcSpan / 4);\n if (fade < 0.5) return nothing;\n\n const {startAngle, endAngle} = area;\n const R = OUTER_RING_RADIUS + this._rOff + 200;\n const px = (deg: number) => R * Math.sin(degToRad(deg));\n const py = (deg: number) => -R * Math.cos(degToRad(deg));\n\n const pieSlice = (a: number, b: number): string => {\n const x1 = px(a),\n y1 = py(a);\n const x2 = px(b),\n y2 = py(b);\n const largeArc = b - a > 180 ? 1 : 0;\n return `M 0 0 L ${x1} ${y1} A ${R} ${R} 0 ${largeArc} 1 ${x2} ${y2} Z`;\n };\n\n const Rm =\n (this._bandRadius(OUTER_RING_RADIUS) +\n this._bandRadius(this.innerRingRadius)) /\n 2;\n const gx = (deg: number) => Rm * Math.sin(degToRad(deg));\n const gy = (deg: number) => -Rm * Math.cos(degToRad(deg));\n\n return svg`\n <defs>\n <linearGradient id=\"tickFadeL\" gradientUnits=\"userSpaceOnUse\"\n x1=\"${gx(startAngle)}\" y1=\"${gy(startAngle)}\"\n x2=\"${gx(startAngle + fade)}\" y2=\"${gy(startAngle + fade)}\">\n <stop offset=\"0\" stop-color=\"black\" />\n <stop offset=\"1\" stop-color=\"white\" />\n </linearGradient>\n <linearGradient id=\"tickFadeR\" gradientUnits=\"userSpaceOnUse\"\n x1=\"${gx(endAngle - fade)}\" y1=\"${gy(endAngle - fade)}\"\n x2=\"${gx(endAngle)}\" y2=\"${gy(endAngle)}\">\n <stop offset=\"0\" stop-color=\"white\" />\n <stop offset=\"1\" stop-color=\"black\" />\n </linearGradient>\n <mask id=\"tickFadeMask\" maskUnits=\"userSpaceOnUse\"\n x=\"${-R}\" y=\"${-R}\" width=\"${R * 2}\" height=\"${R * 2}\">\n <path d=\"${pieSlice(startAngle + fade, endAngle - fade)}\" fill=\"white\" />\n <path d=\"${pieSlice(startAngle, startAngle + fade)}\" fill=\"url(#tickFadeL)\" />\n <path d=\"${pieSlice(endAngle - fade, endAngle)}\" fill=\"url(#tickFadeR)\" />\n </mask>\n </defs>\n `;\n }\n\n private renderCrosshair(\n radius: number,\n labelKnockouts?: {\n positions: LabelPosition[];\n rotation: number | undefined;\n scale: number;\n /** Inner ring radius – crosshair is hidden between labelRadius and this value. */\n innerRingRadius: number;\n },\n centerCutoutRadius?: number\n ): SVGTemplateResult {\n const hasLabelKnockouts =\n !!labelKnockouts && labelKnockouts.positions.length > 0;\n const hasMask = hasLabelKnockouts || centerCutoutRadius !== undefined;\n\n // Radius at which labels sit (distance from centre).\n // Any position is equally valid — they're all at the same radial distance.\n const labelRadius = hasLabelKnockouts\n ? Math.max(\n ...labelKnockouts!.positions.map((l) =>\n Math.abs(l.x !== 0 ? l.x : l.y)\n )\n )\n : 0;\n // Small extra padding so the crosshair doesn't start/end right at the\n // label edge — use the same visual pad as the letter knockouts.\n const ringGapPad = hasLabelKnockouts ? 3 / labelKnockouts!.scale : 0;\n\n return svg`\n ${\n hasMask\n ? svg`\n <defs>\n <mask\n id=\"crosshair-label-mask\"\n maskUnits=\"userSpaceOnUse\"\n x=\"-${radius}\" y=\"-${radius}\"\n width=\"${radius * 2}\" height=\"${radius * 2}\"\n >\n <rect x=\"-${radius}\" y=\"-${radius}\" width=\"${radius * 2}\" height=\"${radius * 2}\" fill=\"white\"/>\n ${\n hasLabelKnockouts\n ? svg`\n <!-- Annular ring knockout: hide crosshair between labels and inner ring -->\n <circle cx=\"0\" cy=\"0\" r=\"${labelKnockouts!.innerRingRadius}\" fill=\"black\"/>\n <circle cx=\"0\" cy=\"0\" r=\"${labelRadius - ringGapPad}\" fill=\"white\"/>\n <!-- Per-label rectangular knockouts -->\n ${labelKnockouts!.positions.map((l) => {\n const fontSize = 12 / labelKnockouts!.scale;\n const pad = 3 / labelKnockouts!.scale;\n const size = fontSize + pad * 2;\n return svg`\n <rect\n x=\"${l.x - size / 2}\" y=\"${l.y - size / 2}\"\n width=\"${size}\" height=\"${size}\"\n fill=\"black\"\n transform=\"rotate(${-(labelKnockouts!.rotation ?? 0)})\"\n transform-origin=\"${l.x} ${l.y}\"\n />\n `;\n })}`\n : nothing\n }\n ${\n centerCutoutRadius !== undefined\n ? svg`<circle cx=\"0\" cy=\"0\" r=\"${centerCutoutRadius}\" fill=\"black\"/>`\n : nothing\n }\n </mask>\n </defs>`\n : nothing\n }\n <g mask=${hasMask ? 'url(#crosshair-label-mask)' : nothing}>\n <line\n x1=\"-${radius}\"\n y1=\"0\"\n x2=\"${radius}\"\n y2=\"0\"\n stroke=\"var(--instrument-frame-tertiary-color)\"\n stroke-width=\"1\"\n vector-effect=\"non-scaling-stroke\"\n />\n <line\n x1=\"0\"\n y1=\"-${radius}\"\n x2=\"0\"\n y2=\"${radius}\"\n stroke=\"var(--instrument-frame-tertiary-color)\"\n stroke-width=\"1\"\n vector-effect=\"non-scaling-stroke\"\n />\n </g>\n `;\n }\n\n /**\n * The split band: the 112..160 annulus as two lane tracks with a bare-face\n * gap. Sector cuts collapse to flush fillets — half the line width on the\n * secondary lane, roundedArch's default on the primary lane.\n */\n private splitBandTracks(): SVGTemplateResult[] {\n const track = 'var(--instrument-frame-secondary-color)';\n if (this.areas.length === 0) {\n const priCenter =\n (this._bandRadius(PRIMARY_SUBBAND_INNER_RADIUS) +\n this._bandRadius(BAND_OUTER_RADIUS)) /\n 2;\n return [\n svg`<circle cx=\"0\" cy=\"0\" r=${this._bandRadius(SECONDARY_LANE_RADIUS)} stroke=${track} stroke-width=${SECONDARY_LINE_WIDTH} fill=\"none\" />`,\n svg`<circle cx=\"0\" cy=\"0\" r=${priCenter} stroke=${track} stroke-width=${BAND_OUTER_RADIUS - PRIMARY_SUBBAND_INNER_RADIUS} fill=\"none\" />`,\n ];\n }\n return this.areas.flatMap((area) => [\n svg`<path d=${roundedArch({\n startAngle: area.startAngle,\n endAngle: area.endAngle,\n R: this._bandRadius(BAND_INNER_RADIUS + SECONDARY_LINE_WIDTH),\n r: this._bandRadius(BAND_INNER_RADIUS),\n roundOutsideCut: true,\n roundInsideCut: true,\n roundRadius: SECONDARY_LINE_WIDTH / 2,\n })} fill=${track} stroke=${track} stroke-width=\"1\" vector-effect=\"non-scaling-stroke\" />`,\n svg`<path d=${roundedArch({\n startAngle: area.startAngle,\n endAngle: area.endAngle,\n R: this._bandRadius(BAND_OUTER_RADIUS),\n r: this._bandRadius(PRIMARY_SUBBAND_INNER_RADIUS),\n roundOutsideCut: true,\n roundInsideCut: true,\n })} fill=${track} stroke=${track} stroke-width=\"1\" vector-effect=\"non-scaling-stroke\" />`,\n ]);\n }\n\n private renderBars(): SVGTemplateResult[] | typeof nothing {\n if (this.barAreas.length === 0) {\n return nothing;\n }\n return this.barAreas.map((bar, index) => {\n let startAngle = Math.min(bar.startAngle, bar.endAngle);\n let endAngle = Math.max(bar.startAngle, bar.endAngle);\n if (this.splitBand && this.areas.length > 0) {\n for (const area of this.areas) {\n if (Math.abs(startAngle - area.startAngle) <= 0.01) {\n startAngle -= SPLIT_BAR_CUT_OVERSHOOT_DEG;\n }\n if (Math.abs(endAngle - area.endAngle) <= 0.01) {\n endAngle += SPLIT_BAR_CUT_OVERSHOOT_DEG;\n }\n }\n }\n const arc = roundedArch({\n r: this._bandRadius(bar.innerRadius ?? RING3_RADIUS),\n R: this._bandRadius(bar.outerRadius ?? RING2_RADIUS),\n startAngle: startAngle,\n endAngle: endAngle,\n roundInsideCut: false,\n roundOutsideCut: false,\n });\n const barMaskR = (bar.outerRadius ?? RING2_RADIUS) + this._rOff + 40;\n // The mask is a sector to cut out the stroke on the start and end of the bar\n const mask = svg`<mask id=\"barMask-${index}\">\n <rect x=\"${-barMaskR}\" y=\"${-barMaskR}\" width=\"${barMaskR * 2}\" height=\"${barMaskR * 2}\" fill=\"black\" />\n <path d=${roundedArch({\n r: 1,\n R: barMaskR,\n startAngle: startAngle,\n endAngle: endAngle,\n roundInsideCut: false,\n roundOutsideCut: false,\n })} fill=\"white\" />\n </mask>`;\n // With roundBandCuts the bar is additionally clipped to the rounded\n // track shape, so its ends round off where they reach the sector cuts\n // (the design's track-mask model) while mid-track ends stay square.\n const trackMask =\n (this.roundBandCuts || this.splitBand) && this.areas.length > 0\n ? svg`<mask id=\"barTrackMask-${index}\">\n <rect x=\"${-barMaskR}\" y=\"${-barMaskR}\" width=\"${barMaskR * 2}\" height=\"${barMaskR * 2}\" fill=\"black\" />\n ${this.areas.map(\n (area) =>\n svg`<path d=${roundedArch({\n startAngle: area.startAngle,\n endAngle: area.endAngle,\n R: this._bandRadius(\n this.splitBand\n ? BAND_OUTER_RADIUS\n : (bar.outerRadius ?? RING2_RADIUS)\n ),\n r: this._bandRadius(\n this.splitBand\n ? PRIMARY_SUBBAND_INNER_RADIUS\n : (bar.innerRadius ?? RING3_RADIUS)\n ),\n // splitBandTracks always rounds the lane cuts, so the\n // split mask must too — else a bar could paint into the\n // arch fillets of an area with square cut flags.\n roundOutsideCut: this.splitBand || area.roundOutsideCut,\n roundInsideCut: this.splitBand || area.roundInsideCut,\n roundRadius: this.splitBand ? undefined : area.roundRadius,\n })} fill=\"white\" stroke=\"white\" stroke-width=\"1\" vector-effect=\"non-scaling-stroke\" />`\n )}\n </mask>`\n : nothing;\n return svg`\n ${mask}\n ${trackMask}\n <g mask=${this.areas.length > 0 && !this.splitBand ? 'url(#cutMask)' : nothing}>\n <g mask=${trackMask !== nothing ? `url(#barTrackMask-${index})` : nothing}>\n <path\n d=${arc}\n fill=${bar.fillColor}\n stroke=${bar.fillColor}\n stroke-width=\"1\"\n vector-effect=\"non-scaling-stroke\"\n mask=\"url(#barMask-${index})\"\n />\n </g>\n </g>\n `;\n });\n }\n\n private renderNeedles(): SVGTemplateResult[] | typeof nothing {\n if (this.needles.length === 0) {\n return nothing;\n }\n return this.needles.map((needle) => {\n return svg`\n <rect\n transform=\"rotate(${needle.angle})\"\n x=\"-4\" y=\"${-this._bandRadius(RING2_RADIUS)}\" width=\"8\" height=\"${needle.length ?? 48}\" rx=\"4\"\n fill=${needle.fillColor}\n stroke=${needle.strokeColor}\n stroke-width=\"1\"\n vector-effect=\"non-scaling-stroke\"\n paint-order=\"stroke fill\"\n />\n `;\n });\n }\n\n private getScale({width, height}: {width: number; height: number}): number {\n const container = measureContainerPx(this);\n const scale = Math.min(container.width / width, container.height / height);\n // On first paint the element and its parent can both still be zero-sized, so\n // the scale is 0 (or non-finite). That value flows into `px / scale` label\n // math and yields ±Infinity coordinates the browser rejects. Fall back to a\n // 1:1 scale until a real size is available; the ResizeController re-renders\n // with the true scale once the element is laid out (issue #1032).\n if (!Number.isFinite(scale) || scale <= 0) {\n return 1;\n }\n return scale;\n }\n\n /**\n * Pixel width of the widest outside label, feeding the frame's label\n * reserve (issue #1021). Explicit `padding` is a hard geometry override\n * and disables the reserve, preserving legacy consumer output.\n */\n private getLabelWidthPx(): number {\n if (this.padding !== undefined || this.tickmarksInside) {\n return 0;\n }\n if (this.showLabels) {\n return NSWE_LABEL_WIDTH_PX;\n }\n return estimateLabelWidthPx(this.tickmarks.map((t) => t.text));\n }\n\n override render() {\n let width: number;\n let height: number;\n let frameX: number;\n let frameY: number;\n let viewBox: string;\n\n if (this.arcFrame) {\n this._rOff = this.arcFrame.radiusOffset;\n this._labelsHidden = false;\n this._ownFrame = undefined;\n width = this.arcFrame.width;\n height = this.arcFrame.height;\n frameX = this.arcFrame.x;\n frameY = this.arcFrame.y;\n viewBox = this.arcFrame.viewBox;\n } else {\n const frame = computeRadialFrame({\n basePadding: this.padding ?? 24,\n labelWidthPx: this.getLabelWidthPx(),\n clips: {\n top: this.clipTop,\n bottom: this.clipBottom,\n left: this.clipLeft,\n right: this.clipRight,\n },\n containerPx: measureContainerPx(this),\n faceDiameter: this.faceDiameter,\n zoomToFitArc: this.zoomToFitArc,\n areas: this.areas,\n innerRadius: this.innerRingRadius,\n });\n this._rOff = frame.radiusOffset;\n this._labelsHidden = frame.labelsHidden;\n this._ownFrame = frame;\n width = frame.width;\n height = frame.height;\n frameX = frame.x;\n frameY = frame.y;\n viewBox = frame.viewBox;\n }\n\n const rOff = this._rOff;\n const scale = this.getScale({width, height});\n // `rotation` turns the element box, which pivots on the box centre — the\n // watch centre only while the frame is origin-centred. A cropped frame\n // (sector window) moves it, so pin the pivot to the SVG origin instead.\n const pivotX = width > 0 ? (-frameX / width) * 100 : 50;\n const pivotY = height > 0 ? (-frameY / height) * 100 : 50;\n // A cropped frame paints beyond the viewBox, which the viewport would cut\n // before the rotation brings it into view. Only those need the escape.\n const cropped = pivotX !== 50 || pivotY !== 50;\n const angleSetpoint = this.renderSetpoint();\n // Route through _bandRadius so inside labels track the (zoom-shifted) inner\n // band edge; the coupling is intentional (see _bandRadius INVARIANT).\n const textRadius = this.tickmarksInside\n ? this._bandRadius(this.innerRingRadius)\n : this._bandRadius(OUTER_RING_RADIUS);\n const maxDigits = Math.max(\n ...this.tickmarks.map((t) => t.text?.length ?? 0)\n );\n const tickmarks = this.tickmarks.map((t) =>\n tickmark(t.angle, {\n size: t.type,\n style: this.tickmarkStyle,\n scale,\n text: this.showLabels || this._labelsHidden ? undefined : t.text,\n inside: this.tickmarksInside,\n textRadius,\n rotation: this.rotation,\n maxDigits,\n color: t.color,\n radiusOffset: rOff,\n endLabelsMaxMin: this.endLabelsMaxMin,\n })\n );\n const advices = this.advices\n ? this.advices.map((a) => renderAdvice(a, rOff))\n : nothing;\n\n // NSWE labels are px-fixed outside decor, so they degrade with\n // `labelsHidden` like the tick label texts. The north arrow is exempt:\n // below the small-scale threshold it becomes a compact triangle at the\n // ring that scales with the face, so it survives the smallest faces\n // without clipping.\n const showNsweLabels = this.showLabels && !this._labelsHidden;\n const showNorthArrow = this.northArrow;\n const insideLabels = this.tickmarksInside && showNsweLabels;\n const includeNorth = !this.northArrow;\n const labelPositions = showNsweLabels\n ? getLabelPositions({\n scale,\n inside: this.tickmarksInside,\n innerRadius: this.innerRingRadius + rOff,\n includeNorth,\n insideFlush: this.insideLabelsFlush,\n })\n : undefined;\n\n const labels = labelPositions\n ? renderLabels({\n scale,\n rotation: this.rotation,\n inside: this.tickmarksInside,\n innerRadius: this.innerRingRadius + rOff,\n includeNorth,\n insideFlush: this.insideLabelsFlush,\n })\n : nothing;\n const northArrowEl = showNorthArrow\n ? this.northMarker\n ? renderNorthMarker({scale, rotation: this.rotation})\n : renderNorthArrow({\n scale,\n rotation: this.rotation,\n inside: this.northArrowInside ?? this.tickmarksInside,\n })\n : nothing;\n const wind =\n this.windKnots != null && this.windFromDirectionDeg != null\n ? svg`<g transform=\"scale(${this.scaleWindIcon})\">${renderWind({\n windKnots: this.windKnots,\n fromDirectionDeg: this.windFromDirectionDeg,\n radius: this.windSymbolRadius ?? WIND_ICON_OUTSIDE_RADIUS,\n color: this.windColor,\n })}</g>`\n : nothing;\n const current =\n this.current != null && this.currentFromDirectionDeg != null\n ? this.currentIconCentered\n ? renderCurrentCentered({\n current: this.current,\n fromDirectionDeg: this.currentFromDirectionDeg,\n scale: this.scaleCurrentIcon,\n color: this.currentColor,\n })\n : renderCurrent({\n current: this.current,\n fromDirectionDeg: this.currentFromDirectionDeg,\n radius: this.currentSymbolRadius ?? CURRENT_ICON_OUTSIDE_RADIUS,\n color: this.currentColor,\n })\n : nothing;\n return html`\n <svg\n class=${cropped ? 'cropped' : nothing}\n width=\"100%\"\n height=\"100%\"\n viewBox=${viewBox}\n style=\"--scale: ${scale}; transform-origin: ${pivotX}% ${pivotY}%\"\n transform=\"rotate(${this.rotation ?? 0})\"\n >\n ${this.watchCircle()} ${this.renderBars()}\n ${\n this.crosshairEnabled\n ? this.renderCrosshair(\n OUTER_RING_RADIUS + rOff,\n insideLabels && labelPositions\n ? {\n positions: labelPositions,\n rotation: this.rotation,\n scale,\n innerRingRadius: this.innerRingRadius + rOff,\n }\n : undefined,\n this.crosshairCenterCutout\n ? this.innerRingRadius + rOff\n : undefined\n )\n : nothing\n }\n ${northArrowEl} ${this.renderStarboardPortIndicator()} ${current}\n ${this._renderTickFadeDefs()} ${wind}\n ${\n this.tickFadeAngle > 0 && this.areas.length > 0\n ? svg`<g mask=\"url(#tickFadeMask)\">${tickmarks}</g>`\n : tickmarks\n }\n ${\n this.areas.length > 0\n ? svg`<g clip-path=\"url(#rot-arc-clip)\">${this.renderRot()}</g>`\n : this.renderRot()\n }\n ${advices} ${angleSetpoint}\n ${\n this.tickFadeAngle > 0 && this.areas.length > 0\n ? svg`<g mask=\"url(#tickFadeMask)\">${labels}</g>`\n : labels\n }\n ${this.renderVesselImage()} ${this.renderNeedles()}\n </svg>\n `;\n }\n\n private getRotColors(): {\n dotColor: string;\n barBgColor: string;\n } {\n const p = this.rotPriority ?? this.priority;\n const isEnhanced = p === Priority.enhanced;\n\n if (this.rotPortStarboard) {\n // For bar type, derive direction from bar angles (visual direction);\n // for dots, use spinner RPM.\n let direction: number;\n if (this.rotType === RotType.bar) {\n const cwSpan = normalizeAngle(this.rotEndAngle - this.rotStartAngle);\n direction = cwSpan <= 180 ? cwSpan : cwSpan - 360;\n } else {\n direction = this._effectiveRpm;\n }\n\n if (direction > 0) {\n return {\n dotColor: 'var(--instrument-starboard-secondary-color)',\n barBgColor: 'var(--instrument-starboard-primary-color)',\n };\n }\n if (direction < 0) {\n return {\n dotColor: 'var(--instrument-port-secondary-color)',\n barBgColor: 'var(--instrument-port-primary-color)',\n };\n }\n }\n\n return {\n dotColor: isEnhanced\n ? 'var(--instrument-enhanced-tertiary-color)'\n : 'var(--instrument-regular-tertiary-color)',\n barBgColor: isEnhanced\n ? 'var(--instrument-enhanced-secondary-color)'\n : 'var(--instrument-regular-secondary-color)',\n };\n }\n\n private renderRot(): SVGTemplateResult | typeof nothing {\n if (!this.rotType) return nothing;\n\n const {dotColor, barBgColor} = this.getRotColors();\n const rOff = this._rOff;\n\n if (this.rotType === RotType.bar) {\n const angularDelta = shortestAngularDeltaDeg(\n this.rotStartAngle,\n this.rotEndAngle\n );\n const threshold = rotBarThresholdAngle(this.rotPosition, rOff);\n const zeroDb = Number.isFinite(this.rotAtZeroDeadband)\n ? this.rotAtZeroDeadband\n : ROT_ZERO_DEADBAND_DEG;\n\n if (angularDelta < Math.max(zeroDb, threshold)) {\n return renderRotZeroPill(\n barBgColor,\n this.rotStartAngle,\n this.rotPosition,\n rOff\n );\n }\n\n return svg`\n ${renderRotBarStatic({\n startAngle: this.rotStartAngle,\n endAngle: this.rotEndAngle,\n barColor: barBgColor,\n position: this.rotPosition,\n maskId: 'rot-bar-mask',\n radiusOffset: rOff,\n })}\n ${svg`<g clip-path=\"url(#rot-bar-mask)\">\n <g id=\"rot-spinner\">\n ${renderRotBarDots(dotColor, this.rotPosition, rOff)}\n </g>\n </g>`}\n `;\n }\n\n const p = this.rotPriority ?? this.priority;\n const isEnhanced = p === Priority.enhanced;\n let dotsColor: string = isEnhanced\n ? 'var(--instrument-enhanced-secondary-color)'\n : 'var(--instrument-regular-secondary-color)';\n if (this.rotPortStarboard) {\n if (this._effectiveRpm > 0) {\n dotsColor = 'var(--instrument-starboard-secondary-color)';\n } else if (this._effectiveRpm < 0) {\n dotsColor = 'var(--instrument-port-secondary-color)';\n }\n }\n return svg`\n <g id=\"rot-spinner\">\n ${renderRotDots(dotsColor, this.rotPosition, rOff)}\n </g>\n `;\n }\n\n private renderSetpoint(): SVGTemplateResult | typeof nothing {\n if (this.angleSetpoint === undefined) {\n return nothing;\n }\n\n const derived = deriveRadialSetpointConfig({\n state: this.state,\n priority: this.priority,\n atSetpoint: this.atAngleSetpoint,\n angleSetpoint: this.angleSetpoint,\n setpointAtZeroDeadband: this.angleSetpointAtZeroDeadband,\n newAngleSetpoint: this.newAngleSetpoint,\n touching: this.touching,\n setpointOverride: this.setpointOverride,\n });\n\n const {visualState, colorMode, disabled, hasNewSetpoint} = derived;\n\n const outwardOffset = getSetpointOutwardOffset(visualState);\n const radius =\n RADIAL_SETPOINT_RADIUS +\n this._rOff +\n outwardOffset -\n RADIAL_SETPOINT_INWARD_ADJUST;\n\n // Render original setpoint marker (dimmed when newAngleSetpoint is active)\n const opacity = hasNewSetpoint ? 0.75 : 1;\n const originalMarker = drawSetpointMarker({\n visualState,\n colorMode,\n disabled,\n id: this._setpointId,\n });\n\n const animate = this.animateSetpoint;\n const hasDeparting = this._departingNewAngleSetpoint !== undefined;\n\n // Compute CSS-safe accumulated angle so transitions always take the short path\n const rawAngle = this.angleSetpoint + 90;\n if (!this._setpointCssAngleInit) {\n // First render: set angle without transition\n this._setpointCssAngle = rawAngle;\n this._setpointCssAngleInit = true;\n } else {\n this._setpointCssAngle = cssSafeAngle(this._setpointCssAngle, rawAngle);\n }\n\n // Use CSS style transform when animating for smooth transition\n const originalSetpoint = animate\n ? svg`\n <g style=\"transform: rotate(${this._setpointCssAngle}deg) translateX(${-radius}px) rotate(270deg); opacity: ${opacity}; transition: transform var(${SETPOINT_ANIMATION_CSS_VAR}, ${SETPOINT_ANIMATION_DURATION_DEFAULT}) ease-out, opacity var(${SETPOINT_ANIMATION_CSS_VAR}, ${SETPOINT_ANIMATION_DURATION_DEFAULT}) ease-out;\">\n ${originalMarker}\n </g>\n `\n : svg`\n <g transform=\"rotate(${this.angleSetpoint + 90}) translate(${-radius}, 0) rotate(270)\" opacity=\"${opacity}\">\n ${originalMarker}\n </g>\n `;\n\n // Render newAngleSetpoint in focus state (always on top)\n // OR render departing newAngleSetpoint during confirm fade-out\n if (hasNewSetpoint || hasDeparting) {\n const isActive = hasNewSetpoint;\n const newAngle = isActive\n ? this.newAngleSetpoint!\n : this._departingNewAngleSetpoint!;\n const targetOpacity = isActive ? 1 : 0;\n\n const focusOutwardOffset = getSetpointOutwardOffset(\n SetpointVisualState.focus\n );\n const focusRadius =\n RADIAL_SETPOINT_RADIUS +\n this._rOff +\n focusOutwardOffset -\n RADIAL_SETPOINT_INWARD_ADJUST;\n\n const newMarker = drawSetpointMarker({\n visualState: SetpointVisualState.focus,\n colorMode,\n disabled: false, // newSetpoint is never disabled\n id: this._newSetpointId,\n });\n\n if (animate) {\n const duration = `var(${SETPOINT_ANIMATION_CSS_VAR}, ${SETPOINT_ANIMATION_DURATION_DEFAULT})`;\n return svg`\n ${originalSetpoint}\n <g style=\"transform: rotate(${newAngle + 90}deg) translateX(${-focusRadius}px) rotate(270deg); opacity: ${targetOpacity}; transition: opacity ${duration} ease-out;\">\n ${newMarker}\n </g>\n `;\n }\n\n return svg`\n ${originalSetpoint}\n <g transform=\"rotate(${newAngle + 90}) translate(${-focusRadius}, 0) rotate(270)\" opacity=\"${targetOpacity}\">\n ${newMarker}\n </g>\n `;\n }\n\n return originalSetpoint;\n }\n\n private renderVesselImage(): SVGTemplateResult[] | typeof nothing {\n if (this.vessels.length === 0) {\n return nothing;\n }\n\n return this.vessels.map((v) => {\n let size;\n switch (v.size) {\n case VesselImageSize.large:\n size = 224;\n break;\n case VesselImageSize.medium:\n size = 160;\n break;\n default:\n size = 100;\n }\n\n const scale = size / 160;\n return svg`<g style=\"transform: ${v.transform} scale(${scale}) translate(-80px, -80px) \">${vesselImages[v.vesselImage]}</g>`;\n });\n }\n\n private renderStarboardPortIndicator(): SVGTemplateResult[] | typeof nothing {\n if (!this.starboardPortIndicator) {\n return nothing;\n }\n return [\n adviceMask(\n 0,\n 180,\n 'var(--instrument-starboard-secondary-color)',\n 'var(--instrument-starboard-secondary-color)'\n ),\n adviceMask(\n 180,\n 360,\n 'var(--instrument-port-secondary-color)',\n 'var(--instrument-port-secondary-color)'\n ),\n ] as SVGTemplateResult[];\n }\n\n static override styles = unsafeCSS(compentStyle);\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'obc-watch': ObcWatch;\n }\n}\n"],"names":["WatchCircleType"],"mappings":"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"}
@@ -9,6 +9,13 @@ export interface ArcViewBox {
9
9
  export interface ZoomToFitArcFrame extends ArcViewBox {
10
10
  radiusOffset: number;
11
11
  }
12
+ /** Shape of the zoom viewBox. */
13
+ export declare enum ArcFrameFit {
14
+ /** Square around the arc — what instruments with a round face expect. */
15
+ square = "square",
16
+ /** The arc's own bounding box, which crops a flat arc's empty height. */
17
+ bbox = "bbox"
18
+ }
12
19
  /**
13
20
  * Normalize a user-supplied watch-arc half-extent (in degrees) to a finite
14
21
  * value clamped into a sane range. Guards against `NaN`/`Infinity` produced
@@ -62,6 +69,7 @@ export declare function computeZoomToFitArcFrame(options: {
62
69
  xMax: number;
63
70
  yMax: number;
64
71
  };
72
+ fit?: ArcFrameFit;
65
73
  }): ZoomToFitArcFrame;
66
74
  /**
67
75
  * Shift an arc viewBox so the arc's outer-edge midpoint coincides with the
@@ -1 +1 @@
1
- {"version":3,"file":"arc-frame.d.ts","sourceRoot":"","sources":["../../src/svghelpers/arc-frame.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,0CAA0C,CAAC;AAGxE,MAAM,WAAW,UAAU;IACzB,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,iBAAkB,SAAQ,UAAU;IACnD,YAAY,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,MAAM,GAAG,SAAS,EACzB,QAAQ,EAAE,MAAM,GACf,MAAM,CAGR;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,EAAE;IAChD,KAAK,EAAE,SAAS,EAAE,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAC,CAAC;CACvE,GAAG,iBAAiB,CAyFpB;AAuED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,wBAAwB,CACtC,KAAK,EAAE,iBAAiB,EACxB,cAAc,EAAE,MAAM,EACtB,eAAe,EAAE,MAAM,EACvB,UAAU,EAAE,MAAM,GACjB,iBAAiB,CAkBnB"}
1
+ {"version":3,"file":"arc-frame.d.ts","sourceRoot":"","sources":["../../src/svghelpers/arc-frame.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,0CAA0C,CAAC;AAGxE,MAAM,WAAW,UAAU;IACzB,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,iBAAkB,SAAQ,UAAU;IACnD,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,iCAAiC;AACjC,oBAAY,WAAW;IACrB,yEAAyE;IACzE,MAAM,WAAW;IACjB,yEAAyE;IACzE,IAAI,SAAS;CACd;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,MAAM,GAAG,SAAS,EACzB,QAAQ,EAAE,MAAM,GACf,MAAM,CAGR;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,EAAE;IAChD,KAAK,EAAE,SAAS,EAAE,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAC,CAAC;IACtE,GAAG,CAAC,EAAE,WAAW,CAAC;CACnB,GAAG,iBAAiB,CA8FpB;AAuED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,wBAAwB,CACtC,KAAK,EAAE,iBAAiB,EACxB,cAAc,EAAE,MAAM,EACtB,eAAe,EAAE,MAAM,EACvB,UAAU,EAAE,MAAM,GACjB,iBAAiB,CAkBnB"}
@@ -1,4 +1,9 @@
1
1
  import { degToRad, normalizeAngle } from "./math.js";
2
+ var ArcFrameFit = /* @__PURE__ */ ((ArcFrameFit2) => {
3
+ ArcFrameFit2["square"] = "square";
4
+ ArcFrameFit2["bbox"] = "bbox";
5
+ return ArcFrameFit2;
6
+ })(ArcFrameFit || {});
2
7
  function normalizeArcAngle(value, fallback) {
3
8
  const v = Number.isFinite(value) ? value : fallback;
4
9
  return Math.min(180, Math.max(2, v));
@@ -11,7 +16,9 @@ function computeZoomToFitArcFrame(options) {
11
16
  extension,
12
17
  targetSize,
13
18
  margin = 0.06,
14
- includeBox
19
+ includeBox,
20
+ fit = "square"
21
+ /* square */
15
22
  } = options;
16
23
  if (areas.length === 0) {
17
24
  const side2 = targetSize;
@@ -61,13 +68,15 @@ function computeZoomToFitArcFrame(options) {
61
68
  const rawW = bbox.xMax - bbox.xMin;
62
69
  const rawH = bbox.yMax - bbox.yMin;
63
70
  const side = Math.max(rawW, rawH);
64
- const padded = side * (1 + margin * 2);
71
+ const pad = side * margin;
65
72
  const cx = (bbox.xMin + bbox.xMax) / 2;
66
73
  const cy = (bbox.yMin + bbox.yMax) / 2;
67
- const x = round4(cx - padded / 2);
68
- const y = round4(cy - padded / 2);
69
- const w = round4(padded);
70
- const h = round4(padded);
74
+ const boxW = fit === "bbox" ? rawW + pad * 2 : side + pad * 2;
75
+ const boxH = fit === "bbox" ? rawH + pad * 2 : side + pad * 2;
76
+ const x = round4(cx - boxW / 2);
77
+ const y = round4(cy - boxH / 2);
78
+ const w = round4(boxW);
79
+ const h = round4(boxH);
71
80
  return {
72
81
  radiusOffset,
73
82
  x,
@@ -155,6 +164,7 @@ function round4(n) {
155
164
  return Math.round(n * 1e4) / 1e4;
156
165
  }
157
166
  export {
167
+ ArcFrameFit,
158
168
  computeZoomToFitArcFrame,
159
169
  normalizeArcAngle,
160
170
  shiftArcFrameToOuterEdge
@@ -1 +1 @@
1
- {"version":3,"file":"arc-frame.js","sources":["../../src/svghelpers/arc-frame.ts"],"sourcesContent":["import type {WatchArea} from '../navigation-instruments/watch/watch.js';\nimport {degToRad, normalizeAngle} from './math.js';\n\nexport interface ArcViewBox {\n x: number;\n y: number;\n width: number;\n height: number;\n viewBox: string;\n}\n\nexport interface ZoomToFitArcFrame extends ArcViewBox {\n radiusOffset: number;\n}\n\n/**\n * Normalize a user-supplied watch-arc half-extent (in degrees) to a finite\n * value clamped into a sane range. Guards against `NaN`/`Infinity` produced\n * by Lit's Number attribute converter for invalid string attributes\n * (e.g. `<obc-pitch arc-angle=\"abc\">`), which would otherwise propagate\n * through trig and `viewBox` math and produce invalid SVG output.\n *\n * @param value Raw arc half-extent in degrees.\n * @param fallback Value to use when `value` is non-finite.\n * @returns A finite number in `[2, 180]`.\n */\nexport function normalizeArcAngle(\n value: number | undefined,\n fallback: number\n): number {\n const v = Number.isFinite(value) ? (value as number) : fallback;\n return Math.min(180, Math.max(2, v));\n}\n\n/**\n * Compute the optimal radius enlargement and square viewBox so that a\n * partial-arc watch face fills its container instead of leaving empty space.\n *\n * When `zoomToFitArc` is enabled on a clipped watch (e.g. rudder ±45°), the\n * visible arc segment is small relative to the full circle — lots of space\n * is wasted. This function finds a `radiusOffset` that, when added to every\n * radius constant, enlarges the circle so the visible band fills the target\n * viewport. Band thickness, tickmark sizes, and all decorative elements\n * remain unchanged in SVG units; only the curvature flattens because the\n * radius is larger.\n *\n * Uses binary search over `radiusOffset` so that the bounding box of the\n * annular arc (outer + extension … inner) fits within `targetSize × (1 − 2 × margin)`.\n *\n * @param options.areas Visible arc segments (same array passed to `<obc-watch>`).\n * @param options.outerRadius Base outer ring radius (typically `OUTER_RING_RADIUS`).\n * @param options.innerRadius Base innermost visible radius (depends on `WatchCircleType`).\n * @param options.extension Extra radial room beyond outer for tickmarks/labels.\n * @param options.targetSize Default SVG viewport side length, e.g. `(176 + padding) * 2`.\n * @param options.margin Fraction of computed extent used as padding (default 0.06).\n * @param options.includeBox Optional bounding box in SVG coordinates that must remain\n * inside the FINAL viewBox. Use this to keep a fixed central\n * element (e.g. a vessel image at the origin) visible after\n * the arc has been enlarged. Does NOT constrain how large\n * the arc can grow — it only widens/tallens the final\n * viewBox to include the requested region.\n */\nexport function computeZoomToFitArcFrame(options: {\n areas: WatchArea[];\n outerRadius: number;\n innerRadius: number;\n extension: number;\n targetSize: number;\n margin?: number;\n includeBox?: {xMin: number; yMin: number; xMax: number; yMax: number};\n}): ZoomToFitArcFrame {\n const {\n areas,\n outerRadius,\n innerRadius,\n extension,\n targetSize,\n margin = 0.06,\n includeBox,\n } = options;\n\n if (areas.length === 0) {\n const side = targetSize;\n return {\n radiusOffset: 0,\n x: -side / 2,\n y: -side / 2,\n width: side,\n height: side,\n viewBox: `${-side / 2} ${-side / 2} ${side} ${side}`,\n };\n }\n\n const available = targetSize * (1 - 2 * margin);\n\n // The search measures the arc-only bbox: folding `includeBox` in here would\n // let the origin-to-arc distance dominate and cap how far the arc can grow\n // at narrow `arcAngle`. It is applied to the final viewBox instead, so the\n // arc grows freely and the central element still fits.\n const measureSize = (radiusOffset: number) => {\n const bbox = computeAnnularArcBBox(\n areas,\n outerRadius + radiusOffset,\n innerRadius + radiusOffset,\n extension\n );\n return Math.max(bbox.xMax - bbox.xMin, bbox.yMax - bbox.yMin);\n };\n\n // Dynamic upper bound: double until the band bbox exceeds the available space.\n let lo = 0;\n let hi = targetSize;\n for (let i = 0; i < 16 && measureSize(hi) < available; i++) {\n hi *= 2;\n }\n\n for (let i = 0; i < 50; i++) {\n const mid = (lo + hi) / 2;\n const size = measureSize(mid);\n if (size < available) {\n lo = mid;\n } else {\n hi = mid;\n }\n }\n\n const radiusOffset = Math.max(0, lo);\n\n // Final bbox at the computed offset\n const bbox = expandBBoxToIncludeBox(\n computeAnnularArcBBox(\n areas,\n outerRadius + radiusOffset,\n innerRadius + radiusOffset,\n extension\n ),\n includeBox\n );\n const rawW = bbox.xMax - bbox.xMin;\n const rawH = bbox.yMax - bbox.yMin;\n const side = Math.max(rawW, rawH);\n const padded = side * (1 + margin * 2);\n\n const cx = (bbox.xMin + bbox.xMax) / 2;\n const cy = (bbox.yMin + bbox.yMax) / 2;\n\n const x = round4(cx - padded / 2);\n const y = round4(cy - padded / 2);\n const w = round4(padded);\n const h = round4(padded);\n\n return {\n radiusOffset,\n x,\n y,\n width: w,\n height: h,\n viewBox: `${x} ${y} ${w} ${h}`,\n };\n}\n\n/**\n * Bounding box of an annular arc (the visible band between inner and outer radii).\n *\n * Samples arc endpoints and cardinal-axis crossings on both the outermost and\n * innermost circles. The result frames just the band, NOT the circle centre.\n */\nfunction computeAnnularArcBBox(\n areas: WatchArea[],\n outerRadius: number,\n innerRadius: number,\n extension: number\n): {xMin: number; xMax: number; yMin: number; yMax: number} {\n const R_vis = outerRadius + extension;\n const R_in = innerRadius;\n\n let xMin = Infinity;\n let xMax = -Infinity;\n let yMin = Infinity;\n let yMax = -Infinity;\n\n const expand = (x: number, y: number) => {\n if (x < xMin) xMin = x;\n if (x > xMax) xMax = x;\n if (y < yMin) yMin = y;\n if (y > yMax) yMax = y;\n };\n\n for (const area of areas) {\n const startRad = degToRad(area.startAngle);\n const endRad = degToRad(area.endAngle);\n\n for (const R of [R_vis, R_in]) {\n expand(R * Math.sin(startRad), -R * Math.cos(startRad));\n expand(R * Math.sin(endRad), -R * Math.cos(endRad));\n }\n\n const startNorm = normalizeAngle(area.startAngle);\n const endNorm = normalizeAngle(area.endAngle);\n const axes = [0, 90, 180, 270];\n for (const axis of axes) {\n if (arcContainsAngle(startNorm, endNorm, axis)) {\n const axisRad = degToRad(axis);\n for (const R of [R_vis, R_in]) {\n expand(R * Math.sin(axisRad), -R * Math.cos(axisRad));\n }\n }\n }\n }\n\n if (xMin === Infinity) {\n return {xMin: 0, xMax: 0, yMin: 0, yMax: 0};\n }\n\n return {xMin, xMax, yMin, yMax};\n}\n\nfunction expandBBoxToIncludeBox(\n bbox: {xMin: number; xMax: number; yMin: number; yMax: number},\n box: {xMin: number; yMin: number; xMax: number; yMax: number} | undefined\n): {xMin: number; xMax: number; yMin: number; yMax: number} {\n if (!box) return bbox;\n return {\n xMin: Math.min(bbox.xMin, box.xMin),\n xMax: Math.max(bbox.xMax, box.xMax),\n yMin: Math.min(bbox.yMin, box.yMin),\n yMax: Math.max(bbox.yMax, box.yMax),\n };\n}\n\n/**\n * Shift an arc viewBox so the arc's outer-edge midpoint coincides with the\n * outer edge of a co-located reference frame (e.g. the central vessel layer\n * with viewBox `-centreHalf -centreHalf 2*centreHalf 2*centreHalf`).\n *\n * The direction is taken from the arc bbox-center, so single-side arcs\n * (pitch on the left, roll on top, rudder on the bottom) are pushed\n * outward to that side. Four-way symmetric arcs (pitch-roll) keep their\n * origin-centred bbox and the function becomes a no-op.\n *\n * The arc's pixel size is preserved; only `x`, `y` and `viewBox` change.\n *\n * @param frame The arc frame returned by `computeZoomToFitArcFrame`.\n * @param arcOuterRadius Outer radius of the arc band INCLUDING `radiusOffset`\n * (i.e. `outerRadius + frame.radiusOffset`). Extension\n * padding is intentionally excluded — the visible band\n * edge is what should align with the reference frame.\n * @param referenceRadius Distance from origin to the reference frame's outer\n * edge, in the reference frame's SVG units. Typically\n * `OUTER_RING_RADIUS` for the original instrument\n * outer ring, or `centreHalf` to align with the\n * container edge directly.\n * @param centreHalf Half-side of the reference frame's square viewBox in\n * SVG units. The arc's outer-edge midpoint is placed\n * on `referenceRadius` in the same direction.\n */\nexport function shiftArcFrameToOuterEdge(\n frame: ZoomToFitArcFrame,\n arcOuterRadius: number,\n referenceRadius: number,\n centreHalf: number\n): ZoomToFitArcFrame {\n const cx = frame.x + frame.width / 2;\n const cy = frame.y + frame.height / 2;\n const dist = Math.hypot(cx, cy);\n if (dist < 0.001) return frame;\n const ux = cx / dist;\n const uy = cy / dist;\n const r = referenceRadius / centreHalf;\n const outerX = arcOuterRadius * ux;\n const outerY = arcOuterRadius * uy;\n const newX = round4(outerX - (frame.width * (r * ux + 1)) / 2);\n const newY = round4(outerY - (frame.height * (r * uy + 1)) / 2);\n return {\n ...frame,\n x: newX,\n y: newY,\n viewBox: `${newX} ${newY} ${frame.width} ${frame.height}`,\n };\n}\n\nfunction arcContainsAngle(\n startDeg: number,\n endDeg: number,\n testDeg: number\n): boolean {\n const s = normalizeAngle(startDeg);\n const e = normalizeAngle(endDeg);\n const t = normalizeAngle(testDeg);\n if (s <= e) {\n return t >= s && t <= e;\n }\n return t >= s || t <= e;\n}\n\nfunction round4(n: number): number {\n return Math.round(n * 10000) / 10000;\n}\n"],"names":["side","radiusOffset","bbox"],"mappings":"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"}
1
+ {"version":3,"file":"arc-frame.js","sources":["../../src/svghelpers/arc-frame.ts"],"sourcesContent":["import type {WatchArea} from '../navigation-instruments/watch/watch.js';\nimport {degToRad, normalizeAngle} from './math.js';\n\nexport interface ArcViewBox {\n x: number;\n y: number;\n width: number;\n height: number;\n viewBox: string;\n}\n\nexport interface ZoomToFitArcFrame extends ArcViewBox {\n radiusOffset: number;\n}\n\n/** Shape of the zoom viewBox. */\nexport enum ArcFrameFit {\n /** Square around the arc — what instruments with a round face expect. */\n square = 'square',\n /** The arc's own bounding box, which crops a flat arc's empty height. */\n bbox = 'bbox',\n}\n\n/**\n * Normalize a user-supplied watch-arc half-extent (in degrees) to a finite\n * value clamped into a sane range. Guards against `NaN`/`Infinity` produced\n * by Lit's Number attribute converter for invalid string attributes\n * (e.g. `<obc-pitch arc-angle=\"abc\">`), which would otherwise propagate\n * through trig and `viewBox` math and produce invalid SVG output.\n *\n * @param value Raw arc half-extent in degrees.\n * @param fallback Value to use when `value` is non-finite.\n * @returns A finite number in `[2, 180]`.\n */\nexport function normalizeArcAngle(\n value: number | undefined,\n fallback: number\n): number {\n const v = Number.isFinite(value) ? (value as number) : fallback;\n return Math.min(180, Math.max(2, v));\n}\n\n/**\n * Compute the optimal radius enlargement and square viewBox so that a\n * partial-arc watch face fills its container instead of leaving empty space.\n *\n * When `zoomToFitArc` is enabled on a clipped watch (e.g. rudder ±45°), the\n * visible arc segment is small relative to the full circle — lots of space\n * is wasted. This function finds a `radiusOffset` that, when added to every\n * radius constant, enlarges the circle so the visible band fills the target\n * viewport. Band thickness, tickmark sizes, and all decorative elements\n * remain unchanged in SVG units; only the curvature flattens because the\n * radius is larger.\n *\n * Uses binary search over `radiusOffset` so that the bounding box of the\n * annular arc (outer + extension … inner) fits within `targetSize × (1 − 2 × margin)`.\n *\n * @param options.areas Visible arc segments (same array passed to `<obc-watch>`).\n * @param options.outerRadius Base outer ring radius (typically `OUTER_RING_RADIUS`).\n * @param options.innerRadius Base innermost visible radius (depends on `WatchCircleType`).\n * @param options.extension Extra radial room beyond outer for tickmarks/labels.\n * @param options.targetSize Default SVG viewport side length, e.g. `(176 + padding) * 2`.\n * @param options.margin Fraction of computed extent used as padding (default 0.06).\n * @param options.includeBox Optional bounding box in SVG coordinates that must remain\n * inside the FINAL viewBox. Use this to keep a fixed central\n * element (e.g. a vessel image at the origin) visible after\n * the arc has been enlarged. Does NOT constrain how large\n * the arc can grow — it only widens/tallens the final\n * viewBox to include the requested region.\n */\nexport function computeZoomToFitArcFrame(options: {\n areas: WatchArea[];\n outerRadius: number;\n innerRadius: number;\n extension: number;\n targetSize: number;\n margin?: number;\n includeBox?: {xMin: number; yMin: number; xMax: number; yMax: number};\n fit?: ArcFrameFit;\n}): ZoomToFitArcFrame {\n const {\n areas,\n outerRadius,\n innerRadius,\n extension,\n targetSize,\n margin = 0.06,\n includeBox,\n fit = ArcFrameFit.square,\n } = options;\n\n if (areas.length === 0) {\n const side = targetSize;\n return {\n radiusOffset: 0,\n x: -side / 2,\n y: -side / 2,\n width: side,\n height: side,\n viewBox: `${-side / 2} ${-side / 2} ${side} ${side}`,\n };\n }\n\n const available = targetSize * (1 - 2 * margin);\n\n // The search measures the arc-only bbox: folding `includeBox` in here would\n // let the origin-to-arc distance dominate and cap how far the arc can grow\n // at narrow `arcAngle`. It is applied to the final viewBox instead, so the\n // arc grows freely and the central element still fits.\n const measureSize = (radiusOffset: number) => {\n const bbox = computeAnnularArcBBox(\n areas,\n outerRadius + radiusOffset,\n innerRadius + radiusOffset,\n extension\n );\n return Math.max(bbox.xMax - bbox.xMin, bbox.yMax - bbox.yMin);\n };\n\n // Dynamic upper bound: double until the band bbox exceeds the available space.\n let lo = 0;\n let hi = targetSize;\n for (let i = 0; i < 16 && measureSize(hi) < available; i++) {\n hi *= 2;\n }\n\n for (let i = 0; i < 50; i++) {\n const mid = (lo + hi) / 2;\n const size = measureSize(mid);\n if (size < available) {\n lo = mid;\n } else {\n hi = mid;\n }\n }\n\n const radiusOffset = Math.max(0, lo);\n\n // Final bbox at the computed offset\n const bbox = expandBBoxToIncludeBox(\n computeAnnularArcBBox(\n areas,\n outerRadius + radiusOffset,\n innerRadius + radiusOffset,\n extension\n ),\n includeBox\n );\n const rawW = bbox.xMax - bbox.xMin;\n const rawH = bbox.yMax - bbox.yMin;\n const side = Math.max(rawW, rawH);\n const pad = side * margin;\n\n const cx = (bbox.xMin + bbox.xMax) / 2;\n const cy = (bbox.yMin + bbox.yMax) / 2;\n\n // 'bbox' hugs the arc; a flat arc in a square box is mostly empty.\n const boxW = fit === ArcFrameFit.bbox ? rawW + pad * 2 : side + pad * 2;\n const boxH = fit === ArcFrameFit.bbox ? rawH + pad * 2 : side + pad * 2;\n\n const x = round4(cx - boxW / 2);\n const y = round4(cy - boxH / 2);\n const w = round4(boxW);\n const h = round4(boxH);\n\n return {\n radiusOffset,\n x,\n y,\n width: w,\n height: h,\n viewBox: `${x} ${y} ${w} ${h}`,\n };\n}\n\n/**\n * Bounding box of an annular arc (the visible band between inner and outer radii).\n *\n * Samples arc endpoints and cardinal-axis crossings on both the outermost and\n * innermost circles. The result frames just the band, NOT the circle centre.\n */\nfunction computeAnnularArcBBox(\n areas: WatchArea[],\n outerRadius: number,\n innerRadius: number,\n extension: number\n): {xMin: number; xMax: number; yMin: number; yMax: number} {\n const R_vis = outerRadius + extension;\n const R_in = innerRadius;\n\n let xMin = Infinity;\n let xMax = -Infinity;\n let yMin = Infinity;\n let yMax = -Infinity;\n\n const expand = (x: number, y: number) => {\n if (x < xMin) xMin = x;\n if (x > xMax) xMax = x;\n if (y < yMin) yMin = y;\n if (y > yMax) yMax = y;\n };\n\n for (const area of areas) {\n const startRad = degToRad(area.startAngle);\n const endRad = degToRad(area.endAngle);\n\n for (const R of [R_vis, R_in]) {\n expand(R * Math.sin(startRad), -R * Math.cos(startRad));\n expand(R * Math.sin(endRad), -R * Math.cos(endRad));\n }\n\n const startNorm = normalizeAngle(area.startAngle);\n const endNorm = normalizeAngle(area.endAngle);\n const axes = [0, 90, 180, 270];\n for (const axis of axes) {\n if (arcContainsAngle(startNorm, endNorm, axis)) {\n const axisRad = degToRad(axis);\n for (const R of [R_vis, R_in]) {\n expand(R * Math.sin(axisRad), -R * Math.cos(axisRad));\n }\n }\n }\n }\n\n if (xMin === Infinity) {\n return {xMin: 0, xMax: 0, yMin: 0, yMax: 0};\n }\n\n return {xMin, xMax, yMin, yMax};\n}\n\nfunction expandBBoxToIncludeBox(\n bbox: {xMin: number; xMax: number; yMin: number; yMax: number},\n box: {xMin: number; yMin: number; xMax: number; yMax: number} | undefined\n): {xMin: number; xMax: number; yMin: number; yMax: number} {\n if (!box) return bbox;\n return {\n xMin: Math.min(bbox.xMin, box.xMin),\n xMax: Math.max(bbox.xMax, box.xMax),\n yMin: Math.min(bbox.yMin, box.yMin),\n yMax: Math.max(bbox.yMax, box.yMax),\n };\n}\n\n/**\n * Shift an arc viewBox so the arc's outer-edge midpoint coincides with the\n * outer edge of a co-located reference frame (e.g. the central vessel layer\n * with viewBox `-centreHalf -centreHalf 2*centreHalf 2*centreHalf`).\n *\n * The direction is taken from the arc bbox-center, so single-side arcs\n * (pitch on the left, roll on top, rudder on the bottom) are pushed\n * outward to that side. Four-way symmetric arcs (pitch-roll) keep their\n * origin-centred bbox and the function becomes a no-op.\n *\n * The arc's pixel size is preserved; only `x`, `y` and `viewBox` change.\n *\n * @param frame The arc frame returned by `computeZoomToFitArcFrame`.\n * @param arcOuterRadius Outer radius of the arc band INCLUDING `radiusOffset`\n * (i.e. `outerRadius + frame.radiusOffset`). Extension\n * padding is intentionally excluded — the visible band\n * edge is what should align with the reference frame.\n * @param referenceRadius Distance from origin to the reference frame's outer\n * edge, in the reference frame's SVG units. Typically\n * `OUTER_RING_RADIUS` for the original instrument\n * outer ring, or `centreHalf` to align with the\n * container edge directly.\n * @param centreHalf Half-side of the reference frame's square viewBox in\n * SVG units. The arc's outer-edge midpoint is placed\n * on `referenceRadius` in the same direction.\n */\nexport function shiftArcFrameToOuterEdge(\n frame: ZoomToFitArcFrame,\n arcOuterRadius: number,\n referenceRadius: number,\n centreHalf: number\n): ZoomToFitArcFrame {\n const cx = frame.x + frame.width / 2;\n const cy = frame.y + frame.height / 2;\n const dist = Math.hypot(cx, cy);\n if (dist < 0.001) return frame;\n const ux = cx / dist;\n const uy = cy / dist;\n const r = referenceRadius / centreHalf;\n const outerX = arcOuterRadius * ux;\n const outerY = arcOuterRadius * uy;\n const newX = round4(outerX - (frame.width * (r * ux + 1)) / 2);\n const newY = round4(outerY - (frame.height * (r * uy + 1)) / 2);\n return {\n ...frame,\n x: newX,\n y: newY,\n viewBox: `${newX} ${newY} ${frame.width} ${frame.height}`,\n };\n}\n\nfunction arcContainsAngle(\n startDeg: number,\n endDeg: number,\n testDeg: number\n): boolean {\n const s = normalizeAngle(startDeg);\n const e = normalizeAngle(endDeg);\n const t = normalizeAngle(testDeg);\n if (s <= e) {\n return t >= s && t <= e;\n }\n return t >= s || t <= e;\n}\n\nfunction round4(n: number): number {\n return Math.round(n * 10000) / 10000;\n}\n"],"names":["ArcFrameFit","side","radiusOffset","bbox"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  import { WatchArea } from '../navigation-instruments/watch/watch.js';
2
- import { ZoomToFitArcFrame } from './arc-frame.js';
2
+ import { ArcFrameFit, ZoomToFitArcFrame } from './arc-frame.js';
3
3
  /** Approximate digit advance at 12px font-instrument-label. */
4
4
  export declare const DIGIT_WIDTH_PX = 7;
5
5
  /** Compass NSWE label width assumed by label.ts (`labelWidth = 16`). */
@@ -63,6 +63,19 @@ export interface RadialFrameOptions {
63
63
  areas?: WatchArea[];
64
64
  /** Innermost visible radius (depends on WatchCircleType); zoom path only. */
65
65
  innerRadius?: number;
66
+ /** Zoom viewBox shape; `bbox` crops a flat arc's empty height. */
67
+ zoomFit?: ArcFrameFit;
68
+ /**
69
+ * Region that must stay inside the zoomed viewBox — an overlay the arc's own
70
+ * box would leave out, such as a needle reaching in toward the centre. It
71
+ * widens the final box only; the arc still grows freely.
72
+ */
73
+ zoomIncludeBox?: {
74
+ xMin: number;
75
+ yMin: number;
76
+ xMax: number;
77
+ yMax: number;
78
+ };
66
79
  }
67
80
  export interface RadialFrame extends ZoomToFitArcFrame {
68
81
  /** Px per SVG unit implied by container or faceDiameter (1 when unknown). */
@@ -1 +1 @@
1
- {"version":3,"file":"radial-frame.d.ts","sourceRoot":"","sources":["../../src/svghelpers/radial-frame.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,0CAA0C,CAAC;AACxE,OAAO,EAA2B,KAAK,iBAAiB,EAAC,MAAM,gBAAgB,CAAC;AA6DhF,+DAA+D;AAC/D,eAAO,MAAM,cAAc,IAAI,CAAC;AAChC,wEAAwE;AACxE,eAAO,MAAM,mBAAmB,KAAK,CAAC;AACtC;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,KAAK,CAAC;AACvC;;;;;;GAMG;AACH,eAAO,MAAM,0BAA0B,OAAO,CAAC;AAC/C;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,KAAK,CAAC;AAC3C;;;GAGG;AACH,eAAO,MAAM,kBAAkB,KAAK,CAAC;AAErC,6EAA6E;AAC7E,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,kBAAkB;IACjC,+EAA+E;IAC/E,WAAW,EAAE,MAAM,CAAC;IACpB,2EAA2E;IAC3E,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,mEAAmE;IACnE,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,8EAA8E;IAC9E,WAAW,CAAC,EAAE;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAC,CAAC;IAC9C,8EAA8E;IAC9E,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,wDAAwD;IACxD,KAAK,CAAC,EAAE,SAAS,EAAE,CAAC;IACpB,6EAA6E;IAC7E,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,WAAY,SAAQ,iBAAiB;IACpD,6EAA6E;IAC7E,KAAK,EAAE,MAAM,CAAC;IACd,gFAAgF;IAChF,YAAY,EAAE,MAAM,CAAC;IACrB,8EAA8E;IAC9E,YAAY,EAAE,OAAO,CAAC;IACtB;;;;OAIG;IACH,aAAa,EAAE,OAAO,CAAC;IACvB,sEAAsE;IACtE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,sEAAsE;IACtE,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,WAAW,GAAG;IACrD,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB,CAWA;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,WAAW,EACjB,KAAK,EAAE,WAAW,GAAG,SAAS,EAC9B,SAAS,EAAE,OAAO,GACjB,OAAO,CAaT;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAC7B,UAAU,EAAE;IAAC,OAAO,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI,CAAA;CAAC,EAC5C,UAAU,EAAE,UAAU,GACrB,IAAI,CAKN;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,SAAS,CAAC,MAAM,GAAG,SAAS,CAAC,EAAE,GACrC,MAAM,CAQR;AAoJD;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,kBAAkB,GAAG,WAAW,CA4DxE"}
1
+ {"version":3,"file":"radial-frame.d.ts","sourceRoot":"","sources":["../../src/svghelpers/radial-frame.ts"],"names":[],"mappings":"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"}