mysticsymbolic.github.io/lib/svg-symbol.tsx

187 wiersze
4.3 KiB
TypeScript

import React from "react";
import { SVGProps } from "react";
import { BBox } from "../vendor/bezier-js";
import { FILL_REPLACEMENT_COLOR, STROKE_REPLACEMENT_COLOR } from "./colors";
import { AttachmentPointType, PointWithNormal, Specs } from "./specs";
import type { SvgSymbolMetadata } from "./svg-symbol-metadata";
import { VisibleSpecs } from "./visible-specs";
const DEFAULT_UNIFORM_STROKE_WIDTH = 1;
export type SvgSymbolData = {
name: string;
bbox: BBox;
layers: SvgSymbolElement[];
meta?: SvgSymbolMetadata;
specs?: Specs;
};
export type SvgSymbolElement = (
| {
tagName: "g";
props: SVGProps<SVGGElement>;
}
| {
tagName: "path";
props: SVGProps<SVGPathElement>;
}
) & {
children: SvgSymbolElement[];
};
export type SvgSymbolContext = {
/** The stroke color of the symbol, as a hex hash (e.g. '#ff0000'). */
stroke: string;
/** The fill color of the symbol, as a hex hash (e.g. '#ff0000'). */
fill: string;
/**
* Whether or not to visibly show the specifications for the symbol,
* e.g. its attachment points, nesting boxes, and so on.
*/
showSpecs: boolean;
/**
* Whether or not to forcibly apply a uniform stroke width to all
* the shapes in the symbol. If defined, the stroke width will
* *not* vary as the symbol is scaled.
*/
uniformStrokeWidth?: number;
};
const DEFAULT_CONTEXT: SvgSymbolContext = {
stroke: "#000000",
fill: "#ffffff",
showSpecs: false,
uniformStrokeWidth: DEFAULT_UNIFORM_STROKE_WIDTH,
};
/**
* If the given symbol context is visibly showing its specifications,
* return one with its fill color set to "none" so that the specs can
* be seen more easily.
*/
export function noFillIfShowingSpecs<T extends SvgSymbolContext>(ctx: T): T {
return {
...ctx,
fill: ctx.showSpecs ? "none" : ctx.fill,
};
}
/**
* Return a symbol context with the stroke and fill colors swapped.
*/
export function swapColors<T extends SvgSymbolContext>(ctx: T): T {
return {
...ctx,
fill: ctx.stroke,
stroke: ctx.fill,
};
}
export function createSvgSymbolContext(
ctx: Partial<SvgSymbolContext> = {}
): SvgSymbolContext {
return {
...DEFAULT_CONTEXT,
...ctx,
};
}
function getColor(
ctx: SvgSymbolContext,
color: string | undefined
): string | undefined {
switch (color) {
case STROKE_REPLACEMENT_COLOR:
return ctx.stroke;
case FILL_REPLACEMENT_COLOR:
return ctx.fill;
}
return color;
}
function reactifySvgSymbolElement(
ctx: SvgSymbolContext,
el: SvgSymbolElement,
key: number
): JSX.Element {
let { fill, stroke, strokeWidth } = el.props;
let vectorEffect;
fill = getColor(ctx, fill);
stroke = getColor(ctx, stroke);
if (strokeWidth !== undefined && typeof ctx.uniformStrokeWidth === "number") {
strokeWidth = ctx.uniformStrokeWidth;
vectorEffect = "non-scaling-stroke";
}
const props: typeof el.props = {
...el.props,
id: undefined,
vectorEffect,
strokeWidth,
fill,
stroke,
key,
};
return React.createElement(
el.tagName,
props,
el.children.map(reactifySvgSymbolElement.bind(null, ctx))
);
}
export const SvgSymbolContent: React.FC<
{ data: SvgSymbolData } & SvgSymbolContext
> = (props) => {
const d = props.data;
return (
<g data-symbol-name={d.name}>
{props.data.layers.map(reactifySvgSymbolElement.bind(null, props))}
{props.showSpecs && d.specs && <VisibleSpecs specs={d.specs} />}
</g>
);
};
export class AttachmentPointError extends Error {}
export function getAttachmentPoint(
s: SvgSymbolData,
type: AttachmentPointType,
idx: number = 0
): PointWithNormal {
const { specs } = s;
if (!specs) {
throw new AttachmentPointError(`Symbol ${s.name} has no specs.`);
}
const points = specs[type];
if (!(points && points.length > idx)) {
throw new AttachmentPointError(
`Expected symbol ${s.name} to have at least ${
idx + 1
} ${type} attachment point(s).`
);
}
return points[idx];
}
export function safeGetAttachmentPoint(
s: SvgSymbolData,
type: AttachmentPointType,
idx: number = 0
): PointWithNormal | null {
try {
return getAttachmentPoint(s, type, idx);
} catch (e) {
if (e instanceof AttachmentPointError) {
console.log(e.message);
} else {
throw e;
}
}
return null;
}