From f70cdbad8d69c5fe473b19c28ca29d6eab1c48dd Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Sat, 13 Feb 2021 20:47:28 -0500 Subject: [PATCH] Extract attachment points and nesting boxes. --- lib/bounding-box.ts | 101 ++++++++------------------------------------ lib/path.ts | 58 +++++++++++++++++++++++++ lib/specs.ts | 34 ++++++++++----- lib/util.ts | 22 ++++++++++ 4 files changed, 122 insertions(+), 93 deletions(-) create mode 100644 lib/path.ts create mode 100644 lib/util.ts diff --git a/lib/bounding-box.ts b/lib/bounding-box.ts index 0683028..1195edb 100644 --- a/lib/bounding-box.ts +++ b/lib/bounding-box.ts @@ -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(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): 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; diff --git a/lib/path.ts b/lib/path.ts new file mode 100644 index 0000000..6cdff01 --- /dev/null +++ b/lib/path.ts @@ -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; +} diff --git a/lib/specs.ts b/lib/specs.ts index f676d67..1fff8c3 100644 --- a/lib/specs.ts +++ b/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(first: T[] | undefined, second: T[]): T[] { diff --git a/lib/util.ts b/lib/util.ts new file mode 100644 index 0000000..a02e7a9 --- /dev/null +++ b/lib/util.ts @@ -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(arr: T[][]): T[] { + const result: T[] = []; + + for (let subarr of arr) { + result.push(...subarr); + } + + return result; +}