import { computed, type Ref } from "vue"; import type { ProjectFile } from "../../domain/project"; import type { Node, VariantGroupNode } from "../../domain/phy"; import { apply, compose, flattenKeys, inverse, matrix, worldTransform, type Affine, } from "../../domain/geometry"; import { removeFocusedSubtree, structuralNodes } from "./logic"; type Options = { project: () => ProjectFile; size: Ref<{ w: number; h: number }>; pan: Ref<{ x: number; y: number }>; zoom: Ref; focus: Ref; variants: Ref>; grid: Ref; selected: Ref; node: (id: string) => Node | undefined; parentId: (id: string) => string | null; parentLabel: (id: string | null) => string; selectedContainer: (id: string) => string | null; }; export function usePhyCanvasView(options: Options) { const keys = computed(() => flattenKeys(options.project().phy, options.variants.value)); const groupsAll = computed(() => structuralNodes(options.project().phy.root)); const outsideNodes = computed(() => options.focus.value ? removeFocusedSubtree(options.project().phy.root, options.focus.value) : [], ); const gridLines = computed(() => { const step = options.grid.value; const xMin = Math.floor(-options.pan.value.x / options.zoom.value / step); const xMax = Math.ceil((options.size.value.w - options.pan.value.x) / options.zoom.value / step); const yMin = Math.floor(-options.pan.value.y / options.zoom.value / step); const yMax = Math.ceil((options.size.value.h - options.pan.value.y) / options.zoom.value / step); const lines: { d: string; major: boolean }[] = []; for (let index = xMin; index <= xMax; index++) { lines.push({ d: `M ${index * step} -100 V 100`, major: Math.abs((index * step) % 1) < 1e-8 }); } for (let index = yMin; index <= yMax; index++) { lines.push({ d: `M -100 ${index * step} H 100`, major: Math.abs((index * step) % 1) < 1e-8 }); } return lines; }); const variantGroups = computed(() => groupsAll.value.filter((node): node is VariantGroupNode => node.type === "variantGroup"), ); const focusGridLines = computed(() => { const focusId = options.focus.value; if (!focusId || !options.node(focusId)) return []; const screen = compose( { a: options.zoom.value, b: 0, c: 0, d: options.zoom.value, e: options.pan.value.x, f: options.pan.value.y, }, worldTransform(options.project().phy, focusId), ); const toLocal = inverse(screen); const corners = [ { x: 0, y: 0 }, { x: options.size.value.w, y: 0 }, { x: options.size.value.w, y: options.size.value.h }, { x: 0, y: options.size.value.h }, ].map((point) => apply(toLocal, point)); const xs = corners.map((point) => point.x); const ys = corners.map((point) => point.y); const step = options.grid.value; const lines: { d: string; major: boolean }[] = []; for ( let index = Math.floor(Math.min(...xs) / step); index <= Math.ceil(Math.max(...xs) / step); index++ ) { lines.push({ d: `M ${index * step} -100 V 100`, major: Math.abs((index * step) % 1) < 1e-8 }); } for ( let index = Math.floor(Math.min(...ys) / step); index <= Math.ceil(Math.max(...ys) / step); index++ ) { lines.push({ d: `M -100 ${index * step} H 100`, major: Math.abs((index * step) % 1) < 1e-8 }); } return lines; }); const moveTargets = computed(() => { const excluded = new Set(); const add = (node: Node) => { excluded.add(node.id); if (node.type === "cluster") node.children.forEach(add); if (node.type === "variantGroup") { node.children.forEach((variant) => { excluded.add(variant.id); variant.children.forEach(add); }); } }; options.selected.value.forEach((id) => { const node = options.node(id); if (node) add(node); }); return [ { id: "__root__", label: "root" }, ...groupsAll.value .filter((node) => node.type === "cluster" && !excluded.has(node.id)) .map((node) => ({ id: node.id, label: node.name || "クラスター" })), ...variantGroups.value.flatMap((group) => group.children .filter((variant) => !excluded.has(variant.id)) .map((variant) => ({ id: variant.id, label: `${group.name} / ${variant.name}` })), ), ]; }); function parentMatrix(id: string): Affine { const node = options.node(id); return node ? compose(worldTransform(options.project().phy, id), inverse(matrix(node.transform))) : { a: 1, b: 0, c: 0, d: 1, e: 0, f: 0 }; } function focusAngle(id: string) { const transform = parentMatrix(id); return (Math.atan2(transform.b, transform.a) * 180) / Math.PI; } function affineText(transform: Affine) { return `matrix(${transform.a} ${transform.b} ${transform.c} ${transform.d} ${transform.e} ${transform.f})`; } function focusCrumbs(id: string): { id: string | null; label: string }[] { const ids = [id]; let parent = options.parentId(id); while (parent) { ids.unshift(parent); parent = options.parentId(parent); } return [ { id: null, label: "root" }, ...ids.map((item) => ({ id: groupsAll.value.find( (node) => node.type === "variantGroup" && node.children.some((variant) => variant.id === item), )?.id || item, label: options.parentLabel(item), })), ]; } const propertyNode = computed(() => options.selected.value.length === 1 ? options.node(options.selected.value[0]!) : null, ); return { keys, groupsAll, outsideNodes, gridLines, variantGroups, focusGridLines, moveTargets, parentMatrix, focusAngle, affineText, focusCrumbs, propertyNode, }; }