import { Point, BBox } from "../vendor/bezier-js"; import { getBoundingBoxForBeziers } from "./bounding-box"; import * as colors from "./colors"; import { pathToShapes } from "./path"; import { normalizePoint, subtractPoints } from "./point"; import type { SvgSymbolElement } from "./svg-symbol"; const SPEC_LAYER_ID_RE = /^specs.*/i; export type PointWithNormal = { point: Point; normal: Point; }; type AttachmentPointSpecs = { anchor: PointWithNormal[]; tail: PointWithNormal[]; leg: PointWithNormal[]; arm: PointWithNormal[]; horn: PointWithNormal[]; crown: PointWithNormal[]; }; type FullSpecs = AttachmentPointSpecs & { nesting: BBox[]; }; export type Specs = Partial; export type AttachmentPointType = keyof AttachmentPointSpecs; export type AttachmentPoint = PointWithNormal & { type: AttachmentPointType; index: number; }; export const ATTACHMENT_POINT_TYPES: AttachmentPointType[] = [ "anchor", "tail", "leg", "arm", "horn", "crown", ]; const ATTACHMENT_POINT_SET = new Set(ATTACHMENT_POINT_TYPES); export function isAttachmentPointType( value: any ): value is AttachmentPointType { return ATTACHMENT_POINT_SET.has(value); } export function* iterAttachmentPoints(specs: Specs): Iterable { for (let type of ATTACHMENT_POINT_TYPES) { const points = specs[type]; if (points) { let index = 0; for (let point of points) { yield { ...point, type, index }; index += 1; } } } } const NUM_ARROW_POINTS = 4; const ARROW_TOP_POINT_IDX = 0; const ARROW_BOTTOM_POINT_IDX = 2; function getArrowPoints(path: string): PointWithNormal[] { const shapes = pathToShapes(path); const points: PointWithNormal[] = []; for (let shape of shapes) { 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, }); } return points; } function getBoundingBoxes(path: string): BBox[] { const shapes = pathToShapes(path); const bboxes: BBox[] = []; for (let shape of shapes) { bboxes.push(getBoundingBoxForBeziers(shape)); } return bboxes; } /** * Sort points from top to bottom, left to right. */ function sortPoints(a: PointWithNormal, b: PointWithNormal): number { if (a.point.y < b.point.y) return -1; if (a.point.y > b.point.y) return 1; if (a.point.x < b.point.x) return -1; if (a.point.x > b.point.x) return 1; return 0; } function sortedPoints(points: PointWithNormal[]): PointWithNormal[] { const copy = [...points]; copy.sort(sortPoints); return copy; } function concat(first: T[] | undefined, second: T[]): T[] { return first ? [...first, ...second] : second; } const ATTACHMENT_COLOR_MAP = new Map( ATTACHMENT_POINT_TYPES.map((type) => [ colors.ATTACHMENT_POINT_COLORS[type], type, ]) ); function updateSpecs(fill: string, path: string, specs: Specs): Specs { const attachmentType = ATTACHMENT_COLOR_MAP.get(fill); if (attachmentType) { return { ...specs, [attachmentType]: sortedPoints( concat(specs[attachmentType], getArrowPoints(path)) ), }; } if (fill === colors.NESTING_BOUNDING_BOX_COLOR) { return { ...specs, nesting: concat(specs.nesting, getBoundingBoxes(path)), }; } console.log( `Not sure what to do with specs path with fill "${fill}", ignoring it.` ); return specs; } function getSpecs(layers: SvgSymbolElement[]): Specs { let specs: Specs = {}; for (let layer of layers) { if (layer.tagName !== "path") { throw new Error( `Found an unexpected <${layer.tagName}> in the specs layer!` ); } const { fill, d } = layer.props; if (!(fill && d)) { throw new Error( `Specs layer does not contain 'fill' and/or 'd' attributes!` ); } specs = updateSpecs(fill, d, specs); } return specs; } export function extractSpecs( layers: SvgSymbolElement[] ): [Specs | undefined, SvgSymbolElement[]] { const layersWithoutSpecs: SvgSymbolElement[] = []; let specs: Specs | undefined = undefined; const setSpecs = (s: Specs | undefined) => { if (s) { if (specs) { throw new Error("Duplicate specs layers found!"); } specs = s; } }; for (let layer of layers) { switch (layer.tagName) { case "g": const { id } = layer.props; if (id && SPEC_LAYER_ID_RE.test(id)) { setSpecs(getSpecs(layer.children)); } else { let [s, children] = extractSpecs(layer.children); setSpecs(s); layersWithoutSpecs.push({ ...layer, children, }); } break; case "path": layersWithoutSpecs.push(layer); break; } } return [specs, layersWithoutSpecs]; }