Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Eirby/modules viz zoom pan #424

Merged
merged 8 commits into from
Sep 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
},
}
}