Massively refactor and simplify creature-symbol.tsx. (#37)

This refactors `creature-symbol.tsx` so that it doesn't have to rely on awkwardly introspecting `JSX.Element` instances to do its job.  Now all of that mumbo-jumbo, which is only really useful for when we want to manually construct symbols like the eye creature, is encapsulated in `creature-symbol-factory.tsx`.
pull/103/head
Atul Varma 2021-02-27 18:55:14 -05:00 zatwierdzone przez GitHub
rodzic 6aba6b665f
commit 80d8f5f72a
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
3 zmienionych plików z 340 dodań i 250 usunięć

Wyświetl plik

@ -0,0 +1,143 @@
import React from "react";
import {
AttachedCreatureSymbol,
CreatureSymbol,
NestedCreatureSymbol,
} from "./creature-symbol";
import { AttachmentPointType } from "./specs";
import { SvgSymbolData } from "./svg-symbol";
type AttachmentIndices = {
left?: boolean;
right?: boolean;
};
type AttachmentChildren = JSX.Element | JSX.Element[];
type SimpleCreatureSymbolProps = AttachmentIndices & {
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 SimpleCreatureSymbolFC = React.FC<SimpleCreatureSymbolProps> & {
creatureSymbolData: SvgSymbolData;
};
/**
* Create a factory that can be used to return React components to
* render a `<CreatureSymbol>`.
*/
export function createCreatureSymbolFactory(
getSymbol: (name: string) => SvgSymbolData
) {
/**
* Returns a React component that renders a `<CreatureSymbol>`, using the symbol
* with the given name as its default data.
*/
return function createCreatureSymbol(
name: string
): React.FC<SimpleCreatureSymbolProps> {
const data = getSymbol(name);
const Component: SimpleCreatureSymbolFC = (props) => {
const symbol = getCreatureSymbol(data, props);
return <CreatureSymbol {...symbol} />;
};
Component.creatureSymbolData = data;
return Component;
};
}
function isSimpleCreatureSymbolFC(fn: any): fn is SimpleCreatureSymbolFC {
return !!fn.creatureSymbolData;
}
function extractNestedCreatureSymbol(el: JSX.Element): NestedCreatureSymbol {
const base = extractCreatureSymbolFromElement(el);
const props: SimpleCreatureSymbolProps = el.props;
const indices = props.indices || getAttachmentIndices(props);
const result: NestedCreatureSymbol = {
...base,
indices,
};
return result;
}
function extractAttachedCreatureSymbol(
el: JSX.Element
): AttachedCreatureSymbol {
const base = extractNestedCreatureSymbol(el);
const props: SimpleCreatureSymbolProps = el.props;
const { attachTo } = props;
if (!attachTo) {
throw new Error("Expected attachment to have `attachTo` prop!");
}
const result: AttachedCreatureSymbol = {
...base,
attachTo,
};
return result;
}
function getCreatureSymbol(
data: SvgSymbolData,
props: SimpleCreatureSymbolProps
): CreatureSymbol {
const { attachments, nests } = splitCreatureSymbolChildren(props.children);
const result: CreatureSymbol = {
data,
attachments: attachments.map(extractAttachedCreatureSymbol),
nests: nests.map(extractNestedCreatureSymbol),
};
return result;
}
export function extractCreatureSymbolFromElement(
el: JSX.Element
): CreatureSymbol {
if (isSimpleCreatureSymbolFC(el.type)) {
return getCreatureSymbol(el.type.creatureSymbolData, el.props);
}
throw new Error("Found unknown component type!");
}

Wyświetl plik

@ -1,8 +1,8 @@
import React, { useContext } from "react";
import { getAttachmentTransforms } from "./attach";
import { scalePointXY, subtractPoints } from "./point";
import { BBox, Point } from "../vendor/bezier-js";
import { getAttachmentTransforms } from "./attach";
import { getBoundingBoxCenter, uniformlyScaleToFit } from "./bounding-box";
import { scalePointXY, subtractPoints } from "./point";
import { AttachmentPointType, PointWithNormal } from "./specs";
import {
createSvgSymbolContext,
@ -54,8 +54,6 @@ function safeGetAttachmentPoint(
return null;
}
type AttachmentChildren = JSX.Element | JSX.Element[];
export type CreatureContextType = SvgSymbolContext & {
attachmentScale: number;
parent: SvgSymbolData | null;
@ -67,206 +65,29 @@ export const CreatureContext = React.createContext<CreatureContextType>({
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;
export type AttachedCreatureSymbol = CreatureSymbol & {
attachTo: AttachmentPointType;
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.log(
`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}
>
<g
data-attach-parent={parent.name}
data-attach-type="nesting"
data-attach-index={nestIndex}
>
{symbol}
</g>
</AttachmentTransform>
);
}
return <>{children}</>;
export type NestedCreatureSymbol = CreatureSymbol & {
indices: number[];
};
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}
>
<g
data-attach-parent={parent.name}
data-attach-type={attachTo}
data-attach-index={attachIndex}
>
{symbol}
</g>
</AttachmentTransform>
);
}
return <>{children}</>;
export type CreatureSymbol = {
data: SvgSymbolData;
attachments: AttachedCreatureSymbol[];
nests: NestedCreatureSymbol[];
};
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);
export type CreatureSymbolProps = CreatureSymbol;
// 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>
)}
</>
);
type NestedCreatureSymbolProps = NestedCreatureSymbol & {
parent: SvgSymbolData;
};
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} />;
type AttachedCreatureSymbolProps = AttachedCreatureSymbol & {
parent: SvgSymbolData;
};
function getNestingTransforms(parent: BBox, child: BBox) {
@ -310,3 +131,126 @@ const AttachmentTransform: React.FC<AttachmentTransformProps> = (props) => (
</g>
</g>
);
const AttachedCreatureSymbol: React.FC<AttachedCreatureSymbolProps> = ({
indices,
parent,
attachTo,
data,
...props
}) => {
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}
>
<g
data-attach-parent={parent.name}
data-attach-type={attachTo}
data-attach-index={attachIndex}
>
<CreatureSymbol data={data} {...props} />
</g>
</AttachmentTransform>
);
}
return <>{children}</>;
};
const NestedCreatureSymbol: React.FC<NestedCreatureSymbolProps> = ({
indices,
parent,
data,
...props
}) => {
const children: JSX.Element[] = [];
for (let nestIndex of indices) {
const parentNest = (parent.specs?.nesting ?? [])[nestIndex];
if (!parentNest) {
console.log(
`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}
>
<g
data-attach-parent={parent.name}
data-attach-type="nesting"
data-attach-index={nestIndex}
>
<CreatureSymbol data={data} {...props} />
</g>
</AttachmentTransform>
);
}
return <>{children}</>;
};
export const CreatureSymbol: React.FC<CreatureSymbolProps> = (props) => {
const ctx = useContext(CreatureContext);
const { data, attachments, nests } = props;
const childCtx: CreatureContextType = { ...ctx, parent: data };
// 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.
return (
<>
{attachments.length && (
<CreatureContext.Provider value={childCtx}>
{attachments.map((a, i) => (
<AttachedCreatureSymbol key={i} {...a} parent={data} />
))}
</CreatureContext.Provider>
)}
<SvgSymbolContent data={data} {...ctx} />
{nests.length && (
<CreatureContext.Provider value={childCtx}>
{nests.map((n, i) => (
<NestedCreatureSymbol key={i} {...n} parent={data} />
))}
</CreatureContext.Provider>
)}
</>
);
};

Wyświetl plik

@ -8,11 +8,15 @@ import { range } from "../util";
import { AutoSizingSvg } from "../auto-sizing-svg";
import { exportSvg } from "../export-svg";
import {
createCreatureSymbolFactory,
extractCreatureSymbolFromElement,
} from "../creature-symbol-factory";
import {
CreatureContext,
CreatureContextType,
CreatureSymbol,
CreatureSymbolProps,
NestedCreatureSymbol,
} from "../creature-symbol";
import { HoverDebugHelper } from "../hover-debug-helper";
@ -57,13 +61,21 @@ function getSymbol(name: string): SvgSymbolData {
* Can return an empty array e.g. if the parent symbol doesn't have
* any nesting areas.
*/
function getNestingChildren(parent: SvgSymbolData, rng: Random): JSX.Element[] {
function getNestingChildren(
parent: SvgSymbolData,
rng: Random
): NestedCreatureSymbol[] {
const { meta, specs } = parent;
if (meta?.always_nest && specs?.nesting) {
const indices = range(specs.nesting.length);
const child = rng.choice(NESTED_SYMBOLS);
return [
<CreatureSymbol data={child} key="nested" nestInside indices={indices} />,
{
data: child,
attachments: [],
nests: [],
indices,
},
];
}
return [];
@ -77,9 +89,13 @@ function getNestingChildren(parent: SvgSymbolData, rng: Random): JSX.Element[] {
function getSymbolWithAttachments(
numAttachmentKinds: number,
rng: Random
): JSX.Element {
const children: JSX.Element[] = [];
): CreatureSymbol {
const root = rng.choice(ROOT_SYMBOLS);
const result: CreatureSymbol = {
data: root,
attachments: [],
nests: getNestingChildren(root, rng),
};
if (root.specs) {
const attachmentKinds = rng.uniqueChoices(
Array.from(iterAttachmentPoints(root.specs))
@ -90,60 +106,39 @@ function getSymbolWithAttachments(
for (let kind of attachmentKinds) {
const attachment = rng.choice(ATTACHMENT_SYMBOLS);
const indices = range(root.specs[kind]?.length ?? 0);
children.push(
<CreatureSymbol
data={attachment}
key={children.length}
attachTo={kind}
indices={indices}
children={getNestingChildren(attachment, rng)}
/>
);
result.attachments.push({
data: attachment,
attachTo: kind,
indices,
attachments: [],
nests: getNestingChildren(attachment, rng),
});
}
}
children.push(...getNestingChildren(root, rng));
return <CreatureSymbol data={root} children={children} />;
return result;
}
/**
* A creature symbol that comes with default (but overrideable) symbol data.
* This makes it easy to use the symbol in JSX, but also easy to dynamically
* replace the symbol with a different one.
*/
type CreatureSymbolWithDefaultProps = Omit<CreatureSymbolProps, "data"> & {
data?: SvgSymbolData;
};
const symbol = createCreatureSymbolFactory(getSymbol);
/**
* Returns a React component that renders a `<CreatureSymbol>`, using the symbol
* with the given name as its default data.
*/
function createCreatureSymbol(
name: string
): React.FC<CreatureSymbolWithDefaultProps> {
const data = getSymbol(name);
return (props) => <CreatureSymbol data={props.data || data} {...props} />;
}
const Eye = symbol("eye");
const Eye = createCreatureSymbol("eye");
const Hand = symbol("hand");
const Hand = createCreatureSymbol("hand");
const Arm = symbol("arm");
const Arm = createCreatureSymbol("arm");
const Antler = symbol("antler");
const Antler = createCreatureSymbol("antler");
const Crown = symbol("crown");
const Crown = createCreatureSymbol("crown");
const Wing = symbol("wing");
const Wing = createCreatureSymbol("wing");
const MuscleArm = symbol("muscle_arm");
const MuscleArm = createCreatureSymbol("muscle_arm");
const Leg = symbol("leg");
const Leg = createCreatureSymbol("leg");
const Tail = symbol("tail");
const Tail = createCreatureSymbol("tail");
const Lightning = createCreatureSymbol("lightning");
const Lightning = symbol("lightning");
const EYE_CREATURE = (
<Eye>
@ -165,22 +160,28 @@ const EYE_CREATURE = (
</Eye>
);
const EYE_CREATURE_SYMBOL = extractCreatureSymbolFromElement(EYE_CREATURE);
/**
* Randomly replace all the parts of the given creature. Note that this
* might end up logging some console messages about not being able to find
* attachment/nesting indices, because it doesn't really check to make
* sure the final creature structure is fully valid.
*/
function randomlyReplaceParts(rng: Random, creature: JSX.Element): JSX.Element {
return React.cloneElement<CreatureSymbolWithDefaultProps>(creature, {
function randomlyReplaceParts<T extends CreatureSymbol>(
rng: Random,
creature: T
): T {
const result: T = {
...creature,
data: rng.choice(SvgVocabulary),
children: React.Children.map(creature.props.children, (child, i) => {
return randomlyReplaceParts(rng, child);
}),
});
attachments: creature.attachments.map((a) => randomlyReplaceParts(rng, a)),
nests: creature.nests.map((n) => randomlyReplaceParts(rng, n)),
};
return result;
}
type CreatureGenerator = (rng: Random) => JSX.Element;
type CreatureGenerator = (rng: Random) => CreatureSymbol;
/**
* Each index of this array represents the algorithm we use to
@ -191,7 +192,7 @@ type CreatureGenerator = (rng: Random) => JSX.Element;
*/
const COMPLEXITY_LEVEL_GENERATORS: CreatureGenerator[] = [
...range(5).map((i) => getSymbolWithAttachments.bind(null, i)),
(rng) => randomlyReplaceParts(rng, EYE_CREATURE),
(rng) => randomlyReplaceParts(rng, EYE_CREATURE_SYMBOL),
];
const MAX_COMPLEXITY_LEVEL = COMPLEXITY_LEVEL_GENERATORS.length - 1;
@ -221,7 +222,7 @@ export const CreaturePage: React.FC<{}> = () => {
};
const creature =
randomSeed === null
? EYE_CREATURE
? EYE_CREATURE_SYMBOL
: COMPLEXITY_LEVEL_GENERATORS[complexity](new Random(randomSeed));
const handleSvgExport = () =>
exportSvg(getDownloadFilename(randomSeed), svgRef);
@ -262,7 +263,9 @@ export const CreaturePage: React.FC<{}> = () => {
<CreatureContext.Provider value={ctx}>
<HoverDebugHelper>
<AutoSizingSvg padding={20} ref={svgRef} bgColor={bgColor}>
<g transform="scale(0.5 0.5)">{creature}</g>
<g transform="scale(0.5 0.5)">
<CreatureSymbol {...creature} />
</g>
</AutoSizingSvg>
</HoverDebugHelper>
</CreatureContext.Provider>