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
rodzic
6aba6b665f
commit
80d8f5f72a
|
@ -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!");
|
||||||
|
}
|
|
@ -1,8 +1,8 @@
|
||||||
import React, { useContext } from "react";
|
import React, { useContext } from "react";
|
||||||
import { getAttachmentTransforms } from "./attach";
|
|
||||||
import { scalePointXY, subtractPoints } from "./point";
|
|
||||||
import { BBox, Point } from "../vendor/bezier-js";
|
import { BBox, Point } from "../vendor/bezier-js";
|
||||||
|
import { getAttachmentTransforms } from "./attach";
|
||||||
import { getBoundingBoxCenter, uniformlyScaleToFit } from "./bounding-box";
|
import { getBoundingBoxCenter, uniformlyScaleToFit } from "./bounding-box";
|
||||||
|
import { scalePointXY, subtractPoints } from "./point";
|
||||||
import { AttachmentPointType, PointWithNormal } from "./specs";
|
import { AttachmentPointType, PointWithNormal } from "./specs";
|
||||||
import {
|
import {
|
||||||
createSvgSymbolContext,
|
createSvgSymbolContext,
|
||||||
|
@ -54,8 +54,6 @@ function safeGetAttachmentPoint(
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
type AttachmentChildren = JSX.Element | JSX.Element[];
|
|
||||||
|
|
||||||
export type CreatureContextType = SvgSymbolContext & {
|
export type CreatureContextType = SvgSymbolContext & {
|
||||||
attachmentScale: number;
|
attachmentScale: number;
|
||||||
parent: SvgSymbolData | null;
|
parent: SvgSymbolData | null;
|
||||||
|
@ -67,206 +65,29 @@ export const CreatureContext = React.createContext<CreatureContextType>({
|
||||||
parent: null,
|
parent: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
type AttachmentIndices = {
|
export type AttachedCreatureSymbol = CreatureSymbol & {
|
||||||
left?: boolean;
|
attachTo: AttachmentPointType;
|
||||||
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[];
|
indices: number[];
|
||||||
};
|
};
|
||||||
|
|
||||||
const NestedCreatureSymbol: React.FC<ChildCreatureSymbolProps> = ({
|
export type NestedCreatureSymbol = CreatureSymbol & {
|
||||||
symbol,
|
indices: number[];
|
||||||
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}</>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const AttachedCreatureSymbol: React.FC<
|
export type CreatureSymbol = {
|
||||||
ChildCreatureSymbolProps & {
|
data: SvgSymbolData;
|
||||||
attachTo: AttachmentPointType;
|
attachments: AttachedCreatureSymbol[];
|
||||||
}
|
nests: NestedCreatureSymbol[];
|
||||||
> = ({ 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 const CreatureSymbol: React.FC<CreatureSymbolProps> = (props) => {
|
export type CreatureSymbolProps = CreatureSymbol;
|
||||||
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
|
type NestedCreatureSymbolProps = NestedCreatureSymbol & {
|
||||||
// appear behind our symbol, while anything nested within our symbol
|
parent: SvgSymbolData;
|
||||||
// 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)) {
|
type AttachedCreatureSymbolProps = AttachedCreatureSymbol & {
|
||||||
return symbol;
|
parent: SvgSymbolData;
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
function getNestingTransforms(parent: BBox, child: BBox) {
|
||||||
|
@ -310,3 +131,126 @@ const AttachmentTransform: React.FC<AttachmentTransformProps> = (props) => (
|
||||||
</g>
|
</g>
|
||||||
</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>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
|
@ -8,11 +8,15 @@ import { range } from "../util";
|
||||||
|
|
||||||
import { AutoSizingSvg } from "../auto-sizing-svg";
|
import { AutoSizingSvg } from "../auto-sizing-svg";
|
||||||
import { exportSvg } from "../export-svg";
|
import { exportSvg } from "../export-svg";
|
||||||
|
import {
|
||||||
|
createCreatureSymbolFactory,
|
||||||
|
extractCreatureSymbolFromElement,
|
||||||
|
} from "../creature-symbol-factory";
|
||||||
import {
|
import {
|
||||||
CreatureContext,
|
CreatureContext,
|
||||||
CreatureContextType,
|
CreatureContextType,
|
||||||
CreatureSymbol,
|
CreatureSymbol,
|
||||||
CreatureSymbolProps,
|
NestedCreatureSymbol,
|
||||||
} from "../creature-symbol";
|
} from "../creature-symbol";
|
||||||
import { HoverDebugHelper } from "../hover-debug-helper";
|
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
|
* Can return an empty array e.g. if the parent symbol doesn't have
|
||||||
* any nesting areas.
|
* any nesting areas.
|
||||||
*/
|
*/
|
||||||
function getNestingChildren(parent: SvgSymbolData, rng: Random): JSX.Element[] {
|
function getNestingChildren(
|
||||||
|
parent: SvgSymbolData,
|
||||||
|
rng: Random
|
||||||
|
): NestedCreatureSymbol[] {
|
||||||
const { meta, specs } = parent;
|
const { meta, specs } = parent;
|
||||||
if (meta?.always_nest && specs?.nesting) {
|
if (meta?.always_nest && specs?.nesting) {
|
||||||
const indices = range(specs.nesting.length);
|
const indices = range(specs.nesting.length);
|
||||||
const child = rng.choice(NESTED_SYMBOLS);
|
const child = rng.choice(NESTED_SYMBOLS);
|
||||||
return [
|
return [
|
||||||
<CreatureSymbol data={child} key="nested" nestInside indices={indices} />,
|
{
|
||||||
|
data: child,
|
||||||
|
attachments: [],
|
||||||
|
nests: [],
|
||||||
|
indices,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
|
@ -77,9 +89,13 @@ function getNestingChildren(parent: SvgSymbolData, rng: Random): JSX.Element[] {
|
||||||
function getSymbolWithAttachments(
|
function getSymbolWithAttachments(
|
||||||
numAttachmentKinds: number,
|
numAttachmentKinds: number,
|
||||||
rng: Random
|
rng: Random
|
||||||
): JSX.Element {
|
): CreatureSymbol {
|
||||||
const children: JSX.Element[] = [];
|
|
||||||
const root = rng.choice(ROOT_SYMBOLS);
|
const root = rng.choice(ROOT_SYMBOLS);
|
||||||
|
const result: CreatureSymbol = {
|
||||||
|
data: root,
|
||||||
|
attachments: [],
|
||||||
|
nests: getNestingChildren(root, rng),
|
||||||
|
};
|
||||||
if (root.specs) {
|
if (root.specs) {
|
||||||
const attachmentKinds = rng.uniqueChoices(
|
const attachmentKinds = rng.uniqueChoices(
|
||||||
Array.from(iterAttachmentPoints(root.specs))
|
Array.from(iterAttachmentPoints(root.specs))
|
||||||
|
@ -90,60 +106,39 @@ function getSymbolWithAttachments(
|
||||||
for (let kind of attachmentKinds) {
|
for (let kind of attachmentKinds) {
|
||||||
const attachment = rng.choice(ATTACHMENT_SYMBOLS);
|
const attachment = rng.choice(ATTACHMENT_SYMBOLS);
|
||||||
const indices = range(root.specs[kind]?.length ?? 0);
|
const indices = range(root.specs[kind]?.length ?? 0);
|
||||||
children.push(
|
result.attachments.push({
|
||||||
<CreatureSymbol
|
data: attachment,
|
||||||
data={attachment}
|
attachTo: kind,
|
||||||
key={children.length}
|
indices,
|
||||||
attachTo={kind}
|
attachments: [],
|
||||||
indices={indices}
|
nests: getNestingChildren(attachment, rng),
|
||||||
children={getNestingChildren(attachment, rng)}
|
});
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
children.push(...getNestingChildren(root, rng));
|
return result;
|
||||||
return <CreatureSymbol data={root} children={children} />;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
const symbol = createCreatureSymbolFactory(getSymbol);
|
||||||
* 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 Eye = symbol("eye");
|
||||||
* 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 = 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 = symbol("lightning");
|
||||||
|
|
||||||
const Lightning = createCreatureSymbol("lightning");
|
|
||||||
|
|
||||||
const EYE_CREATURE = (
|
const EYE_CREATURE = (
|
||||||
<Eye>
|
<Eye>
|
||||||
|
@ -165,22 +160,28 @@ const EYE_CREATURE = (
|
||||||
</Eye>
|
</Eye>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const EYE_CREATURE_SYMBOL = extractCreatureSymbolFromElement(EYE_CREATURE);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Randomly replace all the parts of the given creature. Note that this
|
* 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
|
* might end up logging some console messages about not being able to find
|
||||||
* attachment/nesting indices, because it doesn't really check to make
|
* attachment/nesting indices, because it doesn't really check to make
|
||||||
* sure the final creature structure is fully valid.
|
* sure the final creature structure is fully valid.
|
||||||
*/
|
*/
|
||||||
function randomlyReplaceParts(rng: Random, creature: JSX.Element): JSX.Element {
|
function randomlyReplaceParts<T extends CreatureSymbol>(
|
||||||
return React.cloneElement<CreatureSymbolWithDefaultProps>(creature, {
|
rng: Random,
|
||||||
|
creature: T
|
||||||
|
): T {
|
||||||
|
const result: T = {
|
||||||
|
...creature,
|
||||||
data: rng.choice(SvgVocabulary),
|
data: rng.choice(SvgVocabulary),
|
||||||
children: React.Children.map(creature.props.children, (child, i) => {
|
attachments: creature.attachments.map((a) => randomlyReplaceParts(rng, a)),
|
||||||
return randomlyReplaceParts(rng, child);
|
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
|
* 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[] = [
|
const COMPLEXITY_LEVEL_GENERATORS: CreatureGenerator[] = [
|
||||||
...range(5).map((i) => getSymbolWithAttachments.bind(null, i)),
|
...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;
|
const MAX_COMPLEXITY_LEVEL = COMPLEXITY_LEVEL_GENERATORS.length - 1;
|
||||||
|
@ -221,7 +222,7 @@ export const CreaturePage: React.FC<{}> = () => {
|
||||||
};
|
};
|
||||||
const creature =
|
const creature =
|
||||||
randomSeed === null
|
randomSeed === null
|
||||||
? EYE_CREATURE
|
? EYE_CREATURE_SYMBOL
|
||||||
: COMPLEXITY_LEVEL_GENERATORS[complexity](new Random(randomSeed));
|
: COMPLEXITY_LEVEL_GENERATORS[complexity](new Random(randomSeed));
|
||||||
const handleSvgExport = () =>
|
const handleSvgExport = () =>
|
||||||
exportSvg(getDownloadFilename(randomSeed), svgRef);
|
exportSvg(getDownloadFilename(randomSeed), svgRef);
|
||||||
|
@ -262,7 +263,9 @@ export const CreaturePage: React.FC<{}> = () => {
|
||||||
<CreatureContext.Provider value={ctx}>
|
<CreatureContext.Provider value={ctx}>
|
||||||
<HoverDebugHelper>
|
<HoverDebugHelper>
|
||||||
<AutoSizingSvg padding={20} ref={svgRef} bgColor={bgColor}>
|
<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>
|
</AutoSizingSvg>
|
||||||
</HoverDebugHelper>
|
</HoverDebugHelper>
|
||||||
</CreatureContext.Provider>
|
</CreatureContext.Provider>
|
||||||
|
|
Ładowanie…
Reference in New Issue