Files
Beyond-KLE/mock/src/ui/phy/usePhyViewport.ts
T
ewdernaandClaude Opus 5.5 a4eabe3691 mock: キャンバスの viewBox を、ツールバーを除いた SVG の大きさに合わせる
ResizeObserver がツールバーを含むペインを見ていたため、viewBox の高さが SVG の実寸より
ツールバーの分だけ大きく、描画が縮んでいた。ツールバーが折り返すとずれが広がる。SVG 自身を見る。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ShoEXYqi8PWTjB9E86Ltot
2026-10-23 20:59:42 +09:00

74 lines
2.5 KiB
TypeScript

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<SVGSVGElement | undefined>;
size: Ref<{ w: number; h: number }>;
zoom: Ref<number>;
pan: Ref<{ x: number; y: number }>;
selected: Ref<string[]>;
variants: Ref<Record<string, string>>;
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 };
}