@woosh/meep-engine 2.168.0 → 2.169.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/build/bundle-worker-image-decoder.js +1 -1
- package/package.json +1 -1
- package/src/REVIEW_2026_08_06/adv-core.md +511 -0
- package/src/REVIEW_2026_08_06/adv-gfx.md +480 -0
- package/src/REVIEW_2026_08_06/adv-view.md +446 -0
- package/src/REVIEW_2026_08_06/api-core.md +296 -0
- package/src/REVIEW_2026_08_06/api-engine.md +195 -0
- package/src/REVIEW_2026_08_06/bvh-phys-sound-net.md +507 -0
- package/src/REVIEW_2026_08_06/core-binary.md +348 -0
- package/src/REVIEW_2026_08_06/core-collection.md +376 -0
- package/src/REVIEW_2026_08_06/core-math.md +221 -0
- package/src/REVIEW_2026_08_06/core-process.md +266 -0
- package/src/REVIEW_2026_08_06/core-science.md +223 -0
- package/src/REVIEW_2026_08_06/engine-ai-nav.md +279 -0
- package/src/REVIEW_2026_08_06/engine-animation.md +167 -0
- package/src/REVIEW_2026_08_06/engine-asset.md +166 -0
- package/src/REVIEW_2026_08_06/engine-ecs.md +920 -0
- package/src/REVIEW_2026_08_06/engine-input.md +197 -0
- package/src/REVIEW_2026_08_06/engine-misc.md +248 -0
- package/src/REVIEW_2026_08_06/engine-terrain.md +52 -0
- package/src/REVIEW_2026_08_06/generation.md +176 -0
- package/src/REVIEW_2026_08_06/geom-2d.md +434 -0
- package/src/REVIEW_2026_08_06/geom-3d.md +117 -0
- package/src/REVIEW_2026_08_06/gfx-gi.md +143 -0
- package/src/REVIEW_2026_08_06/gfx-render.md +740 -0
- package/src/REVIEW_2026_08_06/gfx-texture.md +690 -0
- package/src/REVIEW_2026_08_06/prior-criticals.md +49 -0
- package/src/REVIEW_2026_08_06/prior-highs.md +138 -0
- package/src/REVIEW_2026_08_06/test-gaps.md +364 -0
- package/src/REVIEW_2026_08_06/view.md +667 -0
- package/src/REVIEW_2026_08_06.md +571 -0
- package/src/core/binary/BinaryBuffer.d.ts.map +1 -1
- package/src/core/binary/BinaryBuffer.js +12 -1
- package/src/core/binary/BitImage2.d.ts +5 -1
- package/src/core/binary/BitImage2.d.ts.map +1 -1
- package/src/core/binary/BitImage2.js +5 -1
- package/src/core/bvh8/build/NodeProxy.d.ts.map +1 -1
- package/src/core/bvh8/build/NodeProxy.js +314 -308
- package/src/core/collection/list/List.d.ts +15 -0
- package/src/core/collection/list/List.d.ts.map +1 -1
- package/src/core/collection/list/List.js +34 -0
- package/src/core/collection/list/SortedListProjection.d.ts.map +1 -1
- package/src/core/collection/list/SortedListProjection.js +4 -1
- package/src/core/collection/map/BiMap.d.ts +25 -2
- package/src/core/collection/map/BiMap.d.ts.map +1 -1
- package/src/core/collection/map/BiMap.js +57 -3
- package/src/core/debug/matchers/IsAnything.d.ts.map +1 -1
- package/src/core/debug/matchers/IsAnything.js +5 -1
- package/src/core/events/signal/Signal.d.ts.map +1 -1
- package/src/core/events/signal/Signal.js +3 -1
- package/src/core/geom/2d/polygon/TRIANGULATION_DESIGN.md +433 -0
- package/src/core/geom/2d/polygon/polygon2_is_counter_clockwise.d.ts +31 -0
- package/src/core/geom/2d/polygon/polygon2_is_counter_clockwise.d.ts.map +1 -0
- package/src/core/geom/2d/polygon/polygon2_is_counter_clockwise.js +84 -0
- package/src/core/geom/2d/polygon/polygon2_signed_area.d.ts +24 -2
- package/src/core/geom/2d/polygon/polygon2_signed_area.d.ts.map +1 -1
- package/src/core/geom/2d/polygon/polygon2_signed_area.js +52 -34
- package/src/core/geom/2d/polygon/polygon2_triangulate.corpus.d.ts +28 -0
- package/src/core/geom/2d/polygon/polygon2_triangulate.corpus.d.ts.map +1 -0
- package/src/core/geom/2d/polygon/polygon2_triangulate.corpus.js +86 -0
- package/src/core/geom/2d/polygon/polygon2_triangulate.d.ts +40 -0
- package/src/core/geom/2d/polygon/polygon2_triangulate.d.ts.map +1 -0
- package/src/core/geom/2d/polygon/polygon2_triangulate.js +1280 -0
- package/src/core/geom/2d/polygon/polygon2_triangulation_deviation.d.ts +28 -0
- package/src/core/geom/2d/polygon/polygon2_triangulation_deviation.d.ts.map +1 -0
- package/src/core/geom/2d/polygon/polygon2_triangulation_deviation.js +77 -0
- package/src/core/geom/2d/triangle/tri2_rasterize_conservative.d.ts +6 -3
- package/src/core/geom/2d/triangle/tri2_rasterize_conservative.d.ts.map +1 -1
- package/src/core/geom/2d/triangle/tri2_rasterize_conservative.js +26 -13
- package/src/core/geom/2d/triangle/tri2_signed_area.d.ts +17 -1
- package/src/core/geom/2d/triangle/tri2_signed_area.d.ts.map +1 -1
- package/src/core/geom/2d/triangle/tri2_signed_area.js +35 -23
- package/src/core/geom/2d/v2_morton_encode.d.ts +20 -0
- package/src/core/geom/2d/v2_morton_encode.d.ts.map +1 -0
- package/src/core/geom/2d/v2_morton_encode.js +23 -0
- package/src/core/geom/3d/atlas/segment/atlas_merge_charts.d.ts.map +1 -1
- package/src/core/geom/3d/atlas/segment/atlas_merge_charts.js +4 -1
- package/src/core/geom/3d/polygon/polygon3_compute_normal.d.ts +27 -0
- package/src/core/geom/3d/polygon/polygon3_compute_normal.d.ts.map +1 -0
- package/src/core/geom/3d/polygon/polygon3_compute_normal.js +60 -0
- package/src/core/geom/3d/polygon/polygon3_triangulate.d.ts +34 -0
- package/src/core/geom/3d/polygon/polygon3_triangulate.d.ts.map +1 -0
- package/src/core/geom/3d/polygon/polygon3_triangulate.js +94 -0
- package/src/core/geom/3d/quaternion/quat3_multiply.d.ts +8 -2
- package/src/core/geom/3d/quaternion/quat3_multiply.d.ts.map +1 -1
- package/src/core/geom/3d/quaternion/quat3_multiply.js +8 -2
- package/src/core/geom/3d/shape/ConvexHullShape3D.d.ts.map +1 -1
- package/src/core/geom/3d/shape/ConvexHullShape3D.js +32 -2
- package/src/core/geom/3d/topology/struct/binary/io/face/bt_face_triangulate.d.ts +23 -6
- package/src/core/geom/3d/topology/struct/binary/io/face/bt_face_triangulate.d.ts.map +1 -1
- package/src/core/geom/3d/topology/struct/binary/io/face/bt_face_triangulate.js +268 -124
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_assert_valid.d.ts +17 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_assert_valid.d.ts.map +1 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_assert_valid.js +44 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_count_edge_faces.d.ts +20 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_count_edge_faces.d.ts.map +1 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_count_edge_faces.js +40 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_is_closed.d.ts +19 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_is_closed.d.ts.map +1 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_is_closed.js +34 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_is_manifold.d.ts +22 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_is_manifold.d.ts.map +1 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_is_manifold.js +37 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_validate.d.ts +87 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_validate.d.ts.map +1 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_validate.js +599 -0
- package/src/core/geom/3d/triangle/tri3_compute_signed_volume.d.ts +32 -0
- package/src/core/geom/3d/triangle/tri3_compute_signed_volume.d.ts.map +1 -1
- package/src/core/geom/3d/triangle/tri3_compute_signed_volume.js +66 -7
- package/src/core/geom/3d/triangle/tri3_mesh_compute_signed_volume.d.ts +22 -1
- package/src/core/geom/3d/triangle/tri3_mesh_compute_signed_volume.d.ts.map +1 -1
- package/src/core/geom/3d/triangle/tri3_mesh_compute_signed_volume.js +83 -38
- package/src/core/geom/MANIFOLD_COMPARISON_2026_08_01.md +554 -0
- package/src/core/geom/vec3/v3_cotangent.d.ts +6 -2
- package/src/core/geom/vec3/v3_cotangent.d.ts.map +1 -1
- package/src/core/geom/vec3/v3_cotangent.js +6 -2
- package/src/core/geom/vec3/v3_cross.d.ts +6 -2
- package/src/core/geom/vec3/v3_cross.d.ts.map +1 -1
- package/src/core/geom/vec3/v3_cross.js +6 -2
- package/src/core/geom/vec3/v3_multiply.d.ts +6 -2
- package/src/core/geom/vec3/v3_multiply.d.ts.map +1 -1
- package/src/core/geom/vec3/v3_multiply.js +6 -2
- package/src/core/geom/vec3/v3_subtract.d.ts +6 -2
- package/src/core/geom/vec3/v3_subtract.d.ts.map +1 -1
- package/src/core/geom/vec3/v3_subtract.js +6 -2
- package/src/core/localization/Localization.js +1 -1
- package/src/core/math/random/randomSeed.d.ts +13 -0
- package/src/core/math/random/randomSeed.d.ts.map +1 -0
- package/src/core/math/random/randomSeed.js +16 -0
- package/src/core/math/spline/spline3_hermite_apply_transform.d.ts +50 -0
- package/src/core/math/spline/spline3_hermite_apply_transform.d.ts.map +1 -0
- package/src/core/math/spline/spline3_hermite_apply_transform.js +107 -0
- package/src/core/math/spline/spline3_hermite_reverse.d.ts +34 -0
- package/src/core/math/spline/spline3_hermite_reverse.d.ts.map +1 -0
- package/src/core/math/spline/spline3_hermite_reverse.js +49 -0
- package/src/core/math/statistics/hammersley_sequence.d.ts +17 -4
- package/src/core/math/statistics/hammersley_sequence.d.ts.map +1 -1
- package/src/core/math/statistics/hammersley_sequence.js +25 -16
- package/src/core/process/Future.d.ts.map +1 -1
- package/src/core/process/Future.js +12 -0
- package/src/core/process/PromiseWatcher.d.ts.map +1 -1
- package/src/core/process/PromiseWatcher.js +9 -2
- package/src/core/process/executor/ConcurrentExecutor.d.ts.map +1 -1
- package/src/core/process/executor/ConcurrentExecutor.js +12 -2
- package/src/core/process/worker/WorkerProxy.d.ts.map +1 -1
- package/src/core/process/worker/WorkerProxy.js +5 -2
- package/src/core/process/worker/extractTransferables.d.ts.map +1 -1
- package/src/core/process/worker/extractTransferables.js +4 -1
- package/src/engine/Engine.d.ts +12 -12
- package/src/engine/Engine.d.ts.map +1 -1
- package/src/engine/Engine.js +149 -10
- package/src/engine/EngineHarness.d.ts.map +1 -1
- package/src/engine/EngineHarness.js +4 -1
- package/src/engine/asset/loaders/ArrayBufferLoader.d.ts.map +1 -1
- package/src/engine/asset/loaders/ArrayBufferLoader.js +7 -0
- package/src/engine/ecs/speaker/lines/sets/LineSetDescription.d.ts.map +1 -1
- package/src/engine/ecs/speaker/lines/sets/LineSetDescription.js +3 -1
- package/src/engine/ecs/terrain/ecs/TerrainSystem.d.ts.map +1 -1
- package/src/engine/ecs/terrain/ecs/TerrainSystem.js +4 -1
- package/src/engine/ecs/terrain/tiles/TerrainTileManager.d.ts +29 -0
- package/src/engine/ecs/terrain/tiles/TerrainTileManager.d.ts.map +1 -1
- package/src/engine/ecs/terrain/tiles/TerrainTileManager.js +84 -23
- package/src/engine/graphics/CONTEXT_LOSS_RECOVERY_PLAN.md +446 -0
- package/src/engine/graphics/GraphicsEngine.d.ts +56 -7
- package/src/engine/graphics/GraphicsEngine.d.ts.map +1 -1
- package/src/engine/graphics/GraphicsEngine.js +196 -24
- package/src/engine/graphics/context/WebGLContextFailureReason.d.ts +9 -0
- package/src/engine/graphics/context/WebGLContextFailureReason.d.ts.map +1 -0
- package/src/engine/graphics/context/WebGLContextFailureReason.js +17 -0
- package/src/engine/graphics/context/WebGLContextMonitor.d.ts +106 -0
- package/src/engine/graphics/context/WebGLContextMonitor.d.ts.map +1 -0
- package/src/engine/graphics/context/WebGLContextMonitor.js +341 -0
- package/src/engine/graphics/context/WebGLContextState.d.ts +14 -0
- package/src/engine/graphics/context/WebGLContextState.d.ts.map +1 -0
- package/src/engine/graphics/context/WebGLContextState.js +24 -0
- package/src/engine/graphics/context/testWebGLContextLoss.d.ts +2 -0
- package/src/engine/graphics/context/testWebGLContextLoss.d.ts.map +1 -0
- package/src/engine/graphics/context/testWebGLContextLoss.js +696 -0
- package/src/engine/graphics/ecs/camera/serialization/CameraSerializationAdapter.d.ts.map +1 -1
- package/src/engine/graphics/ecs/camera/serialization/CameraSerializationAdapter.js +68 -32
- package/src/engine/graphics/ecs/camera/serialization/CameraSerializationUpgrader_0_1.d.ts +14 -0
- package/src/engine/graphics/ecs/camera/serialization/CameraSerializationUpgrader_0_1.d.ts.map +1 -0
- package/src/engine/graphics/ecs/camera/serialization/CameraSerializationUpgrader_0_1.js +41 -0
- package/src/engine/graphics/ecs/decal/v2/FPDecalSystem.js +1 -1
- package/src/engine/graphics/ecs/mesh-v2/ShadedGeometrySystem.d.ts.map +1 -1
- package/src/engine/graphics/ecs/mesh-v2/ShadedGeometrySystem.js +5 -0
- package/src/engine/graphics/ecs/trail2d/Trail2D.d.ts +5 -1
- package/src/engine/graphics/ecs/trail2d/Trail2D.d.ts.map +1 -1
- package/src/engine/graphics/ecs/trail2d/Trail2D.js +15 -0
- package/src/engine/graphics/ecs/trail2d/Trail2DSystem.d.ts +1 -1
- package/src/engine/graphics/ecs/trail2d/Trail2DSystem.d.ts.map +1 -1
- package/src/engine/graphics/ecs/trail2d/Trail2DSystem.js +2 -1
- package/src/engine/graphics/ecs/trail3d/Trail3D.d.ts +4 -1
- package/src/engine/graphics/ecs/trail3d/Trail3D.d.ts.map +1 -1
- package/src/engine/graphics/ecs/trail3d/Trail3D.js +488 -476
- package/src/engine/graphics/ecs/trail3d/Trail3DSystem.d.ts +1 -1
- package/src/engine/graphics/ecs/trail3d/Trail3DSystem.d.ts.map +1 -1
- package/src/engine/graphics/ecs/trail3d/Trail3DSystem.js +280 -253
- package/src/engine/graphics/geometry/MikkT/BuildNeighborsFast.d.ts.map +1 -1
- package/src/engine/graphics/geometry/MikkT/BuildNeighborsFast.js +3 -2
- package/src/engine/graphics/geometry/MikkT/GenerateSharedVerticesIndexList.d.ts.map +1 -1
- package/src/engine/graphics/geometry/MikkT/GenerateSharedVerticesIndexList.js +3 -1
- package/src/engine/graphics/geometry/computeMeshSurfaceArea.js +1 -1
- package/src/engine/graphics/load_and_set_cubemap_v0.d.ts +5 -0
- package/src/engine/graphics/load_and_set_cubemap_v0.d.ts.map +1 -1
- package/src/engine/graphics/load_and_set_cubemap_v0.js +26 -21
- package/src/engine/graphics/particles/particular/group/ParticleGroup.d.ts.map +1 -1
- package/src/engine/graphics/particles/particular/group/ParticleGroup.js +19 -8
- package/src/engine/graphics/particles/particular/group/optimizeCommandQueue.js +1 -1
- package/src/engine/graphics/texture/atlas/AtlasLookupTexture.d.ts.map +1 -1
- package/src/engine/graphics/texture/atlas/AtlasLookupTexture.js +10 -3
- package/src/engine/graphics/texture/atlas/TextureAtlas.d.ts.map +1 -1
- package/src/engine/graphics/texture/atlas/TextureAtlas.js +4 -1
- package/src/engine/graphics/texture/atlas/gpu/WebGLTextureAtlas.d.ts.map +1 -1
- package/src/engine/graphics/texture/atlas/gpu/WebGLTextureAtlas.js +11 -0
- package/src/engine/graphics/texture/cubemap/load_environment_map.d.ts.map +1 -1
- package/src/engine/graphics/texture/cubemap/load_environment_map.js +5 -2
- package/src/engine/graphics/texture/sampler/distance/computeSignedDistanceField_Chamfer.d.ts.map +1 -1
- package/src/engine/graphics/texture/sampler/distance/computeSignedDistanceField_Chamfer.js +26 -18
- package/src/engine/graphics/texture/sampler/filter/sampler2d_blur_gaussian.d.ts +15 -2
- package/src/engine/graphics/texture/sampler/filter/sampler2d_blur_gaussian.d.ts.map +1 -1
- package/src/engine/graphics/texture/sampler/filter/sampler2d_blur_gaussian.js +29 -3
- package/src/engine/graphics/texture/sampler/resize/sampler2d_scale_down_lanczos.d.ts +11 -1
- package/src/engine/graphics/texture/sampler/resize/sampler2d_scale_down_lanczos.d.ts.map +1 -1
- package/src/engine/graphics/texture/sampler/resize/sampler2d_scale_down_lanczos.js +158 -171
- package/src/engine/graphics/texture/sampler/sampler2d_paint.js +1 -1
- package/src/engine/graphics/texture/sampler/util/bitSet2Sampler2D.js +2 -2
- package/src/engine/input/ecs/util/TopDownCameraControllerHelper.d.ts.map +1 -1
- package/src/engine/input/ecs/util/TopDownCameraControllerHelper.js +10 -3
- package/src/engine/navigation/ecs/components/PathSerializationUpgrader_2_3.d.ts +14 -0
- package/src/engine/navigation/ecs/components/PathSerializationUpgrader_2_3.d.ts.map +1 -0
- package/src/engine/navigation/ecs/components/PathSerializationUpgrader_2_3.js +38 -0
- package/src/engine/network/orchestrator/ServerAuthoritativeClient.d.ts.map +1 -1
- package/src/engine/network/orchestrator/ServerAuthoritativeClient.js +433 -425
- package/src/engine/physics/constraint/solve_constraints.d.ts +4 -1
- package/src/engine/physics/constraint/solve_constraints.d.ts.map +1 -1
- package/src/engine/physics/constraint/solve_constraints.js +38 -13
- package/src/engine/physics/contact/ManifoldStore.d.ts +15 -3
- package/src/engine/physics/contact/ManifoldStore.d.ts.map +1 -1
- package/src/engine/physics/contact/ManifoldStore.js +15 -3
- package/src/engine/physics/ecs/Joint.d.ts +7 -4
- package/src/engine/physics/ecs/Joint.d.ts.map +1 -1
- package/src/engine/physics/ecs/Joint.js +7 -4
- package/src/engine/physics/ecs/PhysicsSystem.d.ts +14 -0
- package/src/engine/physics/ecs/PhysicsSystem.d.ts.map +1 -1
- package/src/engine/physics/ecs/PhysicsSystem.js +16 -0
- package/src/engine/physics/fluid/ecs/FluidObstacleSystem.d.ts +4 -4
- package/src/engine/physics/fluid/ecs/FluidSystem.d.ts +3 -3
- package/src/engine/physics/narrowphase/convex_convex_manifold.d.ts +15 -7
- package/src/engine/physics/narrowphase/convex_convex_manifold.d.ts.map +1 -1
- package/src/engine/physics/narrowphase/convex_convex_manifold.js +33 -10
- package/src/engine/physics/narrowphase/refine_ray_concave.d.ts +6 -2
- package/src/engine/physics/narrowphase/refine_ray_concave.d.ts.map +1 -1
- package/src/engine/physics/narrowphase/refine_ray_concave.js +6 -2
- package/src/engine/physics/narrowphase/refine_ray_hit.d.ts +6 -2
- package/src/engine/physics/narrowphase/refine_ray_hit.d.ts.map +1 -1
- package/src/engine/physics/narrowphase/refine_ray_hit.js +6 -2
- package/src/engine/physics/queries/raycast.d.ts.map +1 -1
- package/src/engine/physics/queries/raycast.js +11 -4
- package/src/engine/save/storage/IndexedDBStorage.d.ts.map +1 -1
- package/src/engine/save/storage/IndexedDBStorage.js +21 -3
- package/src/engine/simulation/Ticker.d.ts +12 -0
- package/src/engine/simulation/Ticker.d.ts.map +1 -1
- package/src/engine/simulation/Ticker.js +18 -0
- package/src/engine/sound/simulation/AcousticSimulator.d.ts.map +1 -1
- package/src/engine/sound/simulation/AcousticSimulator.js +9 -1
- package/src/engine/sound/simulation/core/VolumeField.d.ts +6 -2
- package/src/engine/sound/simulation/core/VolumeField.d.ts.map +1 -1
- package/src/engine/sound/simulation/core/VolumeField.js +6 -2
- package/src/engine/sound/simulation/probe/AcousticProbeField.d.ts +6 -2
- package/src/engine/sound/simulation/probe/AcousticProbeField.d.ts.map +1 -1
- package/src/engine/sound/simulation/probe/AcousticProbeField.js +6 -2
- package/src/engine/sound/simulation/probe/acoustic_probe_transfer.d.ts +6 -2
- package/src/engine/sound/simulation/probe/acoustic_probe_transfer.d.ts.map +1 -1
- package/src/engine/sound/simulation/probe/acoustic_probe_transfer.js +6 -2
- package/src/engine/sound/simulation/probe/bakeProbeReflectors.d.ts +3 -1
- package/src/engine/sound/simulation/probe/bakeProbeReflectors.d.ts.map +1 -1
- package/src/engine/sound/simulation/probe/bakeProbeReflectors.js +3 -1
- package/src/engine/ui/GUIEngine.d.ts.map +1 -1
- package/src/engine/ui/GUIEngine.js +8 -1
- package/src/generation/theme/TerrainTheme.d.ts.map +1 -1
- package/src/generation/theme/TerrainTheme.js +2 -1
- package/src/generation/theme/ThemeEngine.d.ts.map +1 -1
- package/src/generation/theme/ThemeEngine.js +6 -5
- package/src/view/ViewGroup.d.ts.map +1 -1
- package/src/view/ViewGroup.js +9 -7
- package/src/view/graphics/WebGLContextFailureView.d.ts +19 -0
- package/src/view/graphics/WebGLContextFailureView.d.ts.map +1 -0
- package/src/view/graphics/WebGLContextFailureView.js +76 -0
- package/src/view/minimap/Minimap.d.ts +8 -0
- package/src/view/minimap/Minimap.d.ts.map +1 -1
- package/src/view/minimap/Minimap.js +16 -3
- package/src/view/minimap/dom/MinimapMarkerView.d.ts.map +1 -1
- package/src/view/minimap/dom/MinimapMarkerView.js +3 -1
|
@@ -0,0 +1,1280 @@
|
|
|
1
|
+
import { assert } from "../../../assert.js";
|
|
2
|
+
import { UINT32_MAX } from "../../../binary/UINT32_MAX.js";
|
|
3
|
+
import { orient2d_indexed } from "../convex-hull/orient2d_indexed.js";
|
|
4
|
+
import { v2_morton_encode } from "../v2_morton_encode.js";
|
|
5
|
+
import { polygon2_is_counter_clockwise } from "./polygon2_is_counter_clockwise.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Sentinel for "no node", matching the codebase-wide typed-array pointer
|
|
9
|
+
* convention (`NULL_POINTER` in {@link BinaryTopology} is the same value).
|
|
10
|
+
* @type {number}
|
|
11
|
+
*/
|
|
12
|
+
const NULL_NODE = UINT32_MAX;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Vertex count above which the point-in-ear test switches from a full ring walk
|
|
16
|
+
* to a Morton-code range walk. Below it the ring is short enough that the walk is
|
|
17
|
+
* cheaper than maintaining the Z-order list; above it the walk is the entire cost
|
|
18
|
+
* of the algorithm.
|
|
19
|
+
*
|
|
20
|
+
* Not a public option: both paths compute the same triangulation, so exposing the
|
|
21
|
+
* choice would be a knob with no semantics. Tests assert the two agree.
|
|
22
|
+
* @type {number}
|
|
23
|
+
*/
|
|
24
|
+
const HASH_THRESHOLD = 80;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Largest quantised coordinate fed to {@link v2_morton_encode}. 15 bits per axis,
|
|
28
|
+
* matching the resolution the Z-order range test can usefully distinguish.
|
|
29
|
+
* @type {number}
|
|
30
|
+
*/
|
|
31
|
+
const MORTON_SCALE = 32767;
|
|
32
|
+
|
|
33
|
+
// --------------------------------------------------------------------------
|
|
34
|
+
// Ring storage.
|
|
35
|
+
//
|
|
36
|
+
// A vertex ring is a doubly-linked list held in flat typed arrays with integer
|
|
37
|
+
// handles -- the same shape as every other meep structure of this kind
|
|
38
|
+
// (`BinaryElementPool`, `BinaryTopology`, `bvh8`), and unlike the object-node
|
|
39
|
+
// lists the reference implementations of this algorithm use. Nodes are never
|
|
40
|
+
// compacted: removal unlinks, so handles stay stable for the whole call.
|
|
41
|
+
//
|
|
42
|
+
// Coordinates are copied in rather than read back through `points`, for three
|
|
43
|
+
// reasons: the bridge nodes created by hole elimination have no entry in
|
|
44
|
+
// `points`; `orient2d_indexed` wants exactly this interleaved layout; and it
|
|
45
|
+
// keeps no reference to caller memory alive.
|
|
46
|
+
//
|
|
47
|
+
// Module-static and grown on demand, so a call allocates nothing in the steady
|
|
48
|
+
// state. The consequence -- documented on the public function -- is that
|
|
49
|
+
// `polygon2_triangulate` is NOT re-entrant, same as `bt_mesh_simplify_by_error`.
|
|
50
|
+
// --------------------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
/** Interleaved (x, y) per node. The array `orient2d_indexed` is applied to. */
|
|
53
|
+
let _node_xy = new Float64Array(0);
|
|
54
|
+
/** Index into the caller's `points` that this node reports on output. */
|
|
55
|
+
let _node_point = new Uint32Array(0);
|
|
56
|
+
let _node_next = new Uint32Array(0);
|
|
57
|
+
let _node_prev = new Uint32Array(0);
|
|
58
|
+
/** Morton code, only meaningful while the Z-order list is live. */
|
|
59
|
+
let _node_z = new Uint32Array(0);
|
|
60
|
+
let _node_znext = new Uint32Array(0);
|
|
61
|
+
let _node_zprev = new Uint32Array(0);
|
|
62
|
+
/**
|
|
63
|
+
* 1 for a node introduced by hole elimination rather than by the caller. Steiner
|
|
64
|
+
* nodes are exempt from the degenerate-point filter: they are deliberately
|
|
65
|
+
* coincident with another node and removing them would undo the bridge.
|
|
66
|
+
*/
|
|
67
|
+
let _node_steiner = new Uint8Array(0);
|
|
68
|
+
|
|
69
|
+
/** Allocation cursor into the node arrays. */
|
|
70
|
+
let _node_count = 0;
|
|
71
|
+
|
|
72
|
+
/** Output buffer and write cursor for the current call. */
|
|
73
|
+
let _out = null;
|
|
74
|
+
let _out_cursor = 0;
|
|
75
|
+
|
|
76
|
+
/** Morton quantisation state for the current call. */
|
|
77
|
+
let _min_x = 0;
|
|
78
|
+
let _min_y = 0;
|
|
79
|
+
let _inv_size = 0;
|
|
80
|
+
/** True while the Z-order accelerated ear test is in use. */
|
|
81
|
+
let _hashed = false;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* @param {number} capacity
|
|
85
|
+
*/
|
|
86
|
+
function ensure_node_capacity(capacity) {
|
|
87
|
+
if (_node_point.length >= capacity) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
_node_xy = new Float64Array(capacity * 2);
|
|
92
|
+
_node_point = new Uint32Array(capacity);
|
|
93
|
+
_node_next = new Uint32Array(capacity);
|
|
94
|
+
_node_prev = new Uint32Array(capacity);
|
|
95
|
+
_node_z = new Uint32Array(capacity);
|
|
96
|
+
_node_znext = new Uint32Array(capacity);
|
|
97
|
+
_node_zprev = new Uint32Array(capacity);
|
|
98
|
+
_node_steiner = new Uint8Array(capacity);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* @param {number} point_index
|
|
103
|
+
* @param {number} x
|
|
104
|
+
* @param {number} y
|
|
105
|
+
* @returns {number} node
|
|
106
|
+
*/
|
|
107
|
+
function create_node(point_index, x, y) {
|
|
108
|
+
const node = _node_count++;
|
|
109
|
+
|
|
110
|
+
_node_xy[node * 2] = x;
|
|
111
|
+
_node_xy[node * 2 + 1] = y;
|
|
112
|
+
_node_point[node] = point_index;
|
|
113
|
+
_node_next[node] = NULL_NODE;
|
|
114
|
+
_node_prev[node] = NULL_NODE;
|
|
115
|
+
_node_z[node] = 0;
|
|
116
|
+
_node_znext[node] = NULL_NODE;
|
|
117
|
+
_node_zprev[node] = NULL_NODE;
|
|
118
|
+
_node_steiner[node] = 0;
|
|
119
|
+
|
|
120
|
+
return node;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Append a node after `last` in the ring, or start a new ring when `last` is
|
|
125
|
+
* {@link NULL_NODE}.
|
|
126
|
+
*
|
|
127
|
+
* @param {number} point_index
|
|
128
|
+
* @param {number} x
|
|
129
|
+
* @param {number} y
|
|
130
|
+
* @param {number} last
|
|
131
|
+
* @returns {number} the new node
|
|
132
|
+
*/
|
|
133
|
+
function insert_node(point_index, x, y, last) {
|
|
134
|
+
const node = create_node(point_index, x, y);
|
|
135
|
+
|
|
136
|
+
if (last === NULL_NODE) {
|
|
137
|
+
_node_next[node] = node;
|
|
138
|
+
_node_prev[node] = node;
|
|
139
|
+
|
|
140
|
+
return node;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const after = _node_next[last];
|
|
144
|
+
|
|
145
|
+
_node_next[node] = after;
|
|
146
|
+
_node_prev[node] = last;
|
|
147
|
+
_node_prev[after] = node;
|
|
148
|
+
_node_next[last] = node;
|
|
149
|
+
|
|
150
|
+
return node;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Unlink `node` from the ring and from the Z-order list.
|
|
155
|
+
* @param {number} node
|
|
156
|
+
*/
|
|
157
|
+
function remove_node(node) {
|
|
158
|
+
const next = _node_next[node];
|
|
159
|
+
const prev = _node_prev[node];
|
|
160
|
+
|
|
161
|
+
_node_prev[next] = prev;
|
|
162
|
+
_node_next[prev] = next;
|
|
163
|
+
|
|
164
|
+
const zprev = _node_zprev[node];
|
|
165
|
+
const znext = _node_znext[node];
|
|
166
|
+
|
|
167
|
+
if (zprev !== NULL_NODE) {
|
|
168
|
+
_node_znext[zprev] = znext;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (znext !== NULL_NODE) {
|
|
172
|
+
_node_zprev[znext] = zprev;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function x_of(node) {
|
|
177
|
+
return _node_xy[node * 2];
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function y_of(node) {
|
|
181
|
+
return _node_xy[node * 2 + 1];
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Exact orientation of the node triple: **positive = counter-clockwise**, zero =
|
|
186
|
+
* collinear. Every combinatorial decision in this file routes through here, so
|
|
187
|
+
* the triangulation is decided exactly regardless of coordinate magnitude.
|
|
188
|
+
*
|
|
189
|
+
* Note the negation. Shewchuk's `orient2d` -- and therefore `robust-predicates`
|
|
190
|
+
* and {@link orient2d_indexed} -- returns a *negative* value for a
|
|
191
|
+
* counter-clockwise triple with y up (`orient2d(0,0, 1,0, 0,1) === -1`), and
|
|
192
|
+
* `-0` for collinear. Negating a double flips the sign exactly, so this keeps the
|
|
193
|
+
* predicate exact while giving this file the conventional sign. Reading the
|
|
194
|
+
* inverted convention into every comparison below would be a standing invitation
|
|
195
|
+
* to get one of them backwards.
|
|
196
|
+
*
|
|
197
|
+
* @param {number} a
|
|
198
|
+
* @param {number} b
|
|
199
|
+
* @param {number} c
|
|
200
|
+
* @returns {number}
|
|
201
|
+
*/
|
|
202
|
+
function orient(a, b, c) {
|
|
203
|
+
return -orient2d_indexed(_node_xy, a, b, c);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* @param {number} a
|
|
208
|
+
* @param {number} b
|
|
209
|
+
* @returns {boolean} true when the two nodes sit at exactly the same point
|
|
210
|
+
*/
|
|
211
|
+
function nodes_equal(a, b) {
|
|
212
|
+
return _node_xy[a * 2] === _node_xy[b * 2] && _node_xy[a * 2 + 1] === _node_xy[b * 2 + 1];
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Build a ring from `points[from .. to)` (vertex indices), oriented so that its
|
|
217
|
+
* signed area has the sign `want_positive` asks for -- counter-clockwise for the
|
|
218
|
+
* outer contour, clockwise for holes.
|
|
219
|
+
*
|
|
220
|
+
* Normalising here rather than demanding it of the caller is deliberate: winding
|
|
221
|
+
* is derivable from the data, so requiring it would be redundant state the caller
|
|
222
|
+
* could contradict.
|
|
223
|
+
*
|
|
224
|
+
* @param {ArrayLike<number>} points
|
|
225
|
+
* @param {number} from first vertex index, inclusive
|
|
226
|
+
* @param {number} to last vertex index, exclusive
|
|
227
|
+
* @param {boolean} want_positive
|
|
228
|
+
* @returns {number} the last node of the ring, or {@link NULL_NODE} when empty
|
|
229
|
+
*/
|
|
230
|
+
function link_contour(points, from, to, want_positive) {
|
|
231
|
+
let last = NULL_NODE;
|
|
232
|
+
|
|
233
|
+
const count = to - from;
|
|
234
|
+
|
|
235
|
+
if (count <= 0) {
|
|
236
|
+
return NULL_NODE;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Exactly, via one orientation predicate at a hull vertex. The shoelace sum
|
|
240
|
+
// cannot be used here: for a small contour, or one far from the origin, its
|
|
241
|
+
// sign is rounding noise, and getting the winding backwards makes every corner
|
|
242
|
+
// look reflex and the whole contour untriangulatable.
|
|
243
|
+
const is_ccw = polygon2_is_counter_clockwise(points, from, count);
|
|
244
|
+
|
|
245
|
+
if (is_ccw === want_positive) {
|
|
246
|
+
for (let i = from; i < to; i++) {
|
|
247
|
+
last = insert_node(i, points[i * 2], points[i * 2 + 1], last);
|
|
248
|
+
}
|
|
249
|
+
} else {
|
|
250
|
+
for (let i = to - 1; i >= from; i--) {
|
|
251
|
+
last = insert_node(i, points[i * 2], points[i * 2 + 1], last);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (last !== NULL_NODE && nodes_equal(last, _node_next[last])) {
|
|
256
|
+
// the contour repeats its first point as its last
|
|
257
|
+
remove_node(last);
|
|
258
|
+
last = _node_next[last];
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return last;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Drop nodes that are coincident with their successor or collinear with both
|
|
266
|
+
* neighbours -- neither can contribute a triangle, and both stall the ear search.
|
|
267
|
+
* Steiner nodes are kept.
|
|
268
|
+
*
|
|
269
|
+
* @param {number} start
|
|
270
|
+
* @param {number} end node to finish at; pass {@link NULL_NODE} for `start`
|
|
271
|
+
* @returns {number} a surviving node, or {@link NULL_NODE} if the ring collapsed
|
|
272
|
+
*/
|
|
273
|
+
function filter_points(start, end) {
|
|
274
|
+
if (start === NULL_NODE) {
|
|
275
|
+
return NULL_NODE;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
let stop = end === NULL_NODE ? start : end;
|
|
279
|
+
|
|
280
|
+
let p = start;
|
|
281
|
+
let again;
|
|
282
|
+
|
|
283
|
+
do {
|
|
284
|
+
again = false;
|
|
285
|
+
|
|
286
|
+
const next = _node_next[p];
|
|
287
|
+
const prev = _node_prev[p];
|
|
288
|
+
|
|
289
|
+
if (_node_steiner[p] === 0 && (nodes_equal(p, next) || orient(prev, p, next) === 0)) {
|
|
290
|
+
remove_node(p);
|
|
291
|
+
|
|
292
|
+
p = prev;
|
|
293
|
+
stop = prev;
|
|
294
|
+
|
|
295
|
+
if (p === _node_next[p]) {
|
|
296
|
+
return NULL_NODE;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
again = true;
|
|
300
|
+
} else {
|
|
301
|
+
p = next;
|
|
302
|
+
}
|
|
303
|
+
} while (again || p !== stop);
|
|
304
|
+
|
|
305
|
+
return p;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Inclusive point-in-triangle for a counter-clockwise triangle (a, b, c).
|
|
310
|
+
*
|
|
311
|
+
* @param {number} a
|
|
312
|
+
* @param {number} b
|
|
313
|
+
* @param {number} c
|
|
314
|
+
* @param {number} p
|
|
315
|
+
* @returns {boolean}
|
|
316
|
+
*/
|
|
317
|
+
function point_in_triangle(a, b, c, p) {
|
|
318
|
+
return orient(a, b, p) >= 0 && orient(b, c, p) >= 0 && orient(c, a, p) >= 0;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* As {@link point_in_triangle}, but a point coincident with `a` does not count.
|
|
323
|
+
*
|
|
324
|
+
* Hole elimination bridges a hole into the outer ring by duplicating a vertex, so
|
|
325
|
+
* a point sitting exactly on `a` is an expected, legitimate configuration. Letting
|
|
326
|
+
* it block would make every bridged hole untriangulatable.
|
|
327
|
+
*
|
|
328
|
+
* @param {number} a
|
|
329
|
+
* @param {number} b
|
|
330
|
+
* @param {number} c
|
|
331
|
+
* @param {number} p
|
|
332
|
+
* @returns {boolean}
|
|
333
|
+
*/
|
|
334
|
+
function point_in_triangle_except_first(a, b, c, p) {
|
|
335
|
+
return !nodes_equal(a, p) && point_in_triangle(a, b, c, p);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Is the segment `a -> b` inside the polygon in the immediate neighbourhood of
|
|
340
|
+
* `a`? Splits on whether `a` is a convex or a reflex corner.
|
|
341
|
+
*
|
|
342
|
+
* @param {number} a
|
|
343
|
+
* @param {number} b
|
|
344
|
+
* @returns {boolean}
|
|
345
|
+
*/
|
|
346
|
+
function locally_inside(a, b) {
|
|
347
|
+
const prev = _node_prev[a];
|
|
348
|
+
const next = _node_next[a];
|
|
349
|
+
|
|
350
|
+
if (orient(prev, a, next) > 0) {
|
|
351
|
+
// convex corner: b must be inside the wedge
|
|
352
|
+
return orient(a, b, next) <= 0 && orient(a, prev, b) <= 0;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// reflex corner: b must be outside the (smaller) reflex wedge
|
|
356
|
+
return orient(a, b, prev) > 0 || orient(a, next, b) > 0;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Is the midpoint of `a -> b` inside the ring? Even-odd ray cast along +x.
|
|
361
|
+
*
|
|
362
|
+
* @param {number} a
|
|
363
|
+
* @param {number} b
|
|
364
|
+
* @returns {boolean}
|
|
365
|
+
*/
|
|
366
|
+
function middle_inside(a, b) {
|
|
367
|
+
const px = (x_of(a) + x_of(b)) / 2;
|
|
368
|
+
const py = (y_of(a) + y_of(b)) / 2;
|
|
369
|
+
|
|
370
|
+
let p = a;
|
|
371
|
+
let inside = false;
|
|
372
|
+
|
|
373
|
+
do {
|
|
374
|
+
const next = _node_next[p];
|
|
375
|
+
|
|
376
|
+
const py0 = y_of(p);
|
|
377
|
+
const py1 = y_of(next);
|
|
378
|
+
|
|
379
|
+
if (((py0 > py) !== (py1 > py))
|
|
380
|
+
&& py1 !== py0
|
|
381
|
+
&& (px < (x_of(next) - x_of(p)) * (py - py0) / (py1 - py0) + x_of(p))) {
|
|
382
|
+
inside = !inside;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
p = next;
|
|
386
|
+
} while (p !== a);
|
|
387
|
+
|
|
388
|
+
return inside;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Does `q` lie on the segment `p -> r`, given the three are already known to be
|
|
393
|
+
* collinear?
|
|
394
|
+
*
|
|
395
|
+
* @param {number} p
|
|
396
|
+
* @param {number} q
|
|
397
|
+
* @param {number} r
|
|
398
|
+
* @returns {boolean}
|
|
399
|
+
*/
|
|
400
|
+
function on_segment(p, q, r) {
|
|
401
|
+
return x_of(q) <= Math.max(x_of(p), x_of(r))
|
|
402
|
+
&& x_of(q) >= Math.min(x_of(p), x_of(r))
|
|
403
|
+
&& y_of(q) <= Math.max(y_of(p), y_of(r))
|
|
404
|
+
&& y_of(q) >= Math.min(y_of(p), y_of(r));
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* @param {number} value
|
|
409
|
+
* @returns {number} -1, 0 or 1
|
|
410
|
+
*/
|
|
411
|
+
function sign_of(value) {
|
|
412
|
+
return value > 0 ? 1 : value < 0 ? -1 : 0;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Do the segments `p1 -> q1` and `p2 -> q2` intersect, including collinear
|
|
417
|
+
* touching?
|
|
418
|
+
*
|
|
419
|
+
* @param {number} p1
|
|
420
|
+
* @param {number} q1
|
|
421
|
+
* @param {number} p2
|
|
422
|
+
* @param {number} q2
|
|
423
|
+
* @returns {boolean}
|
|
424
|
+
*/
|
|
425
|
+
function segments_intersect(p1, q1, p2, q2) {
|
|
426
|
+
const o1 = sign_of(orient(p1, q1, p2));
|
|
427
|
+
const o2 = sign_of(orient(p1, q1, q2));
|
|
428
|
+
const o3 = sign_of(orient(p2, q2, p1));
|
|
429
|
+
const o4 = sign_of(orient(p2, q2, q1));
|
|
430
|
+
|
|
431
|
+
if (o1 !== o2 && o3 !== o4) {
|
|
432
|
+
return true;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if (o1 === 0 && on_segment(p1, p2, q1)) {
|
|
436
|
+
return true;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (o2 === 0 && on_segment(p1, q2, q1)) {
|
|
440
|
+
return true;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
if (o3 === 0 && on_segment(p2, p1, q2)) {
|
|
444
|
+
return true;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
if (o4 === 0 && on_segment(p2, q1, q2)) {
|
|
448
|
+
return true;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
return false;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Does the candidate diagonal `a -> b` cross any edge of the ring?
|
|
456
|
+
*
|
|
457
|
+
* @param {number} a
|
|
458
|
+
* @param {number} b
|
|
459
|
+
* @returns {boolean}
|
|
460
|
+
*/
|
|
461
|
+
function intersects_polygon(a, b) {
|
|
462
|
+
let p = a;
|
|
463
|
+
|
|
464
|
+
do {
|
|
465
|
+
const next = _node_next[p];
|
|
466
|
+
|
|
467
|
+
if (_node_point[p] !== _node_point[a]
|
|
468
|
+
&& _node_point[next] !== _node_point[a]
|
|
469
|
+
&& _node_point[p] !== _node_point[b]
|
|
470
|
+
&& _node_point[next] !== _node_point[b]
|
|
471
|
+
&& segments_intersect(p, next, a, b)) {
|
|
472
|
+
return true;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
p = next;
|
|
476
|
+
} while (p !== a);
|
|
477
|
+
|
|
478
|
+
return false;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Is `a -> b` a diagonal that splits the ring into two simple sub-rings?
|
|
483
|
+
*
|
|
484
|
+
* @param {number} a
|
|
485
|
+
* @param {number} b
|
|
486
|
+
* @returns {boolean}
|
|
487
|
+
*/
|
|
488
|
+
function is_valid_diagonal(a, b) {
|
|
489
|
+
if (_node_point[_node_next[a]] === _node_point[b] || _node_point[_node_prev[a]] === _node_point[b]) {
|
|
490
|
+
return false;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
if (intersects_polygon(a, b)) {
|
|
494
|
+
return false;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const locally_visible = locally_inside(a, b)
|
|
498
|
+
&& locally_inside(b, a)
|
|
499
|
+
&& middle_inside(a, b)
|
|
500
|
+
// and does not produce two opposite-facing sectors
|
|
501
|
+
&& (orient(_node_prev[a], a, _node_prev[b]) !== 0 || orient(a, _node_prev[b], b) !== 0);
|
|
502
|
+
|
|
503
|
+
if (locally_visible) {
|
|
504
|
+
return true;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// zero-length diagonal between two coincident reflex corners still separates
|
|
508
|
+
return nodes_equal(a, b)
|
|
509
|
+
&& orient(_node_prev[a], a, _node_next[a]) < 0
|
|
510
|
+
&& orient(_node_prev[b], b, _node_next[b]) < 0;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Cut the ring in two along the diagonal `a -> b`, duplicating both endpoints.
|
|
515
|
+
*
|
|
516
|
+
* @param {number} a
|
|
517
|
+
* @param {number} b
|
|
518
|
+
* @returns {number} a node of the second ring
|
|
519
|
+
*/
|
|
520
|
+
function split_ring(a, b) {
|
|
521
|
+
const a2 = create_node(_node_point[a], x_of(a), y_of(a));
|
|
522
|
+
const b2 = create_node(_node_point[b], x_of(b), y_of(b));
|
|
523
|
+
|
|
524
|
+
const an = _node_next[a];
|
|
525
|
+
const bp = _node_prev[b];
|
|
526
|
+
|
|
527
|
+
_node_next[a] = b;
|
|
528
|
+
_node_prev[b] = a;
|
|
529
|
+
|
|
530
|
+
_node_next[a2] = an;
|
|
531
|
+
_node_prev[an] = a2;
|
|
532
|
+
|
|
533
|
+
_node_next[b2] = a2;
|
|
534
|
+
_node_prev[a2] = b2;
|
|
535
|
+
|
|
536
|
+
_node_next[bp] = b2;
|
|
537
|
+
_node_prev[b2] = bp;
|
|
538
|
+
|
|
539
|
+
return b2;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* Quantise a coordinate pair into the Morton grid for the current call.
|
|
544
|
+
*
|
|
545
|
+
* @param {number} x
|
|
546
|
+
* @param {number} y
|
|
547
|
+
* @returns {number}
|
|
548
|
+
*/
|
|
549
|
+
function morton_of(x, y) {
|
|
550
|
+
return v2_morton_encode(
|
|
551
|
+
((x - _min_x) * _inv_size) | 0,
|
|
552
|
+
((y - _min_y) * _inv_size) | 0
|
|
553
|
+
);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* (Re)build the Z-order doubly-linked list over the ring starting at `start`.
|
|
558
|
+
*
|
|
559
|
+
* @param {number} start
|
|
560
|
+
*/
|
|
561
|
+
function index_curve(start) {
|
|
562
|
+
let p = start;
|
|
563
|
+
|
|
564
|
+
do {
|
|
565
|
+
_node_z[p] = morton_of(x_of(p), y_of(p));
|
|
566
|
+
_node_zprev[p] = _node_prev[p];
|
|
567
|
+
_node_znext[p] = _node_next[p];
|
|
568
|
+
|
|
569
|
+
p = _node_next[p];
|
|
570
|
+
} while (p !== start);
|
|
571
|
+
|
|
572
|
+
const last = _node_zprev[p];
|
|
573
|
+
|
|
574
|
+
_node_znext[last] = NULL_NODE;
|
|
575
|
+
_node_zprev[p] = NULL_NODE;
|
|
576
|
+
|
|
577
|
+
sort_z_linked(p);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* Bottom-up merge sort of the Z-order list by Morton code. Operates on links, so
|
|
582
|
+
* it moves no data and needs no scratch array.
|
|
583
|
+
*
|
|
584
|
+
* @param {number} list head node
|
|
585
|
+
* @returns {number} the new head
|
|
586
|
+
*/
|
|
587
|
+
function sort_z_linked(list) {
|
|
588
|
+
let in_size = 1;
|
|
589
|
+
let num_merges;
|
|
590
|
+
|
|
591
|
+
do {
|
|
592
|
+
let p = list;
|
|
593
|
+
let tail = NULL_NODE;
|
|
594
|
+
|
|
595
|
+
list = NULL_NODE;
|
|
596
|
+
num_merges = 0;
|
|
597
|
+
|
|
598
|
+
while (p !== NULL_NODE) {
|
|
599
|
+
num_merges++;
|
|
600
|
+
|
|
601
|
+
let q = p;
|
|
602
|
+
let p_size = 0;
|
|
603
|
+
|
|
604
|
+
for (let i = 0; i < in_size; i++) {
|
|
605
|
+
p_size++;
|
|
606
|
+
q = _node_znext[q];
|
|
607
|
+
|
|
608
|
+
if (q === NULL_NODE) {
|
|
609
|
+
break;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
let q_size = in_size;
|
|
614
|
+
|
|
615
|
+
while (p_size > 0 || (q_size > 0 && q !== NULL_NODE)) {
|
|
616
|
+
let e;
|
|
617
|
+
|
|
618
|
+
if (p_size !== 0 && (q_size === 0 || q === NULL_NODE || _node_z[p] <= _node_z[q])) {
|
|
619
|
+
e = p;
|
|
620
|
+
p = _node_znext[p];
|
|
621
|
+
p_size--;
|
|
622
|
+
} else {
|
|
623
|
+
e = q;
|
|
624
|
+
q = _node_znext[q];
|
|
625
|
+
q_size--;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
if (tail !== NULL_NODE) {
|
|
629
|
+
_node_znext[tail] = e;
|
|
630
|
+
} else {
|
|
631
|
+
list = e;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
_node_zprev[e] = tail;
|
|
635
|
+
tail = e;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
p = q;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
_node_znext[tail] = NULL_NODE;
|
|
642
|
+
in_size *= 2;
|
|
643
|
+
} while (num_merges > 1);
|
|
644
|
+
|
|
645
|
+
return list;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/**
|
|
649
|
+
* Is `ear` an ear -- a convex corner whose triangle contains no other reflex
|
|
650
|
+
* corner of the ring? Linear scan; used below {@link HASH_THRESHOLD}.
|
|
651
|
+
*
|
|
652
|
+
* @param {number} ear
|
|
653
|
+
* @returns {boolean}
|
|
654
|
+
*/
|
|
655
|
+
function is_ear(ear) {
|
|
656
|
+
const a = _node_prev[ear];
|
|
657
|
+
const c = _node_next[ear];
|
|
658
|
+
|
|
659
|
+
if (orient(a, ear, c) <= 0) {
|
|
660
|
+
// reflex or straight -- cannot be an ear
|
|
661
|
+
return false;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
const x0 = Math.min(x_of(a), x_of(ear), x_of(c));
|
|
665
|
+
const y0 = Math.min(y_of(a), y_of(ear), y_of(c));
|
|
666
|
+
const x1 = Math.max(x_of(a), x_of(ear), x_of(c));
|
|
667
|
+
const y1 = Math.max(y_of(a), y_of(ear), y_of(c));
|
|
668
|
+
|
|
669
|
+
let p = _node_next[c];
|
|
670
|
+
|
|
671
|
+
while (p !== a) {
|
|
672
|
+
const px = x_of(p);
|
|
673
|
+
const py = y_of(p);
|
|
674
|
+
|
|
675
|
+
if (px >= x0 && px <= x1 && py >= y0 && py <= y1
|
|
676
|
+
&& point_in_triangle_except_first(a, ear, c, p)
|
|
677
|
+
&& orient(_node_prev[p], p, _node_next[p]) <= 0) {
|
|
678
|
+
return false;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
p = _node_next[p];
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
return true;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/**
|
|
688
|
+
* Does node `p` block the ear (a, ear, c)? Shared by the two Z-order walks.
|
|
689
|
+
*
|
|
690
|
+
* @param {number} p
|
|
691
|
+
* @param {number} a
|
|
692
|
+
* @param {number} ear
|
|
693
|
+
* @param {number} c
|
|
694
|
+
* @param {number} x0
|
|
695
|
+
* @param {number} y0
|
|
696
|
+
* @param {number} x1
|
|
697
|
+
* @param {number} y1
|
|
698
|
+
* @returns {boolean}
|
|
699
|
+
*/
|
|
700
|
+
function blocks_ear(p, a, ear, c, x0, y0, x1, y1) {
|
|
701
|
+
if (p === a || p === c) {
|
|
702
|
+
return false;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
const px = x_of(p);
|
|
706
|
+
const py = y_of(p);
|
|
707
|
+
|
|
708
|
+
if (px < x0 || px > x1 || py < y0 || py > y1) {
|
|
709
|
+
return false;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
if (!point_in_triangle(a, ear, c, p)) {
|
|
713
|
+
return false;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
return orient(_node_prev[p], p, _node_next[p]) <= 0;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
/**
|
|
720
|
+
* {@link is_ear} restricted to nodes whose Morton code falls in the ear's
|
|
721
|
+
* bounding-box code range -- the walk that keeps large rings out of O(n^2).
|
|
722
|
+
*
|
|
723
|
+
* @param {number} ear
|
|
724
|
+
* @returns {boolean}
|
|
725
|
+
*/
|
|
726
|
+
function is_ear_hashed(ear) {
|
|
727
|
+
const a = _node_prev[ear];
|
|
728
|
+
const c = _node_next[ear];
|
|
729
|
+
|
|
730
|
+
if (orient(a, ear, c) <= 0) {
|
|
731
|
+
return false;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
const x0 = Math.min(x_of(a), x_of(ear), x_of(c));
|
|
735
|
+
const y0 = Math.min(y_of(a), y_of(ear), y_of(c));
|
|
736
|
+
const x1 = Math.max(x_of(a), x_of(ear), x_of(c));
|
|
737
|
+
const y1 = Math.max(y_of(a), y_of(ear), y_of(c));
|
|
738
|
+
|
|
739
|
+
const min_z = morton_of(x0, y0);
|
|
740
|
+
const max_z = morton_of(x1, y1);
|
|
741
|
+
|
|
742
|
+
let p = _node_zprev[ear];
|
|
743
|
+
let n = _node_znext[ear];
|
|
744
|
+
|
|
745
|
+
while (p !== NULL_NODE && _node_z[p] >= min_z && n !== NULL_NODE && _node_z[n] <= max_z) {
|
|
746
|
+
if (blocks_ear(p, a, ear, c, x0, y0, x1, y1)) {
|
|
747
|
+
return false;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
p = _node_zprev[p];
|
|
751
|
+
|
|
752
|
+
if (blocks_ear(n, a, ear, c, x0, y0, x1, y1)) {
|
|
753
|
+
return false;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
n = _node_znext[n];
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
while (p !== NULL_NODE && _node_z[p] >= min_z) {
|
|
760
|
+
if (blocks_ear(p, a, ear, c, x0, y0, x1, y1)) {
|
|
761
|
+
return false;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
p = _node_zprev[p];
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
while (n !== NULL_NODE && _node_z[n] <= max_z) {
|
|
768
|
+
if (blocks_ear(n, a, ear, c, x0, y0, x1, y1)) {
|
|
769
|
+
return false;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
n = _node_znext[n];
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
return true;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
/**
|
|
779
|
+
* @param {number} a
|
|
780
|
+
* @param {number} b
|
|
781
|
+
* @param {number} c
|
|
782
|
+
*/
|
|
783
|
+
function emit_triangle(a, b, c) {
|
|
784
|
+
_out[_out_cursor] = _node_point[a];
|
|
785
|
+
_out[_out_cursor + 1] = _node_point[b];
|
|
786
|
+
_out[_out_cursor + 2] = _node_point[c];
|
|
787
|
+
_out_cursor += 3;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
/**
|
|
791
|
+
* Is the ring through `node` down to fewer than three nodes -- i.e. is there no
|
|
792
|
+
* triangle left in it at all?
|
|
793
|
+
*
|
|
794
|
+
* @param {number} node
|
|
795
|
+
* @returns {boolean}
|
|
796
|
+
*/
|
|
797
|
+
function ring_is_spent(node) {
|
|
798
|
+
return _node_next[node] === node || _node_next[_node_next[node]] === node;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
/**
|
|
802
|
+
* Second-chance pass: when the ring has no ear left, look for a pair of adjacent
|
|
803
|
+
* edges that cross and cut the offending vertex pair out as one triangle.
|
|
804
|
+
*
|
|
805
|
+
* Takes the output of {@link filter_points} directly, so it accepts and
|
|
806
|
+
* propagates that function's collapsed-ring sentinel.
|
|
807
|
+
*
|
|
808
|
+
* @param {number} start a node, or {@link NULL_NODE}
|
|
809
|
+
* @returns {number} a node of the repaired ring, or {@link NULL_NODE}
|
|
810
|
+
*/
|
|
811
|
+
function cure_local_intersections(start) {
|
|
812
|
+
if (start === NULL_NODE) {
|
|
813
|
+
return NULL_NODE;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
let p = start;
|
|
817
|
+
|
|
818
|
+
do {
|
|
819
|
+
const a = _node_prev[p];
|
|
820
|
+
const next = _node_next[p];
|
|
821
|
+
const b = _node_next[next];
|
|
822
|
+
|
|
823
|
+
if (!nodes_equal(a, b)
|
|
824
|
+
&& segments_intersect(a, p, next, b)
|
|
825
|
+
&& locally_inside(a, b)
|
|
826
|
+
&& locally_inside(b, a)) {
|
|
827
|
+
emit_triangle(a, p, b);
|
|
828
|
+
|
|
829
|
+
remove_node(p);
|
|
830
|
+
remove_node(next);
|
|
831
|
+
|
|
832
|
+
// Two nodes just left the ring. Once fewer than three remain there is
|
|
833
|
+
// no triangle to cure and the walk has nothing left to walk: `p` and
|
|
834
|
+
// `start` would both be chasing unlinked nodes.
|
|
835
|
+
if (ring_is_spent(b)) {
|
|
836
|
+
return NULL_NODE;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
p = b;
|
|
840
|
+
start = b;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
p = _node_next[p];
|
|
844
|
+
} while (p !== start);
|
|
845
|
+
|
|
846
|
+
return filter_points(p, NULL_NODE);
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
/**
|
|
850
|
+
* Last-chance pass: split the ring along any valid diagonal and triangulate the
|
|
851
|
+
* two halves independently.
|
|
852
|
+
*
|
|
853
|
+
* @param {number} start
|
|
854
|
+
*/
|
|
855
|
+
function split_ear_clip(start) {
|
|
856
|
+
let a = start;
|
|
857
|
+
|
|
858
|
+
do {
|
|
859
|
+
let b = _node_next[_node_next[a]];
|
|
860
|
+
|
|
861
|
+
while (b !== _node_prev[a]) {
|
|
862
|
+
if (_node_point[a] !== _node_point[b] && is_valid_diagonal(a, b)) {
|
|
863
|
+
let c = split_ring(a, b);
|
|
864
|
+
|
|
865
|
+
a = filter_points(a, _node_next[a]);
|
|
866
|
+
c = filter_points(c, _node_next[c]);
|
|
867
|
+
|
|
868
|
+
ear_clip(a, 0);
|
|
869
|
+
ear_clip(c, 0);
|
|
870
|
+
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
b = _node_next[b];
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
a = _node_next[a];
|
|
878
|
+
} while (a !== start);
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
/**
|
|
882
|
+
* Slice ears off the ring until it is a single triangle.
|
|
883
|
+
*
|
|
884
|
+
* `pass` records how many recovery tiers have been tried on this ring: 0 = plain,
|
|
885
|
+
* 1 = after removing degenerate points, 2 = after curing local self-intersections.
|
|
886
|
+
* Reaching the end of tier 2 without an ear means the ring is not triangulatable
|
|
887
|
+
* and its remaining area is dropped -- reported to the caller as a short triangle
|
|
888
|
+
* count.
|
|
889
|
+
*
|
|
890
|
+
* @param {number} ear
|
|
891
|
+
* @param {number} pass
|
|
892
|
+
*/
|
|
893
|
+
function ear_clip(ear, pass) {
|
|
894
|
+
if (ear === NULL_NODE) {
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
if (pass === 0 && _hashed) {
|
|
899
|
+
index_curve(ear);
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
let stop = ear;
|
|
903
|
+
|
|
904
|
+
while (_node_prev[ear] !== _node_next[ear]) {
|
|
905
|
+
const prev = _node_prev[ear];
|
|
906
|
+
const next = _node_next[ear];
|
|
907
|
+
|
|
908
|
+
if (_hashed ? is_ear_hashed(ear) : is_ear(ear)) {
|
|
909
|
+
emit_triangle(prev, ear, next);
|
|
910
|
+
|
|
911
|
+
remove_node(ear);
|
|
912
|
+
|
|
913
|
+
// skipping the next vertex leads to fewer sliver triangles
|
|
914
|
+
ear = _node_next[next];
|
|
915
|
+
stop = _node_next[next];
|
|
916
|
+
|
|
917
|
+
continue;
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
ear = next;
|
|
921
|
+
|
|
922
|
+
if (ear !== stop) {
|
|
923
|
+
continue;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
if (pass === 0) {
|
|
927
|
+
ear_clip(filter_points(ear, NULL_NODE), 1);
|
|
928
|
+
} else if (pass === 1) {
|
|
929
|
+
const cured = cure_local_intersections(filter_points(ear, NULL_NODE));
|
|
930
|
+
|
|
931
|
+
ear_clip(cured, 2);
|
|
932
|
+
} else if (pass === 2) {
|
|
933
|
+
split_ear_clip(ear);
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
return;
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
/**
|
|
941
|
+
* Leftmost (then lowest) node of a ring.
|
|
942
|
+
*
|
|
943
|
+
* @param {number} start
|
|
944
|
+
* @returns {number}
|
|
945
|
+
*/
|
|
946
|
+
function get_leftmost(start) {
|
|
947
|
+
let p = start;
|
|
948
|
+
let leftmost = start;
|
|
949
|
+
|
|
950
|
+
do {
|
|
951
|
+
if (x_of(p) < x_of(leftmost) || (x_of(p) === x_of(leftmost) && y_of(p) < y_of(leftmost))) {
|
|
952
|
+
leftmost = p;
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
p = _node_next[p];
|
|
956
|
+
} while (p !== start);
|
|
957
|
+
|
|
958
|
+
return leftmost;
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
/**
|
|
962
|
+
* Find the outer-ring node that the hole's leftmost node can see, so a bridge
|
|
963
|
+
* between them stays inside the polygon.
|
|
964
|
+
*
|
|
965
|
+
* Casts a ray in -x from the hole vertex, takes the closest outer edge it hits,
|
|
966
|
+
* then -- if any reflex outer vertex lies in the triangle formed by the hit -- picks
|
|
967
|
+
* the one at the smallest angle to the ray instead.
|
|
968
|
+
*
|
|
969
|
+
* @param {number} hole
|
|
970
|
+
* @param {number} outer
|
|
971
|
+
* @returns {number} the bridge node, or {@link NULL_NODE} when none is visible
|
|
972
|
+
*/
|
|
973
|
+
function find_hole_bridge(hole, outer) {
|
|
974
|
+
const hx = x_of(hole);
|
|
975
|
+
const hy = y_of(hole);
|
|
976
|
+
|
|
977
|
+
let qx = -Infinity;
|
|
978
|
+
let m = NULL_NODE;
|
|
979
|
+
|
|
980
|
+
let p = outer;
|
|
981
|
+
|
|
982
|
+
if (nodes_equal(hole, p)) {
|
|
983
|
+
return p;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
do {
|
|
987
|
+
const next = _node_next[p];
|
|
988
|
+
|
|
989
|
+
if (nodes_equal(hole, next)) {
|
|
990
|
+
return next;
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
const py = y_of(p);
|
|
994
|
+
const ny = y_of(next);
|
|
995
|
+
|
|
996
|
+
if (hy <= py && hy >= ny && ny !== py) {
|
|
997
|
+
const x = x_of(p) + (hy - py) * (x_of(next) - x_of(p)) / (ny - py);
|
|
998
|
+
|
|
999
|
+
if (x <= hx && x > qx) {
|
|
1000
|
+
qx = x;
|
|
1001
|
+
m = x_of(p) < x_of(next) ? p : next;
|
|
1002
|
+
|
|
1003
|
+
if (x === hx) {
|
|
1004
|
+
// the hole touches the outer contour exactly here
|
|
1005
|
+
return m;
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
p = next;
|
|
1011
|
+
} while (p !== outer);
|
|
1012
|
+
|
|
1013
|
+
if (m === NULL_NODE) {
|
|
1014
|
+
return NULL_NODE;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
// Search the sector between the ray hit and the chosen endpoint for a reflex
|
|
1018
|
+
// vertex that would be occluded by the naive bridge.
|
|
1019
|
+
const stop = m;
|
|
1020
|
+
const mx = x_of(m);
|
|
1021
|
+
const my = y_of(m);
|
|
1022
|
+
|
|
1023
|
+
let tan_min = Infinity;
|
|
1024
|
+
|
|
1025
|
+
p = m;
|
|
1026
|
+
|
|
1027
|
+
do {
|
|
1028
|
+
const px = x_of(p);
|
|
1029
|
+
const py = y_of(p);
|
|
1030
|
+
|
|
1031
|
+
if (hx >= px && px >= mx && hx !== px
|
|
1032
|
+
&& point_in_triangle_xy(hy < my ? hx : qx, hy, mx, my, hy < my ? qx : hx, hy, px, py)) {
|
|
1033
|
+
const tan = Math.abs(hy - py) / (hx - px);
|
|
1034
|
+
|
|
1035
|
+
if (locally_inside(p, hole)
|
|
1036
|
+
&& (tan < tan_min
|
|
1037
|
+
|| (tan === tan_min
|
|
1038
|
+
&& (px > x_of(m) || (px === x_of(m) && sector_contains_sector(m, p)))))) {
|
|
1039
|
+
m = p;
|
|
1040
|
+
tan_min = tan;
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
p = _node_next[p];
|
|
1045
|
+
} while (p !== stop);
|
|
1046
|
+
|
|
1047
|
+
return m;
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
/**
|
|
1051
|
+
* Point-in-triangle on raw coordinates. The bridge search builds a triangle from
|
|
1052
|
+
* the ray hit, which is not a ring node, so it cannot use the node-indexed test.
|
|
1053
|
+
*
|
|
1054
|
+
* Inexact by necessity (`qx` is an intersection, computed with a division) -- but
|
|
1055
|
+
* this only selects *which* legal bridge to use, never whether the result is
|
|
1056
|
+
* valid, so an unlucky rounding costs triangle quality and nothing else.
|
|
1057
|
+
*
|
|
1058
|
+
* @returns {boolean}
|
|
1059
|
+
*/
|
|
1060
|
+
function point_in_triangle_xy(ax, ay, bx, by, cx, cy, px, py) {
|
|
1061
|
+
return (cx - px) * (ay - py) >= (ax - px) * (cy - py)
|
|
1062
|
+
&& (ax - px) * (by - py) >= (bx - px) * (ay - py)
|
|
1063
|
+
&& (bx - px) * (cy - py) >= (cx - px) * (by - py);
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
/**
|
|
1067
|
+
* Whether m's interior sector contains p's -- the tie-break when two candidate
|
|
1068
|
+
* bridges sit at the same angle.
|
|
1069
|
+
*
|
|
1070
|
+
* @param {number} m
|
|
1071
|
+
* @param {number} p
|
|
1072
|
+
* @returns {boolean}
|
|
1073
|
+
*/
|
|
1074
|
+
function sector_contains_sector(m, p) {
|
|
1075
|
+
return orient(_node_prev[m], m, _node_prev[p]) > 0 && orient(_node_next[p], m, _node_next[m]) > 0;
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
/**
|
|
1079
|
+
* Splice one hole into the outer ring through a doubled bridge edge.
|
|
1080
|
+
*
|
|
1081
|
+
* @param {number} hole leftmost node of the hole ring
|
|
1082
|
+
* @param {number} outer
|
|
1083
|
+
* @returns {number} the outer ring node to continue from
|
|
1084
|
+
*/
|
|
1085
|
+
function eliminate_hole(hole, outer) {
|
|
1086
|
+
if (outer === NULL_NODE) {
|
|
1087
|
+
return NULL_NODE;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
const bridge = find_hole_bridge(hole, outer);
|
|
1091
|
+
|
|
1092
|
+
if (bridge === NULL_NODE) {
|
|
1093
|
+
return outer;
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
const bridge_reverse = split_ring(bridge, hole);
|
|
1097
|
+
|
|
1098
|
+
filter_points(bridge_reverse, _node_next[bridge_reverse]);
|
|
1099
|
+
|
|
1100
|
+
return filter_points(bridge, _node_next[bridge]);
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
/**
|
|
1104
|
+
* Compare two hole rings by their leftmost node, so bridges are cut left to right
|
|
1105
|
+
* and therefore never cross one another.
|
|
1106
|
+
*
|
|
1107
|
+
* @param {number} a leftmost node of one hole
|
|
1108
|
+
* @param {number} b leftmost node of another
|
|
1109
|
+
* @returns {number}
|
|
1110
|
+
*/
|
|
1111
|
+
function compare_hole_leftmost(a, b) {
|
|
1112
|
+
let result = x_of(a) - x_of(b);
|
|
1113
|
+
|
|
1114
|
+
if (result !== 0) {
|
|
1115
|
+
return result;
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
result = y_of(a) - y_of(b);
|
|
1119
|
+
|
|
1120
|
+
if (result !== 0) {
|
|
1121
|
+
return result;
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
// Two holes meeting at one point: order them by outgoing slope so the bridge
|
|
1125
|
+
// is always cut at the vertex where they meet.
|
|
1126
|
+
const a_slope = (y_of(_node_next[a]) - y_of(a)) / (x_of(_node_next[a]) - x_of(a));
|
|
1127
|
+
const b_slope = (y_of(_node_next[b]) - y_of(b)) / (x_of(_node_next[b]) - x_of(b));
|
|
1128
|
+
|
|
1129
|
+
return a_slope - b_slope;
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
/**
|
|
1133
|
+
* Splice every hole contour into the outer ring.
|
|
1134
|
+
*
|
|
1135
|
+
* @param {ArrayLike<number>} points
|
|
1136
|
+
* @param {ArrayLike<number>} contour_ends
|
|
1137
|
+
* @param {number} contour_count
|
|
1138
|
+
* @param {number} outer
|
|
1139
|
+
* @returns {number} the merged ring
|
|
1140
|
+
*/
|
|
1141
|
+
function eliminate_holes(points, contour_ends, contour_count, outer) {
|
|
1142
|
+
const queue = [];
|
|
1143
|
+
|
|
1144
|
+
for (let c = 1; c < contour_count; c++) {
|
|
1145
|
+
const from = contour_ends[c - 1];
|
|
1146
|
+
const to = contour_ends[c];
|
|
1147
|
+
|
|
1148
|
+
const last = link_contour(points, from, to, false);
|
|
1149
|
+
|
|
1150
|
+
if (last === NULL_NODE) {
|
|
1151
|
+
continue;
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
if (last === _node_next[last]) {
|
|
1155
|
+
// the hole collapsed to a single point; keep it as a Steiner vertex
|
|
1156
|
+
_node_steiner[last] = 1;
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
queue.push(get_leftmost(last));
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
queue.sort(compare_hole_leftmost);
|
|
1163
|
+
|
|
1164
|
+
for (let i = 0; i < queue.length; i++) {
|
|
1165
|
+
outer = eliminate_hole(queue[i], outer);
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
return outer;
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
/**
|
|
1172
|
+
* Triangulate a 2D polygon with holes.
|
|
1173
|
+
*
|
|
1174
|
+
* Ear clipping over a doubly-linked vertex ring, with every combinatorial
|
|
1175
|
+
* decision made by the exact `orient2d` predicate already used elsewhere in
|
|
1176
|
+
* `core/geom` -- so the result does not depend on coordinate magnitude, and
|
|
1177
|
+
* collinear or coincident input is decided rather than guessed. Above
|
|
1178
|
+
* {@link HASH_THRESHOLD} vertices the point-in-ear test is restricted to a
|
|
1179
|
+
* Morton-code range, which is what keeps large rings off the quadratic path.
|
|
1180
|
+
*
|
|
1181
|
+
* Contour `i` spans `points[(i === 0 ? 0 : contour_ends[i - 1]) .. contour_ends[i])`
|
|
1182
|
+
* in vertex units. Contour 0 is the outer boundary and the rest are holes.
|
|
1183
|
+
* Winding is normalised internally, so callers cannot get it subtly wrong.
|
|
1184
|
+
*
|
|
1185
|
+
* Output is triples of indices into `points`, in ear-cut order, each triangle
|
|
1186
|
+
* counter-clockwise.
|
|
1187
|
+
*
|
|
1188
|
+
* A well-formed polygon yields exactly `V + 2H - 2` triangles, where `V` is the
|
|
1189
|
+
* total vertex count and `H` the hole count. **A smaller return value is how this
|
|
1190
|
+
* function reports that the input could not be fully triangulated** -- it is
|
|
1191
|
+
* self-intersecting, or partly degenerate. It never throws for that: a partial
|
|
1192
|
+
* triangulation is still the best available answer, and the caller is told how
|
|
1193
|
+
* much of one it got. {@link polygon2_triangulation_deviation} measures the
|
|
1194
|
+
* shortfall in area terms.
|
|
1195
|
+
*
|
|
1196
|
+
* Not re-entrant: the vertex ring lives in module-static buffers so a call
|
|
1197
|
+
* allocates nothing in the steady state.
|
|
1198
|
+
*
|
|
1199
|
+
* @param {Uint32Array} result receives 3 point indices per triangle
|
|
1200
|
+
* @param {number} result_offset first index written in `result`
|
|
1201
|
+
* @param {ArrayLike<number>} points flat (x, y) pairs, stride 2
|
|
1202
|
+
* @param {ArrayLike<number>} contour_ends exclusive end vertex index per contour, non-decreasing
|
|
1203
|
+
* @param {number} contour_count number of contours, at least 1
|
|
1204
|
+
* @returns {number} number of triangles written
|
|
1205
|
+
*
|
|
1206
|
+
* @author Alex Goldring
|
|
1207
|
+
* @copyright Company Named Limited (c) 2026
|
|
1208
|
+
*/
|
|
1209
|
+
export function polygon2_triangulate(result, result_offset, points, contour_ends, contour_count) {
|
|
1210
|
+
assert.defined(result, "result");
|
|
1211
|
+
assert.isNonNegativeInteger(result_offset, "result_offset");
|
|
1212
|
+
assert.isArrayLike(points, "points");
|
|
1213
|
+
assert.isArrayLike(contour_ends, "contour_ends");
|
|
1214
|
+
assert.isPositiveInteger(contour_count, "contour_count");
|
|
1215
|
+
|
|
1216
|
+
const vertex_count = contour_ends[contour_count - 1];
|
|
1217
|
+
|
|
1218
|
+
assert.isNonNegativeInteger(vertex_count, "contour_ends[contour_count - 1]");
|
|
1219
|
+
assert.greaterThanOrEqual(points.length, vertex_count * 2, "points.length");
|
|
1220
|
+
|
|
1221
|
+
if (vertex_count < 3) {
|
|
1222
|
+
return 0;
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
// Hole elimination duplicates two vertices per bridge, and each recovery split
|
|
1226
|
+
// duplicates two more. Splits only ever happen on a ring that already failed to
|
|
1227
|
+
// clip, and each consumes at least one vertex, so 2 per vertex bounds both.
|
|
1228
|
+
ensure_node_capacity(vertex_count * 3 + contour_count * 2);
|
|
1229
|
+
|
|
1230
|
+
_node_count = 0;
|
|
1231
|
+
_out = result;
|
|
1232
|
+
_out_cursor = result_offset;
|
|
1233
|
+
|
|
1234
|
+
let outer = link_contour(points, 0, contour_ends[0], true);
|
|
1235
|
+
|
|
1236
|
+
if (outer === NULL_NODE || _node_next[outer] === _node_prev[outer]) {
|
|
1237
|
+
_out = null;
|
|
1238
|
+
|
|
1239
|
+
return 0;
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
if (contour_count > 1) {
|
|
1243
|
+
outer = eliminate_holes(points, contour_ends, contour_count, outer);
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
_hashed = vertex_count > HASH_THRESHOLD;
|
|
1247
|
+
|
|
1248
|
+
if (_hashed) {
|
|
1249
|
+
let min_x = Infinity;
|
|
1250
|
+
let min_y = Infinity;
|
|
1251
|
+
let max_x = -Infinity;
|
|
1252
|
+
let max_y = -Infinity;
|
|
1253
|
+
|
|
1254
|
+
for (let i = 0; i < vertex_count; i++) {
|
|
1255
|
+
const x = points[i * 2];
|
|
1256
|
+
const y = points[i * 2 + 1];
|
|
1257
|
+
|
|
1258
|
+
if (x < min_x) min_x = x;
|
|
1259
|
+
if (y < min_y) min_y = y;
|
|
1260
|
+
if (x > max_x) max_x = x;
|
|
1261
|
+
if (y > max_y) max_y = y;
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
_min_x = min_x;
|
|
1265
|
+
_min_y = min_y;
|
|
1266
|
+
|
|
1267
|
+
const extent = Math.max(max_x - min_x, max_y - min_y);
|
|
1268
|
+
|
|
1269
|
+
// A zero-extent polygon has no area to triangulate; any finite scale works.
|
|
1270
|
+
_inv_size = extent > 0 ? MORTON_SCALE / extent : 0;
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
ear_clip(outer, 0);
|
|
1274
|
+
|
|
1275
|
+
const triangles = (_out_cursor - result_offset) / 3;
|
|
1276
|
+
|
|
1277
|
+
_out = null;
|
|
1278
|
+
|
|
1279
|
+
return triangles;
|
|
1280
|
+
}
|