Add attach_to metadata. (#52)
This fixes #49 by implementing the `attach_to` metadata property.pull/54/head
rodzic
d67848fb67
commit
612195e2f2
|
@ -1,7 +1,11 @@
|
||||||
import React, { useContext, useRef, useState } from "react";
|
import React, { useContext, useRef, useState } from "react";
|
||||||
import { SvgVocabulary } from "../svg-vocabulary";
|
import { SvgVocabulary } from "../svg-vocabulary";
|
||||||
import { createSvgSymbolContext, SvgSymbolData } from "../svg-symbol";
|
import { createSvgSymbolContext, SvgSymbolData } from "../svg-symbol";
|
||||||
import { iterAttachmentPoints } from "../specs";
|
import {
|
||||||
|
AttachmentPointType,
|
||||||
|
ATTACHMENT_POINT_TYPES,
|
||||||
|
iterAttachmentPoints,
|
||||||
|
} from "../specs";
|
||||||
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";
|
||||||
|
@ -34,8 +38,41 @@ const ROOT_SYMBOLS = SvgVocabulary.filter(
|
||||||
(data) => data.meta?.always_be_nested !== true
|
(data) => data.meta?.always_be_nested !== true
|
||||||
);
|
);
|
||||||
|
|
||||||
/** Symbols that can be attached to the main body of a creature. */
|
type AttachmentSymbolMap = {
|
||||||
const ATTACHMENT_SYMBOLS = ROOT_SYMBOLS;
|
[key in AttachmentPointType]: SvgSymbolData[];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Symbols that can be attached to the main body of a creature,
|
||||||
|
* at a particular attachment point.
|
||||||
|
*/
|
||||||
|
const ATTACHMENT_SYMBOLS: AttachmentSymbolMap = (() => {
|
||||||
|
const result = {} as AttachmentSymbolMap;
|
||||||
|
|
||||||
|
for (let type of ATTACHMENT_POINT_TYPES) {
|
||||||
|
result[type] = SvgVocabulary.filter((data) => {
|
||||||
|
const { meta } = data;
|
||||||
|
|
||||||
|
// If we have no metadata whatsoever, it can attach anywhere.
|
||||||
|
if (!meta) return true;
|
||||||
|
|
||||||
|
if (meta.always_be_nested === true) {
|
||||||
|
// This symbol should *only* ever be nested, so return false.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have no "attach_to", it can attach anywhere.
|
||||||
|
if (!meta.attach_to) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only attach to points listed in "attach_to".
|
||||||
|
return meta.attach_to.includes(type);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
})();
|
||||||
|
|
||||||
/** Symbols that can be nested within any part of a creature. */
|
/** Symbols that can be nested within any part of a creature. */
|
||||||
const NESTED_SYMBOLS = SvgVocabulary.filter(
|
const NESTED_SYMBOLS = SvgVocabulary.filter(
|
||||||
|
@ -113,7 +150,7 @@ function getSymbolWithAttachments(
|
||||||
numAttachmentKinds
|
numAttachmentKinds
|
||||||
);
|
);
|
||||||
for (let kind of attachmentKinds) {
|
for (let kind of attachmentKinds) {
|
||||||
const attachment = rng.choice(ATTACHMENT_SYMBOLS);
|
const attachment = rng.choice(ATTACHMENT_SYMBOLS[kind]);
|
||||||
const indices = range(root.specs[kind]?.length ?? 0);
|
const indices = range(root.specs[kind]?.length ?? 0);
|
||||||
result.attachments.push({
|
result.attachments.push({
|
||||||
data: attachment,
|
data: attachment,
|
||||||
|
|
|
@ -43,6 +43,14 @@ export const ATTACHMENT_POINT_TYPES: AttachmentPointType[] = [
|
||||||
"crown",
|
"crown",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const ATTACHMENT_POINT_SET = new Set(ATTACHMENT_POINT_TYPES);
|
||||||
|
|
||||||
|
export function isAttachmentPointType(
|
||||||
|
value: any
|
||||||
|
): value is AttachmentPointType {
|
||||||
|
return ATTACHMENT_POINT_SET.has(value);
|
||||||
|
}
|
||||||
|
|
||||||
export function* iterAttachmentPoints(specs: Specs): Iterable<AttachmentPoint> {
|
export function* iterAttachmentPoints(specs: Specs): Iterable<AttachmentPoint> {
|
||||||
for (let type of ATTACHMENT_POINT_TYPES) {
|
for (let type of ATTACHMENT_POINT_TYPES) {
|
||||||
const points = specs[type];
|
const points = specs[type];
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import toml from "toml";
|
import toml from "toml";
|
||||||
import { validateSvgSymbolMetadata } from "./svg-symbol-metadata";
|
import {
|
||||||
|
validateAttachTo,
|
||||||
|
validateSvgSymbolMetadata,
|
||||||
|
} from "./svg-symbol-metadata";
|
||||||
|
import { withMockConsoleLog } from "./test-util";
|
||||||
|
|
||||||
const templatePath = path.join(__dirname, "..", "svg", "_template.toml");
|
const templatePath = path.join(__dirname, "..", "svg", "_template.toml");
|
||||||
|
|
||||||
|
@ -51,3 +55,24 @@ describe("validateSvgSymbolMetadata()", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("validateAttachTo()", () => {
|
||||||
|
it("works", () => {
|
||||||
|
expect(validateAttachTo(["tail", "leg"])).toEqual(["tail", "leg"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("works", () => {
|
||||||
|
withMockConsoleLog((mockLog) => {
|
||||||
|
expect(validateAttachTo(["beanbag"])).toEqual([]);
|
||||||
|
expect(mockLog).toHaveBeenCalledWith(
|
||||||
|
"Item 'beanbag' in \"attach_to\" is not a valid attachment point."
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("raises error when value is not an array", () => {
|
||||||
|
expect(() => validateAttachTo("blah")).toThrow(
|
||||||
|
'Expected "attach_to" to be an array, but it is a string!'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { AttachmentPointType, isAttachmentPointType } from "./specs";
|
||||||
|
|
||||||
type SvgSymbolMetadataBooleans = {
|
type SvgSymbolMetadataBooleans = {
|
||||||
/**
|
/**
|
||||||
* If true, this indicates that the symbol should always have
|
* If true, this indicates that the symbol should always have
|
||||||
|
@ -37,7 +39,14 @@ function isSvgSymbolMetadataBoolean(
|
||||||
return METADATA_BOOLEANS.has(key as any);
|
return METADATA_BOOLEANS.has(key as any);
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SvgSymbolMetadata = SvgSymbolMetadataBooleans;
|
export type SvgSymbolMetadata = SvgSymbolMetadataBooleans & {
|
||||||
|
/**
|
||||||
|
* If defined, this indicates the kinds of attachment points
|
||||||
|
* that this symbol can attach to. If not defined, it will
|
||||||
|
* be able to attach to any symbol.
|
||||||
|
*/
|
||||||
|
attach_to?: AttachmentPointType[];
|
||||||
|
};
|
||||||
|
|
||||||
export function validateSvgSymbolMetadata(
|
export function validateSvgSymbolMetadata(
|
||||||
obj: any
|
obj: any
|
||||||
|
@ -53,9 +62,33 @@ export function validateSvgSymbolMetadata(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
metadata[key] = value;
|
metadata[key] = value;
|
||||||
|
} else if (key === "attach_to") {
|
||||||
|
metadata.attach_to = validateAttachTo(obj[key]);
|
||||||
} else {
|
} else {
|
||||||
unknownProperties.push(key);
|
unknownProperties.push(key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return { metadata, unknownProperties };
|
return { metadata, unknownProperties };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function validateAttachTo(value: unknown): AttachmentPointType[] {
|
||||||
|
if (!Array.isArray(value)) {
|
||||||
|
throw new Error(
|
||||||
|
`Expected "attach_to" to be an array, but it is a ${typeof value}!`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result: AttachmentPointType[] = [];
|
||||||
|
|
||||||
|
for (let item of value) {
|
||||||
|
if (isAttachmentPointType(item)) {
|
||||||
|
result.push(item);
|
||||||
|
} else {
|
||||||
|
console.log(
|
||||||
|
`Item '${item}' in "attach_to" is not a valid attachment point.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
export function withMockConsoleLog(fn: (mock: jest.Mock) => void) {
|
||||||
|
const originalLog = console.log;
|
||||||
|
const mockLog = jest.fn();
|
||||||
|
console.log = mockLog;
|
||||||
|
try {
|
||||||
|
fn(mockLog);
|
||||||
|
} finally {
|
||||||
|
console.log = originalLog;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { withMockConsoleLog } from "./test-util";
|
||||||
import { convertSvgMarkupToSymbolData } from "./vocabulary-builder";
|
import { convertSvgMarkupToSymbolData } from "./vocabulary-builder";
|
||||||
|
|
||||||
const CIRCLE = `<path fill="#ffffff" fill-rule="evenodd" stroke="#000000" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" d="M 360.000 29.751 C 542.791 29.751 690.249 177.209 690.249 360.000 C 690.249 542.791 542.791 690.249 360.000 690.249 C 177.209 690.249 29.751 542.791 29.751 360.000 C 29.751 177.209 177.209 29.751 360.000 29.751 Z"/>`;
|
const CIRCLE = `<path fill="#ffffff" fill-rule="evenodd" stroke="#000000" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" d="M 360.000 29.751 C 542.791 29.751 690.249 177.209 690.249 360.000 C 690.249 542.791 542.791 690.249 360.000 690.249 C 177.209 690.249 29.751 542.791 29.751 360.000 C 29.751 177.209 177.209 29.751 360.000 29.751 Z"/>`;
|
||||||
|
@ -6,17 +7,6 @@ function arrow(color: string) {
|
||||||
return `<path fill="${color}" fill-rule="evenodd" stroke="none" d="M 360.000 679.153 C 360.001 679.156 372.114 713.074 372.116 713.077 C 372.114 713.076 360.001 701.805 360.000 701.804 C 359.999 701.805 347.886 713.076 347.884 713.077 C 347.886 713.074 359.999 679.156 360.000 679.153 Z"/>`;
|
return `<path fill="${color}" fill-rule="evenodd" stroke="none" d="M 360.000 679.153 C 360.001 679.156 372.114 713.074 372.116 713.077 C 372.114 713.076 360.001 701.805 360.000 701.804 C 359.999 701.805 347.886 713.076 347.884 713.077 C 347.886 713.074 359.999 679.156 360.000 679.153 Z"/>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function withMockConsoleLog(fn: (mock: jest.Mock) => void) {
|
|
||||||
const originalLog = console.log;
|
|
||||||
const mockLog = jest.fn();
|
|
||||||
console.log = mockLog;
|
|
||||||
try {
|
|
||||||
fn(mockLog);
|
|
||||||
} finally {
|
|
||||||
console.log = originalLog;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("convertSvgMarkupToSymbolData()", () => {
|
describe("convertSvgMarkupToSymbolData()", () => {
|
||||||
it("works with SVGs that just have a path and no specs", () => {
|
it("works with SVGs that just have a path and no specs", () => {
|
||||||
expect(
|
expect(
|
||||||
|
|
|
@ -7,6 +7,10 @@
|
||||||
#
|
#
|
||||||
# Also note that any lines that begin with a "#" are comments,
|
# Also note that any lines that begin with a "#" are comments,
|
||||||
# so they will be ignored by the code.
|
# so they will be ignored by the code.
|
||||||
|
#
|
||||||
|
# Note also that all properties that are booleans
|
||||||
|
# (i.e., true or false) will, unless otherwise noted,
|
||||||
|
# always default to false if left undefined.
|
||||||
|
|
||||||
# If true, this indicates that the symbol should always have
|
# If true, this indicates that the symbol should always have
|
||||||
# a symbol nested within its nesting area(s).
|
# a symbol nested within its nesting area(s).
|
||||||
|
@ -23,3 +27,9 @@ never_be_nested = false
|
||||||
# If true, this indicates that any symbols nested on this
|
# If true, this indicates that any symbols nested on this
|
||||||
# symbol’s nesting area should have their colors inverted.
|
# symbol’s nesting area should have their colors inverted.
|
||||||
invert_nested = false
|
invert_nested = false
|
||||||
|
|
||||||
|
# If set to an array of strings, this indicates the kinds of
|
||||||
|
# attachment points that this symbol can attach to. If not
|
||||||
|
# defined, this symbol will be able to attach to any attachment
|
||||||
|
# point.
|
||||||
|
attach_to = ["tail", "leg", "arm", "horn", "crown"]
|
||||||
|
|
Ładowanie…
Reference in New Issue