diff --git a/public/models/fleet/aircraft/a320.glb b/public/models/fleet/aircraft/a320.glb new file mode 100644 index 000000000..017d8ed2c Binary files /dev/null and b/public/models/fleet/aircraft/a320.glb differ diff --git a/public/models/fleet/aircraft/a333.glb b/public/models/fleet/aircraft/a333.glb new file mode 100644 index 000000000..7daeb8319 Binary files /dev/null and b/public/models/fleet/aircraft/a333.glb differ diff --git a/public/models/fleet/aircraft/a343.glb b/public/models/fleet/aircraft/a343.glb new file mode 100644 index 000000000..a850cea4a Binary files /dev/null and b/public/models/fleet/aircraft/a343.glb differ diff --git a/public/models/fleet/aircraft/a359.glb b/public/models/fleet/aircraft/a359.glb new file mode 100644 index 000000000..745681b27 Binary files /dev/null and b/public/models/fleet/aircraft/a359.glb differ diff --git a/public/models/fleet/aircraft/a388.glb b/public/models/fleet/aircraft/a388.glb new file mode 100644 index 000000000..58e04f2d0 Binary files /dev/null and b/public/models/fleet/aircraft/a388.glb differ diff --git a/public/models/fleet/aircraft/at72.glb b/public/models/fleet/aircraft/at72.glb new file mode 100644 index 000000000..bbe129d7e Binary files /dev/null and b/public/models/fleet/aircraft/at72.glb differ diff --git a/public/models/fleet/aircraft/b738.glb b/public/models/fleet/aircraft/b738.glb new file mode 100644 index 000000000..6e4a94a38 Binary files /dev/null and b/public/models/fleet/aircraft/b738.glb differ diff --git a/public/models/fleet/aircraft/b744.glb b/public/models/fleet/aircraft/b744.glb new file mode 100644 index 000000000..f5f9e421d Binary files /dev/null and b/public/models/fleet/aircraft/b744.glb differ diff --git a/public/models/fleet/aircraft/b763.glb b/public/models/fleet/aircraft/b763.glb new file mode 100644 index 000000000..34c59a912 Binary files /dev/null and b/public/models/fleet/aircraft/b763.glb differ diff --git a/public/models/fleet/aircraft/b77w.glb b/public/models/fleet/aircraft/b77w.glb new file mode 100644 index 000000000..a19051300 Binary files /dev/null and b/public/models/fleet/aircraft/b77w.glb differ diff --git a/public/models/fleet/aircraft/b789.glb b/public/models/fleet/aircraft/b789.glb new file mode 100644 index 000000000..5474a55a4 Binary files /dev/null and b/public/models/fleet/aircraft/b789.glb differ diff --git a/public/models/fleet/aircraft/bcs3.glb b/public/models/fleet/aircraft/bcs3.glb new file mode 100644 index 000000000..422ebe700 Binary files /dev/null and b/public/models/fleet/aircraft/bcs3.glb differ diff --git a/public/models/fleet/aircraft/be20.glb b/public/models/fleet/aircraft/be20.glb new file mode 100644 index 000000000..d1a2c4dfd Binary files /dev/null and b/public/models/fleet/aircraft/be20.glb differ diff --git a/public/models/fleet/aircraft/c17.glb b/public/models/fleet/aircraft/c17.glb new file mode 100644 index 000000000..19c51a523 Binary files /dev/null and b/public/models/fleet/aircraft/c17.glb differ diff --git a/public/models/fleet/aircraft/c172.glb b/public/models/fleet/aircraft/c172.glb new file mode 100644 index 000000000..9a5b7d73f Binary files /dev/null and b/public/models/fleet/aircraft/c172.glb differ diff --git a/public/models/fleet/aircraft/c30j.glb b/public/models/fleet/aircraft/c30j.glb new file mode 100644 index 000000000..b5d4a7122 Binary files /dev/null and b/public/models/fleet/aircraft/c30j.glb differ diff --git a/public/models/fleet/aircraft/c56x.glb b/public/models/fleet/aircraft/c56x.glb new file mode 100644 index 000000000..2185dfe79 Binary files /dev/null and b/public/models/fleet/aircraft/c56x.glb differ diff --git a/public/models/fleet/aircraft/cl35.glb b/public/models/fleet/aircraft/cl35.glb new file mode 100644 index 000000000..f2df5f86b Binary files /dev/null and b/public/models/fleet/aircraft/cl35.glb differ diff --git a/public/models/fleet/aircraft/crj9.glb b/public/models/fleet/aircraft/crj9.glb new file mode 100644 index 000000000..24e6448b6 Binary files /dev/null and b/public/models/fleet/aircraft/crj9.glb differ diff --git a/public/models/fleet/aircraft/dh8d.glb b/public/models/fleet/aircraft/dh8d.glb new file mode 100644 index 000000000..f763fec21 Binary files /dev/null and b/public/models/fleet/aircraft/dh8d.glb differ diff --git a/public/models/fleet/aircraft/e175.glb b/public/models/fleet/aircraft/e175.glb new file mode 100644 index 000000000..acdc5230e Binary files /dev/null and b/public/models/fleet/aircraft/e175.glb differ diff --git a/public/models/fleet/aircraft/e55p.glb b/public/models/fleet/aircraft/e55p.glb new file mode 100644 index 000000000..b130d2a9e Binary files /dev/null and b/public/models/fleet/aircraft/e55p.glb differ diff --git a/public/models/fleet/aircraft/generic_plane.glb b/public/models/fleet/aircraft/generic_plane.glb new file mode 100644 index 000000000..a126c8b26 Binary files /dev/null and b/public/models/fleet/aircraft/generic_plane.glb differ diff --git a/public/models/fleet/aircraft/glf4.glb b/public/models/fleet/aircraft/glf4.glb new file mode 100644 index 000000000..31ddd2c74 Binary files /dev/null and b/public/models/fleet/aircraft/glf4.glb differ diff --git a/public/models/fleet/aircraft/glider.glb b/public/models/fleet/aircraft/glider.glb new file mode 100644 index 000000000..60b722726 Binary files /dev/null and b/public/models/fleet/aircraft/glider.glb differ diff --git a/public/models/fleet/aircraft/k35r.glb b/public/models/fleet/aircraft/k35r.glb new file mode 100644 index 000000000..7a7cfbe73 Binary files /dev/null and b/public/models/fleet/aircraft/k35r.glb differ diff --git a/public/models/fleet/aircraft/md11.glb b/public/models/fleet/aircraft/md11.glb new file mode 100644 index 000000000..885a81624 Binary files /dev/null and b/public/models/fleet/aircraft/md11.glb differ diff --git a/public/models/fleet/aircraft/mq9.glb b/public/models/fleet/aircraft/mq9.glb new file mode 100644 index 000000000..737912fc3 Binary files /dev/null and b/public/models/fleet/aircraft/mq9.glb differ diff --git a/public/models/fleet/aircraft/p28a.glb b/public/models/fleet/aircraft/p28a.glb new file mode 100644 index 000000000..ca47aec1a Binary files /dev/null and b/public/models/fleet/aircraft/p28a.glb differ diff --git a/public/models/fleet/aircraft/pc12.glb b/public/models/fleet/aircraft/pc12.glb new file mode 100644 index 000000000..2b2666a0a Binary files /dev/null and b/public/models/fleet/aircraft/pc12.glb differ diff --git a/public/models/fleet/aircraft/sr22.glb b/public/models/fleet/aircraft/sr22.glb new file mode 100644 index 000000000..7c2d66fe6 Binary files /dev/null and b/public/models/fleet/aircraft/sr22.glb differ diff --git a/public/models/fleet/aircraft/t38.glb b/public/models/fleet/aircraft/t38.glb new file mode 100644 index 000000000..b9c8cd9ee Binary files /dev/null and b/public/models/fleet/aircraft/t38.glb differ diff --git a/public/models/fleet/aircraft/tex2.glb b/public/models/fleet/aircraft/tex2.glb new file mode 100644 index 000000000..8a3ec3e00 Binary files /dev/null and b/public/models/fleet/aircraft/tex2.glb differ diff --git a/public/models/fleet/aircraft/ultralight.glb b/public/models/fleet/aircraft/ultralight.glb new file mode 100644 index 000000000..965d9cddd Binary files /dev/null and b/public/models/fleet/aircraft/ultralight.glb differ diff --git a/public/models/fleet/aircraft/v22.glb b/public/models/fleet/aircraft/v22.glb new file mode 100644 index 000000000..01021ca2d Binary files /dev/null and b/public/models/fleet/aircraft/v22.glb differ diff --git a/public/models/fleet/buoys/barrel_yellow.glb b/public/models/fleet/buoys/barrel_yellow.glb new file mode 100644 index 000000000..d2fb0e017 Binary files /dev/null and b/public/models/fleet/buoys/barrel_yellow.glb differ diff --git a/public/models/fleet/buoys/buoy_cardinal_east.glb b/public/models/fleet/buoys/buoy_cardinal_east.glb new file mode 100644 index 000000000..a0053edb0 Binary files /dev/null and b/public/models/fleet/buoys/buoy_cardinal_east.glb differ diff --git a/public/models/fleet/buoys/buoy_cardinal_north.glb b/public/models/fleet/buoys/buoy_cardinal_north.glb new file mode 100644 index 000000000..adac99ccb Binary files /dev/null and b/public/models/fleet/buoys/buoy_cardinal_north.glb differ diff --git a/public/models/fleet/buoys/buoy_cardinal_south.glb b/public/models/fleet/buoys/buoy_cardinal_south.glb new file mode 100644 index 000000000..b84b392f2 Binary files /dev/null and b/public/models/fleet/buoys/buoy_cardinal_south.glb differ diff --git a/public/models/fleet/buoys/buoy_cardinal_west.glb b/public/models/fleet/buoys/buoy_cardinal_west.glb new file mode 100644 index 000000000..d9891b253 Binary files /dev/null and b/public/models/fleet/buoys/buoy_cardinal_west.glb differ diff --git a/public/models/fleet/buoys/buoy_isolated_danger.glb b/public/models/fleet/buoys/buoy_isolated_danger.glb new file mode 100644 index 000000000..e27187ac1 Binary files /dev/null and b/public/models/fleet/buoys/buoy_isolated_danger.glb differ diff --git a/public/models/fleet/buoys/buoy_lateral_port.glb b/public/models/fleet/buoys/buoy_lateral_port.glb new file mode 100644 index 000000000..dcc6bf1c1 Binary files /dev/null and b/public/models/fleet/buoys/buoy_lateral_port.glb differ diff --git a/public/models/fleet/buoys/buoy_lateral_starboard.glb b/public/models/fleet/buoys/buoy_lateral_starboard.glb new file mode 100644 index 000000000..c025c8cb3 Binary files /dev/null and b/public/models/fleet/buoys/buoy_lateral_starboard.glb differ diff --git a/public/models/fleet/buoys/buoy_safe_water.glb b/public/models/fleet/buoys/buoy_safe_water.glb new file mode 100644 index 000000000..47fbc5602 Binary files /dev/null and b/public/models/fleet/buoys/buoy_safe_water.glb differ diff --git a/public/models/fleet/buoys/buoy_special.glb b/public/models/fleet/buoys/buoy_special.glb new file mode 100644 index 000000000..faee37740 Binary files /dev/null and b/public/models/fleet/buoys/buoy_special.glb differ diff --git a/public/models/fleet/buoys/can_red.glb b/public/models/fleet/buoys/can_red.glb new file mode 100644 index 000000000..97beeffa7 Binary files /dev/null and b/public/models/fleet/buoys/can_red.glb differ diff --git a/public/models/fleet/buoys/can_red_cylinder.glb b/public/models/fleet/buoys/can_red_cylinder.glb new file mode 100644 index 000000000..bd87b55f4 Binary files /dev/null and b/public/models/fleet/buoys/can_red_cylinder.glb differ diff --git a/public/models/fleet/buoys/conical_green.glb b/public/models/fleet/buoys/conical_green.glb new file mode 100644 index 000000000..36eb6729c Binary files /dev/null and b/public/models/fleet/buoys/conical_green.glb differ diff --git a/public/models/fleet/buoys/conical_green_cone.glb b/public/models/fleet/buoys/conical_green_cone.glb new file mode 100644 index 000000000..c5a6b4476 Binary files /dev/null and b/public/models/fleet/buoys/conical_green_cone.glb differ diff --git a/public/models/fleet/buoys/pile_green_cone.glb b/public/models/fleet/buoys/pile_green_cone.glb new file mode 100644 index 000000000..6960071c9 Binary files /dev/null and b/public/models/fleet/buoys/pile_green_cone.glb differ diff --git a/public/models/fleet/buoys/pile_red_cylinder.glb b/public/models/fleet/buoys/pile_red_cylinder.glb new file mode 100644 index 000000000..a61336602 Binary files /dev/null and b/public/models/fleet/buoys/pile_red_cylinder.glb differ diff --git a/public/models/fleet/buoys/spar_lateral_port.glb b/public/models/fleet/buoys/spar_lateral_port.glb new file mode 100644 index 000000000..1152dd928 Binary files /dev/null and b/public/models/fleet/buoys/spar_lateral_port.glb differ diff --git a/public/models/fleet/buoys/spar_lateral_starboard.glb b/public/models/fleet/buoys/spar_lateral_starboard.glb new file mode 100644 index 000000000..193882ac8 Binary files /dev/null and b/public/models/fleet/buoys/spar_lateral_starboard.glb differ diff --git a/public/models/fleet/buoys/spherical_green.glb b/public/models/fleet/buoys/spherical_green.glb new file mode 100644 index 000000000..fc06b7920 Binary files /dev/null and b/public/models/fleet/buoys/spherical_green.glb differ diff --git a/public/models/fleet/buoys/spherical_yellow.glb b/public/models/fleet/buoys/spherical_yellow.glb new file mode 100644 index 000000000..d8b660b7c Binary files /dev/null and b/public/models/fleet/buoys/spherical_yellow.glb differ diff --git a/public/models/fleet/buoys/spherical_yellow_x.glb b/public/models/fleet/buoys/spherical_yellow_x.glb new file mode 100644 index 000000000..2ebbee87c Binary files /dev/null and b/public/models/fleet/buoys/spherical_yellow_x.glb differ diff --git a/public/models/fleet/buoys/stake_green_cone.glb b/public/models/fleet/buoys/stake_green_cone.glb new file mode 100644 index 000000000..25fc6758b Binary files /dev/null and b/public/models/fleet/buoys/stake_green_cone.glb differ diff --git a/public/models/fleet/buoys/stake_red_cylinder.glb b/public/models/fleet/buoys/stake_red_cylinder.glb new file mode 100644 index 000000000..31164848d Binary files /dev/null and b/public/models/fleet/buoys/stake_red_cylinder.glb differ diff --git a/public/models/fleet/buoys/stake_yellow_x.glb b/public/models/fleet/buoys/stake_yellow_x.glb new file mode 100644 index 000000000..cdd756643 Binary files /dev/null and b/public/models/fleet/buoys/stake_yellow_x.glb differ diff --git a/public/models/fleet/catalog.json b/public/models/fleet/catalog.json new file mode 100644 index 000000000..1ce347760 --- /dev/null +++ b/public/models/fleet/catalog.json @@ -0,0 +1,2301 @@ +{ + "a139": { + "category": "helicopters", + "file": "helicopters/a139.glb", + "min": [ + -8.045, + -6.626, + -1.575 + ], + "max": [ + 7.638, + 6.626, + 2.979 + ], + "length": 15.683, + "beam": 13.253, + "clearance": 1.575, + "tris": 1464, + "icon": "heli" + }, + "a320": { + "category": "aircraft", + "file": "aircraft/a320.glb", + "min": [ + -18.785, + -17.806, + -3.063 + ], + "max": [ + 18.785, + 17.806, + 7.56 + ], + "length": 37.57, + "beam": 35.611, + "clearance": 3.063, + "tris": 2338, + "icon": "jet" + }, + "a333": { + "category": "aircraft", + "file": "aircraft/a333.glb", + "min": [ + -31.845, + -29.842, + -4.251 + ], + "max": [ + 31.845, + 29.842, + 10.854 + ], + "length": 63.69, + "beam": 59.684, + "clearance": 4.251, + "tris": 2338, + "icon": "widebody" + }, + "a343": { + "category": "aircraft", + "file": "aircraft/a343.glb", + "min": [ + -31.845, + -29.842, + -3.343 + ], + "max": [ + 31.845, + 29.842, + 10.854 + ], + "length": 63.69, + "beam": 59.684, + "clearance": 3.343, + "tris": 2638, + "icon": "quad" + }, + "a359": { + "category": "aircraft", + "file": "aircraft/a359.glb", + "min": [ + -33.4, + -32.106, + -4.83 + ], + "max": [ + 33.4, + 32.106, + 11.233 + ], + "length": 66.8, + "beam": 64.212, + "clearance": 4.83, + "tris": 2338, + "icon": "widebody" + }, + "a388": { + "category": "aircraft", + "file": "aircraft/a388.glb", + "min": [ + -36.36, + -39.291, + -4.827 + ], + "max": [ + 37.527, + 39.291, + 14.899 + ], + "length": 73.887, + "beam": 78.582, + "clearance": 4.827, + "tris": 2658, + "icon": "quad" + }, + "articulated_bus": { + "category": "transit", + "file": "transit/articulated_bus.glb", + "min": [ + -9.05, + -1.32, + -0.0 + ], + "max": [ + 9.05, + 1.355, + 3.33 + ], + "length": 18.1, + "beam": 2.675, + "clearance": 0.0, + "tris": 604, + "icon": "bus" + }, + "as50": { + "category": "helicopters", + "file": "helicopters/as50.glb", + "min": [ + -5.465, + -4.709, + -1.365 + ], + "max": [ + 6.023, + 4.709, + 2.267 + ], + "length": 11.488, + "beam": 9.418, + "clearance": 1.365, + "tris": 1488, + "icon": "heli" + }, + "at72": { + "category": "aircraft", + "file": "aircraft/at72.glb", + "min": [ + -13.585, + -13.255, + -1.402 + ], + "max": [ + 13.585, + 13.255, + 5.503 + ], + "length": 27.17, + "beam": 26.509, + "clearance": 1.402, + "tris": 2300, + "icon": "turboprop" + }, + "b06": { + "category": "helicopters", + "file": "helicopters/b06.glb", + "min": [ + -6.695, + -1.029, + -1.265 + ], + "max": [ + 5.294, + 1.029, + 1.91 + ], + "length": 11.989, + "beam": 2.057, + "clearance": 1.265, + "tris": 1476, + "icon": "heli" + }, + "b407": { + "category": "helicopters", + "file": "helicopters/b407.glb", + "min": [ + -7.137, + -5.335, + -1.315 + ], + "max": [ + 5.83, + 5.335, + 2.128 + ], + "length": 12.967, + "beam": 10.67, + "clearance": 1.315, + "tris": 1500, + "icon": "heli" + }, + "b738": { + "category": "aircraft", + "file": "aircraft/b738.glb", + "min": [ + -19.735, + -17.802, + -2.528 + ], + "max": [ + 19.735, + 17.802, + 8.122 + ], + "length": 39.47, + "beam": 35.603, + "clearance": 2.528, + "tris": 2350, + "icon": "jet" + }, + "b744": { + "category": "aircraft", + "file": "aircraft/b744.glb", + "min": [ + -35.33, + -31.777, + -3.786 + ], + "max": [ + 35.809, + 31.777, + 12.595 + ], + "length": 71.139, + "beam": 63.554, + "clearance": 3.786, + "tris": 2910, + "icon": "quad" + }, + "b763": { + "category": "aircraft", + "file": "aircraft/b763.glb", + "min": [ + -27.45, + -23.692, + -3.852 + ], + "max": [ + 27.45, + 23.692, + 10.008 + ], + "length": 54.9, + "beam": 47.385, + "clearance": 3.852, + "tris": 2338, + "icon": "widebody" + }, + "b77w": { + "category": "aircraft", + "file": "aircraft/b77w.glb", + "min": [ + -36.95, + -31.752, + -5.444 + ], + "max": [ + 36.95, + 31.752, + 11.469 + ], + "length": 73.9, + "beam": 63.504, + "clearance": 5.444, + "tris": 2186, + "icon": "widebody" + }, + "b789": { + "category": "aircraft", + "file": "aircraft/b789.glb", + "min": [ + -31.4, + -29.449, + -4.769 + ], + "max": [ + 31.4, + 29.449, + 10.973 + ], + "length": 62.8, + "beam": 58.898, + "clearance": 4.769, + "tris": 2186, + "icon": "widebody" + }, + "balloon": { + "category": "other", + "file": "other/balloon.glb", + "min": [ + -8.5, + -8.5, + -5.95 + ], + "max": [ + 8.5, + 8.5, + 19.55 + ], + "length": 16.999, + "beam": 16.999, + "clearance": 5.95, + "tris": 504, + "icon": "balloon" + }, + "barrel_yellow": { + "category": "buoys", + "file": "buoys/barrel_yellow.glb", + "min": [ + -0.85, + -0.5, + -0.3 + ], + "max": [ + 0.85, + 0.5, + 0.85 + ], + "length": 1.7, + "beam": 1.0, + "clearance": 0.3, + "tris": 80, + "icon": "buoy" + }, + "bcs3": { + "category": "aircraft", + "file": "aircraft/bcs3.glb", + "min": [ + -19.35, + -17.439, + -3.196 + ], + "max": [ + 19.35, + 17.439, + 7.086 + ], + "length": 38.7, + "beam": 34.879, + "clearance": 3.196, + "tris": 2338, + "icon": "jet" + }, + "be20": { + "category": "aircraft", + "file": "aircraft/be20.glb", + "min": [ + -6.67, + -8.139, + -0.871 + ], + "max": [ + 7.162, + 8.139, + 3.351 + ], + "length": 13.832, + "beam": 16.278, + "clearance": 0.871, + "tris": 2386, + "icon": "turboprop" + }, + "blimp": { + "category": "other", + "file": "other/blimp.glb", + "min": [ + -37.5, + -8.427, + -10.6 + ], + "max": [ + 37.5, + 8.427, + 8.427 + ], + "length": 75.0, + "beam": 16.855, + "clearance": 10.6, + "tris": 1044, + "icon": "balloon" + }, + "bulk_carrier": { + "category": "ships", + "file": "ships/bulk_carrier.glb", + "min": [ + -98.8, + -16.0, + -10.88 + ], + "max": [ + 95.0, + 16.0, + 30.76 + ], + "length": 193.8, + "beam": 32.0, + "clearance": 10.88, + "tris": 3121, + "icon": "ship" + }, + "buoy_cardinal_east": { + "category": "buoys", + "file": "buoys/buoy_cardinal_east.glb", + "min": [ + -1.56, + -1.56, + -3.2 + ], + "max": [ + 1.56, + 1.56, + 5.7 + ], + "length": 3.12, + "beam": 3.12, + "clearance": 3.2, + "tris": 654, + "icon": "buoy" + }, + "buoy_cardinal_north": { + "category": "buoys", + "file": "buoys/buoy_cardinal_north.glb", + "min": [ + -1.56, + -1.56, + -3.2 + ], + "max": [ + 1.56, + 1.56, + 5.7 + ], + "length": 3.12, + "beam": 3.12, + "clearance": 3.2, + "tris": 606, + "icon": "buoy" + }, + "buoy_cardinal_south": { + "category": "buoys", + "file": "buoys/buoy_cardinal_south.glb", + "min": [ + -1.56, + -1.56, + -3.2 + ], + "max": [ + 1.56, + 1.56, + 5.7 + ], + "length": 3.12, + "beam": 3.12, + "clearance": 3.2, + "tris": 606, + "icon": "buoy" + }, + "buoy_cardinal_west": { + "category": "buoys", + "file": "buoys/buoy_cardinal_west.glb", + "min": [ + -1.56, + -1.56, + -3.2 + ], + "max": [ + 1.56, + 1.56, + 5.7 + ], + "length": 3.12, + "beam": 3.12, + "clearance": 3.2, + "tris": 654, + "icon": "buoy" + }, + "buoy_isolated_danger": { + "category": "buoys", + "file": "buoys/buoy_isolated_danger.glb", + "min": [ + -1.56, + -1.56, + -3.2 + ], + "max": [ + 1.56, + 1.56, + 5.65 + ], + "length": 3.12, + "beam": 3.12, + "clearance": 3.2, + "tris": 986, + "icon": "buoy" + }, + "buoy_lateral_port": { + "category": "buoys", + "file": "buoys/buoy_lateral_port.glb", + "min": [ + -1.56, + -1.56, + -3.2 + ], + "max": [ + 1.56, + 1.56, + 4.95 + ], + "length": 3.12, + "beam": 3.12, + "clearance": 3.2, + "tris": 558, + "icon": "buoy" + }, + "buoy_lateral_starboard": { + "category": "buoys", + "file": "buoys/buoy_lateral_starboard.glb", + "min": [ + -1.56, + -1.56, + -3.2 + ], + "max": [ + 1.56, + 1.56, + 4.95 + ], + "length": 3.12, + "beam": 3.12, + "clearance": 3.2, + "tris": 528, + "icon": "buoy" + }, + "buoy_safe_water": { + "category": "buoys", + "file": "buoys/buoy_safe_water.glb", + "min": [ + -1.56, + -1.56, + -3.2 + ], + "max": [ + 1.56, + 1.56, + 4.95 + ], + "length": 3.12, + "beam": 3.12, + "clearance": 3.2, + "tris": 694, + "icon": "buoy" + }, + "buoy_special": { + "category": "buoys", + "file": "buoys/buoy_special.glb", + "min": [ + -1.56, + -1.56, + -3.2 + ], + "max": [ + 1.56, + 1.56, + 5.202 + ], + "length": 3.12, + "beam": 3.12, + "clearance": 3.2, + "tris": 514, + "icon": "buoy" + }, + "c17": { + "category": "aircraft", + "file": "aircraft/c17.glb", + "min": [ + -26.52, + -25.688, + -3.467 + ], + "max": [ + 28.899, + 25.688, + 11.893 + ], + "length": 55.419, + "beam": 51.377, + "clearance": 3.467, + "tris": 2768, + "icon": "widebody" + }, + "c172": { + "category": "aircraft", + "file": "aircraft/c172.glb", + "min": [ + -4.416, + -5.39, + -1.49 + ], + "max": [ + 4.548, + 5.39, + 1.97 + ], + "length": 8.964, + "beam": 10.78, + "clearance": 1.49, + "tris": 2162, + "icon": "light" + }, + "c30j": { + "category": "aircraft", + "file": "aircraft/c30j.glb", + "min": [ + -14.895, + -19.801, + -2.328 + ], + "max": [ + 15.001, + 19.801, + 9.051 + ], + "length": 29.896, + "beam": 39.602, + "clearance": 2.328, + "tris": 2800, + "icon": "turboprop" + }, + "c56x": { + "category": "aircraft", + "file": "aircraft/c56x.glb", + "min": [ + -7.895, + -8.413, + -0.911 + ], + "max": [ + 8.028, + 8.413, + 3.458 + ], + "length": 15.923, + "beam": 16.827, + "clearance": 0.911, + "tris": 2198, + "icon": "rear_jet" + }, + "cable_tram": { + "category": "transit", + "file": "transit/cable_tram.glb", + "min": [ + -4.3, + -1.445, + -0.0 + ], + "max": [ + 4.3, + 1.445, + 3.13 + ], + "length": 8.6, + "beam": 2.89, + "clearance": 0.0, + "tris": 384, + "icon": "tram" + }, + "can_red": { + "category": "buoys", + "file": "buoys/can_red.glb", + "min": [ + -0.8, + -0.8, + -0.6 + ], + "max": [ + 0.8, + 0.8, + 1.18 + ], + "length": 1.6, + "beam": 1.6, + "clearance": 0.6, + "tris": 228, + "icon": "buoy" + }, + "can_red_cylinder": { + "category": "buoys", + "file": "buoys/can_red_cylinder.glb", + "min": [ + -0.8, + -0.8, + -0.6 + ], + "max": [ + 0.8, + 0.8, + 1.9 + ], + "length": 1.6, + "beam": 1.6, + "clearance": 0.6, + "tris": 308, + "icon": "buoy" + }, + "car": { + "category": "other", + "file": "other/car.glb", + "min": [ + -2.31, + -0.906, + 0.019 + ], + "max": [ + 2.3, + 0.906, + 1.948 + ], + "length": 4.61, + "beam": 1.812, + "clearance": 0.0, + "tris": 206, + "icon": "vehicle" + }, + "car_carrier": { + "category": "ships", + "file": "ships/car_carrier.glb", + "min": [ + -101.0, + -16.0, + -8.96 + ], + "max": [ + 100.0, + 18.315, + 38.525 + ], + "length": 201.0, + "beam": 34.315, + "clearance": 8.96, + "tris": 3949, + "icon": "ship" + }, + "city_bus": { + "category": "transit", + "file": "transit/city_bus.glb", + "min": [ + -6.05, + -1.32, + -0.0 + ], + "max": [ + 6.05, + 1.355, + 3.33 + ], + "length": 12.1, + "beam": 2.675, + "clearance": 0.0, + "tris": 432, + "icon": "bus" + }, + "cl35": { + "category": "aircraft", + "file": "aircraft/cl35.glb", + "min": [ + -10.46, + -10.446, + -1.189 + ], + "max": [ + 12.27, + 10.446, + 4.351 + ], + "length": 22.73, + "beam": 20.892, + "clearance": 1.189, + "tris": 2434, + "icon": "rear_jet" + }, + "coach": { + "category": "transit", + "file": "transit/coach.glb", + "min": [ + -6.51, + -1.32, + -0.0 + ], + "max": [ + 6.5, + 1.355, + 3.739 + ], + "length": 13.01, + "beam": 2.675, + "clearance": 0.0, + "tris": 474, + "icon": "bus" + }, + "commuter_emu": { + "category": "transit", + "file": "transit/commuter_emu.glb", + "min": [ + -49.05, + -1.53, + -0.0 + ], + "max": [ + 49.05, + 1.53, + 5.733 + ], + "length": 98.1, + "beam": 3.06, + "clearance": 0.0, + "tris": 1570, + "icon": "rail" + }, + "conical_green": { + "category": "buoys", + "file": "buoys/conical_green.glb", + "min": [ + -0.85, + -0.85, + -0.6 + ], + "max": [ + 0.85, + 0.85, + 1.7 + ], + "length": 1.7, + "beam": 1.7, + "clearance": 0.6, + "tris": 172, + "icon": "buoy" + }, + "conical_green_cone": { + "category": "buoys", + "file": "buoys/conical_green_cone.glb", + "min": [ + -0.85, + -0.85, + -0.6 + ], + "max": [ + 0.85, + 0.85, + 2.42 + ], + "length": 1.7, + "beam": 1.7, + "clearance": 0.6, + "tris": 222, + "icon": "buoy" + }, + "container_ship": { + "category": "ships", + "file": "ships/container_ship.glb", + "min": [ + -159.9, + -24.0, + -13.92 + ], + "max": [ + 150.0, + 24.0, + 47.94 + ], + "length": 309.9, + "beam": 48.0, + "clearance": 13.92, + "tris": 14337, + "icon": "ship" + }, + "crj9": { + "category": "aircraft", + "file": "aircraft/crj9.glb", + "min": [ + -18.1, + -12.347, + -1.339 + ], + "max": [ + 18.396, + 12.347, + 5.684 + ], + "length": 36.496, + "beam": 24.694, + "clearance": 1.339, + "tris": 2334, + "icon": "rear_jet" + }, + "cruise_ship": { + "category": "ships", + "file": "ships/cruise_ship.glb", + "min": [ + -168.0, + -22.5, + -8.4 + ], + "max": [ + 150.0, + 22.5, + 67.55 + ], + "length": 318.0, + "beam": 45.0, + "clearance": 8.4, + "tris": 3759, + "icon": "passenger" + }, + "dh8d": { + "category": "aircraft", + "file": "aircraft/dh8d.glb", + "min": [ + -16.4, + -13.916, + -1.339 + ], + "max": [ + 16.4, + 13.916, + 5.709 + ], + "length": 32.8, + "beam": 27.832, + "clearance": 1.339, + "tris": 2364, + "icon": "turboprop" + }, + "double_decker": { + "category": "transit", + "file": "transit/double_decker.glb", + "min": [ + -5.55, + -1.32, + -0.0 + ], + "max": [ + 5.55, + 1.355, + 4.387 + ], + "length": 11.1, + "beam": 2.675, + "clearance": 0.0, + "tris": 442, + "icon": "bus" + }, + "dredger": { + "category": "ships", + "file": "ships/dredger.glb", + "min": [ + -53.0, + -10.06, + -7.0 + ], + "max": [ + 50.0, + 11.0, + 17.955 + ], + "length": 103.0, + "beam": 21.06, + "clearance": 7.0, + "tris": 2597, + "icon": "ship" + }, + "e175": { + "category": "aircraft", + "file": "aircraft/e175.glb", + "min": [ + -15.84, + -12.917, + -2.427 + ], + "max": [ + 15.84, + 12.917, + 5.818 + ], + "length": 31.68, + "beam": 25.835, + "clearance": 2.427, + "tris": 2338, + "icon": "jet" + }, + "e55p": { + "category": "aircraft", + "file": "aircraft/e55p.glb", + "min": [ + -7.95, + -8.042, + -0.896 + ], + "max": [ + 8.222, + 8.042, + 3.469 + ], + "length": 16.172, + "beam": 16.084, + "clearance": 0.896, + "tris": 2414, + "icon": "rear_jet" + }, + "ec35": { + "category": "helicopters", + "file": "helicopters/ec35.glb", + "min": [ + -6.834, + -5.1, + -1.365 + ], + "max": [ + 5.304, + 5.1, + 2.0 + ], + "length": 12.138, + "beam": 10.2, + "clearance": 1.365, + "tris": 1420, + "icon": "heli" + }, + "ec45": { + "category": "helicopters", + "file": "helicopters/ec45.glb", + "min": [ + -7.472, + -5.5, + -1.54 + ], + "max": [ + 6.032, + 5.5, + 2.65 + ], + "length": 13.504, + "beam": 11.0, + "clearance": 1.54, + "tris": 1420, + "icon": "heli" + }, + "fast_ferry": { + "category": "ships", + "file": "ships/fast_ferry.glb", + "min": [ + -22.4, + -5.56, + -1.32 + ], + "max": [ + 20.0, + 5.56, + 8.3 + ], + "length": 42.4, + "beam": 11.12, + "clearance": 1.32, + "tris": 4754, + "icon": "passenger" + }, + "feeder": { + "category": "ships", + "file": "ships/feeder.glb", + "min": [ + -79.95, + -11.5, + -6.67 + ], + "max": [ + 75.0, + 11.5, + 28.09 + ], + "length": 154.95, + "beam": 23.0, + "clearance": 6.67, + "tris": 3519, + "icon": "ship" + }, + "ferry": { + "category": "ships", + "file": "ships/ferry.glb", + "min": [ + -70.2, + -12.9, + -5.28 + ], + "max": [ + 65.05, + 12.9, + 31.575 + ], + "length": 135.25, + "beam": 25.8, + "clearance": 5.28, + "tris": 3157, + "icon": "passenger" + }, + "fire_truck": { + "category": "other", + "file": "other/fire_truck.glb", + "min": [ + -5.51, + -1.5, + 0.027 + ], + "max": [ + 5.5, + 1.5, + 4.08 + ], + "length": 11.01, + "beam": 3.0, + "clearance": 0.0, + "tris": 314, + "icon": "vehicle" + }, + "fishing_boat": { + "category": "ships", + "file": "ships/fishing_boat.glb", + "min": [ + -7.2, + -2.0, + -1.2 + ], + "max": [ + 6.0, + 2.0, + 5.753 + ], + "length": 13.2, + "beam": 4.0, + "clearance": 1.2, + "tris": 2839, + "icon": "boat" + }, + "funicular": { + "category": "transit", + "file": "transit/funicular.glb", + "min": [ + -7.05, + -1.345, + -0.0 + ], + "max": [ + 7.05, + 1.345, + 5.43 + ], + "length": 14.1, + "beam": 2.69, + "clearance": 0.0, + "tris": 308, + "icon": "rail" + }, + "general_cargo": { + "category": "ships", + "file": "ships/general_cargo.glb", + "min": [ + -47.7, + -7.0, + -5.32 + ], + "max": [ + 45.0, + 7.0, + 18.846 + ], + "length": 92.7, + "beam": 14.0, + "clearance": 5.32, + "tris": 2797, + "icon": "ship" + }, + "generic_heli": { + "category": "helicopters", + "file": "helicopters/generic_heli.glb", + "min": [ + -7.37, + -5.5, + -1.365 + ], + "max": [ + 5.927, + 5.5, + 2.28 + ], + "length": 13.297, + "beam": 11.0, + "clearance": 1.365, + "tris": 1272, + "icon": "heli" + }, + "generic_plane": { + "category": "aircraft", + "file": "aircraft/generic_plane.glb", + "min": [ + -18.0, + -16.66, + -2.906 + ], + "max": [ + 18.0, + 16.66, + 7.373 + ], + "length": 36.0, + "beam": 33.32, + "clearance": 2.906, + "tris": 2166, + "icon": "jet" + }, + "generic_vessel": { + "category": "ships", + "file": "ships/generic_vessel.glb", + "min": [ + -21.6, + -4.5, + -2.7 + ], + "max": [ + 20.0, + 4.5, + 11.6 + ], + "length": 41.6, + "beam": 9.0, + "clearance": 2.7, + "tris": 2389, + "icon": "ship" + }, + "glf4": { + "category": "aircraft", + "file": "aircraft/glf4.glb", + "min": [ + -13.46, + -11.79, + -1.189 + ], + "max": [ + 15.73, + 11.79, + 5.022 + ], + "length": 29.19, + "beam": 23.58, + "clearance": 1.189, + "tris": 2474, + "icon": "rear_jet" + }, + "glider": { + "category": "aircraft", + "file": "aircraft/glider.glb", + "min": [ + -3.4, + -7.396, + -0.398 + ], + "max": [ + 3.561, + 7.396, + 1.384 + ], + "length": 6.961, + "beam": 14.793, + "clearance": 0.398, + "tris": 2314, + "icon": "light" + }, + "gondola": { + "category": "transit", + "file": "transit/gondola.glb", + "min": [ + -1.05, + -0.938, + 0.014 + ], + "max": [ + 1.05, + 0.938, + 4.6 + ], + "length": 2.1, + "beam": 1.876, + "clearance": 0.0, + "tris": 282, + "icon": "unknown" + }, + "h47": { + "category": "helicopters", + "file": "helicopters/h47.glb", + "min": [ + -10.3, + -9.151, + -2.588 + ], + "max": [ + 13.921, + 8.103, + 4.78 + ], + "length": 24.221, + "beam": 17.254, + "clearance": 2.588, + "tris": 1430, + "icon": "heli_tandem" + }, + "h60": { + "category": "helicopters", + "file": "helicopters/h60.glb", + "min": [ + -10.774, + -8.18, + -1.765 + ], + "max": [ + 9.13, + 8.18, + 3.265 + ], + "length": 19.904, + "beam": 16.36, + "clearance": 1.765, + "tris": 1560, + "icon": "heli" + }, + "h64": { + "category": "helicopters", + "file": "helicopters/h64.glb", + "min": [ + -9.875, + -7.315, + -1.475 + ], + "max": [ + 8.545, + 7.315, + 2.742 + ], + "length": 18.42, + "beam": 14.63, + "clearance": 1.475, + "tris": 2040, + "icon": "heli" + }, + "high_speed": { + "category": "transit", + "file": "transit/high_speed.glb", + "min": [ + -103.1, + -1.555, + -0.0 + ], + "max": [ + 103.1, + 1.555, + 5.285 + ], + "length": 206.2, + "beam": 3.11, + "clearance": 0.0, + "tris": 2844, + "icon": "rail" + }, + "inland_cargo": { + "category": "ships", + "file": "ships/inland_cargo.glb", + "min": [ + -56.32, + -5.725, + -3.0 + ], + "max": [ + 55.0, + 5.725, + 9.995 + ], + "length": 111.32, + "beam": 11.45, + "clearance": 3.0, + "tris": 3277, + "icon": "ship" + }, + "inland_tanker": { + "category": "ships", + "file": "ships/inland_tanker.glb", + "min": [ + -56.32, + -5.725, + -3.2 + ], + "max": [ + 55.0, + 5.725, + 7.5 + ], + "length": 111.32, + "beam": 11.45, + "clearance": 3.2, + "tris": 2709, + "icon": "tanker" + }, + "intercity": { + "category": "transit", + "file": "transit/intercity.glb", + "min": [ + -91.4, + -1.545, + -0.0 + ], + "max": [ + 91.45, + 1.545, + 5.933 + ], + "length": 182.85, + "beam": 3.09, + "clearance": 0.0, + "tris": 3168, + "icon": "rail" + }, + "k35r": { + "category": "aircraft", + "file": "aircraft/k35r.glb", + "min": [ + -20.765, + -19.541, + -2.478 + ], + "max": [ + 28.186, + 19.541, + 8.482 + ], + "length": 48.951, + "beam": 39.082, + "clearance": 2.478, + "tris": 2626, + "icon": "jet" + }, + "light_rail": { + "category": "transit", + "file": "transit/light_rail.glb", + "min": [ + -14.1, + -1.405, + -0.0 + ], + "max": [ + 14.1, + 1.405, + 5.641 + ], + "length": 28.2, + "beam": 2.81, + "clearance": 0.0, + "tris": 760, + "icon": "tram" + }, + "lng_carrier": { + "category": "ships", + "file": "ships/lng_carrier.glb", + "min": [ + -152.25, + -23.0, + -11.5 + ], + "max": [ + 145.0, + 23.0, + 45.38 + ], + "length": 297.25, + "beam": 46.0, + "clearance": 11.5, + "tris": 2951, + "icon": "tanker" + }, + "md11": { + "category": "aircraft", + "file": "aircraft/md11.glb", + "min": [ + -30.8, + -25.74, + -4.162 + ], + "max": [ + 33.345, + 25.74, + 10.88 + ], + "length": 64.145, + "beam": 51.48, + "clearance": 4.162, + "tris": 2476, + "icon": "widebody" + }, + "metro": { + "category": "transit", + "file": "transit/metro.glb", + "min": [ + -57.55, + -1.53, + -0.0 + ], + "max": [ + 57.55, + 1.53, + 3.691 + ], + "length": 115.1, + "beam": 3.06, + "clearance": 0.0, + "tris": 2226, + "icon": "subway" + }, + "monorail": { + "category": "transit", + "file": "transit/monorail.glb", + "min": [ + -27.75, + -1.53, + 0.0 + ], + "max": [ + 27.75, + 1.53, + 3.587 + ], + "length": 55.5, + "beam": 3.06, + "clearance": 0.0, + "tris": 814, + "icon": "subway" + }, + "motor_yacht": { + "category": "ships", + "file": "ships/motor_yacht.glb", + "min": [ + -9.92, + -2.3, + -1.15 + ], + "max": [ + 9.2, + 2.3, + 5.46 + ], + "length": 19.12, + "beam": 4.6, + "clearance": 1.15, + "tris": 2477, + "icon": "boat" + }, + "mq9": { + "category": "aircraft", + "file": "aircraft/mq9.glb", + "min": [ + -5.5, + -9.8, + -1.616 + ], + "max": [ + 5.85, + 9.8, + 1.582 + ], + "length": 11.35, + "beam": 19.6, + "clearance": 1.616, + "tris": 1936, + "icon": "light" + }, + "p28a": { + "category": "aircraft", + "file": "aircraft/p28a.glb", + "min": [ + -3.901, + -5.228, + -1.341 + ], + "max": [ + 3.877, + 5.228, + 1.801 + ], + "length": 7.778, + "beam": 10.457, + "clearance": 1.341, + "tris": 2136, + "icon": "light" + }, + "patrol": { + "category": "ships", + "file": "ships/patrol.glb", + "min": [ + -36.0, + -4.5, + -2.7 + ], + "max": [ + 30.0, + 4.5, + 19.6 + ], + "length": 66.0, + "beam": 9.0, + "clearance": 2.7, + "tris": 2587, + "icon": "ship" + }, + "pc12": { + "category": "aircraft", + "file": "aircraft/pc12.glb", + "min": [ + -7.62, + -8.053, + -1.435 + ], + "max": [ + 7.3, + 8.053, + 3.396 + ], + "length": 14.92, + "beam": 16.106, + "clearance": 1.435, + "tris": 2292, + "icon": "turboprop" + }, + "pile_green_cone": { + "category": "buoys", + "file": "buoys/pile_green_cone.glb", + "min": [ + -0.35, + -0.35, + -1.5 + ], + "max": [ + 0.35, + 0.35, + 4.72 + ], + "length": 0.7, + "beam": 0.7, + "clearance": 1.5, + "tris": 138, + "icon": "buoy" + }, + "pile_red_cylinder": { + "category": "buoys", + "file": "buoys/pile_red_cylinder.glb", + "min": [ + -0.35, + -0.35, + -1.5 + ], + "max": [ + 0.35, + 0.35, + 4.72 + ], + "length": 0.7, + "beam": 0.7, + "clearance": 1.5, + "tris": 168, + "icon": "buoy" + }, + "pilot_boat": { + "category": "ships", + "file": "ships/pilot_boat.glb", + "min": [ + -10.54, + -2.88, + -1.3 + ], + "max": [ + 8.5, + 2.88, + 8.716 + ], + "length": 19.04, + "beam": 5.76, + "clearance": 1.3, + "tris": 2985, + "icon": "boat" + }, + "product_tanker": { + "category": "ships", + "file": "ships/product_tanker.glb", + "min": [ + -58.76, + -9.5, + -6.46 + ], + "max": [ + 56.5, + 9.5, + 25.672 + ], + "length": 115.26, + "beam": 19.0, + "clearance": 6.46, + "tris": 2995, + "icon": "tanker" + }, + "psv": { + "category": "ships", + "file": "ships/psv.glb", + "min": [ + -45.05, + -9.5, + -6.27 + ], + "max": [ + 42.5, + 9.5, + 24.119 + ], + "length": 87.55, + "beam": 19.0, + "clearance": 6.27, + "tris": 3059, + "icon": "ship" + }, + "pushback": { + "category": "other", + "file": "other/pushback.glb", + "min": [ + -3.0, + -1.45, + 0.024 + ], + "max": [ + 3.0, + 1.45, + 2.1 + ], + "length": 6.0, + "beam": 2.9, + "clearance": 0.0, + "tris": 176, + "icon": "vehicle" + }, + "r44": { + "category": "helicopters", + "file": "helicopters/r44.glb", + "min": [ + -7.116, + -0.744, + -1.14 + ], + "max": [ + 5.079, + 0.744, + 2.03 + ], + "length": 12.195, + "beam": 1.488, + "clearance": 1.14, + "tris": 1172, + "icon": "heli" + }, + "regional_dmu": { + "category": "transit", + "file": "transit/regional_dmu.glb", + "min": [ + -24.75, + -1.53, + -0.0 + ], + "max": [ + 24.75, + 1.53, + 4.36 + ], + "length": 49.5, + "beam": 3.06, + "clearance": 0.0, + "tris": 770, + "icon": "rail" + }, + "river_cruise": { + "category": "ships", + "file": "ships/river_cruise.glb", + "min": [ + -69.12, + -5.7, + -1.8 + ], + "max": [ + 67.5, + 5.7, + 9.9 + ], + "length": 136.62, + "beam": 11.4, + "clearance": 1.8, + "tris": 2675, + "icon": "passenger" + }, + "s76": { + "category": "helicopters", + "file": "helicopters/s76.glb", + "min": [ + -8.949, + -6.705, + -1.525 + ], + "max": [ + 7.428, + 6.705, + 2.776 + ], + "length": 16.377, + "beam": 13.41, + "clearance": 1.525, + "tris": 1356, + "icon": "heli" + }, + "sailboat": { + "category": "ships", + "file": "ships/sailboat.glb", + "min": [ + -7.8, + -2.05, + -2.695 + ], + "max": [ + 6.5, + 2.05, + 18.698 + ], + "length": 14.3, + "beam": 4.1, + "clearance": 2.695, + "tris": 2421, + "icon": "sail" + }, + "spar_lateral_port": { + "category": "buoys", + "file": "buoys/spar_lateral_port.glb", + "min": [ + -0.47, + -0.47, + -1.5 + ], + "max": [ + 0.47, + 0.47, + 3.36 + ], + "length": 0.94, + "beam": 0.94, + "clearance": 1.5, + "tris": 290, + "icon": "buoy" + }, + "spar_lateral_starboard": { + "category": "buoys", + "file": "buoys/spar_lateral_starboard.glb", + "min": [ + -0.47, + -0.47, + -1.5 + ], + "max": [ + 0.47, + 0.47, + 3.36 + ], + "length": 0.94, + "beam": 0.94, + "clearance": 1.5, + "tris": 262, + "icon": "buoy" + }, + "speedboat": { + "category": "ships", + "file": "ships/speedboat.glb", + "min": [ + -4.96, + -1.3, + -0.55 + ], + "max": [ + 4.45, + 1.3, + 2.03 + ], + "length": 9.41, + "beam": 2.6, + "clearance": 0.55, + "tris": 2369, + "icon": "boat" + }, + "spherical_green": { + "category": "buoys", + "file": "buoys/spherical_green.glb", + "min": [ + -0.75, + -0.739, + -0.4 + ], + "max": [ + 0.75, + 0.739, + 1.25 + ], + "length": 1.5, + "beam": 1.477, + "clearance": 0.4, + "tris": 344, + "icon": "buoy" + }, + "spherical_yellow": { + "category": "buoys", + "file": "buoys/spherical_yellow.glb", + "min": [ + -0.75, + -0.739, + -0.4 + ], + "max": [ + 0.75, + 0.739, + 1.25 + ], + "length": 1.5, + "beam": 1.477, + "clearance": 0.4, + "tris": 344, + "icon": "buoy" + }, + "spherical_yellow_x": { + "category": "buoys", + "file": "buoys/spherical_yellow_x.glb", + "min": [ + -0.75, + -0.739, + -0.4 + ], + "max": [ + 0.75, + 0.739, + 2.019 + ], + "length": 1.5, + "beam": 1.477, + "clearance": 0.4, + "tris": 380, + "icon": "buoy" + }, + "sr22": { + "category": "aircraft", + "file": "aircraft/sr22.glb", + "min": [ + -4.3, + -5.723, + -1.325 + ], + "max": [ + 4.124, + 5.723, + 1.765 + ], + "length": 8.424, + "beam": 11.446, + "clearance": 1.325, + "tris": 2428, + "icon": "light" + }, + "stake_green_cone": { + "category": "buoys", + "file": "buoys/stake_green_cone.glb", + "min": [ + -0.198, + -0.198, + -1.5 + ], + "max": [ + 0.198, + 0.198, + 3.63 + ], + "length": 0.396, + "beam": 0.396, + "clearance": 1.5, + "tris": 78, + "icon": "buoy" + }, + "stake_red_cylinder": { + "category": "buoys", + "file": "buoys/stake_red_cylinder.glb", + "min": [ + -0.154, + -0.154, + -1.5 + ], + "max": [ + 0.154, + 0.154, + 3.63 + ], + "length": 0.308, + "beam": 0.308, + "clearance": 1.5, + "tris": 108, + "icon": "buoy" + }, + "stake_yellow_x": { + "category": "buoys", + "file": "buoys/stake_yellow_x.glb", + "min": [ + -0.08, + -0.232, + -1.5 + ], + "max": [ + 0.08, + 0.232, + 3.769 + ], + "length": 0.16, + "beam": 0.463, + "clearance": 1.5, + "tris": 64, + "icon": "buoy" + }, + "superyacht": { + "category": "ships", + "file": "ships/superyacht.glb", + "min": [ + -36.0, + -5.25, + -3.15 + ], + "max": [ + 30.0, + 5.25, + 17.565 + ], + "length": 66.0, + "beam": 10.5, + "clearance": 3.15, + "tris": 2689, + "icon": "passenger" + }, + "t38": { + "category": "aircraft", + "file": "aircraft/t38.glb", + "min": [ + -7.07, + -3.773, + -0.746 + ], + "max": [ + 7.074, + 3.773, + 2.592 + ], + "length": 14.144, + "beam": 7.546, + "clearance": 0.746, + "tris": 2246, + "icon": "fighter" + }, + "tanker": { + "category": "ships", + "file": "ships/tanker.glb", + "min": [ + -130.0, + -22.0, + -14.96 + ], + "max": [ + 125.0, + 22.0, + 35.608 + ], + "length": 255.0, + "beam": 44.0, + "clearance": 14.96, + "tris": 3393, + "icon": "tanker" + }, + "tex2": { + "category": "aircraft", + "file": "aircraft/tex2.glb", + "min": [ + -5.452, + -4.993, + -1.293 + ], + "max": [ + 5.134, + 4.993, + 2.168 + ], + "length": 10.586, + "beam": 9.986, + "clearance": 1.293, + "tris": 2380, + "icon": "turboprop" + }, + "tour_boat": { + "category": "ships", + "file": "ships/tour_boat.glb", + "min": [ + -14.5, + -3.0, + -1.68 + ], + "max": [ + 12.5, + 3.0, + 5.57 + ], + "length": 27.0, + "beam": 6.0, + "clearance": 1.68, + "tris": 2449, + "icon": "boat" + }, + "tower": { + "category": "other", + "file": "other/tower.glb", + "min": [ + -0.696, + -1.135, + -0.016 + ], + "max": [ + 1.296, + 1.135, + 60.6 + ], + "length": 1.992, + "beam": 2.27, + "clearance": 0.016, + "tris": 568, + "icon": "vehicle" + }, + "tram": { + "category": "transit", + "file": "transit/tram.glb", + "min": [ + -15.0, + -1.28, + -0.0 + ], + "max": [ + 15.0, + 1.28, + 5.241 + ], + "length": 30.0, + "beam": 2.56, + "clearance": 0.0, + "tris": 1004, + "icon": "tram" + }, + "trawler": { + "category": "ships", + "file": "ships/trawler.glb", + "min": [ + -12.1, + -6.338, + -2.8 + ], + "max": [ + 11.0, + 6.338, + 12.529 + ], + "length": 23.1, + "beam": 12.676, + "clearance": 2.8, + "tris": 3121, + "icon": "ship" + }, + "trolleybus": { + "category": "transit", + "file": "transit/trolleybus.glb", + "min": [ + -6.05, + -1.32, + -0.0 + ], + "max": [ + 6.05, + 1.355, + 5.656 + ], + "length": 12.1, + "beam": 2.675, + "clearance": 0.0, + "tris": 458, + "icon": "bus" + }, + "tug": { + "category": "ships", + "file": "ships/tug.glb", + "min": [ + -15.8, + -5.35, + -3.7 + ], + "max": [ + 14.4, + 5.35, + 14.1 + ], + "length": 30.2, + "beam": 10.7, + "clearance": 3.7, + "tris": 3661, + "icon": "ship" + }, + "ultralight": { + "category": "aircraft", + "file": "aircraft/ultralight.glb", + "min": [ + -3.51, + -5.243, + -1.217 + ], + "max": [ + 3.6, + 5.243, + 1.644 + ], + "length": 7.11, + "beam": 10.486, + "clearance": 1.217, + "tris": 2068, + "icon": "light" + }, + "v22": { + "category": "aircraft", + "file": "aircraft/v22.glb", + "min": [ + -8.74, + -11.647, + -1.468 + ], + "max": [ + 8.74, + 11.647, + 4.569 + ], + "length": 17.48, + "beam": 23.294, + "clearance": 1.468, + "tris": 2250, + "icon": "turboprop" + }, + "van": { + "category": "other", + "file": "other/van.glb", + "min": [ + -2.76, + -1.0, + 0.019 + ], + "max": [ + 2.75, + 1.0, + 2.438 + ], + "length": 5.51, + "beam": 2.0, + "clearance": 0.0, + "tris": 206, + "icon": "vehicle" + } +} \ No newline at end of file diff --git a/public/models/fleet/helicopters/a139.glb b/public/models/fleet/helicopters/a139.glb new file mode 100644 index 000000000..f61975dfb Binary files /dev/null and b/public/models/fleet/helicopters/a139.glb differ diff --git a/public/models/fleet/helicopters/as50.glb b/public/models/fleet/helicopters/as50.glb new file mode 100644 index 000000000..597d916e9 Binary files /dev/null and b/public/models/fleet/helicopters/as50.glb differ diff --git a/public/models/fleet/helicopters/b06.glb b/public/models/fleet/helicopters/b06.glb new file mode 100644 index 000000000..25baa7fa4 Binary files /dev/null and b/public/models/fleet/helicopters/b06.glb differ diff --git a/public/models/fleet/helicopters/b407.glb b/public/models/fleet/helicopters/b407.glb new file mode 100644 index 000000000..09d67e67e Binary files /dev/null and b/public/models/fleet/helicopters/b407.glb differ diff --git a/public/models/fleet/helicopters/ec35.glb b/public/models/fleet/helicopters/ec35.glb new file mode 100644 index 000000000..c52f3d156 Binary files /dev/null and b/public/models/fleet/helicopters/ec35.glb differ diff --git a/public/models/fleet/helicopters/ec45.glb b/public/models/fleet/helicopters/ec45.glb new file mode 100644 index 000000000..b9e5cb27e Binary files /dev/null and b/public/models/fleet/helicopters/ec45.glb differ diff --git a/public/models/fleet/helicopters/generic_heli.glb b/public/models/fleet/helicopters/generic_heli.glb new file mode 100644 index 000000000..bc8f1a239 Binary files /dev/null and b/public/models/fleet/helicopters/generic_heli.glb differ diff --git a/public/models/fleet/helicopters/h47.glb b/public/models/fleet/helicopters/h47.glb new file mode 100644 index 000000000..bb2f71426 Binary files /dev/null and b/public/models/fleet/helicopters/h47.glb differ diff --git a/public/models/fleet/helicopters/h60.glb b/public/models/fleet/helicopters/h60.glb new file mode 100644 index 000000000..45f79180d Binary files /dev/null and b/public/models/fleet/helicopters/h60.glb differ diff --git a/public/models/fleet/helicopters/h64.glb b/public/models/fleet/helicopters/h64.glb new file mode 100644 index 000000000..3a44d8e50 Binary files /dev/null and b/public/models/fleet/helicopters/h64.glb differ diff --git a/public/models/fleet/helicopters/r44.glb b/public/models/fleet/helicopters/r44.glb new file mode 100644 index 000000000..845727936 Binary files /dev/null and b/public/models/fleet/helicopters/r44.glb differ diff --git a/public/models/fleet/helicopters/s76.glb b/public/models/fleet/helicopters/s76.glb new file mode 100644 index 000000000..a773aba73 Binary files /dev/null and b/public/models/fleet/helicopters/s76.glb differ diff --git a/public/models/fleet/other/balloon.glb b/public/models/fleet/other/balloon.glb new file mode 100644 index 000000000..016ad0719 Binary files /dev/null and b/public/models/fleet/other/balloon.glb differ diff --git a/public/models/fleet/other/blimp.glb b/public/models/fleet/other/blimp.glb new file mode 100644 index 000000000..3aba8e86b Binary files /dev/null and b/public/models/fleet/other/blimp.glb differ diff --git a/public/models/fleet/other/car.glb b/public/models/fleet/other/car.glb new file mode 100644 index 000000000..f8a2f6f8f Binary files /dev/null and b/public/models/fleet/other/car.glb differ diff --git a/public/models/fleet/other/fire_truck.glb b/public/models/fleet/other/fire_truck.glb new file mode 100644 index 000000000..a07a2e0ae Binary files /dev/null and b/public/models/fleet/other/fire_truck.glb differ diff --git a/public/models/fleet/other/pushback.glb b/public/models/fleet/other/pushback.glb new file mode 100644 index 000000000..9d04c17f3 Binary files /dev/null and b/public/models/fleet/other/pushback.glb differ diff --git a/public/models/fleet/other/tower.glb b/public/models/fleet/other/tower.glb new file mode 100644 index 000000000..39ff5c990 Binary files /dev/null and b/public/models/fleet/other/tower.glb differ diff --git a/public/models/fleet/other/van.glb b/public/models/fleet/other/van.glb new file mode 100644 index 000000000..0cf6ebaac Binary files /dev/null and b/public/models/fleet/other/van.glb differ diff --git a/public/models/fleet/ships/bulk_carrier.glb b/public/models/fleet/ships/bulk_carrier.glb new file mode 100644 index 000000000..3fe73d3bf Binary files /dev/null and b/public/models/fleet/ships/bulk_carrier.glb differ diff --git a/public/models/fleet/ships/car_carrier.glb b/public/models/fleet/ships/car_carrier.glb new file mode 100644 index 000000000..04ad3e779 Binary files /dev/null and b/public/models/fleet/ships/car_carrier.glb differ diff --git a/public/models/fleet/ships/container_ship.glb b/public/models/fleet/ships/container_ship.glb new file mode 100644 index 000000000..f8686176b Binary files /dev/null and b/public/models/fleet/ships/container_ship.glb differ diff --git a/public/models/fleet/ships/cruise_ship.glb b/public/models/fleet/ships/cruise_ship.glb new file mode 100644 index 000000000..d2dcfd88e Binary files /dev/null and b/public/models/fleet/ships/cruise_ship.glb differ diff --git a/public/models/fleet/ships/dredger.glb b/public/models/fleet/ships/dredger.glb new file mode 100644 index 000000000..4d9ab5264 Binary files /dev/null and b/public/models/fleet/ships/dredger.glb differ diff --git a/public/models/fleet/ships/fast_ferry.glb b/public/models/fleet/ships/fast_ferry.glb new file mode 100644 index 000000000..65b008200 Binary files /dev/null and b/public/models/fleet/ships/fast_ferry.glb differ diff --git a/public/models/fleet/ships/feeder.glb b/public/models/fleet/ships/feeder.glb new file mode 100644 index 000000000..1c40203bf Binary files /dev/null and b/public/models/fleet/ships/feeder.glb differ diff --git a/public/models/fleet/ships/ferry.glb b/public/models/fleet/ships/ferry.glb new file mode 100644 index 000000000..fcaa48abc Binary files /dev/null and b/public/models/fleet/ships/ferry.glb differ diff --git a/public/models/fleet/ships/fishing_boat.glb b/public/models/fleet/ships/fishing_boat.glb new file mode 100644 index 000000000..084f5dd3b Binary files /dev/null and b/public/models/fleet/ships/fishing_boat.glb differ diff --git a/public/models/fleet/ships/general_cargo.glb b/public/models/fleet/ships/general_cargo.glb new file mode 100644 index 000000000..0cd1cd4cd Binary files /dev/null and b/public/models/fleet/ships/general_cargo.glb differ diff --git a/public/models/fleet/ships/generic_vessel.glb b/public/models/fleet/ships/generic_vessel.glb new file mode 100644 index 000000000..e8c748544 Binary files /dev/null and b/public/models/fleet/ships/generic_vessel.glb differ diff --git a/public/models/fleet/ships/inland_cargo.glb b/public/models/fleet/ships/inland_cargo.glb new file mode 100644 index 000000000..9f9facc4a Binary files /dev/null and b/public/models/fleet/ships/inland_cargo.glb differ diff --git a/public/models/fleet/ships/inland_tanker.glb b/public/models/fleet/ships/inland_tanker.glb new file mode 100644 index 000000000..db46c2476 Binary files /dev/null and b/public/models/fleet/ships/inland_tanker.glb differ diff --git a/public/models/fleet/ships/lng_carrier.glb b/public/models/fleet/ships/lng_carrier.glb new file mode 100644 index 000000000..d06330645 Binary files /dev/null and b/public/models/fleet/ships/lng_carrier.glb differ diff --git a/public/models/fleet/ships/motor_yacht.glb b/public/models/fleet/ships/motor_yacht.glb new file mode 100644 index 000000000..d7283750a Binary files /dev/null and b/public/models/fleet/ships/motor_yacht.glb differ diff --git a/public/models/fleet/ships/patrol.glb b/public/models/fleet/ships/patrol.glb new file mode 100644 index 000000000..6b3352e11 Binary files /dev/null and b/public/models/fleet/ships/patrol.glb differ diff --git a/public/models/fleet/ships/pilot_boat.glb b/public/models/fleet/ships/pilot_boat.glb new file mode 100644 index 000000000..f4197ab0f Binary files /dev/null and b/public/models/fleet/ships/pilot_boat.glb differ diff --git a/public/models/fleet/ships/product_tanker.glb b/public/models/fleet/ships/product_tanker.glb new file mode 100644 index 000000000..6e31083d3 Binary files /dev/null and b/public/models/fleet/ships/product_tanker.glb differ diff --git a/public/models/fleet/ships/psv.glb b/public/models/fleet/ships/psv.glb new file mode 100644 index 000000000..d943978ec Binary files /dev/null and b/public/models/fleet/ships/psv.glb differ diff --git a/public/models/fleet/ships/river_cruise.glb b/public/models/fleet/ships/river_cruise.glb new file mode 100644 index 000000000..2da8589f0 Binary files /dev/null and b/public/models/fleet/ships/river_cruise.glb differ diff --git a/public/models/fleet/ships/sailboat.glb b/public/models/fleet/ships/sailboat.glb new file mode 100644 index 000000000..51ae8818f Binary files /dev/null and b/public/models/fleet/ships/sailboat.glb differ diff --git a/public/models/fleet/ships/speedboat.glb b/public/models/fleet/ships/speedboat.glb new file mode 100644 index 000000000..eda10fb84 Binary files /dev/null and b/public/models/fleet/ships/speedboat.glb differ diff --git a/public/models/fleet/ships/superyacht.glb b/public/models/fleet/ships/superyacht.glb new file mode 100644 index 000000000..012e69dd3 Binary files /dev/null and b/public/models/fleet/ships/superyacht.glb differ diff --git a/public/models/fleet/ships/tanker.glb b/public/models/fleet/ships/tanker.glb new file mode 100644 index 000000000..68de5a001 Binary files /dev/null and b/public/models/fleet/ships/tanker.glb differ diff --git a/public/models/fleet/ships/tour_boat.glb b/public/models/fleet/ships/tour_boat.glb new file mode 100644 index 000000000..22ee9cb43 Binary files /dev/null and b/public/models/fleet/ships/tour_boat.glb differ diff --git a/public/models/fleet/ships/trawler.glb b/public/models/fleet/ships/trawler.glb new file mode 100644 index 000000000..bb8651f2c Binary files /dev/null and b/public/models/fleet/ships/trawler.glb differ diff --git a/public/models/fleet/ships/tug.glb b/public/models/fleet/ships/tug.glb new file mode 100644 index 000000000..f20e72b6b Binary files /dev/null and b/public/models/fleet/ships/tug.glb differ diff --git a/public/models/fleet/transit/articulated_bus.glb b/public/models/fleet/transit/articulated_bus.glb new file mode 100644 index 000000000..4b10d2ef0 Binary files /dev/null and b/public/models/fleet/transit/articulated_bus.glb differ diff --git a/public/models/fleet/transit/cable_tram.glb b/public/models/fleet/transit/cable_tram.glb new file mode 100644 index 000000000..10b12a3ee Binary files /dev/null and b/public/models/fleet/transit/cable_tram.glb differ diff --git a/public/models/fleet/transit/city_bus.glb b/public/models/fleet/transit/city_bus.glb new file mode 100644 index 000000000..4e2b1dc8a Binary files /dev/null and b/public/models/fleet/transit/city_bus.glb differ diff --git a/public/models/fleet/transit/coach.glb b/public/models/fleet/transit/coach.glb new file mode 100644 index 000000000..00e23f7f5 Binary files /dev/null and b/public/models/fleet/transit/coach.glb differ diff --git a/public/models/fleet/transit/commuter_emu.glb b/public/models/fleet/transit/commuter_emu.glb new file mode 100644 index 000000000..e53f0a1be Binary files /dev/null and b/public/models/fleet/transit/commuter_emu.glb differ diff --git a/public/models/fleet/transit/double_decker.glb b/public/models/fleet/transit/double_decker.glb new file mode 100644 index 000000000..416bd2cbc Binary files /dev/null and b/public/models/fleet/transit/double_decker.glb differ diff --git a/public/models/fleet/transit/funicular.glb b/public/models/fleet/transit/funicular.glb new file mode 100644 index 000000000..4b14d6622 Binary files /dev/null and b/public/models/fleet/transit/funicular.glb differ diff --git a/public/models/fleet/transit/gondola.glb b/public/models/fleet/transit/gondola.glb new file mode 100644 index 000000000..530ccb448 Binary files /dev/null and b/public/models/fleet/transit/gondola.glb differ diff --git a/public/models/fleet/transit/high_speed.glb b/public/models/fleet/transit/high_speed.glb new file mode 100644 index 000000000..c3f09d094 Binary files /dev/null and b/public/models/fleet/transit/high_speed.glb differ diff --git a/public/models/fleet/transit/intercity.glb b/public/models/fleet/transit/intercity.glb new file mode 100644 index 000000000..2845594ec Binary files /dev/null and b/public/models/fleet/transit/intercity.glb differ diff --git a/public/models/fleet/transit/light_rail.glb b/public/models/fleet/transit/light_rail.glb new file mode 100644 index 000000000..22f80d197 Binary files /dev/null and b/public/models/fleet/transit/light_rail.glb differ diff --git a/public/models/fleet/transit/metro.glb b/public/models/fleet/transit/metro.glb new file mode 100644 index 000000000..6154049b9 Binary files /dev/null and b/public/models/fleet/transit/metro.glb differ diff --git a/public/models/fleet/transit/monorail.glb b/public/models/fleet/transit/monorail.glb new file mode 100644 index 000000000..a8dab0f07 Binary files /dev/null and b/public/models/fleet/transit/monorail.glb differ diff --git a/public/models/fleet/transit/regional_dmu.glb b/public/models/fleet/transit/regional_dmu.glb new file mode 100644 index 000000000..1a091e2d6 Binary files /dev/null and b/public/models/fleet/transit/regional_dmu.glb differ diff --git a/public/models/fleet/transit/tram.glb b/public/models/fleet/transit/tram.glb new file mode 100644 index 000000000..514cff436 Binary files /dev/null and b/public/models/fleet/transit/tram.glb differ diff --git a/public/models/fleet/transit/trolleybus.glb b/public/models/fleet/transit/trolleybus.glb new file mode 100644 index 000000000..a7691b71e Binary files /dev/null and b/public/models/fleet/transit/trolleybus.glb differ diff --git a/public/models/fleet/types.json b/public/models/fleet/types.json new file mode 100644 index 000000000..2390bcae4 --- /dev/null +++ b/public/models/fleet/types.json @@ -0,0 +1,1997 @@ +{ + "aircraft": { + "A320": [ + "a320", + 37.57 + ], + "A20N": [ + "a320", + 37.57 + ], + "A319": [ + "a320", + 33.84 + ], + "A19N": [ + "a320", + 33.84 + ], + "A321": [ + "a320", + 44.51 + ], + "A21N": [ + "a320", + 44.51 + ], + "A318": [ + "a320", + 31.44 + ], + "B738": [ + "b738", + 39.47 + ], + "B38M": [ + "b738", + 39.52 + ], + "B737": [ + "b738", + 33.63 + ], + "B37M": [ + "b738", + 35.56 + ], + "B739": [ + "b738", + 42.11 + ], + "B39M": [ + "b738", + 42.16 + ], + "B736": [ + "b738", + 31.24 + ], + "B733": [ + "b738", + 33.4 + ], + "B734": [ + "b738", + 36.4 + ], + "B735": [ + "b738", + 31.0 + ], + "B752": [ + "b738", + 47.32 + ], + "B753": [ + "b738", + 54.4 + ], + "P8": [ + "b738", + 39.47 + ], + "BCS3": [ + "bcs3", + 38.7 + ], + "BCS1": [ + "bcs3", + 35.0 + ], + "E75L": [ + "e175", + 31.68 + ], + "E75S": [ + "e175", + 31.68 + ], + "E170": [ + "e175", + 29.9 + ], + "E190": [ + "e175", + 36.24 + ], + "E195": [ + "e175", + 38.65 + ], + "E290": [ + "e175", + 36.2 + ], + "E295": [ + "e175", + 41.5 + ], + "CRJ9": [ + "crj9", + 36.2 + ], + "CRJ7": [ + "crj9", + 32.5 + ], + "CRJ2": [ + "crj9", + 26.77 + ], + "CRJX": [ + "crj9", + 39.1 + ], + "E145": [ + "crj9", + 29.87 + ], + "E135": [ + "crj9", + 26.33 + ], + "E35L": [ + "crj9", + 26.33 + ], + "B789": [ + "b789", + 62.8 + ], + "B788": [ + "b789", + 56.7 + ], + "B78X": [ + "b789", + 68.3 + ], + "A359": [ + "a359", + 66.8 + ], + "A35K": [ + "a359", + 73.8 + ], + "B77W": [ + "b77w", + 73.9 + ], + "B77L": [ + "b77w", + 63.7 + ], + "B772": [ + "b77w", + 63.7 + ], + "B773": [ + "b77w", + 73.9 + ], + "B778": [ + "b77w", + 70.9 + ], + "B779": [ + "b77w", + 76.7 + ], + "B763": [ + "b763", + 54.9 + ], + "B762": [ + "b763", + 48.5 + ], + "B764": [ + "b763", + 61.4 + ], + "A333": [ + "a333", + 63.69 + ], + "A332": [ + "a333", + 58.82 + ], + "A339": [ + "a333", + 63.66 + ], + "A338": [ + "a333", + 58.82 + ], + "DH8D": [ + "dh8d", + 32.8 + ], + "DH8C": [ + "dh8d", + 25.7 + ], + "DH8A": [ + "dh8d", + 22.25 + ], + "DH8B": [ + "dh8d", + 22.25 + ], + "AT72": [ + "at72", + 27.17 + ], + "AT76": [ + "at72", + 27.17 + ], + "AT75": [ + "at72", + 27.17 + ], + "AT45": [ + "at72", + 22.67 + ], + "AT43": [ + "at72", + 22.67 + ], + "AT46": [ + "at72", + 22.67 + ], + "E55P": [ + "e55p", + 15.9 + ], + "E545": [ + "e55p", + 19.7 + ], + "E550": [ + "e55p", + 20.7 + ], + "E50P": [ + "e55p", + 12.8 + ], + "LJ45": [ + "e55p", + 17.7 + ], + "LJ75": [ + "e55p", + 17.7 + ], + "LJ35": [ + "e55p", + 14.83 + ], + "LJ60": [ + "e55p", + 17.9 + ], + "C25B": [ + "e55p", + 15.6 + ], + "C25A": [ + "e55p", + 14.5 + ], + "C525": [ + "e55p", + 13.0 + ], + "C25C": [ + "e55p", + 16.3 + ], + "C680": [ + "e55p", + 19.4 + ], + "C68A": [ + "e55p", + 19.7 + ], + "C700": [ + "e55p", + 22.3 + ], + "HDJT": [ + "e55p", + 13.0 + ], + "PC24": [ + "e55p", + 16.85 + ], + "C56X": [ + "c56x", + 15.79 + ], + "C560": [ + "c56x", + 14.9 + ], + "C550": [ + "c56x", + 14.4 + ], + "C510": [ + "c56x", + 12.4 + ], + "C750": [ + "c56x", + 22.0 + ], + "C501": [ + "c56x", + 13.3 + ], + "CL35": [ + "cl35", + 20.92 + ], + "CL30": [ + "cl35", + 20.92 + ], + "CL60": [ + "cl35", + 20.85 + ], + "F2TH": [ + "cl35", + 20.2 + ], + "FA50": [ + "cl35", + 18.5 + ], + "F900": [ + "cl35", + 20.2 + ], + "FA7X": [ + "cl35", + 23.2 + ], + "FA8X": [ + "cl35", + 24.5 + ], + "G280": [ + "cl35", + 20.3 + ], + "H25B": [ + "cl35", + 15.6 + ], + "GLF4": [ + "glf4", + 26.92 + ], + "GLF5": [ + "glf4", + 29.4 + ], + "GLF6": [ + "glf4", + 30.4 + ], + "GLEX": [ + "glf4", + 30.3 + ], + "GL5T": [ + "glf4", + 29.5 + ], + "GL7T": [ + "glf4", + 33.8 + ], + "GA6C": [ + "glf4", + 30.4 + ], + "C172": [ + "c172", + 8.28 + ], + "C182": [ + "c172", + 8.84 + ], + "C152": [ + "c172", + 7.34 + ], + "C150": [ + "c172", + 7.29 + ], + "C72R": [ + "c172", + 8.28 + ], + "C206": [ + "c172", + 8.61 + ], + "C210": [ + "c172", + 8.59 + ], + "C208": [ + "c172", + 11.46 + ], + "C170": [ + "c172", + 7.6 + ], + "C180": [ + "c172", + 7.8 + ], + "C177": [ + "c172", + 8.4 + ], + "P28A": [ + "p28a", + 7.25 + ], + "P28R": [ + "p28a", + 7.5 + ], + "P28B": [ + "p28a", + 7.5 + ], + "PA46": [ + "p28a", + 8.8 + ], + "DA40": [ + "p28a", + 8.06 + ], + "BE36": [ + "p28a", + 8.38 + ], + "BE35": [ + "p28a", + 8.0 + ], + "M20P": [ + "p28a", + 7.52 + ], + "M20T": [ + "p28a", + 8.15 + ], + "P32R": [ + "p28a", + 8.4 + ], + "BE33": [ + "p28a", + 7.7 + ], + "RV7": [ + "p28a", + 6.2 + ], + "RV8": [ + "p28a", + 6.3 + ], + "GLAS": [ + "p28a", + 6.0 + ], + "PA18": [ + "p28a", + 6.9 + ], + "SR22": [ + "sr22", + 7.92 + ], + "SR20": [ + "sr22", + 7.92 + ], + "S22T": [ + "sr22", + 7.92 + ], + "COL4": [ + "sr22", + 7.7 + ], + "DA20": [ + "sr22", + 7.2 + ], + "PC12": [ + "pc12", + 14.4 + ], + "TBM7": [ + "pc12", + 10.7 + ], + "TBM8": [ + "pc12", + 10.7 + ], + "TBM9": [ + "pc12", + 10.7 + ], + "P46T": [ + "pc12", + 9.0 + ], + "BE20": [ + "be20", + 13.34 + ], + "BE9L": [ + "be20", + 10.82 + ], + "B350": [ + "be20", + 14.22 + ], + "BE30": [ + "be20", + 13.34 + ], + "PA31": [ + "be20", + 10.55 + ], + "BE58": [ + "be20", + 9.09 + ], + "PA44": [ + "be20", + 8.41 + ], + "DA42": [ + "be20", + 8.56 + ], + "PA34": [ + "be20", + 8.72 + ], + "C414": [ + "be20", + 11.1 + ], + "C421": [ + "be20", + 11.1 + ], + "C310": [ + "be20", + 9.7 + ], + "BE55": [ + "be20", + 8.5 + ], + "P180": [ + "be20", + 14.4 + ], + "TEX2": [ + "tex2", + 10.16 + ], + "PC21": [ + "tex2", + 11.2 + ], + "PC9": [ + "tex2", + 10.1 + ], + "PC7": [ + "tex2", + 9.8 + ], + "T6": [ + "tex2", + 8.8 + ], + "T38": [ + "t38", + 14.14 + ], + "HAWK": [ + "t38", + 12.43 + ], + "F16": [ + "t38", + 15.06 + ], + "F18": [ + "t38", + 17.1 + ], + "F18S": [ + "t38", + 18.31 + ], + "F35": [ + "t38", + 15.7 + ], + "F15": [ + "t38", + 19.43 + ], + "EUFI": [ + "t38", + 15.96 + ], + "F22": [ + "t38", + 18.9 + ], + "T45": [ + "t38", + 11.99 + ], + "M346": [ + "t38", + 11.49 + ], + "C30J": [ + "c30j", + 34.36 + ], + "C130": [ + "c30j", + 29.79 + ], + "A400": [ + "c30j", + 45.1 + ], + "C17": [ + "c17", + 53.04 + ], + "K35R": [ + "k35r", + 41.53 + ], + "E3TF": [ + "k35r", + 46.61 + ], + "E3CF": [ + "k35r", + 46.61 + ], + "B744": [ + "b744", + 70.66 + ], + "B748": [ + "b744", + 76.3 + ], + "B742": [ + "b744", + 70.66 + ], + "B74S": [ + "b744", + 56.3 + ], + "A388": [ + "a388", + 72.72 + ], + "B712": [ + "crj9", + 37.8 + ], + "MD82": [ + "crj9", + 45.0 + ], + "MD83": [ + "crj9", + 45.0 + ], + "MD88": [ + "crj9", + 45.0 + ], + "MD87": [ + "crj9", + 39.75 + ], + "A306": [ + "a333", + 54.08 + ], + "A310": [ + "a333", + 46.66 + ], + "T206": [ + "c172", + 8.61 + ], + "C82T": [ + "c172", + 8.84 + ], + "C82S": [ + "c172", + 8.84 + ], + "C82R": [ + "c172", + 8.84 + ], + "DV20": [ + "sr22", + 7.3 + ], + "PA32": [ + "p28a", + 8.2 + ], + "PA24": [ + "p28a", + 7.6 + ], + "AA5": [ + "p28a", + 6.7 + ], + "RV12": [ + "p28a", + 6.2 + ], + "EPIC": [ + "pc12", + 10.8 + ], + "C402": [ + "be20", + 11.1 + ], + "BE95": [ + "be20", + 7.8 + ], + "PA30": [ + "be20", + 7.7 + ], + "SF34": [ + "be20", + 19.73 + ], + "BE40": [ + "e55p", + 14.8 + ], + "GALX": [ + "cl35", + 18.97 + ], + "GA5C": [ + "p28a", + 6.7 + ], + "MD11": [ + "md11", + 61.6 + ], + "DC10": [ + "md11", + 55.5 + ], + "L101": [ + "md11", + 54.2 + ], + "A343": [ + "a343", + 63.69 + ], + "A346": [ + "a343", + 75.36 + ], + "A342": [ + "a343", + 59.4 + ], + "A345": [ + "a343", + 67.9 + ], + "GLID": [ + "glider", + 6.8 + ], + "GLIM": [ + "glider", + 7.0 + ], + "DRON": [ + "mq9", + 11.0 + ], + "Q9": [ + "mq9", + 11.0 + ], + "Q4": [ + "mq9", + 14.5 + ], + "Q1": [ + "mq9", + 8.2 + ], + "PIAT": [ + "ultralight", + 6.5 + ], + "PIVI": [ + "ultralight", + 6.5 + ], + "ULAC": [ + "ultralight", + 6.0 + ], + "E45X": [ + "crj9", + 29.87 + ], + "SB20": [ + "be20", + 27.28 + ], + "C500": [ + "c56x", + 13.26 + ], + "BE50": [ + "be20", + 9.6 + ], + "AC68": [ + "be20", + 13.1 + ], + "B190": [ + "be20", + 17.63 + ], + "FA6X": [ + "cl35", + 25.7 + ], + "FA20": [ + "cl35", + 17.15 + ], + "T204": [ + "b738", + 46.1 + ], + "GA7": [ + "be20", + 9.1 + ], + "C320": [ + "be20", + 10.0 + ], + "SLG4": [ + "glider", + 6.5 + ], + "SLG2": [ + "glider", + 6.5 + ], + "SF50": [ + "sr22", + 9.4 + ], + "T210": [ + "c172", + 8.6 + ], + "PA23": [ + "be20", + 8.8 + ], + "BE23": [ + "p28a", + 7.8 + ], + "RV6": [ + "p28a", + 6.1 + ], + "CH7B": [ + "ultralight", + 6.6 + ], + "BT7": [ + "tex2", + 9.6 + ], + "TB30": [ + "tex2", + 7.6 + ], + "IL62": [ + "md11", + 53.1 + ], + "BALL": [ + "balloon", + 17.0 + ], + "SHIP": [ + "blimp", + 75.0 + ] + }, + "helicopters": { + "R44": [ + "r44", + 9.07 + ], + "R22": [ + "r44", + 6.3 + ], + "R66": [ + "r44", + 9.2 + ], + "H500": [ + "r44", + 7.0 + ], + "H269": [ + "r44", + 6.8 + ], + "B06": [ + "b06", + 9.5 + ], + "B505": [ + "b06", + 9.6 + ], + "B206": [ + "b06", + 9.5 + ], + "B407": [ + "b407", + 10.6 + ], + "B212": [ + "b407", + 12.9 + ], + "B412": [ + "b407", + 12.9 + ], + "UH1": [ + "b407", + 12.8 + ], + "AS50": [ + "as50", + 10.93 + ], + "EC30": [ + "as50", + 10.68 + ], + "A119": [ + "as50", + 11.1 + ], + "AS55": [ + "as50", + 10.93 + ], + "EC35": [ + "ec35", + 10.2 + ], + "B429": [ + "ec35", + 11.3 + ], + "EC45": [ + "ec45", + 11.6 + ], + "B105": [ + "ec45", + 8.8 + ], + "A139": [ + "a139", + 13.77 + ], + "A169": [ + "a139", + 12.1 + ], + "A109": [ + "a139", + 11.4 + ], + "A189": [ + "a139", + 14.6 + ], + "S76": [ + "s76", + 13.2 + ], + "AS65": [ + "s76", + 11.6 + ], + "EC55": [ + "s76", + 12.1 + ], + "LYNX": [ + "s76", + 12.0 + ], + "EC75": [ + "s76", + 13.7 + ], + "H60": [ + "h60", + 15.26 + ], + "S92": [ + "h60", + 17.1 + ], + "EH10": [ + "h60", + 19.5 + ], + "NH90": [ + "h60", + 16.1 + ], + "H47": [ + "h47", + 15.46 + ], + "H64": [ + "h64", + 15.06 + ], + "H1": [ + "h64", + 13.6 + ], + "TIGR": [ + "h64", + 14.0 + ], + "V22": [ + "v22", + 17.48 + ], + "GYRO": [ + "r44", + 5.0 + ] + }, + "vessels": [ + { + "types": [ + 70, + 79 + ], + "rules": [ + { + "min_len": 70, + "max_len": 140, + "max_bl": 0.12, + "model": "inland_cargo" + }, + { + "name_re": "\\b(LEADER|HIGHWAY|ACE|GLOVIS|HOEGH|SPIRIT OF)\\b", + "min_len": 150, + "model": "car_carrier" + }, + { + "min_len": 250, + "model": "container_ship" + }, + { + "min_len": 160, + "max_bl": 0.145, + "model": "container_ship" + }, + { + "min_len": 160, + "model": "bulk_carrier" + }, + { + "min_len": 120, + "model": "feeder" + }, + { + "model": "general_cargo" + } + ] + }, + { + "types": [ + 80, + 89 + ], + "rules": [ + { + "name_re": "\\bLNG\\b|\\bGAS\\b", + "min_len": 150, + "model": "lng_carrier" + }, + { + "min_len": 70, + "max_len": 140, + "max_bl": 0.12, + "model": "inland_tanker" + }, + { + "min_len": 180, + "model": "tanker" + }, + { + "model": "product_tanker" + } + ] + }, + { + "types": [ + 60, + 69 + ], + "rules": [ + { + "min_len": 70, + "max_len": 140, + "max_bl": 0.12, + "model": "river_cruise" + }, + { + "min_len": 200, + "model": "cruise_ship" + }, + { + "min_len": 40, + "model": "ferry" + }, + { + "min_len": 15, + "model": "tour_boat" + }, + { + "model": "speedboat" + } + ] + }, + { + "types": [ + 37, + 37 + ], + "rules": [ + { + "min_len": 35, + "model": "superyacht" + }, + { + "min_len": 12, + "model": "motor_yacht" + }, + { + "model": "speedboat" + } + ] + }, + { + "types": [ + 36, + 36 + ], + "rules": [ + { + "model": "sailboat" + } + ] + }, + { + "types": [ + 30, + 30 + ], + "rules": [ + { + "min_len": 15, + "model": "trawler" + }, + { + "model": "fishing_boat" + } + ] + }, + { + "types": [ + 31, + 32 + ], + "rules": [ + { + "model": "tug" + } + ] + }, + { + "types": [ + 52, + 52 + ], + "rules": [ + { + "model": "tug" + } + ] + }, + { + "types": [ + 50, + 51 + ], + "rules": [ + { + "model": "pilot_boat" + } + ] + }, + { + "types": [ + 53, + 54 + ], + "rules": [ + { + "model": "pilot_boat" + } + ] + }, + { + "types": [ + 35, + 35 + ], + "rules": [ + { + "model": "patrol" + } + ] + }, + { + "types": [ + 55, + 55 + ], + "rules": [ + { + "min_len": 25, + "model": "patrol" + }, + { + "model": "pilot_boat" + } + ] + }, + { + "types": [ + 40, + 49 + ], + "rules": [ + { + "min_len": 20, + "model": "fast_ferry" + }, + { + "model": "speedboat" + } + ] + }, + { + "types": [ + 33, + 33 + ], + "rules": [ + { + "min_len": 50, + "model": "dredger" + }, + { + "model": "tug" + } + ] + }, + { + "types": [ + 0, + 99 + ], + "rules": [ + { + "min_len": 70, + "max_len": 140, + "max_bl": 0.12, + "model": "inland_cargo" + }, + { + "model": "generic_vessel" + } + ] + } + ], + "categories": { + "A1": [ + "c172", + 8.3 + ], + "A2": [ + "be20", + 14.0 + ], + "A3": [ + "a320", + 37.6 + ], + "A4": [ + "b738", + 47.3 + ], + "A5": [ + "b77w", + 70.0 + ], + "A6": [ + "t38", + 15.0 + ], + "A7": [ + "generic_heli", + 11.0 + ], + "B1": [ + "glider", + 6.8 + ], + "B2": [ + "balloon", + 17.0 + ], + "B3": [ + "ultralight", + 5.0 + ], + "B4": [ + "ultralight", + 6.5 + ], + "B6": [ + "mq9", + 11.0 + ], + "B7": [ + "ultralight", + 4.0 + ], + "C1": [ + "fire_truck", + 11.0 + ], + "C2": [ + "van", + 5.5 + ], + "C0": [ + "tower", + 60.0 + ], + "C3": [ + "tower", + 60.0 + ], + "C4": [ + "tower", + 60.0 + ], + "C5": [ + "tower", + 60.0 + ], + "A0": [ + "generic_plane", + 36.0 + ] + }, + "default_aircraft": [ + "generic_plane", + 36.0 + ], + "default_vessel": "generic_vessel", + "transit": { + "modes": { + "bus": [ + "city_bus", + 12.0 + ], + "articulated_bus": [ + "articulated_bus", + 18.0 + ], + "double_decker": [ + "double_decker", + 11.0 + ], + "trolleybus": [ + "trolleybus", + 12.0 + ], + "coach": [ + "coach", + 13.0 + ], + "tram": [ + "tram", + 30.0 + ], + "light_rail": [ + "light_rail", + 28.2 + ], + "cable_tram": [ + "cable_tram", + 8.4 + ], + "subway": [ + "metro", + 115.0 + ], + "metro": [ + "metro", + 115.0 + ], + "monorail": [ + "monorail", + 55.5 + ], + "rail": [ + "commuter_emu", + 98.1 + ], + "commuter": [ + "commuter_emu", + 98.1 + ], + "regional": [ + "regional_dmu", + 49.5 + ], + "intercity": [ + "intercity", + 182.8 + ], + "high_speed": [ + "high_speed", + 206.2 + ], + "ferry": [ + "fast_ferry", + 40.0 + ], + "aerial_lift": [ + "gondola", + 2.1 + ], + "gondola": [ + "gondola", + 2.1 + ], + "funicular": [ + "funicular", + 14.0 + ] + }, + "route_types": { + "0": [ + "tram", + 30.0 + ], + "1": [ + "metro", + 115.0 + ], + "2": [ + "commuter_emu", + 98.1 + ], + "3": [ + "city_bus", + 12.0 + ], + "4": [ + "fast_ferry", + 40.0 + ], + "5": [ + "cable_tram", + 8.4 + ], + "6": [ + "gondola", + 2.1 + ], + "7": [ + "funicular", + 14.0 + ], + "11": [ + "trolleybus", + 12.0 + ], + "12": [ + "monorail", + 55.5 + ], + "100": [ + "intercity", + 182.8 + ], + "101": [ + "high_speed", + 206.2 + ], + "102": [ + "intercity", + 182.8 + ], + "103": [ + "intercity", + 182.8 + ], + "104": [ + "intercity", + 182.8 + ], + "105": [ + "intercity", + 182.8 + ], + "106": [ + "regional_dmu", + 49.5 + ], + "107": [ + "regional_dmu", + 49.5 + ], + "108": [ + "metro", + 115.0 + ], + "109": [ + "commuter_emu", + 98.1 + ], + "110": [ + "regional_dmu", + 49.5 + ], + "111": [ + "intercity", + 182.8 + ], + "112": [ + "intercity", + 182.8 + ], + "113": [ + "intercity", + 182.8 + ], + "114": [ + "intercity", + 182.8 + ], + "115": [ + "intercity", + 182.8 + ], + "116": [ + "regional_dmu", + 49.5 + ], + "117": [ + "intercity", + 182.8 + ], + "200": [ + "coach", + 13.0 + ], + "201": [ + "coach", + 13.0 + ], + "202": [ + "coach", + 13.0 + ], + "203": [ + "coach", + 13.0 + ], + "204": [ + "coach", + 13.0 + ], + "205": [ + "coach", + 13.0 + ], + "206": [ + "coach", + 13.0 + ], + "207": [ + "coach", + 13.0 + ], + "208": [ + "coach", + 13.0 + ], + "209": [ + "coach", + 13.0 + ], + "400": [ + "metro", + 115.0 + ], + "401": [ + "metro", + 115.0 + ], + "402": [ + "metro", + 115.0 + ], + "403": [ + "metro", + 115.0 + ], + "404": [ + "metro", + 115.0 + ], + "405": [ + "monorail", + 55.5 + ], + "700": [ + "city_bus", + 12.0 + ], + "701": [ + "coach", + 13.0 + ], + "702": [ + "coach", + 13.0 + ], + "703": [ + "city_bus", + 12.0 + ], + "704": [ + "city_bus", + 12.0 + ], + "705": [ + "city_bus", + 12.0 + ], + "706": [ + "coach", + 13.0 + ], + "707": [ + "city_bus", + 12.0 + ], + "708": [ + "city_bus", + 12.0 + ], + "709": [ + "city_bus", + 12.0 + ], + "710": [ + "double_decker", + 11.0 + ], + "711": [ + "city_bus", + 12.0 + ], + "712": [ + "city_bus", + 12.0 + ], + "713": [ + "city_bus", + 12.0 + ], + "714": [ + "coach", + 13.0 + ], + "715": [ + "van", + 5.5 + ], + "716": [ + "city_bus", + 12.0 + ], + "800": [ + "trolleybus", + 12.0 + ], + "900": [ + "tram", + 30.0 + ], + "901": [ + "tram", + 30.0 + ], + "902": [ + "tram", + 30.0 + ], + "903": [ + "light_rail", + 28.2 + ], + "904": [ + "tram", + 30.0 + ], + "905": [ + "tram", + 30.0 + ], + "906": [ + "tram", + 30.0 + ], + "1000": [ + "fast_ferry", + 40.0 + ], + "1200": [ + "fast_ferry", + 40.0 + ], + "1300": [ + "gondola", + 2.1 + ], + "1301": [ + "gondola", + 2.1 + ], + "1302": [ + "gondola", + 2.1 + ], + "1306": [ + "gondola", + 2.1 + ], + "1307": [ + "gondola", + 2.1 + ], + "1400": [ + "funicular", + 14.0 + ], + "1500": [ + "car", + 4.6 + ], + "1501": [ + "van", + 5.5 + ], + "1502": [ + "car", + 4.6 + ], + "1503": [ + "car", + 4.6 + ], + "1504": [ + "car", + 4.6 + ], + "1505": [ + "car", + 4.6 + ], + "1506": [ + "car", + 4.6 + ], + "1507": [ + "car", + 4.6 + ] + }, + "rail": { + "transit": { + "rail": [ + "commuter_emu", + 98.1 + ], + "metro": [ + "metro", + 115.0 + ], + "tram": [ + "tram", + 30.0 + ] + }, + "local": { + "rail": [ + "commuter_emu", + 98.1 + ], + "metro": [ + "metro", + 115.0 + ], + "tram": [ + "tram", + 30.0 + ] + }, + "regional": { + "rail": [ + "regional_dmu", + 49.5 + ], + "metro": [ + "metro", + 115.0 + ], + "tram": [ + "light_rail", + 28.2 + ] + }, + "national": { + "rail": [ + "intercity", + 182.8 + ], + "metro": [ + "metro", + 115.0 + ], + "tram": [ + "light_rail", + 28.2 + ] + }, + "international": { + "rail": [ + "high_speed", + 206.2 + ], + "metro": [ + "metro", + 115.0 + ], + "tram": [ + "light_rail", + 28.2 + ] + } + }, + "default": [ + "city_bus", + 12.0 + ], + "elevation": { + "monorail": 6.0, + "gondola": 20.0 + } + } +} \ No newline at end of file diff --git a/scripts/format-scope.json b/scripts/format-scope.json index df01df506..5ddbbcf34 100644 --- a/scripts/format-scope.json +++ b/scripts/format-scope.json @@ -1428,5 +1428,9 @@ "src/ui/ownerFlagSheet.js", "src/data/placePacksLoading.test.mjs", "src/ui/placePacksLoading.js", - "src/ui/placePacksLoading.test.mjs" + "src/ui/placePacksLoading.test.mjs", + "src/data/fleetModels.test.mjs", + "src/data/fleetModelInstances.test.mjs", + "src/testSupport/fleetGlb.mjs", + "scripts/measure-fleet-trail-anchors.mjs" ] diff --git a/scripts/measure-fleet-trail-anchors.mjs b/scripts/measure-fleet-trail-anchors.mjs new file mode 100644 index 000000000..c26874dc7 --- /dev/null +++ b/scripts/measure-fleet-trail-anchors.mjs @@ -0,0 +1,53 @@ +#!/usr/bin/env node +/** + * Re-measure the trail attachment point of every aircraft-layer fleet model + * and print `src/data/fleetTrailAnchors.js`. + * + * node scripts/measure-fleet-trail-anchors.mjs > src/data/fleetTrailAnchors.js + * + * Run it whenever a model under `public/models/fleet/` changes; the unit + * suite (`src/data/fleetModels.test.mjs`) re-derives each value and fails + * on a table that no longer matches its GLB. + */ +import fs from 'node:fs'; +import path from 'node:path'; +import { projectRoot } from './project-root.mjs'; +import { + fleetMesh, + measuredTrailAnchor, +} from '../src/testSupport/fleetGlb.mjs'; + +/** Catalog categories the aircraft layers draw (ADS-B ground vehicles and balloons included). */ +export const TRAIL_ANCHOR_CATEGORIES = ['aircraft', 'helicopters', 'other']; + +const root = projectRoot(import.meta.url); +const dir = path.join(root, 'public', 'models', 'fleet'); +const catalog = JSON.parse( + fs.readFileSync(path.join(dir, 'catalog.json'), 'utf8'), +); +const rows = []; +for (const [name, model] of Object.entries(catalog)) { + if (!TRAIL_ANCHOR_CATEGORIES.includes(model.category)) continue; + const anchor = measuredTrailAnchor( + await fleetMesh(path.join(dir, model.file)), + ); + if (!anchor) continue; + const [x, y] = anchor.map((value) => Number(value.toFixed(4)) || 0); + rows.push( + ` ${/^[a-z_]\w*$/.test(name) ? name : `'${name}'`}: Object.freeze([${x}, ${y}, 0]),`, + ); +} +process.stdout.write(`/** + * Aft-belly trail attachment points of the fleet models the aircraft layers + * draw, in raw glTF axes (X along the body, nose toward -X; Y up), metres + * before scale, node transforms applied. Each is the point of the model's + * centreline profile (its z = 0 slice) nearest its aft-belly bounding-box + * corner: on the hull, aft, and on the centreline (see modelVisualAnchor.js). + * + * Written by scripts/measure-fleet-trail-anchors.mjs; re-measured against the + * GLBs by fleetModels.test.mjs. + */ +export const FLEET_TRAIL_ANCHORS = Object.freeze({ +${rows.join('\n')} +}); +`); diff --git a/src/data/fleetModels.js b/src/data/fleetModels.js new file mode 100644 index 000000000..7c0204fdd --- /dev/null +++ b/src/data/fleetModels.js @@ -0,0 +1,475 @@ +/** + * @module fleetModels + * @description The project's procedural fleet models (`public/models/fleet/`): + * which model draws an aircraft, a vessel or a transit or rail vehicle, and at + * what size, from the set's `catalog.json` (each model's file, bounds, + * length, beam, clearance and icon class) and `types.json` (which model + * draws which type, and the fallbacks). + * + * Every model is metres, glTF Y-up, nose toward local -X, origin at the body + * centre (an aircraft's fuselage axis, a vessel's waterline, a road or rail + * vehicle's road or rail top), and is drawn life size: a type's published + * length over the model's built length is its scale. + * + * The resolvers are plain functions over the parsed JSON; `loadFleetModelSet` + * fetches the JSON and every GLB once, up front, so no model is fetched + * mid-session. + */ + +import { classifyAircraft } from './aircraftClass.js'; +import { FLEET_TRAIL_ANCHORS } from './fleetTrailAnchors.js'; + +/** Where the model set is served, relative to the application base. */ +export const FLEET_MODEL_BASE = '/models/fleet/'; + +/** + * Longest and widest hulls afloat, metres (Seawise Giant 458 m; the TI + * tankers 68 m abeam). AIS gives the bow and stern from the antenna in 9 bits + * and each side in 6, their largest values (511, 63) meaning "that or more" + * or a set never configured: a 30 m tug reported 511 m long would be drawn 17 + * times its size. + */ +const MAX_HULL_LENGTH_M = 460; +const MAX_HULL_BEAM_M = 75; + +/** The model a known aircraft class draws when its type has no entry. */ +const CLASS_MODELS = Object.freeze({ + light: 'c172', + glider: 'glider', + turboprop: 'at72', + widebody: 'b789', + quadjet: 'b744', + helicopter: 'generic_heli', + fastjet: 't38', + bizjet: 'c56x', + uav: 'mq9', +}); + +/** OpenSky's integer aircraft category (state vector index 17) → ADS-B emitter category. */ +const OPENSKY_EMITTER_CATEGORY = Object.freeze({ + 2: 'A1', + 3: 'A2', + 4: 'A3', + 5: 'A4', + 6: 'A5', + 7: 'A6', + 8: 'A7', + 9: 'B1', + 10: 'B2', + 11: 'B3', + 12: 'B4', + 14: 'B6', + 15: 'B7', + 16: 'C1', + 17: 'C2', + 18: 'C3', + 19: 'C4', + 20: 'C5', +}); + +/** A text vessel type (as some AIS sources name it) → a representative AIS ship-type code. */ +const TEXT_SHIP_TYPES = [ + [/tanker|lng|lpg|gas carrier/i, 80], + [/cargo|container|bulk|carrier|freighter|ro-?ro/i, 70], + [/passenger|ferry|cruise/i, 60], + [/high.?speed/i, 40], + [/fishing|trawler/i, 30], + [/tug|towing/i, 52], + [/pilot/i, 50], + [/search|rescue|\bsar\b/i, 51], + [/dredg/i, 33], + [/military|naval|navy/i, 35], + [/law|police|patrol|coast ?guard/i, 55], + [/sail/i, 36], + [/pleasure|yacht/i, 37], +]; + +/** + * The model for an aircraft of ICAO type `type` (A320, B738, R44) and ADS-B + * emitter category `category`: the type's, else the category's, else + * `default_aircraft`. + * @param {object} types Parsed `types.json`. + * @param {string} [type] + * @param {string} [category] + * @returns {{model: string, length: number|null}} + */ +export function resolveAircraft(types, type, category) { + const t = type?.trim().toUpperCase(); + const [model, length] = + (t && (types.aircraft[t] ?? types.helicopters[t])) || + (category && types.categories[category]) || + types.default_aircraft; + return { model, length }; +} + +/** + * A reported length as one a hull can have, else undefined (drawn as built). + * @param {number} [length] + * @returns {number|undefined} + */ +export function hullLength(length) { + return length !== undefined && + length !== null && + length > 0 && + length <= MAX_HULL_LENGTH_M + ? length + : undefined; +} + +/** + * A reported beam as one a hull can have, else undefined. + * @param {number} [beam] + * @returns {number|undefined} + */ +export function hullBeam(beam) { + return beam !== undefined && + beam !== null && + beam > 0 && + beam <= MAX_HULL_BEAM_M + ? beam + : undefined; +} + +/** + * The model for a vessel of AIS ship type `shipType` and the reported length + * and beam (metres) and name: the first rule that matches in the first type + * range that has one, else `default_vessel`. Drawn at the reported length, as + * built when it is unknown. + * @param {object} types Parsed `types.json`. + * @param {number} [shipType] + * @param {number} [length] + * @param {number} [beam] + * @param {string} [name] + * @returns {{model: string, length: number|null}} + */ +export function resolveVessel(types, shipType, length, beam, name) { + const L = hullLength(length) ?? 0; + const B = hullBeam(beam) ?? 0; + const drawn = L > 0 ? L : null; + if (shipType !== undefined && shipType !== null) { + for (const group of types.vessels) { + if (!(shipType >= group.types[0] && shipType <= group.types[1])) continue; + for (const rule of group.rules) { + if (rule.min_len && L < rule.min_len) continue; + if (rule.max_len && L > rule.max_len) continue; + if (rule.name_re && !new RegExp(rule.name_re).test(name ?? '')) + continue; + if (rule.max_bl && !(B > 0 && L > 0 && B / L <= rule.max_bl)) continue; + return { model: rule.model, length: drawn }; + } + } + } + return { model: types.default_vessel, length: drawn }; +} + +/** + * The model for a public transport vehicle: a rail category and mode + * (`transit`/`local`/`regional`/`national`/`international` × `rail`/`metro`/ + * `tram`), else a GTFS `route_type` (basic 0-12 or extended 100-1507), else a + * mode name (`bus`, `tram`, `subway`, `rail`, `ferry`, `light_rail`, …), + * else `transit.default`. Each comes with the length to draw it at. + * @param {object} types Parsed `types.json`. + * @param {{railCategory?: string, railMode?: string, routeType?: number|string, + * mode?: string}} vehicle + * @returns {{model: string, length: number|null}} + */ +export function resolveTransit( + types, + { railCategory, railMode, routeType, mode } = {}, +) { + const transit = types.transit; + const rail = railCategory ? transit.rail[railCategory] : null; + const routeKey = + routeType === undefined || routeType === null || routeType === '' + ? null + : String(Number(routeType)); + const [model, length] = + (rail && (rail[railMode || 'rail'] ?? rail.rail)) || + (routeKey && transit.route_types[routeKey]) || + (mode && transit.modes[mode]) || + transit.default; + return { model, length }; +} + +/** + * An AIS ship type as its numeric code: a code as given ("70"), or the code + * of the family a text type names ("Cargo" → 70); undefined when it names + * none. + * @param {string|number|null|undefined} type + * @returns {number|undefined} + */ +export function aisShipTypeCode(type) { + const text = String(type ?? '').trim(); + if (!text) return undefined; + if (/^\d+$/.test(text)) { + const code = Number(text); + return code > 0 && code < 100 ? code : undefined; + } + for (const [pattern, code] of TEXT_SHIP_TYPES) + if (pattern.test(text)) return code; + return undefined; +} + +/** + * An aircraft category as an ADS-B emitter category: an emitter string as + * given ("A3"), or OpenSky's integer category mapped to one. + * @param {string|number|null|undefined} category + * @returns {string|undefined} + */ +export function emitterCategory(category) { + if (typeof category === 'number') + return OPENSKY_EMITTER_CATEGORY[category] || undefined; + const text = String(category ?? '') + .trim() + .toUpperCase(); + if (/^\d+$/.test(text)) return OPENSKY_EMITTER_CATEGORY[Number(text)]; + return /^[A-D][0-7]$/.test(text) ? text : undefined; +} + +/** + * The scale of model `model` drawn `length` metres long, along the model's + * length, beam and height. A vessel's reported beam stretches it sideways; + * its height follows the mean of the two. + * @param {object} model Catalog entry. + * @param {number|null} length + * @param {number} [beam] A vessel's reported beam. + * @returns {[number, number, number]} + */ +export function placeScale(model, length, beam) { + if (length === null || length === undefined || !(model.length > 0)) + return [1, 1, 1]; + const sx = length / model.length; + const b = hullBeam(beam); + const sy = b !== undefined && model.beam > 0 ? b / model.beam : sx; + return [sx, sy, (sx + sy) / 2]; +} + +/** Half the diagonal of a model's bounds: the radius Cesium bounds it by. */ +function boundingRadius(model) { + const [x0, y0, z0] = model.min; + const [x1, y1, z1] = model.max; + return Math.hypot(x1 - x0, y1 - y0, z1 - z0) / 2; +} + +/** + * The drawing of model `name` at `length` metres: what the layers place. + * + * `scale` is the uniform scale (along the model's length) a Cesium Model + * takes; `scaleXYZ` the model's length, beam and height scales, for a vessel + * stretched to its reported beam. `clearanceM` is how far the model reaches + * below its origin at that scale (wheels below an aircraft's axis, a keel + * below the waterline): a grounded aircraft stands its origin that far up. + * `elevationM` lifts a vehicle off the route it follows (a monorail, a + * gondola). `tinted` models carry the `_TINT` vertex attribute marking where + * a route colour applies. `nativeRadiusM` (unscaled), `visualCenterNative` + * and `trailAnchorNative` (raw glTF axes, unscaled) serve the aircraft + * layers' pixel-size, bracket and trail geometry. + * @param {{catalog: object, types: object}} set + * @param {string} name Catalog model name. + * @param {number|null} length Metres along the body; null draws it as built. + * @param {{beam?: number}} [options] + * @returns {object} + */ +export function fleetModelSpec(set, name, length, { beam } = {}) { + const model = set.catalog[name]; + // Ground vehicles, balloons and obstacles are built at the size their type + // names (a tower's 60 m is its height, not a length to scale to). + const drawnLength = model.category === 'other' ? null : length; + const scaleXYZ = placeScale(model, drawnLength, beam); + const elevationM = set.types.transit?.elevation?.[name] || 0; + return Object.freeze({ + model: name, + url: FLEET_MODEL_BASE + model.file, + category: model.category, + icon: model.icon, + length: model.length * scaleXYZ[0], + scale: scaleXYZ[0], + scaleXYZ: Object.freeze(scaleXYZ), + clearanceM: model.clearance * scaleXYZ[2], + elevationM, + tinted: model.category === 'transit', + nativeRadiusM: boundingRadius(model), + visualCenterNative: CENTRED, + trailAnchorNative: FLEET_TRAIL_ANCHORS[name] || CENTRED, + key: `${name}@${scaleXYZ.map((s) => s.toFixed(4)).join('x')}`, + }); +} + +const CENTRED = Object.freeze([0, 0, 0]); + +/** `resolved` drawn from `set`, its model replaced by `fallback` when the catalog lacks it. */ +function specOf(set, resolved, fallback, options) { + const known = set.catalog[resolved.model] ? resolved : fallback(resolved); + return fleetModelSpec(set, known.model, known.length, options); +} + +/** + * The model an aircraft is drawn with. The type's entry first; then, for a + * type the map lacks, the model of its class (a fast jet, a helicopter, a + * glider) when the type names one; then the emitter category's; then + * `default_aircraft`. + * @param {{catalog: object, types: object}} set + * @param {{typeCode?: string|null, type?: string|null, + * category?: string|number|null}} aircraft + * @returns {object} {@link fleetModelSpec} + */ +export function aircraftModelSpec(set, { typeCode, type, category } = {}) { + const { types, catalog } = set; + const code = String(typeCode ?? type ?? '') + .trim() + .toUpperCase(); + const emitter = emitterCategory(category); + let resolved = resolveAircraft(types, code, emitter); + const typed = code && (types.aircraft[code] || types.helicopters[code]); + if (!typed && code) { + const model = CLASS_MODELS[classifyAircraft({ typeCode: code })]; + if (model && catalog[model]) + resolved = { model, length: catalog[model].length }; + } + return specOf(set, resolved, () => ({ + model: types.default_aircraft[0], + length: types.default_aircraft[1], + })); +} + +/** + * The model a vessel is drawn with, from its AIS ship type (a code or a text + * family), reported length and beam (metres) and name. + * @param {{catalog: object, types: object}} set + * @param {{type?: string|number|null, length?: number|null, + * beam?: number|null, name?: string|null}} vessel + * @returns {object} {@link fleetModelSpec} + */ +export function vesselModelSpec(set, { type, length, beam, name } = {}) { + const resolved = resolveVessel( + set.types, + aisShipTypeCode(type), + length ?? undefined, + beam ?? undefined, + name ?? undefined, + ); + return specOf( + set, + resolved, + (r) => ({ model: set.types.default_vessel, length: r.length }), + { beam: beam ?? undefined }, + ); +} + +/** + * The model a transit or rail vehicle is drawn with ({@link resolveTransit}). + * @param {{catalog: object, types: object}} set + * @param {object} vehicle + * @returns {object} {@link fleetModelSpec} + */ +export function transitModelSpec(set, vehicle) { + return specOf(set, resolveTransit(set.types, vehicle), () => ({ + model: set.types.transit.default[0], + length: set.types.transit.default[1], + })); +} + +/** + * Every model the type map names, defaults and fallbacks included. + * @param {object} types Parsed `types.json`. + * @returns {Set} + */ +export function namedFleetModels(types) { + const pairs = (map) => Object.values(map).map(([model]) => model); + const transit = types.transit; + return new Set([ + ...pairs(types.aircraft), + ...pairs(types.helicopters), + ...pairs(types.categories), + ...types.vessels.flatMap((group) => group.rules.map((rule) => rule.model)), + types.default_aircraft[0], + types.default_vessel, + ...pairs(transit.modes), + ...pairs(transit.route_types), + ...Object.values(transit.rail).flatMap(pairs), + transit.default[0], + ...Object.keys(transit.elevation), + ...Object.values(CLASS_MODELS), + ]); +} + +let loading = null; +let loaded = null; + +/** + * The model set once {@link loadFleetModelSet} has loaded it, else null. + * @returns {object|null} + */ +export function fleetModelSet() { + return loaded; +} + +/** + * Fetch the model set once for every layer: `catalog.json`, `types.json` and + * every GLB the catalog names, all up front, so no model is fetched while a + * session runs. Resolves to `{catalog, types, bytes, loadMs, totalBytes}`, + * `bytes` holding each model's GLB by name; a GLB that fails to load is left + * out (that model then draws nothing and its entities stay icons). + * @param {{resolveAsset?: (url: string) => string, + * fetchImpl?: typeof fetch, now?: () => number}} [options] + * @returns {Promise} + */ +export function loadFleetModelSet({ + resolveAsset = (url) => url, + fetchImpl = globalThis.fetch, + now = () => globalThis.performance?.now?.() ?? Date.now(), +} = {}) { + if (loading) return loading; + const started = now(); + const json = async (name) => { + const response = await fetchImpl(resolveAsset(FLEET_MODEL_BASE + name)); + if (!response.ok) throw new Error(`${name}: HTTP ${response.status}`); + return response.json(); + }; + loading = (async () => { + const [catalog, types] = await Promise.all([ + json('catalog.json'), + json('types.json'), + ]); + const bytes = new Map(); + let totalBytes = 0; + await Promise.all( + Object.entries(catalog).map(async ([name, model]) => { + try { + const response = await fetchImpl( + resolveAsset(FLEET_MODEL_BASE + model.file), + ); + if (!response.ok) return; + const glb = new Uint8Array(await response.arrayBuffer()); + bytes.set(name, glb); + totalBytes += glb.byteLength; + } catch { + /* that model draws nothing; its entities stay icons */ + } + }), + ); + loaded = Object.freeze({ + catalog, + types, + bytes, + loadMs: now() - started, + totalBytes, + }); + console.info( + `[Fleet] ${bytes.size}/${Object.keys(catalog).length} models ` + + `(${(totalBytes / 1048576).toFixed(2)} MB) loaded in ${Math.round(loaded.loadMs)} ms`, + ); + return loaded; + })(); + // A failed catalog load may be retried by the next layer that starts. + loading.catch(() => { + loading = null; + }); + return loading; +} + +/** Forget the loaded set (tests). */ +export function resetFleetModelSet() { + loading = null; + loaded = null; +} diff --git a/src/data/fleetModels.test.mjs b/src/data/fleetModels.test.mjs new file mode 100644 index 000000000..9eb484a6a --- /dev/null +++ b/src/data/fleetModels.test.mjs @@ -0,0 +1,427 @@ +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { + FLEET_MODEL_BASE, + aircraftModelSpec, + aisShipTypeCode, + emitterCategory, + fleetModelSet, + fleetModelSpec, + hullBeam, + hullLength, + loadFleetModelSet, + namedFleetModels, + placeScale, + resetFleetModelSet, + resolveAircraft, + resolveTransit, + resolveVessel, + transitModelSpec, + vesselModelSpec, +} from './fleetModels.js'; +import { FLEET_TRAIL_ANCHORS } from './fleetTrailAnchors.js'; +import { + centrelineSegments, + distanceToSurface, + fleetMesh, + measuredTrailAnchor, + readGlb, + vertexBounds, +} from '../testSupport/fleetGlb.mjs'; + +const ROOT = path.resolve( + path.dirname(fileURLToPath(import.meta.url)), + '../..', +); +const DIR = path.join(ROOT, 'public', 'models', 'fleet'); +const json = (name) => + JSON.parse(fs.readFileSync(path.join(DIR, name), 'utf8')); +const catalog = json('catalog.json'); +const types = json('types.json'); +const set = { catalog, types }; + +// A slice of the set's types.json and catalog.json, as the resolver's own +// reference tests use, so these read independently of later model changes. +const sliceTypes = { + aircraft: { + A320: ['a320', 37.57], + A21N: ['a320', 44.51], + A319: ['a320', 33.84], + B38M: ['b738', 39.52], + }, + helicopters: { EC35: ['ec35', 10.2] }, + categories: { A1: ['c172', 8.3] }, + vessels: [ + { + types: [70, 79], + rules: [ + { min_len: 70, max_len: 140, max_bl: 0.12, model: 'inland_cargo' }, + { + name_re: '\\b(LEADER|HIGHWAY|ACE|GLOVIS|HOEGH|SPIRIT OF)\\b', + min_len: 150, + model: 'car_carrier', + }, + { min_len: 250, model: 'container_ship' }, + { model: 'general_cargo' }, + ], + }, + { types: [31, 32], rules: [{ model: 'tug' }] }, + { types: [52, 52], rules: [{ model: 'tug' }] }, + { types: [0, 99], rules: [{ model: 'generic_vessel' }] }, + ], + default_aircraft: ['generic_plane', 36], + default_vessel: 'generic_vessel', +}; +const model = (category, file, length, beam, clearance) => ({ + category, + file, + min: [0, 0, 0], + max: [0, 0, 0], + length, + beam, + clearance, + tris: 1, + icon: 'jet', +}); +const sliceCatalog = { + a320: model('aircraft', 'aircraft/a320.glb', 37.57, 35.6, 3.063), + container_ship: model('ships', 'ships/container_ship.glb', 309.9, 48, 13.9), +}; + +test('aircraft resolve by type, then emitter category, then the generic plane', () => { + assert.deepEqual(resolveAircraft(sliceTypes, 'A320'), { + model: 'a320', + length: 37.57, + }); + assert.deepEqual(resolveAircraft(sliceTypes, ' b38m '), { + model: 'b738', + length: 39.52, + }); + assert.equal(resolveAircraft(sliceTypes, 'EC35').model, 'ec35'); + assert.deepEqual(resolveAircraft(sliceTypes, 'ZZZZ', 'A1'), { + model: 'c172', + length: 8.3, + }); + assert.deepEqual(resolveAircraft(sliceTypes, undefined, 'Q9'), { + model: 'generic_plane', + length: 36, + }); +}); + +test('vessels: the first matching rule of the first type range with one, else the generic vessel', () => { + assert.equal(resolveVessel(sliceTypes, 70, 366, 51).model, 'container_ship'); + assert.equal( + resolveVessel(sliceTypes, 71, 199, 32, 'GLOVIS SPIRIT').model, + 'car_carrier', + ); + assert.equal(resolveVessel(sliceTypes, 79, 110, 11.4).model, 'inland_cargo'); + assert.equal(resolveVessel(sliceTypes, 70, 90, 15).model, 'general_cargo'); + assert.deepEqual(resolveVessel(sliceTypes, 52, 30, 10), { + model: 'tug', + length: 30, + }); + assert.deepEqual(resolveVessel(sliceTypes, undefined, 50), { + model: 'generic_vessel', + length: 50, + }); + assert.deepEqual(resolveVessel(sliceTypes, 0), { + model: 'generic_vessel', + length: null, + }); +}); + +test("vessels: AIS's out-of-range sizes (511 m long, 126 m abeam) are unknown, drawn as built", () => { + assert.deepEqual(resolveVessel(sliceTypes, 52, 511), { + model: 'tug', + length: null, + }); + assert.equal(hullLength(511), undefined); + assert.equal(hullLength(0), undefined); + assert.equal(hullBeam(126), undefined); + assert.equal(hullBeam(32), 32); + const ship = sliceCatalog.container_ship; + assert.deepEqual(placeScale(ship, ship.length, 126), [1, 1, 1]); +}); + +test('scale: aircraft by the type length, vessels by length and beam', () => { + const a320 = sliceCatalog.a320; + const [sx, sy, sz] = placeScale(a320, 44.51); + assert.ok(Math.abs(sx - 44.51 / 37.57) < 1e-12); + assert.equal(sy, sx); + assert.equal(sz, sx); + const ship = sliceCatalog.container_ship; + assert.deepEqual(placeScale(ship, ship.length / 2, 24), [0.5, 0.5, 0.5]); + assert.deepEqual(placeScale(ship, null), [1, 1, 1]); +}); + +test('every model the type map names is in the catalog, and its file is a GLB on disk', () => { + for (const name of namedFleetModels(types)) + assert.ok( + catalog[name], + `${name}: named by types.json but not in the catalog`, + ); + assert.equal(catalog.generic_plane?.category, 'aircraft'); + assert.equal(catalog.generic_vessel?.category, 'ships'); + assert.equal(catalog.city_bus?.category, 'transit'); + for (const [name, metres] of Object.entries(types.transit.elevation)) { + assert.ok(catalog[name], name); + assert.ok(metres > 0, name); + } + let total = 0; + for (const [name, entry] of Object.entries(catalog)) { + const file = path.join(DIR, entry.file); + assert.ok(fs.existsSync(file), `${name}: ${entry.file} is missing`); + const bytes = fs.readFileSync(file); + assert.equal(bytes.subarray(0, 4).toString('latin1'), 'glTF', name); + assert.ok(entry.length > 0, name); + assert.ok(entry.tris <= 50_000, name); + total += bytes.length; + } + // The set ships with the repository and loads up front: keep it small. + assert.ok(total < 3 * 1024 * 1024, `fleet GLBs total ${total} bytes`); +}); + +test('transit bodies mark the share of the route colour they take; other models mark none', () => { + for (const [name, entry] of Object.entries(catalog)) { + const { json: gltf } = readGlb(path.join(DIR, entry.file)); + for (const mesh of gltf.meshes) + for (const primitive of mesh.primitives) + assert.equal( + '_TINT' in primitive.attributes, + entry.category === 'transit', + name, + ); + assert.equal( + fleetModelSpec(set, name, null).tinted, + entry.category === 'transit', + ); + } +}); + +test('aircraft specs: by type, then class for an unlisted type, then emitter category', () => { + assert.equal(aircraftModelSpec(set, { typeCode: 'A320' }).model, 'a320'); + assert.equal(aircraftModelSpec(set, { type: 'b744' }).model, 'b744'); + // An unlisted fast jet is still a fast jet, not the generic airliner. + assert.equal( + aircraftModelSpec(set, { typeCode: 'FA18', category: 'A3' }).model, + 't38', + ); + assert.equal(aircraftModelSpec(set, { typeCode: 'MQ9' }).model, 'mq9'); + assert.equal( + aircraftModelSpec(set, { typeCode: 'PUMA' }).model, + 'generic_heli', + ); + // An unlisted type of no special class falls to its category. + assert.equal( + aircraftModelSpec(set, { typeCode: 'ZZZZ', category: 'A5' }).model, + 'b77w', + ); + // OpenSky's integer category: 8 is rotorcraft (A7), 2 light (A1). + assert.equal(emitterCategory(8), 'A7'); + assert.equal(emitterCategory('a3'), 'A3'); + assert.equal(emitterCategory(1), undefined); + assert.equal(aircraftModelSpec(set, { category: 8 }).model, 'generic_heli'); + assert.equal(aircraftModelSpec(set, { category: 2 }).model, 'c172'); + assert.equal(aircraftModelSpec(set, {}).model, types.default_aircraft[0]); +}); + +test('aircraft specs are life size, standing on their scaled clearance', () => { + const spec = aircraftModelSpec(set, { typeCode: 'A21N' }); + const [, length] = types.aircraft.A21N; + assert.equal(spec.model, types.aircraft.A21N[0]); + assert.ok(Math.abs(spec.length - length) < 1e-9); + assert.ok(Math.abs(spec.scale - length / catalog[spec.model].length) < 1e-12); + assert.ok( + Math.abs(spec.clearanceM - catalog[spec.model].clearance * spec.scale) < + 1e-12, + ); + assert.equal(spec.url, `${FLEET_MODEL_BASE}${catalog[spec.model].file}`); + assert.equal(spec.tinted, false); + assert.equal(spec.elevationM, 0); + assert.ok( + spec.nativeRadiusM * spec.scale > 25 && + spec.nativeRadiusM * spec.scale < 60, + ); + assert.deepEqual(spec.trailAnchorNative, FLEET_TRAIL_ANCHORS[spec.model]); + // A grounded service vehicle and an obstacle are drawn as built: the + // tower's 60 m is its height, not a length to stretch it to. + const tower = aircraftModelSpec(set, { category: 'C3' }); + assert.equal(tower.model, 'tower'); + assert.equal(tower.scale, 1); + assert.equal(aircraftModelSpec(set, { category: 'C1' }).scale, 1); +}); + +test('vessel specs read AIS codes and text families, stretched to the reported beam', () => { + assert.equal(aisShipTypeCode('70'), 70); + assert.equal(aisShipTypeCode(' Cargo '), 70); + assert.equal(aisShipTypeCode('Tanker'), 80); + assert.equal(aisShipTypeCode('Tug'), 52); + assert.equal(aisShipTypeCode('0'), undefined); + assert.equal(aisShipTypeCode(''), undefined); + const box = vesselModelSpec(set, { type: '71', length: 366, beam: 51 }); + assert.equal(box.model, 'container_ship'); + assert.ok(Math.abs(box.length - 366) < 1e-9); + const [sx, sy, sz] = box.scaleXYZ; + assert.ok(Math.abs(sy - 51 / catalog.container_ship.beam) < 1e-12); + assert.ok(Math.abs(sz - (sx + sy) / 2) < 1e-12); + assert.equal(vesselModelSpec(set, { type: 'Tug', length: 30 }).model, 'tug'); + assert.equal(vesselModelSpec(set, { type: '36' }).model, 'sailboat'); + // No type, no size: the generic vessel as built. + const unknown = vesselModelSpec(set, {}); + assert.equal(unknown.model, types.default_vessel); + assert.equal(unknown.scale, 1); +}); + +test('transit specs: rail category and mode, then route type, then mode, then the default', () => { + assert.deepEqual(resolveTransit(types, { railCategory: 'national' }), { + model: 'intercity', + length: types.transit.rail.national.rail[1], + }); + assert.equal( + resolveTransit(types, { railCategory: 'transit', railMode: 'metro' }).model, + 'metro', + ); + assert.equal( + resolveTransit(types, { railCategory: 'regional', railMode: 'tram' }).model, + 'light_rail', + ); + assert.equal(resolveTransit(types, { routeType: 3 }).model, 'city_bus'); + assert.equal(resolveTransit(types, { routeType: '0' }).model, 'tram'); + assert.equal(resolveTransit(types, { routeType: 109 }).model, 'commuter_emu'); + assert.equal(resolveTransit(types, { mode: 'subway' }).model, 'metro'); + assert.equal(resolveTransit(types, { mode: 'ferry' }).model, 'fast_ferry'); + assert.equal( + resolveTransit(types, { mode: 'unknown' }).model, + types.transit.default[0], + ); + const tram = transitModelSpec(set, { mode: 'tram' }); + assert.equal(tram.tinted, true); + assert.equal(tram.clearanceM, 0); + assert.ok(Math.abs(tram.length - types.transit.modes.tram[1]) < 1e-9); + assert.equal( + transitModelSpec(set, { routeType: 12 }).elevationM, + types.transit.elevation.monorail, + ); + assert.equal( + transitModelSpec(set, { mode: 'gondola' }).elevationM, + types.transit.elevation.gondola, + ); + assert.equal(transitModelSpec(set, { mode: 'ferry' }).tinted, false); +}); + +test('a model missing from the catalog falls back to the default of its kind', () => { + const thin = { + catalog: { ...catalog }, + types: { + ...types, + aircraft: { ...types.aircraft, XXXX: ['no_such_model', 20] }, + }, + }; + assert.equal( + aircraftModelSpec(thin, { typeCode: 'XXXX' }).model, + types.default_aircraft[0], + ); +}); + +const ANCHORED = Object.entries(catalog).filter(([, entry]) => + ['aircraft', 'helicopters', 'other'].includes(entry.category), +); + +test('every aircraft-layer model has a trail anchor on its real aft-belly hull', async () => { + assert.deepEqual( + new Set(Object.keys(FLEET_TRAIL_ANCHORS)), + new Set(ANCHORED.map(([name]) => name)), + 're-run scripts/measure-fleet-trail-anchors.mjs', + ); + for (const [name, entry] of ANCHORED) { + const mesh = await fleetMesh(path.join(DIR, entry.file)); + const { min, max } = vertexBounds(mesh.verts); + const anchor = FLEET_TRAIL_ANCHORS[name]; + // On the hull, not in the air beside it. + const offHull = distanceToSurface(anchor, mesh.verts, mesh.tris); + assert.ok( + offHull <= 1e-3, + `${name}: trail anchor is ${offHull.toFixed(4)} m off the hull`, + ); + // Aft of the middle, so the head segment does not run into the body. + assert.ok( + anchor[0] > (min[0] + max[0]) / 2, + `${name}: trail anchor is not aft`, + ); + // Nothing of the centreline hangs lower at or behind its station. + const lowest = centrelineSegments(mesh.verts, mesh.tris) + .flat() + .filter((p) => p[0] >= anchor[0] - 1e-3) + .reduce((low, p) => Math.min(low, p[1]), Infinity); + assert.ok( + anchor[1] <= lowest + 1e-3, + `${name}: trail anchor is above the hull aft of it`, + ); + assert.equal(anchor[2], 0, `${name}: trail anchor is off the centreline`); + // And it is the construction, re-derived. + const measured = measuredTrailAnchor(mesh); + for (const axis of [0, 1]) + assert.ok( + Math.abs(anchor[axis] - measured[axis]) <= 1e-3, + `${name} axis ${axis}: table ${anchor[axis]} vs measured ${measured[axis].toFixed(4)}`, + ); + } +}); + +test('catalog bounds match the GLBs they describe', async () => { + for (const [name, entry] of Object.entries(catalog)) { + const { min, max } = vertexBounds( + (await fleetMesh(path.join(DIR, entry.file))).verts, + ); + // The catalog is Z up (glTF Y), glTF Z across. + const tolerance = 0.01 * Math.max(1, entry.length); + assert.ok(Math.abs(min[0] - entry.min[0]) <= tolerance, `${name} min x`); + assert.ok(Math.abs(max[0] - entry.max[0]) <= tolerance, `${name} max x`); + assert.ok(Math.abs(min[1] - entry.min[2]) <= tolerance, `${name} min up`); + assert.ok( + Math.abs(-min[1] - entry.clearance) <= tolerance, + `${name} clearance`, + ); + assert.ok( + Math.abs(max[2] - min[2] - (entry.max[1] - entry.min[1])) <= tolerance, + `${name} beam`, + ); + } +}); + +test('the model set loads once, every GLB up front, shared by every layer', async () => { + resetFleetModelSet(); + const requested = []; + const fetchImpl = async (url) => { + requested.push(url); + const file = path.join(ROOT, 'public', url.replace(/^\/app/, '')); + if (!fs.existsSync(file)) return { ok: false, status: 404 }; + const bytes = fs.readFileSync(file); + return { + ok: true, + json: async () => JSON.parse(bytes.toString('utf8')), + arrayBuffer: async () => + bytes.buffer.slice( + bytes.byteOffset, + bytes.byteOffset + bytes.byteLength, + ), + }; + }; + const resolveAsset = (url) => `/app${url}`; + const [first, second] = await Promise.all([ + loadFleetModelSet({ resolveAsset, fetchImpl }), + loadFleetModelSet({ resolveAsset, fetchImpl }), + ]); + assert.equal(first, second); + assert.equal(fleetModelSet(), first); + assert.equal(first.bytes.size, Object.keys(catalog).length); + assert.equal(requested.length, Object.keys(catalog).length + 2); + assert.ok(requested.every((url) => url.startsWith('/app/models/fleet/'))); + assert.ok(first.totalBytes > 0 && Number.isFinite(first.loadMs)); + await loadFleetModelSet({ resolveAsset, fetchImpl }); + assert.equal(requested.length, Object.keys(catalog).length + 2); + resetFleetModelSet(); +}); diff --git a/src/data/fleetTrailAnchors.js b/src/data/fleetTrailAnchors.js new file mode 100644 index 000000000..4c109801b --- /dev/null +++ b/src/data/fleetTrailAnchors.js @@ -0,0 +1,66 @@ +/** + * Aft-belly trail attachment points of the fleet models the aircraft layers + * draw, in raw glTF axes (X along the body, nose toward -X; Y up), metres + * before scale, node transforms applied. Each is the point of the model's + * centreline profile (its z = 0 slice) nearest its aft-belly bounding-box + * corner: on the hull, aft, and on the centreline (see modelVisualAnchor.js). + * + * Written by scripts/measure-fleet-trail-anchors.mjs; re-measured against the + * GLBs by fleetModels.test.mjs. + */ +export const FLEET_TRAIL_ANCHORS = Object.freeze({ + a139: Object.freeze([6.8847, 0.2766, 0]), + a320: Object.freeze([17.2078, 0.7416, 0]), + a333: Object.freeze([29.7276, 1.1084, 0]), + a343: Object.freeze([30.0371, 1.2335, 0]), + a359: Object.freeze([31.0731, 1.1573, 0]), + a388: Object.freeze([34.3139, 1.9279, 0]), + as50: Object.freeze([5.2278, -0.0999, 0]), + at72: Object.freeze([12.8258, 0.6792, 0]), + b06: Object.freeze([4.62, -0.3002, 0]), + b407: Object.freeze([5.1264, -0.2797, 0]), + b738: Object.freeze([18.3819, 0.8391, 0]), + b744: Object.freeze([33.6232, 1.5411, 0]), + b763: Object.freeze([25.5464, 0.9889, 0]), + b77w: Object.freeze([34.4639, 1.2084, 0]), + b789: Object.freeze([29.0604, 1.0649, 0]), + balloon: Object.freeze([3.8215, -0.0009, 0]), + bcs3: Object.freeze([17.8658, 0.713, 0]), + be20: Object.freeze([6.6699, 0.5505, 0]), + blimp: Object.freeze([32.2493, -7.1382, 0]), + c17: Object.freeze([26.5207, 2.332, 0]), + c172: Object.freeze([4.0507, 0.4481, 0]), + c30j: Object.freeze([13.568, 0.9917, 0]), + c56x: Object.freeze([7.4627, 0.4526, 0]), + car: Object.freeze([2.3, 0.418, 0]), + cl35: Object.freeze([10.4607, 0.8247, 0]), + crj9: Object.freeze([17.8413, 0.8624, 0]), + dh8d: Object.freeze([15.7902, 0.7501, 0]), + e175: Object.freeze([14.697, 0.6077, 0]), + e55p: Object.freeze([7.6566, 0.503, 0]), + ec35: Object.freeze([5.1003, 0.1471, 0]), + ec45: Object.freeze([5.8004, 0.555, 0]), + fire_truck: Object.freeze([5.5, 0.6051, 0]), + generic_heli: Object.freeze([5.4997, 0.1604, 0]), + generic_plane: Object.freeze([16.5228, 0.712, 0]), + glf4: Object.freeze([13.4603, 0.8243, 0]), + glider: Object.freeze([3.3997, 0.1283, 0]), + h47: Object.freeze([7.7303, 0.0035, 0]), + h60: Object.freeze([7.6295, 0.1365, 0]), + h64: Object.freeze([6.9228, -0.5553, 0]), + k35r: Object.freeze([28.0221, -2.4508, 0]), + md11: Object.freeze([30.8002, 1.2291, 0]), + mq9: Object.freeze([5.8498, -0.062, 0]), + p28a: Object.freeze([3.3695, 0.3959, 0]), + pc12: Object.freeze([6.6416, 0.5288, 0]), + pushback: Object.freeze([3, 0.2499, 0]), + r44: Object.freeze([4.4219, -0.3001, 0]), + s76: Object.freeze([6.6001, 0.1915, 0]), + sr22: Object.freeze([3.6762, 0.2376, 0]), + t38: Object.freeze([7.0246, -0.3791, 0]), + tex2: Object.freeze([4.7383, 0.2302, 0]), + tower: Object.freeze([1.2952, -0.0007, 0]), + ultralight: Object.freeze([3.2271, 0.3259, 0]), + v22: Object.freeze([7.9868, 0.2956, 0]), + van: Object.freeze([2.75, 0.418, 0]), +}); diff --git a/src/testSupport/fleetGlb.mjs b/src/testSupport/fleetGlb.mjs new file mode 100644 index 000000000..c797cc836 --- /dev/null +++ b/src/testSupport/fleetGlb.mjs @@ -0,0 +1,356 @@ +/** + * Reads the procedural fleet models' GLBs in Node: their JSON chunk, and their + * triangles in scene space (raw glTF axes, node transforms applied, metres). + * + * The GLBs use EXT_meshopt_compression and KHR_mesh_quantization, so their + * buffers are decoded with the meshoptimizer decoder Cesium itself ships (a + * dependency of `@cesium/engine`, resolved through it rather than named as one + * of this package's own). + */ +import fs from 'node:fs'; +import { createRequire } from 'node:module'; +import { pathToFileURL } from 'node:url'; + +let decoderPromise = null; + +/** The meshoptimizer decoder Cesium decodes these models with. */ +async function meshoptDecoder() { + decoderPromise ??= (async () => { + const fromHere = createRequire(import.meta.url); + const fromCesium = createRequire(fromHere.resolve('cesium/package.json')); + const fromEngine = createRequire( + fromCesium.resolve('@cesium/engine/package.json'), + ); + const module = await import( + pathToFileURL(fromEngine.resolve('meshoptimizer')).href + ); + await module.MeshoptDecoder.ready; + return module.MeshoptDecoder; + })(); + return decoderPromise; +} + +/** + * A GLB's JSON and binary chunks. + * @param {string} file + * @returns {{json: object, bin: Uint8Array}} + */ +export function readGlb(file) { + const bytes = fs.readFileSync(file); + if (bytes.readUInt32LE(0) !== 0x46546c67) + throw new Error(`${file}: not a GLB`); + const jsonLength = bytes.readUInt32LE(12); + const json = JSON.parse(bytes.subarray(20, 20 + jsonLength).toString('utf8')); + const binStart = 20 + jsonLength; + const bin = + binStart + 8 <= bytes.length + ? new Uint8Array( + bytes.buffer, + bytes.byteOffset + binStart + 8, + bytes.readUInt32LE(binStart), + ) + : new Uint8Array(0); + return { json, bin }; +} + +/** Column-major node matrix from a glTF node's TRS (or its matrix). */ +function nodeMatrix(node) { + if (node.matrix) return node.matrix; + const t = node.translation || [0, 0, 0]; + const [x, y, z, w] = node.rotation || [0, 0, 0, 1]; + const s = node.scale || [1, 1, 1]; + return [ + (1 - 2 * (y * y + z * z)) * s[0], + 2 * (x * y + z * w) * s[0], + 2 * (x * z - y * w) * s[0], + 0, + 2 * (x * y - z * w) * s[1], + (1 - 2 * (x * x + z * z)) * s[1], + 2 * (y * z + x * w) * s[1], + 0, + 2 * (x * z + y * w) * s[2], + 2 * (y * z - x * w) * s[2], + (1 - 2 * (x * x + y * y)) * s[2], + 0, + t[0], + t[1], + t[2], + 1, + ]; +} + +function multiply(a, b) { + const out = new Array(16).fill(0); + for (let c = 0; c < 4; c++) + for (let r = 0; r < 4; r++) + for (let k = 0; k < 4; k++) out[c * 4 + r] += a[k * 4 + r] * b[c * 4 + k]; + return out; +} + +const COMPONENTS = { SCALAR: 1, VEC2: 2, VEC3: 3, VEC4: 4 }; +const READERS = { + 5120: ['getInt8', 1, 127], + 5121: ['getUint8', 1, 255], + 5122: ['getInt16', 2, 32767], + 5123: ['getUint16', 2, 65535], + 5125: ['getUint32', 4, 4294967295], + 5126: ['getFloat32', 4, 1], +}; + +/** + * Every element of accessor `index`, normalised integers mapped to -1..1 or + * 0..1 the way a renderer reads them. + */ +function readAccessor(gltf, views, index) { + const accessor = gltf.accessors[index]; + const view = gltf.bufferViews[accessor.bufferView]; + const bytes = views[accessor.bufferView]; + const [getter, size, max] = READERS[accessor.componentType]; + const components = COMPONENTS[accessor.type]; + const stride = view.byteStride || size * components; + const data = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); + const out = new Float64Array(accessor.count * components); + for (let i = 0; i < accessor.count; i++) + for (let c = 0; c < components; c++) { + const value = data[getter]( + (accessor.byteOffset || 0) + i * stride + c * size, + true, + ); + out[i * components + c] = accessor.normalized + ? Math.max(value / max, -1) + : value; + } + return out; +} + +/** Each buffer view's bytes, meshopt-compressed ones decoded. */ +async function bufferViews(gltf, bin) { + const decoder = await meshoptDecoder(); + return gltf.bufferViews.map((view) => { + const meshopt = view.extensions?.EXT_meshopt_compression; + if (!meshopt) + return bin.subarray( + view.byteOffset || 0, + (view.byteOffset || 0) + view.byteLength, + ); + const source = bin.subarray( + meshopt.byteOffset || 0, + (meshopt.byteOffset || 0) + meshopt.byteLength, + ); + const target = new Uint8Array(meshopt.count * meshopt.byteStride); + decoder.decodeGltfBuffer( + target, + meshopt.count, + meshopt.byteStride, + source, + meshopt.mode, + meshopt.filter, + ); + return target; + }); +} + +/** + * A fleet model's triangles in scene space: raw glTF axes (X along the body, + * nose toward -X; Y up; Z across), node transforms applied, metres. + * @param {string} file + * @returns {Promise<{verts: number[][], tris: number[][]}>} + */ +export async function fleetMesh(file) { + const { json: gltf, bin } = readGlb(file); + const views = await bufferViews(gltf, bin); + const verts = []; + const tris = []; + const visit = (index, parent) => { + const node = gltf.nodes[index]; + const m = multiply(parent, nodeMatrix(node)); + if (node.mesh != null) { + for (const primitive of gltf.meshes[node.mesh].primitives) { + const base = verts.length; + const positions = readAccessor( + gltf, + views, + primitive.attributes.POSITION, + ); + for (let i = 0; i < positions.length; i += 3) { + const [x, y, z] = [positions[i], positions[i + 1], positions[i + 2]]; + verts.push([ + m[0] * x + m[4] * y + m[8] * z + m[12], + m[1] * x + m[5] * y + m[9] * z + m[13], + m[2] * x + m[6] * y + m[10] * z + m[14], + ]); + } + const indices = readAccessor(gltf, views, primitive.indices); + for (let i = 0; i + 2 < indices.length; i += 3) + tris.push([ + base + indices[i], + base + indices[i + 1], + base + indices[i + 2], + ]); + } + } + for (const child of node.children || []) visit(child, m); + }; + const identity = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]; + for (const root of gltf.scenes[gltf.scene ?? 0].nodes) visit(root, identity); + return { verts, tris }; +} + +/** Axis-aligned bounds of a vertex list. */ +export function vertexBounds(verts) { + const min = [Infinity, Infinity, Infinity]; + const max = [-Infinity, -Infinity, -Infinity]; + for (const v of verts) + for (let a = 0; a < 3; a++) { + if (v[a] < min[a]) min[a] = v[a]; + if (v[a] > max[a]) max[a] = v[a]; + } + return { min, max }; +} + +const dist3 = (a, b) => Math.hypot(a[0] - b[0], a[1] - b[1], a[2] - b[2]); + +/** Closest point of triangle abc to p (Ericson, Real-Time Collision Detection). */ +function closestOnTriangle(p, a, b, c) { + const sub = (u, v) => [u[0] - v[0], u[1] - v[1], u[2] - v[2]]; + const dot = (u, v) => u[0] * v[0] + u[1] * v[1] + u[2] * v[2]; + const ab = sub(b, a); + const ac = sub(c, a); + const ap = sub(p, a); + const d1 = dot(ab, ap); + const d2 = dot(ac, ap); + if (d1 <= 0 && d2 <= 0) return a; + const bp = sub(p, b); + const d3 = dot(ab, bp); + const d4 = dot(ac, bp); + if (d3 >= 0 && d4 <= d3) return b; + const vc = d1 * d4 - d3 * d2; + if (vc <= 0 && d1 >= 0 && d3 <= 0) { + const v = d1 / (d1 - d3); + return [a[0] + ab[0] * v, a[1] + ab[1] * v, a[2] + ab[2] * v]; + } + const cp = sub(p, c); + const d5 = dot(ab, cp); + const d6 = dot(ac, cp); + if (d6 >= 0 && d5 <= d6) return c; + const vb = d5 * d2 - d1 * d6; + if (vb <= 0 && d2 >= 0 && d6 <= 0) { + const w = d2 / (d2 - d6); + return [a[0] + ac[0] * w, a[1] + ac[1] * w, a[2] + ac[2] * w]; + } + const va = d3 * d6 - d5 * d4; + if (va <= 0 && d4 - d3 >= 0 && d5 - d6 >= 0) { + const w = (d4 - d3) / (d4 - d3 + (d5 - d6)); + return [ + b[0] + (c[0] - b[0]) * w, + b[1] + (c[1] - b[1]) * w, + b[2] + (c[2] - b[2]) * w, + ]; + } + const den = 1 / (va + vb + vc); + const v = vb * den; + const w = vc * den; + return [ + a[0] + ab[0] * v + ac[0] * w, + a[1] + ab[1] * v + ac[1] * w, + a[2] + ab[2] * v + ac[2] * w, + ]; +} + +/** How far a point lies from the nearest triangle of the mesh. */ +export function distanceToSurface(p, verts, tris) { + let best = Infinity; + for (const [i, j, k] of tris) { + const d = dist3(p, closestOnTriangle(p, verts[i], verts[j], verts[k])); + if (d < best) best = d; + } + return best; +} + +/** The segments where the mesh's triangles cross the z = 0 (centreline) plane. */ +export function centrelineSegments(verts, tris) { + const segments = []; + for (const [i, j, k] of tris) { + const t = [verts[i], verts[j], verts[k]]; + const points = []; + for (let e = 0; e < 3; e++) { + const a = t[e]; + const b = t[(e + 1) % 3]; + if (a[2] === 0) points.push(a); + if ((a[2] < 0 && b[2] > 0) || (a[2] > 0 && b[2] < 0)) { + const s = a[2] / (a[2] - b[2]); + points.push([a[0] + (b[0] - a[0]) * s, a[1] + (b[1] - a[1]) * s, 0]); + } + } + if (points.length >= 2) segments.push([points[0], points[1]]); + } + return segments; +} + +/** Closest point of the centreline segments to `target`, or null. */ +export function closestCentrelinePoint(target, segments) { + let best = null; + let bestD = Infinity; + for (const [a, b] of segments) { + const ab = [b[0] - a[0], b[1] - a[1], b[2] - a[2]]; + const den = ab[0] * ab[0] + ab[1] * ab[1] + ab[2] * ab[2]; + let q = a; + if (den > 0) { + let s = + ((target[0] - a[0]) * ab[0] + + (target[1] - a[1]) * ab[1] + + (target[2] - a[2]) * ab[2]) / + den; + s = Math.max(0, Math.min(1, s)); + q = [a[0] + ab[0] * s, a[1] + ab[1] * s, a[2] + ab[2] * s]; + } + const d = dist3(target, q); + if (d < bestD) { + bestD = d; + best = q; + } + } + return best; +} + +/** + * The trail attachment point a model's construction gives: the point of its + * centreline profile nearest its aft-belly bounding-box corner, in scene + * space (raw glTF axes, metres). Null when the centreline crosses no triangle. + * @param {{verts: number[][], tris: number[][]}} mesh + * @returns {number[]|null} + */ +export function measuredTrailAnchor({ verts, tris }) { + const { min, max } = vertexBounds(verts); + return closestCentrelinePoint( + [max[0], min[1], 0], + centrelineSegments(verts, tris), + ); +} + +/** The repository's `public/` directory, where the fleet set is served from. */ +const PUBLIC_DIR = new URL('../../public/', import.meta.url); + +/** + * A `fetch` that serves the repository's `public/` files from disk, for + * loading the fleet model set in Node. + * @param {string} url Site path, e.g. `/models/fleet/catalog.json`. + * @returns {Promise<{ok: boolean, status: number, json: () => Promise, + * arrayBuffer: () => Promise}>} + */ +export async function fetchPublicFile(url) { + const file = new URL(String(url).replace(/^\//, ''), PUBLIC_DIR); + let body; + try { + body = fs.readFileSync(file); + } catch { + return { ok: false, status: 404 }; + } + return { + ok: true, + status: 200, + json: async () => JSON.parse(body.toString('utf8')), + arrayBuffer: async () => + body.buffer.slice(body.byteOffset, body.byteOffset + body.byteLength), + }; +}