Extract attachment points and nesting boxes.
rodzic
26d4c721cf
commit
f70cdbad8d
|
@ -2,6 +2,8 @@ import { Bezier, Point } from "../vendor/bezier-js";
|
|||
import { SVGProps } from "react";
|
||||
|
||||
import type { SvgSymbolElement } from "./vocabulary";
|
||||
import { flatten, float } from "./util";
|
||||
import { pathToShapes } from "./path";
|
||||
|
||||
export type Bbox = {
|
||||
minX: number;
|
||||
|
@ -17,6 +19,15 @@ export function getBoundingBoxSize(bbox: Bbox): [number, number] {
|
|||
return [width, height];
|
||||
}
|
||||
|
||||
export function getBoundingBoxCenter(bbox: Bbox): Point {
|
||||
const [width, height] = getBoundingBoxSize(bbox);
|
||||
|
||||
return {
|
||||
x: bbox.minX + width / 2,
|
||||
y: bbox.minY + height / 2,
|
||||
};
|
||||
}
|
||||
|
||||
function dilateBoundingBox(bbox: Bbox, amount: number): Bbox {
|
||||
return {
|
||||
minX: bbox.minX - amount,
|
||||
|
@ -26,7 +37,7 @@ function dilateBoundingBox(bbox: Bbox, amount: number): Bbox {
|
|||
};
|
||||
}
|
||||
|
||||
function coalesceBoundingBoxes(bboxes: Bbox[]): Bbox {
|
||||
export function coalesceBoundingBoxes(bboxes: Bbox[]): Bbox {
|
||||
let minX = Infinity;
|
||||
let minY = Infinity;
|
||||
let maxX = -Infinity;
|
||||
|
@ -82,85 +93,6 @@ function getBoundingBoxForPoints(points: Point[]): Bbox {
|
|||
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 flatten<T>(arr: T[][]): T[] {
|
||||
const result: T[] = [];
|
||||
|
||||
for (let subarr of arr) {
|
||||
result.push(...subarr);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function pathToBeziers(path: string): Bezier[][] {
|
||||
const parts = path.trim().split(" ");
|
||||
let x = 0;
|
||||
let y = 0;
|
||||
let i = 0;
|
||||
const shapes: Bezier[][] = [];
|
||||
let currShape: Bezier[] = [];
|
||||
|
||||
const chomp = () => {
|
||||
if (i >= parts.length) {
|
||||
throw new Error(`Ran out of path parts!`);
|
||||
}
|
||||
const val = parts[i];
|
||||
i++;
|
||||
return val;
|
||||
};
|
||||
|
||||
const finishCurrShape = () => {
|
||||
if (currShape.length) {
|
||||
shapes.push(currShape);
|
||||
currShape = [];
|
||||
}
|
||||
};
|
||||
|
||||
while (i < parts.length) {
|
||||
const command = chomp();
|
||||
switch (command) {
|
||||
case "M":
|
||||
finishCurrShape();
|
||||
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());
|
||||
currShape.push(new Bezier(x, y, x1, y1, x2, y2, endX, endY));
|
||||
x = endX;
|
||||
y = endY;
|
||||
break;
|
||||
case "Z":
|
||||
finishCurrShape();
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown SVG path command: '${command}'`);
|
||||
}
|
||||
}
|
||||
|
||||
finishCurrShape();
|
||||
|
||||
return shapes;
|
||||
}
|
||||
|
||||
function getBezierBoundingBox(bezier: Bezier): Bbox {
|
||||
const start = bezier.get(0.0);
|
||||
const end = bezier.get(1.0);
|
||||
|
@ -169,13 +101,16 @@ function getBezierBoundingBox(bezier: Bezier): Bbox {
|
|||
return getBoundingBoxForPoints([start, end, ...extrema]);
|
||||
}
|
||||
|
||||
export function getBoundingBoxForBeziers(beziers: Bezier[]): Bbox {
|
||||
return coalesceBoundingBoxes(beziers.map(getBezierBoundingBox));
|
||||
}
|
||||
|
||||
function getPathBoundingBox(props: SVGProps<SVGPathElement>): Bbox {
|
||||
if (!props.d) {
|
||||
throw new Error(`SVG path has no 'd' attribute value!`);
|
||||
}
|
||||
const beziers = flatten(pathToBeziers(props.d));
|
||||
const bezierBboxes = beziers.map(getBezierBoundingBox);
|
||||
const bbox = coalesceBoundingBoxes(bezierBboxes);
|
||||
const beziers = flatten(pathToShapes(props.d));
|
||||
const bbox = getBoundingBoxForBeziers(beziers);
|
||||
return props.strokeWidth
|
||||
? dilateBoundingBox(bbox, float(props.strokeWidth) / 2)
|
||||
: bbox;
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
import { Bezier } from "../vendor/bezier-js";
|
||||
import { float } from "./util";
|
||||
|
||||
export function pathToShapes(path: string): Bezier[][] {
|
||||
const parts = path.trim().split(" ");
|
||||
let x = 0;
|
||||
let y = 0;
|
||||
let i = 0;
|
||||
const shapes: Bezier[][] = [];
|
||||
let currShape: Bezier[] = [];
|
||||
|
||||
const chomp = () => {
|
||||
if (i >= parts.length) {
|
||||
throw new Error(`Ran out of path parts!`);
|
||||
}
|
||||
const val = parts[i];
|
||||
i++;
|
||||
return val;
|
||||
};
|
||||
|
||||
const finishCurrShape = () => {
|
||||
if (currShape.length) {
|
||||
shapes.push(currShape);
|
||||
currShape = [];
|
||||
}
|
||||
};
|
||||
|
||||
while (i < parts.length) {
|
||||
const command = chomp();
|
||||
switch (command) {
|
||||
case "M":
|
||||
finishCurrShape();
|
||||
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());
|
||||
currShape.push(new Bezier(x, y, x1, y1, x2, y2, endX, endY));
|
||||
x = endX;
|
||||
y = endY;
|
||||
break;
|
||||
case "Z":
|
||||
finishCurrShape();
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown SVG path command: '${command}'`);
|
||||
}
|
||||
}
|
||||
|
||||
finishCurrShape();
|
||||
|
||||
return shapes;
|
||||
}
|
34
lib/specs.ts
34
lib/specs.ts
|
@ -1,14 +1,15 @@
|
|||
import { Bbox } from "./bounding-box";
|
||||
import { Point } from "../vendor/bezier-js";
|
||||
import {
|
||||
Bbox,
|
||||
getBoundingBoxCenter,
|
||||
getBoundingBoxForBeziers,
|
||||
} from "./bounding-box";
|
||||
import * as colors from "./colors";
|
||||
import { pathToShapes } from "./path";
|
||||
import type { SvgSymbolElement } from "./vocabulary";
|
||||
|
||||
const SPEC_LAYER_ID_RE = /^specs.*/i;
|
||||
|
||||
export type Point = {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
|
||||
export type Specs = {
|
||||
tail?: Point[];
|
||||
leg?: Point[];
|
||||
|
@ -19,13 +20,26 @@ export type Specs = {
|
|||
};
|
||||
|
||||
function getPoints(path: string): Point[] {
|
||||
// TODO: Implement this.
|
||||
return [];
|
||||
const shapes = pathToShapes(path);
|
||||
const points: Point[] = [];
|
||||
|
||||
for (let shape of shapes) {
|
||||
const bbox = getBoundingBoxForBeziers(shape);
|
||||
points.push(getBoundingBoxCenter(bbox));
|
||||
}
|
||||
|
||||
return points;
|
||||
}
|
||||
|
||||
function getBoundingBoxes(path: string): Bbox[] {
|
||||
// TODO: Implement this.
|
||||
return [];
|
||||
const shapes = pathToShapes(path);
|
||||
const bboxes: Bbox[] = [];
|
||||
|
||||
for (let shape of shapes) {
|
||||
bboxes.push(getBoundingBoxForBeziers(shape));
|
||||
}
|
||||
|
||||
return bboxes;
|
||||
}
|
||||
|
||||
function concat<T>(first: T[] | undefined, second: T[]): T[] {
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
export 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;
|
||||
}
|
||||
|
||||
export function flatten<T>(arr: T[][]): T[] {
|
||||
const result: T[] = [];
|
||||
|
||||
for (let subarr of arr) {
|
||||
result.push(...subarr);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
Ładowanie…
Reference in New Issue