venuekit 0.0.0-stage → 0.10.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.
@@ -0,0 +1,1311 @@
1
+ // ../core/src/labels.ts
2
+ var LATIN_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
3
+ var SPANISH_ALPHABET = "ABCDEFGHIJKLMN\xD1OPQRSTUVWXYZ";
4
+ function letters(alphabet) {
5
+ return alphabet === "es" ? SPANISH_ALPHABET : LATIN_ALPHABET;
6
+ }
7
+ function letterLabel(index, alphabet) {
8
+ if (!Number.isSafeInteger(index) || index < 0) throw new RangeError(`\xCDndice de letra inv\xE1lido: ${index}`);
9
+ const abc = letters(alphabet);
10
+ let n = index + 1;
11
+ let out = "";
12
+ while (n > 0) {
13
+ const rem = (n - 1) % abc.length;
14
+ out = abc[rem] + out;
15
+ n = Math.floor((n - 1) / abc.length);
16
+ }
17
+ return out;
18
+ }
19
+ function letterIndex(label, alphabet) {
20
+ const abc = letters(alphabet);
21
+ const upper = label.toUpperCase();
22
+ if (upper.length === 0 || [...upper].some((ch) => !abc.includes(ch))) {
23
+ throw new RangeError(`Etiqueta de letras inv\xE1lida: ${label.slice(0, 20)}`);
24
+ }
25
+ let n = 0;
26
+ for (const ch of upper) {
27
+ n = n * abc.length + abc.indexOf(ch) + 1;
28
+ if (!Number.isSafeInteger(n)) throw new RangeError("Etiqueta de letras demasiado larga");
29
+ }
30
+ return n - 1;
31
+ }
32
+ function labelSequence(rule, count) {
33
+ const out = [];
34
+ if (rule.kind === "letters") {
35
+ const skip2 = new Set((rule.skip ?? []).map((s) => s.toUpperCase()));
36
+ let i = letterIndex(rule.start, rule.alphabet);
37
+ while (out.length < count) {
38
+ const label = letterLabel(i++, rule.alphabet);
39
+ if (!skip2.has(label)) out.push(label);
40
+ }
41
+ return out;
42
+ }
43
+ const skip = new Set(rule.skip ?? []);
44
+ let n = Number(rule.start);
45
+ if (!/^\d+$/.test(rule.start) || !Number.isSafeInteger(n)) throw new RangeError(`Inicio num\xE9rico inv\xE1lido: ${rule.start.slice(0, 20)}`);
46
+ const pattern = rule.pattern ?? "sequential";
47
+ const step = pattern === "odd" || pattern === "even" ? 2 : 1;
48
+ if (pattern === "odd" && n % 2 === 0 || pattern === "even" && n % 2 === 1) n++;
49
+ while (out.length < count) {
50
+ if (!Number.isSafeInteger(n)) throw new RangeError("La numeraci\xF3n pasa el rango seguro");
51
+ const label = String(n);
52
+ n += step;
53
+ if (!skip.has(label)) out.push(label);
54
+ }
55
+ return out;
56
+ }
57
+ function fromCentre(sequence) {
58
+ const out = new Array(sequence.length);
59
+ let right = Math.floor(sequence.length / 2);
60
+ let left = right - 1;
61
+ sequence.forEach((label, k) => {
62
+ if (k % 2 === 0) out[right++] = label;
63
+ else out[left--] = label;
64
+ });
65
+ return out;
66
+ }
67
+ function expandLabels(rule, count) {
68
+ const sequence = labelSequence(rule, count);
69
+ const out = rule.kind === "numbers" && rule.pattern === "center" ? fromCentre(sequence) : sequence;
70
+ return rule.direction === "rtl" ? out.reverse() : out;
71
+ }
72
+
73
+ // ../core/src/ids.ts
74
+ var ID_SEP = ":";
75
+ function seatUnitId(blockId, rowLabel, seatLabel) {
76
+ return `${blockId}${ID_SEP}${rowLabel}${ID_SEP}${seatLabel}`;
77
+ }
78
+ function placeUnitId(tableId, n) {
79
+ return `${tableId}${ID_SEP}${n}`;
80
+ }
81
+ function parseUnitId(id) {
82
+ return id.split(ID_SEP);
83
+ }
84
+
85
+ // ../core/src/landmarks.ts
86
+ var landmarks = [
87
+ // Accesos y circulación
88
+ { id: "entrance", name: "Ingreso", group: "access", width: 140, height: 60 },
89
+ { id: "exit", name: "Salida", group: "access", width: 140, height: 60 },
90
+ {
91
+ id: "emergency-exit",
92
+ name: "Salida de emergencia",
93
+ group: "access",
94
+ width: 140,
95
+ height: 60
96
+ },
97
+ {
98
+ id: "vehicle-entrance",
99
+ name: "Ingreso vehicular",
100
+ group: "access",
101
+ width: 180,
102
+ height: 70
103
+ },
104
+ {
105
+ id: "box-office",
106
+ name: "Boleter\xEDa",
107
+ group: "access",
108
+ width: 140,
109
+ height: 70
110
+ },
111
+ {
112
+ id: "cloakroom",
113
+ name: "Guardarropa",
114
+ group: "access",
115
+ width: 140,
116
+ height: 70
117
+ },
118
+ {
119
+ id: "meeting-point",
120
+ name: "Punto de encuentro",
121
+ group: "access",
122
+ width: 120,
123
+ height: 70
124
+ },
125
+ // Barras y consumo
126
+ { id: "bar", name: "Barra", group: "bar", width: 300, height: 60 },
127
+ {
128
+ id: "drinks-bar",
129
+ name: "Barra de bebidas",
130
+ group: "bar",
131
+ width: 300,
132
+ height: 60
133
+ },
134
+ {
135
+ id: "food-bar",
136
+ name: "Barra de comida",
137
+ group: "bar",
138
+ width: 300,
139
+ height: 60
140
+ },
141
+ {
142
+ id: "food-trucks",
143
+ name: "Food trucks",
144
+ group: "bar",
145
+ width: 320,
146
+ height: 90
147
+ },
148
+ {
149
+ id: "top-up",
150
+ name: "Carga de cr\xE9dito",
151
+ group: "bar",
152
+ width: 140,
153
+ height: 70
154
+ },
155
+ { id: "merch", name: "Merchandising", group: "bar", width: 160, height: 70 },
156
+ // Servicios
157
+ { id: "toilets", name: "Ba\xF1os", group: "service", width: 120, height: 80 },
158
+ {
159
+ id: "accessible-toilets",
160
+ name: "Ba\xF1os accesibles",
161
+ group: "service",
162
+ width: 120,
163
+ height: 80
164
+ },
165
+ {
166
+ id: "first-aid",
167
+ name: "Primeros auxilios",
168
+ group: "service",
169
+ width: 130,
170
+ height: 80
171
+ },
172
+ {
173
+ id: "security",
174
+ name: "Seguridad",
175
+ group: "service",
176
+ width: 120,
177
+ height: 70
178
+ },
179
+ {
180
+ id: "smoking-area",
181
+ name: "Sector fumadores",
182
+ group: "service",
183
+ width: 200,
184
+ height: 120
185
+ },
186
+ // Escenario y producción
187
+ { id: "stage", name: "Escenario", group: "stage", width: 400, height: 70 },
188
+ {
189
+ id: "second-stage",
190
+ name: "Escenario secundario",
191
+ group: "stage",
192
+ width: 260,
193
+ height: 60
194
+ },
195
+ { id: "dj", name: "DJ", group: "stage", width: 200, height: 60 },
196
+ {
197
+ id: "sound-desk",
198
+ name: "Consola de sonido",
199
+ group: "stage",
200
+ width: 140,
201
+ height: 80
202
+ },
203
+ { id: "screen", name: "Pantalla", group: "stage", width: 240, height: 40 },
204
+ {
205
+ id: "dressing-rooms",
206
+ name: "Camarines",
207
+ group: "stage",
208
+ width: 180,
209
+ height: 100
210
+ },
211
+ // Espacios
212
+ {
213
+ id: "dance-floor",
214
+ name: "Pista de baile",
215
+ group: "space",
216
+ width: 400,
217
+ height: 260
218
+ },
219
+ {
220
+ id: "accessible-area",
221
+ name: "Sector accesible",
222
+ group: "space",
223
+ width: 200,
224
+ height: 120
225
+ },
226
+ { id: "tent", name: "Carpa", group: "space", width: 260, height: 180 },
227
+ { id: "pool", name: "Pileta", group: "space", width: 300, height: 180 },
228
+ {
229
+ id: "parking",
230
+ name: "Estacionamiento",
231
+ group: "space",
232
+ width: 360,
233
+ height: 220
234
+ }
235
+ ];
236
+ function landmarkById(id) {
237
+ return landmarks.find((l) => l.id === id) ?? null;
238
+ }
239
+
240
+ // ../core/src/schema.ts
241
+ import { z } from "zod";
242
+ var MAX_UNITS = 1e5;
243
+ var MAX_ID_LENGTH = 64;
244
+ var MAX_NAME_LENGTH = 120;
245
+ var MAX_REF_LENGTH = 120;
246
+ var MAX_TABLE_LABEL_LENGTH = 20;
247
+ var MAX_SRC_LENGTH = 4096;
248
+ var MAX_ELEMENTS = 5e3;
249
+ var MAX_LETTERS_START = 3;
250
+ var MAX_SKIP = 100;
251
+ var MIN_SCALE = 0.4;
252
+ var MAX_SCALE = 2;
253
+ var idSchema = z.string().max(MAX_ID_LENGTH).regex(/^[A-Za-z0-9_-]+$/, "El id s\xF3lo admite letras, n\xFAmeros, _ y -");
254
+ var rotationSchema = z.union([z.literal(0), z.literal(90), z.literal(180), z.literal(270)]);
255
+ var transformSchema = z.object({
256
+ x: z.number(),
257
+ y: z.number(),
258
+ rotation: rotationSchema.default(0)
259
+ });
260
+ var LETTERS_RE = /^[A-Za-z]{1,3}$/;
261
+ var LETTERS_ES_RE = /^[A-Za-zÑñ]{1,3}$/;
262
+ var NUMBERS_RE = /^\d{1,9}$/;
263
+ var labelRuleSchema = z.object({
264
+ kind: z.enum(["letters", "numbers"]),
265
+ start: z.string().min(1),
266
+ direction: z.enum(["ltr", "rtl"]),
267
+ skip: z.array(z.string().max(9)).max(MAX_SKIP).optional(),
268
+ /** "es" suma la Ñ después de la N. */
269
+ alphabet: z.enum(["latin", "es"]).optional(),
270
+ /** Seguidas, sólo impares, sólo pares o desde el centro (ver `expandLabels`). */
271
+ pattern: z.enum(["sequential", "odd", "even", "center"]).optional()
272
+ }).superRefine((rule, ctx) => {
273
+ const re = rule.kind === "numbers" ? NUMBERS_RE : rule.alphabet === "es" ? LETTERS_ES_RE : LETTERS_RE;
274
+ const lettersHint = rule.alphabet === "es" ? "letras A-Z o \xD1" : "letras A-Z";
275
+ const hint = rule.kind === "letters" ? `Con kind "letters", start debe ser s\xF3lo ${lettersHint} (hasta ${MAX_LETTERS_START})` : `Con kind "numbers", start debe ser s\xF3lo d\xEDgitos (hasta 9)`;
276
+ if (!re.test(rule.start)) {
277
+ ctx.addIssue({ code: "custom", path: ["start"], message: hint });
278
+ }
279
+ if (rule.kind === "letters" && rule.pattern !== void 0 && rule.pattern !== "sequential") {
280
+ ctx.addIssue({ code: "custom", path: ["pattern"], message: `Con kind "letters" la numeraci\xF3n s\xF3lo puede ser "sequential"` });
281
+ }
282
+ if (rule.kind === "numbers" && rule.alphabet !== void 0) {
283
+ ctx.addIssue({ code: "custom", path: ["alphabet"], message: `alphabet s\xF3lo se usa con kind "letters"` });
284
+ }
285
+ rule.skip?.forEach((s, i) => {
286
+ if (!re.test(s)) {
287
+ ctx.addIssue({
288
+ code: "custom",
289
+ path: ["skip", i],
290
+ message: rule.kind === "letters" ? `Con kind "letters", skip debe contener s\xF3lo ${lettersHint} (hasta ${MAX_LETTERS_START})` : `Con kind "numbers", skip debe contener s\xF3lo d\xEDgitos (hasta 9)`
291
+ });
292
+ }
293
+ });
294
+ });
295
+ var placeTypeLinkSchema = z.object({
296
+ ref: z.string().max(MAX_REF_LENGTH),
297
+ name: z.string().max(MAX_NAME_LENGTH)
298
+ });
299
+ var seatBlockSchema = z.object({
300
+ id: idSchema,
301
+ type: z.literal("seatBlock"),
302
+ transform: transformSchema,
303
+ rows: z.number().int().min(1).max(500),
304
+ cols: z.number().int().min(1).max(1e3),
305
+ rowLabels: labelRuleSchema,
306
+ seatLabels: labelRuleSchema,
307
+ spacing: z.object({ x: z.number().positive(), y: z.number().positive() }),
308
+ curve: z.number().min(0).max(180).default(0),
309
+ scale: z.number().min(MIN_SCALE).max(MAX_SCALE).default(1),
310
+ removed: z.array(z.string().max(MAX_ID_LENGTH * 2)).max(MAX_UNITS).default([]),
311
+ placeType: placeTypeLinkSchema.nullable().default(null)
312
+ });
313
+ var tableSchema = z.object({
314
+ id: idSchema,
315
+ type: z.literal("table"),
316
+ transform: transformSchema,
317
+ shape: z.enum(["round", "rect"]),
318
+ places: z.number().int().min(1).max(64),
319
+ label: z.string().min(1).max(MAX_TABLE_LABEL_LENGTH),
320
+ mode: z.enum(["whole", "perPlace"]),
321
+ scale: z.number().min(MIN_SCALE).max(MAX_SCALE).default(1),
322
+ placeType: placeTypeLinkSchema.nullable().default(null)
323
+ });
324
+ var landmarkSchema = z.object({
325
+ id: idSchema,
326
+ type: z.literal("landmark"),
327
+ transform: transformSchema,
328
+ kind: z.string().refine((k) => landmarkById(k) !== null, "El tipo de referencia no existe en el cat\xE1logo"),
329
+ name: z.string().min(1).max(60),
330
+ width: z.number().positive(),
331
+ height: z.number().positive()
332
+ });
333
+ var zoneSchema = z.object({
334
+ id: idSchema,
335
+ type: z.literal("zone"),
336
+ transform: transformSchema,
337
+ width: z.number().positive(),
338
+ height: z.number().positive(),
339
+ /** Grados de arco, como `seatBlock.curve`: con curva la zona es una banda (tribuna curva). */
340
+ curve: z.number().min(0).max(180).default(0),
341
+ placeType: placeTypeLinkSchema.nullable().default(null)
342
+ });
343
+ var elementSchema = z.discriminatedUnion("type", [seatBlockSchema, tableSchema, landmarkSchema, zoneSchema]);
344
+ var backgroundSchema = z.object({
345
+ src: z.string().min(1).max(MAX_SRC_LENGTH),
346
+ x: z.number(),
347
+ y: z.number(),
348
+ width: z.number().positive(),
349
+ height: z.number().positive(),
350
+ opacity: z.number().min(0.1).max(1),
351
+ locked: z.boolean()
352
+ });
353
+ var metaSchema = z.object({
354
+ id: idSchema,
355
+ name: z.string().max(MAX_NAME_LENGTH),
356
+ version: z.literal(2),
357
+ width: z.number().positive(),
358
+ height: z.number().positive(),
359
+ background: backgroundSchema.nullable().default(null)
360
+ });
361
+ var layoutSchema = z.object({
362
+ meta: metaSchema,
363
+ elements: z.array(elementSchema).max(MAX_ELEMENTS)
364
+ }).superRefine((layout, ctx) => {
365
+ const elementIds = /* @__PURE__ */ new Set();
366
+ let totalUnits = 0;
367
+ for (const e of layout.elements) {
368
+ if (e.type === "table") totalUnits += e.places;
369
+ else if (e.type === "seatBlock") totalUnits += e.rows * e.cols;
370
+ }
371
+ const overLimit = totalUnits > MAX_UNITS;
372
+ if (overLimit) {
373
+ ctx.addIssue({
374
+ code: "custom",
375
+ path: ["elements"],
376
+ message: `El documento supera el m\xE1ximo de unidades vendibles (${totalUnits} > ${MAX_UNITS})`
377
+ });
378
+ }
379
+ layout.elements.forEach((e, i) => {
380
+ if (elementIds.has(e.id)) {
381
+ ctx.addIssue({ code: "custom", path: ["elements", i, "id"], message: `Id de elemento duplicado: ${e.id}` });
382
+ }
383
+ elementIds.add(e.id);
384
+ if (e.type !== "seatBlock" || e.removed.length === 0 || overLimit) return;
385
+ if (!labelRuleSchema.safeParse(e.rowLabels).success || !labelRuleSchema.safeParse(e.seatLabels).success) return;
386
+ const rows = new Set(expandLabels(e.rowLabels, e.rows));
387
+ const seats = new Set(expandLabels(e.seatLabels, e.cols));
388
+ e.removed.forEach((id, j) => {
389
+ const [blockId, row, seat, ...extra] = parseUnitId(id);
390
+ const ok = blockId === e.id && extra.length === 0 && row !== void 0 && seat !== void 0 && rows.has(row) && seats.has(seat);
391
+ if (!ok) {
392
+ ctx.addIssue({ code: "custom", path: ["elements", i, "removed", j], message: `El bloque ${e.id} no genera la butaca ${id}` });
393
+ }
394
+ });
395
+ });
396
+ });
397
+ var spanishParseOptions = { error: z.locales.es().localeError };
398
+
399
+ // ../core/src/geometry.ts
400
+ var DEG = Math.PI / 180;
401
+ function applyTransform(p, t) {
402
+ const r = t.rotation * DEG;
403
+ if (r === 0) return { x: p.x + t.x, y: p.y + t.y };
404
+ const cos = Math.cos(r);
405
+ const sin = Math.sin(r);
406
+ return { x: p.x * cos - p.y * sin + t.x, y: p.x * sin + p.y * cos + t.y };
407
+ }
408
+ function bbox(points) {
409
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
410
+ for (const p of points) {
411
+ if (p.x < minX) minX = p.x;
412
+ if (p.y < minY) minY = p.y;
413
+ if (p.x > maxX) maxX = p.x;
414
+ if (p.y > maxY) maxY = p.y;
415
+ }
416
+ return { minX, minY, maxX, maxY };
417
+ }
418
+ function pointInPolygon(p, polygon) {
419
+ let inside = false;
420
+ for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
421
+ const [xi, yi] = polygon[i];
422
+ const [xj, yj] = polygon[j];
423
+ const crosses = yi > p.y !== yj > p.y && p.x < (xj - xi) * (p.y - yi) / (yj - yi) + xi;
424
+ if (crosses) inside = !inside;
425
+ }
426
+ return inside;
427
+ }
428
+ function distance(a, b) {
429
+ return Math.hypot(a.x - b.x, a.y - b.y);
430
+ }
431
+
432
+ // ../core/src/bounds.ts
433
+ function placeTypeBounds(_layout, units, ref) {
434
+ const points = units.filter((u) => u.placeTypeRef === ref).map((u) => ({ x: u.x, y: u.y }));
435
+ if (points.length === 0) return null;
436
+ return bbox(points);
437
+ }
438
+
439
+ // ../core/src/zone-shape.ts
440
+ var DEG2 = Math.PI / 180;
441
+ function zoneArc(zone) {
442
+ const curve = zone.curve ?? 0;
443
+ if (curve <= 0) return null;
444
+ const theta = curve * DEG2;
445
+ const mid = zone.width / theta;
446
+ return { half: theta / 2, mid, inner: Math.max(0, mid - zone.height / 2), outer: mid + zone.height / 2 };
447
+ }
448
+ function polar(arc, r, phi) {
449
+ return { x: r * Math.sin(phi), y: -arc.mid + r * Math.cos(phi) };
450
+ }
451
+ function localBox(zone) {
452
+ const arc = zoneArc(zone);
453
+ if (!arc) return { minX: -zone.width / 2, minY: -zone.height / 2, maxX: zone.width / 2, maxY: zone.height / 2 };
454
+ const hw = arc.half >= Math.PI / 2 ? arc.outer : arc.outer * Math.sin(arc.half);
455
+ const top = -arc.mid + arc.inner * Math.cos(arc.half);
456
+ return { minX: -hw, minY: top, maxX: hw, maxY: -arc.mid + arc.outer };
457
+ }
458
+ function zoneLocalSize(zone) {
459
+ const b = localBox(zone);
460
+ return { width: b.maxX - b.minX, height: b.maxY - b.minY };
461
+ }
462
+ function zoneBounds(zone) {
463
+ const b = localBox(zone);
464
+ const corners = [
465
+ { x: b.minX, y: b.minY },
466
+ { x: b.maxX, y: b.minY },
467
+ { x: b.maxX, y: b.maxY },
468
+ { x: b.minX, y: b.maxY }
469
+ ].map((p) => applyTransform(p, zone.transform));
470
+ return {
471
+ minX: Math.min(...corners.map((p) => p.x)),
472
+ minY: Math.min(...corners.map((p) => p.y)),
473
+ maxX: Math.max(...corners.map((p) => p.x)),
474
+ maxY: Math.max(...corners.map((p) => p.y))
475
+ };
476
+ }
477
+ function pointInZone(zone, p) {
478
+ const back = (360 - zone.transform.rotation) % 360;
479
+ const local = applyTransform({ x: p.x - zone.transform.x, y: p.y - zone.transform.y }, { x: 0, y: 0, rotation: back });
480
+ const arc = zoneArc(zone);
481
+ if (!arc) return Math.abs(local.x) <= zone.width / 2 && Math.abs(local.y) <= zone.height / 2;
482
+ const dx = local.x;
483
+ const dy = local.y + arc.mid;
484
+ const r = Math.hypot(dx, dy);
485
+ if (r < arc.inner || r > arc.outer) return false;
486
+ return Math.abs(Math.atan2(dx, dy)) <= arc.half + 1e-9;
487
+ }
488
+ function zoneOutline(zone, corner = 0) {
489
+ const arc = zoneArc(zone);
490
+ if (!arc) return [];
491
+ const steps2 = Math.max(8, Math.ceil(arc.half * 2 / (3 * DEG2)));
492
+ const r = Math.max(0, Math.min(corner, zone.height / 3, arc.outer * arc.half * 0.3));
493
+ const out = [];
494
+ const arcPoints = (radius, from, to) => {
495
+ for (let i = 0; i <= steps2; i++) out.push(polar(arc, radius, from + (to - from) * i / steps2));
496
+ };
497
+ const fillet = (a, control, b) => {
498
+ for (let i = 1; i < 4; i++) {
499
+ const t = i / 4;
500
+ const u = 1 - t;
501
+ out.push({ x: u * u * a.x + 2 * u * t * control.x + t * t * b.x, y: u * u * a.y + 2 * u * t * control.y + t * t * b.y });
502
+ }
503
+ };
504
+ const dOut = r / arc.outer;
505
+ const hasInner = arc.inner > r * 2;
506
+ const dIn = hasInner ? r / arc.inner : 0;
507
+ arcPoints(arc.outer, -arc.half + dOut, arc.half - dOut);
508
+ fillet(polar(arc, arc.outer, arc.half - dOut), polar(arc, arc.outer, arc.half), polar(arc, arc.outer - r, arc.half));
509
+ if (hasInner) {
510
+ out.push(polar(arc, arc.outer - r, arc.half), polar(arc, arc.inner + r, arc.half));
511
+ fillet(polar(arc, arc.inner + r, arc.half), polar(arc, arc.inner, arc.half), polar(arc, arc.inner, arc.half - dIn));
512
+ arcPoints(arc.inner, arc.half - dIn, -arc.half + dIn);
513
+ fillet(polar(arc, arc.inner, -arc.half + dIn), polar(arc, arc.inner, -arc.half), polar(arc, arc.inner + r, -arc.half));
514
+ out.push(polar(arc, arc.outer - r, -arc.half));
515
+ } else {
516
+ out.push(polar(arc, arc.outer - r, arc.half));
517
+ if (arc.inner > 0) arcPoints(arc.inner, arc.half, -arc.half);
518
+ else out.push(polar(arc, 0, 0));
519
+ out.push(polar(arc, arc.outer - r, -arc.half));
520
+ }
521
+ fillet(polar(arc, arc.outer - r, -arc.half), polar(arc, arc.outer, -arc.half), polar(arc, arc.outer, -arc.half + dOut));
522
+ return out.map((p) => applyTransform(p, zone.transform));
523
+ }
524
+ function zoneLabelSize(zone) {
525
+ const arc = zoneArc(zone);
526
+ if (!arc) return { width: zone.width, height: zone.height };
527
+ const byRadius = Math.sqrt(Math.max(0, arc.outer * arc.outer - arc.mid * arc.mid));
528
+ const byAngle = arc.half >= Math.PI / 2 ? Infinity : arc.mid * Math.tan(arc.half);
529
+ return { width: 2 * Math.min(byRadius, byAngle), height: arc.outer - arc.inner };
530
+ }
531
+ function zoneWithLocalSize(zone, width, height) {
532
+ const arc = zoneArc(zone);
533
+ if (!arc) return { ...zone, width, height };
534
+ const theta = arc.half * 2;
535
+ const sin = arc.half >= Math.PI / 2 ? 1 : Math.sin(arc.half);
536
+ const cos = Math.max(0, Math.cos(arc.half));
537
+ const outer = width / (2 * sin);
538
+ let inner;
539
+ if (cos > 1e-6) inner = (outer - height) / cos;
540
+ else inner = outer - (arc.outer - arc.inner) * (outer / arc.outer);
541
+ inner = Math.max(0, Math.min(inner, outer - 1));
542
+ const thickness = outer - inner;
543
+ const mid = inner === 0 ? outer / 2 : (outer + inner) / 2;
544
+ return { ...zone, width: mid * theta, height: inner === 0 ? outer : thickness };
545
+ }
546
+
547
+ // ../core/src/expand.ts
548
+ var DEG3 = Math.PI / 180;
549
+ function localSeatPosition(block, r, c) {
550
+ const curve = block.curve;
551
+ const sx = block.spacing.x * (block.scale ?? 1);
552
+ const sy = block.spacing.y * (block.scale ?? 1);
553
+ if (curve === 0 || block.cols === 1) {
554
+ return { x: c * sx, y: r * sy };
555
+ }
556
+ const theta = curve * DEG3;
557
+ const arcLen = (block.cols - 1) * sx;
558
+ const r0 = arcLen / theta;
559
+ const radius = r0 + r * sy;
560
+ const angle = -theta / 2 + theta * c / (block.cols - 1);
561
+ return { x: arcLen / 2 + radius * Math.sin(angle), y: -r0 + radius * Math.cos(angle) };
562
+ }
563
+ function expandSeatBlock(block) {
564
+ const rowLabels = expandLabels(block.rowLabels, block.rows);
565
+ const seatLabels = expandLabels(block.seatLabels, block.cols);
566
+ const removed = new Set(block.removed);
567
+ const out = [];
568
+ for (let r = 0; r < block.rows; r++) {
569
+ const row = rowLabels[r];
570
+ for (let c = 0; c < block.cols; c++) {
571
+ const seat = seatLabels[c];
572
+ const id = seatUnitId(block.id, row, seat);
573
+ if (removed.has(id)) continue;
574
+ const p = applyTransform(localSeatPosition(block, r, c), block.transform);
575
+ out.push({
576
+ id,
577
+ label: `${row}${seat}`,
578
+ kind: "seat",
579
+ x: p.x,
580
+ y: p.y,
581
+ elementId: block.id,
582
+ placeTypeRef: block.placeType?.ref || null,
583
+ row,
584
+ seat,
585
+ rowIndex: r,
586
+ colIndex: c
587
+ });
588
+ }
589
+ }
590
+ return out;
591
+ }
592
+ var TABLE_RADIUS = 30;
593
+ var TABLE_WIDTH = 80;
594
+ var TABLE_HEIGHT = 40;
595
+ var PLACE_OFFSET = 10;
596
+ function localPlacePositions(table) {
597
+ const s = table.scale ?? 1;
598
+ return basePlacePositions(table).map((p) => ({ x: p.x * s, y: p.y * s }));
599
+ }
600
+ function basePlacePositions(table) {
601
+ const n = table.places;
602
+ if (table.shape === "round") {
603
+ const r = TABLE_RADIUS + PLACE_OFFSET;
604
+ return Array.from({ length: n }, (_, i) => {
605
+ const a = 2 * Math.PI * i / n - Math.PI / 2;
606
+ return { x: r * Math.cos(a), y: r * Math.sin(a) };
607
+ });
608
+ }
609
+ const top = Math.ceil(n / 2);
610
+ const bottom = n - top;
611
+ const spread = (count, y) => Array.from({ length: count }, (_, i) => ({ x: -TABLE_WIDTH / 2 + TABLE_WIDTH * (i + 0.5) / count, y }));
612
+ return [...spread(top, -TABLE_HEIGHT / 2 - PLACE_OFFSET), ...spread(bottom, TABLE_HEIGHT / 2 + PLACE_OFFSET)];
613
+ }
614
+ function expandTable(table) {
615
+ const placeTypeRef = table.placeType?.ref || null;
616
+ if (table.mode === "whole") {
617
+ const centre = applyTransform({ x: 0, y: 0 }, table.transform);
618
+ return [{ id: table.id, label: table.label, kind: "table", x: centre.x, y: centre.y, elementId: table.id, placeTypeRef, table: table.label }];
619
+ }
620
+ return localPlacePositions(table).map((local, i) => {
621
+ const p = applyTransform(local, table.transform);
622
+ const n = i + 1;
623
+ return { id: placeUnitId(table.id, n), label: `${table.label} \xB7 ${n}`, kind: "place", x: p.x, y: p.y, elementId: table.id, placeTypeRef, table: table.label, place: n };
624
+ });
625
+ }
626
+ function expand(layout) {
627
+ const out = [];
628
+ for (const el of layout.elements) {
629
+ if (el.type === "seatBlock") out.push(...expandSeatBlock(el));
630
+ else if (el.type === "table") out.push(...expandTable(el));
631
+ }
632
+ return out;
633
+ }
634
+
635
+ // ../core/src/spatial.ts
636
+ var Node = class _Node {
637
+ constructor(box, capacity) {
638
+ this.box = box;
639
+ this.capacity = capacity;
640
+ }
641
+ box;
642
+ capacity;
643
+ items = [];
644
+ children = null;
645
+ insert(item) {
646
+ if (this.children) {
647
+ this.childFor(item).insert(item);
648
+ return;
649
+ }
650
+ this.items.push(item);
651
+ if (this.items.length > this.capacity && this.canSplit()) this.split();
652
+ }
653
+ canSplit() {
654
+ return this.box.maxX - this.box.minX > 1e-6 || this.box.maxY - this.box.minY > 1e-6;
655
+ }
656
+ split() {
657
+ const { minX, minY, maxX, maxY } = this.box;
658
+ const mx = (minX + maxX) / 2;
659
+ const my = (minY + maxY) / 2;
660
+ this.children = [
661
+ new _Node({ minX, minY, maxX: mx, maxY: my }, this.capacity),
662
+ new _Node({ minX: mx, minY, maxX, maxY: my }, this.capacity),
663
+ new _Node({ minX, minY: my, maxX: mx, maxY }, this.capacity),
664
+ new _Node({ minX: mx, minY: my, maxX, maxY }, this.capacity)
665
+ ];
666
+ for (const it of this.items) this.childFor(it).items.push(it);
667
+ this.items = [];
668
+ }
669
+ childFor(p) {
670
+ const mx = (this.box.minX + this.box.maxX) / 2;
671
+ const my = (this.box.minY + this.box.maxY) / 2;
672
+ const i = (p.x >= mx ? 1 : 0) + (p.y >= my ? 2 : 0);
673
+ return this.children?.[i];
674
+ }
675
+ queryRect(q, out) {
676
+ if (q.maxX < this.box.minX || q.minX > this.box.maxX || q.maxY < this.box.minY || q.minY > this.box.maxY) return;
677
+ if (this.children) {
678
+ for (const c of this.children) c.queryRect(q, out);
679
+ return;
680
+ }
681
+ for (const it of this.items) {
682
+ if (it.x >= q.minX && it.x <= q.maxX && it.y >= q.minY && it.y <= q.maxY) out.push(it.id);
683
+ }
684
+ }
685
+ nearest(p, best) {
686
+ const dx = Math.max(this.box.minX - p.x, 0, p.x - this.box.maxX);
687
+ const dy = Math.max(this.box.minY - p.y, 0, p.y - this.box.maxY);
688
+ if (Math.hypot(dx, dy) > best.d) return;
689
+ if (this.children) {
690
+ for (const c of this.children) c.nearest(p, best);
691
+ return;
692
+ }
693
+ for (const it of this.items) {
694
+ const d = distance(p, it);
695
+ if (d < best.d) {
696
+ best.d = d;
697
+ best.id = it.id;
698
+ }
699
+ }
700
+ }
701
+ };
702
+ var SpatialIndex = class {
703
+ root;
704
+ size;
705
+ constructor(items, capacity = 16) {
706
+ this.size = items.length;
707
+ if (items.length === 0) {
708
+ this.root = null;
709
+ return;
710
+ }
711
+ const b = bbox(items);
712
+ this.root = new Node({ minX: b.minX - 1, minY: b.minY - 1, maxX: b.maxX + 1, maxY: b.maxY + 1 }, capacity);
713
+ for (const it of items) this.root.insert(it);
714
+ }
715
+ queryRect(box) {
716
+ const out = [];
717
+ this.root?.queryRect(box, out);
718
+ return out;
719
+ }
720
+ nearest(p, maxDistance) {
721
+ const best = { id: null, d: maxDistance };
722
+ this.root?.nearest(p, best);
723
+ return best.id;
724
+ }
725
+ };
726
+
727
+ // ../core/src/commands.ts
728
+ var CommandError = class extends Error {
729
+ constructor(message) {
730
+ super(message);
731
+ this.name = "CommandError";
732
+ }
733
+ };
734
+ function shift(el, dx, dy) {
735
+ return { ...el, transform: { ...el.transform, x: el.transform.x + dx, y: el.transform.y + dy } };
736
+ }
737
+ function requireElement(layout, id) {
738
+ const i = layout.elements.findIndex((e) => e.id === id);
739
+ if (i < 0) throw new CommandError(`No existe el elemento ${id}`);
740
+ return i;
741
+ }
742
+ function replaceAt(arr, i, value) {
743
+ return arr.map((v, j) => j === i ? value : v);
744
+ }
745
+ function reduce(layout, cmd) {
746
+ switch (cmd.type) {
747
+ case "addElement":
748
+ return { ...layout, elements: [...layout.elements, cmd.element] };
749
+ case "removeElement": {
750
+ const i = requireElement(layout, cmd.id);
751
+ return { ...layout, elements: layout.elements.filter((_, j) => j !== i) };
752
+ }
753
+ case "updateElement": {
754
+ const i = requireElement(layout, cmd.id);
755
+ const merged = { ...layout.elements[i], ...cmd.patch };
756
+ return { ...layout, elements: replaceAt(layout.elements, i, merged) };
757
+ }
758
+ case "moveElement": {
759
+ const i = requireElement(layout, cmd.id);
760
+ return { ...layout, elements: replaceAt(layout.elements, i, shift(layout.elements[i], cmd.dx, cmd.dy)) };
761
+ }
762
+ case "replaceElement": {
763
+ const i = requireElement(layout, cmd.element.id);
764
+ return { ...layout, elements: replaceAt(layout.elements, i, cmd.element) };
765
+ }
766
+ case "setBackground":
767
+ return { ...layout, meta: { ...layout.meta, background: cmd.background } };
768
+ case "batch":
769
+ return cmd.commands.reduce(reduce, layout);
770
+ default:
771
+ throw new CommandError(`Comando desconocido: ${cmd.type}`);
772
+ }
773
+ }
774
+ function applyCommand(layout, cmd) {
775
+ const next = reduce(layout, cmd);
776
+ const parsed = layoutSchema.safeParse(next, spanishParseOptions);
777
+ if (!parsed.success) {
778
+ const first = parsed.error.issues[0];
779
+ throw new CommandError(first ? `${first.path.join(".")}: ${first.message}` : "Documento inv\xE1lido");
780
+ }
781
+ return parsed.data;
782
+ }
783
+ var HISTORY_LIMIT = 100;
784
+ var History = class {
785
+ past = [];
786
+ future = [];
787
+ /** La clave de agrupación del último `apply`. Ver `apply`. */
788
+ lastKey = null;
789
+ present;
790
+ constructor(initial) {
791
+ this.present = initial;
792
+ }
793
+ get canUndo() {
794
+ return this.past.length > 0;
795
+ }
796
+ get canRedo() {
797
+ return this.future.length > 0;
798
+ }
799
+ /**
800
+ * Aplica un comando y lo anota en la historia.
801
+ *
802
+ * `key` agrupa: escribir un nombre en un campo son quince comandos seguidos,
803
+ * no quince pasos. Con la misma clave que el anterior, el comando pisa al
804
+ * presente sin apilar otro paso — Deshacer vuelve al nombre de antes, no
805
+ * borra una letra. Sin clave, cada comando es su propio paso, que es lo que
806
+ * corresponde a dibujar, borrar o mover.
807
+ */
808
+ apply(cmd, key) {
809
+ const next = applyCommand(this.present, cmd);
810
+ if (key !== void 0 && key === this.lastKey) {
811
+ this.future = [];
812
+ this.present = next;
813
+ return next;
814
+ }
815
+ this.past.push(this.present);
816
+ if (this.past.length > HISTORY_LIMIT) this.past.shift();
817
+ this.future = [];
818
+ this.present = next;
819
+ this.lastKey = key ?? null;
820
+ return next;
821
+ }
822
+ /**
823
+ * Reemplaza el documento entero, y se puede deshacer.
824
+ *
825
+ * Es cargar un plano: elegir de qué forma partir, o abrir uno guardado. No es
826
+ * un comando porque no describe una edición — no hay un "cambio" que aplicar
827
+ * sobre lo anterior, hay un documento nuevo.
828
+ *
829
+ * Va a la historia igual, y ahí está la gracia: alguien que dibujó media hora
830
+ * y elige otra forma por error tiene que poder volver con Deshacer.
831
+ */
832
+ load(next) {
833
+ this.lastKey = null;
834
+ this.past.push(this.present);
835
+ if (this.past.length > HISTORY_LIMIT) this.past.shift();
836
+ this.future = [];
837
+ this.present = next;
838
+ return next;
839
+ }
840
+ /** El documento al que llevaría `undo`, sin moverse. Para chequearlo antes (el candado). */
841
+ peekUndo() {
842
+ return this.past[this.past.length - 1] ?? null;
843
+ }
844
+ peekRedo() {
845
+ return this.future[this.future.length - 1] ?? null;
846
+ }
847
+ undo() {
848
+ this.lastKey = null;
849
+ const prev = this.past.pop();
850
+ if (prev) {
851
+ this.future.push(this.present);
852
+ this.present = prev;
853
+ }
854
+ return this.present;
855
+ }
856
+ redo() {
857
+ this.lastKey = null;
858
+ const next = this.future.pop();
859
+ if (next) {
860
+ this.past.push(this.present);
861
+ this.present = next;
862
+ }
863
+ return this.present;
864
+ }
865
+ /**
866
+ * Pasa todos los documentos (pasado, presente y futuro) por `fn`, sin agregar
867
+ * un paso. Es para lo que cambia por fuera del plano, como una entrada que el
868
+ * host borró: deshacer no puede devolver una pieza a un tipo que ya no existe.
869
+ */
870
+ rewrite(fn) {
871
+ this.past = this.past.map(fn);
872
+ this.future = this.future.map(fn);
873
+ this.present = fn(this.present);
874
+ return this.present;
875
+ }
876
+ };
877
+
878
+ // ../core/src/lock.ts
879
+ function samePlace(before, after) {
880
+ return after.label === before.label && after.placeTypeRef === before.placeTypeRef && after.elementId === before.elementId && after.kind === before.kind && after.rowIndex === before.rowIndex && after.colIndex === before.colIndex && after.place === before.place;
881
+ }
882
+ function findLockViolations(before, after, locked) {
883
+ if (locked.size === 0) return [];
884
+ const afterUnits = new Map(expand(after).map((u) => [u.id, u]));
885
+ const out = [];
886
+ const seen = /* @__PURE__ */ new Set();
887
+ for (const u of expand(before)) {
888
+ if (!locked.has(u.id)) continue;
889
+ const now = afterUnits.get(u.id);
890
+ if (now && samePlace(u, now)) continue;
891
+ const v = u.table !== void 0 ? { reason: "table", table: u.table } : { reason: "row", row: u.row ?? u.label, elementId: u.elementId };
892
+ const key = v.reason === "table" ? `t\0${v.table}` : `r\0${v.elementId}\0${v.row}`;
893
+ if (seen.has(key)) continue;
894
+ seen.add(key);
895
+ out.push(v);
896
+ }
897
+ return out;
898
+ }
899
+
900
+ // ../core/src/pieces.ts
901
+ var DEFAULT_BLOCK = { rows: 5, cols: 10 };
902
+ var DEFAULT_TABLE_PLACES = 6;
903
+ var DEFAULT_ZONE = { width: 240, height: 140 };
904
+ var DEFAULT_ROW_LABELS = { kind: "letters", start: "A", direction: "ltr" };
905
+ var DEFAULT_SEAT_LABELS = { kind: "numbers", start: "1", direction: "ltr" };
906
+ var BLOCK_COLS = [10, 9, 8, 12, 7, 6];
907
+ var BLOCK_COLS_MAX = 50;
908
+ var DEFAULT_SPACING = { x: 26, y: 26 };
909
+ var GAP = 20;
910
+ var PLACE_PITCH = 22;
911
+ function newElementId(layout, prefix) {
912
+ const used = new Set(layout.elements.map((e) => e.id));
913
+ for (let n = 1; ; n++) {
914
+ const id = `${prefix}-${n}`;
915
+ if (!used.has(id)) return id;
916
+ }
917
+ }
918
+ function tableSize(shape, places) {
919
+ if (shape === "round") {
920
+ const needed = PLACE_PITCH / (2 * Math.sin(Math.PI / Math.max(2, places))) - PLACE_OFFSET;
921
+ return { radius: Math.max(TABLE_RADIUS, Math.round(needed)) };
922
+ }
923
+ const perSide = Math.ceil(places / 2);
924
+ return { width: Math.max(80, perSide * PLACE_PITCH + 20), height: TABLE_HEIGHT };
925
+ }
926
+ function nextTableLabel(layout) {
927
+ const used = new Set(layout.elements.flatMap((e) => e.type === "table" ? [e.label] : []));
928
+ for (let n = 1; ; n++) if (!used.has(String(n))) return String(n);
929
+ }
930
+ function blockCols(n) {
931
+ if (n <= 60) return BLOCK_COLS.find((c) => n % c === 0) ?? 10;
932
+ const target = Math.min(BLOCK_COLS_MAX, Math.max(10, Math.round(Math.sqrt(n * 2.5))));
933
+ for (let d = 0; d <= 5; d++) {
934
+ for (const c of d === 0 ? [target] : [target - d, target + d]) {
935
+ if (c >= 10 && c <= BLOCK_COLS_MAX && n % c === 0) return c;
936
+ }
937
+ }
938
+ return target;
939
+ }
940
+ function blockShape(seats, blockId) {
941
+ const n = Math.max(1, Math.floor(seats));
942
+ if (n <= 10) return { rows: 1, cols: n, removed: [] };
943
+ const cols = blockCols(n);
944
+ if (n % cols === 0) return { rows: n / cols, cols, removed: [] };
945
+ const rows = Math.ceil(n / cols);
946
+ const lastRow = expandLabels(DEFAULT_ROW_LABELS, rows)[rows - 1];
947
+ const seatLabels = expandLabels(DEFAULT_SEAT_LABELS, cols);
948
+ const removed = seatLabels.slice(n - (rows - 1) * cols).map((seat) => seatUnitId(blockId, lastRow, seat));
949
+ return { rows, cols, removed };
950
+ }
951
+ function createPiece(kind, id, at, layout, opts = {}) {
952
+ if (kind === "seatBlock") {
953
+ const { rows, cols, removed } = opts.seats === void 0 ? { ...DEFAULT_BLOCK, removed: [] } : blockShape(opts.seats, id);
954
+ return {
955
+ id,
956
+ type: "seatBlock",
957
+ transform: { x: at.x - (cols - 1) * DEFAULT_SPACING.x / 2, y: at.y - (rows - 1) * DEFAULT_SPACING.y / 2, rotation: 0 },
958
+ rows,
959
+ cols,
960
+ rowLabels: { ...DEFAULT_ROW_LABELS },
961
+ seatLabels: { ...DEFAULT_SEAT_LABELS },
962
+ spacing: { ...DEFAULT_SPACING },
963
+ curve: 0,
964
+ scale: 1,
965
+ removed,
966
+ placeType: null
967
+ };
968
+ }
969
+ if (kind === "zone") {
970
+ return { id, type: "zone", transform: { x: at.x, y: at.y, rotation: 0 }, ...DEFAULT_ZONE, curve: 0, placeType: null };
971
+ }
972
+ if (kind === "roundTable" || kind === "rectTable") {
973
+ return {
974
+ id,
975
+ type: "table",
976
+ transform: { x: at.x, y: at.y, rotation: 0 },
977
+ shape: kind === "roundTable" ? "round" : "rect",
978
+ places: DEFAULT_TABLE_PLACES,
979
+ label: nextTableLabel(layout),
980
+ mode: "whole",
981
+ scale: 1,
982
+ placeType: null
983
+ };
984
+ }
985
+ const def = landmarkById(kind.slice("landmark:".length));
986
+ if (!def) throw new Error(`La referencia ${kind} no existe en el cat\xE1logo`);
987
+ return { id, type: "landmark", transform: { x: at.x, y: at.y, rotation: 0 }, kind: def.id, name: def.name, width: def.width, height: def.height };
988
+ }
989
+ function worldWidth(el) {
990
+ const swap = el.transform.rotation === 90 || el.transform.rotation === 270;
991
+ if (el.type === "seatBlock") {
992
+ const k = el.scale ?? 1;
993
+ const w = el.cols * el.spacing.x * k;
994
+ const h = el.rows * el.spacing.y * k;
995
+ return swap ? h : w;
996
+ }
997
+ if (el.type === "table") {
998
+ const k = el.scale ?? 1;
999
+ const s = tableSize(el.shape, el.places);
1000
+ if ("radius" in s) return 2 * (s.radius + PLACE_OFFSET) * k;
1001
+ return ((swap ? s.height : s.width) + 2 * PLACE_OFFSET) * k;
1002
+ }
1003
+ if (el.type === "zone") {
1004
+ const size = zoneLocalSize(el);
1005
+ return swap ? size.height : size.width;
1006
+ }
1007
+ return swap ? el.height : el.width;
1008
+ }
1009
+ function followingRows(rule, rows) {
1010
+ const next = expandLabels({ ...rule, direction: "ltr" }, rows + 1)[rows];
1011
+ const candidate = { ...rule, start: next };
1012
+ return labelRuleSchema.safeParse(candidate).success ? candidate : null;
1013
+ }
1014
+ function duplicatePiece(el, id, layout) {
1015
+ const transform = { ...el.transform, x: el.transform.x + worldWidth(el) + GAP };
1016
+ if (el.type === "table") return { ...el, id, transform, label: nextTableLabel(layout) };
1017
+ if (el.type === "seatBlock") {
1018
+ const rowLabels = followingRows(el.rowLabels, el.rows) ?? el.rowLabels;
1019
+ const before = expandLabels(el.rowLabels, el.rows);
1020
+ const after = expandLabels(rowLabels, el.rows);
1021
+ const removed = el.removed.map((r) => {
1022
+ const [, row, seat] = parseUnitId(r);
1023
+ const i = row === void 0 ? -1 : before.indexOf(row);
1024
+ return i < 0 || seat === void 0 ? r : seatUnitId(id, after[i], seat);
1025
+ });
1026
+ return { ...el, id, transform, rowLabels, removed };
1027
+ }
1028
+ return { ...el, id, transform };
1029
+ }
1030
+ function rotatePiece(el) {
1031
+ const rotation = (el.transform.rotation + 90) % 360;
1032
+ return { ...el, transform: { ...el.transform, rotation } };
1033
+ }
1034
+ function toggleRemovedSeat(block, unitId) {
1035
+ const removed = block.removed.includes(unitId) ? block.removed.filter((r) => r !== unitId) : [...block.removed, unitId];
1036
+ return { ...block, removed };
1037
+ }
1038
+ function snapScale(scale, step = 0.05) {
1039
+ const snapped = Math.round(scale / step) * step;
1040
+ return Math.min(MAX_SCALE, Math.max(MIN_SCALE, Math.round(snapped * 100) / 100));
1041
+ }
1042
+ function scalePiece(el, scale) {
1043
+ const next = snapScale(scale, 0.01);
1044
+ if (el.type === "table") return { ...el, scale: next };
1045
+ const block = el;
1046
+ const base = expandSeatBlock({ ...block, removed: [], scale: 1, transform: { x: 0, y: 0, rotation: 0 } });
1047
+ const b = bbox(base);
1048
+ const centre = { x: (b.minX + b.maxX) / 2, y: (b.minY + b.maxY) / 2 };
1049
+ const k = (block.scale ?? 1) - next;
1050
+ const shift2 = applyTransform({ x: centre.x * k, y: centre.y * k }, { x: 0, y: 0, rotation: block.transform.rotation });
1051
+ return { ...el, scale: next, transform: { ...el.transform, x: el.transform.x + shift2.x, y: el.transform.y + shift2.y } };
1052
+ }
1053
+
1054
+ // ../core/src/place-types.ts
1055
+ function canAssign(el, option) {
1056
+ if (el.type === "seatBlock") return option.accepts === "seats";
1057
+ if (el.type === "table") return option.accepts === "tableWhole" || option.accepts === "tablePerPlace";
1058
+ if (el.type === "zone") return option.accepts === "zone";
1059
+ return false;
1060
+ }
1061
+ function steps(option) {
1062
+ const positive = (option.capacitySteps ?? []).filter((n) => Number.isFinite(n) && n > 0);
1063
+ return [...new Set(positive)].sort((a, b) => a - b);
1064
+ }
1065
+ function placeTypeCapacity(option) {
1066
+ const s = steps(option);
1067
+ return s.length > 0 ? s[s.length - 1] : null;
1068
+ }
1069
+ function nextBlockUnits(option, placed) {
1070
+ const s = steps(option);
1071
+ if (s.length === 0) return DEFAULT_BLOCK.rows * DEFAULT_BLOCK.cols;
1072
+ const next = s.find((n) => n > placed);
1073
+ return next === void 0 ? 0 : next - placed;
1074
+ }
1075
+ function capacityViolations(before, after, options) {
1076
+ const capped = options.filter((o) => placeTypeCapacity(o) !== null);
1077
+ if (capped.length === 0) return [];
1078
+ const was = summarize(before).byRef;
1079
+ const now = summarize(after).byRef;
1080
+ const out = [];
1081
+ for (const o of capped) {
1082
+ const capacity = placeTypeCapacity(o);
1083
+ const units = now[o.ref]?.units ?? 0;
1084
+ if (units > (was[o.ref]?.units ?? 0) && units > capacity) out.push({ ref: o.ref, label: o.label, capacity, units });
1085
+ }
1086
+ return out;
1087
+ }
1088
+ function assignPlaceType(el, option) {
1089
+ const placeType = option ? { ref: option.ref, name: option.label } : null;
1090
+ if (el.type === "seatBlock" || el.type === "zone") return { ...el, placeType };
1091
+ if (!option) return { ...el, placeType };
1092
+ return { ...el, placeType, mode: option.accepts === "tablePerPlace" ? "perPlace" : "whole" };
1093
+ }
1094
+ function normalizeName(s) {
1095
+ return s.normalize("NFD").replace(/\p{M}/gu, "").trim().replace(/\s+/g, " ").toLocaleLowerCase("es");
1096
+ }
1097
+ function rebindPlaceTypes(layout, options) {
1098
+ const elements = layout.elements.map((el) => {
1099
+ if (el.type === "landmark" || !el.placeType) return el;
1100
+ const name = normalizeName(el.placeType.name);
1101
+ const matches = options.filter((o) => canAssign(el, o) && normalizeName(o.label) === name);
1102
+ if (matches.length === 1) return assignPlaceType(el, matches[0]);
1103
+ return { ...el, placeType: { ref: "", name: el.placeType.name } };
1104
+ });
1105
+ return { ...layout, elements };
1106
+ }
1107
+ function summarize(layout) {
1108
+ const byRef = {};
1109
+ const slot = (ref) => byRef[ref] ??= { units: 0, seats: 0, tables: 0, places: 0, zones: 0, duplicates: { seats: [], tables: [] } };
1110
+ const seen = /* @__PURE__ */ new Map();
1111
+ const repeated = (ref, key, out, name) => {
1112
+ let set = seen.get(ref);
1113
+ if (!set) seen.set(ref, set = /* @__PURE__ */ new Set());
1114
+ if (!set.has(key)) {
1115
+ set.add(key);
1116
+ return;
1117
+ }
1118
+ if (!out.includes(name)) out.push(name);
1119
+ };
1120
+ let seats = 0;
1121
+ let places = 0;
1122
+ for (const u of expand(layout)) {
1123
+ const s = u.placeTypeRef ? slot(u.placeTypeRef) : null;
1124
+ if (s) s.units++;
1125
+ if (u.kind === "seat") {
1126
+ seats++;
1127
+ if (s) {
1128
+ s.seats++;
1129
+ const name = `${u.row}:${u.seat}`;
1130
+ repeated(u.placeTypeRef, `s\0${name}`, s.duplicates.seats, name);
1131
+ }
1132
+ } else if (u.kind === "place") {
1133
+ places++;
1134
+ if (s) s.places++;
1135
+ }
1136
+ }
1137
+ let tables = 0;
1138
+ let zones = 0;
1139
+ let unassignedPieces = 0;
1140
+ for (const el of layout.elements) {
1141
+ if (el.type === "landmark") continue;
1142
+ const ref = el.placeType?.ref || null;
1143
+ if (el.type === "table") {
1144
+ tables++;
1145
+ if (ref) {
1146
+ const s = slot(ref);
1147
+ s.tables++;
1148
+ repeated(ref, `t\0${el.label}`, s.duplicates.tables, el.label);
1149
+ }
1150
+ }
1151
+ if (el.type === "zone") {
1152
+ zones++;
1153
+ if (ref) slot(ref).zones++;
1154
+ }
1155
+ if (!ref) unassignedPieces++;
1156
+ }
1157
+ return { seats, tables, places, zones, types: Object.keys(byRef).length, unassignedPieces, byRef };
1158
+ }
1159
+
1160
+ // ../core/src/selection.ts
1161
+ var DEFAULT_MAX_SELECTION = 10;
1162
+ function unitStates(units, taken, selection, held) {
1163
+ const mine = new Set(selection);
1164
+ const out = /* @__PURE__ */ new Map();
1165
+ for (const u of units) {
1166
+ if (held.has(u.id)) out.set(u.id, "held");
1167
+ else if (mine.has(u.id)) out.set(u.id, "selected");
1168
+ else if (taken.has(u.id)) out.set(u.id, "taken");
1169
+ else out.set(u.id, "free");
1170
+ }
1171
+ return out;
1172
+ }
1173
+ function toggleUnit(selection, unitId, states, maxSelection) {
1174
+ if (selection.includes(unitId)) {
1175
+ return { selection: selection.filter((id) => id !== unitId) };
1176
+ }
1177
+ if (states.get(unitId) === "taken") return { selection: [...selection], rejected: "taken" };
1178
+ if (selection.length >= maxSelection) return { selection: [...selection], rejected: "max" };
1179
+ return { selection: [...selection, unitId] };
1180
+ }
1181
+
1182
+ // ../core/src/migrate.ts
1183
+ import { z as z2 } from "zod";
1184
+ var CURRENT_VERSION = 2;
1185
+ var LayoutError = class extends Error {
1186
+ issues;
1187
+ constructor(message, issues = []) {
1188
+ super(message);
1189
+ this.name = "LayoutError";
1190
+ this.issues = issues;
1191
+ }
1192
+ };
1193
+ var versionProbe = z2.object({ meta: z2.object({ version: z2.number() }) });
1194
+ var migrations = {};
1195
+ function migrate(input) {
1196
+ const probe = versionProbe.safeParse(input);
1197
+ if (!probe.success) throw new LayoutError("El documento no tiene meta.version");
1198
+ let version = probe.data.meta.version;
1199
+ let doc = input;
1200
+ if (version > CURRENT_VERSION) {
1201
+ throw new LayoutError(`El documento es de una versi\xF3n m\xE1s nueva (${version}) que la librer\xEDa (${CURRENT_VERSION})`);
1202
+ }
1203
+ while (version < CURRENT_VERSION) {
1204
+ const step = migrations[version];
1205
+ if (!step) throw new LayoutError(`No hay migraci\xF3n desde la versi\xF3n ${version}`);
1206
+ doc = step(doc);
1207
+ version++;
1208
+ }
1209
+ const parsed = layoutSchema.safeParse(doc, spanishParseOptions);
1210
+ if (!parsed.success) {
1211
+ throw new LayoutError(
1212
+ "El documento no es v\xE1lido",
1213
+ parsed.error.issues.map((i) => ({ path: i.path.join("."), message: i.message }))
1214
+ );
1215
+ }
1216
+ return parsed.data;
1217
+ }
1218
+
1219
+ // ../core/src/index.ts
1220
+ var VERSION = "0.10.0";
1221
+
1222
+ export {
1223
+ LATIN_ALPHABET,
1224
+ SPANISH_ALPHABET,
1225
+ letterLabel,
1226
+ letterIndex,
1227
+ labelSequence,
1228
+ expandLabels,
1229
+ ID_SEP,
1230
+ seatUnitId,
1231
+ placeUnitId,
1232
+ parseUnitId,
1233
+ landmarks,
1234
+ landmarkById,
1235
+ MAX_UNITS,
1236
+ MAX_ID_LENGTH,
1237
+ MAX_NAME_LENGTH,
1238
+ MAX_REF_LENGTH,
1239
+ MAX_TABLE_LABEL_LENGTH,
1240
+ MAX_SRC_LENGTH,
1241
+ MAX_ELEMENTS,
1242
+ MAX_LETTERS_START,
1243
+ MAX_SKIP,
1244
+ MIN_SCALE,
1245
+ MAX_SCALE,
1246
+ idSchema,
1247
+ rotationSchema,
1248
+ transformSchema,
1249
+ labelRuleSchema,
1250
+ placeTypeLinkSchema,
1251
+ seatBlockSchema,
1252
+ tableSchema,
1253
+ landmarkSchema,
1254
+ zoneSchema,
1255
+ elementSchema,
1256
+ backgroundSchema,
1257
+ metaSchema,
1258
+ layoutSchema,
1259
+ spanishParseOptions,
1260
+ applyTransform,
1261
+ bbox,
1262
+ pointInPolygon,
1263
+ distance,
1264
+ placeTypeBounds,
1265
+ zoneArc,
1266
+ zoneLocalSize,
1267
+ zoneBounds,
1268
+ pointInZone,
1269
+ zoneOutline,
1270
+ zoneLabelSize,
1271
+ zoneWithLocalSize,
1272
+ expandSeatBlock,
1273
+ TABLE_RADIUS,
1274
+ TABLE_WIDTH,
1275
+ TABLE_HEIGHT,
1276
+ PLACE_OFFSET,
1277
+ expandTable,
1278
+ expand,
1279
+ SpatialIndex,
1280
+ CommandError,
1281
+ applyCommand,
1282
+ HISTORY_LIMIT,
1283
+ History,
1284
+ findLockViolations,
1285
+ DEFAULT_BLOCK,
1286
+ DEFAULT_TABLE_PLACES,
1287
+ DEFAULT_ZONE,
1288
+ newElementId,
1289
+ tableSize,
1290
+ blockShape,
1291
+ createPiece,
1292
+ duplicatePiece,
1293
+ rotatePiece,
1294
+ toggleRemovedSeat,
1295
+ snapScale,
1296
+ scalePiece,
1297
+ canAssign,
1298
+ placeTypeCapacity,
1299
+ nextBlockUnits,
1300
+ capacityViolations,
1301
+ assignPlaceType,
1302
+ rebindPlaceTypes,
1303
+ summarize,
1304
+ DEFAULT_MAX_SELECTION,
1305
+ unitStates,
1306
+ toggleUnit,
1307
+ CURRENT_VERSION,
1308
+ LayoutError,
1309
+ migrate,
1310
+ VERSION
1311
+ };