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