@rickyzhangca/fpsr 0.0.1

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.
Files changed (41) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/LICENSE +21 -0
  3. package/README.md +267 -0
  4. package/dist/abort-CrgARaZd.js +43 -0
  5. package/dist/abort-CrgARaZd.js.map +1 -0
  6. package/dist/assets-Dy4fqZBV.d.ts +85 -0
  7. package/dist/assets-Dy4fqZBV.d.ts.map +1 -0
  8. package/dist/canvas.d.ts +98 -0
  9. package/dist/canvas.d.ts.map +1 -0
  10. package/dist/canvas.js +3 -0
  11. package/dist/execute-CRZV5zw1.js +963 -0
  12. package/dist/execute-CRZV5zw1.js.map +1 -0
  13. package/dist/host-DdX4g1YM.d.ts +34 -0
  14. package/dist/host-DdX4g1YM.d.ts.map +1 -0
  15. package/dist/index.d.ts +463 -0
  16. package/dist/index.d.ts.map +1 -0
  17. package/dist/index.js +1776 -0
  18. package/dist/index.js.map +1 -0
  19. package/dist/node.d.ts +16 -0
  20. package/dist/node.d.ts.map +1 -0
  21. package/dist/node.js +101 -0
  22. package/dist/node.js.map +1 -0
  23. package/dist/planner.d.ts +418 -0
  24. package/dist/planner.d.ts.map +1 -0
  25. package/dist/planner.js +96 -0
  26. package/dist/planner.js.map +1 -0
  27. package/dist/profile-CTN1Q5SI.d.ts +365 -0
  28. package/dist/profile-CTN1Q5SI.d.ts.map +1 -0
  29. package/dist/profile-DR4i6HDp.js +610 -0
  30. package/dist/profile-DR4i6HDp.js.map +1 -0
  31. package/dist/render-db-BFQIspDB.d.ts +426 -0
  32. package/dist/render-db-BFQIspDB.d.ts.map +1 -0
  33. package/dist/render-db.d.ts +2 -0
  34. package/dist/render-db.js +1 -0
  35. package/dist/tiled-draw-list-Dc-5Ledi.js +3990 -0
  36. package/dist/tiled-draw-list-Dc-5Ledi.js.map +1 -0
  37. package/dist/types-CHgZBSra.d.ts +105 -0
  38. package/dist/types-CHgZBSra.d.ts.map +1 -0
  39. package/dist/upgrade-planner-CNjnMHPJ.d.ts +385 -0
  40. package/dist/upgrade-planner-CNjnMHPJ.d.ts.map +1 -0
  41. package/package.json +86 -0
@@ -0,0 +1,610 @@
1
+ //#region src/types/draw-list.ts
2
+ /**
3
+ * Full Factorio `RenderLayer` enum (lowest → highest).
4
+ * Source: https://lua-api.factorio.com/latest/types/RenderLayer.html
5
+ * (same data as Factorio.app `doc-html/prototype-api.json`, 2.1.11).
6
+ * Values are the enum indices; order is the contract.
7
+ */
8
+ const FACTORIO_RENDER_LAYERS = {
9
+ zero: 0,
10
+ "background-transitions": 1,
11
+ "under-tiles": 2,
12
+ decals: 3,
13
+ "above-tiles": 4,
14
+ "ground-layer-1": 5,
15
+ "ground-layer-2": 6,
16
+ "ground-layer-3": 7,
17
+ "ground-layer-4": 8,
18
+ "ground-layer-5": 9,
19
+ "lower-radius-visualization": 10,
20
+ "radius-visualization": 11,
21
+ "transport-belt-integration": 12,
22
+ resource: 13,
23
+ "building-smoke": 14,
24
+ "rail-stone-path-lower": 15,
25
+ "rail-stone-path": 16,
26
+ "rail-tie": 17,
27
+ decorative: 18,
28
+ "ground-patch": 19,
29
+ "ground-patch-higher": 20,
30
+ "ground-patch-higher2": 21,
31
+ "rail-chain-signal-metal": 22,
32
+ "rail-screw": 23,
33
+ "rail-metal": 24,
34
+ remnants: 25,
35
+ floor: 26,
36
+ "transport-belt": 27,
37
+ "transport-belt-endings": 28,
38
+ "floor-mechanics-under-corpse": 29,
39
+ corpse: 30,
40
+ "floor-mechanics": 31,
41
+ item: 32,
42
+ "transport-belt-reader": 33,
43
+ "lower-object": 34,
44
+ "transport-belt-circuit-connector": 35,
45
+ "lower-object-above-shadow": 36,
46
+ "lower-object-overlay": 37,
47
+ "object-under": 38,
48
+ object: 39,
49
+ "cargo-hatch": 40,
50
+ "higher-object-under": 41,
51
+ "higher-object-above": 42,
52
+ "train-stop-top": 43,
53
+ "item-in-inserter-hand": 44,
54
+ "above-inserters": 45,
55
+ wires: 46,
56
+ "under-elevated": 47,
57
+ "elevated-rail-stone-path-lower": 48,
58
+ "elevated-rail-stone-path": 49,
59
+ "elevated-rail-tie": 50,
60
+ "elevated-rail-screw": 51,
61
+ "elevated-rail-metal": 52,
62
+ "elevated-lower-object": 53,
63
+ "elevated-object": 54,
64
+ "elevated-higher-object": 55,
65
+ "fluid-visualization": 56,
66
+ "wires-above": 57,
67
+ "entity-info-icon": 58,
68
+ "entity-info-icon-above": 59,
69
+ explosion: 60,
70
+ projectile: 61,
71
+ smoke: 62,
72
+ "air-object": 63,
73
+ "air-entity-info-icon": 64,
74
+ "light-effect": 65,
75
+ "selection-box": 66,
76
+ "higher-selection-box": 67,
77
+ "collision-selection-box": 68,
78
+ arrow: 69,
79
+ cursor: 70
80
+ };
81
+ /**
82
+ * fpsr-only names (not in Factorio's enum) and legacy aliases.
83
+ * Aliases share the official index of the Factorio name they stand for.
84
+ */
85
+ const FPSR_RENDER_LAYERS = {
86
+ /** ≈ `background-transitions` */
87
+ "tile-transition": 1,
88
+ /** ≈ `under-tiles` */
89
+ "ground-tile": 2,
90
+ "water-tile": 2,
91
+ /** alias of `rail-tie` */
92
+ "rail-ties": 17,
93
+ /** Dedicated `draw_as_shadow` pass (no Factorio RenderLayer for this) */
94
+ shadow: 37,
95
+ /** alias of `elevated-rail-tie` */
96
+ "elevated-rail-ties": 50,
97
+ /** alias of `entity-info-icon` */
98
+ icons: 58
99
+ };
100
+ const RENDER_LAYERS = {
101
+ ...FACTORIO_RENDER_LAYERS,
102
+ ...FPSR_RENDER_LAYERS
103
+ };
104
+ /** Comparator implementing the contract sort order. */
105
+ function compareDrawCmd(a, b) {
106
+ return a.layer - b.layer || a.sortY - b.sortY || a.sortX - b.sortX || a.entity - b.entity || a.sub - b.sub;
107
+ }
108
+ /**
109
+ * Canonical serialization for Tier-2 snapshots: stable key order, numbers
110
+ * rounded to 4 decimals, one command per line for reviewable diffs.
111
+ */
112
+ function serializeDrawList(list) {
113
+ const roundDeep = (v) => {
114
+ if (typeof v === "number") return Math.round(v * 1e4) / 1e4;
115
+ if (Array.isArray(v)) return v.map(roundDeep);
116
+ if (v && typeof v === "object") {
117
+ const out = {};
118
+ for (const k of Object.keys(v)) out[k] = roundDeep(v[k]);
119
+ return out;
120
+ }
121
+ return v;
122
+ };
123
+ const canon = (obj) => {
124
+ const out = {};
125
+ for (const k of Object.keys(obj).sort()) out[k] = roundDeep(obj[k]);
126
+ return out;
127
+ };
128
+ const lines = list.commands.map((c) => JSON.stringify(canon(c)));
129
+ const bounds = canon(list.bounds);
130
+ return `{\n"schema": ${list.schema},\n"bounds": ${JSON.stringify(bounds)},\n"commands": [\n${lines.join(",\n")}\n]\n}\n`;
131
+ }
132
+ //#endregion
133
+ //#region src/resolve/trains.ts
134
+ /**
135
+ * Vehicle RotatedAnimation art is already authored in oblique screen angles.
136
+ * Convert the world-space orientation back into that authored ellipse before
137
+ * selecting a pose. This is the inverse axis scaling of rolling-stock
138
+ * projection; vehicles also have no rail/bogie geometry.
139
+ */
140
+ function projectVehicleOrientation(orientation) {
141
+ const turn = (orientation % 1 + 1) % 1;
142
+ let x = Math.sin(turn * Math.PI * 2);
143
+ const y = -Math.cos(turn * Math.PI * 2);
144
+ x *= Math.SQRT1_2;
145
+ let out = Math.atan2(x, -y) / (Math.PI * 2);
146
+ if (out < 0) out += 1;
147
+ return out;
148
+ }
149
+ /**
150
+ * Map continuous blueprint `orientation` ∈ [0,1) to a distilled pose index.
151
+ * `round(orientation * N) % N` — orientation 0 → pose 0 (north).
152
+ *
153
+ * With `backEqualsFront` (cargo/fluid wagons): the sheet only authors half a
154
+ * turn; Factorio indexes as `round(o * 2N) % N` so east and west share a pose.
155
+ * @see FBSR FPRotatedSprite.getIndex
156
+ */
157
+ function trainOrientationIndex(orientation, poseCount, backEqualsFront = false) {
158
+ if (poseCount <= 0) return 0;
159
+ const o = (orientation % 1 + 1) % 1;
160
+ if (backEqualsFront) return Math.round(o * poseCount * 2) % poseCount;
161
+ return Math.round(o * poseCount) % poseCount;
162
+ }
163
+ /**
164
+ * Factorio's camera is a 45° orthographic tilt: circular orientations project
165
+ * onto an ellipse (Y scaled by 1/√2) before picking rotated sprites.
166
+ * @see https://forums.factorio.com/viewtopic.php?t=109695
167
+ */
168
+ function projectTrainOrientation(orientation) {
169
+ const turn = (orientation % 1 + 1) % 1;
170
+ const x = Math.sin(turn * Math.PI * 2);
171
+ let y = -Math.cos(turn * Math.PI * 2);
172
+ y *= Math.SQRT1_2;
173
+ let out = Math.atan2(x, -y) / (Math.PI * 2);
174
+ if (out < 0) out += 1;
175
+ return out;
176
+ }
177
+ /**
178
+ * Rolling-stock "rail shift": FBSR stores this as height and flattens with
179
+ * `y - height`. Equivalent forum form: `-(0.25 * abs(cos(o*TAU + PI/2)))`.
180
+ * @see https://forums.factorio.com/viewtopic.php?t=109695
181
+ */
182
+ function trainRailShiftY(orientation) {
183
+ const angle = (orientation % 1 + 1) % 1 * Math.PI * 2;
184
+ return -.25 * Math.abs(Math.cos(angle + Math.PI / 2));
185
+ }
186
+ /** Factorio oblique Y/Z scale (1/√2). @see FBSR FPUtils.PROJECTION_CONSTANT */
187
+ const TRAIN_PROJECTION = Math.SQRT1_2;
188
+ /**
189
+ * Artillery-wagon cannon mount offset (tile units, before rail-shift Y).
190
+ * Ports FBSR ArtilleryWagonRendering.calculateCannonPosition for slope=0.
191
+ */
192
+ function artilleryCannonShift(orientation, opts) {
193
+ const o = (orientation % 1 + 1) % 1;
194
+ const n = typeof opts.orientationCount === "number" && opts.orientationCount > 0 ? opts.orientationCount : 64;
195
+ const aligned = trainOrientationIndex(projectTrainOrientation(o), n) / n;
196
+ const vf = Math.abs(Math.abs(aligned - .5) - .25) * 4;
197
+ const vert = opts.cannonBaseShiftWhenVertical ?? 0;
198
+ const horiz = opts.cannonBaseShiftWhenHorizontal ?? 0;
199
+ const offsetForward = -(vert * vf + horiz * (1 - vf));
200
+ const offsetHeight = opts.cannonBaseHeight ?? 0;
201
+ const rotation = o * Math.PI * 2 + Math.PI / 2;
202
+ return [offsetForward * Math.cos(rotation), offsetForward * Math.sin(rotation) * TRAIN_PROJECTION - offsetHeight * TRAIN_PROJECTION];
203
+ }
204
+ /** Dual bogie shifts for a rolling-stock entity (tile units relative to center). */
205
+ function trainWheelShifts(orientation, jointDistance) {
206
+ const o = (orientation % 1 + 1) % 1;
207
+ const projected = projectTrainOrientation(o);
208
+ const half = jointDistance / 2;
209
+ const angle = o * Math.PI * 2;
210
+ const ox = Math.sin(angle) * half;
211
+ const oy = -Math.cos(angle) * half;
212
+ const vert = trainRailShiftY(o);
213
+ const flipped = projectTrainOrientation((o + .5) % 1);
214
+ return [{
215
+ shift: [ox, oy + vert],
216
+ orientation: flipped
217
+ }, {
218
+ shift: [-ox, -oy + vert],
219
+ orientation: projected
220
+ }];
221
+ }
222
+ /**
223
+ * Map blueprint rail direction (0..15) to a direction8 picture index.
224
+ * Straight / half-diagonal pieces only author 4 of 8 keys; fold 4..7 → 0..3.
225
+ */
226
+ function railDirectionIndex(direction, foldTo4) {
227
+ let i = Math.floor((direction % 16 + 16) % 16 / 2) % 8;
228
+ if (foldTo4) i = i % 4;
229
+ return i;
230
+ }
231
+ //#endregion
232
+ //#region src/icon-silhouette.ts
233
+ /** Pixel radius for feMorphology-style alpha dilation on entity-info icons. */
234
+ const ENTITY_INFO_SILHOUETTE_RADIUS_PX = 12;
235
+ /** Soft blur applied after dilation to feather the silhouette edge. */
236
+ const ENTITY_INFO_SILHOUETTE_BLUR_PX = 16;
237
+ /** Total source-pixel padding around icon crops for spread + blur. */
238
+ function entityInfoSilhouettePadPx(dilateRadius = 12, blurRadius = 16) {
239
+ return dilateRadius + blurRadius;
240
+ }
241
+ /** Box dilation on the alpha channel; output RGB is solid black. */
242
+ function dilateAlphaChannel(src, width, height, radius) {
243
+ if (radius <= 0) {
244
+ const out = new Uint8Array(width * height);
245
+ for (let pixel = 0; pixel < out.length; pixel++) out[pixel] = src[pixel * 4 + 3];
246
+ return out;
247
+ }
248
+ const r = Math.floor(radius);
249
+ if (r <= 0) return dilateAlphaChannel(src, width, height, 0);
250
+ const horizontal = new Uint8Array(width * height);
251
+ const deque = new Int32Array(Math.max(width, height));
252
+ for (let y = 0; y < height; y++) {
253
+ let head = 0;
254
+ let tail = 0;
255
+ let next = 0;
256
+ const row = y * width;
257
+ for (let x = 0; x < width; x++) {
258
+ const addThrough = Math.min(width - 1, x + r);
259
+ while (next <= addThrough) {
260
+ const alpha = src[(row + next) * 4 + 3];
261
+ while (tail > head) {
262
+ if (src[(row + deque[tail - 1]) * 4 + 3] > alpha) break;
263
+ tail--;
264
+ }
265
+ deque[tail++] = next++;
266
+ }
267
+ const removeBefore = x - r;
268
+ while (tail > head && deque[head] < removeBefore) head++;
269
+ horizontal[row + x] = src[(row + deque[head]) * 4 + 3];
270
+ }
271
+ }
272
+ const out = new Uint8Array(width * height);
273
+ for (let x = 0; x < width; x++) {
274
+ let head = 0;
275
+ let tail = 0;
276
+ let next = 0;
277
+ for (let y = 0; y < height; y++) {
278
+ const addThrough = Math.min(height - 1, y + r);
279
+ while (next <= addThrough) {
280
+ const alpha = horizontal[next * width + x];
281
+ while (tail > head) {
282
+ if (horizontal[deque[tail - 1] * width + x] > alpha) break;
283
+ tail--;
284
+ }
285
+ deque[tail++] = next++;
286
+ }
287
+ const removeBefore = y - r;
288
+ while (tail > head && deque[head] < removeBefore) head++;
289
+ out[y * width + x] = horizontal[deque[head] * width + x];
290
+ }
291
+ }
292
+ return out;
293
+ }
294
+ function writeBlurredBlackRgba(alpha, target, width, height, radius) {
295
+ const r = Math.floor(radius);
296
+ if (r <= 0) {
297
+ for (let pixel = 0; pixel < alpha.length; pixel++) {
298
+ const i = pixel * 4;
299
+ target[i] = 0;
300
+ target[i + 1] = 0;
301
+ target[i + 2] = 0;
302
+ target[i + 3] = alpha[pixel];
303
+ }
304
+ return;
305
+ }
306
+ const stride = width + 1;
307
+ const integral = new Uint32Array((width + 1) * (height + 1));
308
+ for (let y = 0; y < height; y++) {
309
+ let rowSum = 0;
310
+ const srcRow = y * width;
311
+ const integralRow = (y + 1) * stride;
312
+ const previousRow = y * stride;
313
+ for (let x = 0; x < width; x++) {
314
+ rowSum += alpha[srcRow + x];
315
+ integral[integralRow + x + 1] = integral[previousRow + x + 1] + rowSum;
316
+ }
317
+ }
318
+ for (let y = 0; y < height; y++) {
319
+ const top = Math.max(0, y - r);
320
+ const bottom = Math.min(height, y + r + 1);
321
+ for (let x = 0; x < width; x++) {
322
+ const left = Math.max(0, x - r);
323
+ const right = Math.min(width, x + r + 1);
324
+ const sum = integral[bottom * stride + right] - integral[top * stride + right] - integral[bottom * stride + left] + integral[top * stride + left];
325
+ const i = (y * width + x) * 4;
326
+ target[i] = 0;
327
+ target[i + 1] = 0;
328
+ target[i + 2] = 0;
329
+ target[i + 3] = Math.round(sum / ((right - left) * (bottom - top)));
330
+ }
331
+ }
332
+ }
333
+ /** Bake from an already-cropped icon without reading back a second offscreen canvas. */
334
+ function bakeEntityInfoSilhouetteFromImageData(iconData, createCanvas, dilateRadius = 12, blurRadius = 16) {
335
+ const iconWidth = iconData.width;
336
+ const iconHeight = iconData.height;
337
+ const pad = entityInfoSilhouettePadPx(dilateRadius, blurRadius);
338
+ const width = iconWidth + 2 * pad;
339
+ const height = iconHeight + 2 * pad;
340
+ const canvas = createCanvas(width, height);
341
+ canvas.width = width;
342
+ canvas.height = height;
343
+ const ctx = canvas.getContext("2d");
344
+ if (!ctx) return void 0;
345
+ const imageData = ctx.createImageData?.(width, height) ?? ctx.getImageData(0, 0, width, height);
346
+ for (let y = 0; y < iconHeight; y++) for (let x = 0; x < iconWidth; x++) {
347
+ const source = (y * iconWidth + x) * 4 + 3;
348
+ const target = ((y + pad) * width + x + pad) * 4 + 3;
349
+ imageData.data[target] = iconData.data[source];
350
+ }
351
+ writeBlurredBlackRgba(dilateAlphaChannel(imageData.data, width, height, dilateRadius), imageData.data, width, height, blurRadius);
352
+ ctx.putImageData(imageData, 0, 0);
353
+ return canvas;
354
+ }
355
+ /**
356
+ * Dilate and blur an isolated icon crop into a padded black silhouette image.
357
+ * Returns undefined when the canvas context cannot read/write ImageData.
358
+ */
359
+ function bakeEntityInfoSilhouette(iconImage, iconWidth, iconHeight, createCanvas, dilateRadius = 12, blurRadius = 16) {
360
+ const pad = entityInfoSilhouettePadPx(dilateRadius, blurRadius);
361
+ const width = iconWidth + 2 * pad;
362
+ const height = iconHeight + 2 * pad;
363
+ const canvas = createCanvas(width, height);
364
+ canvas.width = width;
365
+ canvas.height = height;
366
+ const ctx = canvas.getContext("2d");
367
+ if (!ctx) return void 0;
368
+ ctx.drawImage(iconImage, pad, pad);
369
+ const imageData = ctx.getImageData(0, 0, width, height);
370
+ writeBlurredBlackRgba(dilateAlphaChannel(imageData.data, width, height, dilateRadius), imageData.data, width, height, blurRadius);
371
+ ctx.putImageData(imageData, 0, 0);
372
+ return canvas;
373
+ }
374
+ //#endregion
375
+ //#region src/train-chains.ts
376
+ /**
377
+ * Rolling-stock coupling chain overlay: green joint circles + straight segments
378
+ * along connected wagons (Factorio attach/coupling visualization).
379
+ */
380
+ /** Default Factorio RollingStock connection_distance when not distilled. */
381
+ const DEFAULT_CONNECTION_DISTANCE = 3;
382
+ /** Default Factorio RollingStock joint_distance when not distilled. */
383
+ const DEFAULT_JOINT_DISTANCE = 4;
384
+ /** Hollow joint circle radius in tiles — segments stop at this edge. */
385
+ const TRAIN_CHAIN_JOINT_RADIUS = .3;
386
+ /** Tolerance when inferring couplings from joint proximity (tiles). */
387
+ const COUPLE_TOLERANCE = .25;
388
+ function round4(n) {
389
+ return Math.round(n * 1e4) / 1e4;
390
+ }
391
+ function jointDistanceOf(def) {
392
+ const j = def.data?.jointDistance;
393
+ return typeof j === "number" && j > 0 ? j : 4;
394
+ }
395
+ function connectionDistanceOf(def) {
396
+ const c = def.data?.connectionDistance;
397
+ return typeof c === "number" && c > 0 ? c : 3;
398
+ }
399
+ /**
400
+ * World-space front/back joint positions for a rolling-stock entity.
401
+ * Matches bogie placement from `trainWheelShifts` (index 0 = front/+half).
402
+ */
403
+ function trainJointWorldPoints(entity, jointDistance) {
404
+ const bogies = trainWheelShifts(entity.orientation ?? 0, jointDistance);
405
+ const front = bogies[0];
406
+ const back = bogies[1];
407
+ return {
408
+ front: [round4(entity.position.x + front.shift[0]), round4(entity.position.y + front.shift[1])],
409
+ back: [round4(entity.position.x + back.shift[0]), round4(entity.position.y + back.shift[1])]
410
+ };
411
+ }
412
+ function dist(a, b) {
413
+ return Math.hypot(a[0] - b[0], a[1] - b[1]);
414
+ }
415
+ function jointKey(x, y) {
416
+ return `${Math.round(x * 1e4) / 1e4},${Math.round(y * 1e4) / 1e4}`;
417
+ }
418
+ function addSegment(segments, seen, a, b, endInset = 0) {
419
+ const k1 = `${jointKey(a[0], a[1])}|${jointKey(b[0], b[1])}`;
420
+ const k2 = `${jointKey(b[0], b[1])}|${jointKey(a[0], a[1])}`;
421
+ if (seen.has(k1) || seen.has(k2)) return;
422
+ seen.add(k1);
423
+ let x1 = a[0];
424
+ let y1 = a[1];
425
+ let x2 = b[0];
426
+ let y2 = b[1];
427
+ if (endInset > 0) {
428
+ const len = Math.hypot(x2 - x1, y2 - y1);
429
+ if (len <= endInset * 2) return;
430
+ const ux = (x2 - x1) / len;
431
+ const uy = (y2 - y1) / len;
432
+ x1 = round4(x1 + ux * endInset);
433
+ y1 = round4(y1 + uy * endInset);
434
+ x2 = round4(x2 - ux * endInset);
435
+ y2 = round4(y2 - uy * endInset);
436
+ }
437
+ segments.push({
438
+ x1,
439
+ y1,
440
+ x2,
441
+ y2
442
+ });
443
+ }
444
+ function addJoint(joints, seen, p) {
445
+ const k = jointKey(p[0], p[1]);
446
+ if (seen.has(k)) return;
447
+ seen.add(k);
448
+ joints.push({
449
+ x: p[0],
450
+ y: p[1]
451
+ });
452
+ }
453
+ function sideOfNeighbor(links, neighbor, stock, fallback) {
454
+ const n = links.get(neighbor);
455
+ if (n?.front === stock) return "front";
456
+ if (n?.back === stock) return "back";
457
+ return fallback;
458
+ }
459
+ function parseStockConnections(raw, trainNums) {
460
+ if (!raw?.length) return [];
461
+ const links = /* @__PURE__ */ new Map();
462
+ for (const entry of raw) {
463
+ if (!entry || typeof entry.stock !== "number") continue;
464
+ if (!trainNums.has(entry.stock)) continue;
465
+ const cur = links.get(entry.stock) ?? {};
466
+ if (typeof entry.front === "number") cur.front = entry.front;
467
+ if (typeof entry.back === "number") cur.back = entry.back;
468
+ links.set(entry.stock, cur);
469
+ }
470
+ const edges = [];
471
+ const seen = /* @__PURE__ */ new Set();
472
+ for (const [stock, sides] of links) for (const aSide of ["front", "back"]) {
473
+ const neighbor = sides[aSide];
474
+ if (typeof neighbor !== "number" || !trainNums.has(neighbor) || neighbor === stock) continue;
475
+ const key = `${Math.min(stock, neighbor)}-${Math.max(stock, neighbor)}`;
476
+ if (seen.has(key)) continue;
477
+ seen.add(key);
478
+ const bSide = sideOfNeighbor(links, neighbor, stock, aSide === "front" ? "back" : "front");
479
+ edges.push({
480
+ a: stock,
481
+ aSide,
482
+ b: neighbor,
483
+ bSide
484
+ });
485
+ }
486
+ return edges;
487
+ }
488
+ /**
489
+ * Infer couplings when `stock_connections` is absent: facing joints within
490
+ * connection_distance ± tolerance, orientations aligned or opposite.
491
+ */
492
+ function inferCouplings(trains, jointsByNum) {
493
+ const edges = [];
494
+ const seen = /* @__PURE__ */ new Set();
495
+ for (let i = 0; i < trains.length; i++) {
496
+ const a = trains[i];
497
+ const ja = jointsByNum.get(a.entity.entity_number);
498
+ if (!ja) continue;
499
+ const connA = connectionDistanceOf(a.def);
500
+ for (let j = i + 1; j < trains.length; j++) {
501
+ const b = trains[j];
502
+ const jb = jointsByNum.get(b.entity.entity_number);
503
+ if (!jb) continue;
504
+ const expected = (connA + connectionDistanceOf(b.def)) / 2;
505
+ const candidates = [
506
+ {
507
+ aSide: "front",
508
+ bSide: "front",
509
+ d: dist(ja.front, jb.front)
510
+ },
511
+ {
512
+ aSide: "front",
513
+ bSide: "back",
514
+ d: dist(ja.front, jb.back)
515
+ },
516
+ {
517
+ aSide: "back",
518
+ bSide: "front",
519
+ d: dist(ja.back, jb.front)
520
+ },
521
+ {
522
+ aSide: "back",
523
+ bSide: "back",
524
+ d: dist(ja.back, jb.back)
525
+ }
526
+ ];
527
+ let best;
528
+ for (const c of candidates) {
529
+ if (Math.abs(c.d - expected) > COUPLE_TOLERANCE) continue;
530
+ if (!best || c.d < best.d) best = c;
531
+ }
532
+ if (!best) continue;
533
+ const key = `${Math.min(a.entity.entity_number, b.entity.entity_number)}-${Math.max(a.entity.entity_number, b.entity.entity_number)}`;
534
+ if (seen.has(key)) continue;
535
+ seen.add(key);
536
+ edges.push({
537
+ a: a.entity.entity_number,
538
+ aSide: best.aSide,
539
+ b: b.entity.entity_number,
540
+ bSide: best.bSide
541
+ });
542
+ }
543
+ }
544
+ return edges;
545
+ }
546
+ function pointOf(joints, side) {
547
+ return side === "front" ? joints.front : joints.back;
548
+ }
549
+ /**
550
+ * Build train-chain overlay geometry for all coupled rolling stock in a blueprint.
551
+ *
552
+ * Draws hollow circles only at coupled joints (free ends omitted), and straight
553
+ * segments between those facing joints. Segment endpoints are inset to the
554
+ * circle perimeter so strokes meet the rings instead of crossing through them.
555
+ *
556
+ * Isolated single wagons with no neighbors are omitted.
557
+ */
558
+ function buildTrainChainGeometry(bp, byNumber) {
559
+ const trains = [];
560
+ for (const entry of byNumber.values()) if (entry.def.kind === "train") trains.push(entry);
561
+ if (trains.length === 0) return null;
562
+ const trainNums = new Set(trains.map((t) => t.entity.entity_number));
563
+ const jointsByNum = /* @__PURE__ */ new Map();
564
+ for (const t of trains) jointsByNum.set(t.entity.entity_number, trainJointWorldPoints(t.entity, jointDistanceOf(t.def)));
565
+ let edges = parseStockConnections(bp.stock_connections, trainNums);
566
+ if (edges.length === 0) edges = inferCouplings(trains, jointsByNum);
567
+ if (edges.length === 0) return null;
568
+ const segments = [];
569
+ const joints = [];
570
+ const segSeen = /* @__PURE__ */ new Set();
571
+ const jointSeen = /* @__PURE__ */ new Set();
572
+ for (const e of edges) {
573
+ const ja = jointsByNum.get(e.a);
574
+ const jb = jointsByNum.get(e.b);
575
+ if (!ja || !jb) continue;
576
+ const aPt = pointOf(ja, e.aSide);
577
+ const bPt = pointOf(jb, e.bSide);
578
+ addJoint(joints, jointSeen, aPt);
579
+ addJoint(joints, jointSeen, bPt);
580
+ addSegment(segments, segSeen, aPt, bPt, TRAIN_CHAIN_JOINT_RADIUS);
581
+ }
582
+ if (segments.length === 0 && joints.length === 0) return null;
583
+ return {
584
+ segments,
585
+ joints
586
+ };
587
+ }
588
+ //#endregion
589
+ //#region src/profile.ts
590
+ /** High-resolution clock; falls back to Date.now in exotic hosts. */
591
+ function nowMs() {
592
+ if (typeof performance !== "undefined" && typeof performance.now === "function") return performance.now();
593
+ return Date.now();
594
+ }
595
+ /** Best-effort Performance API mark (ignored when unavailable). */
596
+ function perfMark(name) {
597
+ try {
598
+ performance.mark?.(name);
599
+ } catch {}
600
+ }
601
+ /** Best-effort Performance API measure between two marks. */
602
+ function perfMeasure(name, startMark, endMark) {
603
+ try {
604
+ performance.measure?.(name, startMark, endMark);
605
+ } catch {}
606
+ }
607
+ //#endregion
608
+ export { compareDrawCmd as C, RENDER_LAYERS as S, railDirectionIndex as _, DEFAULT_JOINT_DISTANCE as a, trainWheelShifts as b, trainJointWorldPoints as c, bakeEntityInfoSilhouette as d, bakeEntityInfoSilhouetteFromImageData as f, projectVehicleOrientation as g, projectTrainOrientation as h, DEFAULT_CONNECTION_DISTANCE as i, ENTITY_INFO_SILHOUETTE_BLUR_PX as l, artilleryCannonShift as m, perfMark as n, TRAIN_CHAIN_JOINT_RADIUS as o, entityInfoSilhouettePadPx as p, perfMeasure as r, buildTrainChainGeometry as s, nowMs as t, ENTITY_INFO_SILHOUETTE_RADIUS_PX as u, trainOrientationIndex as v, serializeDrawList as w, FACTORIO_RENDER_LAYERS as x, trainRailShiftY as y };
609
+
610
+ //# sourceMappingURL=profile-DR4i6HDp.js.map