Skip to content

Commit

Permalink
Eirby/modules viz zoom pan (#424)
Browse files Browse the repository at this point in the history
This adds basic pan zoom functionality to the modules page. 
Next up  #421 


**Note:** This requires some tweaking honestly especially on the reset.

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
  • Loading branch information
EdwardIrby and github-actions[bot] authored Sep 28, 2023
1 parent 92db013 commit 9d05682
Show file tree
Hide file tree
Showing 9 changed files with 156 additions and 18 deletions.
6 changes: 6 additions & 0 deletions console/client/.eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ module.exports = {
'func-style': ['error', 'expression'],
'@typescript-eslint/consistent-type-definitions': ['error', 'interface'],
'@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }],
'@typescript-eslint/ban-ts-comment': [
2,
{
'ts-ignore': 'allow-with-description',
},
],
},
settings: {
react: {
Expand Down
19 changes: 19 additions & 0 deletions console/client/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions console/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@
"@headlessui/react": "1.7.16",
"@heroicons/react": "2.0.18",
"@monaco-editor/react": "4.5.2",
"@svgdotjs/svg.js": "3.2.0",
"@svgdotjs/svg.panzoom.js": "2.1.2",
"@tailwindcss/forms": "^0.5.6",
"@vitejs/plugin-react": "^4.0.4",
"@viz-js/viz": "3.2.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,13 @@ html.dark:root {
--edge-color: var(--white);
}

g.graph > polygon {
fill: transparent;
#modules-flow-chart {
width: fit-content;
& > .graph > polygon {
fill: transparent;
}
}

#svg-pan-zoom-controls {
#pan-zoom-controls {
& path {
fill-opacity: 0.75;
fill: var(--field-name-color);
Expand Down
46 changes: 36 additions & 10 deletions console/client/src/features/modules/ModulesPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,54 @@ import { modulesContext } from '../../providers/modules-provider'
import { generateDot } from './generate-dot'
import { dotToSVG } from './dot-to-svg'
import { formatSVG } from './format-svg'
import './graph.css'
import { svgZoom } from './svg-zoom'
import { createControls } from './create-controls'
import './Modules.css'

export const ModulesPage = () => {
const modules = React.useContext(modulesContext)
const dot = generateDot(modules)
const ref = React.useRef<HTMLDivElement>(null)
const viewportRef = React.useRef<HTMLDivElement>(null)
const controlRef = React.useRef<HTMLDivElement>(null)
const [viewport, setViewPort] = React.useState<HTMLDivElement>()
const [controls, setControls] = React.useState<HTMLDivElement>()
const [svg, setSVG] = React.useState<SVGSVGElement>()

React.useEffect(() => {
const cur = ref.current
cur && setViewPort(cur)
const viewCur = viewportRef.current
viewCur && setViewPort(viewCur)

const ctlCur = controlRef.current
ctlCur && setControls(ctlCur)
}, [])

React.useEffect(() => {
const renderSvg = async () => {
const svg = await dotToSVG(dot)
svg && viewport?.replaceChildren(formatSVG(svg))
const dot = generateDot(modules)
const unformattedSVG = await dotToSVG(dot)
if (unformattedSVG) {
const formattedSVG = formatSVG(unformattedSVG)
viewport?.replaceChildren(formattedSVG)
setSVG(formattedSVG)
}
}
viewport && void renderSvg()
}, [dot, viewport])
// console.log(generateDotFile(modules))
}, [modules, viewport])

React.useEffect(() => {
if (controls && svg) {
const zoom = svgZoom()
const [buttons, removeListeners] = createControls(zoom)
controls.replaceChildren(...buttons.values())
return () => {
removeListeners()
}
}
}, [controls, svg])
return (
<div className='h-full w-full flex flex-col'>
<PageHeader icon={<Square3Stack3DIcon />} title='Modules' />
<div ref={ref} className='viewport' />
<div ref={controlRef} className='zoom-pan-controls'></div>
<div ref={viewportRef} className='viewport flex-1 overflow-hidden' />
</div>
)
}
20 changes: 18 additions & 2 deletions console/client/src/features/modules/constants.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,27 @@
export const callIconID = 'call-icon'

export const callIcon = `<defs>
<symbol id="${callIconID}" viewBox="0 0 24 24" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" fill="currentColor" d="M20.25 3.75v4.5m0-4.5h-4.5m4.5 0l-6 6m3 12c-8.284 0-15-6.716-15-15V4.5A2.25 2.25 0 014.5 2.25h1.372c.516 0 .966.351 1.091.852l1.106 4.423c.11.44-.054.902-.417 1.173l-1.293.97a1.062 1.062 0 00-.38 1.21 12.035 12.035 0 007.143 7.143c.441.162.928-.004 1.21-.38l.97-1.293a1.125 1.125 0 011.173-.417l4.423 1.106c.5.125.852.575.852 1.091V19.5a2.25 2.25 0 01-2.25 2.25h-2.25z" />
<symbol id="${callIconID}" fill="currentColor" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M20.25 3.75v4.5m0-4.5h-4.5m4.5 0l-6 6m3 12c-8.284 0-15-6.716-15-15V4.5A2.25 2.25 0 014.5 2.25h1.372c.516 0 .966.351 1.091.852l1.106 4.423c.11.44-.054.902-.417 1.173l-1.293.97a1.062 1.062 0 00-.38 1.21 12.035 12.035 0 007.143 7.143c.441.162.928-.004 1.21-.38l.97-1.293a1.125 1.125 0 011.173-.417l4.423 1.106c.5.125.852.575.852 1.091V19.5a2.25 2.25 0 01-2.25 2.25h-2.25z" />
</symbol>
</defs>
`

export const controlIcons = {
in: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
</svg>
`,
out: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 12h-15" />
</svg>`,
reset: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M7.5 3.75H6A2.25 2.25 0 003.75 6v1.5M16.5 3.75H18A2.25 2.25 0 0120.25 6v1.5m0 9V18A2.25 2.25 0 0118 20.25h-1.5m-9 0H6A2.25 2.25 0 013.75 18v-1.5M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
`,
}

export const moduleVerbCls = 'module-verb'
export const moduleTitleCls = 'module-title'
export const vizID = 'modules-flow-chart'
export const controlsID = 'pan-zoom-controls'
33 changes: 33 additions & 0 deletions console/client/src/features/modules/create-controls.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { svgZoom } from './svg-zoom'
import { controlIcons } from './constants'

export const createControls = (
zoom: ReturnType<typeof svgZoom>,
): [Map<'in' | 'out' | 'reset', HTMLButtonElement>, () => void] => {
const actions = ['in', 'out', 'reset'] as const
const buttons: Map<(typeof actions)[number], HTMLButtonElement> = new Map()
for (const action of actions) {
const btn = document.createElement('button')
btn.classList.add(
...'relative inline-flex items-center bg-white dark:hover:bg-indigo-700 dark:bg-gray-700/40 px-2 py-2 text-gray-500 dark:text-gray-400 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-10'.split(
' ',
),
)
const scr = document.createElement('span')
scr.classList.add('sr-only')
scr.innerText = action
const parser = new DOMParser()
const doc = parser.parseFromString(controlIcons[action], 'image/svg+xml')
const svg = doc.documentElement
btn.replaceChildren(scr, svg)
btn.addEventListener('click', zoom[action])
buttons.set(action, btn)
}

const removeEventListeners = () => {
for (const action of actions) {
buttons.get(action)?.removeEventListener('click', zoom[action])
}
}
return [buttons, removeEventListeners]
}
6 changes: 4 additions & 2 deletions console/client/src/features/modules/format-svg.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { callIcon, moduleVerbCls, callIconID } from './constants'
import { callIcon, moduleVerbCls, callIconID, vizID } from './constants'
export const formatSVG = (svg: SVGSVGElement): SVGSVGElement => {
svg.insertAdjacentHTML('afterbegin', callIcon)

svg.removeAttribute('width')
svg.removeAttribute('height')
svg.setAttribute('id', vizID)
for (const $a of svg.querySelectorAll('a')) {
const $g = $a.parentNode! as SVGSVGElement

Expand Down
32 changes: 32 additions & 0 deletions console/client/src/features/modules/svg-zoom.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { SVG } from '@svgdotjs/svg.js'
import '@svgdotjs/svg.panzoom.js/dist/svg.panzoom.esm.js'
import { vizID } from './constants'

export const svgZoom = () => {
// enables panZoom
const canvas = SVG(`#${vizID}`)
//@ts-ignore: lib types bad
?.panZoom()
const box = canvas.bbox()
return {
to(id: string) {
const module = canvas.findOne(`#${id}`)
//@ts-ignore: lib types bad
const bbox = module?.bbox()
if (bbox) {
canvas.zoom(2, { x: bbox.x, y: bbox.y })
}
},
in() {
const zoomLevel = canvas.zoom()
canvas.zoom(zoomLevel + 0.1) // Increase the zoom level by 0.1
},
out() {
const zoomLevel = canvas.zoom()
canvas.zoom(zoomLevel - 0.1) // Decrease the zoom level by 0.1
},
reset() {
canvas.viewbox(box).zoom(1, { x: 0, y: 0 }) // Reset zoom level to 1 and pan to origin
},
}
}

0 comments on commit 9d05682

Please sign in to comment.