@onlyworlds/sdk 4.10.0 → 4.11.0
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/AGENTS.md +5 -0
- package/CHANGELOG.md +25 -0
- package/README.md +17 -0
- package/dist/index.d.ts +200 -1
- package/dist/index.js +437 -0
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -40,6 +40,11 @@ against live data). The v1 surface (`OnlyWorldsClient`, the `ElementType` enum)
|
|
|
40
40
|
- Images: `uploadImage(bytes)` returns `{ url, ... }`; set `url` as `image_url`. The bytes go
|
|
41
41
|
to the edge with a single-use ticket from keel, never with the API key. `removeImage(urlOrKey)`
|
|
42
42
|
takes one back (its uploader or the world's owner); it leaves `image_url` on elements as is.
|
|
43
|
+
- Lookup without a model (since 4.11.0): `buildWorldIndex(elements)` then
|
|
44
|
+
`lookupWorld(index, question)` ranks the elements a question is about (names, then kind, one
|
|
45
|
+
link step, then text); `renderElement(el, index)` is a compact card with links by name.
|
|
46
|
+
- Photos (browser only, since 4.11.0): `preparePhoto(blob)` shrinks to 2048 px and converts to
|
|
47
|
+
webp (JPEG where webp can't be encoded) before `uploadImage`.
|
|
43
48
|
- World files (no account needed; since 4.10.0): `readWorldFile(bytes)` opens a `.zip` holding one
|
|
44
49
|
world folder and returns `{ worldId, name, world, files: [{ path, data }] }`;
|
|
45
50
|
`writeWorldFile(files, topDir)` makes one. Refusals throw `OwWorldFileError` with a `reason`.
|
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,31 @@ earlier history lives in git log only.
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [4.11.0] — 2026-10-12
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **World lookup.** `buildWorldIndex(elements)`, `lookupWorld(index, question, { limit })` and
|
|
12
|
+
`renderElement(element, index)` find the elements a plain-language question is about, in a
|
|
13
|
+
world held in memory: no model, no network, a few milliseconds. Ranking: names and short
|
|
14
|
+
forms first, then name words, supertype and subtype, one link step in both directions, then
|
|
15
|
+
other text and numbers (always below any name hit). Each hit says why it matched.
|
|
16
|
+
`renderElement` gives a compact text card with links by name, which is what a model reads
|
|
17
|
+
when one phrases the answer. Measured on two real worlds (Assembly's Lantern Ask trial):
|
|
18
|
+
the answer was in the first three results for 60 of 60 questions that name their subject
|
|
19
|
+
and 17 of 24 held-out questions that only describe it; this code returns the same top 8
|
|
20
|
+
as the trial's on all 100 questions. Names in any script are matched (Chinese and Japanese
|
|
21
|
+
only where spaces or punctuation separate the words: there is no word splitting). The question words
|
|
22
|
+
that steer the link step ("where", "rules", "allies") are English.
|
|
23
|
+
- **Photo preparation.** `preparePhoto(blob, { maxSide, quality, maxBytes })` shrinks and
|
|
24
|
+
converts a picture in the browser before `uploadImage`: longest side 2048 by default, webp at
|
|
25
|
+
quality 0.82, from the upload page's converter. A browser that cannot encode webp returns a
|
|
26
|
+
PNG when asked; the result's real type is checked, and a photo then falls back to JPEG while a
|
|
27
|
+
PNG stays PNG. When re-encoding would only make a file bigger, the original is kept.
|
|
28
|
+
`planPhoto` is the pure part (sizes and type order). Walked in Chromium, WebKit and Firefox
|
|
29
|
+
(a 4032×3024 photo became a 2048-wide webp of about 0.5 MB) and with webp encoding forced off.
|
|
30
|
+
Needs a browser; in Node it throws a `TypeError` that says so. `PHOTO_TYPES`,
|
|
31
|
+
`PHOTO_MAX_BYTES` are keel's accepted types and its 15 MB limit.
|
|
32
|
+
|
|
8
33
|
## [4.10.0] — 2026-10-11
|
|
9
34
|
|
|
10
35
|
### Added
|
package/README.md
CHANGED
|
@@ -99,6 +99,8 @@ const image = await writer.uploadImage(file); // a Blob, File, ArrayBuffer or Ui
|
|
|
99
99
|
await writer.patch('character', id, { image_url: image.url });
|
|
100
100
|
```
|
|
101
101
|
|
|
102
|
+
In a browser, shrink a phone photo first: `const photo = await preparePhoto(file)` gives a webp (or JPEG where the browser can't encode webp) of at most 2048 px on its longest side; upload `photo.blob`.
|
|
103
|
+
|
|
102
104
|
The SDK asks the API for a single-use ticket, then sends the bytes straight to the upload host, which never sees your key. webp, png, jpeg or avif (no SVG or gif), up to 15 MB. Each upload counts toward the world's daily limit and the account's storage. To upload yourself (for a progress bar), call `createMediaTicket()` and POST the bytes to its `upload_url` with `Authorization: Bearer <ticket>`.
|
|
103
105
|
|
|
104
106
|
```typescript
|
|
@@ -135,6 +137,21 @@ try {
|
|
|
135
137
|
|
|
136
138
|
The check is per element and the server never merges. Compare the copy you edited from with `err.current`: when the fields you changed are not the ones that moved, send again with `ifMatch: err.current.change_seq`; when the same field moved on both sides, ask the user which to keep. Without `ifMatch` a write is unconditional, as before.
|
|
137
139
|
|
|
140
|
+
## Finding things in a world
|
|
141
|
+
|
|
142
|
+
When you hold a world's elements in memory (from a sync, or a world file), `lookupWorld` finds the ones a plain question is about, with no model and no network:
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
import { buildWorldIndex, lookupWorld, renderElement } from '@onlyworlds/sdk';
|
|
146
|
+
|
|
147
|
+
const index = buildWorldIndex(elements); // once per world; each element needs id and type
|
|
148
|
+
const hits = lookupWorld(index, 'who runs the mill in Ostvik?');
|
|
149
|
+
hits[0].element; hits[0].reasons; // best first, with why each matched
|
|
150
|
+
renderElement(hits[0].element, index); // a short text card, links by name
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Names come first, then supertype and subtype, then one link step in either direction, then the elements' other text. To have a model phrase an answer, give it the cards of the top hits and ask it to answer only from them.
|
|
154
|
+
|
|
138
155
|
## World files
|
|
139
156
|
|
|
140
157
|
A world file is one `.zip` holding a whole world folder (`world.json` and `elements/<type>/*.json`, as Atlas keeps a world on disk), the way Atlas downloads and opens one. It needs no account and works in the browser and in Node.
|
package/dist/index.d.ts
CHANGED
|
@@ -2526,6 +2526,205 @@ declare function readWorldFile(bytes: Uint8Array, opts?: OwReadWorldFileOptions)
|
|
|
2526
2526
|
*/
|
|
2527
2527
|
declare function writeWorldFile(files: OwWorldFileEntry[], topDir: string): Uint8Array;
|
|
2528
2528
|
|
|
2529
|
+
/**
|
|
2530
|
+
* World lookup: find the elements a plain-language question is about, in a
|
|
2531
|
+
* world held in memory, with no model and no network. Ported from Assembly's
|
|
2532
|
+
* Lantern Ask trial (`dossiers/lantern-ask/lookup.mjs`, round 2), with the
|
|
2533
|
+
* same ranking, so the trial's measurements on two real worlds describe this
|
|
2534
|
+
* code. Pure: runs in a browser and in Node alike.
|
|
2535
|
+
*
|
|
2536
|
+
* const index = buildWorldIndex(elements); // API elements or a world folder's files
|
|
2537
|
+
* const hits = lookupWorld(index, question); // ranked, best first, each with its reasons
|
|
2538
|
+
* const text = renderElement(hits[0].element, index); // a compact text card, links by name
|
|
2539
|
+
*
|
|
2540
|
+
* Each element needs `id` and `type` (a world folder's files may lack `type`:
|
|
2541
|
+
* set it from the folder name), and usually `name`.
|
|
2542
|
+
*
|
|
2543
|
+
* LINKS: a field is a link when `FIELD_SCHEMA` says so for the element's type,
|
|
2544
|
+
* or when its value is an id-shaped string or an array of them. The second
|
|
2545
|
+
* rule is the trial's own, and it is kept because real data links outside the
|
|
2546
|
+
* schema's names: a world folder's pins and markers carry `map_id` and
|
|
2547
|
+
* `zone_id`, and extension fields (`x_*`) hold references. A link to an id
|
|
2548
|
+
* outside the set is kept as unresolved, so a card can say a link exists.
|
|
2549
|
+
*
|
|
2550
|
+
* RANKING (higher score first; ties: shorter name, then id):
|
|
2551
|
+
* 0. Question and names are normalised: lower case, diacritics stripped,
|
|
2552
|
+
* possessive 's dropped, "7,875" read as 7875; letters and digits of any
|
|
2553
|
+
* script are kept, anything else is a space. A content word has 3+
|
|
2554
|
+
* characters and is no English stopword; a word holding a non-Latin letter
|
|
2555
|
+
* always counts (a CJK name is often one or two characters). Words are
|
|
2556
|
+
* split on spaces and punctuation only, so a CJK name is found when the
|
|
2557
|
+
* question sets it apart that way.
|
|
2558
|
+
* 1. Name phrase: the whole name appears in the question -> 100 + 10 per word.
|
|
2559
|
+
* 2. Alias phrase: a short form appears -> 80 + 10 per word. Short forms: the
|
|
2560
|
+
* name without a leading "the"; the part before ":" or "("; for characters
|
|
2561
|
+
* also the part before " of " or "," and the first word.
|
|
2562
|
+
* 3. Name words: question words (3+ letters, not stopwords) equal to a name
|
|
2563
|
+
* word, or sharing a 5+ letter prefix with it -> 20 + 40 x the
|
|
2564
|
+
* rarity-weighted share of the name's words matched. Kept only when better
|
|
2565
|
+
* than 1-2.
|
|
2566
|
+
* 4. Kind: +15 when a question word matches the supertype or subtype (a word
|
|
2567
|
+
* the name already used does not count again).
|
|
2568
|
+
* 5. One link step from the 12 best direct hits, both directions: a linked-to
|
|
2569
|
+
* element scores 0.6 x the hit, a linking element 0.45 x; x1.5 when the
|
|
2570
|
+
* field fits a question word ("where" -> location fields, "rules" ->
|
|
2571
|
+
* primary_power, ...). Every reason is kept; the best score counts.
|
|
2572
|
+
* 6. Text: question words (and numbers) found in the element's other text and
|
|
2573
|
+
* number fields add 3 x their summed rarity. An element found only this
|
|
2574
|
+
* way scores under 19, so below every name hit.
|
|
2575
|
+
* 7. Map furniture (pin, marker, zone, map) x0.85, so a pin never outranks
|
|
2576
|
+
* its location; it carries no text.
|
|
2577
|
+
*/
|
|
2578
|
+
/** What the lookup reads from an element: `id`, `type`, `name`, and any other fields. `OwElement` fits. */
|
|
2579
|
+
interface OwLookupElement {
|
|
2580
|
+
id: string;
|
|
2581
|
+
type: string;
|
|
2582
|
+
name?: string;
|
|
2583
|
+
[field: string]: unknown;
|
|
2584
|
+
}
|
|
2585
|
+
/** One link between two elements of the index, by field. */
|
|
2586
|
+
interface OwLookupLink {
|
|
2587
|
+
field: string;
|
|
2588
|
+
id: string;
|
|
2589
|
+
}
|
|
2590
|
+
/**
|
|
2591
|
+
* The index `lookupWorld` and `renderElement` read, built once per world by
|
|
2592
|
+
* `buildWorldIndex`. `byId` is the elements by id; the other tables are the
|
|
2593
|
+
* lookup's working state: read them if useful, but their shape may change.
|
|
2594
|
+
*/
|
|
2595
|
+
interface OwWorldIndex<T extends OwLookupElement = OwLookupElement> {
|
|
2596
|
+
byId: ReadonlyMap<string, T>;
|
|
2597
|
+
/** Links each element holds to elements in the set. */
|
|
2598
|
+
out: ReadonlyMap<string, readonly OwLookupLink[]>;
|
|
2599
|
+
/** Links pointing at each element, with the holder's id. */
|
|
2600
|
+
inn: ReadonlyMap<string, readonly OwLookupLink[]>;
|
|
2601
|
+
/** Links each element holds to ids not in the set. */
|
|
2602
|
+
unresolved: ReadonlyMap<string, readonly OwLookupLink[]>;
|
|
2603
|
+
phrase: ReadonlyMap<string, readonly {
|
|
2604
|
+
id: string;
|
|
2605
|
+
kind: 'name' | 'alias';
|
|
2606
|
+
}[]>;
|
|
2607
|
+
tokenIds: ReadonlyMap<string, ReadonlySet<string>>;
|
|
2608
|
+
nameWords: ReadonlyMap<string, readonly string[]>;
|
|
2609
|
+
idf: ReadonlyMap<string, number>;
|
|
2610
|
+
textIds: ReadonlyMap<string, ReadonlySet<string>>;
|
|
2611
|
+
textIdf: ReadonlyMap<string, number>;
|
|
2612
|
+
}
|
|
2613
|
+
/** Why an element matched: by its name or a short form of it, by name words, by other text, or by one link. */
|
|
2614
|
+
type OwLookupReason = {
|
|
2615
|
+
kind: 'name' | 'alias' | 'word' | 'text';
|
|
2616
|
+
text: string;
|
|
2617
|
+
} | {
|
|
2618
|
+
kind: 'link';
|
|
2619
|
+
dir: 'out' | 'in';
|
|
2620
|
+
field: string;
|
|
2621
|
+
from: string | undefined;
|
|
2622
|
+
fromId: string;
|
|
2623
|
+
};
|
|
2624
|
+
/** One ranked result. `dir: 'out'` on a link reason: the hit `from` links to this element; `'in'`: this element links to it. */
|
|
2625
|
+
interface OwLookupHit<T extends OwLookupElement = OwLookupElement> {
|
|
2626
|
+
element: T;
|
|
2627
|
+
/** Rounded to one decimal; comparable within one lookup only. */
|
|
2628
|
+
score: number;
|
|
2629
|
+
reasons: OwLookupReason[];
|
|
2630
|
+
}
|
|
2631
|
+
interface OwLookupOptions {
|
|
2632
|
+
/** How many hits to return (default 8). */
|
|
2633
|
+
limit?: number;
|
|
2634
|
+
}
|
|
2635
|
+
interface OwRenderOptions {
|
|
2636
|
+
/** Description characters kept (default 500). */
|
|
2637
|
+
descMax?: number;
|
|
2638
|
+
/** Linked names listed per field (default 12). */
|
|
2639
|
+
linkMax?: number;
|
|
2640
|
+
/** Incoming names listed per type and field (default 10). */
|
|
2641
|
+
incomingMax?: number;
|
|
2642
|
+
}
|
|
2643
|
+
/**
|
|
2644
|
+
* Index a world's elements for `lookupWorld` and `renderElement`. Elements
|
|
2645
|
+
* without an `id` are left out; a repeated id keeps the last. Build it once
|
|
2646
|
+
* per world and again after the world changes.
|
|
2647
|
+
*/
|
|
2648
|
+
declare function buildWorldIndex<T extends OwLookupElement>(elements: Iterable<T>): OwWorldIndex<T>;
|
|
2649
|
+
/**
|
|
2650
|
+
* The elements a question is most likely about, best first (`limit`, default
|
|
2651
|
+
* 8). Names rank first, then elements one link away from a named one, then
|
|
2652
|
+
* elements whose other text holds the question's words. An empty question
|
|
2653
|
+
* returns no hits.
|
|
2654
|
+
*/
|
|
2655
|
+
declare function lookupWorld<T extends OwLookupElement>(index: OwWorldIndex<T>, question: string, { limit }?: OwLookupOptions): OwLookupHit<T>[];
|
|
2656
|
+
/**
|
|
2657
|
+
* A compact text card for one element, for a person or a model: name, type,
|
|
2658
|
+
* kind, description, its fields, its links by name (or how many point outside
|
|
2659
|
+
* the data), and what links to it.
|
|
2660
|
+
*/
|
|
2661
|
+
declare function renderElement<T extends OwLookupElement>(element: T, index: OwWorldIndex<T>, { descMax, linkMax, incomingMax }?: OwRenderOptions): string;
|
|
2662
|
+
|
|
2663
|
+
/**
|
|
2664
|
+
* Photo preparation: make a picture from a phone's camera or files ready for
|
|
2665
|
+
* `uploadImage` (webp, png, jpeg or avif, at most 15 MB), in the browser,
|
|
2666
|
+
* before it leaves the device. Lifted from the upload page's converter
|
|
2667
|
+
* (upload.onlyworlds.com, Boss's): canvas, webp at quality 0.82, alpha kept.
|
|
2668
|
+
*
|
|
2669
|
+
* Two traps it handles:
|
|
2670
|
+
* - A browser that cannot encode webp hands back a PNG instead (Safari has
|
|
2671
|
+
* long done this), and a PNG of a photo is huge. The result's real type is
|
|
2672
|
+
* checked; when webp did not come back, a photo falls back to JPEG at the
|
|
2673
|
+
* same quality and a PNG source stays PNG (it may have transparency).
|
|
2674
|
+
* - Re-encoding an already compressed file at the same size can make it
|
|
2675
|
+
* bigger: then the original is kept, if its type is one keel accepts.
|
|
2676
|
+
*
|
|
2677
|
+
* `planPhoto` is the pure part (sizes and types), so it is tested anywhere;
|
|
2678
|
+
* `preparePhoto` needs a browser (`createImageBitmap` and a canvas).
|
|
2679
|
+
*/
|
|
2680
|
+
/** The picture types keel's image upload accepts. */
|
|
2681
|
+
declare const PHOTO_TYPES: readonly ["image/webp", "image/png", "image/jpeg", "image/avif"];
|
|
2682
|
+
/** keel's limit for one picture. */
|
|
2683
|
+
declare const PHOTO_MAX_BYTES: number;
|
|
2684
|
+
interface OwPhotoOptions {
|
|
2685
|
+
/** The longest side in pixels, after shrinking. Default 2048: a notebook page stays readable and a phone upload stays small. */
|
|
2686
|
+
maxSide?: number;
|
|
2687
|
+
/** Encoder quality for webp and jpeg, 0 to 1. Default 0.82 (the media pipeline's q82). */
|
|
2688
|
+
quality?: number;
|
|
2689
|
+
/** Refuse a result over this many bytes. Default 15 MB, keel's limit. */
|
|
2690
|
+
maxBytes?: number;
|
|
2691
|
+
}
|
|
2692
|
+
interface OwPhotoPlan {
|
|
2693
|
+
width: number;
|
|
2694
|
+
height: number;
|
|
2695
|
+
/** 1 when no shrinking is needed. */
|
|
2696
|
+
scale: number;
|
|
2697
|
+
/** The types to try, in order; the first one the browser really produces wins. */
|
|
2698
|
+
targets: string[];
|
|
2699
|
+
}
|
|
2700
|
+
interface OwPreparedPhoto {
|
|
2701
|
+
blob: Blob;
|
|
2702
|
+
type: string;
|
|
2703
|
+
width: number;
|
|
2704
|
+
height: number;
|
|
2705
|
+
/** The input, for showing "4032×3024 · 3.1 MB → 2048×1536 webp · 410 KB". */
|
|
2706
|
+
original: {
|
|
2707
|
+
width: number;
|
|
2708
|
+
height: number;
|
|
2709
|
+
type: string;
|
|
2710
|
+
size: number;
|
|
2711
|
+
};
|
|
2712
|
+
/** True when the original bytes were kept unchanged. */
|
|
2713
|
+
kept: boolean;
|
|
2714
|
+
}
|
|
2715
|
+
/** Sizes and types for one picture: pure, no browser needed. */
|
|
2716
|
+
declare function planPhoto(source: {
|
|
2717
|
+
width: number;
|
|
2718
|
+
height: number;
|
|
2719
|
+
type: string;
|
|
2720
|
+
}, opts?: OwPhotoOptions): OwPhotoPlan;
|
|
2721
|
+
/**
|
|
2722
|
+
* Shrink and convert a picture for upload. Throws `TypeError` outside a
|
|
2723
|
+
* browser, for a file the browser cannot decode, or when the result is still
|
|
2724
|
+
* over `maxBytes`.
|
|
2725
|
+
*/
|
|
2726
|
+
declare function preparePhoto(input: Blob, opts?: OwPhotoOptions): Promise<OwPreparedPhoto>;
|
|
2727
|
+
|
|
2529
2728
|
/**
|
|
2530
2729
|
* OnlyWorlds Token Management API Types
|
|
2531
2730
|
*
|
|
@@ -2810,4 +3009,4 @@ declare class TokenResource {
|
|
|
2810
3009
|
getEncryptionInfo(): Promise<EncryptionInfo>;
|
|
2811
3010
|
}
|
|
2812
3011
|
|
|
2813
|
-
export { type AccessKeyResponse, ELEMENT_FAMILIES, ELEMENT_ICONS, ELEMENT_LABELS, ELEMENT_SECTIONS, ELEMENT_TYPES, type ElementFamily, type ElementType, type EncryptionInfo, FAMILY_COLORS, FAMILY_ORDER, FIELD_SCHEMA, type FieldInfo, type FieldType, GameTier, type ListParams, ONLYWORLDS_VERSION, OwApiError, type OwAuthErrorCode, type OwBulkItem, type OwBulkItemResult, type OwBulkResponse, type OwChange, type OwChangesPage, type OwClientConfig, type OwElement, type OwElementBase, type OwErrorBody, type OwKeyKind, type OwLinkEdit, type OwMediaTicket, OwNetworkError, type OwPage, OwPreconditionFailedError, type OwReadWorldFile, type OwReadWorldFileOptions, type OwRemovalTicket, type OwRemovedImage, type OwUploadedImage, OwV2Client, type OwWorldFileEntry, OwWorldFileError, type OwWorldFileReason, type OwWorldMeta, type RevokeAllSessionsResponse, type RevokeSessionResponse, SPATIAL_TYPES, type SectionInfo, type TokenConsumeParams, type TokenConsumeResponse, TokenResource, type TokenStatus, type TokenTransport, WORLD_FILE_MAX_BYTES, WORLD_FILE_MAX_ENTRIES, detectKeyKind, elementColor, errorFromResponse, familyOf, getElementIcon, getElementLabel, isDemoKey, kindCanWrite, parseErrorEnvelope, pinExpectation, readWorldFile, writeWorldFile };
|
|
3012
|
+
export { type AccessKeyResponse, ELEMENT_FAMILIES, ELEMENT_ICONS, ELEMENT_LABELS, ELEMENT_SECTIONS, ELEMENT_TYPES, type ElementFamily, type ElementType, type EncryptionInfo, FAMILY_COLORS, FAMILY_ORDER, FIELD_SCHEMA, type FieldInfo, type FieldType, GameTier, type ListParams, ONLYWORLDS_VERSION, OwApiError, type OwAuthErrorCode, type OwBulkItem, type OwBulkItemResult, type OwBulkResponse, type OwChange, type OwChangesPage, type OwClientConfig, type OwElement, type OwElementBase, type OwErrorBody, type OwKeyKind, type OwLinkEdit, type OwLookupElement, type OwLookupHit, type OwLookupLink, type OwLookupOptions, type OwLookupReason, type OwMediaTicket, OwNetworkError, type OwPage, type OwPhotoOptions, type OwPhotoPlan, OwPreconditionFailedError, type OwPreparedPhoto, type OwReadWorldFile, type OwReadWorldFileOptions, type OwRemovalTicket, type OwRemovedImage, type OwRenderOptions, type OwUploadedImage, OwV2Client, type OwWorldFileEntry, OwWorldFileError, type OwWorldFileReason, type OwWorldIndex, type OwWorldMeta, PHOTO_MAX_BYTES, PHOTO_TYPES, type RevokeAllSessionsResponse, type RevokeSessionResponse, SPATIAL_TYPES, type SectionInfo, type TokenConsumeParams, type TokenConsumeResponse, TokenResource, type TokenStatus, type TokenTransport, WORLD_FILE_MAX_BYTES, WORLD_FILE_MAX_ENTRIES, buildWorldIndex, detectKeyKind, elementColor, errorFromResponse, familyOf, getElementIcon, getElementLabel, isDemoKey, kindCanWrite, lookupWorld, parseErrorEnvelope, pinExpectation, planPhoto, preparePhoto, readWorldFile, renderElement, writeWorldFile };
|
package/dist/index.js
CHANGED
|
@@ -1467,6 +1467,436 @@ function writeWorldFile(files, topDir) {
|
|
|
1467
1467
|
return zipSync(input);
|
|
1468
1468
|
}
|
|
1469
1469
|
|
|
1470
|
+
// src/lookup.ts
|
|
1471
|
+
var STOP = new Set("a an the of in on at to for from by with and or is are was were be been who whom whose what which where when why how does do did doesn has have had it its this that these those there their they them he she his her him i me my we our you your about tell know any some all can could would should will not no yes than then so if into out up down over under one two own same other more most much many very just also only s t don isn".split(" "));
|
|
1472
|
+
var SPATIAL = /* @__PURE__ */ new Set(["pin", "marker", "zone", "map"]);
|
|
1473
|
+
var FIELD_HINTS = {
|
|
1474
|
+
where: ["location", "parent_location", "birthplace", "locations", "estates", "element_id"],
|
|
1475
|
+
live: ["location", "locations", "populations"],
|
|
1476
|
+
lives: ["location", "locations", "populations"],
|
|
1477
|
+
located: ["location", "parent_location"],
|
|
1478
|
+
location: ["location", "parent_location"],
|
|
1479
|
+
born: ["birthplace"],
|
|
1480
|
+
birthplace: ["birthplace"],
|
|
1481
|
+
species: ["species", "parent_species"],
|
|
1482
|
+
race: ["species"],
|
|
1483
|
+
people: ["species", "populations"],
|
|
1484
|
+
rules: ["primary_power", "governs", "operator"],
|
|
1485
|
+
rule: ["primary_power", "governs", "operator"],
|
|
1486
|
+
ruled: ["primary_power", "governs"],
|
|
1487
|
+
holds: ["primary_power", "governs"],
|
|
1488
|
+
realm: ["primary_power"],
|
|
1489
|
+
govern: ["primary_power", "governs"],
|
|
1490
|
+
governs: ["primary_power", "governs"],
|
|
1491
|
+
controls: ["primary_power", "operator"],
|
|
1492
|
+
culture: ["populations"],
|
|
1493
|
+
cultures: ["populations"],
|
|
1494
|
+
folk: ["populations"],
|
|
1495
|
+
ally: ["allies", "friends"],
|
|
1496
|
+
allies: ["allies", "friends"],
|
|
1497
|
+
allied: ["allies"],
|
|
1498
|
+
friends: ["friends"],
|
|
1499
|
+
friend: ["friends"],
|
|
1500
|
+
enemy: ["adversaries", "rivals"],
|
|
1501
|
+
enemies: ["adversaries", "rivals"],
|
|
1502
|
+
rival: ["rivals"],
|
|
1503
|
+
rivals: ["rivals"],
|
|
1504
|
+
fought: ["institutions"],
|
|
1505
|
+
war: ["institutions"],
|
|
1506
|
+
fight: ["institutions"],
|
|
1507
|
+
family: ["family"],
|
|
1508
|
+
house: ["family"],
|
|
1509
|
+
clan: ["family"],
|
|
1510
|
+
member: ["characters", "institutions", "family"],
|
|
1511
|
+
members: ["characters", "institutions", "family"],
|
|
1512
|
+
belong: ["characters", "institutions", "family"],
|
|
1513
|
+
belongs: ["characters", "institutions", "family"],
|
|
1514
|
+
ancestor: ["ancestors"],
|
|
1515
|
+
ancestors: ["ancestors"],
|
|
1516
|
+
region: ["parent_location"],
|
|
1517
|
+
part: ["parent_location", "parent_species"],
|
|
1518
|
+
parent: ["parent_location", "parent_species"]
|
|
1519
|
+
};
|
|
1520
|
+
var TEXT_SKIP = /* @__PURE__ */ new Set([
|
|
1521
|
+
"id",
|
|
1522
|
+
"name",
|
|
1523
|
+
"type",
|
|
1524
|
+
"image_url",
|
|
1525
|
+
"created_at",
|
|
1526
|
+
"updated_at",
|
|
1527
|
+
"local_updated_at",
|
|
1528
|
+
"server_updated_at",
|
|
1529
|
+
"world",
|
|
1530
|
+
"image_media_id",
|
|
1531
|
+
"element_type"
|
|
1532
|
+
]);
|
|
1533
|
+
var RENDER_SKIP = /* @__PURE__ */ new Set([
|
|
1534
|
+
"id",
|
|
1535
|
+
"name",
|
|
1536
|
+
"description",
|
|
1537
|
+
"type",
|
|
1538
|
+
"supertype",
|
|
1539
|
+
"subtype",
|
|
1540
|
+
"image_url",
|
|
1541
|
+
"created_at",
|
|
1542
|
+
"updated_at",
|
|
1543
|
+
"local_updated_at",
|
|
1544
|
+
"server_updated_at",
|
|
1545
|
+
"world",
|
|
1546
|
+
"image_media_id"
|
|
1547
|
+
]);
|
|
1548
|
+
var ID_SHAPE = /^[0-9a-f-]{32,36}$/i;
|
|
1549
|
+
var MARKS = new RegExp(`[${String.fromCharCode(768)}-${String.fromCharCode(879)}]`, "g");
|
|
1550
|
+
var POSSESSIVE = new RegExp(`['${String.fromCharCode(8217)}]s\\b`, "g");
|
|
1551
|
+
function normalize(s) {
|
|
1552
|
+
return String(s ?? "").normalize("NFD").replace(MARKS, "").toLowerCase().replace(POSSESSIVE, "").replace(/(\d),(\d{3})\b/g, "$1$2").replace(/[^\p{L}\p{N}]+/gu, " ").trim();
|
|
1553
|
+
}
|
|
1554
|
+
var words = (s) => s ? s.split(" ") : [];
|
|
1555
|
+
var NON_LATIN = /[^\p{Script=Latin}\p{N}]/u;
|
|
1556
|
+
var isContent = (w) => w.length >= 3 && !STOP.has(w) || NON_LATIN.test(w);
|
|
1557
|
+
var schemaLinks;
|
|
1558
|
+
function linkFieldsOf(type) {
|
|
1559
|
+
if (!schemaLinks) {
|
|
1560
|
+
schemaLinks = /* @__PURE__ */ new Map();
|
|
1561
|
+
for (const [t, fields] of Object.entries(FIELD_SCHEMA)) {
|
|
1562
|
+
const s = /* @__PURE__ */ new Set();
|
|
1563
|
+
for (const [f, info] of Object.entries(fields)) if (info.type === "single_link" || info.type === "multi_link") s.add(f);
|
|
1564
|
+
schemaLinks.set(t, s);
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
return typeof type === "string" ? schemaLinks.get(type) : void 0;
|
|
1568
|
+
}
|
|
1569
|
+
function aliasesOf(el) {
|
|
1570
|
+
const raw = String(el.name ?? "");
|
|
1571
|
+
const full = normalize(raw);
|
|
1572
|
+
const out = /* @__PURE__ */ new Set();
|
|
1573
|
+
const add = (s) => {
|
|
1574
|
+
const n = normalize(s);
|
|
1575
|
+
if (n && n !== full && words(n).some(isContent)) out.add(n);
|
|
1576
|
+
};
|
|
1577
|
+
if (/^the\s/i.test(raw)) add(raw.replace(/^the\s+/i, ""));
|
|
1578
|
+
if (/[:(]/.test(raw)) add(raw.split(/[:(]/)[0]);
|
|
1579
|
+
if (el.type === "character") {
|
|
1580
|
+
if (/\sof\s/i.test(raw)) add(raw.split(/\sof\s/i)[0]);
|
|
1581
|
+
if (raw.includes(",")) add(raw.split(",")[0]);
|
|
1582
|
+
const first = words(full)[0];
|
|
1583
|
+
if (first && isContent(first)) add(first);
|
|
1584
|
+
}
|
|
1585
|
+
return [...out];
|
|
1586
|
+
}
|
|
1587
|
+
function buildWorldIndex(elements) {
|
|
1588
|
+
const byId = /* @__PURE__ */ new Map();
|
|
1589
|
+
for (const el of elements) if (el && el.id) byId.set(el.id, el);
|
|
1590
|
+
const out = /* @__PURE__ */ new Map(), inn = /* @__PURE__ */ new Map();
|
|
1591
|
+
const unresolved = /* @__PURE__ */ new Map();
|
|
1592
|
+
const phrase = /* @__PURE__ */ new Map();
|
|
1593
|
+
const tokenIds = /* @__PURE__ */ new Map();
|
|
1594
|
+
const nameWords = /* @__PURE__ */ new Map();
|
|
1595
|
+
const textIds = /* @__PURE__ */ new Map();
|
|
1596
|
+
const pushTo = (m, k, v) => {
|
|
1597
|
+
if (!m.has(k)) m.set(k, []);
|
|
1598
|
+
m.get(k).push(v);
|
|
1599
|
+
};
|
|
1600
|
+
for (const el of byId.values()) {
|
|
1601
|
+
const schema = linkFieldsOf(el.type);
|
|
1602
|
+
const linkFields = /* @__PURE__ */ new Set();
|
|
1603
|
+
for (const [field, v] of Object.entries(el)) {
|
|
1604
|
+
if (field === "id") continue;
|
|
1605
|
+
const vals = Array.isArray(v) ? v : [v];
|
|
1606
|
+
if (!vals.length) continue;
|
|
1607
|
+
if (schema?.has(field)) {
|
|
1608
|
+
const ids = vals.filter((x) => typeof x === "string" && x !== "");
|
|
1609
|
+
if (!ids.length) continue;
|
|
1610
|
+
linkFields.add(field);
|
|
1611
|
+
for (const id of ids) {
|
|
1612
|
+
if (byId.has(id)) {
|
|
1613
|
+
pushTo(out, el.id, { field, id });
|
|
1614
|
+
pushTo(inn, id, { field, id: el.id });
|
|
1615
|
+
} else pushTo(unresolved, el.id, { field, id });
|
|
1616
|
+
}
|
|
1617
|
+
continue;
|
|
1618
|
+
}
|
|
1619
|
+
if (!vals.every((x) => typeof x === "string" && ID_SHAPE.test(x))) continue;
|
|
1620
|
+
for (const id of vals) {
|
|
1621
|
+
if (byId.has(id)) {
|
|
1622
|
+
pushTo(out, el.id, { field, id });
|
|
1623
|
+
pushTo(inn, id, { field, id: el.id });
|
|
1624
|
+
} else pushTo(unresolved, el.id, { field, id });
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
if (!SPATIAL.has(el.type)) {
|
|
1628
|
+
const tw = /* @__PURE__ */ new Set();
|
|
1629
|
+
for (const [field, v] of Object.entries(el)) {
|
|
1630
|
+
if (TEXT_SKIP.has(field) || linkFields.has(field) || field.startsWith("x_") || field.startsWith("atlas_")) continue;
|
|
1631
|
+
const vals = Array.isArray(v) ? v : [v];
|
|
1632
|
+
for (const x of vals) {
|
|
1633
|
+
if (typeof x === "number" && Number.isFinite(x)) tw.add(String(x));
|
|
1634
|
+
else if (typeof x === "string" && x && !ID_SHAPE.test(x)) {
|
|
1635
|
+
for (const w of words(normalize(x))) if (isContent(w) || /^\d+$/.test(w)) tw.add(w);
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
for (const w of tw) {
|
|
1640
|
+
if (!textIds.has(w)) textIds.set(w, /* @__PURE__ */ new Set());
|
|
1641
|
+
textIds.get(w).add(el.id);
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
const full = normalize(el.name);
|
|
1645
|
+
if (!full) continue;
|
|
1646
|
+
pushTo(phrase, full, { id: el.id, kind: "name" });
|
|
1647
|
+
for (const a of aliasesOf(el)) pushTo(phrase, a, { id: el.id, kind: "alias" });
|
|
1648
|
+
const cw = [...new Set(words(full).filter(isContent))];
|
|
1649
|
+
nameWords.set(el.id, cw);
|
|
1650
|
+
for (const w of cw) {
|
|
1651
|
+
if (!tokenIds.has(w)) tokenIds.set(w, /* @__PURE__ */ new Set());
|
|
1652
|
+
tokenIds.get(w).add(el.id);
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
const N = byId.size || 1;
|
|
1656
|
+
const idf = /* @__PURE__ */ new Map();
|
|
1657
|
+
for (const [w, ids] of tokenIds) idf.set(w, Math.log(1 + N / ids.size));
|
|
1658
|
+
const textIdf = /* @__PURE__ */ new Map();
|
|
1659
|
+
for (const [w, ids] of textIds) textIdf.set(w, Math.log(1 + N / ids.size));
|
|
1660
|
+
return { byId, out, inn, unresolved, phrase, tokenIds, nameWords, idf, textIds, textIdf };
|
|
1661
|
+
}
|
|
1662
|
+
function similarWords(w, vocab) {
|
|
1663
|
+
const res = [];
|
|
1664
|
+
if (vocab.has(w)) res.push(w);
|
|
1665
|
+
if (w.length >= 5 && !/^\d+$/.test(w)) {
|
|
1666
|
+
for (const t of vocab.keys()) {
|
|
1667
|
+
if (t === w || t.length < 5) continue;
|
|
1668
|
+
const p = Math.min(w.length, t.length);
|
|
1669
|
+
if (p >= 5 && Math.abs(w.length - t.length) <= 3 && (t.startsWith(w) || w.startsWith(t))) res.push(t);
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
return res;
|
|
1673
|
+
}
|
|
1674
|
+
function lookupWorld(index, question, { limit = 8 } = {}) {
|
|
1675
|
+
const seeds = 12;
|
|
1676
|
+
const q = words(normalize(question));
|
|
1677
|
+
const qSet = new Set(q);
|
|
1678
|
+
const cand = /* @__PURE__ */ new Map();
|
|
1679
|
+
const bump = (id, score, reason, direct) => {
|
|
1680
|
+
const c = cand.get(id) || { score: 0, direct: 0, reasons: [] };
|
|
1681
|
+
c.reasons.push(reason);
|
|
1682
|
+
if (score > c.score) c.score = score;
|
|
1683
|
+
if (direct && score > c.direct) c.direct = score;
|
|
1684
|
+
cand.set(id, c);
|
|
1685
|
+
};
|
|
1686
|
+
const qUsed = /* @__PURE__ */ new Map();
|
|
1687
|
+
const useWords = (id, ws) => {
|
|
1688
|
+
if (!qUsed.has(id)) qUsed.set(id, /* @__PURE__ */ new Set());
|
|
1689
|
+
for (const w of ws) qUsed.get(id).add(w);
|
|
1690
|
+
};
|
|
1691
|
+
for (let n = Math.min(8, q.length); n >= 1; n--) {
|
|
1692
|
+
for (let i = 0; i + n <= q.length; i++) {
|
|
1693
|
+
const g = q.slice(i, i + n);
|
|
1694
|
+
if (n === 1 && !isContent(g[0])) continue;
|
|
1695
|
+
const hits = index.phrase.get(g.join(" "));
|
|
1696
|
+
if (!hits) continue;
|
|
1697
|
+
for (const h of hits) {
|
|
1698
|
+
const base = h.kind === "name" ? 100 : 80;
|
|
1699
|
+
bump(h.id, base + 10 * n, { kind: h.kind, text: g.join(" ") }, true);
|
|
1700
|
+
useWords(h.id, g);
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
const matched = /* @__PURE__ */ new Map();
|
|
1705
|
+
for (const w of qSet) {
|
|
1706
|
+
if (!isContent(w)) continue;
|
|
1707
|
+
for (const t of similarWords(w, index.tokenIds)) {
|
|
1708
|
+
for (const id of index.tokenIds.get(t)) {
|
|
1709
|
+
if (!matched.has(id)) matched.set(id, /* @__PURE__ */ new Set());
|
|
1710
|
+
matched.get(id).add(t);
|
|
1711
|
+
useWords(id, [w]);
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
for (const [id, ws] of matched) {
|
|
1716
|
+
const nw = index.nameWords.get(id) || [];
|
|
1717
|
+
const tot = nw.reduce((s, w) => s + index.idf.get(w), 0) || 1;
|
|
1718
|
+
const got = [...ws].reduce((s, w) => s + index.idf.get(w), 0);
|
|
1719
|
+
const score = 20 + 40 * (got / tot);
|
|
1720
|
+
const c = cand.get(id);
|
|
1721
|
+
if (!c || c.direct < score) bump(id, score, { kind: "word", text: [...ws].join(" ") }, true);
|
|
1722
|
+
}
|
|
1723
|
+
const kindBonus = (id) => {
|
|
1724
|
+
const el = index.byId.get(id);
|
|
1725
|
+
const kw = new Set(words(normalize(`${el.supertype ?? ""} ${el.subtype ?? ""}`)).filter(isContent));
|
|
1726
|
+
const used = qUsed.get(id);
|
|
1727
|
+
for (const w of qSet) {
|
|
1728
|
+
if (used?.has(w)) continue;
|
|
1729
|
+
if (kw.has(w) || [...kw].some((k) => k.length >= 5 && w.length >= 5 && (k.startsWith(w) || w.startsWith(k)))) {
|
|
1730
|
+
useWords(id, [w]);
|
|
1731
|
+
return 15;
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
return 0;
|
|
1735
|
+
};
|
|
1736
|
+
for (const [id, c] of cand) {
|
|
1737
|
+
const b = kindBonus(id);
|
|
1738
|
+
if (b) {
|
|
1739
|
+
c.score += b;
|
|
1740
|
+
c.direct += b;
|
|
1741
|
+
c.kinded = true;
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
const hintFields = /* @__PURE__ */ new Set();
|
|
1745
|
+
for (const w of qSet) for (const f of FIELD_HINTS[w] || []) hintFields.add(f);
|
|
1746
|
+
const seedList = [...cand.entries()].filter(([, c]) => c.direct > 0).sort((a, b) => b[1].direct - a[1].direct).slice(0, seeds);
|
|
1747
|
+
for (const [sid, sc] of seedList) {
|
|
1748
|
+
const sname = index.byId.get(sid).name;
|
|
1749
|
+
for (const { field, id } of index.out.get(sid) || []) {
|
|
1750
|
+
const m = hintFields.has(field) ? 1.5 : 1;
|
|
1751
|
+
bump(id, sc.direct * 0.6 * m, { kind: "link", dir: "out", field, from: sname, fromId: sid }, false);
|
|
1752
|
+
}
|
|
1753
|
+
for (const { field, id } of index.inn.get(sid) || []) {
|
|
1754
|
+
const m = hintFields.has(field) ? 1.5 : 1;
|
|
1755
|
+
bump(id, sc.direct * 0.45 * m, { kind: "link", dir: "in", field, from: sname, fromId: sid }, false);
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
for (const [id, c] of cand) {
|
|
1759
|
+
if (!c.kinded) {
|
|
1760
|
+
const b = kindBonus(id);
|
|
1761
|
+
if (b && c.direct === 0) c.score += b;
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
const textHits = /* @__PURE__ */ new Map();
|
|
1765
|
+
for (const w of qSet) {
|
|
1766
|
+
if (!isContent(w) && !/^\d+$/.test(w)) continue;
|
|
1767
|
+
for (const t of similarWords(w, index.textIds)) {
|
|
1768
|
+
const f = index.textIdf.get(t);
|
|
1769
|
+
for (const id of index.textIds.get(t)) {
|
|
1770
|
+
if (!textHits.has(id)) textHits.set(id, /* @__PURE__ */ new Map());
|
|
1771
|
+
const m = textHits.get(id);
|
|
1772
|
+
if (!((m.get(w) ?? NaN) >= f)) m.set(w, f);
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
for (const [id, m] of textHits) {
|
|
1777
|
+
const used = qUsed.get(id);
|
|
1778
|
+
const ws = [...m.keys()].filter((w) => !used?.has(w));
|
|
1779
|
+
if (!ws.length) continue;
|
|
1780
|
+
const raw = 3 * ws.reduce((s, w) => s + m.get(w), 0);
|
|
1781
|
+
const reason = { kind: "text", text: ws.join(" ") };
|
|
1782
|
+
const c = cand.get(id);
|
|
1783
|
+
if (c) {
|
|
1784
|
+
c.score += raw;
|
|
1785
|
+
c.reasons.push(reason);
|
|
1786
|
+
} else cand.set(id, { score: 19 * (1 - Math.exp(-raw / 30)), direct: 0, reasons: [reason] });
|
|
1787
|
+
}
|
|
1788
|
+
for (const [id, c] of cand) if (SPATIAL.has(index.byId.get(id).type)) c.score *= 0.85;
|
|
1789
|
+
return [...cand.entries()].map(([id, c]) => ({ element: index.byId.get(id), score: Math.round(c.score * 10) / 10, reasons: c.reasons })).sort((a, b) => b.score - a.score || String(a.element.name).length - String(b.element.name).length || (a.element.id < b.element.id ? -1 : 1)).slice(0, limit);
|
|
1790
|
+
}
|
|
1791
|
+
function renderElement(element, index, { descMax = 500, linkMax = 12, incomingMax = 10 } = {}) {
|
|
1792
|
+
const el = element;
|
|
1793
|
+
const name = (id) => index.byId.get(id)?.name ?? "?";
|
|
1794
|
+
const kind = [el.supertype, el.subtype].filter((x) => x && x !== "undefined").join(" / ");
|
|
1795
|
+
const lines = [`${el.name} (${el.type}${kind ? `; ${kind}` : ""})`];
|
|
1796
|
+
if (el.description) {
|
|
1797
|
+
const d = String(el.description);
|
|
1798
|
+
lines.push(`description: ${d.length > descMax ? d.slice(0, descMax) + "..." : d}`);
|
|
1799
|
+
}
|
|
1800
|
+
const outBy = /* @__PURE__ */ new Map();
|
|
1801
|
+
for (const { field, id } of index.out.get(el.id) || []) {
|
|
1802
|
+
if (!outBy.has(field)) outBy.set(field, []);
|
|
1803
|
+
outBy.get(field).push(name(id));
|
|
1804
|
+
}
|
|
1805
|
+
const unres = /* @__PURE__ */ new Map();
|
|
1806
|
+
for (const { field } of index.unresolved.get(el.id) || []) unres.set(field, (unres.get(field) || 0) + 1);
|
|
1807
|
+
for (const [field, v] of Object.entries(el)) {
|
|
1808
|
+
if (RENDER_SKIP.has(field) || field.startsWith("x_") || field.startsWith("atlas_")) continue;
|
|
1809
|
+
if (outBy.has(field) || unres.has(field)) {
|
|
1810
|
+
const names = outBy.get(field) || [];
|
|
1811
|
+
let s = names.slice(0, linkMax).join(", ");
|
|
1812
|
+
if (names.length > linkMax) s += ` (+${names.length - linkMax} more)`;
|
|
1813
|
+
const u = unres.get(field);
|
|
1814
|
+
if (u) s += `${s ? "; " : ""}${u} linked element(s) not present in the world data`;
|
|
1815
|
+
lines.push(`${field}: ${s}`);
|
|
1816
|
+
continue;
|
|
1817
|
+
}
|
|
1818
|
+
if (v === null || v === void 0 || v === "" || Array.isArray(v) && !v.length || typeof v === "object" && !Array.isArray(v)) continue;
|
|
1819
|
+
lines.push(`${field}: ${Array.isArray(v) ? v.join(", ") : v}`);
|
|
1820
|
+
}
|
|
1821
|
+
const inBy = /* @__PURE__ */ new Map();
|
|
1822
|
+
for (const { field, id } of index.inn.get(el.id) || []) {
|
|
1823
|
+
const src = index.byId.get(id);
|
|
1824
|
+
if (SPATIAL.has(src.type)) continue;
|
|
1825
|
+
const k = `${src.type}.${field}`;
|
|
1826
|
+
if (!inBy.has(k)) inBy.set(k, []);
|
|
1827
|
+
inBy.get(k).push(src.name);
|
|
1828
|
+
}
|
|
1829
|
+
for (const [k, names] of inBy) {
|
|
1830
|
+
let s = names.slice(0, incomingMax).join(", ");
|
|
1831
|
+
if (names.length > incomingMax) s += ` (+${names.length - incomingMax} more)`;
|
|
1832
|
+
lines.push(`linked from (${k}): ${s}`);
|
|
1833
|
+
}
|
|
1834
|
+
return lines.join("\n");
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1837
|
+
// src/photo.ts
|
|
1838
|
+
var PHOTO_TYPES = ["image/webp", "image/png", "image/jpeg", "image/avif"];
|
|
1839
|
+
var PHOTO_MAX_BYTES = 15 * 1024 * 1024;
|
|
1840
|
+
function planPhoto(source, opts = {}) {
|
|
1841
|
+
const maxSide = opts.maxSide ?? 2048;
|
|
1842
|
+
if (!(maxSide > 0)) throw new TypeError("preparePhoto: maxSide must be a positive number");
|
|
1843
|
+
if (!(source.width > 0 && source.height > 0)) throw new TypeError("preparePhoto: the picture has no size");
|
|
1844
|
+
const scale = Math.min(1, maxSide / Math.max(source.width, source.height));
|
|
1845
|
+
const width = Math.max(1, Math.round(source.width * scale));
|
|
1846
|
+
const height = Math.max(1, Math.round(source.height * scale));
|
|
1847
|
+
const fallback = source.type === "image/png" ? "image/png" : "image/jpeg";
|
|
1848
|
+
return { width, height, scale, targets: ["image/webp", fallback] };
|
|
1849
|
+
}
|
|
1850
|
+
function makeCanvas(width, height) {
|
|
1851
|
+
const g = globalThis;
|
|
1852
|
+
if (g.OffscreenCanvas) {
|
|
1853
|
+
const canvas = new g.OffscreenCanvas(width, height);
|
|
1854
|
+
return { canvas, encode: (type, quality) => canvas.convertToBlob({ type, quality }).catch(() => null) };
|
|
1855
|
+
}
|
|
1856
|
+
if (g.document) {
|
|
1857
|
+
const canvas = g.document.createElement("canvas");
|
|
1858
|
+
canvas.width = width;
|
|
1859
|
+
canvas.height = height;
|
|
1860
|
+
return { canvas, encode: (type, quality) => new Promise((res) => canvas.toBlob(res, type, quality)) };
|
|
1861
|
+
}
|
|
1862
|
+
throw new TypeError("preparePhoto needs a browser: no canvas here");
|
|
1863
|
+
}
|
|
1864
|
+
async function preparePhoto(input, opts = {}) {
|
|
1865
|
+
const quality = opts.quality ?? 0.82;
|
|
1866
|
+
const maxBytes = opts.maxBytes ?? PHOTO_MAX_BYTES;
|
|
1867
|
+
const decode = globalThis.createImageBitmap;
|
|
1868
|
+
if (!decode) throw new TypeError("preparePhoto needs a browser: no createImageBitmap here");
|
|
1869
|
+
let bitmap;
|
|
1870
|
+
try {
|
|
1871
|
+
bitmap = await decode(input, { imageOrientation: "from-image" });
|
|
1872
|
+
} catch {
|
|
1873
|
+
throw new TypeError("preparePhoto: this browser cannot read that picture");
|
|
1874
|
+
}
|
|
1875
|
+
const original = { width: bitmap.width, height: bitmap.height, type: input.type, size: input.size };
|
|
1876
|
+
try {
|
|
1877
|
+
const plan = planPhoto(original, opts);
|
|
1878
|
+
const { canvas, encode } = makeCanvas(plan.width, plan.height);
|
|
1879
|
+
canvas.getContext("2d").drawImage(bitmap, 0, 0, plan.width, plan.height);
|
|
1880
|
+
let made = null;
|
|
1881
|
+
for (const type of plan.targets) {
|
|
1882
|
+
const b = await encode(type, quality);
|
|
1883
|
+
if (b && b.type === type) {
|
|
1884
|
+
made = b;
|
|
1885
|
+
break;
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
if (!made) throw new TypeError("preparePhoto: this browser could not encode the picture");
|
|
1889
|
+
const keepOriginal = plan.scale === 1 && made.size >= input.size && PHOTO_TYPES.includes(input.type);
|
|
1890
|
+
const out = keepOriginal ? { blob: input, type: input.type, width: original.width, height: original.height, kept: true } : { blob: made, type: made.type, width: plan.width, height: plan.height, kept: false };
|
|
1891
|
+
if (out.blob.size > maxBytes) {
|
|
1892
|
+
throw new TypeError(`preparePhoto: still ${Math.ceil(out.blob.size / 1048576)} MB after shrinking; try a smaller maxSide`);
|
|
1893
|
+
}
|
|
1894
|
+
return { ...out, original };
|
|
1895
|
+
} finally {
|
|
1896
|
+
bitmap.close?.();
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
|
|
1470
1900
|
// src/token-resource.ts
|
|
1471
1901
|
var TokenResource = class {
|
|
1472
1902
|
constructor(client) {
|
|
@@ -1653,10 +2083,13 @@ export {
|
|
|
1653
2083
|
OwPreconditionFailedError,
|
|
1654
2084
|
OwV2Client,
|
|
1655
2085
|
OwWorldFileError,
|
|
2086
|
+
PHOTO_MAX_BYTES,
|
|
2087
|
+
PHOTO_TYPES,
|
|
1656
2088
|
SPATIAL_TYPES,
|
|
1657
2089
|
TokenResource,
|
|
1658
2090
|
WORLD_FILE_MAX_BYTES,
|
|
1659
2091
|
WORLD_FILE_MAX_ENTRIES,
|
|
2092
|
+
buildWorldIndex,
|
|
1660
2093
|
detectKeyKind,
|
|
1661
2094
|
elementColor,
|
|
1662
2095
|
errorFromResponse,
|
|
@@ -1665,8 +2098,12 @@ export {
|
|
|
1665
2098
|
getElementLabel,
|
|
1666
2099
|
isDemoKey,
|
|
1667
2100
|
kindCanWrite,
|
|
2101
|
+
lookupWorld,
|
|
1668
2102
|
parseErrorEnvelope,
|
|
1669
2103
|
pinExpectation,
|
|
2104
|
+
planPhoto,
|
|
2105
|
+
preparePhoto,
|
|
1670
2106
|
readWorldFile,
|
|
2107
|
+
renderElement,
|
|
1671
2108
|
writeWorldFile
|
|
1672
2109
|
};
|