187 wiersze
4.3 KiB
TypeScript
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;
|
|
}
|