Add basic support for nesting. (#30)

This adds basic support for nesting (#17).  It does not swap fill/stroke colors based on the position of the nesting box, though. It also only nests a symbol in the eye creature and "bonkers" complexity setting for now--support for nesting in complexity levels 1-4 is forthcoming.

The nested symbol is essentially uniformly scaled as much as possible without extending outside the boundaries of its parent's nesting box.
pull/33/head
Atul Varma 2021-02-25 21:57:10 -05:00 zatwierdzone przez GitHub
rodzic fd1da96285
commit 520ea6aff4
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
3 zmienionych plików z 222 dodań i 30 usunięć

Wyświetl plik

@ -0,0 +1,72 @@
import { BBox } from "../vendor/bezier-js";
import { uniformlyScaleToFit } from "./bounding-box";
describe("uniformlyScaleToFit()", () => {
it("returns 1 for identical boxes", () => {
const box: BBox = {
x: { min: 0, max: 1 },
y: { min: 0, max: 1 },
};
expect(uniformlyScaleToFit(box, box)).toBe(1.0);
});
it("returns 1 for identically-sized boxes", () => {
const box1: BBox = {
x: { min: 0, max: 1 },
y: { min: 0, max: 1 },
};
const box2: BBox = {
x: { min: -5, max: -4 },
y: { min: -20, max: -19 },
};
expect(uniformlyScaleToFit(box1, box2)).toBe(1.0);
});
it("returns 2 when child is half the size of parent", () => {
const parent: BBox = {
x: { min: 0, max: 1 },
y: { min: 0, max: 1 },
};
const child: BBox = {
x: { min: 0, max: 0.5 },
y: { min: 0, max: 0.5 },
};
expect(uniformlyScaleToFit(parent, child)).toBe(2.0);
});
it("returns 0.5 when child is twice the size of parent", () => {
const parent: BBox = {
x: { min: 0, max: 1 },
y: { min: 0, max: 1 },
};
const child: BBox = {
x: { min: 0, max: 2 },
y: { min: 0, max: 2 },
};
expect(uniformlyScaleToFit(parent, child)).toBe(0.5);
});
it("returns 1 when child is same width as parent but shorter", () => {
const parent: BBox = {
x: { min: 0, max: 1 },
y: { min: 0, max: 1 },
};
const child: BBox = {
x: { min: 0, max: 1 },
y: { min: 0, max: 0.1 },
};
expect(uniformlyScaleToFit(parent, child)).toBe(1);
});
it("returns 1 when child is same height as parent but thinner", () => {
const parent: BBox = {
x: { min: 0, max: 1 },
y: { min: 0, max: 1 },
};
const child: BBox = {
x: { min: 0, max: 0.1 },
y: { min: 0, max: 1 },
};
expect(uniformlyScaleToFit(parent, child)).toBe(1);
});
});

Wyświetl plik

@ -91,3 +91,18 @@ export function getSvgBoundingBox(
return getPathBoundingBox(element.props);
}
}
/**
* Assuming the origins of the giving boxes are aligned and
* the transform origin is set to their center, return the maximum
* amount the child needs to be scaled to fit within the parent.
*/
export function uniformlyScaleToFit(parent: BBox, child: BBox): number {
const [pWidth, pHeight] = getBoundingBoxSize(parent);
const [cWidth, cHeight] = getBoundingBoxSize(child);
const widthScale = pWidth / cWidth;
const heightScale = pHeight / cHeight;
return Math.min(widthScale, heightScale);
}

Wyświetl plik

@ -12,11 +12,12 @@ import {
PointWithNormal,
} from "../specs";
import { getAttachmentTransforms } from "../attach";
import { scalePointXY } from "../point";
import { Point } from "../../vendor/bezier-js";
import { scalePointXY, subtractPoints } from "../point";
import { BBox, Point } from "../../vendor/bezier-js";
import { Random } from "../random";
import { SymbolContextWidget } from "../symbol-context-widget";
import { range } from "../util";
import { getBoundingBoxCenter, uniformlyScaleToFit } from "../bounding-box";
const DEFAULT_BG_COLOR = "#858585";
@ -89,6 +90,7 @@ type AttachmentIndices = {
type CreatureSymbolProps = AttachmentIndices & {
data: SvgSymbolData;
nestInside?: boolean;
children?: AttachmentChildren;
attachTo?: AttachmentPointType;
indices?: number[];
@ -109,40 +111,80 @@ function getAttachmentIndices(ai: AttachmentIndices): number[] {
return result;
}
const CreatureSymbol: React.FC<CreatureSymbolProps> = (props) => {
const ctx = useContext(CreatureContext);
const { data, attachTo } = props;
const ourSymbol = (
<>
{props.children && (
<CreatureContext.Provider
value={{
...ctx,
parent: data,
}}
>
{props.children}
</CreatureContext.Provider>
)}
<SvgSymbolContent data={data} {...ctx} />
</>
);
type SplitCreatureSymbolChildren = {
attachments: JSX.Element[];
nests: JSX.Element[];
};
if (!attachTo) {
return ourSymbol;
}
function splitCreatureSymbolChildren(
children?: AttachmentChildren
): SplitCreatureSymbolChildren {
const result: SplitCreatureSymbolChildren = {
attachments: [],
nests: [],
};
if (!children) return result;
const parent = ctx.parent;
if (!parent) {
throw new Error(
`Cannot attach ${props.data.name} because it has no parent!`
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>
);
}
const attachmentIndices = props.indices || getAttachmentIndices(props);
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 attachmentIndices) {
for (let attachIndex of indices) {
const parentAp = safeGetAttachmentPoint(parent, attachTo, attachIndex);
const ourAp = safeGetAttachmentPoint(data, "anchor");
@ -173,7 +215,7 @@ const CreatureSymbol: React.FC<CreatureSymbolProps> = (props) => {
scale={{ x: ctx.attachmentScale * xFlip, y: ctx.attachmentScale }}
rotate={xFlip * t.rotation}
>
{ourSymbol}
{symbol}
</AttachmentTransform>
);
}
@ -181,6 +223,66 @@ const CreatureSymbol: React.FC<CreatureSymbolProps> = (props) => {
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;
@ -242,6 +344,8 @@ const Leg = createCreatureSymbol("leg");
const Tail = createCreatureSymbol("tail");
const Lightning = createCreatureSymbol("lightning");
function getSymbolWithAttachments(
numAttachmentKinds: number,
rng: Random
@ -273,6 +377,7 @@ function getSymbolWithAttachments(
const EYE_CREATURE = (
<Eye>
<Lightning nestInside />
<Arm attachTo="arm" left>
<Wing attachTo="arm" left right />
</Arm>