Factor out creature-symbol.tsx.
rodzic
9a014dba56
commit
4098a23482
|
@ -0,0 +1,294 @@
|
|||
import React, { useContext } from "react";
|
||||
import { getAttachmentTransforms } from "./attach";
|
||||
import { scalePointXY, subtractPoints } from "./point";
|
||||
import { BBox, Point } from "../vendor/bezier-js";
|
||||
import { getBoundingBoxCenter, uniformlyScaleToFit } from "./bounding-box";
|
||||
import { AttachmentPointType, PointWithNormal } from "./specs";
|
||||
import {
|
||||
createSvgSymbolContext,
|
||||
SvgSymbolContent,
|
||||
SvgSymbolContext,
|
||||
SvgSymbolData,
|
||||
} from "./svg-symbol";
|
||||
|
||||
function getAttachmentPoint(
|
||||
s: SvgSymbolData,
|
||||
type: AttachmentPointType,
|
||||
idx: number = 0
|
||||
): PointWithNormal {
|
||||
const { specs } = s;
|
||||
if (!specs) {
|
||||
throw new Error(`Symbol ${s.name} has no specs!`);
|
||||
}
|
||||
const points = specs[type];
|
||||
if (!(points && points.length > idx)) {
|
||||
throw new Error(
|
||||
`Expected symbol ${s.name} to have at least ${
|
||||
idx + 1
|
||||
} ${type} attachment point(s)!`
|
||||
);
|
||||
}
|
||||
|
||||
return points[idx];
|
||||
}
|
||||
|
||||
function safeGetAttachmentPoint(
|
||||
s: SvgSymbolData,
|
||||
type: AttachmentPointType,
|
||||
idx: number = 0
|
||||
): PointWithNormal | null {
|
||||
try {
|
||||
return getAttachmentPoint(s, type, idx);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
type AttachmentChildren = JSX.Element | JSX.Element[];
|
||||
|
||||
export type CreatureContextType = SvgSymbolContext & {
|
||||
attachmentScale: number;
|
||||
parent: SvgSymbolData | null;
|
||||
};
|
||||
|
||||
const DEFAULT_ATTACHMENT_SCALE = 0.5;
|
||||
|
||||
export const CreatureContext = React.createContext<CreatureContextType>({
|
||||
...createSvgSymbolContext(),
|
||||
attachmentScale: DEFAULT_ATTACHMENT_SCALE,
|
||||
parent: null,
|
||||
});
|
||||
|
||||
type AttachmentIndices = {
|
||||
left?: boolean;
|
||||
right?: boolean;
|
||||
};
|
||||
|
||||
export type CreatureSymbolProps = AttachmentIndices & {
|
||||
data: SvgSymbolData;
|
||||
nestInside?: boolean;
|
||||
children?: AttachmentChildren;
|
||||
attachTo?: AttachmentPointType;
|
||||
indices?: number[];
|
||||
};
|
||||
|
||||
function getAttachmentIndices(ai: AttachmentIndices): number[] {
|
||||
const result: number[] = [];
|
||||
|
||||
if (ai.left) {
|
||||
result.push(0);
|
||||
}
|
||||
if (ai.right) {
|
||||
result.push(1);
|
||||
}
|
||||
if (result.length === 0) {
|
||||
result.push(0);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
type SplitCreatureSymbolChildren = {
|
||||
attachments: JSX.Element[];
|
||||
nests: JSX.Element[];
|
||||
};
|
||||
|
||||
function splitCreatureSymbolChildren(
|
||||
children?: AttachmentChildren
|
||||
): SplitCreatureSymbolChildren {
|
||||
const result: SplitCreatureSymbolChildren = {
|
||||
attachments: [],
|
||||
nests: [],
|
||||
};
|
||||
if (!children) return result;
|
||||
|
||||
React.Children.forEach(children, (child) => {
|
||||
if (child.props.nestInside) {
|
||||
result.nests.push(child);
|
||||
} else {
|
||||
result.attachments.push(child);
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
type ChildCreatureSymbolProps = {
|
||||
symbol: JSX.Element;
|
||||
data: SvgSymbolData;
|
||||
parent: SvgSymbolData;
|
||||
indices: number[];
|
||||
};
|
||||
|
||||
const NestedCreatureSymbol: React.FC<ChildCreatureSymbolProps> = ({
|
||||
symbol,
|
||||
data,
|
||||
parent,
|
||||
indices,
|
||||
}) => {
|
||||
const children: JSX.Element[] = [];
|
||||
|
||||
for (let nestIndex of indices) {
|
||||
const parentNest = (parent.specs?.nesting ?? [])[nestIndex];
|
||||
if (!parentNest) {
|
||||
console.error(
|
||||
`Parent symbol ${parent.name} has no nesting index ${nestIndex}!`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
const t = getNestingTransforms(parentNest, data.bbox);
|
||||
children.push(
|
||||
<AttachmentTransform
|
||||
key={nestIndex}
|
||||
transformOrigin={t.transformOrigin}
|
||||
translate={t.translation}
|
||||
scale={t.scaling}
|
||||
rotate={0}
|
||||
>
|
||||
{symbol}
|
||||
</AttachmentTransform>
|
||||
);
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
const AttachedCreatureSymbol: React.FC<
|
||||
ChildCreatureSymbolProps & {
|
||||
attachTo: AttachmentPointType;
|
||||
}
|
||||
> = ({ symbol, data, parent, indices, attachTo }) => {
|
||||
const ctx = useContext(CreatureContext);
|
||||
const children: JSX.Element[] = [];
|
||||
|
||||
for (let attachIndex of indices) {
|
||||
const parentAp = safeGetAttachmentPoint(parent, attachTo, attachIndex);
|
||||
const ourAp = safeGetAttachmentPoint(data, "anchor");
|
||||
|
||||
if (!parentAp || !ourAp) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If we're attaching something oriented towards the left, horizontally flip
|
||||
// the attachment image.
|
||||
let xFlip = parentAp.normal.x < 0 ? -1 : 1;
|
||||
|
||||
// Er, things look weird if we don't inverse the flip logic for
|
||||
// the downward-facing attachments, like legs...
|
||||
if (parentAp.normal.y > 0) {
|
||||
xFlip *= -1;
|
||||
}
|
||||
|
||||
const t = getAttachmentTransforms(parentAp, {
|
||||
point: ourAp.point,
|
||||
normal: scalePointXY(ourAp.normal, xFlip, 1),
|
||||
});
|
||||
|
||||
children.push(
|
||||
<AttachmentTransform
|
||||
key={attachIndex}
|
||||
transformOrigin={ourAp.point}
|
||||
translate={t.translation}
|
||||
scale={{ x: ctx.attachmentScale * xFlip, y: ctx.attachmentScale }}
|
||||
rotate={xFlip * t.rotation}
|
||||
>
|
||||
{symbol}
|
||||
</AttachmentTransform>
|
||||
);
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
export const CreatureSymbol: React.FC<CreatureSymbolProps> = (props) => {
|
||||
const ctx = useContext(CreatureContext);
|
||||
const { data, attachTo, nestInside } = props;
|
||||
const childCtx: CreatureContextType = { ...ctx, parent: data };
|
||||
const { nests, attachments } = splitCreatureSymbolChildren(props.children);
|
||||
|
||||
// The attachments should be before our symbol in the DOM so they
|
||||
// appear behind our symbol, while anything nested within our symbol
|
||||
// should be after our symbol so they appear in front of it.
|
||||
const symbol = (
|
||||
<>
|
||||
{attachments.length && (
|
||||
<CreatureContext.Provider value={childCtx}>
|
||||
{attachments}
|
||||
</CreatureContext.Provider>
|
||||
)}
|
||||
<SvgSymbolContent data={data} {...ctx} />
|
||||
{nests.length && (
|
||||
<CreatureContext.Provider value={childCtx}>
|
||||
{nests}
|
||||
</CreatureContext.Provider>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
if (!(attachTo || nestInside)) {
|
||||
return symbol;
|
||||
}
|
||||
|
||||
const parent = ctx.parent;
|
||||
if (!parent) {
|
||||
throw new Error(
|
||||
`Cannot attach/nest ${props.data.name} because it has no parent!`
|
||||
);
|
||||
}
|
||||
|
||||
const childProps: ChildCreatureSymbolProps = {
|
||||
parent,
|
||||
symbol,
|
||||
data,
|
||||
indices: props.indices || getAttachmentIndices(props),
|
||||
};
|
||||
|
||||
if (attachTo) {
|
||||
return <AttachedCreatureSymbol {...childProps} attachTo={attachTo} />;
|
||||
}
|
||||
|
||||
return <NestedCreatureSymbol {...childProps} />;
|
||||
};
|
||||
|
||||
function getNestingTransforms(parent: BBox, child: BBox) {
|
||||
const parentCenter = getBoundingBoxCenter(parent);
|
||||
const childCenter = getBoundingBoxCenter(child);
|
||||
const translation = subtractPoints(parentCenter, childCenter);
|
||||
const uniformScaling = uniformlyScaleToFit(parent, child);
|
||||
const scaling: Point = { x: uniformScaling, y: uniformScaling };
|
||||
|
||||
return { translation, transformOrigin: childCenter, scaling };
|
||||
}
|
||||
|
||||
type AttachmentTransformProps = {
|
||||
transformOrigin: Point;
|
||||
translate: Point;
|
||||
scale: Point;
|
||||
rotate: number;
|
||||
children: JSX.Element;
|
||||
};
|
||||
|
||||
const AttachmentTransform: React.FC<AttachmentTransformProps> = (props) => (
|
||||
<g transform={`translate(${props.translate.x} ${props.translate.y})`}>
|
||||
{/**
|
||||
* We originally used "transform-origin" here but that's not currently
|
||||
* supported by Safari. Instead, we'll set the origin of our symbol to
|
||||
* the transform origin, do the transform, and then move our origin back to
|
||||
* the original origin, which is equivalent to setting "transform-origin".
|
||||
**/}
|
||||
<g
|
||||
transform={`translate(${props.transformOrigin.x} ${props.transformOrigin.y})`}
|
||||
>
|
||||
<g
|
||||
transform={`scale(${props.scale.x} ${props.scale.y}) rotate(${props.rotate})`}
|
||||
>
|
||||
<g
|
||||
transform={`translate(-${props.transformOrigin.x} -${props.transformOrigin.y})`}
|
||||
>
|
||||
{props.children}
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
);
|
|
@ -1,25 +1,19 @@
|
|||
import React, { useContext, useRef, useState } from "react";
|
||||
import { SvgVocabulary } from "../svg-vocabulary";
|
||||
import {
|
||||
createSvgSymbolContext,
|
||||
SvgSymbolContent,
|
||||
SvgSymbolContext,
|
||||
SvgSymbolData,
|
||||
} from "../svg-symbol";
|
||||
import {
|
||||
AttachmentPointType,
|
||||
iterAttachmentPoints,
|
||||
PointWithNormal,
|
||||
} from "../specs";
|
||||
import { getAttachmentTransforms } from "../attach";
|
||||
import { scalePointXY, subtractPoints } from "../point";
|
||||
import { BBox, Point } from "../../vendor/bezier-js";
|
||||
import { createSvgSymbolContext, SvgSymbolData } from "../svg-symbol";
|
||||
import { iterAttachmentPoints } from "../specs";
|
||||
import { Random } from "../random";
|
||||
import { SymbolContextWidget } from "../symbol-context-widget";
|
||||
import { range } from "../util";
|
||||
import { getBoundingBoxCenter, uniformlyScaleToFit } from "../bounding-box";
|
||||
|
||||
import { AutoSizingSvg } from "../auto-sizing-svg";
|
||||
import { exportSvg } from "../export-svg";
|
||||
import {
|
||||
CreatureContext,
|
||||
CreatureContextType,
|
||||
CreatureSymbol,
|
||||
CreatureSymbolProps,
|
||||
} from "../creature-symbol";
|
||||
|
||||
const DEFAULT_BG_COLOR = "#858585";
|
||||
|
||||
|
@ -35,288 +29,6 @@ function getSymbol(name: string): SvgSymbolData {
|
|||
return symbol;
|
||||
}
|
||||
|
||||
function getAttachmentPoint(
|
||||
s: SvgSymbolData,
|
||||
type: AttachmentPointType,
|
||||
idx: number = 0
|
||||
): PointWithNormal {
|
||||
const { specs } = s;
|
||||
if (!specs) {
|
||||
throw new Error(`Symbol ${s.name} has no specs!`);
|
||||
}
|
||||
const points = specs[type];
|
||||
if (!(points && points.length > idx)) {
|
||||
throw new Error(
|
||||
`Expected symbol ${s.name} to have at least ${
|
||||
idx + 1
|
||||
} ${type} attachment point(s)!`
|
||||
);
|
||||
}
|
||||
|
||||
return points[idx];
|
||||
}
|
||||
|
||||
function safeGetAttachmentPoint(
|
||||
s: SvgSymbolData,
|
||||
type: AttachmentPointType,
|
||||
idx: number = 0
|
||||
): PointWithNormal | null {
|
||||
try {
|
||||
return getAttachmentPoint(s, type, idx);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
type AttachmentChildren = JSX.Element | JSX.Element[];
|
||||
|
||||
type CreatureContextType = SvgSymbolContext & {
|
||||
attachmentScale: number;
|
||||
parent: SvgSymbolData | null;
|
||||
};
|
||||
|
||||
const DEFAULT_ATTACHMENT_SCALE = 0.5;
|
||||
|
||||
const CreatureContext = React.createContext<CreatureContextType>({
|
||||
...createSvgSymbolContext(),
|
||||
attachmentScale: DEFAULT_ATTACHMENT_SCALE,
|
||||
parent: null,
|
||||
});
|
||||
|
||||
type AttachmentIndices = {
|
||||
left?: boolean;
|
||||
right?: boolean;
|
||||
};
|
||||
|
||||
type CreatureSymbolProps = AttachmentIndices & {
|
||||
data: SvgSymbolData;
|
||||
nestInside?: boolean;
|
||||
children?: AttachmentChildren;
|
||||
attachTo?: AttachmentPointType;
|
||||
indices?: number[];
|
||||
};
|
||||
|
||||
function getAttachmentIndices(ai: AttachmentIndices): number[] {
|
||||
const result: number[] = [];
|
||||
|
||||
if (ai.left) {
|
||||
result.push(0);
|
||||
}
|
||||
if (ai.right) {
|
||||
result.push(1);
|
||||
}
|
||||
if (result.length === 0) {
|
||||
result.push(0);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
type SplitCreatureSymbolChildren = {
|
||||
attachments: JSX.Element[];
|
||||
nests: JSX.Element[];
|
||||
};
|
||||
|
||||
function splitCreatureSymbolChildren(
|
||||
children?: AttachmentChildren
|
||||
): SplitCreatureSymbolChildren {
|
||||
const result: SplitCreatureSymbolChildren = {
|
||||
attachments: [],
|
||||
nests: [],
|
||||
};
|
||||
if (!children) return result;
|
||||
|
||||
React.Children.forEach(children, (child) => {
|
||||
if (child.props.nestInside) {
|
||||
result.nests.push(child);
|
||||
} else {
|
||||
result.attachments.push(child);
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
type ChildCreatureSymbolProps = {
|
||||
symbol: JSX.Element;
|
||||
data: SvgSymbolData;
|
||||
parent: SvgSymbolData;
|
||||
indices: number[];
|
||||
};
|
||||
|
||||
const NestedCreatureSymbol: React.FC<ChildCreatureSymbolProps> = ({
|
||||
symbol,
|
||||
data,
|
||||
parent,
|
||||
indices,
|
||||
}) => {
|
||||
const children: JSX.Element[] = [];
|
||||
|
||||
for (let nestIndex of indices) {
|
||||
const parentNest = (parent.specs?.nesting ?? [])[nestIndex];
|
||||
if (!parentNest) {
|
||||
console.error(
|
||||
`Parent symbol ${parent.name} has no nesting index ${nestIndex}!`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
const t = getNestingTransforms(parentNest, data.bbox);
|
||||
children.push(
|
||||
<AttachmentTransform
|
||||
key={nestIndex}
|
||||
transformOrigin={t.transformOrigin}
|
||||
translate={t.translation}
|
||||
scale={t.scaling}
|
||||
rotate={0}
|
||||
>
|
||||
{symbol}
|
||||
</AttachmentTransform>
|
||||
);
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
const AttachedCreatureSymbol: React.FC<
|
||||
ChildCreatureSymbolProps & {
|
||||
attachTo: AttachmentPointType;
|
||||
}
|
||||
> = ({ symbol, data, parent, indices, attachTo }) => {
|
||||
const ctx = useContext(CreatureContext);
|
||||
const children: JSX.Element[] = [];
|
||||
|
||||
for (let attachIndex of indices) {
|
||||
const parentAp = safeGetAttachmentPoint(parent, attachTo, attachIndex);
|
||||
const ourAp = safeGetAttachmentPoint(data, "anchor");
|
||||
|
||||
if (!parentAp || !ourAp) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If we're attaching something oriented towards the left, horizontally flip
|
||||
// the attachment image.
|
||||
let xFlip = parentAp.normal.x < 0 ? -1 : 1;
|
||||
|
||||
// Er, things look weird if we don't inverse the flip logic for
|
||||
// the downward-facing attachments, like legs...
|
||||
if (parentAp.normal.y > 0) {
|
||||
xFlip *= -1;
|
||||
}
|
||||
|
||||
const t = getAttachmentTransforms(parentAp, {
|
||||
point: ourAp.point,
|
||||
normal: scalePointXY(ourAp.normal, xFlip, 1),
|
||||
});
|
||||
|
||||
children.push(
|
||||
<AttachmentTransform
|
||||
key={attachIndex}
|
||||
transformOrigin={ourAp.point}
|
||||
translate={t.translation}
|
||||
scale={{ x: ctx.attachmentScale * xFlip, y: ctx.attachmentScale }}
|
||||
rotate={xFlip * t.rotation}
|
||||
>
|
||||
{symbol}
|
||||
</AttachmentTransform>
|
||||
);
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
const CreatureSymbol: React.FC<CreatureSymbolProps> = (props) => {
|
||||
const ctx = useContext(CreatureContext);
|
||||
const { data, attachTo, nestInside } = props;
|
||||
const childCtx: CreatureContextType = { ...ctx, parent: data };
|
||||
const { nests, attachments } = splitCreatureSymbolChildren(props.children);
|
||||
|
||||
// The attachments should be before our symbol in the DOM so they
|
||||
// appear behind our symbol, while anything nested within our symbol
|
||||
// should be after our symbol so they appear in front of it.
|
||||
const symbol = (
|
||||
<>
|
||||
{attachments.length && (
|
||||
<CreatureContext.Provider value={childCtx}>
|
||||
{attachments}
|
||||
</CreatureContext.Provider>
|
||||
)}
|
||||
<SvgSymbolContent data={data} {...ctx} />
|
||||
{nests.length && (
|
||||
<CreatureContext.Provider value={childCtx}>
|
||||
{nests}
|
||||
</CreatureContext.Provider>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
if (!(attachTo || nestInside)) {
|
||||
return symbol;
|
||||
}
|
||||
|
||||
const parent = ctx.parent;
|
||||
if (!parent) {
|
||||
throw new Error(
|
||||
`Cannot attach/nest ${props.data.name} because it has no parent!`
|
||||
);
|
||||
}
|
||||
|
||||
const childProps: ChildCreatureSymbolProps = {
|
||||
parent,
|
||||
symbol,
|
||||
data,
|
||||
indices: props.indices || getAttachmentIndices(props),
|
||||
};
|
||||
|
||||
if (attachTo) {
|
||||
return <AttachedCreatureSymbol {...childProps} attachTo={attachTo} />;
|
||||
}
|
||||
|
||||
return <NestedCreatureSymbol {...childProps} />;
|
||||
};
|
||||
|
||||
function getNestingTransforms(parent: BBox, child: BBox) {
|
||||
const parentCenter = getBoundingBoxCenter(parent);
|
||||
const childCenter = getBoundingBoxCenter(child);
|
||||
const translation = subtractPoints(parentCenter, childCenter);
|
||||
const uniformScaling = uniformlyScaleToFit(parent, child);
|
||||
const scaling: Point = { x: uniformScaling, y: uniformScaling };
|
||||
|
||||
return { translation, transformOrigin: childCenter, scaling };
|
||||
}
|
||||
|
||||
type AttachmentTransformProps = {
|
||||
transformOrigin: Point;
|
||||
translate: Point;
|
||||
scale: Point;
|
||||
rotate: number;
|
||||
children: JSX.Element;
|
||||
};
|
||||
|
||||
const AttachmentTransform: React.FC<AttachmentTransformProps> = (props) => (
|
||||
<g transform={`translate(${props.translate.x} ${props.translate.y})`}>
|
||||
{/**
|
||||
* We originally used "transform-origin" here but that's not currently
|
||||
* supported by Safari. Instead, we'll set the origin of our symbol to
|
||||
* the transform origin, do the transform, and then move our origin back to
|
||||
* the original origin, which is equivalent to setting "transform-origin".
|
||||
**/}
|
||||
<g
|
||||
transform={`translate(${props.transformOrigin.x} ${props.transformOrigin.y})`}
|
||||
>
|
||||
<g
|
||||
transform={`scale(${props.scale.x} ${props.scale.y}) rotate(${props.rotate})`}
|
||||
>
|
||||
<g
|
||||
transform={`translate(-${props.transformOrigin.x} -${props.transformOrigin.y})`}
|
||||
>
|
||||
{props.children}
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
);
|
||||
|
||||
type CreatureSymbolWithDefaultProps = Omit<CreatureSymbolProps, "data"> & {
|
||||
data?: SvgSymbolData;
|
||||
};
|
||||
|
|
Ładowanie…
Reference in New Issue