PageSourceSearch

https://estimote.com/javascript/spacetimeos/animation.js?v=4

js estimote.com collected 2026-10-01 09:10:30 UTC 149,467 bytes, 3,559 lines download raw bytes

1(() => {
2  var __defProp = Object.defineProperty;
3  var __getOwnPropNames = Object.getOwnPropertyNames;
4  var __esm = (fn, res) => function __init() {
5    return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
6  };
7  var __export = (target, all) => {
8    for (var name in all)
9      __defProp(target, name, { get: all[name], enumerable: true });
10  };
11
12  // public:javascript/spacetimeos/ui.js
13  var palette, roundRect, drawCard, drawIcon, drawHeader, drawCaption, drawRing, getDashboardGrid;
14  var init_ui = __esm({
15    "public:javascript/spacetimeos/ui.js"() {
16      palette = {
17        ink: "#202020",
18        muted: "#999999",
19        blue: "#082487",
20        bar: "#4778ff",
21        orange: "#ff7000",
22        yellow: "#ffe89a"
23      };
24      roundRect = (context, x, y, width, height, radius) => {
25        context.beginPath();
26        context.roundRect(x, y, width, height, radius);
27      };
28      drawCard = (context, x, y, width, size, color = "#ffffff") => {
29        context.save();
30        const transform = context.getTransform();
31        const scale = Math.hypot(transform.c, transform.d);
32        context.fillStyle = color;
33        context.shadowColor = "rgba(0, 0, 0, 0.09)";
34        context.shadowBlur = size * 0.065 * scale;
35        context.shadowOffsetY = size * 0.018 * scale;
36        roundRect(context, x, y, width, size, size * 0.11);
37        context.fill();
38        context.restore();
39      };
40      drawIcon = (context, x, y, size, type, fill) => {
41        context.save();
42        context.translate(x, y);
43        context.scale(size, size);
44        context.strokeStyle = palette.ink;
45        context.lineWidth = 0.05;
46        context.lineCap = "round";
47        context.lineJoin = "round";
48        roundRect(context, 0, 0, 1, 1, 0.23);
49        if (fill) {
50          context.fillStyle = fill;
51          context.fill();
52        } else {
53          context.stroke();
54        }
55        context.beginPath();
56        if (type === "circle") {
57          context.arc(0.5, 0.5, 0.26, 0, Math.PI * 2);
58        } else if (type === "square") {
59          context.rect(0.29, 0.29, 0.42, 0.42);
60        } else if (type === "triangle") {
61          context.moveTo(0.5, 0.24);
62          context.lineTo(0.77, 0.72);
63          context.lineTo(0.23, 0.72);
64          context.closePath();
65        } else if (type === "rows") {
66          for (const row of [0.32, 0.5, 0.68]) {
67            context.moveTo(0.26, row);
68            context.lineTo(0.74, row);
69          }
70        } else if (type === "chart") {
71          context.moveTo(0.23, 0.68);
72          context.lineTo(0.4, 0.47);
73          context.lineTo(0.57, 0.58);
74          context.lineTo(0.77, 0.29);
75        } else if (type === "clock") {
76          context.arc(0.5, 0.5, 0.29, 0, Math.PI * 2);
77          context.moveTo(0.5, 0.31);
78          context.lineTo(0.5, 0.5);
79          context.lineTo(0.64, 0.58);
80        } else if (type === "staff") {
81          context.arc(0.5, 0.33, 0.14, 0, Math.PI * 2);
82          context.moveTo(0.24, 0.78);
83          context.bezierCurveTo(0.24, 0.43, 0.76, 0.43, 0.76, 0.78);
84        }
85        context.stroke();
86        context.restore();
87      };
88      drawHeader = (context, x, y, size, label, icon, fill, color = palette.ink) => {
89        const iconSize = size * 0.148;
90        const inset = size * 0.07;
91        drawIcon(context, x + inset, y + inset, iconSize, icon, fill);
92        context.fillStyle = color;
93        context.textAlign = "left";
94        context.textBaseline = "middle";
95        context.font = `600 ${size * 0.078}px Arial, Helvetica, sans-serif`;
96        const textX = x + inset + iconSize + size * 0.036;
97        context.fillText(label, textX, y + inset + iconSize / 2);
98      };
99      drawCaption = (context, x, y, size, label, color = palette.muted) => {
100        context.fillStyle = color;
101        context.textAlign = "center";
102        context.textBaseline = "middle";
103        context.font = `400 ${size * 0.057}px Arial, Helvetica, sans-serif`;
104        context.fillText(label, x + size / 2, y + size * 0.88);
105      };
106      drawRing = (context, x, y, size, progress, color, track) => {
107        const centerX = x + size * 0.5;
108        const centerY = y + size * 0.53;
109        const radius = size * 0.218;
110        context.lineWidth = size * 0.05;
111        context.lineCap = "round";
112        context.strokeStyle = track;
113        context.beginPath();
114        context.arc(centerX, centerY, radius, -Math.PI / 2, Math.PI * 1.5);
115        context.stroke();
116        if (progress > 0) {
117          context.strokeStyle = color;
118          context.beginPath();
119          context.arc(centerX, centerY, radius, -Math.PI / 2, -Math.PI / 2 + Math.PI * 2 * Math.min(1, progress));
120          context.stroke();
121        }
122      };
123      getDashboardGrid = (layout) => {
124        const margin = layout.contentHalfWidth * 0.033;
125        const gapRatio = 0.065;
126        const size = Math.min(
127          (layout.contentHalfWidth - margin * 2) / (3 + 2 * gapRatio),
128          layout.contentHeight / (3.8 + 2 * gapRatio)
129        );
130        const gap = size * gapRatio;
131        const x = layout.leftContentX + margin;
132        const y = layout.contentY + size * 0.28;
133        const secondY = y + size + gap;
134        const thirdY = secondY + size + gap;
135        const middleX = x + size + gap;
136        const lastX = middleX + size + gap;
137        return { x, y, size, gap, secondY, thirdY, middleX, lastX };
138      };
139    }
140  });
141
142  // public:javascript/spacetimeos/radar-scene.js
143  var radarColors, radarHalfAngle, people, getRadarTargets, drawSensor, drawPerson, drawRadarScene;
144  var init_radar_scene = __esm({
145    "public:javascript/spacetimeos/radar-scene.js"() {
146      radarColors = {
147        cyan: "#60c3df",
148        blue: "#355fe5",
149        pink: "#f47fa5",
150        green: "#8dcea5",
151        purple: "#a72a59",
152        yellow: "#efdf26",
153        head: "#575354"
154      };
155      radarHalfAngle = Math.PI * 0.175;
156      people = [
157        { id: "sitting", pose: "sitting", color: radarColors.pink, range: 0.65, travel: 0.22, pace: 0.32, phase: 1.35, angle: -0.25, sway: 0.045 },
158        { id: "standing", pose: "standing", color: radarColors.blue, range: 0.49, travel: 0.24, pace: 0.4, phase: -0.35, angle: -0.02, sway: 0.075 },
159        { id: "hand", pose: "hand", color: radarColors.green, range: 0.6, travel: 0.23, pace: 0.29, phase: 0.38, angle: 0.3, sway: 0.06 },
160        { id: "visitor", pose: "standing", color: radarColors.blue, range: 0.4, travel: 0.1, pace: 0.35, phase: 0, angle: 0.68, sway: 0.26 }
161      ];
162      getRadarTargets = (time) =>
162 people.map((person, index) => {
163        const distance = person.range + person.travel * Math.sin(time * person.pace + person.phase);
164        const angle = person.angle + person.sway * Math.sin(time * (index === 3 ? 0.27 : 0.2) + (index === 3 ? 2.25 : index * 0.6));
165        const signal = Math.max(0, Math.min(1, (radarHalfAngle - Math.abs(angle)) / 0.055));
166        return { ...person, distance, angle, signal, detected: signal > 0.2 };
167      });
168      drawSensor = (context, x, y, size) => {
169        context.save();
170        context.translate(x - size / 2, y - size * 0.52);
171        context.scale(size / 110, size / 110);
172        context.fillStyle = "#ffffff";
173        context.strokeStyle = "#c97398";
174        context.lineWidth = 1.2;
175        context.beginPath();
176        context.moveTo(55, 5);
177        context.bezierCurveTo(64, 5, 70, 12, 75, 21);
178        context.lineTo(105, 72);
179        context.bezierCurveTo(116, 91, 104, 105, 88, 105);
180        context.lineTo(22, 105);
181        context.bezierCurveTo(6, 105, -6, 91, 5, 72);
182        context.lineTo(35, 21);
183        context.bezierCurveTo(40, 12, 46, 5, 55, 5);
184        context.closePath();
185        context.fill();
186        context.stroke();
187        context.strokeStyle = "#f2e9ee";
188        context.lineWidth = 1;
189        context.save();
190        context.translate(5, 5);
191        context.scale(0.91, 0.91);
192        context.stroke();
193        context.restore();
194        context.strokeStyle = radarColors.purple;
195        context.lineWidth = 4.8;
196        context.lineCap = "round";
197        for (let index = 0; index < 3; index++) {
198          const angle = index * Math.PI * 2 / 3 - Math.PI / 2;
199          context.beginPath();
200          context.arc(55, 63, 18, angle + 0.2, angle + Math.PI * 2 / 3 - 0.2);
201          context.stroke();
202        }
203        context.restore();
204      };
205      drawPerson = (context, x, y, size, person, selected) => {
206        context.save();
207        context.translate(x, y);
208        if (selected) {
209          context.fillStyle = "rgba(255, 255, 255, 0.96)";
210          context.beginPath();
211          context.arc(0, 0, size * 0.4, 0, Math.PI * 2);
212          context.fill();
213        }
214        context.fillStyle = "#dddddd";
215        context.beginPath();
216        context.ellipse(0, 0, size / 2, size * 0.18, 0, 0, Math.PI * 2);
217        context.fill();
218        context.globalAlpha = person.signal;
219        context.fillStyle = person.color;
220        context.fill();
221        context.globalAlpha = 1;
222        context.fillStyle = person.detected ? radarColors.head : "#c2c2c2";
223        context.beginPath();
224        context.ellipse(0, 0, size * 0.24, size * 0.25, 0, 0, Math.PI * 2);
225        context.fill();
226        context.restore();
227      };
228      drawRadarScene = (context, layout, state) => {
229        const { rightContentX: x, contentY: y, contentHalfWidth: width, contentHeight: height } = layout;
230        const centerX = x + width * 0.5;
231        const centerY = y + height * 0.24;
232        const radius = Math.min(width * 0.72 / (2 * Math.sin(radarHalfAngle)), height * 0.6);
233        context.save();
234        context.beginPath();
235        context.rect(x, y, width, height);
236        context.clip();
237        context.fillStyle = "#f2f2f2";
238        context.fillRect(x, y, width, height);
239        const spacing = width * 0.028;
240        context.fillStyle = "#d9d9d9";
241        context.beginPath();
242        for (let dotY = y + spacing * 0.5; dotY < y + height; dotY += spacing) {
243          for (let dotX = x + spacing * 0.8; dotX < x + width; dotX += spacing) {
244            context.moveTo(dotX + width * 165e-5, dotY);
245            context.arc(dotX, dotY, width * 165e-5, 0, Math.PI * 2);
246          }
247        }
248        context.fill();
249        for (const [range, color] of [[1, "#bfe6f1"], [0.67, "#a1dced"], [0.34, "#84cfe5"]]) {
250          context.fillStyle = color;
251          context.beginPath();
252          context.moveTo(centerX, centerY);
253          context.arc(centerX, centerY, radius * range, Math.PI / 2 - radarHalfAngle, Math.PI / 2 + radarHalfAngle);
254          context.closePath();
255          context.fill();
256        }
257        const pulse = state.elapsed * 0.23 % 1;
258        context.strokeStyle = `rgba(255, 255, 255, ${0.22 * Math.sin(pulse * Math.PI)})`;
259        context.lineWidth = width * 2e-3;
260        context.beginPath();
261        context.arc(centerX, centerY, radius * pulse, Math.PI / 2 - radarHalfAngle, Math.PI / 2 + radarHalfAngle);
262        context.stroke();
263        for (const person of [...state.targets].sort((a, b) => a.distance - b.distance)) {
264          drawPerson(
265            context,
266            centerX + Math.sin(person.angle) * person.distance * radius,
267            centerY + Math.cos(person.angle) * person.distance * radius,
268            width * 0.12,
269            person,
270            person.id === state.selectedId
271          );
272        }
273        drawSensor(context, centerX, centerY, width * 0.09);
274        context.restore();
275      };
276    }
277  });
278
279  // public:javascript/spacetimeos/radar-widgets.js
280  var posePaths, drawPose, drawBadge, drawHeader2, drawButton, drawWaterfall, drawSamples, drawCollection, drawTraining, drawInference, createWaterfall, drawRadarDashboard;
281  var init_radar_widgets = __esm({
282    "public:javascript/spacetimeos/radar-widgets.js"() {
283      init_ui();
284      init_radar_scene();
285      posePaths = {
286        standing: new Path2D("M12 8C8 8 7 12 7 17L8 23H10L9 34H12L13 23H14L15 34H18L17 22H19L19 14C19 10 17 8 14 8 M10 13L10 22 M16 13L17 22 M10 5C10 0 17 0 17 5C17 10 10 10 10 5Z"),
287        sitting: new Path2D("M16 8C13 8 13 12 12 18L6 19L6 30L2 33H8L10 23L18 23L19 12 M16 12L13 18H6 M18 23L19 34 M20 15L21 26H10 M11 26L10 34 M15 5C15 0 22 0 22 5C22 10 15 10 15 5Z"),
288        hand: new Path2D("M8 35L7 29C3 25 3 19 3 14C3 12 5 12 6 15L8 21L6 5C6 2 9 2 10 5L12 18L10 2C10 -1 13 -1 14 2L16 18L15 3C15 0 18 0 19 3L20 19L20 9C20 6 23 6 23 9L23 24L28 19C31 17 32 20 29 23L24 30L21 32L21 35 M10 24C14 21 18 22 19 26")
289      };
290      drawPose = (context, x, y, height, pose) => {
291        context.save();
292        context.translate(x - height * 0.4, y - height / 2);
293        context.scale(height / 36, height / 36);
294        context.strokeStyle = palette.ink;
295        context.lineWidth = 0.9;
296        context.lineJoin = "round";
297        context.lineCap = "round";
298        context.stroke(posePaths[pose]);
299        context.restore();
300      };
301      drawBadge = (context, x, y, size, kind, color = "#ffffff") => {
302        context.save();
303        context.translate(x, y);
304        context.scale(size, size);
305        context.fillStyle = color;
306        if (kind === "wave") {
307          const gradient = context.createLinearGradient(0, 0, 0, 1);
308          gradient.addColorStop(0, radarColors.green);
309          gradient.addColorStop(1, radarColors.cyan);
310          context.fillStyle = gradient;
311        }
312        const transform = context.getTransform();
313        const scale = Math.hypot(transform.c, transform.d);
314        context.shadowColor = "rgba(0, 0, 0, 0.14)";
315        context.shadowBlur = scale * 0.09;
316        context.shadowOffsetY = scale * 0.035;
317        roundRect(context, 0, 0, 1, 1, 0.22);
318        context.fill();
319        context.shadowColor = "transparent";
320        context.strokeStyle = palette.ink;
321        context.lineWidth = 0.045;
322        context.lineCap = "round";
323        context.lineJoin = "round";
324        context.beginPath();
325        if (kind === "wave") {
326          context.moveTo(0.14, 0.59);
327          context.bezierCurveTo(0.4, 0.66, 0.23, 0.24, 0.42, 0.3);
328          context.bezierCurveTo(0.54, 0.34, 0.48, 0.78, 0.6, 0.5);
329          context.bezierCurveTo(0.72, 0.26, 0.65, 0.63, 0.85, 0.58);
330        } else if (kind === "download") {
331          context.fillStyle = "#ffffff";
332          roundRect(context, 0.25, 0.39, 0.5, 0.32, 0.035);
333          context.fill();
334          context.beginPath();
335          context.moveTo(0.5, 0.26);
336          context.lineTo(0.5, 0.57);
337          context.moveTo(0.4, 0.47);
338          context.lineTo(0.5, 0.57);
339          context.lineTo(0.6, 0.47);
340        } else if (kind === "samples") {
341          context.fillStyle = radarColors.cyan;
342          for (const level of [0.62, 0.46, 0.3]) {
343            context.beginPath();
344            context.ellipse(0.5, level, 0.26, 0.12, 0, 0, Math.PI * 2);
345            context.fill();
346            context.strokeStyle = "#ffffff";
347            context.lineWidth = 0.025;
348            context.stroke();
349          }
350          context.beginPath();
351        } else if (kind === "radar" || kind === "live") {
352          context.strokeStyle = kind === "live" ? radarColors.purple : palette.ink;
353          for (const radius of [0.12, 0.22, 0.32]) {
354            context.moveTo(0.5 + Math.cos(-0.4) * radius, 0.51 + Math.sin(-0.4) * radius);
355            context.arc(0.5, 0.51, radius, -0.4, Math.PI * 1.5);
356          }
357          context.moveTo(0.5, 0.51);
358          context.lineTo(0.67, 0.32);
359        } else if (kind === "network") {
360          context.strokeStyle = "#499fb9";
361          const nodes = [[0.25, 0.26], [0.25, 0.52], [0.25, 0.76], [0.51, 0.37], [0.51, 0.67], [0.78, 0.3], [0.78, 0.64]];
362          for (const [a, b] of [[0, 3], [0, 4], [1, 3], [1, 4], [2, 4], [3, 5], [3, 6], [4, 5], [4, 6]]) {
363            context.moveTo(...nodes[a]);
364            context.lineTo(...nodes[b]);
365          }
366          context.stroke();
367          context.fillStyle = "#b4e3ee";
368          for (const [nx, ny] of nodes) {
369            context.beginPath();
370            context.arc(nx, ny, 0.058, 0, Math.PI * 2);
371            context.fill();
372            context.stroke();
373          }
374          context.beginPath();
375        }
376        context.stroke();
377        context.restore();
378      };
379      drawHeader2 = (context, x, y, size, label, kind, color, wide = false) => {
380        drawBadge(context, x + size * 0.07, y + size * 0.07, size * 0.148, kind, color);
381        context.fillStyle = palette.ink;
382        context.textAlign = "left";
383        context.textBaseline = "middle";
384        const fontSize = size * 0.078;
385        context.font = `500 ${fontSize}px Arial, Helvetica, sans-serif`;
386        if (!wide) {
387          const width = context.measureText(label).width;
388          if (width > size * 0.68) context.font = `500 ${fontSize * size * 0.68 / width}px Arial, Helvetica, sans-serif`;
389        }
390        context.fillText(label, x + size * 0.254, y + size * 0.144);
391      };
392      drawButton = (context, x, y, width, height, label, border = "#aaaaaa") => {
393        context.fillStyle = "#ffffff";
394        context.strokeStyle = border;
395        context.lineWidth = height * 0.05;
396        roundRect(context, x, y, width, height, height * 0.08);
397        context.fill();
398        context.stroke();
399        context.fillStyle = palette.ink;
400        context.textAlign = "center";
401        context.textBaseline = "middle";
402        context.font = `500 ${height * 0.4}px Arial, Helvetica, sans-serif`;
403        context.fillText(label, x + width / 2, y + height * 0.52);
404      };
405      drawWaterfall = (context, x, y, width, size, waterfall) => {
406        drawCard(context, x, y, width, size);
407        drawHeader2(context, x, y, size, "Radar waterfall", "wave", radarColors.green, true);
408        const chartX = x + size * 0.07;
409        const chartY = y + size * 0.29;
410        const chartWidth = width - size * 0.34;
411        const chartHeight = size * 0.64;
412        context.save();
413        roundRect(context, chartX, chartY, chartWidth, chartHeight, size * 0.022);
414        context.clip();
415        context.imageSmoothingEnabled = true;
416        context.drawImage(waterfall, chartX, chartY, chartWidth, chartHeight);
417        context.restore();
418        const legendX = x + width - size * 0.24;
419        const gradient = context.createLinearGradient(0, chartY, 0, chartY + chartHeight);
420        gradient.addColorStop(0, radarColors.green);
421        gradient.addColorStop(0.5, radarColors.cyan);
422        gradient.addColorStop(1, radarColors.blue);
423        context.fillStyle = gradient;
424        roundRect(context, legendX, chartY, size * 0.12, chartHeight, size * 0.02);
425        context.fill();
426        context.fillStyle = palette.muted;
427        context.font = `400 ${size * 0.05}px Arial, Helvetica, sans-serif`;
428        context.textAlign = "left";
429        context.fillText("1", legendX + size * 0.14, chartY + size * 0.035);
430        context.fillText("0", legendX + size * 0.14, chartY + chartHeight - size * 0.025);
431      };
432      drawSamples = (context, x, y, size) => {
433        drawCard(context, x, y, size, size);
434        drawHeader2(context, x, y, size, "Samples", "samples");
435        context.fillStyle = "#000000";
436        context.font = `600 ${size * 0.23}px Arial, Helvetica, sans-serif`;
437        context.fillText("48", x + size * 0.07, y + size * 0.46);
438        context.fillStyle = palette.muted;
439        context.textAlign = "right";
440        context.font = `400 ${size * 0.05}px Arial, Helvetica, sans-serif`;
441        context.fillText("samples", x + size * 0.92, y + size * 0.41);
442        context.fillText("per save", x + size * 0.92, y + size * 0.465);
443        context.fillText("100", x + size * 0.88, y + size * 0.82);
444        context.textAlign = "left";
445        context.fillText("0", x + size * 0.07, y + size * 0.82);
446        context.fillStyle = "#f1f1f1";
447        roundRect(context, x + size * 0.07, y + size * 0.87, size * 0.81, size * 0.05, size * 0.025);
448        context.fill();
449        context.fillStyle = radarColors.cyan;
450        roundRect(context, x + size * 0.07, y + size * 0.87, size * 0.81 * 0.48, size * 0.05, size * 0.025);
451        context.fill();
452        context.fillStyle = "#d1d1d1";
453        context.beginPath();
454        context.arc(x + size * (0.07 + 0.81 * 0.48), y + size * 0.895, size * 0.05, 0, Math.PI * 2);
455        context.fill();
456      };
457      drawCollection = (context, x, y, size, kind, count, color) => {
458        drawCard(context, x, y, size, size);
459        drawHeader2(context, x, y, size, `Save data \u2013 ${kind === "hand" ? "hand up" : kind}`, "download", color);
460        const centerY = y + size * 0.49;
461        context.lineCap = "round";
462        context.lineWidth = size * 0.05;
463        context.strokeStyle = "#f2f2f2";
464        context.beginPath();
465        context.arc(x + size / 2, centerY, size * 0.18, 0, Math.PI * 2);
466        context.stroke();
467        context.strokeStyle = color;
468        context.beginPath();
469        context.arc(x + size / 2, centerY, size * 0.18, -Math.PI / 2, -Math.PI / 2 + Math.PI * 2 * count / 2e3);
470        context.stroke();
471        drawPose(context, x + size / 2, centerY, size * 0.18, kind);
472        context.fillStyle = palette.muted;
473        context.textAlign = "center";
474        context.font = `400 ${size * 0.059}px Arial, Helvetica, sans-serif`;
475        context.fillText(String(Math.floor(count)), x + size * 0.14, centerY);
476        context.fillText("2000", x + size * 0.86, centerY);
477        drawButton(context, x + size * 0.07, y + size * 0.765, size * 0.86, size * 0.165, "Save samples");
478      };
479      drawTraining = (context, x, y, size, elapsed) => {
480        drawCard(context, x, y, size, size, "#a4dfed");
481        context.save();
482        roundRect(context, x, y, size, size, size * 0.11);
483        context.clip();
484        context.beginPath();
485        context.moveTo(x, y + size);
486        for (let i = 0; i <= 80; i++) {
487          const t = i / 80;
488          const loss = Math.exp(-t * 5) * 0.49;
489          const noise = (Math.sin(i * 1.7 + elapsed * 0.8) + Math.cos(i * 3.1)) * 0.012 * (1 - t);
490          context.lineTo(x + t * size, y + size * (0.64 - loss + noise));
491        }
492        context.lineTo(x + size, y + size);
493        context.closePath();
494        context.fillStyle = "#83cce0";
495        context.fill();
496        context.strokeStyle = "#52abc4";
497        context.lineWidth = size * 8e-3;
498        context.stroke();
499        context.restore();
500        drawBadge(context, x + size * 0.07, y + size * 0.12, size * 0.148, "network");
501        context.fillStyle = "#000000";
502        context.textAlign = "right";
503        context.font = `600 ${size * 0.205}px Arial, Helvetica, sans-serif`;
504        context.fillText("95%", x + size * 0.93, y + size * 0.22);
505        drawButton(context, x + size * 0.07, y + size * 0.7, size * 0.86, size * 0.165, "Train", "#58b1ca");
506        context.fillStyle = "#eeeeee";
507        roundRect(context, x + size * 0.07, y + size * 0.905, size * 0.86, size * 0.05, size * 0.025);
508        context.fill();
509        context.save();
510        context.clip();
511        context.fillStyle = "#50adc7";
512        context.fillRect(x + size * 0.07, y + size * 0.905, size * (0.03 + elapsed % 12 / 12 * 0.83), size * 0.05);
513        context.restore();
514      };
515      drawInference = (context, x, y, width, size, angle) => {
516        drawCard(context, x, y, width, size);
517        drawHeader2(context, x, y, size, "Live inference", "live", "#ffffff", true);
518        const centerX = x + width / 2;
519        const centerY = y + size * 0.9;
520        const radius = size * 0.65;
521        const innerRadius = size * 0.37;
522        const poses = [null, "sitting", "standing", "hand"];
523        [radarColors.yellow, "#f59cbb", "#809ce9", "#a7d9b8"].forEach((color, index) => {
524          const start = Math.PI + index * Math.PI / 4 + 0.013;
525          const end = start + Math.PI / 4 - 0.026;
526          context.beginPath();
527          context.arc(centerX, centerY, radius, start, end);
528          context.arc(centerX, centerY, innerRadius, end, start, true);
529          context.closePath();
530          context.fillStyle = color;
531          context.fill();
532          if (poses[index]) {
533            const mid = (start + end) / 2;
534            drawPose(context, centerX + Math.cos(mid) * size * 0.51, centerY + Math.sin(mid) * size * 0.51, size * 0.21, poses[index]);
535          }
536        });
537        context.save();
538        context.translate(centerX, centerY - size * 0.015);
539        context.rotate(angle);
540        context.fillStyle = "#000000";
541        context.beginPath();
542        context.moveTo(size * 0.35, -size * 8e-3);
543        context.quadraticCurveTo(size * 0.365, 0, size * 0.35, size * 8e-3);
544        context.lineTo(0, size * 0.047);
545        context.arc(0, 0, size * 0.047, Math.PI / 2, Math.PI * 1.5);
546        context.closePath();
547        context.fill();
548        context.restore();
549      };
550      createWaterfall = () => {
551        const canvas = document.createElement("canvas");
552        canvas.width = 128;
553        canvas.height = 56;
554        const context = canvas.getContext("2d");
555        const image = context.createImageData(canvas.width, canvas.height);
556        const stops = [[53, 96, 230], [95, 193, 221], [151, 204, 164]];
557        let lastFrame = -1;
558        return {
559          canvas,
560          update(elapsed) {
561            const frame = Math.floor(elapsed * 10);
562            if (frame === lastFrame) return;
563            lastFrame = frame;
564            for (let col = 0; col < canvas.width; col++) {
565              const targets = getRadarTargets(elapsed - (canvas.width - 1 - col) * 0.075);
566              for (let row = 0; row < canvas.height; row++) {
567                const range = row / (canvas.height - 1);
568                let signal = 0.58 * Math.sin(range * Math.PI);
569                for (const person of targets) signal += Math.exp(-Math.pow((range - person.distance) / 0.1, 2)) * 0.16 * person.signal;
570                signal = Math.min(1, signal);
571                const segment = signal < 0.5 ? 0 : 1;
572                const mix = signal < 0.5 ? signal * 2 : (signal - 0.5) * 2;
573                const offset = (row * canvas.width + col) * 4;
574                for (let channel = 0; channel < 3; channel++) image.data[offset + channel] = stops[segment][channel] + (stops[segment + 1][channel] - stops[segment][channel]) * mix;
575                image.data[offset + 3] = 255;
576              }
577            }
578            context.putImageData(image, 0, 0);
579          }
580        };
581      };
582      drawRadarDashboard = (context, layout, state, waterfall) => {
583        const { x, y, size, gap, secondY, thirdY, middleX, lastX } = getDashboardGrid(layout);
584        context.save();
585        const titleY = layout.contentY + size * 0.14;
586        drawBadge(context, x, titleY - size * 0.075, size * 0.15, "radar", radarColors.cyan);
587        context.fillStyle = palette.ink;
588        context.textAlign = "left";
589        context.textBaseline = "middle";
590        context.font = `600 ${size * 0.11}px Arial, Helvetica, sans-serif`;
591        context.fillText("Radar demo", x + size * 0.2, titleY);
592        drawWaterfall(context, x, y, size * 2 + gap, size, waterfall);
593        drawSamples(context, lastX, y, size);
594        drawCollection(context, x, secondY, size, "sitting", state.counts.sitting, radarColors.pink);
595        drawCollection(context, middleX, secondY, size, "standing", state.counts.standing, radarColors.blue);
596        drawCollection(context, lastX, secondY, size, "hand", state.counts.hand, radarColors.green);
597        drawTraining(context, x, thirdY, size, state.elapsed);
598        drawInference(context, middleX, thirdY, size * 2 + gap, size, state.inferenceAngle);
599        context.restore();
600      };
601    }
602  });
603
604  // public:javascript/spacetimeos/radar.js
605  var radar_exports = {};
606  __export(radar_exports, {
607    createRadarDemo: () => createRadarDemo
608  });
609  var createRadarDemo;
610  var init_radar = __esm({
611    "public:javascript/spacetimeos/radar.js"() {
612      init_radar_scene();
613      init_radar_widgets();
614      createRadarDemo = () => {
615        const waterfall = createWaterfall();
616        const state = {
617          elapsed: 0,
618          targets: getRadarTargets(0),
619          counts: { sitting: 980, standing: 1480, hand: 532 },
620          selectedId: "standing",
621          inferenceAngle: Math.PI * 1.625
622        };
623        return {
624          drawScene(context, layout, { deltaTime }) {
625            state.elapsed += deltaTime;
626            state.targets = getRadarTargets(state.elapsed);
627            const detected = state.targets.filter((person) => person.detected);
628            const selected = detected.reduce((nearest, person) => !nearest || person.distance < nearest.distance ? person : nearest, null);
629            state.selectedId = selected?.id;
630            const sector = { sitting: 1, standing: 2, hand: 3 }[selected?.pose] ?? 0;
631            const angle = Math.PI + (sector + 0.5) * Math.PI / 4;
632            state.inferenceAngle += (angle - state.inferenceAngle) * Math.min(1, deltaTime * 4);
633            for (const person of detected) state.counts[person.pose] = Math.min(2e3, state.counts[person.pose] + deltaTime * 1.5);
634            waterfall.update(state.elapsed);
635            drawRadarScene(context, layout, state);
636          },
637          drawDashboard(context, layout) {
638            drawRadarDashboard(context, layout, state, waterfall.canvas);
639          }
640        };
641      };
642    }
643  });
644
645  // public:javascript/spacetimeos/interface.js
646  var laptopScreen = {
647    x: 441,
648    y: 103,
649    width: 914,
650    height: 605
651  };
652  var design = {
653    width: 2394,
654    height: 1454
655  };
656  var navbar = {
657    width: 95,
658    background: "#f1f1f1",
659    border: "#d8d8d8"
660  };
661  var topbar = {
662    height: 95,
663    background: "#ffffff",
664    border: "#d8d8d8"
665  };
666  var scaleX = (value, width) => value * width / design.width;
667  var scaleY = (value, height) => value * height / design.height;
668  var roundedRect = (context, x, y, width, height, radius) => {
669    const r = Math.min(radius, width / 2, height / 2);
670    context.beginPath();
671    context.moveTo(x + r, y);
672    context.lineTo(x + width - r, y);
673    context.quadraticCurveTo(x + width, y, x + width, y + r);
674    context.lineTo(x + width, y + height - r);
675    context.quadraticCurveTo(x + width, y + height, x + width - r, y + height);
676    context.lineTo(x + r, y + height);
677    context.quadraticCurveTo(x, y + height, x, y + height - r);
678    context.lineTo(x, y + r);
679    context.quadraticCurveTo(x, y, x + r, y);
680    context.closePath();
681  };
682  var pointInRect = (x, y, rect, padding = 0) => {
683    return x >= rect.x - padding && x <= rect.x + rect.width + padding && y >= rect.y - padding && y <= rect.y + rect.height + padding;
684  };
685  var isPointWalkable = (x, y, floorplan, padding = 0) => {
686    if (!floorplan) return false;
687    const insideFloorplan = x >= floorplan.bounds.x + padding && x <= floorplan.bounds.x + floorplan.bounds.width - padding && y >= floorplan.bounds.y + padding && y <= floorplan.bounds.y + floorplan.bounds.height - padding;
688    if (!insideFloorplan) return false;
689    return !floorplan.obstacles.some((obstacle) => {
690      return pointInRect(x, y, obstacle, padding);
691    });
692  };
693  var getInterfaceLayout = (width, height) => {
694    const navbarWidth = scaleX(navbar.width, width);
695    const topbarHeight = scaleY(topbar.height, height);
696    const contentX = navbarWidth;
697    const contentY = topbarHeight;
698    const contentWidth = width - navbarWidth;
699    const contentHeight = height - topbarHeight;
700    const contentHalfWidth = contentWidth / 2;
701    const leftContentX = contentX;
702    const rightContentX = contentX + contentHalfWidth;
703    return {
704      navbarWidth,
705      topbarHeight,
706      contentX,
707      contentY,
708      contentWidth,
709      contentHeight,
710      contentHalfWidth,
711      leftContentX,
712      rightContentX
713    };
714  };
715  var drawInterfaceBackground = (context, width, height, layout) => {
716    context.fillStyle = "#f7f7f7";
717    context.fillRect(0, 0, width, height);
718    context.fillStyle = "#ffffff";
719    context.fillRect(
720      layout.leftContentX,
721      layout.contentY,
722      layout.contentHalfWidth,
723      layout.contentHeight
724    );
725    context.fillStyle = "#f7f7f7";
726    context.fillRect(
727      layout.rightContentX,
728      layout.contentY,
729      layout.contentHalfWidth,
730      layout.contentHeight
731    );
732  };
733  var drawFloorplanWalls = (context, x, y, width, height) => {
734    const wallThickness = width * 0.018;
735    const wallColor = "#111400";
736    const wallX = x + width * 0.07;
737    const topWallY = y + height * 0.14;
738    const floorplanWidth = width - width * 0.07;
739    const floorplanHeight = height - topWallY + y;
740    const obstacles = [];
741    const shelves = [];
742    context.fillStyle = "#FCFCFC";
743    context.fillRect(
744      wallX,
745      topWallY,
746      floorplanWidth,
747      floorplanHeight
748    );
749    const aisleColor = "#eeeeee";
750    const aisleLeftX = wallX + floorplanWidth * 0.14;
751    const aisleRightX = wallX + floorplanWidth * 0.62;
752    const originalAisleWidth = floorplanWidth * 0.36;
753    const originalAisleHeight = floorplanHeight * 0.095;
754    const aisleWidth = originalAisleWidth * 0.85;
755    const aisleHeight = originalAisleHeight * 0.765;
756    const aisleStartY = topWallY + floorplanHeight * 0.15;
757    const aisleGapY = floorplanHeight * 0.155;
758    context.fillStyle = aisleColor;
759    for (let index = 0; index < 6; index++) {
760      const aisleY = aisleStartY + index * aisleGapY;
761      const leftShelf = {
762        id: `left-${index}`,
763        x: aisleLeftX + (originalAisleWidth - aisleWidth) / 2,
764        y: aisleY + (originalAisleHeight - aisleHeight) / 2,
765        width: aisleWidth,
766        height: aisleHeight
767      };
768      const rightShelf = {
769        id: `right-${index}`,
770        x: aisleRightX + (originalAisleWidth - aisleWidth) / 2,
771        y: aisleY + (originalAisleHeight - aisleHeight) / 2,
772        width: aisleWidth,
773        height: aisleHeight
774      };
775      obstacles.push(leftShelf, rightShelf);
776      shelves.push(leftShelf, rightShelf);
777      context.fillRect(
778        leftShelf.x,
779        leftShelf.y,
780        leftShelf.width,
781        leftShelf.height
782      );
783      context.fillRect(
784        rightShelf.x,
785        rightShelf.y,
786        rightShelf.width,
787        rightShelf.height
788      );
789    }
790    context.fillStyle = wallColor;
791    context.fillRect(
792      wallX,
793      topWallY,
794      floorplanWidth,
795      wallThickness
796    );
797    context.fillRect(
798      wallX,
799      topWallY,
800      wallThickness,
801      height * 0.15
802    );
803    context.fillRect(
804      wallX,
805      y + height * 0.54,
806      wallThickness,
807      height * 0.15
808    );
809    context.fillRect(
810      wallX,
811      y + height * 0.85,
812      wallThickness,
813      height * 0.15
814    );
815    const floorplan = {
816      bounds: {
817        x: wallX + wallThickness,
818        y: topWallY + wallThickness,
819        width: floorplanWidth - wallThickness,
820        height: floorplanHeight - wallThickness
821      },
822      obstacles,
823      spawnPoints: [],
824      slots: [],
825      pickupLaneX: wallX + wallThickness + floorplanWidth * 0.085
826    };
827    const gapOneStart = topWallY + height * 0.15;
828    const gapOneEnd = y + height * 0.54;
829    const gapTwoStart = y + height * 0.69;
830    const gapTwoEnd = y + height * 0.85;
831    const spawnX = wallX + wallThickness + floorplanWidth * 0.045;
832    floorplan.spawnPoints.push(
833      {
834        x: spawnX,
835        y: (gapOneStart + gapOneEnd) / 2
836      },
837      {
838        x: spawnX,
839        y: (gapTwoStart + gapTwoEnd) / 2
840      }
841    );
842    for (const shelf of shelves) {
843      const margin = Math.max(20, width * 0.035);
844      const candidates = [
845        {
846          x: shelf.x + shelf.width / 2,
847          y: shelf.y - margin
848        },
849        {
850          x: shelf.x + shelf.width / 2,
851          y: shelf.y + shelf.height + margin
852        }
853      ];
854      const approaches = candidates.filter((candidate) => {
855        return isPointWalkable(candidate.x, candidate.y, floorplan, Math.max(2.5, width * 4e-3));
856      });
857      if (approaches.length) {
858        const primaryApproach = approaches[0];
859        floorplan.slots.push({
860          id: shelf.id,
861          shelfX: shelf.x + shelf.width / 2,
862          shelfY: shelf.y + shelf.height / 2,
863          approachX: primaryApproach.x,
864          approachY: primaryApproach.y,
865          approaches
866        });
867      }
868    }
869    return floorplan;
870  };
871  var navigationIcons = [
872    new Path2D("M4 2H8Q10 2 10 4V8Q10 10 8 10H4Q2 10 2 8V4Q2 2 4 2Z M16 2H20Q22 2 22 4V8Q22 10 20 10H16Q14 10 14 8V4Q14 2 16 2Z M4 14H8Q10 14 10 16V20Q10 22 8 22H4Q2 22 2 20V16Q2 14 4 14Z M16 14H20Q22 14 22 16V20Q22 22 20 22H16Q14 22 14 20V16Q14 14 16 14Z"),
873    new Path2D("M3 4H21Q22 4 22 5V20Q22 21 21 21H3Q2 21 2 20V5Q2 4 3 4Z M2 7H22 M8 10L5 13.5L8 17 M16 10L19 13.5L16 17 M13.5 9.5L10.5 17.5"),
874    new Path2D("M4 9V22H21V9 M2 9L5 3H19L23 9 M2 9H23 M2 9C2 13 7 13 7 9C7 13 12 13 12 9C12 13 17 13 17 9C17 13 23 13 23 9 M7 22V15H12V22 M15 15H18V18H15Z"),
875    new Path2D("M2 6L12 0.5L23 7V21L17 24L12 21L8 24L2 20Z M2 6L12 12L23 7 M12 12V21 M8 24V15L17 10V24 M5 20V17L7 18V22 M19 22V17L21 16V21 M7 9L17 3.5"),
876    new Path2D("M12 0.5L18 4V11L12 14.5L6 11V4Z M6 4L12 7.5L18 4 M12 7.5V14.5 M6 11L12 14.5V21L6 24L0.5 20.5V14.5Z M0.5 14.5L6 18L12 14.5 M6 18V24 M18 11L23.5 14.5V20.5L18 24L12 21V14.5Z M12 14.5L18 18L23.5 14.5 M18 18V24")
877  ];
878  var drawNavigationIcons = (context, layout) => {
879    const size = layout.navbarWidth * 0.45;
880    const x = (layout.navbarWidth - size) / 2 - layout.navbarWidth * 0.035;
881    const top = layout.topbarHeight + layout.navbarWidth * 0.29;
882    context.save();
883    context.lineCap = "round";
884    context.lineJoin = "round";
885    navigationIcons.forEach((icon, index) => {
886      context.save();
887      context.translate(x, top + index * layout.navbarWidth);
888      context.scale(size / 24, size / 24);
889      context.strokeStyle = index === 0 ? "#252525" : "#999b9b";
890      context.lineWidth = index === 0 ? 1.65 : 1.25;
891      context.stroke(icon);
892      context.restore();
893    });
894    context.restore();
895  };
896  var drawInterfaceChrome = (context, width, height, layout) => {
897    context.fillStyle = "#ffffff";
898    context.fillRect(
899      layout.leftContentX,
900      layout.contentY,
901      layout.contentHalfWidth,
902      layout.contentHeight
903    );
904    context.strokeStyle = "#e3e3e3";
905    context.lineWidth = Math.max(1, scaleX(1, width));
906    context.beginPath();
907    context.moveTo(layout.rightContentX, layout.contentY);
908    context.lineTo(layout.rightContentX, height);
909    context.stroke();
910    context.fillStyle = navbar.background;
911    context.fillRect(
912      0,
913      layout.topbarHeight,
914      layout.navbarWidth,
915      height - layout.topbarHeight
916    );
917    context.strokeStyle = navbar.border;
918    context.lineWidth = Math.max(1, scaleX(1, width));
919    context.beginPath();
920    context.moveTo(layout.navbarWidth, layout.topbarHeight);
921    context.lineTo(layout.navbarWidth, height);
922    context.stroke();
923    drawNavigationIcons(context, layout);
924    context.fillStyle = topbar.background;
925    context.fillRect(0, 0, width, layout.topbarHeight);
926    context.strokeStyle = topbar.border;
927    context.lineWidth = Math.max(1, scaleY(1, height));
928    context.beginPath();
929    context.moveTo(0, layout.topbarHeight);
930    context.lineTo(width, layout.topbarHeight);
931    context.stroke();
932  };
933
934  // public:javascript/spacetimeos/screen.js
935  var mountSpacetimeScreen = (canvas, demo) => {
936    const figure = canvas?.closest("[data-spacetime-demo]");
937    const heroImage = figure?.querySelector("img") || document.querySelector(".software > img");
938    const frame = figure || heroImage?.parentElement;
939    if (!canvas || !heroImage || !frame) return;
940    const screen = figure ? {
941      x: Number(figure.dataset.screenX),
942      y: Number(figure.dataset.screenY),
943      width: Number(figure.dataset.screenWidth),
944      height: Number(figure.dataset.screenHeight)
945    } : laptopScreen;
946    const context = canvas.getContext("2d");
947    const reducedMotion = matchMedia("(prefers-reduced-motion: reduce)");
948    let animationFrame = null;
949    let lastTime = performance.now();
950    let inView = false;
951    let running = false;
952    const draw = (time = performance.now()) => {
953      const deltaTime = running ? Math.max(0, Math.min(0.04, (time - lastTime) / 1e3)) : 0;
954      lastTime = time;
955      const bounds = canvas.getBoundingClientRect();
956      const pixelRatio = (window.devicePixelRatio || 1) * (window.visualViewport?.scale || 1);
957      const bufferWidth = Math.max(1, Math.round(bounds.width * pixelRatio));
958      const bufferHeight = Math.max(1, Math.round(bounds.height * pixelRatio));
959      if (canvas.width !== bufferWidth || canvas.height !== bufferHeight) {
960        canvas.width = bufferWidth;
961        canvas.height = bufferHeight;
962      }
963      context.setTransform(bufferWidth / screen.width, 0, 0, bufferHeight / screen.height, 0, 0);
964      context.clearRect(0, 0, screen.width, screen.height);
965      const layout = getInterfaceLayout(screen.width, screen.height);
966      const clock = { time, deltaTime };
967      drawInterfaceBackground(context, screen.width, screen.height, layout);
968      demo.drawScene(context, layout, clock);
969      drawInterfaceChrome(context, screen.width, screen.height, layout);
970      demo.drawDashboard(context, layout, clock);
971      animationFrame = running ? requestAnimationFrame(draw) : null;
972    };
973    const positionCanvas = () => {
974      if (!heroImage.naturalWidth || !heroImage.naturalHeight) return;
975      const image = heroImage.getBoundingClientRect();
976      const frameBounds = frame.getBoundingClientRect();
977      const scaleX2 = image.width / heroImage.naturalWidth;
978      const scaleY2 = image.height / heroImage.naturalHeight;
979      canvas.style.setProperty("--screen-left", `${image.left - frameBounds.left + screen.x * scaleX2}px`);
980      canvas.style.setProperty("--screen-top", `${image.top - frameBounds.top + screen.y * scaleY2}px`);
981      canvas.style.setProperty("--screen-width", `${screen.width * scaleX2}px`);
982      canvas.style.setProperty("--screen-height", `${screen.height * scaleY2}px`);
983      if (animationFrame === null) draw();
984    };
985    const updateActivity = () => {
986      running = inView && !document.hidden && !reducedMotion.matches;
987      if (animationFrame !== null) cancelAnimationFrame(animationFrame);
988      animationFrame = null;
989      lastTime = performance.now();
990      if (running) animationFrame = requestAnimationFrame(draw);
991    };
992    const observer = new IntersectionObserver((entries) => {
993      inView = entries.some((entry) => entry.isIntersecting);
994      updateActivity();
995    });
996    observer.observe(canvas);
997    new ResizeObserver(positionCanvas).observe(frame);
998    new ResizeObserver(positionCanvas).observe(heroImage);
999    frame.addEventListener("product-artwork-layout", positionCanvas);
1000    document.addEventListener("visibilitychange", updateActivity);
1001    reducedMotion.addEventListener("change", updateActivity);
1002    window.addEventListener("resize", positionCanvas);
1003    window.visualViewport?.addEventListener("resize", positionCanvas);
1004    if (heroImage.complete) positionCanvas();
1005    else heroImage.addEventListener("load", positionCanvas, { once: true });
1006  };
1007
1008  // public:javascript/spacetimeos/widgets.js
1009  init_ui();
1010  var movedPalletHistory = [];
1011  var drawPalletWidget = (context, x, y, size, palletCount, maxPalletCount = 1) => {
1012    const value = Math.max(0, Math.round(palletCount || 0));
1013    context.save();
1014    drawCard(context, x, y, size, size, palette.orange);
1015    drawHeader(context, x, y, size, "Pallets on site", "circle");
1016    drawRing(context, x, y, size, value / Math.max(1, maxPalletCount), "#ffffff", "rgba(32, 32, 32, 0.18)");
1017    context.fillStyle = palette.ink;
1018    context.textAlign = "center";
1019    context.font = `600 ${size * 0.185}px Arial, Helvetica, sans-serif`;
1020    context.fillText(String(value), x + size / 2, y + size * 0.545);
1021    drawCaption(context, x, y, size, "TOTAL PALLETS", "#784000");
1022    context.restore();
1023  };
1024  var updateMovedPalletHistory = (value) => {
1025    const now = performance.now();
1026    const last = movedPalletHistory[movedPalletHistory.length - 1];
1027    if (last && now - last.time < 450) {
1028      last.value = value;
1029      return;
1030    }
1031    movedPalletHistory.push({ time: now, value });
1032    if (movedPalletHistory.length > 34) movedPalletHistory.shift();
1033  };
1034  var drawMovedPalletsChart = (context, x, y, size, value) => {
1035    context.save();
1036    drawCard(context, x, y, size, size);
1037    drawHeader(context, x, y, size, "Pallets moved", "chart", palette.yellow);
1038    const left = x + size * 0.09;
1039    const top = y + size * 0.34;
1040    const bottom = y + size * 0.72;
1041    const width = size * 0.82;
1042    const samples = movedPalletHistory.length ? movedPalletHistory : [{ value }];
1043    const points = samples.map((sample, index) => ({
1044      x: left + width * (samples.length > 1 ? index / (samples.length - 1) : 1),
1045      y: bottom - (bottom - top) * Math.max(0, Math.min(1, sample.value / 10))
1046    }));
1047    const gradient = context.createLinearGradient(0, top, 0, bottom);
1048    gradient.addColorStop(0, "rgba(255, 213, 91, 0.4)");
1049    gradient.addColorStop(1, "rgba(255, 213, 91, 0.05)");
1050    context.fillStyle = gradient;
1051    context.beginPath();
1052    context.moveTo(points[0].x, bottom);
1053    for (const point of points) context.lineTo(point.x, point.y);
1054    context.lineTo(points[points.length - 1].x, bottom);
1055    context.closePath();
1056    context.fill();
1057    context.strokeStyle = "#d9ad38";
1058    context.lineWidth = size * 0.012;
1059    context.lineCap = "round";
1060    context.lineJoin = "round";
1061    context.beginPath();
1062    points.forEach((point, index) => {
1063      if (index === 0) context.moveTo(point.x, point.y);
1064      else context.lineTo(point.x, point.y);
1065    });
1066    context.stroke();
1067    const last = points[points.length - 1];
1068    context.fillStyle = "#ffffff";
1069    context.beginPath();
1070    context.arc(last.x, last.y, size * 0.022, 0, Math.PI * 2);
1071    context.fill();
1072    context.stroke();
1073    drawCaption(context, x, y, size, "LAST 5 SECONDS");
1074    context.restore();
1075  };
1076  var drawLoadStatusWidget = (context, x, y, size, value, maxValue) => {
1077    const load = Math.max(0, Math.min(1, value / Math.max(1, maxValue)));
1078    context.save();
1079    drawCard(context, x, y, size, size);
1080    drawHeader(context, x, y, size, "Depot load", "triangle", palette.orange);
1081    drawRing(context, x, y, size, load, palette.orange, "#f2f2f2");
1082    const label = String(Math.round(load * 100));
1083    context.font = `600 ${size * 0.18}px Arial, Helvetica, sans-serif`;
1084    const numberWidth = context.measureText(label).width;
1085    context.font = `600 ${size * 0.105}px Arial, Helvetica, sans-serif`;
1086    const percentWidth = context.measureText("%").width;
1087    const left = x + (size - numberWidth - percentWidth) / 2;
1088    context.fillStyle = palette.ink;
1089    context.textBaseline = "alphabetic";
1090    context.font = `600 ${size * 0.18}px Arial, Helvetica, sans-serif`;
1091    context.fillText(label, left, y + size * 0.6);
1092    context.font = `600 ${size * 0.105}px Arial, Helvetica, sans-serif`;
1093    context.fillText("%", left + numberWidth, y + size * 0.6);
1094    drawCaption(context, x, y, size, "TOTAL DEPOT LOAD");
1095    context.restore();
1096  };
1097  var drawStaffWidget = (context, x, y, size, staffCount) => {
1098    context.save();
1099    drawCard(context, x, y, size, size, palette.orange);
1100    drawHeader(context, x, y, size, "Staff on site", "staff");
1101    context.fillStyle = palette.ink;
1102    context.textAlign = "center";
1103    context.font = `600 ${size * 0.26}px Arial, Helvetica, sans-serif`;
1104    context.fillText(String(Math.max(0, Math.round(staffCount || 0))), x + size / 2, y + size * 0.54);
1105    drawCaption(context, x, y, size, "TOTAL EMPLOYEES", "#784000");
1106    context.restore();
1107  };
1108  var drawForkliftWidget = (context, x, y, size, forkliftTimeSeconds) => {
1109    const seconds = Math.max(0, Math.round(forkliftTimeSeconds || 0));
1110    const time = `${Math.floor(seconds / 60).toString().padStart(2, "0")}:${(seconds % 60).toString().padStart(2, "0")}`;
1111    context.save();
1112    drawCard(context, x, y, size, size);
1113    drawHeader(context, x, y, size, "Forklift time", "clock", "#ff4356");
1114    context.fillStyle = palette.ink;
1115    context.textAlign = "center";
1116    context.font = `600 ${size * 0.2}px Arial, Helvetica, sans-serif`;
1117    context.fillText(time, x + size / 2, y + size * 0.54);
1118    drawCaption(context, x, y, size, "MINUTES / SECONDS");
1119    context.restore();
1120  };
1121  var rowLabel = (row) => `${row}${row === 1 ? "st" : row === 2 ? "nd" : row === 3 ? "rd" : "th"} Row`;
1122  var drawRowsWidget = (context, x, y, size, rowCounts) => {
1123    context.save();
1124    drawCard(context, x, y, size, size);
1125    drawHeader(context, x, y, size, "Warehouse rows", "rows");
1126    context.fillStyle = palette.muted;
1127    context.font = `400 ${size * 0.057}px Arial, Helvetica, sans-serif`;
1128    context.fillText("ROW", x + size * 0.07, y + size * 0.32);
1129    context.textAlign = "right";
1130    context.fillText("PALLETS", x + size * 0.93, y + size * 0.32);
1131    rowCounts.forEach((count, index) => {
1132      const rowY = y + size * (0.44 + index * 0.078);
1133      context.fillStyle = palette.ink;
1134      context.textAlign = "left";
1135      context.font = `400 ${size * 0.065}px Arial, Helvetica, sans-serif`;
1136      context.fillText(rowLabel(rowCounts.length - index), x + size * 0.07, rowY);
1137      context.textAlign = "right";
1138      context.font = `600 ${size * 0.065}px Arial, Helvetica, sans-serif`;
1139      context.fillText(String(count), x + size * 0.93, rowY);
1140    });
1141    context.restore();
1142  };
1143  var drawEmptyWidget = (context, x, y, size) => {
1144    context.save();
1145    context.fillStyle = "#f8f8f8";
1146    context.strokeStyle = "#f1f1f1";
1147    context.lineWidth = size * 7e-3;
1148    roundRect(context, x, y, size, size, size * 0.11);
1149    context.fill();
1150    context.stroke();
1151    context.strokeStyle = "#d1d1d1";
1152    roundRect(context, x + size * 0.445, y + size * 0.445, size * 0.11, size * 0.11, size * 0.025);
1153    context.stroke();
1154    context.beginPath();
1155    context.moveTo(x + size * 0.48, y + size * 0.5);
1156    context.lineTo(x + size * 0.52, y + size * 0.5);
1157    context.moveTo(x + size * 0.5, y + size * 0.48);
1158    context.lineTo(x + size * 0.5, y + size * 0.52);
1159    context.stroke();
1160    context.restore();
1161  };
1162  var drawDistributionWidget = (context, x, y, width, size, rowCounts, maxPerRow) => {
1163    const chartTop = y + size * 0.325;
1164    const chartBottom = y + size * 0.8;
1165    const chartLeft = x + size * 0.08;
1166    const chartWidth = width * 0.63;
1167    const barHeight = size * 0.064;
1168    context.save();
1169    drawCard(context, x, y, width, size, palette.blue);
1170    drawHeader(context, x, y, size, "Pallets distribution", "square", palette.bar, "#ffffff");
1171    context.lineWidth = size * 6e-3;
1172    context.setLineDash([size * 0.018, size * 0.018]);
1173    context.strokeStyle = "rgba(207, 224, 255, 0.6)";
1174    for (const ratio of [0, 1 / 3, 2 / 3, 1]) {
1175      const gridX = chartLeft + chartWidth * ratio;
1176      context.beginPath();
1177      context.moveTo(gridX, chartTop - barHeight * 0.6);
1178      context.lineTo(gridX, chartBottom + barHeight * 0.6);
1179      context.stroke();
1180    }
1181    context.setLineDash([]);
1182    context.fillStyle = palette.bar;
1183    context.textAlign = "right";
1184    context.font = `400 ${size * 0.065}px Arial, Helvetica, sans-serif`;
1185    rowCounts.forEach((count, index) => {
1186      const rowY = chartTop + index * (chartBottom - chartTop) / Math.max(1, rowCounts.length - 1);
1187      const barWidth = chartWidth * Math.max(0, Math.min(1, count / Math.max(1, maxPerRow)));
1188      context.fillStyle = palette.bar;
1189      if (barWidth > 0) {
1190        roundRect(context, chartLeft + size * 0.025, rowY - barHeight / 2, barWidth, barHeight, Math.min(barHeight, barWidth) / 2);
1191        context.fill();
1192      }
1193      context.fillStyle = "#a0b9f2";
1194      context.fillText(rowLabel(rowCounts.length - index), x + width - size * 0.08, rowY);
1195    });
1196    context.fillStyle = "rgba(160, 185, 242, 0.6)";
1197    context.textAlign = "center";
1198    context.font = `400 ${size * 0.06}px Arial, Helvetica, sans-serif`;
1199    for (const ratio of [0, 1 / 3, 2 / 3, 1]) {
1200      context.fillText(String(Math.round(maxPerRow * ratio)), chartLeft + chartWidth * ratio, y + size * 0.9);
1201    }
1202    context.restore();
1203  };
1204  var drawPalletDashboard = (context, layout, state) => {
1205    const { x, y, size, gap, secondY, thirdY, middleX, lastX } = getDashboardGrid(layout);
1206    const totalPallets = state.totalPallets || 0;
1207    const maxPallets = state.maxPallets || 1;
1208    const rowCounts = state.rowCounts || [];
1209    context.save();
1210    context.fillStyle = palette.ink;
1211    context.textAlign = "left";
1212    context.textBaseline = "middle";
1213    context.font = `600 ${size * 0.11}px Arial, Helvetica, sans-serif`;
1214    context.fillText("Warehouse", x + size * 0.012, layout.contentY + size * 0.14);
1215    context.restore();
1216    updateMovedPalletHistory(state.palletsMoved || 0);
1217    drawDistributionWidget(context, x, y, size * 2 + gap, size, rowCounts, state.maxPalletsPerRow || 1);
1218    drawPalletWidget(context, lastX, y, size, totalPallets, maxPallets);
1219    drawStaffWidget(context, x, secondY, size, state.staffCount);
1220    drawLoadStatusWidget(context, middleX, secondY, size, totalPallets, maxPallets);
1221    drawRowsWidget(context, lastX, secondY, size, rowCounts);
1222    drawMovedPalletsChart(context, x, thirdY, size, state.palletsMoved || 0);
1223    drawForkliftWidget(context, middleX, thirdY, size, state.forkliftTimeSeconds);
1224    drawEmptyWidget(context, lastX, thirdY, size);
1225  };
1226
1227  // public:javascript/spacetimeos/assets.js
1228  var drawPalletIcon = (context, x, y, size, withCircle = true) => {
1229    context.save();
1230    context.translate(x, y);
1231    if (withCircle) {
1232      context.save();
1233      context.shadowColor = "rgba(0, 0, 0, 0.12)";
1234      context.shadowBlur = size * 0.42;
1235      context.shadowOffsetY = size * 0.08;
1236      context.fillStyle = "#ffffff";
1237      context.beginPath();
1238      context.arc(0, 0, size * 0.95, 0, Math.PI * 2);
1239      context.fill();
1240      context.restore();
1241    }
1242    const boxSize = size * 0.72;
1243    const cellSize = boxSize / 3;
1244    const startX = -boxSize / 2;
1245    const startY = -boxSize / 2;
1246    context.fillStyle = "#F6D8A1";
1247    context.strokeStyle = "#C7AD7B";
1248    context.lineWidth = Math.max(1, size * 0.045);
1249    roundedRect(context, startX, startY, boxSize, boxSize, size * 0.08);
1250    context.fill();
1251    context.stroke();
1252    context.beginPath();
1253    for (let index = 1; index < 3; index++) {
1254      const lineX = startX + cellSize * index;
1255      context.moveTo(lineX, startY);
1256      context.lineTo(lineX, startY + boxSize);
1257      const lineY = startY + cellSize * index;
1258      context.moveTo(startX, lineY);
1259      context.lineTo(startX + boxSize, lineY);
1260    }
1261    context.stroke();
1262    context.restore();
1263  };
1264  var drawHumanIcon = (context, x, y, size, angle = 0, withCircle = true) => {
1265    context.save();
1266    context.translate(x, y);
1267    if (withCircle) {
1268      context.save();
1269      context.shadowColor = "rgba(0, 0, 0, 0.12)";
1270      context.shadowBlur = size * 0.42;
1271      context.shadowOffsetY = size * 0.08;
1272      context.fillStyle = "#ffffff";
1273      context.beginPath();
1274      context.arc(0, 0, size * 0.95, 0, Math.PI * 2);
1275      context.fill();
1276      context.restore();
1277    }
1278    context.rotate(angle + Math.PI);
1279    context.scale(size / 100, size / 100);
1280    const yellow = "#FFD98A";
1281    const skin = "#FFD3B8";
1282    const dark = "#484443";
1283    context.shadowColor = "rgba(0, 0, 0, 0.06)";
1284    context.shadowBlur = 4;
1285    context.shadowOffsetX = 0;
1286    context.shadowOffsetY = 1;
1287    context.fillStyle = yellow;
1288    roundedRect(context, -48, -23, 96, 38, 20);
1289    context.fill();
1290    context.fillStyle = dark;
1291    roundedRect(context, -25, 15, 9, 20, 5);
1292    context.fill();
1293    roundedRect(context, 16, 15, 9, 20, 5);
1294    context.fill();
1295    context.fillStyle = skin;
1296    context.beginPath();
1297    context.moveTo(-26, -4);
1298    context.quadraticCurveTo(-18, 15, 0, 28);
1299    context.quadraticCurveTo(18, 15, 26, -4);
1300    context.lineTo(20, 16);
1301    context.quadraticCurveTo(0, 35, -20, 16);
1302    context.closePath();
1303    context.fill();
1304    context.fillStyle = dark;
1305    context.beginPath();
1306    context.moveTo(-25, -17);
1307    context.quadraticCurveTo(-25, -50, 0, -51);
1308    context.quadraticCurveTo(27, -50, 27, -17);
1309    context.lineTo(27, 4);
1310    context.quadraticCurveTo(22, 3, 20, -3);
1311    context.lineTo(19, 8);
1312    context.quadraticCurveTo(0, 18, -19, 8);
1313    context.lineTo(-25, 4);
1314    context.closePath();
1315    context.fill();
1316    context.restore();
1317  };
1318  var drawForklift = (context, x, y, size, angle, carrying = false) => {
1319    context.save();
1320    context.translate(x, y);
1321    context.save();
1322    context.shadowColor = "rgba(0, 0, 0, 0.12)";
1323    context.shadowBlur = size * 0.45;
1324    context.shadowOffsetX = 0;
1325    context.shadowOffsetY = size * 0.08;
1326    context.fillStyle = "#ffffff";
1327    context.beginPath();
1328    context.arc(0, 0, size * 0.95, 0, Math.PI * 2);
1329    context.fill();
1330    context.restore();
1331    context.rotate(angle);
1332    context.scale(size / 100, size / 100);
1333    const yellow = "#F9C23A";
1334    const yellowDark = "#F1B72C";
1335    const yellowLight = "#FFD865";
1336    const dark = "#333333";
1337    const darkSoft = "#454545";
1338    context.shadowColor = "rgba(0, 0, 0, 0.06)";
1339    context.shadowBlur = 4;
1340    context.shadowOffsetX = 0;
1341    context.shadowOffsetY = 1;
1342    context.fillStyle = dark;
1343    roundedRect(context, 26, -38, 22, 15, 4);
1344    context.fill();
1345    roundedRect(context, 26, 23, 22, 15, 4);
1346    context.fill();
1347    roundedRect(context, -28, -34, 16, 18, 4);
1348    context.fill();
1349    roundedRect(context, -28, 16, 16, 18, 4);
1350    context.fill();
1351    context.fillStyle = yellow;
1352    roundedRect(context, -18, -34, 70, 68, 8);
1353    context.fill();
1354    context.fillStyle = yellowDark;
1355    roundedRect(context, 14, -26, 54, 52, 17);
1356    context.fill();
1357    context.fillStyle = darkSoft;
1358    roundedRect(context, -2, -21, 17, 42, 3);
1359    context.fill();
1360    context.fillStyle = yellowLight;
1361    roundedRect(context, 23, -17, 24, 6, 3);
1362    context.fill();
1363    roundedRect(context, 23, -6, 24, 6, 3);
1364    context.fill();
1365    roundedRect(context, 23, 5, 24, 6, 3);
1366    context.fill();
1367    roundedRect(context, 23, 16, 24, 6, 3);
1368    context.fill();
1369    context.fillStyle = dark;
1370    roundedRect(context, -50, -36, 7, 72, 3);
1371    context.fill();
1372    roundedRect(context, -43, -24, 22, 48, 4);
1373    context.fill();
1374    roundedRect(context, -96, -25, 52, 6, 3);
1375    context.fill();
1376    roundedRect(context, -96, 19, 52, 6, 3);
1377    context.fill();
1378    roundedRect(context, -58, -30, 7, 60, 3);
1379    context.fill();
1380    if (carrying) {
1381      drawPalletIcon(context, -118, 0, 34, false);
1382    }
1383    context.restore();
1384  };
1385  var drawWaitingPallets = (context, size, pallets) => {
1386    for (const pallet of pallets) {
1387      drawPalletIcon(
1388        context,
1389        pallet.x,
1390        pallet.y,
1391        size * 0.84,
1392        true
1393      );
1394    }
1395  };
1396  var drawTruckIcon = (context, x, y, size, angle = 0, withCircle = true) => {
1397    context.save();
1398    context.translate(x, y);
1399    if (withCircle) {
1400      context.save();
1401      context.shadowColor = "rgba(0, 0, 0, 0.12)";
1402      context.shadowBlur = size * 0.42;
1403      context.shadowOffsetY = size * 0.08;
1404      context.fillStyle = "#ffffff";
1405      context.beginPath();
1406      context.arc(0, 0, size * 0.95, 0, Math.PI * 2);
1407      context.fill();
1408      context.restore();
1409    }
1410    context.rotate(angle);
1411    context.scale(size / 100, size / 100);
1412    const yellow = "#F9C23A";
1413    const yellowLight = "#FFD865";
1414    const gray = "#D9D9D9";
1415    const grayDark = "#BFBFBF";
1416    const dark = "#333333";
1417    const red = "#EF3B24";
1418    context.shadowColor = "rgba(0, 0, 0, 0.06)";
1419    context.shadowBlur = 4;
1420    context.shadowOffsetX = 0;
1421    context.shadowOffsetY = 1;
1422    context.fillStyle = gray;
1423    roundedRect(context, -34, -28, 88, 56, 6);
1424    context.fill();
1425    context.fillStyle = grayDark;
1426    roundedRect(context, 48, -28, 6, 56, 3);
1427    context.fill();
1428    context.fillStyle = yellow;
1429    roundedRect(context, -72, -32, 42, 64, 10);
1430    context.fill();
1431    context.fillStyle = yellowLight;
1432    roundedRect(context, -65, -22, 24, 44, 8);
1433    context.fill();
1434    context.fillStyle = dark;
1435    roundedRect(context, -38, -20, 12, 40, 3);
1436    context.fill();
1437    roundedRect(context, -65, -40, 17, 10, 4);
1438    context.fill();
1439    roundedRect(context, -65, 30, 17, 10, 4);
1440    context.fill();
1441    roundedRect(context, 28, -37, 14, 9, 3);
1442    context.fill();
1443    roundedRect(context, 44, -37, 14, 9, 3);
1444    context.fill();
1445    roundedRect(context, 28, 28, 14, 9, 3);
1446    context.fill();
1447    roundedRect(context, 44, 28, 14, 9, 3);
1448    context.fill();
1449    context.fillStyle = red;
1450    roundedRect(context, 56, -20, 5, 10, 2);
1451    context.fill();
1452    roundedRect(context, 56, 10, 5, 10, 2);
1453    context.fill();
1454    context.restore();
1455  };
1456
1457  // public:javascript/spacetimeos/animation.js
1458  (async () => {
1459    const canvas = document.getElementById("spacetimeos-screen");
1460    if (!canvas) return;
1461    const demo = canvas.closest("[data-spacetime-demo]");
1462    if (demo?.dataset.spacetimeDemo === "radar") {
1463      const { createRadarDemo: createRadarDemo2 } = await Promise.resolve().then(() => (init_radar(), radar_exports));
1464      mountSpacetimeScreen(canvas, createRadarDemo2());
1465      return;
1466    }
1467    let nextForkliftId = 1;
1468    const createForklift = () => ({
1469      id: nextForkliftId++,
1470      x: 0,
1471      y: 0,
1472      angle: 0,
1473      speed: 70,
1474      initialized: false,
1475      mode: "idle",
1476      path: [],
1477      pathIndex: 0,
1478      targetPalletId: null,
1479      targetStoredPalletId: null,
1480      targetShelfId: null,
1481      deliverySlotId: null,
1482      preferredDeliverySlotId: null,
1483      carrying: null,
1484      lastMoveX: 1,
1485      lastMoveY: 0,
1486      lastX: 0,
1487      lastY: 0,
1488      stuckTime: 0,
1489      blockedMoveTime: 0,
1490      humanBlockedTime: 0,
1491      recoveryMode: false,
1492      recoveryPath: [],
1493      recoveryPathIndex: 0,
1494      recoveryAttempts: 0,
1495      maxRecoveryAttempts: 3,
1496      idleRetreatUntil: 0,
1497      idleRetreatDone: false
1498    });
1499    let forklift = createForklift();
1500    const forklifts = [forklift, createForklift()];
1501    const pallets = [];
1502    const deliveredPallets = [];
1503    const outboundPallets = [];
1504    const humans = [];
1505    const humanTrails = /* @__PURE__ */ new Map();
1506    const forkliftTrails = /* @__PURE__ */ new Map();
1507    const movedPalletEvents = [];
1508    const deliverySlotShuffle = [];
1509    const ignoredPallets = /* @__PURE__ */ new Map();
1510    const ignoredSlots = /* @__PURE__ */ new Map();
1511    let forkliftOperationTime = 0;
1512    const PALLETS_PER_SHELF = 4;
1513    const SHELF_ROW_COUNT = 6;
1514    const SHELF_COLUMN_COUNT = 2;
1515    const TOTAL_SHELF_COUNT = SHELF_ROW_COUNT * SHELF_COLUMN_COUNT;
1516    const HUMAN_COUNT = 5;
1517    const HUMAN_STUCK_SECONDS = 0.3;
1518    const HUMAN_ESCAPE_MS = 900;
1519    const HUMAN_STOP_DISTANCE_MULTIPLIER = 1.25;
1520    const HUMAN_HISTORY_LIMIT = 32;
1521    const TRAIL_LIFETIME_MS = 1500;
1522    const TRUCK_DOCK_MS = 850;
1523    const TRUCK_LOAD_MS = 950;
1524    const TRUCK_SPEED_MULTIPLIER = 5.2;
1525    const MAX_UNLOADED_PALLETS = 4;
1526    const MIN_TRUCK_UNLOAD_PALLETS = 2;
1527    const MAX_TRUCK_UNLOAD_PALLETS = 4;
1528    const STORED_PALLET_PICKUP_INTERVAL_MS = 6e3;
1529    const MOVED_PALLET_WINDOW_MS = 5e3;
1530    const WAREHOUSE_MIN_FILL_RATIO = 0.1;
1531    const WAREHOUSE_MAX_FILL_RATIO = 0.9;
1532    const WAREHOUSE_TARGET_FILL_EXPONENT = 0.45;
1533    const WAREHOUSE_FILL_PERIOD_MS = 9e4;
1534    const MIN_INBOUND_TRUCK_INTERVAL_MS = 2500;
1535    const MAX_INBOUND_TRUCK_INTERVAL_MS = 12e3;
1536    let nextPalletId = 1;
1537    let nextHumanId = 1;
1538    let nextPalletAt = 0;
1539    let nextStoredPalletPickupAt = 0;
1540    let lastFloorplan = null;
1541    let previousFloorplanSignature = "";
1542    let simulationSeeded = false;
1543    let lastDeliverySlotId = null;
1544    const inboundTruck = {
1545      x: 0,
1546      y: 0,
1547      angle: 0,
1548      state: "idle",
1549      startedAt: 0,
1550      dockUntil: 0,
1551      hidden: true
1552    };
1553    const outboundTruck = {
1554      x: 0,
1555      y: 0,
1556      angle: 0,
1557      state: "idle",
1558      startedAt: 0,
1559      dockUntil: 0,
1560      hidden: true
1561    };
1562    const shuffleArray = (items) => {
1563      for (let i = items.length - 1; i > 0; i--) {
1564        const j = Math.floor(Math.random() * (i + 1));
1565        [items[i], items[j]] = [items[j], items[i]];
1566      }
1567      return items;
1568    };
1569    const getFloorplanSignature = (floorplan) => [
1570      Math.round(floorplan.bounds.x),
1571      Math.round(floorplan.bounds.y),
1572      Math.round(floorplan.bounds.width),
1573      Math.round(floorplan.bounds.height)
1574    ].join(":");
1575    const segmentHitsObstacle = (x1, y1, x2, y2, floorplan, padding) => {
1576      const steps = Math.max(10, Math.ceil(Math.hypot(x2 - x1, y2 - y1) / 6));
1577      for (let i = 0; i <= steps; i++) {
1578        const t = i / steps;
1579        const x = x1 + (x2 - x1) * t;
1580        const y = y1 + (y2 - y1) * t;
1581        if (!isPointWalkable(x, y, floorplan, padding)) return true;
1582      }
1583      return false;
1584    };
1585    const getForkliftPadding = (size) => Math.max(2.5, size * 0.28);
1586    const getForkliftComfortPadding = (size) => Math.max(getForkliftPadding(size), size * 0.42);
1587    const getForkliftPathPadding = (size) => Math.max(getForkliftComfortPadding(size), size * 0.52);
1588    const getHumanPadding = (size) => Math.max(1.4, size * 0.16);
1589    const getHumanRadius = (size) => Math.max(4, size * 0.58);
1590    const getHumanIconSize = (size) => Math.max(7.2, size * 0.82);
1591    const getRandomWalkablePoint = (floorplan, padding) => {
1592      for (let attempt = 0; attempt < 260; attempt++) {
1593        const x = floorplan.bounds.x + Math.random() * floorplan.bounds.width;
1594        const y = floorplan.bounds.y + Math.random() * floorplan.bounds.height;
1595        if (isPointWalkable(x, y, floorplan, padding)) {
1596          return { x, y };
1597        }
1598      }
1599      return {
1600        x: floorplan.bounds.x + floorplan.bounds.width * 0.5,
1601        y: floorplan.bounds.y + floorplan.bounds.height * 0.5
1602      };
1603    };
1604    const getNearestWalkablePoint = (x, y, floorplan, padding) => {
1605      if (isPointWalkable(x, y, floorplan, padding)) {
1606        return { x, y };
1607      }
1608      const maxRadius = Math.max(floorplan.bounds.width, floorplan.bounds.height);
1609      const step = Math.max(4, padding * 0.75);
1610      for (let radius = step; radius <= maxRadius; radius += step) {
1611        const samples = Math.max(16, Math.ceil(radius / 2));
1612        for (let i = 0; i < samples; i++) {
1613          const angle = Math.PI * 2 * i / samples;
1614          const candidate = {
1615            x: x + Math.cos(angle) * radius,
1616            y: y + Math.sin(angle) * radius
1617          };
1618          if (isPointWalkable(candidate.x, candidate.y, floorplan, padding)) {
1619            return candidate;
1620          }
1621        }
1622      }
1623      return getRandomWalkablePoint(floorplan, padding);
1624    };
1625    const getWalkableClearance = (x, y, floorplan, padding, maxDistance) => {
1626      if (!isPointWalkable(x, y, floorplan, padding)) return 0;
1627      let minClearance = maxDistance;
1628      const step = Math.max(2, padding * 0.6);
1629      for (let d = 0; d < 20; d++) {
1630        const angle = Math.PI * 2 * d / 20;
1631        for (let r = step; r <= maxDistance; r += step) {
1632          if (!isPointWalkable(x + Math.cos(angle) * r, y + Math.sin(angle) * r, floorplan, padding)) {
1633            minClearance = Math.min(minClearance, r);
1634            break;
1635          }
1636        }
1637      }
1638      return minClearance;
1639    };
1640    const findPath = (startX, startY, endX, endY, floorplan, padding) => {
1641      const safeStart = getNearestWalkablePoint(startX, startY, floorplan, padding);
1642      const safeEnd = getNearestWalkablePoint(endX, endY, floorplan, padding);
1643      startX = safeStart.x;
1644      startY = safeStart.y;
1645      endX = safeEnd.x;
1646      endY = safeEnd.y;
1647      if (!segmentHitsObstacle(startX, startY, endX, endY, floorplan, padding)) {
1648        return [{ x: endX, y: endY }];
1649      }
1650      const cellSize = Math.max(5, Math.min(8, floorplan.bounds.width * 0.018));
1651      const cols = Math.ceil(floorplan.bounds.width / cellSize);
1652      const rows = Math.ceil(floorplan.bounds.height / cellSize);
1653      const toCell = (x, y) => ({
1654        cx: Math.max(0, Math.min(cols - 1, Math.floor((x - floorplan.bounds.x) / cellSize))),
1655        cy: Math.max(0, Math.min(rows - 1, Math.floor((y - floorplan.bounds.y) / cellSize)))
1656      });
1657      const toPoint = (cx, cy) => ({
1658        x: floorplan.bounds.x + cx * cellSize + cellSize / 2,
1659        y: floorplan.bounds.y + cy * cellSize + cellSize / 2
1660      });
1661      const key = (cx, cy) => `${cx},${cy}`;
1662      const isCellWalkable = (cx, cy) => {
1663        if (cx < 0 || cx >= cols || cy < 0 || cy >= rows) return false;
1664        const point = toPoint(cx, cy);
1665        return isPointWalkable(point.x, point.y, floorplan, padding);
1666      };
1667      const nearestWalkableCell = (cell) => {
1668        if (isCellWalkable(cell.cx, cell.cy)) return cell;
1669        for (let r = 1; r < Math.max(cols, rows); r++) {
1670          for (let dy = -r; dy <= r; dy++) {
1671            for (let dx = -r; dx <= r; dx++) {
1672              const cx = cell.cx + dx;
1673              const cy = cell.cy + dy;
1674              if (isCellWalkable(cx, cy)) return { cx, cy };
1675            }
1676          }
1677        }
1678        return null;
1679      };
1680      const startCell = nearestWalkableCell(toCell(startX, startY));
1681      const endCell = nearestWalkableCell(toCell(endX, endY));
1682      if (!startCell || !endCell) return [];
1683      const open = [{
1684        cx: startCell.cx,
1685        cy: startCell.cy,
1686        g: 0,
1687        f: Math.hypot(endCell.cx - startCell.cx, endCell.cy - startCell.cy),
1688        parent: null
1689      }];
1690      const visited = /* @__PURE__ */ new Map();
1691      const queued = /* @__PURE__ */ new Map();
1692      const directions = [[1, 0], [-1, 0], [0, 1], [0, -1], [1, 1], [1, -1], [-1, 1], [-1, -1]];
1693      queued.set(key(startCell.cx, startCell.cy), open[0]);
1694      while (open.length) {
1695        open.sort((a, b) => a.f - b.f);
1696        const current = open.shift();
1697        const currentKey = key(current.cx, current.cy);
1698        if (visited.has(currentKey)) continue;
1699        visited.set(currentKey, current);
1700        if (current.cx === endCell.cx && current.cy === endCell.cy) {
1701          const rawPath = [];
1702          let node = current;
1703          while (node) {
1704            rawPath.unshift(toPoint(node.cx, node.cy));
1705            node = node.parent;
1706          }
1707          const simplified = [];
1708          for (const point of rawPath) {
1709            if (!simplified.length) {
1710              simplified.push(point);
1711              continue;
1712            }
1713            if (simplified.length < 2 || segmentHitsObstacle(
1714              simplified[simplified.length - 2].x,
1715              simplified[simplified.length - 2].y,
1716              point.x,
1717              point.y,
1718              floorplan,
1719              padding
1720            )) {
1721              simplified.push(point);
1722            } else {
1723              simplified[simplified.length - 1] = point;
1724            }
1725          }
1726          const result = simplified.slice(1);
1727          const last = result[result.length - 1];
1728          if (!last || Math.hypot(last.x - endX, last.y - endY) > 1) {
1729            result.push({ x: endX, y: endY });
1730          }
1731          return result;
1732        }
1733        for (const [dx, dy] of directions) {
1734          const nextCx = current.cx + dx;
1735          const nextCy = current.cy + dy;
1736          const nextKey = key(nextCx, nextCy);
1737          if (visited.has(nextKey) || !isCellWalkable(nextCx, nextCy)) continue;
1738          if (dx !== 0 && dy !== 0) {
1739            if (!isCellWalkable(current.cx + dx, current.cy) || !isCellWalkable(current.cx, current.cy + dy)) {
1740              continue;
1741            }
1742          }
1743          const moveCost = dx !== 0 && dy !== 0 ? 1.414 : 1;
1744          const g = current.g + moveCost;
1745          const h = Math.hypot(endCell.cx - nextCx, endCell.cy - nextCy);
1746          const existing = queued.get(nextKey);
1747          if (!existing || g < existing.g) {
1748            const node = {
1749              cx: nextCx,
1750              cy: nextCy,
1751              g,
1752              f: g + h,
1753              parent: current
1754            };
1755            queued.set(nextKey, node);
1756            open.push(node);
1757          }
1758        }
1759      }
1760      return [];
1761    };
1762    const getShelfRectFromSlotData = (slot) => {
1763      const rect = slot.rect || slot.bounds || slot.shelf || slot.shelfRect || slot.storageRect || slot.area;
1764      if (rect && Number.isFinite(rect.x) && Number.isFinite(rect.y) && Number.isFinite(rect.width) && Number.isFinite(rect.height)) {
1765        return {
1766          x: rect.x,
1767          y: rect.y,
1768          width: rect.width,
1769          height: rect.height
1770        };
1771      }
1772      const x = slot.shelfX ?? slot.storageX ?? slot.palletX ?? slot.slotX;
1773      const y = slot.shelfY ?? slot.storageY ?? slot.palletY ?? slot.slotY;
1774      const width = slot.shelfWidth ?? slot.storageWidth ?? slot.slotWidth ?? slot.width ?? slot.w;
1775      const height = slot.shelfHeight ?? slot.storageHeight ?? slot.slotHeight ?? slot.height ?? slot.h;
1776      if (Number.isFinite(x) && Number.isFinite(y) && Number.isFinite(width) && Number.isFinite(height)) {
1777        return { x, y, width, height };
1778      }
1779      return null;
1780    };
1781    const getRealShelvesFromFloorplan = (floorplan) => {
1782      if (!Array.isArray(floorplan.slots) || floorplan.slots.length < TOTAL_SHELF_COUNT) return [];
1783      const shelves = floorplan.slots.map((slot, index) => {
1784        const rect = getShelfRectFromSlotData(slot);
1785        if (!rect) return null;
1786        return {
1787          id: slot.id ?? `shelf-${index}`,
1788          index,
1789          rowIndex: Math.floor(index / SHELF_COLUMN_COUNT),
1790          columnIndex: index % SHELF_COLUMN_COUNT,
1791          rect,
1792          sourceSlot: slot
1793        };
1794      }).filter(Boolean);
1795      if (shelves.length < TOTAL_SHELF_COUNT) return [];
1796      return shelves.slice(0, TOTAL_SHELF_COUNT).sort((a, b) => {
1797        const centerYA = a.rect.y + a.rect.height * 0.5;
1798        const centerYB = b.rect.y + b.rect.height * 0.5;
1799        if (Math.abs(centerYA - centerYB) > Math.min(a.rect.height, b.rect.height) * 0.6) {
1800          return centerYA - centerYB;
1801        }
1802        return a.rect.x - b.rect.x;
1803      }).map((shelf, index) => ({
1804        ...shelf,
1805        index,
1806        rowIndex: Math.floor(index / SHELF_COLUMN_COUNT),
1807        columnIndex: index % SHELF_COLUMN_COUNT
1808      }));
1809    };
1810    const getVirtualWarehouseShelves = (floorplan) => {
1811      const shelves = [];
1812      const bounds = floorplan.bounds;
1813      const shelfWidth = bounds.width * 0.31;
1814      const shelfHeight = bounds.height * 0.068;
1815      const leftColumnX = bounds.x + bounds.width * 0.165;
1816      const rightColumnX = bounds.x + bounds.width * 0.625;
1817      const firstRowY = bounds.y + bounds.height * 0.145;
1818      const rowGap = bounds.height * 0.145;
1819      for (let rowIndex = 0; rowIndex < SHELF_ROW_COUNT; rowIndex++) {
1820        for (let columnIndex = 0; columnIndex < SHELF_COLUMN_COUNT; columnIndex++) {
1821          const index = rowIndex * SHELF_COLUMN_COUNT + columnIndex;
1822          shelves.push({
1823            id: `shelf-${rowIndex}-${columnIndex}`,
1824            index,
1825            rowIndex,
1826            columnIndex,
1827            rect: {
1828              x: columnIndex === 0 ? leftColumnX : rightColumnX,
1829              y: firstRowY + rowIndex * rowGap,
1830              width: shelfWidth,
1831              height: shelfHeight
1832            },
1833            sourceSlot: null
1834          });
1835        }
1836      }
1837      return shelves;
1838    };
1839    const getWarehouseShelves = (floorplan) => {
1840      const realShelves = getRealShelvesFromFloorplan(floorplan);
1841      return realShelves.length >= TOTAL_SHELF_COUNT ? realShelves : getVirtualWarehouseShelves(floorplan);
1842    };
1843    const getShelfApproachPoints = (shelf, floorplan, size) => {
1844      const rect = shelf.rect;
1845      const centerX = rect.x + rect.width * 0.5;
1846      const centerY = rect.y + rect.height * 0.5;
1847      const points = [];
1848      const sourceSlot = shelf.sourceSlot;
1849      if (sourceSlot?.approaches?.length) {
1850        for (const approach of sourceSlot.approaches) {
1851          points.push({ x: approach.x, y: approach.y });
1852        }
1853      } else if (Number.isFinite(sourceSlot?.approachX) && Number.isFinite(sourceSlot?.approachY)) {
1854        points.push({
1855          x: sourceSlot.approachX,
1856          y: sourceSlot.approachY
1857        });
1858      }
1859      points.push(
1860        { x: rect.x - size * 1.45, y: centerY },
1861        { x: rect.x - size * 2.35, y: centerY },
1862        { x: rect.x - size * 1.45, y: centerY - size * 1.15 },
1863        { x: rect.x - size * 1.45, y: centerY + size * 1.15 },
1864        { x: rect.x + rect.width + size * 1.45, y: centerY },
1865        { x: rect.x + rect.width + size * 1.45, y: centerY - size * 1.15 },
1866        { x: rect.x + rect.width + size * 1.45, y: centerY + size * 1.15 },
1867        { x: centerX, y: rect.y - size * 1.2 },
1868        { x: centerX, y: rect.y + rect.height + size * 1.2 }
1869      );
1870      const unique = [];
1871      const keys = /* @__PURE__ */ new Set();
1872      for (const point of points) {
1873        if (!Number.isFinite(point.x) || !Number.isFinite(point.y)) continue;
1874        const safePoint = getNearestWalkablePoint(point.x, point.y, floorplan, getHumanPadding(size));
1875        const key = `${Math.round(safePoint.x)},${Math.round(safePoint.y)}`;
1876        if (keys.has(key)) continue;
1877        keys.add(key);
1878        unique.push(safePoint);
1879      }
1880      return unique;
1881    };
1882    const getNextRandomDeliverySlotId = (slotIds) => {
1883      const slotIdSet = new Set(slotIds);
1884      for (let i = deliverySlotShuffle.length - 1; i >= 0; i--) {
1885        if (!slotIdSet.has(deliverySlotShuffle[i])) deliverySlotShuffle.splice(i, 1);
1886      }
1887      if (!deliverySlotShuffle.length) {
1888        deliverySlotShuffle.push(...slotIds);
1889        shuffleArray(deliverySlotShuffle);
1890        if (deliverySlotShuffle.length > 1 && deliverySlotShuffle[0] === lastDeliverySlotId) {
1891          const swapIndex = deliverySlotShuffle.findIndex((slotId) => slotId !== lastDeliverySlotId);
1892          if (swapIndex > 0) {
1893            [deliverySlotShuffle[0], deliverySlotShuffle[swapIndex]] = [
1894              deliverySlotShuffle[swapIndex],
1895              deliverySlotShuffle[0]
1896            ];
1897          }
1898        }
1899      }
1900      return deliverySlotShuffle.shift();
1901    };
1902    const getPalletShelfId = (pallet) => pallet.shelfId ?? pallet.slotId;
1903    const setActiveForklift = (activeForklift) => {
1904      forklift = activeForklift;
1905    };
1906    const getOtherForklifts = () => forklifts.filter((item) => item !== forklift);
1907    const isPalletTargetedByOtherForklift = (palletId) => {
1908      return getOtherForklifts().some((item) => item.targetPalletId === palletId);
1909    };
1910    const isStoredPalletTargetedByOtherForklift = (palletId) => {
1911      return getOtherForklifts().some((item) => item.targetStoredPalletId === palletId);
1912    };
1913    const getUpperSpawnPoint = (floorplan) => {
1914      return floorplan.spawnPoints?.[0] || null;
1915    };
1916    const getLowerSpawnPoint = (floorplan) => {
1917      return floorplan.spawnPoints?.[1] || floorplan.spawnPoints?.[0] || null;
1918    };
1919    const getDeliveredPalletCountForShelf = (shelfId) => {
1920      return deliveredPallets.filter((pallet) => getPalletShelfId(pallet) === shelfId).length;
1921    };
1922    const isShelfFull = (shelfId) => getDeliveredPalletCountForShelf(shelfId) >= PALLETS_PER_SHELF;
1923    const removeOldestDeliveredPalletIfWarehouseIsFull = (floorplan) => {
1924      const maxStoredPallets = getWarehouseShelves(floorplan).length * PALLETS_PER_SHELF;
1925      while (deliveredPallets.length >= maxStoredPallets && deliveredPallets.length) {
1926        deliveredPallets.shift();
1927      }
1928    };
1929    const assignRandomDeliveryShelfForCurrentPallet = (floorplan) => {
1930      const shelves = getWarehouseShelves(floorplan);
1931      if (!shelves.length) {
1932        forklift.preferredDeliverySlotId = null;
1933        return null;
1934      }
1935      const shelfId = getNextRandomDeliverySlotId(shelves.map((shelf) => shelf.id));
1936      forklift.preferredDeliverySlotId = shelfId;
1937      return shelfId;
1938    };
1939    const getStoredPalletRenderItems = (floorplan, size) => {
1940      const shelvesById = new Map(getWarehouseShelves(floorplan).map((shelf) => [shelf.id, shelf]));
1941      const countByShelfId = /* @__PURE__ */ new Map();
1942      return deliveredPallets.map((pallet) => {
1943        const shelfId = getPalletShelfId(pallet);
1944        const shelf = shelvesById.get(shelfId);
1945        if (!shelf) return null;
1946        const indexOnShelf = countByShelfId.get(shelf.id) || 0;
1947        countByShelfId.set(shelf.id, indexOnShelf + 1);
1948        if (indexOnShelf >= PALLETS_PER_SHELF) return null;
1949        const shelfRect = shelf.rect;
1950        const iconSpacing = Math.min(size * 2.15, shelfRect.width / PALLETS_PER_SHELF);
1951        const leftPadding = Math.max(size * 0.8, shelfRect.width * 0.08);
1952        const maxX = shelfRect.x + shelfRect.width - size * 1.1;
1953        return {
1954          id: pallet.id,
1955          x: Math.min(shelfRect.x + leftPadding + indexOnShelf * iconSpacing, maxX),
1956          y: shelfRect.y + shelfRect.height * 0.5
1957        };
1958      }).filter(Boolean);
1959    };
1960    const seedInitialStoredPallets = (floorplan) => {
1961      const shelves = getWarehouseShelves(floorplan);
1962      const maxStoredPallets = shelves.length * PALLETS_PER_SHELF;
1963      if (!shelves.length || maxStoredPallets <= 0) return;
1964      const minPallets = Math.min(maxStoredPallets, Math.max(SHELF_ROW_COUNT, Math.round(maxStoredPallets * 0.22)));
1965      const maxPallets = Math.min(maxStoredPallets, Math.max(minPallets, Math.round(maxStoredPallets * 0.62)));
1966      const palletCount = minPallets + Math.floor(Math.random() * (maxPallets - minPallets + 1));
1967      const remainingCapacityByShelfId = new Map(shelves.map((shelf) => [shelf.id, PALLETS_PER_SHELF]));
1968      let availableShelves = [...shelves];
1969      for (let index = 0; index < palletCount && availableShelves.length; index++) {
1970        const shelf = availableShelves[Math.floor(Math.random() * availableShelves.length)];
1971        const remainingCapacity = remainingCapacityByShelfId.get(shelf.id) || 0;
1972        if (remainingCapacity <= 0) {
1973          availableShelves = availableShelves.filter((item) => item.id !== shelf.id);
1974          index--;
1975          continue;
1976        }
1977        deliveredPallets.push({
1978          id: nextPalletId++,
1979          shelfId: shelf.id,
1980          slotId: shelf.id
1981        });
1982        remainingCapacityByShelfId.set(shelf.id, remainingCapacity - 1);
1983        if (remainingCapacity - 1 <= 0) {
1984          availableShelves = availableShelves.filter((item) => item.id !== shelf.id);
1985        }
1986      }
1987    };
1988    const seedForkliftPosition = (floorplan, size) => {
1989      const padding = getForkliftPadding(size);
1990      const point = getRandomWalkablePoint(floorplan, padding);
1991      const angle = Math.random() * Math.PI * 2;
1992      forklift.x = point.x;
1993      forklift.y = point.y;
1994      forklift.angle = angle;
1995      forklift.initialized = true;
1996      forklift.mode = "idle";
1997      forklift.path = [];
1998      forklift.pathIndex = 0;
1999      forklift.targetPalletId = null;
2000      forklift.targetStoredPalletId = null;
2001      forklift.targetShelfId = null;
2002      forklift.deliverySlotId = null;
2003      forklift.preferredDeliverySlotId = null;
2004      forklift.carrying = null;
2005      forklift.lastMoveX = Math.cos(angle + Math.PI);
2006      forklift.lastMoveY = Math.sin(angle + Math.PI);
2007      forklift.lastX = point.x;
2008      forklift.lastY = point.y;
2009      forklift.stuckTime = 0;
2010      forklift.blockedMoveTime = 0;
2011      forklift.humanBlockedTime = 0;
2012      forklift.idleRetreatDone = false;
2013      forklift.idleRetreatUntil = 0;
2014      clearForkliftPath();
2015      clearRecoveryPath();
2016    };
2017    const seedInitialSimulationState = (floorplan, size, time) => {
2018      if (simulationSeeded) return;
2019      pallets.length = 0;
2020      deliveredPallets.length = 0;
2021      outboundPallets.length = 0;
2022      humans.length = 0;
2023      movedPalletEvents.length = 0;
2024      clearAllHumanTrails();
2025      clearAllForkliftTrails();
2026      deliverySlotShuffle.length = 0;
2027      ignoredPallets.clear();
2028      ignoredSlots.clear();
2029      lastDeliverySlotId = null;
2030      inboundTruck.state = "idle";
2031      inboundTruck.hidden = true;
2032      outboundTruck.state = "idle";
2033      outboundTruck.hidden = true;
2034      seedInitialStoredPallets(floorplan);
2035      for (const activeForklift of forklifts) {
2036        setActiveForklift(activeForklift);
2037        seedForkliftPosition(floorplan, size);
2038      }
2039      setActiveForklift(forklifts[0]);
2040      spawnMissingHumans(floorplan, size);
2041      nextPalletAt = time + getInboundTruckDelay(time, floorplan);
2042      nextStoredPalletPickupAt = performance.now() + 2500 + Math.random() * STORED_PALLET_PICKUP_INTERVAL_MS;
2043      simulationSeeded = true;
2044    };
2045    const getForkliftHomePoint = (floorplan, padding) => {
2046      return getNearestWalkablePoint(
2047        floorplan.bounds.x + Math.max(padding * 3, floorplan.bounds.width * 0.035),
2048        floorplan.bounds.y + Math.max(padding * 3, floorplan.bounds.height * 0.055),
2049        floorplan,
2050        padding
2051      );
2052    };
2053    const getCurrentWaypoint = () => {
2054      if (forklift.recoveryMode && forklift.recoveryPath.length > 0) {
2055        return forklift.recoveryPath[forklift.recoveryPathIndex] || null;
2056      }
2057      return forklift.path[forklift.pathIndex] || null;
2058    };
2059    const clearForkliftPath = () => {
2060      forklift.path = [];
2061      forklift.pathIndex = 0;
2062    };
2063    const clearRecoveryPath = () => {
2064      forklift.recoveryPath = [];
2065      forklift.recoveryPathIndex = 0;
2066      forklift.recoveryMode = false;
2067      forklift.recoveryAttempts = 0;
2068    };
2069    const getTruckSize = (size) => Math.max(size * 2.45, 22);
2070    const getTruckOffscreenPoint = (spawnPoint, floorplan, truckSize) => ({
2071      x: floorplan.bounds.x - truckSize * 5.8,
2072      y: spawnPoint.y
2073    });
2074    const getTruckDockPoint = (spawnPoint, truckSize) => ({
2075      // drawTruckIcon has the rear of the trailer on the right side,
2076      // so the truck center stops slightly left of the pallet spawn point.
2077      x: spawnPoint.x - truckSize * 0.62,
2078      y: spawnPoint.y
2079    });
2080    const resetTruckAtOffscreen = (truck, spawnPoint, floorplan, truckSize) => {
2081      const offscreen = getTruckOffscreenPoint(spawnPoint, floorplan, truckSize);
2082      truck.x = offscreen.x;
2083      truck.y = offscreen.y;
2084      truck.angle = 0;
2085      truck.hidden = false;
2086    };
2087    const moveTruckToward = (truck, target, deltaTime, size) => {
2088      const dx = target.x - truck.x;
2089      const dy = target.y - truck.y;
2090      const distance = Math.hypot(dx, dy);
2091      if (distance < 0.4) {
2092        truck.x = target.x;
2093        truck.y = target.y;
2094        return true;
2095      }
2096      const step = Math.min(distance, size * TRUCK_SPEED_MULTIPLIER * deltaTime);
2097      truck.x += dx / distance * step;
2098      truck.y += dy / distance * step;
2099      return distance <= step + 0.4;
2100    };
2101    const startInboundTruck = (floorplan, size) => {
2102      const spawnPoint = getUpperSpawnPoint(floorplan);
2103      if (!spawnPoint || inboundTruck.state !== "idle") return false;
2104      const truckSize = getTruckSize(size);
2105      resetTruckAtOffscreen(inboundTruck, spawnPoint, floorplan, truckSize);
2106      inboundTruck.state = "backingIn";
2107      inboundTruck.startedAt = performance.now();
2108      inboundTruck.dockUntil = 0;
2109      return true;
2110    };
2111    const updateInboundTruck = (deltaTime, time, floorplan, size) => {
2112      if (inboundTruck.state === "idle") return;
2113      const spawnPoint = getUpperSpawnPoint(floorplan);
2114      if (!spawnPoint) {
2115        inboundTruck.state = "idle";
2116        inboundTruck.hidden = true;
2117        return;
2118      }
2119      const truckSize = getTruckSize(size);
2120      const dockPoint = getTruckDockPoint(spawnPoint, truckSize);
2121      const offscreenPoint = getTruckOffscreenPoint(spawnPoint, floorplan, truckSize);
2122      if (inboundTruck.state === "backingIn") {
2123        if (moveTruckToward(inboundTruck, dockPoint, deltaTime, size)) {
2124          inboundTruck.state = "unloading";
2125          inboundTruck.dockUntil = time + TRUCK_DOCK_MS;
2126        }
2127        return;
2128      }
2129      if (inboundTruck.state === "unloading") {
2130        if (time < inboundTruck.dockUntil) return;
2131        const availableUnloadCapacity = MAX_UNLOADED_PALLETS - pallets.length;
2132        if (availableUnloadCapacity > 0) {
2133          const requestedPalletCount = MIN_TRUCK_UNLOAD_PALLETS + Math.floor(Math.random() * (MAX_TRUCK_UNLOAD_PALLETS - MIN_TRUCK_UNLOAD_PALLETS + 1));
2134          const palletCount = Math.min(requestedPalletCount, availableUnloadCapacity);
2135          for (let index = 0; index < palletCount; index++) {
2136            pallets.push({
2137              id: nextPalletId++,
2138              x: spawnPoint.x,
2139              y: spawnPoint.y
2140            });
2141          }
2142        }
2143        nextPalletAt = time + getInboundTruckDelay(time, floorplan);
2144        inboundTruck.state = "drivingOut";
2145        return;
2146      }
2147      if (inboundTruck.state === "drivingOut") {
2148        if (moveTruckToward(inboundTruck, offscreenPoint, deltaTime, size)) {
2149          inboundTruck.state = "idle";
2150          inboundTruck.hidden = true;
2151        }
2152      }
2153    };
2154    const addOutboundPallet = (floorplan) => {
2155      const dropoffPoint = getLowerSpawnPoint(floorplan);
2156      if (!dropoffPoint || !forklift.carrying) return;
2157      outboundPallets.push({
2158        id: forklift.carrying.id,
2159        x: dropoffPoint.x,
2160        y: dropoffPoint.y
2161      });
2162    };
2163    const startOutboundTruck = (floorplan, size) => {
2164      const spawnPoint = getLowerSpawnPoint(floorplan);
2165      if (!spawnPoint || outboundTruck.state !== "idle" || !outboundPallets.length) return false;
2166      const truckSize = getTruckSize(size);
2167      resetTruckAtOffscreen(outboundTruck, spawnPoint, floorplan, truckSize);
2168      outboundTruck.state = "backingIn";
2169      outboundTruck.startedAt = performance.now();
2170      outboundTruck.dockUntil = 0;
2171      return true;
2172    };
2173    const updateOutboundTruck = (deltaTime, time, floorplan, size) => {
2174      if (outboundTruck.state === "idle") {
2175        startOutboundTruck(floorplan, size);
2176        return;
2177      }
2178      const spawnPoint = getLowerSpawnPoint(floorplan);
2179      if (!spawnPoint) {
2180        outboundTruck.state = "idle";
2181        outboundTruck.hidden = true;
2182        return;
2183      }
2184      const truckSize = getTruckSize(size);
2185      const dockPoint = getTruckDockPoint(spawnPoint, truckSize);
2186      const offscreenPoint = getTruckOffscreenPoint(spawnPoint, floorplan, truckSize);
2187      if (outboundTruck.state === "backingIn") {
2188        if (moveTruckToward(outboundTruck, dockPoint, deltaTime, size)) {
2189          outboundTruck.state = "loading";
2190          outboundTruck.dockUntil = time + TRUCK_LOAD_MS;
2191        }
2192        return;
2193      }
2194      if (outboundTruck.state === "loading") {
2195        if (time < outboundTruck.dockUntil) return;
2196        if (outboundPallets.length) outboundPallets.shift();
2197        outboundTruck.state = "drivingOut";
2198        return;
2199      }
2200      if (outboundTruck.state === "drivingOut") {
2201        if (moveTruckToward(outboundTruck, offscreenPoint, deltaTime, size)) {
2202          outboundTruck.state = "idle";
2203          outboundTruck.hidden = true;
2204        }
2205      }
2206    };
2207    const updateTrucks = (deltaTime, time, floorplan, size) => {
2208      updateInboundTruck(deltaTime, time, floorplan, size);
2209      updateOutboundTruck(deltaTime, time, floorplan, size);
2210    };
2211    const drawTrucks = (context, size) => {
2212      if (typeof drawTruckIcon !== "function") return;
2213      const truckSize = getTruckSize(size);
2214      if (!inboundTruck.hidden) {
2215        drawTruckIcon(context, inboundTruck.x, inboundTruck.y, truckSize, inboundTruck.angle, true);
2216      }
2217      if (!outboundTruck.hidden) {
2218        drawTruckIcon(context, outboundTruck.x, outboundTruck.y, truckSize, outboundTruck.angle, true);
2219      }
2220    };
2221    const getBestNearbyClearancePoint = (floorplan, size) => {
2222      const padding = getForkliftPadding(size);
2223      const pathPadding = getForkliftPathPadding(size);
2224      const candidates = [];
2225      const directions = 24;
2226      for (const radius of [size * 1.8, size * 2.8, size * 4, size * 5.5]) {
2227        for (let index = 0; index < directions; index++) {
2228          const angle = Math.PI * 2 * index / directions;
2229          const x = forklift.x + Math.cos(angle) * radius;
2230          const y = forklift.y + Math.sin(angle) * radius;
2231          const point = getNearestWalkablePoint(x, y, floorplan, padding);
2232          if (!isPointWalkable(point.x, point.y, floorplan, padding)) continue;
2233          const path = findPath(forklift.x, forklift.y, point.x, point.y, floorplan, padding);
2234          if (!path.length) continue;
2235          const clearance = getWalkableClearance(point.x, point.y, floorplan, pathPadding, size * 5);
2236          const distance = Math.hypot(point.x - forklift.x, point.y - forklift.y);
2237          candidates.push({
2238            x: point.x,
2239            y: point.y,
2240            path,
2241            score: clearance * 2 - distance * 0.18
2242          });
2243        }
2244      }
2245      candidates.sort((a, b) => b.score - a.score);
2246      return candidates[0] || null;
2247    };
2248    const initiateClearanceRecovery = (floorplan, size) => {
2249      if (forklift.recoveryMode) return true;
2250      const candidate = getBestNearbyClearancePoint(floorplan, size);
2251      if (!candidate) return false;
2252      forklift.recoveryPath = candidate.path;
2253      forklift.recoveryPathIndex = 0;
2254      forklift.recoveryMode = true;
2255      forklift.recoveryAttempts++;
2256      forklift.blockedMoveTime = 0;
2257      return true;
2258    };
2259    const setForkliftDestination = (x, y, floorplan, padding, pathPadding = padding) => {
2260      const target = getNearestWalkablePoint(x, y, floorplan, pathPadding);
2261      const path = findPath(forklift.x, forklift.y, target.x, target.y, floorplan, pathPadding);
2262      if (!path.length) return false;
2263      forklift.path = path;
2264      forklift.pathIndex = 0;
2265      forklift.stuckTime = 0;
2266      forklift.lastX = forklift.x;
2267      forklift.lastY = forklift.y;
2268      clearRecoveryPath();
2269      return true;
2270    };
2271    const startIdleRetreatIfNeeded = (floorplan, size) => {
2272      if (forklift.idleRetreatDone || forklift.carrying || getCurrentWaypoint()) return false;
2273      const padding = getForkliftPadding(size);
2274      const forward = getForkliftForwardVector();
2275      const backward = {
2276        x: -forward.x,
2277        y: -forward.y
2278      };
2279      const sideLeft = { x: -forward.y, y: forward.x };
2280      const sideRight = { x: forward.y, y: -forward.x };
2281      const candidates = [
2282        { x: forklift.x + backward.x * size * 2.2, y: forklift.y + backward.y * size * 2.2 },
2283        { x: forklift.x + (backward.x + sideLeft.x * 0.45) * size * 2, y: forklift.y + (backward.y + sideLeft.y * 0.45) * size * 2 },
2284        { x: forklift.x + (backward.x + sideRight.x * 0.45) * size * 2, y: forklift.y + (backward.y + sideRight.y * 0.45) * size * 2 }
2285      ];
2286      for (const candidate of candidates) {
2287        if (isPointWalkable(candidate.x, candidate.y, floorplan, padding) && !segmentHitsObstacle(forklift.x, forklift.y, candidate.x, candidate.y, floorplan, padding)) {
2288          const path = findPath(forklift.x, forklift.y, candidate.x, candidate.y, floorplan, padding);
2289          if (path.length) {
2290            forklift.path = path;
2291            forklift.pathIndex = 0;
2292            forklift.mode = "idleRetreat";
2293            forklift.idleRetreatDone = true;
2294            forklift.idleRetreatUntil = performance.now() + 1200;
2295            forklift.stuckTime = 0;
2296            forklift.blockedMoveTime = 0;
2297            return true;
2298          }
2299        }
2300      }
2301      forklift.idleRetreatDone = true;
2302      forklift.idleRetreatUntil = performance.now() + 1200;
2303      return false;
2304    };
2305    const clearExpiredIgnoredPallets = () => {
2306      const now = performance.now();
2307      for (const [palletId, expiresAt] of ignoredPallets.entries()) {
2308        if (expiresAt <= now) ignoredPallets.delete(palletId);
2309      }
2310    };
2311    const clearExpiredIgnoredSlots = () => {
2312      const now = performance.now();
2313      for (const [slotId, expiresAt] of ignoredSlots.entries()) {
2314        if (expiresAt <= now) ignoredSlots.delete(slotId);
2315      }
2316    };
2317    const markPalletAsIgnored = (palletId) => {
2318      ignoredPallets.set(palletId, performance.now() + 2500);
2319    };
2320    const markSlotAsIgnored = (slotId) => {
2321      if (slotId !== null && slotId !== void 0) {
2322        ignoredSlots.set(slotId, performance.now() + 6500);
2323      }
2324    };
2325    const getPalletPickupPoint = (pallet, floorplan, padding, pathPadding = padding) => {
2326      const laneX = floorplan.pickupLaneX || pallet.x + padding * 5;
2327      const candidates = [
2328        { x: laneX, y: pallet.y },
2329        { x: laneX, y: pallet.y - padding * 2 },
2330        { x: laneX, y: pallet.y + padding * 2 },
2331        { x: laneX + padding * 2, y: pallet.y },
2332        { x: laneX + padding * 2, y: pallet.y - padding * 2 },
2333        { x: laneX + padding * 2, y: pallet.y + padding * 2 }
2334      ];
2335      const validCandidates = candidates.filter((point) => isPointWalkable(point.x, point.y, floorplan, pathPadding)).map((p
2335oint) => ({
2336        ...point,
2337        path: findPath(forklift.x, forklift.y, point.x, point.y, floorplan, pathPadding),
2338        clearance: getWalkableClearance(point.x, point.y, floorplan, pathPadding, pathPadding * 8),
2339        distance: Math.hypot(point.x - forklift.x, point.y - forklift.y),
2340        palletDistance: Math.hypot(point.x - pallet.x, point.y - pallet.y)
2341      })).filter((point) => point.path.length);
2342      if (!validCandidates.length) return null;
2343      validCandidates.sort((a, b) => {
2344        const scoreA = a.distance + a.palletDistance * 0.2 - a.clearance * 0.35;
2345        const scoreB = b.distance + b.palletDistance * 0.2 - b.clearance * 0.35;
2346        return scoreA - scoreB;
2347      });
2348      return validCandidates[0];
2349    };
2350    const chooseNearestPallet = (floorplan, padding, pathPadding = padding) => {
2351      clearExpiredIgnoredPallets();
2352      const reachablePallets = pallets.filter((pallet) => !ignoredPallets.has(pallet.id) && !isPalletTargetedByOtherForklift(pallet.id)).map((pallet) => ({
2353        pallet,
2354        pickupPoint: getPalletPickupPoint(pallet, floorplan, padding, pathPadding)
2355      })).filter((item) => item.pickupPoint);
2356      if (!reachablePallets.length) return null;
2357      reachablePallets.sort((a, b) => {
2358        const distanceA = Math.hypot(a.pickupPoint.x - forklift.x, a.pickupPoint.y - forklift.y);
2359        const distanceB = Math.hypot(b.pickupPoint.x - forklift.x, b.pickupPoint.y - forklift.y);
2360        return distanceA - distanceB;
2361      });
2362      return reachablePallets[0];
2363    };
2364    const getBestCandidateForShelf = (shelf, floorplan, padding, size, pathPadding = padding) => {
2365      const candidates = [];
2366      for (const approach of getShelfApproachPoints(shelf, floorplan, size)) {
2367        if (!isPointWalkable(approach.x, approach.y, floorplan, pathPadding)) continue;
2368        const path = findPath(forklift.x, forklift.y, approach.x, approach.y, floorplan, pathPadding);
2369        if (!path.length) continue;
2370        const pathDistance = path.reduce((total, point, index) => {
2371          const previous = index === 0 ? forklift : path[index - 1];
2372          return total + Math.hypot(point.x - previous.x, point.y - previous.y);
2373        }, 0);
2374        const clearance = getWalkableClearance(approach.x, approach.y, floorplan, pathPadding, size * 3);
2375        const ignoredPenalty = ignoredSlots.has(shelf.id) ? size * 9 : 0;
2376        const leftSideBonus = approach.x < shelf.rect.x ? size * 2.2 : 0;
2377        const clearanceBonus = Math.min(clearance, size * 3) * 1.4;
2378        candidates.push({
2379          id: shelf.id,
2380          shelfId: shelf.id,
2381          approachX: approach.x,
2382          approachY: approach.y,
2383          pathDistance,
2384          clearance,
2385          score: pathDistance - clearanceBonus - leftSideBonus + ignoredPenalty,
2386          shelf
2387        });
2388      }
2389      if (!candidates.length) return null;
2390      candidates.sort((a, b) => a.score - b.score);
2391      return candidates[0];
2392    };
2393    const chooseDeliverySlot = (floorplan, padding, size, pathPadding = padding) => {
2394      clearExpiredIgnoredSlots();
2395      removeOldestDeliveredPalletIfWarehouseIsFull(floorplan);
2396      const shelves = getWarehouseShelves(floorplan);
2397      if (!shelves.length) return null;
2398      if (forklift.preferredDeliverySlotId === null || forklift.preferredDeliverySlotId === void 0) {
2399        assignRandomDeliveryShelfForCurrentPallet(floorplan);
2400      }
2401      const preferredShelf = shelves.find((shelf) => shelf.id === forklift.preferredDeliverySlotId);
2402      if (preferredShelf && !isShelfFull(preferredShelf.id) && !ignoredSlots.has(preferredShelf.id)) {
2403        const preferredCandidate = getBestCandidateForShelf(preferredShelf, floorplan, padding, size, pathPadding);
2404        if (preferredCandidate) {
2405          lastDeliverySlotId = preferredCandidate.id;
2406          return preferredCandidate;
2407        }
2408      }
2409      const fallbackCandidates = shelves.filter((shelf) => !isShelfFull(shelf.id)).map((shelf) => getBestCandidateForShelf(shelf, floorplan, padding, size, pathPadding)).filter(Boolean);
2410      if (!fallbackCandidates.length) return null;
2411      const fallbackShelfId = getNextRandomDeliverySlotId(fallbackCandidates.map((candidate) => candidate.id));
2412      const fallbackCandidate = fallbackCandidates.find((candidate) => candidate.id === fallbackShelfId) || fallbackCandidates[0];
2413      lastDeliverySlotId = fallbackCandidate.id;
2414      forklift.preferredDeliverySlotId = fallbackCandidate.id;
2415      return fallbackCandidate;
2416    };
2417    const chooseRandomStoredPalletPickup = (floorplan, padding, size, pathPadding = padding) => {
2418      const shelvesById = new Map(getWarehouseShelves(floorplan).map((shelf) => [shelf.id, shelf]));
2419      const candidates = shuffleArray([...deliveredPallets]).filter((pallet) => !isStoredPalletTargetedByOtherForklift(pallet.id));
2420      for (const pallet of candidates) {
2421        const shelf = shelvesById.get(getPalletShelfId(pallet));
2422        if (!shelf) continue;
2423        const pickupPoint = getBestCandidateForShelf(shelf, floorplan, padding, size, pathPadding);
2424        if (!pickupPoint) continue;
2425        return {
2426          pallet,
2427          pickupPoint
2428        };
2429      }
2430      return null;
2431    };
2432    const getTotalPalletsOnSite = () => {
2433      return pallets.length + deliveredPallets.length + outboundPallets.length + forklifts.filter((item) => item.carrying).length;
2434    };
2435    const pruneMovedPalletEvents = (time) => {
2436      const minTime = time - MOVED_PALLET_WINDOW_MS;
2437      while (movedPalletEvents.length && movedPalletEvents[0] < minTime) {
2438        movedPalletEvents.shift();
2439      }
2440    };
2441    const recordMovedPallet = (time = performance.now()) => {
2442      movedPalletEvents.push(time);
2443      pruneMovedPalletEvents(time);
2444    };
2445    const getMovedPalletsInWindow = (time = performance.now()) => {
2446      pruneMovedPalletEvents(time);
2447      return movedPalletEvents.length;
2448    };
2449    const getMaxPalletsOnShelves = (floorplan) => {
2450      return getWarehouseShelves(floorplan).length * PALLETS_PER_SHELF;
2451    };
2452    const getWarehousePalletCapacity = (floorplan) => {
2453      return getMaxPalletsOnShelves(floorplan) + MAX_UNLOADED_PALLETS + forklifts.length;
2454    };
2455    const getWarehouseFillRatio = (floorplan) => {
2456      return Math.max(0, Math.min(1, getTotalPalletsOnSite() / Math.max(1, getWarehousePalletCapacity(floorplan))));
2457    };
2458    const getWarehouseTargetFillRatio = (time) => {
2459      const phase = time / WAREHOUSE_FILL_PERIOD_MS * Math.PI * 2;
2460      const wave = (Math.sin(phase) + 1) * 0.5;
2461      const biasedWave = Math.pow(wave, WAREHOUSE_TARGET_FILL_EXPONENT);
2462      return WAREHOUSE_MIN_FILL_RATIO + (WAREHOUSE_MAX_FILL_RATIO - WAREHOUSE_MIN_FILL_RATIO) * biasedWave;
2463    };
2464    const getInboundTruckDelay = (time, floorplan) => {
2465      const targetRatio = getWarehouseTargetFillRatio(time);
2466      const fillRatio = getWarehouseFillRatio(floorplan);
2467      const deficit = Math.max(0, targetRatio - fillRatio);
2468      const urgency = Math.max(0, Math.min(1, deficit / 0.35));
2469      const baseDelay = MAX_INBOUND_TRUCK_INTERVAL_MS - (MAX_INBOUND_TRUCK_INTERVAL_MS - MIN_INBOUND_TRUCK_INTERVAL_MS) * urgency;
2470      return baseDelay + Math.random() * 1500;
2471    };
2472    const shouldStartInboundTruck = (time, floorplan) => {
2473      const capacity = getWarehousePalletCapacity(floorplan);
2474      const targetPallets = getWarehouseTargetFillRatio(time) * capacity;
2475      const fillRatio = getWarehouseFillRatio(floorplan);
2476      const palletDeficit = targetPallets - getTotalPalletsOnSite();
2477      return fillRatio < WAREHOUSE_MAX_FILL_RATIO && palletDeficit >= MIN_TRUCK_UNLOAD_PALLETS;
2478    };
2479    const getShelfRowPalletDistribution = (floorplan) => {
2480      const shelves = getWarehouseShelves(floorplan);
2481      const shelfRowsById = new Map(shelves.map((shelf) => [shelf.id, shelf.rowIndex]));
2482      const rowCount = shelves.reduce((maxRow, shelf) => Math.max(maxRow, shelf.rowIndex + 1), 0);
2483      const rowCounts = Array.from({ length: rowCount }, () => 0);
2484      for (const pallet of deliveredPallets) {
2485        const rowIndex = shelfRowsById.get(getPalletShelfId(pallet));
2486        if (rowIndex === void 0) continue;
2487        rowCounts[rowIndex]++;
2488      }
2489      return {
2490        rowCounts,
2491        maxPalletsPerRow: SHELF_COLUMN_COUNT * PALLETS_PER_SHELF
2492      };
2493    };
2494    const spawnPallet = (time, floorplan, size) => {
2495      const spawnPoint = getUpperSpawnPoint(floorplan);
2496      if (time < nextPalletAt || pallets.length >= MAX_UNLOADED_PALLETS || !spawnPoint) return;
2497      if (inboundTruck.state !== "idle") return;
2498      if (!shouldStartInboundTruck(time, floorplan)) {
2499        nextPalletAt = time + getInboundTruckDelay(time, floorplan);
2500        return;
2501      }
2502      startInboundTruck(floorplan, size);
2503    };
2504    const completeCurrentForkliftTask = (floorplan, size) => {
2505      if (forklift.mode === "toPickup") {
2506        const pallet = pallets.find((item) => item.id === forklift.targetPalletId);
2507        if (pallet) {
2508          forklift.carrying = { id: pallet.id };
2509          const palletIndex = pallets.findIndex((item) => item.id === pallet.id);
2510          if (palletIndex !== -1) pallets.splice(palletIndex, 1);
2511        }
2512        forklift.targetPalletId = null;
2513        forklift.targetStoredPalletId = null;
2514        forklift.targetShelfId = null;
2515        forklift.deliverySlotId = null;
2516        forklift.preferredDeliverySlotId = forklift.carrying ? assignRandomDeliveryShelfForCurrentPallet(floorplan) : null;
2517        forklift.mode = forklift.carrying ? "toDropoff" : "idle";
2518        clearForkliftPath();
2519        updateForkliftTask(floorplan, size);
2520        return;
2521      }
2522      if (forklift.mode === "toDropoff") {
2523        const shelfId = forklift.deliverySlotId;
2524        if (shelfId && forklift.carrying && !isShelfFull(shelfId)) {
2525          deliveredPallets.push({
2526            id: forklift.carrying.id,
2527            shelfId,
2528            slotId: shelfId
2529          });
2530          recordMovedPallet();
2531          lastDeliverySlotId = shelfId;
2532        }
2533        forklift.carrying = null;
2534        forklift.deliverySlotId = null;
2535        forklift.preferredDeliverySlotId = null;
2536        forklift.targetPalletId = null;
2537        forklift.targetStoredPalletId = null;
2538        forklift.targetShelfId = null;
2539        forklift.mode = "idle";
2540        forklift.idleRetreatDone = false;
2541        clearForkliftPath();
2542        updateForkliftTask(floorplan, size);
2543        return;
2544      }
2545      if (forklift.mode === "toShelfPickup") {
2546        const palletIndex = deliveredPallets.findIndex((item) => item.id === forklift.targetStoredPalletId);
2547        if (palletIndex !== -1) {
2548          const [pallet] = deliveredPallets.splice(palletIndex, 1);
2549          forklift.carrying = {
2550            id: pallet.id,
2551            outbound: true
2552          };
2553          forklift.mode = "toOutboundDropoff";
2554          forklift.idleRetreatDone = false;
2555        } else {
2556          forklift.mode = "idle";
2557        }
2558        forklift.targetStoredPalletId = null;
2559        forklift.targetShelfId = null;
2560        forklift.targetPalletId = null;
2561        clearForkliftPath();
2562        updateForkliftTask(floorplan, size);
2563        return;
2564      }
2565      if (forklift.mode === "toOutboundDropoff") {
2566        if (forklift.carrying?.outbound) {
2567          addOutboundPallet(floorplan);
2568          recordMovedPallet();
2569        }
2570        forklift.carrying = null;
2571        forklift.targetStoredPalletId = null;
2572        forklift.targetShelfId = null;
2573        forklift.targetPalletId = null;
2574        forklift.deliverySlotId = null;
2575        forklift.preferredDeliverySlotId = null;
2576        forklift.mode = "idle";
2577        forklift.idleRetreatDone = false;
2578        clearForkliftPath();
2579        updateForkliftTask(floorplan, size);
2580        return;
2581      }
2582      forklift.mode = "idle";
2583      clearForkliftPath();
2584      updateForkliftTask(floorplan, size);
2585    };
2586    const updateForkliftTask = (floorplan, size) => {
2587      const padding = getForkliftPadding(size);
2588      const pathPadding = getForkliftPathPadding(size);
2589      const now = performance.now();
2590      if (getCurrentWaypoint()) return;
2591      if (forklift.mode === "toShelfPickup" && forklift.targetStoredPalletId !== null) {
2592        const storedPallet = deliveredPallets.find((item) => item.id === forklift.targetStoredPalletId);
2593        const shelf = storedPallet ? getWarehouseShelves(floorplan).find((item) => item.id === getPalletShelfId(storedPallet)) : null;
2594        const pickupPoint = shelf ? getBestCandidateForShelf(shelf, floorplan, padding, size, pathPadding) : null;
2595        if (storedPallet && pickupPoint && setForkliftDestination(pickupPoint.approachX, pickupPoint.approachY, floorplan, padding, pathPadding)) return;
2596        forklift.targetStoredPalletId = null;
2597        forklift.targetShelfId = null;
2598        forklift.mode = "idle";
2599      }
2600      if (forklift.mode === "toOutboundDropoff" && forklift.carrying?.outbound) {
2601        const dropoffPoint = getLowerSpawnPoint(floorplan);
2602        if (dropoffPoint && setForkliftDestination(dropoffPoint.x, dropoffPoint.y, floorplan, padding, pathPadding)) return;
2603        forklift.carrying = null;
2604        forklift.mode = "idle";
2605      }
2606      if (forklift.mode === "toPickup" && forklift.targetPalletId !== null) {
2607        const pallet = pallets.find((item) => item.id === forklift.targetPalletId);
2608        const pickupPoint = pallet ? getPalletPickupPoint(pallet, floorplan, padding, pathPadding) : null;
2609        if (pallet && pickupPoint && setForkliftDestination(pickupPoint.x, pickupPoint.y, floorplan, padding, pathPadding)) return;
2610        if (forklift.targetPalletId !== null) markPalletAsIgnored(forklift.targetPalletId);
2611        forklift.targetPalletId = null;
2612        forklift.mode = "idle";
2613      }
2614      if (!forklift.carrying && now >= nextStoredPalletPickupAt && deliveredPallets.length && !outboundPallets.length && outboundTruck.state === "idle") {
2615        nextStoredPalletPickupAt = now + STORED_PALLET_PICKUP_INTERVAL_MS;
2616        const storedPickup = chooseRandomStoredPalletPickup(floorplan, padding, size, pathPadding);
2617        if (storedPickup) {
2618          forklift.mode = "toShelfPickup";
2619          forklift.idleRetreatDone = false;
2620          forklift.targetStoredPalletId = storedPickup.pallet.id;
2621          forklift.targetShelfId = getPalletShelfId(storedPickup.pallet);
2622          if (setForkliftDestination(storedPickup.pickupPoint.approachX, storedPickup.pickupPoint.approachY, floorplan, padding, pathPadding)) return;
2623          forklift.targetStoredPalletId = null;
2624          forklift.targetShelfId = null;
2625        }
2626      }
2627      if (forklift.carrying) {
2628        const slot = chooseDeliverySlot(floorplan, padding, size, pathPadding);
2629        if (slot && setForkliftDestination(slot.approachX, slot.approachY, floorplan, padding, pathPadding)) {
2630          forklift.mode = "toDropoff";
2631          forklift.idleRetreatDone = false;
2632          forklift.deliverySlotId = slot.id;
2633          return;
2634        }
2635        if (slot) markSlotAsIgnored(slot.id);
2636        forklift.mode = "idle";
2637        return;
2638      }
2639      const nextPallet = chooseNearestPallet(floorplan, padding, pathPadding);
2640      if (nextPallet) {
2641        forklift.mode = "toPickup";
2642        forklift.idleRetreatDone = false;
2643        forklift.targetPalletId = nextPallet.pallet.id;
2644        if (setForkliftDestination(nextPallet.pickupPoint.x, nextPallet.pickupPoint.y, floorplan, padding, pathPadding)) return;
2645        markPalletAsIgnored(nextPallet.pallet.id);
2646        forklift.targetPalletId = null;
2647      }
2648      if (forklift.mode === "idleRetreat") {
2649        if (performance.now() < forklift.idleRetreatUntil) return;
2650        forklift.mode = "idle";
2651        clearForkliftPath();
2652        return;
2653      }
2654      forklift.mode = "idle";
2655      clearForkliftPath();
2656      startIdleRetreatIfNeeded(floorplan, size);
2657    };
2658    const getForkliftForwardVector = () => {
2659      const waypoint = getCurrentWaypoint();
2660      if (waypoint) {
2661        const dx = waypoint.x - forklift.x;
2662        const dy = waypoint.y - forklift.y;
2663        const distance = Math.hypot(dx, dy);
2664        if (distance > 1e-3) {
2665          return {
2666            x: dx / distance,
2667            y: dy / distance
2668          };
2669        }
2670      }
2671      const lastMoveDistance = Math.hypot(forklift.lastMoveX, forklift.lastMoveY);
2672      if (lastMoveDistance > 1e-3) {
2673        return {
2674          x: forklift.lastMoveX / lastMoveDistance,
2675          y: forklift.lastMoveY / lastMoveDistance
2676        };
2677      }
2678      return {
2679        x: Math.cos(forklift.angle + Math.PI),
2680        y: Math.sin(forklift.angle + Math.PI)
2681      };
2682    };
2683    const getForkliftLaneMetrics = (x, y, size) => {
2684      const forward = getForkliftForwardVector();
2685      const relX = x - forklift.x;
2686      const relY = y - forklift.y;
2687      return {
2688        forward,
2689        forwardDistance: relX * forward.x + relY * forward.y,
2690        lateralDistance: relX * -forward.y + relY * forward.x,
2691        laneHalfWidth: size * 1.1 + getHumanRadius(size) * 0.35,
2692        stopDistance: size * 3.4
2693      };
2694    };
2695    const isHumanBlockingForklift = (human, floorplan, size, nextX = forklift.x, nextY = forklift.y) => {
2696      if (!forklift.initialized || !getCurrentWaypoint()) return false;
2697      const metrics = getForkliftLaneMetrics(human.x, human.y, size);
2698      const isInFrontLane = metrics.forwardDistance > 0 && metrics.forwardDistance <= metrics.stopDistance && Math.abs(metrics.lateralDistance) <= metrics.laneHalfWidth;
2699      const isTooCloseToForkliftStep = Math.hypot(human.x - nextX, human.y - nextY) < size * HUMAN_STOP_DISTANCE_MULTIPLIER;
2700      if (!isInFrontLane && !isTooCloseToForkliftStep) return false;
2701      return !segmentHitsObstacle(
2702        forklift.x,
2703        forklift.y,
2704        human.x,
2705        human.y,
2706        floorplan,
2707        getHumanPadding(size)
2708      );
2709    };
2710    const getBlockingHumanForForklift = (floorplan, size, nextX = forklift.x, nextY = forklift.y) => {
2711      let bestHuman = null;
2712      let bestDistance = Infinity;
2713      for (const human of humans) {
2714        if (!isHumanBlockingForklift(human, floorplan, size, nextX, nextY)) continue;
2715        const metrics = getForkliftLaneMetrics(human.x, human.y, size);
2716        const distance = Math.max(0, metrics.forwardDistance);
2717        if (distance < bestDistance) {
2718          bestDistance = distance;
2719          bestHuman = human;
2720        }
2721      }
2722      return bestHuman;
2723    };
2724    const rememberHumanPosition = (human, size) => {
2725      const lastPoint = human.history[human.history.length - 1];
2726      if (lastPoint && Math.hypot(human.x - lastPoint.x, human.y - lastPoint.y) < size * 0.16) return;
2727      human.history.push({
2728        x: human.x,
2729        y: human.y,
2730        time: performance.now()
2731      });
2732      while (human.history.length > HUMAN_HISTORY_LIMIT) {
2733        human.history.shift();
2734      }
2735    };
2736    const clearHumanForceEscape = (human) => {
2737      human.forceEscapeUntil = 0;
2738      human.forceEscapePath = [];
2739      human.forceEscapePathIndex = 0;
2740    };
2741    const getHumanEscapeCandidates = (human, floorplan, size) => {
2742      const padding = getHumanPadding(size);
2743      const metrics = getForkliftLaneMetrics(human.x, human.y, size);
2744      const forward = metrics.forward;
2745      const awayX = forklift.initialized ? human.x - forklift.x : 0;
2746      const awayY = forklift.initialized ? human.y - forklift.y : 1;
2747      const awayDistance = Math.max(1e-3, Math.hypot(awayX, awayY));
2748      const sideA = { x: -forward.y, y: forward.x };
2749      const sideB = { x: forward.y, y: -forward.x };
2750      const backward = { x: -forward.x, y: -forward.y };
2751      const away = { x: awayX / awayDistance, y: awayY / awayDistance };
2752      const candidates = [];
2753      const addCandidate = (x, y, priority = 0, source = "sample") => {
2754        const point = getNearestWalkablePoint(x, y, floorplan, padding);
2755        if (!point || !isPointWalkable(point.x, point.y, floorplan, padding)) return;
2756        const distanceFromHuman = Math.hypot(point.x - human.x, point.y - human.y);
2757        if (distanceFromHuman < size * 0.5) return;
2758        const direct = !segmentHitsObstacle(human.x, human.y, point.x, point.y, floorplan, padding);
2759        const path = findPath(human.x, human.y, point.x, point.y, floorplan, padding);
2760        const usablePath = path.length ? path : direct ? [{ x: point.x, y: point.y }] : [];
2761        if (!usablePath.length) return;
2762        const lane = getForkliftLaneMetrics(point.x, point.y, size);
2763        const outsideLane = Math.abs(lane.lateralDistance) > lane.laneHalfWidth * 1.18;
2764        const behindForklift = lane.forwardDistance < -size * 0.25;
2765        const distanceFromForklift = forklift.initialized ? Math.hypot(point.x - forklift.x, point.y - forklift.y) : 0;
2766        candidates.push({
2767          x: point.x,
2768          y: point.y,
2769          path: usablePath,
2770          score: priority + (source === "backtrack" ? -size * 8 : 0) + (direct ? -size * 7 : 0) + (outsideLane ? -size * 12 : 0) + (behindForklift ? -size * 4 : 0) - distanceFromForklift * 0.35 + distanceFromHuman * 0.06
2771        });
2772      };
2773      for (let index = human.history.length - 1; index >= 0; index--) {
2774        const point = human.history[index];
2775        addCandidate(point.x, point.y, (human.history.length - 1 - index) * size * 0.04, "backtrack");
2776      }
2777      const directions = [
2778        sideA,
2779        sideB,
2780        away,
2781        { x: sideA.x * 0.8 + backward.x * 0.6, y: sideA.y * 0.8 + backward.y * 0.6 },
2782        { x: sideB.x * 0.8 + backward.x * 0.6, y: sideB.y * 0.8 + backward.y * 0.6 },
2783        backward
2784      ];
2785      for (let directionIndex = 0; directionIndex < directions.length; directionIndex++) {
2786        const direction = directions[directionIndex];
2787        const length = Math.max(1e-3, Math.hypot(direction.x, direction.y));
2788        const directionX = direction.x / length;
2789        const directionY = direction.y / length;
2790        for (const distance of [size * 2, size * 3.1, size * 4.6, size * 6.4]) {
2791          addCandidate(
2792            human.x + directionX * distance,
2793            human.y + directionY * distance,
2794            directionIndex * size * 0.16,
2795            "sidestep"
2796          );
2797        }
2798      }
2799      return candidates.sort((a, b) => a.score - b.score);
2800    };
2801    const chooseHumanNextShelf = (human, floorplan, size) => {
2802      if (human.forceEscapeUntil > performance.now()) return true;
2803      const shelves = getWarehouseShelves(floorplan);
2804      if (!shelves.length) return false;
2805      const shuffledShelves = shuffleArray([...shelves]).sort((a, b) => {
2806        if (human.targetShelfId && a.id === human.targetShelfId) return 1;
2807        if (human.targetShelfId && b.id === human.targetShelfId) return -1;
2808        return 0;
2809      });
2810      for (const shelf of shuffledShelves) {
2811        const candidates = shuffleArray(getShelfApproachPoints(shelf, floorplan, size * 0.8));
2812        for (const candidate of candidates) {
2813          if (!isPointWalkable(candidate.x, candidate.y, floorplan, getHumanPadding(size))) continue;
2814          const path2 = findPath(human.x, human.y, candidate.x, candidate.y, floorplan, getHumanPadding(size));
2815          if (!path2.length) continue;
2816          human.path = path2;
2817          human.pathIndex = 0;
2818          human.targetShelfId = shelf.id;
2819          human.targetPoint = candidate;
2820          human.stillTime = 0;
2821          human.watchStillTime = 0;
2822          return true;
2823        }
2824      }
2825      const fallback = getRandomWalkablePoint(floorplan, getHumanPadding(size));
2826      const path = findPath(human.x, human.y, fallback.x, fallback.y, floorplan, getHumanPadding(size));
2827      if (path.length) {
2828        human.path = path;
2829        human.pathIndex = 0;
2830        human.targetShelfId = null;
2831        human.targetPoint = fallback;
2832        return true;
2833      }
2834      return false;
2835    };
2836    const hardNudgeHumanOutOfDeadlock = (human, floorplan, size) => {
2837      const candidates = getHumanEscapeCandidates(human, floorplan, size);
2838      const padding = getHumanPadding(size);
2839      for (const candidate of candidates) {
2840        if (isPointWalkable(candidate.x, candidate.y, floorplan, padding) && !segmentHitsObstacle(human.x, human.y, candidate.x, candidate.y, floorplan, padding)) {
2841          human.x = candidate.x;
2842          human.y = candidate.y;
2843          human.path = [];
2844          human.pathIndex = 0;
2845          human.targetShelfId = null;
2846          human.targetPoint = null;
2847          human.stillTime = 0;
2848          human.watchStillTime = 0;
2849          human.watchX = human.x;
2850          human.watchY = human.y;
2851          human.lastX = human.x;
2852          human.lastY = human.y;
2853          clearHumanForceEscape(human);
2854          rememberHumanPosition(human, size);
2855          chooseHumanNextShelf(human, floorplan, size);
2856          return true;
2857        }
2858      }
2859      return false;
2860    };
2861    const startHumanForceEscape = (human, floorplan, size) => {
2862      if (!human || !forklift.initialized) return false;
2863      const now = performance.now();
2864      if (human.forceEscapeUntil > now && human.forceEscapePath[human.forceEscapePathIndex]) return true;
2865      const candidates = getHumanEscapeCandidates(human, floorplan, size);
2866      if (!candidates.length) return hardNudgeHumanOutOfDeadlock(human, floorplan, size);
2867      const best = candidates[0];
2868      human.path = [];
2869      human.pathIndex = 0;
2870      human.targetShelfId = null;
2871      human.targetPoint = null;
2872      human.forceEscapeUntil = now + HUMAN_ESCAPE_MS;
2873      human.forceEscapePath = best.path;
2874      human.forceEscapePathIndex = 0;
2875      human.stillTime = 0;
2876      human.watchStillTime = 0;
2877      return true;
2878    };
2879    const updateHumanForceEscape = (human, deltaTime, floorplan, size) => {
2880      const now = performance.now();
2881      if (!human.forceEscapeUntil || now > human.forceEscapeUntil) {
2882        clearHumanForceEscape(human);
2883        return false;
2884      }
2885      const waypoint = human.forceEscapePath[human.forceEscapePathIndex] || null;
2886      if (!waypoint) {
2887        clearHumanForceEscape(human);
2888        chooseHumanNextShelf(human, floorplan, size);
2889        return true;
2890      }
2891      const padding = getHumanPadding(size);
2892      const dx = waypoint.x - human.x;
2893      const dy = waypoint.y - human.y;
2894      const distance = Math.hypot(dx, dy);
2895      if (distance < size * 0.22) {
2896        human.forceEscapePathIndex++;
2897        return true;
2898      }
2899      const directionX = dx / distance;
2900      const directionY = dy / distance;
2901      const step = Math.min(distance, human.speed * 2.5 * deltaTime);
2902      const nextX = human.x + directionX * step;
2903      const nextY = human.y + directionY * step;
2904      if (isPointWalkable(nextX, nextY, floorplan, padding) && !segmentHitsObstacle(human.x, human.y, nextX, nextY, floorplan, padding)) {
2905        human.x = nextX;
2906        human.y = nextY;
2907        human.angle = Math.atan2(directionY, directionX) + Math.PI / 2;
2908        human.lastX = human.x;
2909        human.lastY = human.y;
2910        human.watchX = human.x;
2911        human.watchY = human.y;
2912        human.stillTime = 0;
2913        human.watchStillTime = 0;
2914        rememberHumanPosition(human, size);
2915        return true;
2916      }
2917      clearHumanForceEscape(human);
2918      if (!startHumanForceEscape(human, floorplan, size)) {
2919        hardNudgeHumanOutOfDeadlock(human, floorplan, size);
2920      }
2921      return true;
2922    };
2923    const getRandomHumanSpawnPoint = (floorplan, size) => {
2924      const padding = getHumanPadding(size);
2925      for (let attempt = 0; attempt < 280; attempt++) {
2926        const point = getRandomWalkablePoint(floorplan, padding);
2927        if (forklift.initialized && Math.hypot(point.x - forklift.x, point.y - forklift.y) < size * 5) continue;
2928        if (!humans.some((human) => Math.hypot(point.x - human.x, point.y - human.y) < size * 2.6)) {
2929          return point;
2930        }
2931      }
2932      return getRandomWalkablePoint(floorplan, padding);
2933    };
2934    const spawnMissingHumans = (floorplan, size) => {
2935      while (humans.length < HUMAN_COUNT) {
2936        const spawnPoint = getRandomHumanSpawnPoint(floorplan, size);
2937        const human = {
2938          id: nextHumanId++,
2939          x: spawnPoint.x,
2940          y: spawnPoint.y,
2941          angle: Math.random() * Math.PI * 2,
2942          speed: Math.max(24, size * (2.15 + Math.random() * 0.65)),
2943          path: [],
2944          pathIndex: 0,
2945          targetShelfId: null,
2946          targetPoint: null,
2947          lastX: spawnPoint.x,
2948          lastY: spawnPoint.y,
2949          watchX: spawnPoint.x,
2950          watchY: spawnPoint.y,
2951          stillTime: 0,
2952          watchStillTime: 0,
2953          history: [],
2954          forceEscapeUntil: 0,
2955          forceEscapePath: [],
2956          forceEscapePathIndex: 0
2957        };
2958        humans.push(human);
2959        rememberHumanPosition(human, size);
2960        chooseHumanNextShelf(human, floorplan, size);
2961      }
2962      while (humans.length > HUMAN_COUNT) humans.pop();
2963    };
2964    const updateHumanStuckWatch = (human, deltaTime, floorplan, size) => {
2965      const movedDistance = Math.hypot(human.x - human.watchX, human.y - human.watchY);
2966      if (movedDistance < size * 0.035) {
2967        human.watchStillTime += deltaTime;
2968      } else {
2969        human.watchStillTime = 0;
2970        human.watchX = human.x;
2971        human.watchY = human.y;
2972      }
2973      if (human.watchStillTime > HUMAN_STUCK_SECONDS) {
2974        human.path = [];
2975        human.pathIndex = 0;
2976        human.targetShelfId = null;
2977        human.targetPoint = null;
2978        if (forklift.initialized && Math.hypot(human.x - forklift.x, human.y - forklift.y) < size * 6) {
2979          startHumanForceEscape(human, floorplan, size) || hardNudgeHumanOutOfDeadlock(human, floorplan, size) || chooseHumanNextShelf(human, floorplan, size);
2980        } else {
2981          chooseHumanNextShelf(human, floorplan, size);
2982        }
2983        human.watchStillTime = 0;
2984        human.watchX = human.x;
2985        human.watchY = human.y;
2986        return true;
2987      }
2988      return false;
2989    };
2990    const updateHuman = (human, deltaTime, floorplan, size) => {
2991      const padding = getHumanPadding(size);
2992      if (updateHumanForceEscape(human, deltaTime, floorplan, size)) return;
2993      if (!isPointWalkable(human.x, human.y, floorplan, padding)) {
2994        const safePoint = getNearestWalkablePoint(human.x, human.y, floorplan, padding);
2995        human.x = safePoint.x;
2996        human.y = safePoint.y;
2997        human.path = [];
2998        human.pathIndex = 0;
2999        human.targetShelfId = null;
3000        human.targetPoint = null;
3001        human.lastX = human.x;
3002        human.lastY = human.y;
3003        human.watchX = human.x;
3004        human.watchY = human.y;
3005        human.stillTime = 0;
3006        human.watchStillTime = 0;
3007        clearHumanForceEscape(human);
3008        rememberHumanPosition(human, size);
3009      }
3010      if (forklift.initialized && getCurrentWaypoint() && isHumanBlockingForklift(human, floorplan, size)) {
3011        startHumanForceEscape(human, floorplan, size) || hardNudgeHumanOutOfDeadlock(human, floorplan, size);
3012        return;
3013      }
3014      let waypoint = human.path[human.pathIndex] || null;
3015      if (!waypoint) {
3016        chooseHumanNextShelf(human, floorplan, size);
3017        waypoint = human.path[human.pathIndex] || null;
3018      }
3019      if (!waypoint) {
3020        updateHumanStuckWatch(human, deltaTime, floorplan, size);
3021        return;
3022      }
3023      const dx = waypoint.x - human.x;
3024      const dy = waypoint.y - human.y;
3025      const distance = Math.hypot(dx, dy);
3026      if (distance < size * 0.24) {
3027        human.pathIndex++;
3028        if (human.pathIndex >= human.path.length) {
3029          human.path = [];
3030          human.pathIndex = 0;
3031          human.targetShelfId = null;
3032          human.targetPoint = null;
3033          chooseHumanNextShelf(human, floorplan, size);
3034        }
3035        updateHumanStuckWatch(human, deltaTime, floorplan, size);
3036        return;
3037      }
3038      const directionX = dx / distance;
3039      const directionY = dy / distance;
3040      const step = Math.min(distance, human.speed * deltaTime);
3041      const nextX = human.x + directionX * step;
3042      const nextY = human.y + directionY * step;
3043      if (forklift.initialized && Math.hypot(nextX - forklift.x, nextY - forklift.y) < size * 1.35) {
3044        startHumanForceEscape(human, floorplan, size);
3045        return;
3046      }
3047      if (!isPointWalkable(nextX, nextY, floorplan, padding) || segmentHitsObstacle(human.x, human.y, nextX, nextY, floorplan, padding)) {
3048        human.path = [];
3049        human.pathIndex = 0;
3050        human.targetShelfId = null;
3051        human.targetPoint = null;
3052        chooseHumanNextShelf(human, floorplan, size);
3053        updateHumanStuckWatch(human, deltaTime, floorplan, size);
3054        return;
3055      }
3056      human.x = nextX;
3057      human.y = nextY;
3058      human.angle = Math.atan2(directionY, directionX) + Math.PI / 2;
3059      const movedDistance = Math.hypot(human.x - human.lastX, human.y - human.lastY);
3060      if (movedDistance > size * 0.08) {
3061        human.lastX = human.x;
3062        human.lastY = human.y;
3063        human.stillTime = 0;
3064        rememberHumanPosition(human, size);
3065      } else {
3066        human.stillTime += deltaTime;
3067      }
3068      if (human.stillTime > HUMAN_STUCK_SECONDS) {
3069        human.path = [];
3070        human.pathIndex = 0;
3071        human.targetShelfId = null;
3072        human.targetPoint = null;
3073        if (forklift.initialized && Math.hypot(human.x - forklift.x, human.y - forklift.y) < size * 6) {
3074          startHumanForceEscape(human, floorplan, size) || hardNudgeHumanOutOfDeadlock(human, floorplan, size);
3075        } else {
3076          chooseHumanNextShelf(human, floorplan, size);
3077        }
3078        human.stillTime = 0;
3079      }
3080      updateHumanStuckWatch(human, deltaTime, floorplan, size);
3081    };
3082    const updateHumans = (deltaTime, floorplan, size) => {
3083      spawnMissingHumans(floorplan, size);
3084      for (const human of humans) {
3085        updateHuman(human, deltaTime, floorplan, size);
3086      }
3087    };
3088    const updateForklift = (deltaTime, floorplan, size) => {
3089      const padding = getForkliftPadding(size);
3090      if (!forklift.initialized || !isPointWalkable(forklift.x, forklift.y, floorplan, padding)) {
3091        const start = getForkliftHomePoint(floorplan, padding);
3092        forklift.x = start.x;
3093        forklift.y = start.y;
3094        forklift.lastX = start.x;
3095        forklift.lastY = start.y;
3096        forklift.initialized = true;
3097        forklift.mode = forklift.carrying ? "toDropoff" : "idle";
3098        forklift.blockedMoveTime = 0;
3099        forklift.humanBlockedTime = 0;
3100        forklift.idleRetreatDone = false;
3101        forklift.idleRetreatUntil = 0;
3102        clearForkliftPath();
3103        clearRecoveryPath();
3104      }
3105      if (forklift.recoveryMode) {
3106        updateRecoveryMovement(deltaTime, floorplan, size);
3107        return;
3108      }
3109      updateForkliftTask(floorplan, size);
3110      const waypoint = getCurrentWaypoint();
3111      if (!waypoint) {
3112        forklift.humanBlockedTime = 0;
3113        return;
3114      }
3115      const dx = waypoint.x - forklift.x;
3116      const dy = waypoint.y - forklift.y;
3117      const distance = Math.hypot(dx, dy);
3118      if (distance < size * 0.32) {
3119        forklift.pathIndex++;
3120        if (forklift.pathIndex >= forklift.path.length) {
3121          clearForkliftPath();
3122          completeCurrentForkliftTask(floorplan, size);
3123        }
3124        return;
3125      }
3126      const directionX = dx / distance;
3127      const directionY = dy / distance;
3128      const step = Math.min(distance, forklift.speed * deltaTime);
3129      const nextX = forklift.x + directionX * step;
3130      const nextY = forklift.y + directionY * step;
3131      const blockingHuman = getBlockingHumanForForklift(floorplan, size, nextX, nextY);
3132      if (blockingHuman) {
3133        forklift.humanBlockedTime += deltaTime;
3134        forklift.blockedMoveTime = 0;
3135        forklift.stuckTime = 0;
3136        startHumanForceEscape(blockingHuman, floorplan, size);
3137        if (forklift.humanBlockedTime > 0.6) {
3138          hardNudgeHumanOutOfDeadlock(blockingHuman, floorplan, size);
3139          forklift.humanBlockedTime = 0.15;
3140        }
3141        return;
3142      }
3143      forklift.humanBlockedTime = 0;
3144      if (!isPointWalkable(nextX, nextY, floorplan, padding) || segmentHitsObstacle(forklift.x, forklift.y, nextX, nextY, floorplan, padding)) {
3145        forklift.lastMoveX = directionX;
3146        forklift.lastMoveY = directionY;
3147        forklift.blockedMoveTime += deltaTime;
3148        if (forklift.blockedMoveTime > 0.24) {
3149          if (!initiateClearanceRecovery(floorplan, size)) {
3150            initiateLongReverseRecovery(floorplan, size);
3151          }
3152          return;
3153        }
3154        if (forklift.blockedMoveTime > 0.08) {
3155          clearForkliftPath();
3156          updateForkliftTask(floorplan, size);
3157        }
3158        return;
3159      }
3160      forklift.x = nextX;
3161      forklift.y = nextY;
3162      forklift.lastMoveX = directionX;
3163      forklift.lastMoveY = directionY;
3164      forklift.blockedMoveTime = 0;
3165      const movedDistance = Math.hypot(forklift.x - forklift.lastX, forklift.y - forklift.lastY);
3166      if (movedDistance < 0.08) {
3167        forklift.stuckTime += deltaTime;
3168      } else {
3169        forklift.stuckTime = 0;
3170        forklift.lastX = forklift.x;
3171        forklift.lastY = forklift.y;
3172        forkliftOperationTime += deltaTime;
3173      }
3174      if (forklift.stuckTime > 0.9) {
3175        initiateLongReverseRecovery(floorplan, size);
3176      }
3177      forklift.angle = Math.atan2(directionY, directionX) - Math.PI;
3178    };
3179    const initiateLongReverseRecovery = (floorplan, size) => {
3180      if (forklift.recoveryMode) return;
3181      const padding = getForkliftPadding(size);
3182      const forward = getForkliftForwardVector();
3183      const backward = { x: -forward.x, y: -forward.y };
3184      const sideLeft = { x: -forward.y, y: forward.x };
3185      const sideRight = { x: forward.y, y: -forward.x };
3186      const backLeft = { x: backward.x + sideLeft.x * 0.5, y: backward.y + sideLeft.y * 0.5 };
3187      const backRight = { x: backward.x + sideRight.x * 0.5, y: backward.y + sideRight.y * 0.5 };
3188      const candidates = [
3189        { dir: backward, label: "straight back" },
3190        { dir: backLeft, label: "back-left" },
3191        { dir: backRight, label: "back-right" }
3192      ];
3193      let foundValid = false;
3194      for (const { dir, label } of candidates) {
3195        const distance = size * 4;
3196        const targetX = forklift.x + dir.x * distance;
3197        const targetY = forklift.y + dir.y * distance;
3198        if (isPointWalkable(targetX, targetY, floorplan, padding) && !segmentHitsObstacle(forklift.x, forklift.y, targetX, targetY, floorplan, padding)) {
3199          const path = findPath(forklift.x, forklift.y, targetX, targetY, floorplan, padding);
3200          if (path.length) {
3201            forklift.recoveryPath = path;
3202            forklift.recoveryPathIndex = 0;
3203            forklift.recoveryMode = true;
3204            forklift.recoveryAttempts++;
3205            forklift.blockedMoveTime = 0;
3206            foundValid = true;
3207            break;
3208          }
3209        }
3210      }
3211      if (!foundValid) {
3212        const safePoint = getNearestWalkablePoint(forklift.x, forklift.y, floorplan, padding);
3213        if (safePoint.x !== forklift.x || safePoint.y !== forklift.y) {
3214          const path = findPath(forklift.x, forklift.y, safePoint.x, safePoint.y, floorplan, padding);
3215          if (path.length) {
3216            forklift.recoveryPath = path;
3217            forklift.recoveryPathIndex = 0;
3218            forklift.recoveryMode = true;
3219            forklift.recoveryAttempts++;
3220            forklift.blockedMoveTime = 0;
3221          }
3222        }
3223      }
3224      if (!forklift.recoveryMode) {
3225        if (forklift.recoveryAttempts >= forklift.maxRecoveryAttempts) {
3226          forklift.mode = "idle";
3227          forklift.idleRetreatDone = false;
3228          clearForkliftPath();
3229          startIdleRetreatIfNeeded(floorplan, size);
3230        } else {
3231          updateForkliftTask(floorplan, size);
3232        }
3233        forklift.stuckTime = 0;
3234      }
3235    };
3236    const updateRecoveryMovement = (deltaTime, floorplan, size) => {
3237      const padding = getForkliftPadding(size);
3238      const waypoint = getCurrentWaypoint();
3239      if (!waypoint) {
3240        clearRecoveryPath();
3241        forklift.stuckTime = 0;
3242        updateForkliftTask(floorplan, size);
3243        return;
3244      }
3245      const dx = waypoint.x - forklift.x;
3246      const dy = waypoint.y - forklift.y;
3247      const distance = Math.hypot(dx, dy);
3248      if (distance < size * 0.32) {
3249        forklift.recoveryPathIndex++;
3250        if (forklift.recoveryPathIndex >= forklift.recoveryPath.length) {
3251          clearRecoveryPath();
3252          forklift.stuckTime = 0;
3253          updateForkliftTask(floorplan, size);
3254        }
3255        return;
3256      }
3257      const directionX = dx / distance;
3258      const directionY = dy / distance;
3259      const step = Math.min(distance, forklift.speed * deltaTime);
3260      const nextX = forklift.x + directionX * step;
3261      const nextY = forklift.y + directionY * step;
3262      if (!isPointWalkable(nextX, nextY, floorplan, padding) || segmentHitsObstacle(forklift.x, forklift.y, nextX, nextY, floorplan, padding)) {
3263        clearRecoveryPath();
3264        forklift.blockedMoveTime += deltaTime;
3265        if (forklift.blockedMoveTime > 0.24 && !initiateClearanceRecovery(floorplan, size)) {
3266          updateForkliftTask(floorplan, size);
3267        }
3268        return;
3269      }
3270      forklift.x = nextX;
3271      forklift.y = nextY;
3272      forklift.lastMoveX = directionX;
3273      forklift.lastMoveY = directionY;
3274      forklift.blockedMoveTime = 0;
3275      const movedDistance = Math.hypot(forklift.x - forklift.lastX, forklift.y - forklift.lastY);
3276      if (movedDistance < 0.08) {
3277        forklift.stuckTime += deltaTime;
3278      } else {
3279        forklift.stuckTime = 0;
3280        forklift.lastX = forklift.x;
3281        forklift.lastY = forklift.y;
3282      }
3283      if (forklift.stuckTime > 0.9) {
3284        if (forklift.recoveryAttempts >= forklift.maxRecoveryAttempts) {
3285          forklift.mode = "idle";
3286          forklift.idleRetreatDone = false;
3287          clearRecoveryPath();
3288          clearForkliftPath();
3289          startIdleRetreatIfNeeded(floorplan, size);
3290        } else {
3291          initiateLongReverseRecovery(floorplan, size);
3292        }
3293      }
3294      forklift.angle = Math.atan2(directionY, directionX) - Math.PI;
3295    };
3296    const getHumanTrail = (human) => {
3297      let trail = humanTrails.get(human.id);
3298      if (!trail) {
3299        trail = {
3300          points: [],
3301          lastPointAt: 0
3302        };
3303        humanTrails.set(human.id, trail);
3304      }
3305      return trail;
3306    };
3307    const trimHumanTrail = (human, time) => {
3308      const trail = getHumanTrail(human);
3309      const minTime = time - TRAIL_LIFETIME_MS;
3310      while (trail.points.length && trail.points[0].time < minTime) {
3311        trail.points.shift();
3312      }
3313    };
3314    const clearAllHumanTrails = () => {
3315      humanTrails.clear();
3316    };
3317    const clearAllForkliftTrails = () => {
3318      forkliftTrails.clear();
3319    };
3320    const addHumanTrailPoint = (human, time, size) => {
3321      trimHumanTrail(human, time);
3322      const trail = getHumanTrail(human);
3323      const lastPoint = trail.points[trail.points.length - 1];
3324      const minDistance = Math.max(1.6, size * 0.16);
3325      const minInterval = 70;
3326      if (lastPoint) {
3327        const distance = Math.hypot(human.x - lastPoint.x, human.y - lastPoint.y);
3328        if (distance < minDistance && time - trail.lastPointAt < minInterval) return;
3329      }
3330      trail.points.push({
3331        x: human.x,
3332        y: human.y,
3333        time,
3334        breakBefore: lastPoint ? Math.hypot(human.x - lastPoint.x, human.y - lastPoint.y) > size * 6 : false
3335      });
3336      trail.lastPointAt = time;
3337    };
3338    const addHumanTrailPoints = (time, size) => {
3339      const activeHumanIds = /* @__PURE__ */ new Set();
3340      for (const human of humans) {
3341        activeHumanIds.add(human.id);
3342        addHumanTrailPoint(human, time, size);
3343      }
3344      for (const humanId of humanTrails.keys()) {
3345        if (!activeHumanIds.has(humanId)) humanTrails.delete(humanId);
3346      }
3347    };
3348    const getForkliftTrail = (item) => {
3349      let trail = forkliftTrails.get(item.id);
3350      if (!trail) {
3351        trail = {
3352          points: [],
3353          lastPointAt: 0
3354        };
3355        forkliftTrails.set(item.id, trail);
3356      }
3357      return trail;
3358    };
3359    const trimForkliftTrail = (item, time) => {
3360      const trail = getForkliftTrail(item);
3361      const minTime = time - TRAIL_LIFETIME_MS;
3362      while (trail.points.length && trail.points[0].time < minTime) {
3363        trail.points.shift();
3364      }
3365    };
3366    const addForkliftTrailPoint = (item, time, size) => {
3367      if (!item.initialized) return;
3368      trimForkliftTrail(item, time);
3369      const trail = getForkliftTrail(item);
3370      const lastPoint = trail.points[trail.points.length - 1];
3371      const minDistance = Math.max(1.8, size * 0.18);
3372      const minInterval = 70;
3373      if (lastPoint) {
3374        const distance = Math.hypot(item.x - lastPoint.x, item.y - lastPoint.y);
3375        if (distance < minDistance && time - trail.lastPointAt < minInterval) return;
3376      }
3377      trail.points.push({
3378        x: item.x,
3379        y: item.y,
3380        time,
3381        breakBefore: lastPoint ? Math.hypot(item.x - lastPoint.x, item.y - lastPoint.y) > size * 8 : false
3382      });
3383      trail.lastPointAt = time;
3384    };
3385    const addForkliftTrailPoints = (time, size) => {
3386      const activeForkliftIds = /* @__PURE__ */ new Set();
3387      for (const item of forklifts) {
3388        activeForkliftIds.add(item.id);
3389        addForkliftTrailPoint(item, time, size);
3390      }
3391      for (const forkliftId of forkliftTrails.keys()) {
3392        if (!activeForkliftIds.has(forkliftId)) forkliftTrails.delete(forkliftId);
3393      }
3394    };
3395    const splitTrailIntoContinuousSegments = (points) => {
3396      const segments = [];
3397      let currentSegment = [];
3398      for (const point of points) {
3399        if (point.breakBefore && currentSegment.length) {
3400          segments.push(currentSegment);
3401          currentSegment = [];
3402        }
3403        currentSegment.push(point);
3404      }
3405      if (currentSegment.length) segments.push(currentSegment);
3406      return segments;
3407    };
3408    const chaikinSmoothTrail = (points, iterations = 3) => {
3409      if (points.length < 3) return points;
3410      let smoothed = points.map((point) => ({ ...point }));
3411      for (let iteration = 0; iteration < iterations; iteration++) {
3412        const next = [smoothed[0]];
3413        for (let index = 0; index < smoothed.length - 1; index++) {
3414          const p0 = smoothed[index];
3415          const p1 = smoothed[index + 1];
3416          next.push({
3417            x: p0.x * 0.75 + p1.x * 0.25,
3418            y: p0.y * 0.75 + p1.y * 0.25,
3419            time: p0.time * 0.75 + p1.time * 0.25
3420          });
3421          next.push({
3422            x: p0.x * 0.25 + p1.x * 0.75,
3423            y: p0.y * 0.25 + p1.y * 0.75,
3424            time: p0.time * 0.25 + p1.time * 0.75
3425          });
3426        }
3427        next.push(smoothed[smoothed.length - 1]);
3428        smoothed = next;
3429      }
3430      return smoothed;
3431    };
3432    const drawSoftTrailSegment = (context, points, time, size, color = "0, 122, 255", opacityMultiplier = 0.72) => {
3433      if (points.length < 2) return;
3434      const smoothedPoints = chaikinSmoothTrail(points, 6);
3435      for (let index = 1; index < smoothedPoints.length; index++) {
3436        const previousPoint = smoothedPoints[index - 1];
3437        const currentPoint = smoothedPoints[index];
3438        const age = time - currentPoint.time;
3439        const opacity = Math.max(0, Math.min(1, 1 - age / TRAIL_LIFETIME_MS));
3440        if (opacity <= 0) continue;
3441        context.strokeStyle = `rgba(${color}, ${opacity * opacityMultiplier})`;
3442        context.beginPath();
3443        context.moveTo(previousPoint.x, previousPoint.y);
3444        context.lineTo(currentPoint.x, currentPoint.y);
3445        context.stroke();
3446      }
3447    };
3448    const drawHumanTrails = (context, time, size) => {
3449      if (!humanTrails.size) return;
3450      context.save();
3451      context.lineCap = "round";
3452      context.lineJoin = "round";
3453      context.lineWidth = Math.max(1.7, size * 0.24);
3454      for (const human of humans) {
3455        trimHumanTrail(human, time);
3456        const trail = getHumanTrail(human);
3457        if (trail.points.length < 2) continue;
3458        for (const segment of splitTrailIntoContinuousSegments(trail.points)) {
3459          drawSoftTrailSegment(context, segment, time, size);
3460        }
3461      }
3462      context.restore();
3463    };
3464    const drawForkliftTrails = (context, time, size) => {
3465      if (!forkliftTrails.size) return;
3466      context.save();
3467      context.lineCap = "round";
3468      context.lineJoin = "round";
3469      context.lineWidth = Math.max(2, size * 0.28);
3470      for (const item of forklifts) {
3471        trimForkliftTrail(item, time);
3472        const trail = getForkliftTrail(item);
3473        if (trail.points.length < 2) continue;
3474        for (const segment of splitTrailIntoContinuousSegments(trail.points)) {
3475          drawSoftTrailSegment(context, segment, time, size, "255, 112, 0", 0.8);
3476        }
3477      }
3478      context.restore();
3479    };
3480    const drawHumans = (context, size) => {
3481      if (typeof drawHumanIcon !== "function") return;
3482      const humanIconSize = getHumanIconSize(size);
3483      for (const human of humans) {
3484        drawHumanIcon(context, human.x, human.y, humanIconSize, human.angle, true);
3485      }
3486    };
3487    const drawScene = (context, layout, { time, deltaTime }) => {
3488      const floorplan = drawFloorplanWalls(
3489        context,
3490        layout.rightContentX,
3491        layout.contentY,
3492        layout.contentHalfWidth,
3493        layout.contentHeight
3494      );
3495      lastFloorplan = floorplan;
3496      const floorplanSignature = getFloorplanSignature(floorplan);
3497      if (previousFloorplanSignature && previousFloorplanSignature !== floorplanSignature) {
3498        humans.length = 0;
3499        clearAllHumanTrails();
3500        clearAllForkliftTrails();
3501        outboundPallets.length = 0;
3502        movedPalletEvents.length = 0;
3503        inboundTruck.state = "idle";
3504        inboundTruck.hidden = true;
3505        outboundTruck.state = "idle";
3506        outboundTruck.hidden = true;
3507        deliverySlotShuffle.length = 0;
3508        lastDeliverySlotId = null;
3509        forklift.preferredDeliverySlotId = null;
3510        forklift.humanBlockedTime = 0;
3511        for (const activeForklift of forklifts) {
3512          setActiveForklift(activeForklift);
3513          activeForklift.preferredDeliverySlotId = null;
3514          activeForklift.humanBlockedTime = 0;
3515          clearForkliftPath();
3516          clearRecoveryPath();
3517          activeForklift.initialized = false;
3518        }
3519        setActiveForklift(forklifts[0]);
3520        simulationSeeded = false;
3521      }
3522      previousFloorplanSignature = floorplanSignature;
3523      const forkliftSize = Math.max(8.64, Math.min(16.2, layout.contentWidth * 0.021384));
3524      seedInitialSimulationState(floorplan, forkliftSize, time);
3525      spawnPallet(time, floorplan, forkliftSize);
3526      updateTrucks(deltaTime, time, floorplan, forkliftSize);
3527      setActiveForklift(forklifts[0]);
3528      updateHumans(deltaTime, floorplan, forkliftSize);
3529      for (const activeForklift of forklifts) {
3530        setActiveForklift(activeForklift);
3531        updateForklift(deltaTime, floorplan, forkliftSize);
3532      }
3533      setActiveForklift(forklifts[0]);
3534      addHumanTrailPoints(time, forkliftSize);
3535      addForkliftTrailPoints(time, forkliftSize);
3536      drawHumanTrails(context, time, forkliftSize);
3537      drawForkliftTrails(context, time, forkliftSize);
3538      drawTrucks(context, forkliftSize);
3539      drawWaitingPallets(context, forkliftSize, getStoredPalletRenderItems(floorplan, forkliftSize));
3540      drawWaitingPallets(context, forkliftSize, pallets);
3541      drawWaitingPallets(context, forkliftSize, outboundPallets);
3542      drawHumans(context, forkliftSize);
3543      for (const activeForklift of forklifts) {
3544        drawForklift(context, activeForklift.x, activeForklift.y, forkliftSize, activeForklift.angle, Boolean(activeForklift.carrying));
3545      }
3546    };
3547    const drawDashboard = (context, layout, { time }) => {
3548      drawPalletDashboard(context, layout, {
3549        totalPallets: getTotalPalletsOnSite(),
3550        palletsMoved: getMovedPalletsInWindow(time),
3551        staffCount: humans.length,
3552        maxPallets: getMaxPalletsOnShelves(lastFloorplan),
3553        forkliftTimeSeconds: Math.floor(forkliftOperationTime),
3554        ...getShelfRowPalletDistribution(lastFloorplan)
3555      });
3556    };
3557    mountSpacetimeScreen(canvas, { drawScene, drawDashboard });
3558  })();
3559})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.