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.