mysticsymbolic.github.io/lib/bounding-box.ts

109 wiersze
2.8 KiB
TypeScript

import { Bezier, Point, BBox, MinMax } from "../vendor/bezier-js";
import { SVGProps } from "react";
import type { SvgSymbolElement } from "./svg-symbol";
import { flatten, float } from "./util";
import { pathToShapes } from "./path";
export function getBoundingBoxSize(bbox: BBox): [number, number] {
const width = bbox.x.max - bbox.x.min;
const height = bbox.y.max - bbox.y.min;
return [width, height];
}
export function getBoundingBoxCenter(bbox: BBox): Point {
const [width, height] = getBoundingBoxSize(bbox);
return {
x: bbox.x.min + width / 2,
y: bbox.y.min + height / 2,
};
}
function dilateMinMax(minmax: MinMax, amount: number): MinMax {
return {
min: minmax.min - amount,
max: minmax.max + amount,
};
}
export function dilateBoundingBox(bbox: BBox, amount: number): BBox {
return {
x: dilateMinMax(bbox.x, amount),
y: dilateMinMax(bbox.y, amount),
};
}
export function coalesceBoundingBoxes(bboxes: BBox[]): BBox {
let minX = Infinity;
let minY = Infinity;
let maxX = -Infinity;
let maxY = -Infinity;
if (bboxes.length === 0) {
throw new Error(`Must have at least one bounding box!`);
}
for (let bbox of bboxes) {
if (bbox.x.min < minX) {
minX = bbox.x.min;
}
if (bbox.x.max > maxX) {
maxX = bbox.x.max;
}
if (bbox.y.min < minY) {
minY = bbox.y.min;
}
if (bbox.y.max > maxY) {
maxY = bbox.y.max;
}
}
return { x: { min: minX, max: maxX }, y: { min: minY, max: maxY } };
}
export function getBoundingBoxForBeziers(beziers: Bezier[]): BBox {
return coalesceBoundingBoxes(beziers.map((b) => b.bbox()));
}
function getPathBoundingBox(props: SVGProps<SVGPathElement>): BBox {
if (!props.d) {
throw new Error(`SVG path has no 'd' attribute value!`);
}
const beziers = flatten(pathToShapes(props.d));
const bbox = getBoundingBoxForBeziers(beziers);
return props.strokeWidth
? dilateBoundingBox(bbox, float(props.strokeWidth) / 2)
: bbox;
}
export function getSvgBoundingBox(
element: SvgSymbolElement | SvgSymbolElement[]
): BBox {
if (Array.isArray(element)) {
return coalesceBoundingBoxes(element.map(getSvgBoundingBox));
}
switch (element.tagName) {
case "g":
return getSvgBoundingBox(element.children);
case "path":
return getPathBoundingBox(element.props);
}
}
/**
* Assuming the origins of the giving boxes are aligned and
* the transform origin is set to their center, return the maximum
* amount the child needs to be scaled to fit within the parent.
*/
export function uniformlyScaleToFit(parent: BBox, child: BBox): number {
const [pWidth, pHeight] = getBoundingBoxSize(parent);
const [cWidth, cHeight] = getBoundingBoxSize(child);
const widthScale = pWidth / cWidth;
const heightScale = pHeight / cHeight;
return Math.min(widthScale, heightScale);
}