Detect bounding boxes of symbols and crop to them.
rodzic
319d8b63a7
commit
27cbf37209
|
@ -0,0 +1,174 @@
|
|||
import { Bezier, Point } from "../vendor/bezier-js";
|
||||
import { SVGProps } from "react";
|
||||
|
||||
import type { SvgSymbolElement } from "./vocabulary";
|
||||
|
||||
export type Bbox = {
|
||||
minX: number;
|
||||
minY: number;
|
||||
maxX: number;
|
||||
maxY: number;
|
||||
};
|
||||
|
||||
export function getBoundingBoxSize(bbox: Bbox): [number, number] {
|
||||
const width = bbox.maxX - bbox.minX;
|
||||
const height = bbox.maxY - bbox.minY;
|
||||
|
||||
return [width, height];
|
||||
}
|
||||
|
||||
function dilateBoundingBox(bbox: Bbox, amount: number): Bbox {
|
||||
return {
|
||||
minX: bbox.minX - amount,
|
||||
maxX: bbox.maxX + amount,
|
||||
minY: bbox.minY - amount,
|
||||
maxY: bbox.maxY + amount,
|
||||
};
|
||||
}
|
||||
|
||||
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.minX < minX) {
|
||||
minX = bbox.minX;
|
||||
}
|
||||
if (bbox.maxX > maxX) {
|
||||
maxX = bbox.maxX;
|
||||
}
|
||||
if (bbox.minY < minY) {
|
||||
minY = bbox.minY;
|
||||
}
|
||||
if (bbox.maxY > maxY) {
|
||||
maxY = bbox.maxY;
|
||||
}
|
||||
}
|
||||
|
||||
return { minX, minY, maxX, maxY };
|
||||
}
|
||||
|
||||
function getBoundingBoxForPoints(points: Point[]): Bbox {
|
||||
let minX = Infinity;
|
||||
let minY = Infinity;
|
||||
let maxX = -Infinity;
|
||||
let maxY = -Infinity;
|
||||
|
||||
if (points.length === 0) {
|
||||
throw new Error(`Must have at least one point!`);
|
||||
}
|
||||
|
||||
for (let point of points) {
|
||||
if (point.x < minX) {
|
||||
minX = point.x;
|
||||
}
|
||||
if (point.x > maxX) {
|
||||
maxX = point.x;
|
||||
}
|
||||
if (point.y < minY) {
|
||||
minY = point.y;
|
||||
}
|
||||
if (point.y > maxY) {
|
||||
maxY = point.y;
|
||||
}
|
||||
}
|
||||
|
||||
return { minX, minY, maxX, maxY };
|
||||
}
|
||||
|
||||
function float(value: string | number | undefined): number {
|
||||
if (typeof value === "number") return value;
|
||||
if (value === undefined) value = "";
|
||||
|
||||
const float = parseFloat(value);
|
||||
|
||||
if (isNaN(float)) {
|
||||
throw new Error(`Expected '${value}' to be a float!`);
|
||||
}
|
||||
|
||||
return float;
|
||||
}
|
||||
|
||||
function pathToBeziers(path: string): Bezier[] {
|
||||
const parts = path.trim().split(" ");
|
||||
let x = 0;
|
||||
let y = 0;
|
||||
let i = 0;
|
||||
const beziers: Bezier[] = [];
|
||||
|
||||
const chomp = () => {
|
||||
if (i >= parts.length) {
|
||||
throw new Error(`Ran out of path parts!`);
|
||||
}
|
||||
const val = parts[i];
|
||||
i++;
|
||||
return val;
|
||||
};
|
||||
|
||||
while (i < parts.length) {
|
||||
const command = chomp();
|
||||
switch (command) {
|
||||
case "M":
|
||||
x = float(chomp());
|
||||
y = float(chomp());
|
||||
break;
|
||||
case "C":
|
||||
const x1 = float(chomp());
|
||||
const y1 = float(chomp());
|
||||
const x2 = float(chomp());
|
||||
const y2 = float(chomp());
|
||||
const endX = float(chomp());
|
||||
const endY = float(chomp());
|
||||
beziers.push(new Bezier(x, y, x1, y1, x2, y2, endX, endY));
|
||||
x = endX;
|
||||
y = endY;
|
||||
break;
|
||||
case "Z":
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown SVG path command: '${command}'`);
|
||||
}
|
||||
}
|
||||
|
||||
return beziers;
|
||||
}
|
||||
|
||||
function getBezierBoundingBox(bezier: Bezier): Bbox {
|
||||
const start = bezier.get(0.0);
|
||||
const end = bezier.get(1.0);
|
||||
const extrema = bezier.extrema().values.map((t) => bezier.get(t));
|
||||
|
||||
return getBoundingBoxForPoints([start, end, ...extrema]);
|
||||
}
|
||||
|
||||
function getPathBoundingBox(props: SVGProps<SVGPathElement>): Bbox {
|
||||
if (!props.d) {
|
||||
throw new Error(`SVG path has no 'd' attribute value!`);
|
||||
}
|
||||
const beziers = pathToBeziers(props.d);
|
||||
const bezierBboxes = beziers.map(getBezierBoundingBox);
|
||||
const bbox = coalesceBoundingBoxes(bezierBboxes);
|
||||
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);
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
import React, { useState } from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import { getBoundingBoxSize } from "./bounding-box";
|
||||
|
||||
import _SvgVocabulary from "./svg-vocabulary.json";
|
||||
import type { SvgSymbolData, SvgSymbolElement } from "./vocabulary";
|
||||
|
@ -60,12 +61,14 @@ function reactifySvgSymbolElement(
|
|||
const SvgSymbol: React.FC<SvgSymbolProps> = (props) => {
|
||||
const d = props.data;
|
||||
const scale = props.scale || 1;
|
||||
const [width, height] = getBoundingBoxSize(d.bbox);
|
||||
|
||||
return (
|
||||
<svg
|
||||
viewBox={`0 0 ${d.width} ${d.height}`}
|
||||
width={px(d.width * scale)}
|
||||
height={px(d.height * scale)}
|
||||
viewBox={`${d.bbox.minX} ${d.bbox.minY} ${width} ${height}`}
|
||||
width={px(width * scale)}
|
||||
height={px(height * scale)}
|
||||
style={{ margin: "10px" }}
|
||||
>
|
||||
{props.data.layers.map(reactifySvgSymbolElement.bind(null, props))}
|
||||
</svg>
|
||||
|
@ -113,7 +116,7 @@ const App: React.FC<{}> = () => {
|
|||
>
|
||||
{symbolData.name}
|
||||
</div>
|
||||
<div className="checkerboard-bg">
|
||||
<div className="checkerboard-bg" style={{ lineHeight: 0 }}>
|
||||
<SvgSymbol
|
||||
data={symbolData}
|
||||
scale={0.25}
|
||||
|
|
|
@ -2,11 +2,13 @@ import fs from "fs";
|
|||
import path from "path";
|
||||
import cheerio from "cheerio";
|
||||
import { SVGProps } from "react";
|
||||
import { getSvgBoundingBox, Bbox } from "./bounding-box";
|
||||
|
||||
export type SvgSymbolData = {
|
||||
name: string;
|
||||
width: number;
|
||||
height: number;
|
||||
bbox: Bbox;
|
||||
layers: SvgSymbolElement[];
|
||||
};
|
||||
|
||||
|
@ -131,11 +133,13 @@ export function build() {
|
|||
const layers = onlyTags(svgEl.children()).map((ch) =>
|
||||
serializeSvgSymbolElement($, ch)
|
||||
);
|
||||
const bbox = getSvgBoundingBox(layers);
|
||||
|
||||
const symbol: SvgSymbolData = {
|
||||
name,
|
||||
width,
|
||||
height,
|
||||
bbox,
|
||||
layers,
|
||||
};
|
||||
vocab.push(symbol);
|
||||
|
|
Ładowanie…
Reference in New Issue