Skip to content

Commit 2cf6e47

Browse files
authored
Merge pull request #146 from LennartvdM/codex/fix-invalid-center-in-radialurchin
Guard radial urchin from invalid center payloads
2 parents c186bf4 + 38008a6 commit 2cf6e47

2 files changed

Lines changed: 80 additions & 29 deletions

File tree

web/app.js

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -295,18 +295,32 @@ function updateVisuals(payload) {
295295
}
296296
}
297297
}
298-
} else {
299-
maybeCreateUrchinInstance(lastVisualSchedule);
298+
return;
300299
}
301300

302-
if (visualsState.urchin) {
303-
try {
304-
visualsState.urchin.update({ data: visualsState.useLegacy ? null : lastVisualSchedule });
305-
} catch (error) {
306-
console.error('[visuals] failed to update radial urchin:', error);
301+
const readyForRadial = canRenderRadialVisuals();
302+
if (!readyForRadial) {
303+
if (visualsState.urchin) {
304+
resetVisualsInstance();
307305
}
308-
} else if (!visualsState.useLegacy && visualsState.mount && visualsState.mount.childElementCount > 0) {
309-
visualsState.mount.replaceChildren();
306+
return;
307+
}
308+
309+
if (!hasVisualEvents(lastVisualSchedule)) {
310+
resetVisualsInstance();
311+
return;
312+
}
313+
314+
maybeCreateUrchinInstance(lastVisualSchedule);
315+
316+
if (!visualsState.urchin) {
317+
return;
318+
}
319+
320+
try {
321+
visualsState.urchin.update({ data: lastVisualSchedule });
322+
} catch (error) {
323+
console.error('[visuals] failed to update radial urchin:', error);
310324
}
311325
}
312326

@@ -331,11 +345,23 @@ function hasVisualEvents(payload) {
331345
return Boolean(schedule && Array.isArray(schedule.events) && schedule.events.length > 0);
332346
}
333347

348+
function canRenderRadialVisuals() {
349+
if (visualsState.useLegacy) {
350+
return false;
351+
}
352+
const mount = visualsState.mount;
353+
if (!(mount instanceof HTMLElement) || !mount.isConnected) {
354+
return false;
355+
}
356+
return true;
357+
}
358+
334359
function maybeCreateUrchinInstance(schedule) {
335360
if (
336361
visualsState.useLegacy ||
337362
visualsState.urchin ||
338363
!visualsState.mount ||
364+
!visualsState.mount.isConnected ||
339365
!schedule ||
340366
!hasVisualEvents(schedule)
341367
) {

web/ui/visuals/RadialUrchin.js

Lines changed: 45 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,17 @@ export class RadialUrchin {
152152
this.update(this.props);
153153
}
154154

155+
hasValidCenter() {
156+
const center = this.center;
157+
return (
158+
!!center &&
159+
typeof center.x === 'number' &&
160+
Number.isFinite(center.x) &&
161+
typeof center.y === 'number' &&
162+
Number.isFinite(center.y)
163+
);
164+
}
165+
155166
setupDom() {
156167
this.root.classList.add('radial-urchin-root');
157168
this.root.setAttribute('tabindex', '0');
@@ -663,13 +674,7 @@ export class RadialUrchin {
663674
return;
664675
}
665676

666-
if (
667-
!this.center ||
668-
typeof this.center.x !== 'number' ||
669-
Number.isNaN(this.center.x) ||
670-
typeof this.center.y !== 'number' ||
671-
Number.isNaN(this.center.y)
672-
) {
677+
if (!this.hasValidCenter()) {
673678
if (!this.didWarnInvalidCenter) {
674679
console.warn('[RadialUrchin] invalid center point, skipping render', this.center);
675680
this.didWarnInvalidCenter = true;
@@ -696,7 +701,9 @@ export class RadialUrchin {
696701
const dpr = window.devicePixelRatio || 1;
697702
ctx.save();
698703
ctx.scale(dpr, dpr);
699-
ctx.translate(this.center.x, this.center.y);
704+
const center = this.center;
705+
706+
ctx.translate(center.x, center.y);
700707
ctx.lineWidth = 1;
701708
ctx.lineCap = 'butt';
702709
ctx.lineJoin = 'round';
@@ -768,9 +775,13 @@ export class RadialUrchin {
768775
}
769776

770777
processHoverAtPoint(point) {
771-
if (!this.center) {
778+
if (!this.hasValidCenter()) {
779+
this.state.hoverArc = null;
780+
this.hoverPath.setAttribute('d', '');
781+
this.hideTooltip();
772782
return;
773783
}
784+
const center = this.center;
774785
const layout = {
775786
arcs: this.displayArcs.map((arc) => ({
776787
...arc,
@@ -787,8 +798,8 @@ export class RadialUrchin {
787798
}
788799
this.state.hoverArc = hovered;
789800
const path = describeSegmentPath(
790-
this.center.x,
791-
this.center.y,
801+
center.x,
802+
center.y,
792803
hovered.innerRadius,
793804
hovered.outerRadius,
794805
hovered.startAngle,
@@ -886,10 +897,15 @@ export class RadialUrchin {
886897
const html = buildTooltipContent(arc);
887898
this.tooltipMeta.innerHTML = html;
888899
this.tooltip.hidden = false;
900+
if (!this.hasValidCenter()) {
901+
this.hideTooltip();
902+
return;
903+
}
904+
const center = this.center;
889905
const angle = arc.centerAngle;
890906
const radius = (arc.innerRadius + arc.outerRadius) / 2;
891-
const x = this.center.x + Math.cos(angle) * radius;
892-
const y = this.center.y + Math.sin(angle) * radius;
907+
const x = center.x + Math.cos(angle) * radius;
908+
const y = center.y + Math.sin(angle) * radius;
893909
this.tooltip.style.left = `${x + 12}px`;
894910
this.tooltip.style.top = `${y + 12}px`;
895911
}
@@ -900,13 +916,14 @@ export class RadialUrchin {
900916

901917
updateSelectionOverlay() {
902918
const arc = this.state.selectedArc;
903-
if (!arc) {
919+
if (!arc || !this.hasValidCenter()) {
904920
this.selectionPath.setAttribute('d', '');
905921
return;
906922
}
923+
const center = this.center;
907924
const path = describeSegmentPath(
908-
this.center.x,
909-
this.center.y,
925+
center.x,
926+
center.y,
910927
arc.innerRadius,
911928
arc.outerRadius,
912929
arc.startAngle,
@@ -916,13 +933,21 @@ export class RadialUrchin {
916933
}
917934

918935
updateScrubOverlay() {
936+
if (!this.hasValidCenter()) {
937+
this.scrubLine.setAttribute('x1', '0');
938+
this.scrubLine.setAttribute('y1', '0');
939+
this.scrubLine.setAttribute('x2', '0');
940+
this.scrubLine.setAttribute('y2', '0');
941+
return;
942+
}
943+
const center = this.center;
919944
const minutes = this.state.scrubMinutes;
920945
const angle = this.mapMinutesToAngle(minutes - this.getZoom().start);
921946
const radius = this.displayMaxRadius ?? (this.layout?.maxRadius ?? 160);
922-
const x2 = this.center.x + Math.cos(angle) * radius;
923-
const y2 = this.center.y + Math.sin(angle) * radius;
924-
this.scrubLine.setAttribute('x1', String(this.center.x));
925-
this.scrubLine.setAttribute('y1', String(this.center.y));
947+
const x2 = center.x + Math.cos(angle) * radius;
948+
const y2 = center.y + Math.sin(angle) * radius;
949+
this.scrubLine.setAttribute('x1', String(center.x));
950+
this.scrubLine.setAttribute('y1', String(center.y));
926951
this.scrubLine.setAttribute('x2', String(x2));
927952
this.scrubLine.setAttribute('y2', String(y2));
928953
}

0 commit comments

Comments
 (0)