Skip to content

Commit 5248966

Browse files
committed
Add debugger context menu
1 parent 0139a5a commit 5248966

File tree

2 files changed

+225
-32
lines changed

2 files changed

+225
-32
lines changed

lib/react/GenericSolverDebugger.tsx

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import React, { useEffect, useMemo, useReducer, useState } from "react"
22
import type { BaseSolver } from "../BaseSolver"
33
import type { BasePipelineSolver } from "../BasePipelineSolver"
4-
import { InteractiveGraphics } from "graphics-debug/react"
4+
import {
5+
InteractiveGraphics,
6+
InteractiveGraphicsCanvas,
7+
} from "graphics-debug/react"
58
import { GenericSolverToolbar } from "./GenericSolverToolbar"
69
import { PipelineStagesTable } from "./PipelineStagesTable"
710
import { SimpleGraphicsSVG } from "./SimpleGraphicsSVG"
@@ -54,6 +57,13 @@ export const GenericSolverDebugger = ({
5457
}
5558
return solverProp
5659
})
60+
const [renderer, setRenderer] = useState<"vector" | "canvas">(() => {
61+
if (typeof window === "undefined") return "vector"
62+
const stored = window.localStorage.getItem("solver-utils-renderer")
63+
return stored === "canvas" ? "canvas" : "vector"
64+
})
65+
const [currentAnimationSpeed, setCurrentAnimationSpeed] =
66+
useState(animationSpeed)
5767

5868
const visualization = useMemo(() => {
5969
try {
@@ -88,6 +98,10 @@ export const GenericSolverDebugger = ({
8898
}
8999
}, [])
90100

101+
useEffect(() => {
102+
setCurrentAnimationSpeed(animationSpeed)
103+
}, [animationSpeed])
104+
91105
const isPipelineSolver = (solver as any).pipelineDef !== undefined
92106

93107
const handleStepUntilPhase = (phaseName: string) => {
@@ -108,12 +122,38 @@ export const GenericSolverDebugger = ({
108122
}
109123
}
110124

125+
const handleDownloadVisualization = () => {
126+
const visualizationJson = JSON.stringify(visualization, null, 2)
127+
const blob = new Blob([visualizationJson], {
128+
type: "application/json",
129+
})
130+
const url = URL.createObjectURL(blob)
131+
const a = document.createElement("a")
132+
a.download = "visualization.json"
133+
a.href = url
134+
document.body.appendChild(a)
135+
a.click()
136+
document.body.removeChild(a)
137+
URL.revokeObjectURL(url)
138+
}
139+
140+
const handleRendererChange = (nextRenderer: "vector" | "canvas") => {
141+
setRenderer(nextRenderer)
142+
if (typeof window !== "undefined") {
143+
window.localStorage.setItem("solver-utils-renderer", nextRenderer)
144+
}
145+
}
146+
111147
return (
112148
<div>
113149
<GenericSolverToolbar
114150
solver={solver}
115151
triggerRender={incRenderCount}
116-
animationSpeed={animationSpeed}
152+
animationSpeed={currentAnimationSpeed}
153+
renderer={renderer}
154+
onRendererChange={handleRendererChange}
155+
onAnimationSpeedChange={setCurrentAnimationSpeed}
156+
onDownloadVisualization={handleDownloadVisualization}
117157
onSolverStarted={onSolverStarted}
118158
onSolverCompleted={onSolverCompleted}
119159
/>
@@ -125,7 +165,11 @@ export const GenericSolverDebugger = ({
125165
<SimpleGraphicsSVG graphics={visualization as GraphicsObject} />
126166
}
127167
>
128-
<InteractiveGraphics graphics={visualization} />
168+
{renderer === "canvas" ? (
169+
<InteractiveGraphicsCanvas graphics={visualization} />
170+
) : (
171+
<InteractiveGraphics graphics={visualization} />
172+
)}
129173
</ErrorBoundary>
130174
)}
131175
{isPipelineSolver && (

lib/react/GenericSolverToolbar.tsx

Lines changed: 178 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
1-
import React, { useReducer, useRef, useEffect } from "react"
1+
import React, { useReducer, useRef, useEffect, useMemo, useState } from "react"
22
import type { BaseSolver } from "../BaseSolver"
33
import { SolverBreadcrumbInputDownloader } from "./SolverBreadcrumbInputDownloader"
44

5+
type RendererOption = "vector" | "canvas"
6+
57
export interface GenericSolverToolbarProps {
68
solver: BaseSolver
79
triggerRender: () => void
810
animationSpeed?: number
11+
renderer: RendererOption
12+
onRendererChange: (renderer: RendererOption) => void
13+
onAnimationSpeedChange: (speed: number) => void
14+
onDownloadVisualization: () => void
915
onSolverStarted?: (solver: BaseSolver) => void
1016
onSolverCompleted?: (solver: BaseSolver) => void
1117
}
@@ -14,13 +20,57 @@ export const GenericSolverToolbar = ({
1420
solver,
1521
triggerRender,
1622
animationSpeed = 25,
23+
renderer,
24+
onRendererChange,
25+
onAnimationSpeedChange,
26+
onDownloadVisualization,
1727
onSolverStarted,
1828
onSolverCompleted,
1929
}: GenericSolverToolbarProps) => {
2030
const [isAnimating, setIsAnimating] = useReducer((x) => !x, false)
2131
const animationRef = useRef<NodeJS.Timeout | undefined>(undefined)
2232
const lastIterationInputRef = useRef<string | null>(null)
2333
const lastIterationStorageKey = "solver-debugger-last-iteration"
34+
const [openMenu, setOpenMenu] = useState<
35+
"renderer" | "debug" | "animation" | null
36+
>(null)
37+
const menuContainerRef = useRef<HTMLDivElement | null>(null)
38+
39+
const animationSpeedOptions = useMemo(
40+
() => [
41+
{ label: "Slow", value: 250 },
42+
{ label: "Normal", value: 100 },
43+
{ label: "Fast", value: 25 },
44+
{ label: "Very Fast", value: 10 },
45+
],
46+
[],
47+
)
48+
49+
const startAnimation = () => {
50+
animationRef.current = setInterval(() => {
51+
if (solver.solved || solver.failed) {
52+
if (animationRef.current) {
53+
clearInterval(animationRef.current)
54+
animationRef.current = undefined
55+
}
56+
setIsAnimating()
57+
triggerRender()
58+
if (onSolverCompleted && solver.solved) {
59+
onSolverCompleted(solver)
60+
}
61+
return
62+
}
63+
solver.step()
64+
triggerRender()
65+
}, animationSpeed)
66+
}
67+
68+
const stopAnimation = () => {
69+
if (animationRef.current) {
70+
clearInterval(animationRef.current)
71+
animationRef.current = undefined
72+
}
73+
}
2474

2575
const handleStep = () => {
2676
if (!solver.solved && !solver.failed) {
@@ -44,29 +94,11 @@ export const GenericSolverToolbar = ({
4494

4595
const handleAnimate = () => {
4696
if (isAnimating) {
47-
if (animationRef.current) {
48-
clearInterval(animationRef.current)
49-
animationRef.current = undefined
50-
}
97+
stopAnimation()
5198
setIsAnimating()
5299
} else {
53100
setIsAnimating()
54-
animationRef.current = setInterval(() => {
55-
if (solver.solved || solver.failed) {
56-
if (animationRef.current) {
57-
clearInterval(animationRef.current)
58-
animationRef.current = undefined
59-
}
60-
setIsAnimating()
61-
triggerRender()
62-
if (onSolverCompleted && solver.solved) {
63-
onSolverCompleted(solver)
64-
}
65-
return
66-
}
67-
solver.step()
68-
triggerRender()
69-
}, animationSpeed)
101+
startAnimation()
70102
}
71103
}
72104

@@ -119,26 +151,143 @@ export const GenericSolverToolbar = ({
119151
// Cleanup animation on unmount or solver completion
120152
useEffect(() => {
121153
return () => {
122-
if (animationRef.current) {
123-
clearInterval(animationRef.current)
124-
}
154+
stopAnimation()
125155
}
126156
}, [])
127157

128158
useEffect(() => {
129159
if ((solver.solved || solver.failed) && isAnimating) {
130-
if (animationRef.current) {
131-
clearInterval(animationRef.current)
132-
animationRef.current = undefined
133-
}
160+
stopAnimation()
134161
setIsAnimating()
135162
}
136163
}, [solver.solved, solver.failed, isAnimating])
137164

165+
useEffect(() => {
166+
if (isAnimating) {
167+
stopAnimation()
168+
startAnimation()
169+
}
170+
}, [animationSpeed, isAnimating])
171+
172+
useEffect(() => {
173+
if (typeof document === "undefined") return
174+
const handleClickOutside = (event: MouseEvent) => {
175+
if (
176+
menuContainerRef.current &&
177+
!menuContainerRef.current.contains(event.target as Node)
178+
) {
179+
setOpenMenu(null)
180+
}
181+
}
182+
document.addEventListener("mousedown", handleClickOutside)
183+
return () => {
184+
document.removeEventListener("mousedown", handleClickOutside)
185+
}
186+
}, [])
187+
138188
return (
139189
<div className="space-y-2 p-2 border-b">
140190
<div className="flex items-center">
141-
<SolverBreadcrumbInputDownloader solver={solver} />
191+
<div className="flex items-center gap-2" ref={menuContainerRef}>
192+
<div className="flex h-9 items-center space-x-1 rounded-md border border-slate-200 bg-white p-1 shadow-sm">
193+
<div className="relative">
194+
<button
195+
type="button"
196+
onClick={() =>
197+
setOpenMenu(openMenu === "renderer" ? null : "renderer")
198+
}
199+
className="flex select-none items-center rounded-sm px-3 py-1 text-sm font-medium outline-none focus:bg-slate-100 data-[state=open]:bg-slate-100"
200+
data-state={openMenu === "renderer" ? "open" : "closed"}
201+
>
202+
Renderer
203+
</button>
204+
{openMenu === "renderer" && (
205+
<div className="absolute left-0 z-50 mt-1 min-w-[10rem] rounded-md border border-slate-200 bg-white p-1 text-slate-950 shadow-md">
206+
{(["vector", "canvas"] as RendererOption[]).map((option) => (
207+
<button
208+
type="button"
209+
key={option}
210+
onClick={() => {
211+
onRendererChange(option)
212+
setOpenMenu(null)
213+
}}
214+
className="flex w-full items-center rounded-sm px-2 py-1.5 text-sm hover:bg-slate-100"
215+
>
216+
{option === "vector" ? "Vector" : "Canvas"}
217+
{renderer === option && (
218+
<span className="ml-auto text-xs text-slate-500">
219+
220+
</span>
221+
)}
222+
</button>
223+
))}
224+
</div>
225+
)}
226+
</div>
227+
<div className="relative">
228+
<button
229+
type="button"
230+
onClick={() =>
231+
setOpenMenu(openMenu === "debug" ? null : "debug")
232+
}
233+
className="flex select-none items-center rounded-sm px-3 py-1 text-sm font-medium outline-none focus:bg-slate-100 data-[state=open]:bg-slate-100"
234+
data-state={openMenu === "debug" ? "open" : "closed"}
235+
>
236+
Debug
237+
</button>
238+
{openMenu === "debug" && (
239+
<div className="absolute left-0 z-50 mt-1 min-w-[12rem] rounded-md border border-slate-200 bg-white p-1 text-slate-950 shadow-md">
240+
<button
241+
type="button"
242+
onClick={() => {
243+
onDownloadVisualization()
244+
setOpenMenu(null)
245+
}}
246+
className="flex w-full items-center rounded-sm px-2 py-1.5 text-sm hover:bg-slate-100"
247+
>
248+
Download Visualization
249+
</button>
250+
</div>
251+
)}
252+
</div>
253+
<div className="relative">
254+
<button
255+
type="button"
256+
onClick={() =>
257+
setOpenMenu(openMenu === "animation" ? null : "animation")
258+
}
259+
className="flex select-none items-center rounded-sm px-3 py-1 text-sm font-medium outline-none focus:bg-slate-100 data-[state=open]:bg-slate-100"
260+
data-state={openMenu === "animation" ? "open" : "closed"}
261+
>
262+
Animation
263+
</button>
264+
{openMenu === "animation" && (
265+
<div className="absolute left-0 z-50 mt-1 min-w-[12rem] rounded-md border border-slate-200 bg-white p-1 text-slate-950 shadow-md">
266+
{animationSpeedOptions.map((option) => (
267+
<button
268+
type="button"
269+
key={option.value}
270+
onClick={() => {
271+
onAnimationSpeedChange(option.value)
272+
setOpenMenu(null)
273+
}}
274+
className="flex w-full items-center rounded-sm px-2 py-1.5 text-sm hover:bg-slate-100"
275+
>
276+
{option.label}
277+
<span className="ml-auto text-xs text-slate-500">
278+
{option.value}ms
279+
</span>
280+
{animationSpeed === option.value && (
281+
<span className="ml-2 text-xs text-slate-500"></span>
282+
)}
283+
</button>
284+
))}
285+
</div>
286+
)}
287+
</div>
288+
</div>
289+
<SolverBreadcrumbInputDownloader solver={solver} />
290+
</div>
142291
</div>
143292
<div className="flex gap-2 items-center flex-wrap">
144293
<button

0 commit comments

Comments
 (0)