diff --git a/lib/specs.ts b/lib/specs.ts index 10c2160..e7abe55 100644 --- a/lib/specs.ts +++ b/lib/specs.ts @@ -1,11 +1,10 @@ -import { Point, BBox, Bezier, Line } from "../vendor/bezier-js"; +import { Point, BBox } from "../vendor/bezier-js"; import { getBoundingBoxCenter, getBoundingBoxForBeziers } from "./bounding-box"; import * as colors from "./colors"; import { pathToShapes } from "./path"; -import { flatten } from "./util"; import type { SvgSymbolElement } from "./vocabulary"; -const SPEC_LAYER_ID_RE = /^specs.*/i; +const SPEC_LAYER_ID_RE = /^arrows.*/i; export type PointWithNormal = { point: Point; @@ -21,16 +20,42 @@ export type Specs = { nesting?: BBox[]; }; -function getPointsWithEmptyNormals(path: string): PointWithNormal[] { +const NUM_ARROW_POINTS = 4; +const ARROW_TOP_POINT_IDX = 0; +const ARROW_BOTTOM_POINT_IDX = 2; + +function subtractPoints(p1: Point, p2: Point): Point { + return { + x: p1.x - p2.x, + y: p1.y - p2.y, + }; +} + +function normalizePoint(p: Point): Point { + const len = Math.sqrt(Math.pow(p.x, 2) + Math.pow(p.y, 2)); + return { + x: p.x / len, + y: p.y / len, + }; +} + +function getArrowPoints(path: string): PointWithNormal[] { const shapes = pathToShapes(path); const points: PointWithNormal[] = []; for (let shape of shapes) { - const bbox = getBoundingBoxForBeziers(shape); - const point = getBoundingBoxCenter(bbox); + if (shape.length !== NUM_ARROW_POINTS) { + throw new Error( + `Expected arrow to have ${NUM_ARROW_POINTS} points, not ${shape.length}!` + ); + } + const point = shape[ARROW_BOTTOM_POINT_IDX].get(0.0); + const normal = normalizePoint( + subtractPoints(shape[ARROW_TOP_POINT_IDX].get(0.0), point) + ); points.push({ point, - normal: ORIGIN, + normal, }); } @@ -57,27 +82,27 @@ function updateSpecs(fill: string, path: string, specs: Specs): Specs { case colors.TAIL_ATTACHMENT_COLOR: return { ...specs, - tail: concat(specs.tail, getPointsWithEmptyNormals(path)), + tail: concat(specs.tail, getArrowPoints(path)), }; case colors.LEG_ATTACHMENT_COLOR: return { ...specs, - leg: concat(specs.leg, getPointsWithEmptyNormals(path)), + leg: concat(specs.leg, getArrowPoints(path)), }; case colors.ARM_ATTACHMENT_COLOR: return { ...specs, - arm: concat(specs.arm, getPointsWithEmptyNormals(path)), + arm: concat(specs.arm, getArrowPoints(path)), }; case colors.HORN_ATTACHMENT_COLOR: return { ...specs, - horn: concat(specs.horn, getPointsWithEmptyNormals(path)), + horn: concat(specs.horn, getArrowPoints(path)), }; case colors.CROWN_ATTACHMENT_COLOR: return { ...specs, - crown: concat(specs.crown, getPointsWithEmptyNormals(path)), + crown: concat(specs.crown, getArrowPoints(path)), }; case colors.NESTING_BOUNDING_BOX_COLOR: return { @@ -110,163 +135,8 @@ function getSpecs(layers: SvgSymbolElement[]): Specs { return specs; } -function filterElements( - elements: SvgSymbolElement[], - filter: (el: SvgSymbolElement) => boolean -): SvgSymbolElement[] { - const result: SvgSymbolElement[] = []; - - for (let el of elements) { - if (filter(el)) { - switch (el.tagName) { - case "g": - result.push({ - ...el, - children: filterElements(el.children, filter), - }); - break; - case "path": - result.push(el); - break; - } - } - } - - return result; -} - -function getAllShapes(layers: SvgSymbolElement[]): Bezier[][] { - const beziers: Bezier[][] = []; - - for (let layer of layers) { - switch (layer.tagName) { - case "g": - beziers.push(...getAllShapes(layer.children)); - break; - case "path": - if (!layer.props.d) { - throw new Error(` does not have a "d" attribute!`); - } - beziers.push(...pathToShapes(layer.props.d)); - break; - } - } - - return beziers; -} - -const ORIGIN: Point = { x: 0, y: 0 }; - -function invertVector(v: Point) { - v.x = -v.x; - v.y = -v.y; -} - -const TO_INFINITY_AMOUNT = 2000; - -/** - * Return whether the given point is inside the given shape, assuming - * the SVG "evenodd" fill rule: - * - * https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/fill-rule#evenodd - */ -function isPointInsideShape(point: Point, beziers: Bezier[]): boolean { - let intersections = 0; - const line: Line = { - p1: point, - p2: { - x: point.x + TO_INFINITY_AMOUNT, - y: point.y + TO_INFINITY_AMOUNT, - }, - }; - - for (let bezier of beziers) { - const points = bezier.lineIntersects(line); - intersections += points.length; - } - - const isOdd = intersections % 2 === 1; - return isOdd; -} - -function addPoints(p1: Point, p2: Point): Point { - return { - x: p1.x + p2.x, - y: p1.y + p2.y, - }; -} - -function populateNormals(pwns: PointWithNormal[], shapes: Bezier[][]) { - if (shapes.length === 0) { - throw new Error(`Expected beizers to be non-empty!`); - } - - for (let pwn of pwns) { - let minDistance = Infinity; - let minDistanceNormal = ORIGIN; - let minDistancePoint = ORIGIN; - for (let shape of shapes) { - let minDistanceChanged = false; - for (let bezier of shape) { - const { t, d } = bezier.project(pwn.point); - if (d === undefined || t === undefined) { - throw new Error(`Expected bezier.project() to return t and d!`); - } - if (d < minDistance) { - minDistanceChanged = true; - minDistance = d; - minDistanceNormal = bezier.normal(t); - minDistancePoint = bezier.get(t); - } - } - if (minDistanceChanged) { - const pointToNormal = addPoints(minDistancePoint, minDistanceNormal); - if (isPointInsideShape(pointToNormal, shape)) { - invertVector(minDistanceNormal); - } - } - } - pwn.normal = minDistanceNormal; - } -} - -function filterFilledShapes(elements: SvgSymbolElement[]): SvgSymbolElement[] { - return filterElements(elements, (el) => { - if (el.tagName === "path") { - if (el.props.fill === "none") return false; - if (el.props.fillRule !== "evenodd") { - throw new Error( - `Expected to have fill-rule="evenodd" but it is "${el.props.fillRule}"!` - ); - } - } - return true; - }); -} - -function populateSpecNormals(specs: Specs, layers: SvgSymbolElement[]): void { - const shapes = getAllShapes(filterFilledShapes(layers)); - - if (specs.tail) { - populateNormals(specs.tail, shapes); - } - if (specs.leg) { - populateNormals(specs.leg, shapes); - } - if (specs.arm) { - populateNormals(specs.arm, shapes); - } - if (specs.horn) { - populateNormals(specs.horn, shapes); - } - if (specs.crown) { - populateNormals(specs.crown, shapes); - } -} - export function extractSpecs( - layers: SvgSymbolElement[], - populateNormals: boolean = true + layers: SvgSymbolElement[] ): [Specs | undefined, SvgSymbolElement[]] { const layersWithoutSpecs: SvgSymbolElement[] = []; let specs: Specs | undefined = undefined; @@ -287,7 +157,7 @@ export function extractSpecs( if (id && SPEC_LAYER_ID_RE.test(id)) { setSpecs(getSpecs(layer.children)); } else { - let [s, children] = extractSpecs(layer.children, false); + let [s, children] = extractSpecs(layer.children); setSpecs(s); layersWithoutSpecs.push({ ...layer, @@ -301,9 +171,5 @@ export function extractSpecs( } } - if (populateNormals && specs) { - populateSpecNormals(specs, layersWithoutSpecs); - } - return [specs, layersWithoutSpecs]; } diff --git a/svg/antler specs.svg b/svg/antler specs.svg deleted file mode 100644 index b86a32c..0000000 --- a/svg/antler specs.svg +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/svg/arm arrows.svg b/svg/arm arrows.svg new file mode 100644 index 0000000..3d8243c --- /dev/null +++ b/svg/arm arrows.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/svg/arm specs.svg b/svg/arm specs.svg deleted file mode 100644 index e48cfcd..0000000 --- a/svg/arm specs.svg +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/svg/bone arrows.svg b/svg/bone arrows.svg new file mode 100644 index 0000000..b4af7ff --- /dev/null +++ b/svg/bone arrows.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/svg/church arrows.svg b/svg/church arrows.svg new file mode 100644 index 0000000..d91f0eb --- /dev/null +++ b/svg/church arrows.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/svg/circle arrows.svg b/svg/circle arrows.svg new file mode 100644 index 0000000..5dd4159 --- /dev/null +++ b/svg/circle arrows.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/svg/circle specs.svg b/svg/circle specs.svg deleted file mode 100644 index 4e68941..0000000 --- a/svg/circle specs.svg +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/svg/cloud arrows.svg b/svg/cloud arrows.svg new file mode 100644 index 0000000..28aa62c --- /dev/null +++ b/svg/cloud arrows.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/svg/cloud specs.svg b/svg/cloud specs.svg deleted file mode 100644 index f312642..0000000 --- a/svg/cloud specs.svg +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/svg/crown arrows.svg b/svg/crown arrows.svg new file mode 100644 index 0000000..bbfe0be --- /dev/null +++ b/svg/crown arrows.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/svg/crown specs.svg b/svg/crown specs.svg deleted file mode 100644 index 28eddf0..0000000 --- a/svg/crown specs.svg +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/svg/cup arrows.svg b/svg/cup arrows.svg new file mode 100644 index 0000000..dc5019c --- /dev/null +++ b/svg/cup arrows.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/svg/cup specs.svg b/svg/cup specs.svg deleted file mode 100644 index 656c202..0000000 --- a/svg/cup specs.svg +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/svg/eye arrows.svg b/svg/eye arrows.svg new file mode 100644 index 0000000..ef1dfcc --- /dev/null +++ b/svg/eye arrows.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/svg/eye specs.svg b/svg/eye specs.svg deleted file mode 100644 index 1726b23..0000000 --- a/svg/eye specs.svg +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/svg/goat horn arrows.svg b/svg/goat horn arrows.svg new file mode 100644 index 0000000..d1675d0 --- /dev/null +++ b/svg/goat horn arrows.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/svg/goat horn specs.svg b/svg/goat horn specs.svg deleted file mode 100644 index 505403b..0000000 --- a/svg/goat horn specs.svg +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/svg/hand specs.svg b/svg/hand arrows.svg similarity index 52% rename from svg/hand specs.svg rename to svg/hand arrows.svg index be9a9a1..6982050 100644 --- a/svg/hand specs.svg +++ b/svg/hand arrows.svg @@ -6,14 +6,16 @@ - - - - - - - - + + + + + + + + + + diff --git a/svg/heart arrows.svg b/svg/heart arrows.svg new file mode 100644 index 0000000..f0e8885 --- /dev/null +++ b/svg/heart arrows.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/svg/heart specs.svg b/svg/heart specs.svg deleted file mode 100644 index d812f20..0000000 --- a/svg/heart specs.svg +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/svg/leg arrows.svg b/svg/leg arrows.svg new file mode 100644 index 0000000..5ff6699 --- /dev/null +++ b/svg/leg arrows.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/svg/leg hoof arrows.svg b/svg/leg hoof arrows.svg new file mode 100644 index 0000000..b305b1c --- /dev/null +++ b/svg/leg hoof arrows.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/svg/leg hoof specs.svg b/svg/leg hoof specs.svg deleted file mode 100644 index 4edd9c6..0000000 --- a/svg/leg hoof specs.svg +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/svg/leg specs.svg b/svg/leg specs.svg deleted file mode 100644 index 71cde40..0000000 --- a/svg/leg specs.svg +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/svg/lightning arrows.svg b/svg/lightning arrows.svg new file mode 100644 index 0000000..5ff159f --- /dev/null +++ b/svg/lightning arrows.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/svg/muscle arm specs.svg b/svg/muscle arm arrows.svg similarity index 53% rename from svg/muscle arm specs.svg rename to svg/muscle arm arrows.svg index f7b2a7c..76906cb 100644 --- a/svg/muscle arm specs.svg +++ b/svg/muscle arm arrows.svg @@ -4,17 +4,19 @@ - - + + - + - - - - - - + + + + + + + + diff --git a/svg/skull arrows.svg b/svg/skull arrows.svg new file mode 100644 index 0000000..b0aff48 --- /dev/null +++ b/svg/skull arrows.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/svg/sword arrows.svg b/svg/sword arrows.svg new file mode 100644 index 0000000..e42b473 --- /dev/null +++ b/svg/sword arrows.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/svg/tail arrows.svg b/svg/tail arrows.svg new file mode 100644 index 0000000..fe47d4f --- /dev/null +++ b/svg/tail arrows.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/svg/tail specs.svg b/svg/tail specs.svg deleted file mode 100644 index d43d641..0000000 --- a/svg/tail specs.svg +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/svg/teardrop specs.svg b/svg/teardrop specs.svg deleted file mode 100644 index efe9f15..0000000 --- a/svg/teardrop specs.svg +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/svg/wing arrows.svg b/svg/wing arrows.svg new file mode 100644 index 0000000..b702f6c --- /dev/null +++ b/svg/wing arrows.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/svg/wing specs.svg b/svg/wing specs.svg deleted file mode 100644 index 2341366..0000000 --- a/svg/wing specs.svg +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - -