import { onMounted, onUnmounted, type Ref } from "vue"; import type { ProjectFile } from "../../domain/project"; import type { Node } from "../../domain/phy"; import { fitViewport, zoomAround } from "./viewport"; import { localBounds } from "./logic"; type Options = { project: () => ProjectFile; svg: Ref; size: Ref<{ w: number; h: number }>; zoom: Ref; pan: Ref<{ x: number; y: number }>; selected: Ref; variants: Ref>; node: (id: string) => Node | undefined; }; export function usePhyViewport(options: Options) { let observer: ResizeObserver | null = null; function fit() { const bounds = localBounds(options.project().phy.root, options.variants.value); const viewport = fitViewport(bounds, options.size.value.w, options.size.value.h); options.zoom.value = viewport.zoom; options.pan.value = viewport.pan; } function fitSelected() { const nodes = options.selected.value .map((id) => options.node(id)) .filter((node): node is Node => node !== undefined); if (!nodes.length) return; const bounds = localBounds(nodes, options.variants.value); const viewport = fitViewport(bounds, options.size.value.w, options.size.value.h); options.zoom.value = viewport.zoom; options.pan.value = viewport.pan; } function localPoint(element: SVGSVGElement | SVGGElement, x: number, y: number) { const transform = element.getScreenCTM(); if (!transform) return { x: 0, y: 0 }; const point = new DOMPoint(x, y).matrixTransform(transform.inverse()); return { x: point.x, y: point.y }; } function wheel(event: WheelEvent) { const svg = options.svg.value; if (!svg) return; const point = localPoint(svg, event.clientX, event.clientY); const viewport = zoomAround( point, { zoom: options.zoom.value, pan: options.pan.value }, Math.exp(-event.deltaY * 0.001), ); options.zoom.value = viewport.zoom; options.pan.value = viewport.pan; } onMounted(() => { if (!options.svg.value) return; // viewBox はSVG自身の大きさに合わせる。ペインにはツールバーも入り、折り返すと高さがずれる observer = new ResizeObserver((entries) => { const rect = entries[0]!.contentRect; options.size.value = { w: rect.width, h: rect.height }; fit(); }); observer.observe(options.svg.value); fit(); }); onUnmounted(() => observer?.disconnect()); return { fit, fitSelected, localPoint, wheel }; }