pi-roundtable-drawing 0.7.4 → 0.7.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/README.md +2 -1
- package/package.json +2 -2
- package/src/cards/draw.ts +12 -2
- package/src/cards/spread.ts +4 -2
- package/src/magic.ts +11 -3
- package/src/plugin.ts +29 -4
- package/src/relationship-map.ts +7 -5
- package/src/tools/draw-cards-tool.ts +4 -1
- package/src/tools/magic-tools.ts +4 -2
- package/src/tools/relationship-map-tool.ts +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,16 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [0.7.6] - 2026-10-03
|
|
9
|
+
|
|
10
|
+
- `permissive` also accepts an empty node id, a sigil intention with no letters or only blanks, and an unknown sacred-geometry color.
|
|
11
|
+
|
|
12
|
+
## [0.7.5] - 2026-10-03
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- `permissive` accepts what a looser host sends; `mapLimits` values must be whole numbers of at least 1.
|
|
17
|
+
|
|
8
18
|
## [0.7.4] - 2026-10-03
|
|
9
19
|
|
|
10
20
|
- Add `cardPresentation.result` for the card draw result text and `mapLimits` for longer relationship-map text.
|
package/README.md
CHANGED
|
@@ -83,7 +83,8 @@ export default {
|
|
|
83
83
|
| `random` | `() => number` | `Math.random` | The random source of the map layouts and the card draws, returning a number from 0 up to, not including, 1. A seeded source makes a run reproducible: a new `seededRandom(seed)` given the same calls draws the same pictures. `seededRandom(seed)` is exported for that |
|
|
84
84
|
| `minTier` | `"owner" \| "admin" \| "member"` | `member` | The lowest tier that may use the tools. The operator's `toolTiers` setting still wins |
|
|
85
85
|
| `cardPresentation` | `CardPresentation` | package captions | Trusted operator-local headings, reversal suffixes and result text |
|
|
86
|
-
| `mapLimits` | `{ title?, text? }` | 80 and 60 | Longest relationship-map title, and node id, node label or edge label, in characters, for a host whose callers already draw longer text; the renderer's size limit still applies |
|
|
86
|
+
| `mapLimits` | `{ title?, text? }` | 80 and 60 | Longest relationship-map title, and node id, node label or edge label, in characters, for a host whose callers already draw longer text; each is a whole number of at least 1 (anything else stops the start), and the renderer's size limit still applies |
|
|
87
|
+
| `permissive` | `boolean` | `false` | Accept what a looser host's callers already send: a card exclusion that is not a card of the deck is ignored, spread positions may share a cell, a relationship map may repeat or leave empty a node id and may draw an edge from a node to itself, a sigil may have an intention with no letters or only blanks, and a sacred-geometry color that canvas cannot parse is ignored. By default each is refused with a message the model can correct |
|
|
87
88
|
|
|
88
89
|
The plugin is named `drawing`.
|
|
89
90
|
`cardPresentation.heading(draw, turn)` returns `{ title, subtitle }`; `draw` holds the selected `deck`, `count`, and optional `question`, and `turn` carries the host-bound speaker/channel.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-roundtable-drawing",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.6",
|
|
4
4
|
"description": "Relationship maps, magic circles, sigils, sacred geometry, and card spreads drawn locally for pi-roundtable",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"devDependencies": {
|
|
49
49
|
"@biomejs/biome": "2.5.15",
|
|
50
50
|
"@types/bun": "1.4.2",
|
|
51
|
-
"pi-roundtable": "0.7.
|
|
51
|
+
"pi-roundtable": "0.7.6",
|
|
52
52
|
"typescript": "7.0.2"
|
|
53
53
|
}
|
|
54
54
|
}
|
package/src/cards/draw.ts
CHANGED
|
@@ -15,11 +15,21 @@ export interface DrawParams {
|
|
|
15
15
|
/** Whether cards may come up reversed; the deck's own default when absent. */
|
|
16
16
|
allowReversed?: boolean | undefined;
|
|
17
17
|
random: Random;
|
|
18
|
+
/** Ignore exclusions that are not cards of the deck, instead of refusing them. */
|
|
19
|
+
ignoreUnknownExclusions?: boolean | undefined;
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
/** Draws from the whole deck minus the exclusions; there is no persistent deck between draws. */
|
|
21
23
|
export function drawCards(params: DrawParams): DrawnCard[] {
|
|
22
|
-
const {
|
|
24
|
+
const {
|
|
25
|
+
deck,
|
|
26
|
+
count,
|
|
27
|
+
group,
|
|
28
|
+
exclude,
|
|
29
|
+
allowReversed,
|
|
30
|
+
random,
|
|
31
|
+
ignoreUnknownExclusions,
|
|
32
|
+
} = params;
|
|
23
33
|
if (!Number.isInteger(count) || count < 1)
|
|
24
34
|
throw new DrawingError(
|
|
25
35
|
`count must be a whole number of at least 1; got ${count}.`,
|
|
@@ -35,7 +45,7 @@ export function drawCards(params: DrawParams): DrawnCard[] {
|
|
|
35
45
|
const known = new Set(deck.cards.map((card) => card.id));
|
|
36
46
|
const excluded = new Set(exclude ?? []);
|
|
37
47
|
for (const id of excluded)
|
|
38
|
-
if (!known.has(id))
|
|
48
|
+
if (!known.has(id) && !ignoreUnknownExclusions)
|
|
39
49
|
throw new DrawingError(
|
|
40
50
|
`exclude names ${JSON.stringify(id)}, which is not a card of deck ${deck.id}. Use the card ids the deck lists.`,
|
|
41
51
|
);
|
package/src/cards/spread.ts
CHANGED
|
@@ -43,6 +43,7 @@ export function autoLayout(count: number): SpreadPosition[] {
|
|
|
43
43
|
export function checkSpread(
|
|
44
44
|
positions: readonly SpreadPosition[],
|
|
45
45
|
count: number,
|
|
46
|
+
allowSharedCells = false,
|
|
46
47
|
): void {
|
|
47
48
|
if (positions.length !== count)
|
|
48
49
|
throw new DrawingError(
|
|
@@ -55,7 +56,7 @@ export function checkSpread(
|
|
|
55
56
|
`Spread positions need a row and a column that are whole numbers from 0; got row ${row}, column ${col}.`,
|
|
56
57
|
);
|
|
57
58
|
const cell = `${row}:${col}`;
|
|
58
|
-
if (cells.has(cell))
|
|
59
|
+
if (cells.has(cell) && !allowSharedCells)
|
|
59
60
|
throw new DrawingError(
|
|
60
61
|
`Two cards share row ${row}, column ${col}. Give each card its own position.`,
|
|
61
62
|
);
|
|
@@ -113,8 +114,9 @@ export async function renderSpread(
|
|
|
113
114
|
positions: readonly SpreadPosition[],
|
|
114
115
|
heading: { title: string; subtitle: string },
|
|
115
116
|
reversedSuffix = " (reversed)",
|
|
117
|
+
allowSharedCells = false,
|
|
116
118
|
): Promise<Uint8Array> {
|
|
117
|
-
checkSpread(positions, cards.length);
|
|
119
|
+
checkSpread(positions, cards.length, allowSharedCells);
|
|
118
120
|
ensureFont();
|
|
119
121
|
const faces = await Promise.all(
|
|
120
122
|
cards.map(async (card) => {
|
package/src/magic.ts
CHANGED
|
@@ -257,6 +257,8 @@ export interface SigilOptions {
|
|
|
257
257
|
complexity: "simple" | "elaborate";
|
|
258
258
|
style: "traditional" | "modern";
|
|
259
259
|
background: Background;
|
|
260
|
+
/** Draw what a looser caller sends: an intention with no letters, a blank one, or an unknown color. */
|
|
261
|
+
permissive?: boolean;
|
|
260
262
|
}
|
|
261
263
|
|
|
262
264
|
const SIGIL_SIZE = 512;
|
|
@@ -283,9 +285,13 @@ function planetaryRadius(char: string): number {
|
|
|
283
285
|
}
|
|
284
286
|
|
|
285
287
|
export function renderSigil(options: SigilOptions): Uint8Array {
|
|
286
|
-
if (options.intention.trim() === "")
|
|
288
|
+
if (options.intention.trim() === "" && !options.permissive)
|
|
287
289
|
throw new DrawingError("The intention is empty. Give a short phrase.");
|
|
288
|
-
if (
|
|
290
|
+
if (
|
|
291
|
+
options.method !== "geometric" &&
|
|
292
|
+
letters(options.intention).length === 0 &&
|
|
293
|
+
!options.permissive
|
|
294
|
+
)
|
|
289
295
|
throw new DrawingError(
|
|
290
296
|
`The ${options.method} method draws from letters, and the intention has none. Write it in words, or use the geometric method.`,
|
|
291
297
|
);
|
|
@@ -403,6 +409,8 @@ export interface GeometryOptions {
|
|
|
403
409
|
/** CSS colors cycled per layer; the palette's line color when empty. */
|
|
404
410
|
colors: readonly string[];
|
|
405
411
|
background: Background;
|
|
412
|
+
/** Ignore a color canvas cannot parse, as it would, instead of refusing it. */
|
|
413
|
+
permissive?: boolean;
|
|
406
414
|
}
|
|
407
415
|
|
|
408
416
|
const GEOMETRY_SIZE = 1024;
|
|
@@ -433,7 +441,7 @@ export function renderSacredGeometry(options: GeometryOptions): Uint8Array {
|
|
|
433
441
|
`At most ${MAX_LAYERS} colors are used, one for each layer. Give fewer.`,
|
|
434
442
|
);
|
|
435
443
|
for (const color of options.colors)
|
|
436
|
-
if (!isCssColor(color))
|
|
444
|
+
if (!options.permissive && !isCssColor(color))
|
|
437
445
|
throw new DrawingError(
|
|
438
446
|
`${JSON.stringify(color)} is not a CSS color. Use a name such as gold or a code such as #c0a060.`,
|
|
439
447
|
);
|
package/src/plugin.ts
CHANGED
|
@@ -56,6 +56,13 @@ export interface DrawingOptions {
|
|
|
56
56
|
cardPresentation?: CardPresentation;
|
|
57
57
|
/** Longer map text than the defaults, for a host whose callers already draw longer labels. */
|
|
58
58
|
mapLimits?: RelationshipMapLimits;
|
|
59
|
+
/**
|
|
60
|
+
* Accept what a looser host's callers already send: a card exclusion that is not a card of the
|
|
61
|
+
* deck is ignored, spread positions may share a cell, and a relationship map may repeat or leave
|
|
62
|
+
* empty a node id and may draw an edge from a node to itself. Off by default, where each is
|
|
63
|
+
* refused with a message the model can correct.
|
|
64
|
+
*/
|
|
65
|
+
permissive?: boolean;
|
|
59
66
|
}
|
|
60
67
|
|
|
61
68
|
function readDecks(deckDir: string): Deck[] {
|
|
@@ -77,6 +84,11 @@ export function drawing(options: DrawingOptions = {}) {
|
|
|
77
84
|
return definePlugin({
|
|
78
85
|
name: "drawing",
|
|
79
86
|
setup: () => {
|
|
87
|
+
for (const [name, limit] of Object.entries(options.mapLimits ?? {}))
|
|
88
|
+
if (!Number.isSafeInteger(limit) || limit < 1)
|
|
89
|
+
throw new PluginError(
|
|
90
|
+
`plugin drawing: mapLimits.${name} must be a whole number of at least 1; got ${limit}.`,
|
|
91
|
+
);
|
|
80
92
|
// Read at setup, so a wrong deck directory stops the start with a message that names this plugin.
|
|
81
93
|
const decks = options.deckDir ? readDecks(options.deckDir) : [];
|
|
82
94
|
const env: ImageToolEnv = {
|
|
@@ -84,12 +96,25 @@ export function drawing(options: DrawingOptions = {}) {
|
|
|
84
96
|
};
|
|
85
97
|
return {
|
|
86
98
|
tools: [
|
|
87
|
-
relationshipMapTool(
|
|
99
|
+
relationshipMapTool(
|
|
100
|
+
env,
|
|
101
|
+
random,
|
|
102
|
+
options.mapLimits,
|
|
103
|
+
options.permissive,
|
|
104
|
+
),
|
|
88
105
|
magicCircleTool(env),
|
|
89
|
-
sigilTool(env),
|
|
90
|
-
sacredGeometryTool(env),
|
|
106
|
+
sigilTool(env, options.permissive),
|
|
107
|
+
sacredGeometryTool(env, options.permissive),
|
|
91
108
|
...(decks.length > 0
|
|
92
|
-
? [
|
|
109
|
+
? [
|
|
110
|
+
drawCardsTool(
|
|
111
|
+
decks,
|
|
112
|
+
env,
|
|
113
|
+
random,
|
|
114
|
+
options.cardPresentation,
|
|
115
|
+
options.permissive,
|
|
116
|
+
),
|
|
117
|
+
]
|
|
93
118
|
: []),
|
|
94
119
|
],
|
|
95
120
|
};
|
package/src/relationship-map.ts
CHANGED
|
@@ -126,7 +126,7 @@ interface SimLink extends SimulationLinkDatum<SimNode> {
|
|
|
126
126
|
|
|
127
127
|
type RoughCanvas = ReturnType<typeof rough.canvas>;
|
|
128
128
|
|
|
129
|
-
function validate(input: MapInput): void {
|
|
129
|
+
function validate(input: MapInput, permissive: boolean): void {
|
|
130
130
|
if (input.nodes.length === 0)
|
|
131
131
|
throw new DrawingError("nodes cannot be empty. Give at least one node.");
|
|
132
132
|
if (input.nodes.length > MAX_NODES)
|
|
@@ -139,9 +139,9 @@ function validate(input: MapInput): void {
|
|
|
139
139
|
);
|
|
140
140
|
const ids = new Set<string>();
|
|
141
141
|
for (const node of input.nodes) {
|
|
142
|
-
if (node.id.trim() === "")
|
|
142
|
+
if (node.id.trim() === "" && !permissive)
|
|
143
143
|
throw new DrawingError("A node has an empty id. Give every node a name.");
|
|
144
|
-
if (ids.has(node.id))
|
|
144
|
+
if (ids.has(node.id) && !permissive)
|
|
145
145
|
throw new DrawingError(
|
|
146
146
|
`Two nodes share the id ${JSON.stringify(node.id)}. Node ids must be unique.`,
|
|
147
147
|
);
|
|
@@ -153,7 +153,7 @@ function validate(input: MapInput): void {
|
|
|
153
153
|
throw new DrawingError(
|
|
154
154
|
`An edge references the unknown node ${JSON.stringify(end)}. Add it to nodes or fix the edge.`,
|
|
155
155
|
);
|
|
156
|
-
if (edge.from === edge.to)
|
|
156
|
+
if (edge.from === edge.to && !permissive)
|
|
157
157
|
throw new DrawingError(
|
|
158
158
|
`An edge from ${JSON.stringify(edge.from)} to itself cannot be drawn. Connect two different nodes.`,
|
|
159
159
|
);
|
|
@@ -390,8 +390,10 @@ function drawNode(
|
|
|
390
390
|
export function renderRelationshipMap(
|
|
391
391
|
input: MapInput,
|
|
392
392
|
random: Random = Math.random,
|
|
393
|
+
/** Draw maps with repeated or empty node ids and edges from a node to itself instead of refusing them. */
|
|
394
|
+
permissive = false,
|
|
393
395
|
): Uint8Array {
|
|
394
|
-
validate(input);
|
|
396
|
+
validate(input, permissive);
|
|
395
397
|
ensureFont();
|
|
396
398
|
// The shapes grow to hold their text, so the text is measured before the layout.
|
|
397
399
|
const measure = createCanvas(1, 1).getContext("2d");
|
|
@@ -34,6 +34,7 @@ export function drawCardsTool(
|
|
|
34
34
|
env: ImageToolEnv,
|
|
35
35
|
random: Random,
|
|
36
36
|
presentation: CardPresentation = {},
|
|
37
|
+
permissive = false,
|
|
37
38
|
) {
|
|
38
39
|
const [first, ...rest] = decks;
|
|
39
40
|
if (!first) throw new DrawingError("draw_cards needs at least one deck.");
|
|
@@ -92,7 +93,7 @@ export function drawCardsTool(
|
|
|
92
93
|
const positions: SpreadPosition[] =
|
|
93
94
|
args.spread ?? autoLayout(args.count);
|
|
94
95
|
// Checked before the draw, so a refused spread leaves the random source where it was.
|
|
95
|
-
checkSpread(positions, args.count);
|
|
96
|
+
checkSpread(positions, args.count, permissive);
|
|
96
97
|
const cards = drawCards({
|
|
97
98
|
deck,
|
|
98
99
|
count: args.count,
|
|
@@ -100,6 +101,7 @@ export function drawCardsTool(
|
|
|
100
101
|
exclude: args.exclude,
|
|
101
102
|
allowReversed: args.allow_reversed,
|
|
102
103
|
random,
|
|
104
|
+
ignoreUnknownExclusions: permissive,
|
|
103
105
|
});
|
|
104
106
|
const heading = presentation.heading?.(
|
|
105
107
|
{ deck, count: cards.length, question: args.question },
|
|
@@ -116,6 +118,7 @@ export function drawCardsTool(
|
|
|
116
118
|
positions,
|
|
117
119
|
heading,
|
|
118
120
|
presentation.reversedSuffix,
|
|
121
|
+
permissive,
|
|
119
122
|
);
|
|
120
123
|
const lines = cards.map((card, i) => {
|
|
121
124
|
const label = positions[i]?.label ? `${positions[i]?.label}: ` : "";
|
package/src/tools/magic-tools.ts
CHANGED
|
@@ -65,7 +65,7 @@ export function magicCircleTool(env: ImageToolEnv) {
|
|
|
65
65
|
);
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
export function sigilTool(env: ImageToolEnv) {
|
|
68
|
+
export function sigilTool(env: ImageToolEnv, permissive = false) {
|
|
69
69
|
return imageTool(
|
|
70
70
|
{
|
|
71
71
|
name: "sigil_generate",
|
|
@@ -86,6 +86,7 @@ export function sigilTool(env: ImageToolEnv) {
|
|
|
86
86
|
complexity: args.complexity ?? "elaborate",
|
|
87
87
|
style: args.style ?? "traditional",
|
|
88
88
|
background: args.background ?? "dark",
|
|
89
|
+
permissive,
|
|
89
90
|
}),
|
|
90
91
|
text: "The sigil is attached to your reply as {file}.",
|
|
91
92
|
}),
|
|
@@ -94,7 +95,7 @@ export function sigilTool(env: ImageToolEnv) {
|
|
|
94
95
|
);
|
|
95
96
|
}
|
|
96
97
|
|
|
97
|
-
export function sacredGeometryTool(env: ImageToolEnv) {
|
|
98
|
+
export function sacredGeometryTool(env: ImageToolEnv, permissive = false) {
|
|
98
99
|
return imageTool(
|
|
99
100
|
{
|
|
100
101
|
name: "sacred_geometry_generate",
|
|
@@ -131,6 +132,7 @@ export function sacredGeometryTool(env: ImageToolEnv) {
|
|
|
131
132
|
rotation: args.rotation ?? 0,
|
|
132
133
|
colors: args.colors ?? [],
|
|
133
134
|
background: args.background ?? "dark",
|
|
135
|
+
permissive,
|
|
134
136
|
}),
|
|
135
137
|
text: "The sacred geometry is attached to your reply as {file}.",
|
|
136
138
|
}),
|
|
@@ -21,6 +21,7 @@ export function relationshipMapTool(
|
|
|
21
21
|
env: ImageToolEnv,
|
|
22
22
|
random: Random,
|
|
23
23
|
limits: RelationshipMapLimits = {},
|
|
24
|
+
permissive = false,
|
|
24
25
|
) {
|
|
25
26
|
const maxTitle = limits.title ?? MAX_TITLE;
|
|
26
27
|
const maxText = limits.text ?? MAX_TEXT;
|
|
@@ -34,7 +35,7 @@ export function relationshipMapTool(
|
|
|
34
35
|
nodes: Type.Array(
|
|
35
36
|
strictObject({
|
|
36
37
|
id: Type.String({
|
|
37
|
-
minLength: 1,
|
|
38
|
+
minLength: permissive ? 0 : 1,
|
|
38
39
|
maxLength: maxText,
|
|
39
40
|
description: "Character or faction name.",
|
|
40
41
|
}),
|
|
@@ -63,7 +64,7 @@ export function relationshipMapTool(
|
|
|
63
64
|
}),
|
|
64
65
|
draw: (args) => ({
|
|
65
66
|
stem: "relationship-map",
|
|
66
|
-
image: renderRelationshipMap(args, random),
|
|
67
|
+
image: renderRelationshipMap(args, random, permissive),
|
|
67
68
|
text: "The relationship map is attached to your reply as {file}.",
|
|
68
69
|
}),
|
|
69
70
|
},
|