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 { SVGProps } from "react";
|
||||||
|
|
||||||
import type { SvgSymbolElement } from "./vocabulary";
|
import type { SvgSymbolElement } from "./vocabulary";
|
||||||
|
import { flatten, float } from "./util";
|
||||||
|
import { pathToShapes } from "./path";
|
||||||
|
|
||||||
export type Bbox = {
|
export type Bbox = {
|
||||||
minX: number;
|
minX: number;
|
||||||
|
@ -17,6 +19,15 @@ export function getBoundingBoxSize(bbox: Bbox): [number, number] {
|
||||||
return [width, height];
|
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 {
|
function dilateBoundingBox(bbox: Bbox, amount: number): Bbox {
|
||||||
return {
|
return {
|
||||||
minX: bbox.minX - amount,
|
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 minX = Infinity;
|
||||||
let minY = Infinity;
|
let minY = Infinity;
|
||||||
let maxX = -Infinity;
|
let maxX = -Infinity;
|
||||||
|
@ -82,85 +93,6 @@ function getBoundingBoxForPoints(points: Point[]): Bbox {
|
||||||
return { minX, minY, maxX, maxY };
|
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 {
|
function getBezierBoundingBox(bezier: Bezier): Bbox {
|
||||||
const start = bezier.get(0.0);
|
const start = bezier.get(0.0);
|
||||||
const end = bezier.get(1.0);
|
const end = bezier.get(1.0);
|
||||||
|
@ -169,13 +101,16 @@ function getBezierBoundingBox(bezier: Bezier): Bbox {
|
||||||
return getBoundingBoxForPoints([start, end, ...extrema]);
|
return getBoundingBoxForPoints([start, end, ...extrema]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getBoundingBoxForBeziers(beziers: Bezier[]): Bbox {
|
||||||
|
return coalesceBoundingBoxes(beziers.map(getBezierBoundingBox));
|
||||||
|
}
|
||||||
|
|
||||||
function getPathBoundingBox(props: SVGProps<SVGPathElement>): Bbox {
|
function getPathBoundingBox(props: SVGProps<SVGPathElement>): Bbox {
|
||||||
if (!props.d) {
|
if (!props.d) {
|
||||||
throw new Error(`SVG path has no 'd' attribute value!`);
|
throw new Error(`SVG path has no 'd' attribute value!`);
|
||||||
}
|
}
|
||||||
const beziers = flatten(pathToBeziers(props.d));
|
const beziers = flatten(pathToShapes(props.d));
|
||||||
const bezierBboxes = beziers.map(getBezierBoundingBox);
|
const bbox = getBoundingBoxForBeziers(beziers);
|
||||||
const bbox = coalesceBoundingBoxes(bezierBboxes);
|
|
||||||
return props.strokeWidth
|
return props.strokeWidth
|
||||||
? dilateBoundingBox(bbox, float(props.strokeWidth) / 2)
|
? dilateBoundingBox(bbox, float(props.strokeWidth) / 2)
|
||||||
: bbox;
|
: 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 * as colors from "./colors";
|
||||||
|
import { pathToShapes } from "./path";
|
||||||
import type { SvgSymbolElement } from "./vocabulary";
|
import type { SvgSymbolElement } from "./vocabulary";
|
||||||
|
|
||||||
const SPEC_LAYER_ID_RE = /^specs.*/i;
|
const SPEC_LAYER_ID_RE = /^specs.*/i;
|
||||||
|
|
||||||
export type Point = {
|
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type Specs = {
|
export type Specs = {
|
||||||
tail?: Point[];
|
tail?: Point[];
|
||||||
leg?: Point[];
|
leg?: Point[];
|
||||||
|
@ -19,13 +20,26 @@ export type Specs = {
|
||||||
};
|
};
|
||||||
|
|
||||||
function getPoints(path: string): Point[] {
|
function getPoints(path: string): Point[] {
|
||||||
// TODO: Implement this.
|
const shapes = pathToShapes(path);
|
||||||
return [];
|
const points: Point[] = [];
|
||||||
|
|
||||||
|
for (let shape of shapes) {
|
||||||
|
const bbox = getBoundingBoxForBeziers(shape);
|
||||||
|
points.push(getBoundingBoxCenter(bbox));
|
||||||
|
}
|
||||||
|
|
||||||
|
return points;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getBoundingBoxes(path: string): Bbox[] {
|
function getBoundingBoxes(path: string): Bbox[] {
|
||||||
// TODO: Implement this.
|
const shapes = pathToShapes(path);
|
||||||
return [];
|
const bboxes: Bbox[] = [];
|
||||||
|
|
||||||
|
for (let shape of shapes) {
|
||||||
|
bboxes.push(getBoundingBoxForBeziers(shape));
|
||||||
|
}
|
||||||
|
|
||||||
|
return bboxes;
|
||||||
}
|
}
|
||||||
|
|
||||||
function concat<T>(first: T[] | undefined, second: T[]): T[] {
|
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