pi-roundtable-drawing 0.7.4 → 0.7.5
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 +6 -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/plugin.ts +27 -2
- package/src/relationship-map.ts +7 -5
- package/src/tools/draw-cards-tool.ts +4 -1
- package/src/tools/relationship-map-tool.ts +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,12 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [0.7.5] - 2026-10-03
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- `permissive` accepts what a looser host sends; `mapLimits` values must be whole numbers of at least 1.
|
|
13
|
+
|
|
8
14
|
## [0.7.4] - 2026-10-03
|
|
9
15
|
|
|
10
16
|
- 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, and a relationship map may repeat or leave empty a node id and may draw an edge from a node to itself. 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.5",
|
|
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.5",
|
|
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/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
106
|
sigilTool(env),
|
|
90
107
|
sacredGeometryTool(env),
|
|
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}: ` : "";
|
|
@@ -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;
|
|
@@ -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
|
},
|