@storylet-studio/model 0.1.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/README.md +12 -0
- package/dist/index.cjs +574 -0
- package/dist/index.d.cts +1113 -0
- package/dist/index.d.ts +1113 -0
- package/dist/index.js +500 -0
- package/package.json +32 -0
package/README.md
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# @storylet-studio/model
|
|
2
|
+
|
|
3
|
+
Storylets data-model types: source shards (project / box / tags / hands /
|
|
4
|
+
deck files), the compiled `.storyletsc` bundle, and the save
|
|
5
|
+
envelope. The shape source-of-truth; no behaviour.
|
|
6
|
+
|
|
7
|
+
Entity shapes are generic over their expression representation: source
|
|
8
|
+
shards carry plain `src` strings (`Card<string>`), the compiled bundle
|
|
9
|
+
carries `{ src, ast }` envelopes (`Card<Expression>`).
|
|
10
|
+
|
|
11
|
+
Authored against [the bundle format](https://storylet.studio/format/bundle/)
|
|
12
|
+
and [the source format](https://storylet.studio/format/shards/).
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,574 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
BOX_SCHEMA: () => BOX_SCHEMA,
|
|
24
|
+
BUNDLE_EXTENSION: () => BUNDLE_EXTENSION,
|
|
25
|
+
BUNDLE_SCHEMA: () => BUNDLE_SCHEMA,
|
|
26
|
+
DECK_SCHEMA: () => DECK_SCHEMA,
|
|
27
|
+
FURNITURE_COLOURS: () => FURNITURE_COLOURS,
|
|
28
|
+
HANDS_SCHEMA: () => HANDS_SCHEMA,
|
|
29
|
+
NOTES_SCHEMA: () => NOTES_SCHEMA,
|
|
30
|
+
PLACE_GROUP: () => PLACE_GROUP,
|
|
31
|
+
PROJECT_FOLDER_EXTENSION: () => PROJECT_FOLDER_EXTENSION,
|
|
32
|
+
PROJECT_SCHEMA: () => PROJECT_SCHEMA,
|
|
33
|
+
RESERVED_PROPERTY_NAMES: () => import_expr.RESERVED_PROPERTY_NAMES,
|
|
34
|
+
SAVEFILE_SCHEMA: () => SAVEFILE_SCHEMA,
|
|
35
|
+
SAVE_SCHEMA: () => SAVE_SCHEMA,
|
|
36
|
+
SHARD_EXTENSIONS: () => SHARD_EXTENSIONS,
|
|
37
|
+
SPATIAL: () => SPATIAL,
|
|
38
|
+
TAGS_SCHEMA: () => TAGS_SCHEMA,
|
|
39
|
+
VIEW_SCHEMA: () => VIEW_SCHEMA,
|
|
40
|
+
backgroundsOf: () => backgroundsOf,
|
|
41
|
+
bindHand: () => bindHand,
|
|
42
|
+
bundleAssetPath: () => bundleAssetPath,
|
|
43
|
+
byDisplayOrder: () => byDisplayOrder,
|
|
44
|
+
centroid: () => centroid,
|
|
45
|
+
commentsOf: () => commentsOf,
|
|
46
|
+
droppedRect: () => droppedRect,
|
|
47
|
+
effectiveGameId: () => effectiveGameId,
|
|
48
|
+
framesOf: () => framesOf,
|
|
49
|
+
freeGameId: () => freeGameId,
|
|
50
|
+
freeTitle: () => freeTitle,
|
|
51
|
+
gameIdify: () => gameIdify,
|
|
52
|
+
handBinding: () => handBinding,
|
|
53
|
+
inferDeclFromWrite: () => inferDeclFromWrite,
|
|
54
|
+
isCaseOnlyPropertyName: () => import_expr.isCaseOnlyPropertyName,
|
|
55
|
+
isSpatial: () => isSpatial,
|
|
56
|
+
isValidGameId: () => isValidGameId,
|
|
57
|
+
isValidPropertyName: () => import_expr.isValidPropertyName,
|
|
58
|
+
labelPoint: () => labelPoint,
|
|
59
|
+
markOf: () => markOf,
|
|
60
|
+
marksOn: () => marksOn,
|
|
61
|
+
openThreadCounts: () => openThreadCounts,
|
|
62
|
+
pointInPolygon: () => pointInPolygon,
|
|
63
|
+
polygonBounds: () => polygonBounds,
|
|
64
|
+
polygonOf: () => polygonOf,
|
|
65
|
+
propertyNameify: () => import_expr.propertyNameify,
|
|
66
|
+
restack: () => restack,
|
|
67
|
+
spatialOf: () => spatialOf,
|
|
68
|
+
stacked: () => stacked,
|
|
69
|
+
threadsFor: () => threadsFor,
|
|
70
|
+
unbindHand: () => unbindHand,
|
|
71
|
+
withBackgrounds: () => withBackgrounds,
|
|
72
|
+
withPolygon: () => withPolygon,
|
|
73
|
+
withSpatialGroup: () => withSpatialGroup,
|
|
74
|
+
withZ: () => withZ,
|
|
75
|
+
zOf: () => zOf,
|
|
76
|
+
zoneAt: () => zoneAt,
|
|
77
|
+
zonesAt: () => zonesAt
|
|
78
|
+
});
|
|
79
|
+
module.exports = __toCommonJS(index_exports);
|
|
80
|
+
var import_expr = require("@wildwinter/expr");
|
|
81
|
+
|
|
82
|
+
// src/spatial.ts
|
|
83
|
+
var SPATIAL = "spatial";
|
|
84
|
+
function stacked(items) {
|
|
85
|
+
return items.map((item, index) => ({ item, z: item.z ?? index })).sort((a, b) => a.z - b.z).map((entry) => entry.item);
|
|
86
|
+
}
|
|
87
|
+
function restack(items, id, move) {
|
|
88
|
+
const order = items.map((item, index) => ({ id: item.id, z: item.z ?? index })).sort((a, b) => a.z - b.z);
|
|
89
|
+
const at = order.findIndex((entry) => entry.id === id);
|
|
90
|
+
if (at < 0 || order.length < 2) return void 0;
|
|
91
|
+
const front = order.length - 1;
|
|
92
|
+
if (move === "back") return at === 0 ? void 0 : order[0].z - 1;
|
|
93
|
+
if (move === "front") return at === front ? void 0 : order[front].z + 1;
|
|
94
|
+
if (move === "backward") {
|
|
95
|
+
if (at === 0) return void 0;
|
|
96
|
+
const passed2 = order[at - 1].z;
|
|
97
|
+
const beyond2 = at >= 2 ? order[at - 2].z : passed2 - 2;
|
|
98
|
+
return (passed2 + beyond2) / 2;
|
|
99
|
+
}
|
|
100
|
+
if (at === front) return void 0;
|
|
101
|
+
const passed = order[at + 1].z;
|
|
102
|
+
const beyond = at + 2 <= front ? order[at + 2].z : passed + 2;
|
|
103
|
+
return (passed + beyond) / 2;
|
|
104
|
+
}
|
|
105
|
+
function isSpatial(group) {
|
|
106
|
+
return spatialOf(group) !== void 0;
|
|
107
|
+
}
|
|
108
|
+
function spatialOf(group) {
|
|
109
|
+
const bag = group.templates?.[SPATIAL];
|
|
110
|
+
if (!isRecord(bag) || bag["map"] !== true) return void 0;
|
|
111
|
+
const backgrounds = backgroundsOf(group);
|
|
112
|
+
return { map: true, ...backgrounds.length > 0 ? { backgrounds } : {} };
|
|
113
|
+
}
|
|
114
|
+
function backgroundsOf(group) {
|
|
115
|
+
const bag = group.templates?.[SPATIAL];
|
|
116
|
+
if (!isRecord(bag)) return [];
|
|
117
|
+
const list = bag["backgrounds"];
|
|
118
|
+
if (!Array.isArray(list)) return [];
|
|
119
|
+
const out = [];
|
|
120
|
+
for (const entry of list) {
|
|
121
|
+
const one = asBackground(entry);
|
|
122
|
+
if (one) out.push(one);
|
|
123
|
+
}
|
|
124
|
+
return stacked(out);
|
|
125
|
+
}
|
|
126
|
+
function asBackground(entry) {
|
|
127
|
+
if (!isRecord(entry)) return void 0;
|
|
128
|
+
const { id, file, x, y, width, height, opacity, z, hidden, locked } = entry;
|
|
129
|
+
if (typeof id !== "string" || id === "") return void 0;
|
|
130
|
+
if (typeof file !== "string" || file === "") return void 0;
|
|
131
|
+
const nums = [x, y, width, height];
|
|
132
|
+
if (!nums.every((n) => typeof n === "number" && Number.isFinite(n))) return void 0;
|
|
133
|
+
if (width <= 0 || height <= 0) return void 0;
|
|
134
|
+
return {
|
|
135
|
+
id,
|
|
136
|
+
file,
|
|
137
|
+
x,
|
|
138
|
+
y,
|
|
139
|
+
width,
|
|
140
|
+
height,
|
|
141
|
+
...typeof opacity === "number" && Number.isFinite(opacity) ? { opacity: clamp01(opacity) } : {},
|
|
142
|
+
...typeof z === "number" && Number.isFinite(z) ? { z } : {},
|
|
143
|
+
...hidden === true ? { hidden: true } : {},
|
|
144
|
+
...locked === true ? { locked: true } : {}
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
var clamp01 = (n) => Math.min(1, Math.max(0, n));
|
|
148
|
+
function droppedRect(natural, view, scale, at) {
|
|
149
|
+
const safe = (n, fallback) => Number.isFinite(n) && n > 0 ? n : fallback;
|
|
150
|
+
const naturalW = safe(natural.width, 1);
|
|
151
|
+
const naturalH = safe(natural.height, 1);
|
|
152
|
+
const room = { width: safe(view.width, 800) * 0.6, height: safe(view.height, 600) * 0.6 };
|
|
153
|
+
const fit = Math.min(room.width / naturalW, room.height / naturalH) / safe(scale, 1);
|
|
154
|
+
const width = Math.max(1, Math.round(naturalW * fit));
|
|
155
|
+
const height = Math.max(1, Math.round(naturalH * fit));
|
|
156
|
+
return { x: Math.round(at.x - width / 2), y: Math.round(at.y - height / 2), width, height };
|
|
157
|
+
}
|
|
158
|
+
function withBackgrounds(group, backgrounds) {
|
|
159
|
+
const had = isRecord(group.templates?.[SPATIAL]) ? { ...group.templates[SPATIAL] } : {};
|
|
160
|
+
had["map"] = true;
|
|
161
|
+
if (backgrounds.length > 0) {
|
|
162
|
+
had["backgrounds"] = backgrounds.map((b) => ({
|
|
163
|
+
...b,
|
|
164
|
+
// Whole units in, exactly as polygons are: a rect dragged by hand would
|
|
165
|
+
// otherwise write six decimal places of noise into a file two people have
|
|
166
|
+
// to merge.
|
|
167
|
+
x: Math.round(b.x),
|
|
168
|
+
y: Math.round(b.y),
|
|
169
|
+
width: Math.round(b.width),
|
|
170
|
+
height: Math.round(b.height)
|
|
171
|
+
}));
|
|
172
|
+
} else delete had["backgrounds"];
|
|
173
|
+
return setBag(group.templates, had);
|
|
174
|
+
}
|
|
175
|
+
function polygonOf(tag) {
|
|
176
|
+
const bag = tag.templates?.[SPATIAL];
|
|
177
|
+
if (!isRecord(bag)) return void 0;
|
|
178
|
+
const points = bag["polygon"];
|
|
179
|
+
if (!Array.isArray(points) || points.length < 3) return void 0;
|
|
180
|
+
const out = [];
|
|
181
|
+
for (const p of points) {
|
|
182
|
+
if (!isRecord(p)) return void 0;
|
|
183
|
+
const { x, y } = p;
|
|
184
|
+
if (typeof x !== "number" || typeof y !== "number") return void 0;
|
|
185
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) return void 0;
|
|
186
|
+
out.push({ x, y });
|
|
187
|
+
}
|
|
188
|
+
return out;
|
|
189
|
+
}
|
|
190
|
+
function withSpatialGroup(group, on) {
|
|
191
|
+
return setBag(group.templates, on ? { map: true } : void 0);
|
|
192
|
+
}
|
|
193
|
+
function zOf(tag) {
|
|
194
|
+
const bag = tag.templates?.[SPATIAL];
|
|
195
|
+
if (!isRecord(bag)) return void 0;
|
|
196
|
+
const z = bag["z"];
|
|
197
|
+
return typeof z === "number" && Number.isFinite(z) ? z : void 0;
|
|
198
|
+
}
|
|
199
|
+
function withZ(tag, z) {
|
|
200
|
+
const had = isRecord(tag.templates?.[SPATIAL]) ? { ...tag.templates[SPATIAL] } : {};
|
|
201
|
+
had["z"] = Math.round(z * 1e3) / 1e3;
|
|
202
|
+
return setBag(tag.templates, had);
|
|
203
|
+
}
|
|
204
|
+
function withPolygon(tag, polygon) {
|
|
205
|
+
const had = isRecord(tag.templates?.[SPATIAL]) ? { ...tag.templates[SPATIAL] } : {};
|
|
206
|
+
if (polygon === void 0) delete had["polygon"];
|
|
207
|
+
else had["polygon"] = polygon.map((p) => ({ x: Math.round(p.x), y: Math.round(p.y) }));
|
|
208
|
+
return setBag(tag.templates, Object.keys(had).length > 0 ? had : void 0);
|
|
209
|
+
}
|
|
210
|
+
function setBag(templates, bag) {
|
|
211
|
+
const next = { ...templates ?? {} };
|
|
212
|
+
if (bag === void 0) delete next[SPATIAL];
|
|
213
|
+
else next[SPATIAL] = bag;
|
|
214
|
+
return Object.keys(next).length > 0 ? next : void 0;
|
|
215
|
+
}
|
|
216
|
+
var isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
217
|
+
function polygonBounds(polygon) {
|
|
218
|
+
const xs = polygon.map((p) => p.x);
|
|
219
|
+
const ys = polygon.map((p) => p.y);
|
|
220
|
+
const x = Math.min(...xs);
|
|
221
|
+
const y = Math.min(...ys);
|
|
222
|
+
return { x, y, width: Math.max(...xs) - x, height: Math.max(...ys) - y };
|
|
223
|
+
}
|
|
224
|
+
function centroid(polygon) {
|
|
225
|
+
let twiceArea = 0;
|
|
226
|
+
let x = 0;
|
|
227
|
+
let y = 0;
|
|
228
|
+
for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
|
|
229
|
+
const a = polygon[i];
|
|
230
|
+
const b = polygon[j];
|
|
231
|
+
const cross = a.x * b.y - b.x * a.y;
|
|
232
|
+
twiceArea += cross;
|
|
233
|
+
x += (a.x + b.x) * cross;
|
|
234
|
+
y += (a.y + b.y) * cross;
|
|
235
|
+
}
|
|
236
|
+
if (twiceArea === 0) {
|
|
237
|
+
const n = polygon.length;
|
|
238
|
+
return n === 0 ? { x: 0, y: 0 } : { x: polygon.reduce((s, p) => s + p.x, 0) / n, y: polygon.reduce((s, p) => s + p.y, 0) / n };
|
|
239
|
+
}
|
|
240
|
+
const scale = 1 / (3 * twiceArea);
|
|
241
|
+
return { x: x * scale, y: y * scale };
|
|
242
|
+
}
|
|
243
|
+
function labelPoint(polygon, opts = {}) {
|
|
244
|
+
if (polygon.length < 3) return centroid(polygon);
|
|
245
|
+
const box = polygonBounds(polygon);
|
|
246
|
+
if (opts.bias === "top") {
|
|
247
|
+
const lines = [];
|
|
248
|
+
for (let i = 1; i <= 8; i++) {
|
|
249
|
+
const y2 = box.y + box.height * i / 12;
|
|
250
|
+
const run2 = widestRun(polygon, y2);
|
|
251
|
+
if (run2 && run2.width > 0) lines.push({ y: y2, run: run2 });
|
|
252
|
+
}
|
|
253
|
+
const widest = Math.max(0, ...lines.map((l) => l.run.width));
|
|
254
|
+
const pick = lines.find((l) => l.run.width >= widest * 0.7);
|
|
255
|
+
if (pick) return { x: pick.run.x, y: pick.y };
|
|
256
|
+
}
|
|
257
|
+
const y = centroid(polygon).y;
|
|
258
|
+
const run = widestRun(polygon, y);
|
|
259
|
+
if (run) return { x: run.x, y };
|
|
260
|
+
const middle = centroid(polygon);
|
|
261
|
+
if (pointInPolygon(middle, polygon)) return middle;
|
|
262
|
+
const rescue = widestRun(polygon, middle.y);
|
|
263
|
+
return rescue ? { x: rescue.x, y: middle.y } : middle;
|
|
264
|
+
}
|
|
265
|
+
function widestRun(polygon, y) {
|
|
266
|
+
const crossings = [];
|
|
267
|
+
for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
|
|
268
|
+
const a = polygon[i];
|
|
269
|
+
const b = polygon[j];
|
|
270
|
+
if (a.y > y === b.y > y) continue;
|
|
271
|
+
crossings.push(a.x + (y - a.y) / (b.y - a.y) * (b.x - a.x));
|
|
272
|
+
}
|
|
273
|
+
crossings.sort((p, q) => p - q);
|
|
274
|
+
let best;
|
|
275
|
+
for (let i = 0; i + 1 < crossings.length; i += 2) {
|
|
276
|
+
const from = crossings[i];
|
|
277
|
+
const to = crossings[i + 1];
|
|
278
|
+
if (!best || to - from > best.width) best = { x: (from + to) / 2, width: to - from };
|
|
279
|
+
}
|
|
280
|
+
return best;
|
|
281
|
+
}
|
|
282
|
+
function pointInPolygon(point, polygon) {
|
|
283
|
+
let inside = false;
|
|
284
|
+
for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
|
|
285
|
+
const a = polygon[i];
|
|
286
|
+
const b = polygon[j];
|
|
287
|
+
const straddles = a.y > point.y !== b.y > point.y;
|
|
288
|
+
if (!straddles) continue;
|
|
289
|
+
const at = a.x + (point.y - a.y) / (b.y - a.y) * (b.x - a.x);
|
|
290
|
+
if (point.x < at) inside = !inside;
|
|
291
|
+
}
|
|
292
|
+
return inside;
|
|
293
|
+
}
|
|
294
|
+
function zoneAt(point, zones) {
|
|
295
|
+
return zonesAt(point, zones)[0];
|
|
296
|
+
}
|
|
297
|
+
function zonesAt(point, zones) {
|
|
298
|
+
const order = stacked(zones);
|
|
299
|
+
const hits = [];
|
|
300
|
+
for (let i = order.length - 1; i >= 0; i--) {
|
|
301
|
+
const zone = order[i];
|
|
302
|
+
if (pointInPolygon(point, zone.polygon)) hits.push(zone.id);
|
|
303
|
+
}
|
|
304
|
+
return hits;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// src/hands.ts
|
|
308
|
+
function handBinding(hand, template, groupId) {
|
|
309
|
+
if (hand.rule !== void 0) {
|
|
310
|
+
const tag = hand.rule.bindings?.[groupId];
|
|
311
|
+
return tag === void 0 ? { kind: "rule", editable: true } : { kind: "rule", tag, editable: true };
|
|
312
|
+
}
|
|
313
|
+
if (template?.chooses?.includes(groupId)) {
|
|
314
|
+
const tag = hand.chosen?.[groupId];
|
|
315
|
+
return tag === void 0 ? { kind: "chosen", editable: true } : { kind: "chosen", tag, editable: true };
|
|
316
|
+
}
|
|
317
|
+
const fixed = template?.bindings?.[groupId];
|
|
318
|
+
if (fixed !== void 0) return { kind: "fixed", tag: fixed, editable: false };
|
|
319
|
+
return { kind: "none", editable: false };
|
|
320
|
+
}
|
|
321
|
+
function unbindHand(hand, template, groupId) {
|
|
322
|
+
const binding = handBinding(hand, template, groupId);
|
|
323
|
+
if (!binding.editable || binding.tag === void 0) return false;
|
|
324
|
+
if (binding.kind === "rule") {
|
|
325
|
+
const bindings = { ...hand.rule.bindings };
|
|
326
|
+
delete bindings[groupId];
|
|
327
|
+
hand.rule = { ...hand.rule, ...Object.keys(bindings).length > 0 ? { bindings } : {} };
|
|
328
|
+
if (Object.keys(bindings).length === 0) delete hand.rule.bindings;
|
|
329
|
+
return true;
|
|
330
|
+
}
|
|
331
|
+
const chosen = { ...hand.chosen };
|
|
332
|
+
delete chosen[groupId];
|
|
333
|
+
if (Object.keys(chosen).length > 0) hand.chosen = chosen;
|
|
334
|
+
else delete hand.chosen;
|
|
335
|
+
return true;
|
|
336
|
+
}
|
|
337
|
+
function bindHand(hand, template, groupId, tagId) {
|
|
338
|
+
const binding = handBinding(hand, template, groupId);
|
|
339
|
+
if (!binding.editable || binding.tag === tagId) return false;
|
|
340
|
+
if (binding.kind === "rule") {
|
|
341
|
+
hand.rule = { ...hand.rule, bindings: { ...hand.rule.bindings, [groupId]: tagId } };
|
|
342
|
+
return true;
|
|
343
|
+
}
|
|
344
|
+
hand.chosen = { ...hand.chosen, [groupId]: tagId };
|
|
345
|
+
return true;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// src/furniture.ts
|
|
349
|
+
var isRecord2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
350
|
+
var num = (v) => typeof v === "number" && Number.isFinite(v) ? v : void 0;
|
|
351
|
+
var colourOf = (v) => typeof v === "string" && FURNITURE_COLOURS.includes(v) ? v : void 0;
|
|
352
|
+
function framesOf(canvas) {
|
|
353
|
+
const list = canvas?.frames;
|
|
354
|
+
if (!Array.isArray(list)) return [];
|
|
355
|
+
const out = [];
|
|
356
|
+
for (const entry of list) {
|
|
357
|
+
if (!isRecord2(entry)) continue;
|
|
358
|
+
const id = entry["id"];
|
|
359
|
+
const x = num(entry["x"]), y = num(entry["y"]);
|
|
360
|
+
const w = num(entry["w"]), h = num(entry["h"]);
|
|
361
|
+
if (typeof id !== "string" || id === "") continue;
|
|
362
|
+
if (x === void 0 || y === void 0 || w === void 0 || h === void 0) continue;
|
|
363
|
+
if (w <= 0 || h <= 0) continue;
|
|
364
|
+
const title = entry["title"];
|
|
365
|
+
const colour = colourOf(entry["colour"]);
|
|
366
|
+
const z = num(entry["z"]);
|
|
367
|
+
out.push({
|
|
368
|
+
id,
|
|
369
|
+
x,
|
|
370
|
+
y,
|
|
371
|
+
w,
|
|
372
|
+
h,
|
|
373
|
+
...typeof title === "string" && title !== "" ? { title } : {},
|
|
374
|
+
...colour !== void 0 ? { colour } : {},
|
|
375
|
+
...z !== void 0 ? { z } : {}
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
return stacked(out);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// src/comments.ts
|
|
382
|
+
var isRecord3 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
383
|
+
function markOf(thread) {
|
|
384
|
+
const mark = thread.mark;
|
|
385
|
+
if (!mark) return void 0;
|
|
386
|
+
return {
|
|
387
|
+
canvas: mark.canvas,
|
|
388
|
+
x: mark.x,
|
|
389
|
+
y: mark.y,
|
|
390
|
+
...thread.anchor === mark.canvas ? {} : { item: thread.anchor }
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
var marksOn = (shard, canvas) => commentsOf(shard).filter((c) => c.mark?.canvas === canvas);
|
|
394
|
+
function markIn(value) {
|
|
395
|
+
if (!isRecord3(value)) return void 0;
|
|
396
|
+
const canvas = value["canvas"], x = value["x"], y = value["y"];
|
|
397
|
+
if (typeof canvas !== "string" || canvas === "") return void 0;
|
|
398
|
+
if (typeof x !== "number" || !Number.isFinite(x)) return void 0;
|
|
399
|
+
if (typeof y !== "number" || !Number.isFinite(y)) return void 0;
|
|
400
|
+
return { mark: { canvas, x, y } };
|
|
401
|
+
}
|
|
402
|
+
function commentsOf(shard) {
|
|
403
|
+
const list = shard?.comments;
|
|
404
|
+
if (!Array.isArray(list)) return [];
|
|
405
|
+
const out = [];
|
|
406
|
+
for (const entry of list) {
|
|
407
|
+
if (!isRecord3(entry)) continue;
|
|
408
|
+
const id = entry["id"], anchor = entry["anchor"];
|
|
409
|
+
if (typeof id !== "string" || id === "" || typeof anchor !== "string" || anchor === "") continue;
|
|
410
|
+
const raw = Array.isArray(entry["messages"]) ? entry["messages"] : [];
|
|
411
|
+
const messages = [];
|
|
412
|
+
for (const m of raw) {
|
|
413
|
+
if (!isRecord3(m)) continue;
|
|
414
|
+
const body = m["body"];
|
|
415
|
+
if (typeof body !== "string") continue;
|
|
416
|
+
const deleted = m["deleted"] === true;
|
|
417
|
+
if (!deleted && body.trim() === "") continue;
|
|
418
|
+
messages.push({
|
|
419
|
+
author: typeof m["author"] === "string" ? m["author"] : "",
|
|
420
|
+
ts: typeof m["ts"] === "string" ? m["ts"] : "",
|
|
421
|
+
body: deleted ? "" : body,
|
|
422
|
+
...deleted ? { deleted: true } : {}
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
if (messages.length === 0) continue;
|
|
426
|
+
out.push({
|
|
427
|
+
id,
|
|
428
|
+
anchor,
|
|
429
|
+
...entry["resolved"] === true ? { resolved: true } : {},
|
|
430
|
+
...markIn(entry["mark"]) ?? {},
|
|
431
|
+
messages
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
return out;
|
|
435
|
+
}
|
|
436
|
+
var threadsFor = (shard, anchor) => commentsOf(shard).filter((c) => c.anchor === anchor);
|
|
437
|
+
function openThreadCounts(shard) {
|
|
438
|
+
const out = {};
|
|
439
|
+
for (const thread of commentsOf(shard)) {
|
|
440
|
+
if (thread.resolved === true) continue;
|
|
441
|
+
out[thread.anchor] = (out[thread.anchor] ?? 0) + 1;
|
|
442
|
+
}
|
|
443
|
+
return out;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// src/infer.ts
|
|
447
|
+
function inferDeclFromWrite(src) {
|
|
448
|
+
const s = src.trim();
|
|
449
|
+
if (s === "true" || s === "false") return { type: "boolean", default: false };
|
|
450
|
+
if (/^-?\d+(\.\d+)?$/.test(s)) return { type: "number", default: 0 };
|
|
451
|
+
if (/^"[^"]*"$/.test(s) || /^'[^']*'$/.test(s)) return { type: "string", default: "" };
|
|
452
|
+
if (/^(set_flags|clear_flags)\s*\(/.test(s)) return { type: "flags", default: [] };
|
|
453
|
+
if (/[+\-*/%]/.test(s) && !/^@[a-z]+\.[a-z0-9_-]+$/i.test(s)) return { type: "number", default: 0 };
|
|
454
|
+
return void 0;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// src/index.ts
|
|
458
|
+
function gameIdify(text) {
|
|
459
|
+
return text.toLowerCase().replace(/['’]/g, "").replace(/[^a-z0-9-]+/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
|
|
460
|
+
}
|
|
461
|
+
function isValidGameId(gameId) {
|
|
462
|
+
return /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/.test(gameId);
|
|
463
|
+
}
|
|
464
|
+
function effectiveGameId(entity) {
|
|
465
|
+
const pinned = entity.gameId?.trim();
|
|
466
|
+
if (pinned) return pinned;
|
|
467
|
+
const fromTitle = entity.title ? gameIdify(entity.title) : "";
|
|
468
|
+
return fromTitle || entity.id;
|
|
469
|
+
}
|
|
470
|
+
function freeGameId(base, taken) {
|
|
471
|
+
let gameId = base;
|
|
472
|
+
for (let n = 2; taken.has(gameId); n++) gameId = `${base}-${n}`;
|
|
473
|
+
return gameId;
|
|
474
|
+
}
|
|
475
|
+
function byDisplayOrder(items) {
|
|
476
|
+
return items.map((x, i) => ({ x, key: x.order ?? i, i })).sort((a, b) => a.key - b.key || ((a.x.id ?? "") < (b.x.id ?? "") ? -1 : (a.x.id ?? "") > (b.x.id ?? "") ? 1 : a.i - b.i)).map((e) => e.x);
|
|
477
|
+
}
|
|
478
|
+
function freeTitle(base, taken) {
|
|
479
|
+
let title = base;
|
|
480
|
+
for (let n = 2; taken.has(gameIdify(title)); n++) title = `${base} ${n}`;
|
|
481
|
+
return title;
|
|
482
|
+
}
|
|
483
|
+
var PLACE_GROUP = "place";
|
|
484
|
+
var BUNDLE_SCHEMA = "storylets/bundle@0";
|
|
485
|
+
var SAVE_SCHEMA = "storylets/save@1";
|
|
486
|
+
var SAVEFILE_SCHEMA = "storylets/savefile@1";
|
|
487
|
+
var PROJECT_FOLDER_EXTENSION = ".storylets";
|
|
488
|
+
var BUNDLE_EXTENSION = ".storyletsc";
|
|
489
|
+
var bundleAssetPath = (boxGameId, file) => `assets/${boxGameId}/${file}`;
|
|
490
|
+
var SHARD_EXTENSIONS = {
|
|
491
|
+
project: ".storyletproj",
|
|
492
|
+
box: ".storyletbox",
|
|
493
|
+
tags: ".storylettags",
|
|
494
|
+
hands: ".storylethands",
|
|
495
|
+
deck: ".storyletdeck",
|
|
496
|
+
/** The arrangement layer: where things SIT, never what they are. Its own shard
|
|
497
|
+
* because positions churn (an afternoon of tidying a canvas touches every
|
|
498
|
+
* card) and content does not, so a designer arranging and a writer editing
|
|
499
|
+
* never collide on one file (design/graphical-views.md section 1.2). */
|
|
500
|
+
view: ".storyletview",
|
|
501
|
+
/** Threaded comments: content-ADJACENT, so neither in a content shard (a
|
|
502
|
+
* writer's deck edit must not conflict with a reviewer's comment) nor in the
|
|
503
|
+
* arrangement sidecar (this is not where anything sits). One per box,
|
|
504
|
+
* id-keyed (design/annotation.md). Documentation NOTES used to share this
|
|
505
|
+
* file and were retired: `purpose` already says why a thing exists, and
|
|
506
|
+
* Patterpad's typed routing has no destination here. */
|
|
507
|
+
notes: ".storyletnotes"
|
|
508
|
+
};
|
|
509
|
+
var PROJECT_SCHEMA = "storylets/project@0";
|
|
510
|
+
var BOX_SCHEMA = "storylets/box@0";
|
|
511
|
+
var TAGS_SCHEMA = "storylets/tags@0";
|
|
512
|
+
var HANDS_SCHEMA = "storylets/hands@0";
|
|
513
|
+
var DECK_SCHEMA = "storylets/deck@0";
|
|
514
|
+
var VIEW_SCHEMA = "storylets/view@0";
|
|
515
|
+
var NOTES_SCHEMA = "storylets/notes@0";
|
|
516
|
+
var FURNITURE_COLOURS = ["paper", "amber", "sage", "sky", "rose", "slate"];
|
|
517
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
518
|
+
0 && (module.exports = {
|
|
519
|
+
BOX_SCHEMA,
|
|
520
|
+
BUNDLE_EXTENSION,
|
|
521
|
+
BUNDLE_SCHEMA,
|
|
522
|
+
DECK_SCHEMA,
|
|
523
|
+
FURNITURE_COLOURS,
|
|
524
|
+
HANDS_SCHEMA,
|
|
525
|
+
NOTES_SCHEMA,
|
|
526
|
+
PLACE_GROUP,
|
|
527
|
+
PROJECT_FOLDER_EXTENSION,
|
|
528
|
+
PROJECT_SCHEMA,
|
|
529
|
+
RESERVED_PROPERTY_NAMES,
|
|
530
|
+
SAVEFILE_SCHEMA,
|
|
531
|
+
SAVE_SCHEMA,
|
|
532
|
+
SHARD_EXTENSIONS,
|
|
533
|
+
SPATIAL,
|
|
534
|
+
TAGS_SCHEMA,
|
|
535
|
+
VIEW_SCHEMA,
|
|
536
|
+
backgroundsOf,
|
|
537
|
+
bindHand,
|
|
538
|
+
bundleAssetPath,
|
|
539
|
+
byDisplayOrder,
|
|
540
|
+
centroid,
|
|
541
|
+
commentsOf,
|
|
542
|
+
droppedRect,
|
|
543
|
+
effectiveGameId,
|
|
544
|
+
framesOf,
|
|
545
|
+
freeGameId,
|
|
546
|
+
freeTitle,
|
|
547
|
+
gameIdify,
|
|
548
|
+
handBinding,
|
|
549
|
+
inferDeclFromWrite,
|
|
550
|
+
isCaseOnlyPropertyName,
|
|
551
|
+
isSpatial,
|
|
552
|
+
isValidGameId,
|
|
553
|
+
isValidPropertyName,
|
|
554
|
+
labelPoint,
|
|
555
|
+
markOf,
|
|
556
|
+
marksOn,
|
|
557
|
+
openThreadCounts,
|
|
558
|
+
pointInPolygon,
|
|
559
|
+
polygonBounds,
|
|
560
|
+
polygonOf,
|
|
561
|
+
propertyNameify,
|
|
562
|
+
restack,
|
|
563
|
+
spatialOf,
|
|
564
|
+
stacked,
|
|
565
|
+
threadsFor,
|
|
566
|
+
unbindHand,
|
|
567
|
+
withBackgrounds,
|
|
568
|
+
withPolygon,
|
|
569
|
+
withSpatialGroup,
|
|
570
|
+
withZ,
|
|
571
|
+
zOf,
|
|
572
|
+
zoneAt,
|
|
573
|
+
zonesAt
|
|
574
|
+
});
|