🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

@expofp/wayfinding

Package Overview
Dependencies
Maintainers
6
Versions
8
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@expofp/wayfinding - npm Package Compare versions

Comparing version
3.11.11
to
3.11.12
+13
-13
dist/core/createWayfindingEngine.js

@@ -11,3 +11,3 @@ import { createGraphCache } from './graph/graphCache.js';

* Trim/extend the route's FROM tip to land exactly on `entry.projection`.
* The FROM boundary is the first line; `boundary.p0` is its graph node.
* A* returns TO→FROM order, so the FROM boundary is the last line.
* @param lines

@@ -19,15 +19,15 @@ * @param entry

return lines;
const boundary = lines[0];
const chosenEndpoint = boundary.p0;
const boundary = lines[lines.length - 1];
const chosenEndpoint = boundary.p1;
const otherEnd = pointEquals(entry.segment.p0, chosenEndpoint)
? entry.segment.p1
: entry.segment.p0;
if (pointEquals(boundary.p1, otherEnd)) {
return [{ ...boundary, p0: entry.projection }, ...lines.slice(1)];
if (pointEquals(boundary.p0, otherEnd)) {
return [...lines.slice(0, -1), { ...boundary, p1: entry.projection }];
}
return [{ ...entry.segment, p0: entry.projection, p1: chosenEndpoint }, ...lines];
return [...lines, { ...entry.segment, p0: chosenEndpoint, p1: entry.projection }];
}
/**
* Mirror of {@link attachFromEntry} for the TO side: the TO boundary is the
* last line and `boundary.p1` is its graph node.
* Mirror of {@link attachFromEntry} for the TO side. TO boundary is the
* FIRST line; `boundary.p0` is the chosen graph node on the TO end.
* @param lines

@@ -39,11 +39,11 @@ * @param entry

return lines;
const boundary = lines[lines.length - 1];
const chosenEndpoint = boundary.p1;
const boundary = lines[0];
const chosenEndpoint = boundary.p0;
const otherEnd = pointEquals(entry.segment.p0, chosenEndpoint)
? entry.segment.p1
: entry.segment.p0;
if (pointEquals(boundary.p0, otherEnd)) {
return [...lines.slice(0, -1), { ...boundary, p1: entry.projection }];
if (pointEquals(boundary.p1, otherEnd)) {
return [{ ...boundary, p0: entry.projection }, ...lines.slice(1)];
}
return [...lines, { ...entry.segment, p0: chosenEndpoint, p1: entry.projection }];
return [{ ...entry.segment, p0: entry.projection, p1: chosenEndpoint }, ...lines];
}

@@ -50,0 +50,0 @@ function attachOffGraphEntries(lines, fromEntry, toEntry) {

/**
* Default A* cost for an unset virtual link that changes layers. Being far
* larger than any physical segment cost, it makes the pathfinder minimize the
* number of floor changes, so a route only crosses a floor when needed to reach
* the destination. A per-line `virtualLength` overrides it (designer control).
* Default A* cost for a virtual (floor-transition) link. Non-zero so a floor
* change costs more than any same-floor detour; otherwise floor changes are free
* and a route between two points on one floor can detour through another.
*/
export declare const FLOOR_TRANSITION_PENALTY = 10000;
/** Default A* cost for a virtual link that stays on the same layer (free). */
export declare const VIRTUAL_LINE_PENALTY = 0;
export declare const VIRTUAL_LINE_PENALTY = 10000;
/**

@@ -11,0 +8,0 @@ * Fallback line weight when `line.weight` is 0 (meaning "not set" in floorplan data,

/**
* Default A* cost for an unset virtual link that changes layers. Being far
* larger than any physical segment cost, it makes the pathfinder minimize the
* number of floor changes, so a route only crosses a floor when needed to reach
* the destination. A per-line `virtualLength` overrides it (designer control).
* Default A* cost for a virtual (floor-transition) link. Non-zero so a floor
* change costs more than any same-floor detour; otherwise floor changes are free
* and a route between two points on one floor can detour through another.
*/
export const FLOOR_TRANSITION_PENALTY = 10000;
/** Default A* cost for a virtual link that stays on the same layer (free). */
export const VIRTUAL_LINE_PENALTY = 0;
export const VIRTUAL_LINE_PENALTY = 10000;
/**

@@ -11,0 +8,0 @@ * Fallback line weight when `line.weight` is 0 (meaning "not set" in floorplan data,

import { type GraphInstance, type GraphLine, type RoutePoint, type ShortestPathResult } from '../types.js';
/**
* Computes the weighted path cost matching the graph link distance formula.
* Floor-change surcharges are already baked into `linkCost`, so this simply
* sums the per-edge cost.
* @param points

@@ -7,0 +5,0 @@ * @param lines

@@ -9,4 +9,2 @@ import { getRouteLength } from '../routing/getRouteLength.js';

* Computes the weighted path cost matching the graph link distance formula.
* Floor-change surcharges are already baked into `linkCost`, so this simply
* sums the per-edge cost.
* @param points

@@ -13,0 +11,0 @@ * @param lines

import { degToRad, lineAngle, pointDistance } from '@expofp/geometry';
export const toNodeId = (p) => `${p.layer ?? ''}_${p.x}_${p.y}`;
export const toNodeId = (p) => `${p.layer}_${p.x}_${p.y}`;
/** Maximum distance (SVG units) to consider two graph nodes as the same point. */
const POINT_PROXIMITY_THRESHOLD = 1;
// `RoutePoint.layer` is typed as a string, but external data may still supply a
// nullish/invalid layer. Normalize it to '' so point comparisons stay consistent
// with the ids that toNodeId/parseNodeId round-trip to.
export const arePointsClose = (p1, p2) => (p1.layer ?? '') === (p2.layer ?? '') && pointDistance(p1, p2) <= POINT_PROXIMITY_THRESHOLD;
export const arePointsClose = (p1, p2) => p1.layer === p2.layer && pointDistance(p1, p2) <= POINT_PROXIMITY_THRESHOLD;
export const matchesLine = (line, p0, p1) => (arePointsClose(line.p0, p0) && arePointsClose(line.p1, p1)) ||

@@ -10,0 +7,0 @@ (arePointsClose(line.p0, p1) && arePointsClose(line.p1, p0));

import { type GraphLine } from '../types.js';
export declare function changesLayer(line: GraphLine): boolean;
/**
* Weighted cost of a graph edge for A*.
*
* Physical links cost their weighted length. A virtual link uses its configured
* `virtualLength` when set (designer control); otherwise a layer-changing link
* costs `FLOOR_TRANSITION_PENALTY` (routes avoid needless floor changes) and a
* same-layer link is free (`VIRTUAL_LINE_PENALTY`).
* Weighted cost of a route line for A* graph edge (legacy formula).
* @param line

@@ -11,0 +5,0 @@ */

import { pointDistance } from '@expofp/geometry';
import { DEFAULT_LINE_WEIGHT, FLOOR_TRANSITION_PENALTY, VIRTUAL_LINE_PENALTY, } from './constants.js';
function physicalCost(line) {
return pointDistance(line.p0, line.p1) / (line.weight || DEFAULT_LINE_WEIGHT);
}
export function changesLayer(line) {
return (line.p0.layer ?? '') !== (line.p1.layer ?? '');
}
import { DEFAULT_LINE_WEIGHT, VIRTUAL_LINE_PENALTY } from './constants.js';
/**
* Weighted cost of a graph edge for A*.
*
* Physical links cost their weighted length. A virtual link uses its configured
* `virtualLength` when set (designer control); otherwise a layer-changing link
* costs `FLOOR_TRANSITION_PENALTY` (routes avoid needless floor changes) and a
* same-layer link is free (`VIRTUAL_LINE_PENALTY`).
* Weighted cost of a route line for A* graph edge (legacy formula).
* @param line
*/
export function linkCost(line) {
if (!line.virtual)
return physicalCost(line);
if (line.virtualLength != null)
return line.virtualLength;
return changesLayer(line) ? FLOOR_TRANSITION_PENALTY : VIRTUAL_LINE_PENALTY;
if (line.virtual)
return line.virtualLength ?? VIRTUAL_LINE_PENALTY;
return pointDistance(line.p0, line.p1) / (line.weight || DEFAULT_LINE_WEIGHT);
}

@@ -44,5 +44,4 @@ import { lineAngle } from '@expofp/geometry';

/**
* Returns path nodes in **source→target** order (`fromId` first, `toId`
* last). A* reconstructs from the goal, so the raw path is target→source; it
* is reversed here once, at the source, so no downstream reversal is needed.
* Returns path nodes in **target→source** order (A* reconstructs from the goal);
* downstream consumers depend on this ordering.
* @param fromId

@@ -57,10 +56,8 @@ * @param toId

return [];
return nodes
.map((node) => {
return nodes.map((node) => {
const [layer, x, y] = parseNodeId(node.id);
return { id: node.id, layer, x, y };
})
.reverse();
});
},
};
}

@@ -9,2 +9,3 @@ export type { Route, WayfindingEngine } from './createWayfindingEngine.js';

export { computeTransitionPoints } from './rendering/computeTransitionPoints.js';
export { normalizeRouteDirection } from './rendering/normalizeRouteDirection.js';
export { computeBoundingBox } from './rendering/routeGeometry.js';

@@ -11,0 +12,0 @@ export { getRouteLength } from './routing/getRouteLength.js';

@@ -6,2 +6,3 @@ export { createWayfindingEngine } from './createWayfindingEngine.js';

export { computeTransitionPoints } from './rendering/computeTransitionPoints.js';
export { normalizeRouteDirection } from './rendering/normalizeRouteDirection.js';
export { computeBoundingBox } from './rendering/routeGeometry.js';

@@ -8,0 +9,0 @@ // Routing utilities

import { type RouteLine, type RoutePoint, type RouteSnapResult, type SnapToRouteConfig } from '../types.js';
export { SNAP_THRESHOLD_METERS } from './gpsThreshold.js';
/**
* Snap a position onto a from→to route polyline. Pure geometry: nearest segment
* within `snapThreshold`, plus the remaining arc-length to the destination.
* Segments on a layer other than `position.layer` are skipped.
*/
export declare function snapToRoute(position: RoutePoint, routeLines: readonly RouteLine[], { snapThreshold }: SnapToRouteConfig): RouteSnapResult;
export declare function snapToRoute(position: RoutePoint, routeLines: RouteLine[], config: SnapToRouteConfig): RouteSnapResult;
//# sourceMappingURL=snapToRoute.d.ts.map
import { pointDistance } from '@expofp/geometry';
import { pointInPolygon } from '../geometry/pointInPolygon.js';
import { projectPointOnSegment } from '../geometry/projectPointOnSegment.js';
function pointAtDistanceSafe(routeLines, distanceFromStart) {
let acc = 0;
for (const line of routeLines) {
const len = pointDistance(line.p0, line.p1);
if (len === 0)
continue;
if (acc + len >= distanceFromStart) {
const t = (distanceFromStart - acc) / len;
const x = line.p0.x + (line.p1.x - line.p0.x) * t;
const y = line.p0.y + (line.p1.y - line.p0.y) * t;
return {
point: { layer: line.p0.layer, x, y },
line,
};
}
acc += len;
}
const last = routeLines[routeLines.length - 1];
return {
point: { layer: last.p0.layer, x: last.p1.x, y: last.p1.y },
line: last,
};
}
export { SNAP_THRESHOLD_METERS } from './gpsThreshold.js';
/**
* Snap a position onto a from→to route polyline. Pure geometry: nearest segment
* within `snapThreshold`, plus the remaining arc-length to the destination.
* Segments on a layer other than `position.layer` are skipped.
*/
export function snapToRoute(position, routeLines, { snapThreshold }) {
export function snapToRoute(position, routeLines, config) {
const lineCount = routeLines.length;
if (!lineCount)
return { snapped: false };
const { snapThreshold, to, from, minRemainingUnits = 0.5 } = config;
const posLayer = position.layer || null;
let bestDistance = Infinity;
let best = null;
let bestResult = null;
let accumulatedLength = 0;

@@ -32,6 +52,6 @@ let hasSegmentOnPosLayer = posLayer == null;

bestDistance = projection.distance;
best = {
point: { layer: p0.layer, x: projection.projected.x, y: projection.projected.y },
line,
fromStart: accumulatedLength + segLen * projection.t,
bestResult = {
segmentIndex: i,
t: projection.t,
distanceFromPolylineStart: accumulatedLength + segLen * projection.t,
};

@@ -43,10 +63,54 @@ }

return { snapped: false };
if (!best || bestDistance > snapThreshold)
const totalLen = accumulatedLength;
const routeStart = routeLines[0].p0;
const routeEnd = routeLines[lineCount - 1].p1;
const distDestToStart = pointDistance(to, routeStart);
const distDestToEnd = pointDistance(to, routeEnd);
const destinationAtPolylineStart = distDestToStart <= distDestToEnd;
const minRemain = Math.min(minRemainingUnits, totalLen);
if (from?.bounds &&
(posLayer == null || !from.layer || from.layer === posLayer) &&
pointInPolygon({ x: position.x, y: position.y }, from.bounds)) {
const remainingToDestination = totalLen;
const distanceFromStart = destinationAtPolylineStart
? remainingToDestination
: totalLen - remainingToDestination;
const final = pointAtDistanceSafe(routeLines, distanceFromStart);
return {
snapped: true,
snappedPoint: final.point,
snappedLine: final.line,
distance: remainingToDestination,
};
}
if (to.bounds &&
(posLayer == null || !to.layer || to.layer === posLayer) &&
pointInPolygon({ x: position.x, y: position.y }, to.bounds)) {
const remainingToDestination = minRemain;
const passedSvgLength = totalLen - remainingToDestination;
const distanceFromStart = destinationAtPolylineStart ? remainingToDestination : passedSvgLength;
const final = pointAtDistanceSafe(routeLines, distanceFromStart);
return {
snapped: true,
snappedPoint: final.point,
snappedLine: final.line,
distance: remainingToDestination,
};
}
if (!bestResult || bestDistance > snapThreshold)
return { snapped: false };
let remainingToDestination = destinationAtPolylineStart
? bestResult.distanceFromPolylineStart
: totalLen - bestResult.distanceFromPolylineStart;
if (remainingToDestination < minRemain)
remainingToDestination = minRemain;
const passedSvgLength = totalLen - remainingToDestination;
const distanceFromStart = destinationAtPolylineStart ? remainingToDestination : passedSvgLength;
const final = pointAtDistanceSafe(routeLines, distanceFromStart);
return {
snapped: true,
snappedPoint: best.point,
snappedLine: best.line,
distance: accumulatedLength - best.fromStart,
snappedPoint: final.point,
snappedLine: final.line,
distance: remainingToDestination,
};
}

@@ -95,5 +95,5 @@ // ---------------------------------------------------------------------------

direction: resolveDirection(floorOrder, visible.layer, resolvedTarget),
// Current-floor endpoint at `p0` → route departs here → "exit" (clickable
// to advance); at `p1` → arrives here → "entry". Null layer → default "exit".
role: currentLayerName ? (pickP0 ? 'exit' : 'entry') : 'exit',
// When currentLayerName is null, pickP0 is always true — default to "exit"
// to preserve existing behavior (all points show stairs icon).
role: currentLayerName ? (pickP0 ? 'entry' : 'exit') : 'exit',
};

@@ -100,0 +100,0 @@ });

@@ -31,4 +31,12 @@ import { buildRoute } from './buildRoute.js';

}
// Legs concatenate directly: for [A, B, C], buildRoute(A, B) → [A→…→B] and
// buildRoute(B, C) → [B→…→C] share endpoint B, forming one continuous polyline.
// A* returns each leg in target→source order (see aStarPathFinder.find()).
// For candidates [A, B, C], legs are computed as:
// buildRoute(A, B) → lines [B→...→A]
// buildRoute(B, C) → lines [C→...→B]
//
// Without reverse: allLines = [B→...→A, C→...→B] — legs are disconnected.
// With reverse: allLines = [C→...→B, B→...→A] — endpoint B connects the
// chain, creating the continuous target→source polyline that
// route animation and snapping logic expect.
segmentResults.reverse();
const allLines = segmentResults.flatMap((result) => result.lines);

@@ -35,0 +43,0 @@ const totalDistance = segmentResults.reduce((sum, result) => sum + result.totalDistance, 0);

import { type RouteLine } from '../types.js';
export declare function getRouteLength(routeLines: readonly RouteLine[]): number;
export declare function getRouteLength(routeLines: RouteLine[]): number;
//# sourceMappingURL=getRouteLength.d.ts.map

@@ -18,3 +18,3 @@ export interface PolygonVertex {

readonly p1: RoutePoint;
/** Virtual link rather than a physical path segment. May or may not change layers. */
/** Virtual (floor-transition) link rather than a physical path segment. */
readonly virtual: boolean;

@@ -35,6 +35,7 @@ }

/**
* Per-line virtual cost from the designer ("Virtual length"). When set it
* replaces the default policy (`0` = free/preferred, large = avoided);
* otherwise a layer-changing link costs `FLOOR_TRANSITION_PENALTY`, a
* same-layer one is free.
* Optional custom cost for virtual (floor-transition) links, configured
* per-line in the designer as "Virtual length". When set, the pathfinder
* uses this value instead of the default VIRTUAL_LINE_PENALTY, overriding it
* per line — `0` makes the transition free (preferred), a large value makes
* it avoided. When unset, the non-zero default keeps routes on the same floor.
*/

@@ -103,3 +104,6 @@ readonly virtualLength?: number;

readonly snapThreshold: number;
readonly minRemainingUnits?: number;
readonly to: RouteEndpoint;
readonly from?: RouteEndpoint;
}
//# sourceMappingURL=types.d.ts.map
import { Box } from '@expofp/geometry';
import { createRerouteController, getRouteLength, } from '../core/index.js';
import { createRerouteController, getRouteLength, snapToRoute, } from '../core/index.js';
import { createEndpointView } from './endpointView.js';

@@ -10,3 +10,2 @@ import { getRouteLines } from './getRouteLines.js';

import { computeRouteUpdate } from './routeUpdate.js';
import { snapPositionToRoute } from './snapPositionToRoute.js';
import { createTrailView } from './trailView.js';

@@ -89,2 +88,4 @@ import { createTransitionView } from './transitionView.js';

currentRouteLayer: getCurrentRouteLayer(),
from: currentFrom,
to: currentTo,
snap,

@@ -114,10 +115,8 @@ });

const fromAnchor = hasSnappedCurrentRoute ? null : pickAnchor(route.from);
// The trail tip is the route's graph endpoint on the same side as its
// off-graph anchor: first point for FROM, last point for TO.
if (fromAnchor) {
positionTrails.setTrail(route.routePoints[0], fromAnchor);
positionTrails.setTrail(route.routePoints[route.routePoints.length - 1], fromAnchor);
return;
}
const toAnchor = pickAnchor(route.to);
positionTrails.setTrail(toAnchor ? route.routePoints[route.routePoints.length - 1] : null, toAnchor);
positionTrails.setTrail(toAnchor ? route.routePoints[0] : null, toAnchor);
}

@@ -164,3 +163,7 @@ function syncRouteVisuals() {

// non-virtual route directly.
snapPositionToRoute(currentPosition, nonVirtual, currentFrom, currentTo, snapThreshold);
snapToRoute(currentPosition, nonVirtual, {
snapThreshold,
from: currentFrom,
to: currentTo,
});
if (result.snapped)

@@ -167,0 +170,0 @@ hasSnappedCurrentRoute = true;

@@ -13,4 +13,4 @@ import { CURRENT_POSITION_POINT_ID } from './positionView.js';

const toOnFloor = floorContext.isLayerVisible(to.layer);
const sourcePosition = visibleRoutePoints[0];
const destPosition = visibleRoutePoints[visibleRoutePoints.length - 1];
const sourcePosition = visibleRoutePoints[visibleRoutePoints.length - 1];
const destPosition = visibleRoutePoints[0];
const sourceInactive = from.id === CURRENT_POSITION_POINT_ID && snapped;

@@ -17,0 +17,0 @@ renderer.setIcon('source', {

@@ -0,3 +1,3 @@

import { snapToRoute, } from '../core/index.js';
import { createPositionView } from './positionView.js';
import { snapPositionToRoute } from './snapPositionToRoute.js';
const TRAIL_SLOT = 'trail';

@@ -15,3 +15,7 @@ export function createPositionTrailView({ renderer, iconProvider, floorContext, trails, iconLayer, snapThreshold, }) {

applyToRoute(position, route, routeLines) {
const snap = snapPositionToRoute(position, routeLines, route.from, route.to, snapThreshold);
const snap = snapToRoute(position, [...routeLines], {
snapThreshold,
from: route.from,
to: route.to,
});
const hidden = !floorContext.isLayerVisible(position.layer);

@@ -18,0 +22,0 @@ if (snap.snapped) {

@@ -1,2 +0,2 @@

import { type RouteLine, type RouteSnapResult } from '../core/index.js';
import { type RouteEndpoint, type RouteLine, type RouteSnapResult } from '../core/index.js';
interface RouteRenderData {

@@ -15,15 +15,19 @@ /** Route lines behind the current user position (empty when no snap). */

*
* The full non-virtual route is sliced down to the visible floor for rendering.
* The remaining distance is read from `snap.distance` (computed once by the
* runtime snap, `snapPositionToRoute`); with no snap it falls back to the full
* route length.
* The full non-virtual route is normalized to from→to direction once and
* sliced down to the visible floor for rendering. The remaining distance
* is read from `snap.distance` (computed once inside `snapToRoute`); when
* no snap is active we fall back to the full route length.
* @param root0
* @param root0.routeLines
* @param root0.currentRouteLayer
* @param root0.from
* @param root0.to
* @param root0.snap
*/
export declare function computeRouteRenderData({ routeLines, currentRouteLayer, snap, }: {
export declare function computeRouteRenderData({ routeLines, currentRouteLayer, from, to, snap, }: {
readonly routeLines: RouteLine[];
/** null = show-all mode; otherwise the active route floor. */
readonly currentRouteLayer: string | null;
readonly from: RouteEndpoint;
readonly to: RouteEndpoint;
readonly snap: RouteSnapResult | null;

@@ -30,0 +34,0 @@ }): RouteRenderData;

@@ -1,2 +0,2 @@

import { getRouteLength, splitRouteByPoint, } from '../core/index.js';
import { getRouteLength, normalizeRouteDirection, splitRouteByPoint, } from '../core/index.js';
/**

@@ -7,13 +7,15 @@ * Pure pipeline that produces the data needed to render the route: the

*
* The full non-virtual route is sliced down to the visible floor for rendering.
* The remaining distance is read from `snap.distance` (computed once by the
* runtime snap, `snapPositionToRoute`); with no snap it falls back to the full
* route length.
* The full non-virtual route is normalized to from→to direction once and
* sliced down to the visible floor for rendering. The remaining distance
* is read from `snap.distance` (computed once inside `snapToRoute`); when
* no snap is active we fall back to the full route length.
* @param root0
* @param root0.routeLines
* @param root0.currentRouteLayer
* @param root0.from
* @param root0.to
* @param root0.snap
*/
export function computeRouteRenderData({ routeLines, currentRouteLayer, snap, }) {
const nonVirtualOrdered = routeLines.filter((line) => !line.virtual);
export function computeRouteRenderData({ routeLines, currentRouteLayer, from, to, snap, }) {
const nonVirtualOrdered = normalizeRouteDirection(routeLines.filter((line) => !line.virtual), from, to);
const clipToVisibleFloor = (lines) => currentRouteLayer === null

@@ -36,6 +38,3 @@ ? lines

const splitOccurred = remaining !== nonVirtualOrdered;
// Anchor only when `remaining[0]` is on the snap's floor — at a transition it
// already belongs to the next floor, and keeping its own `p0` survives the clip.
const anchorAtSnap = splitOccurred && remaining.length > 0 && remaining[0].p0.layer === snap.snappedPoint.layer;
const adjustedRemaining = anchorAtSnap
const adjustedRemaining = splitOccurred && remaining.length > 0
? [{ ...remaining[0], p0: snap.snappedPoint }, ...remaining.slice(1)]

@@ -42,0 +41,0 @@ : remaining;

{
"name": "@expofp/wayfinding",
"version": "3.11.11",
"version": "3.11.12",
"type": "module",

@@ -37,3 +37,3 @@ "description": "ExpoFP SDK internal: framework-neutral wayfinding (routing, snapping, scene rendering)",

"tslib": "^2.3.0",
"@expofp/geometry": "3.11.11"
"@expofp/geometry": "3.11.12"
},

@@ -40,0 +40,0 @@ "peerDependencies": {

import { type RouteEndpoint, type RouteLine, type RoutePoint, type RouteSnapResult } from '../core/index.js';
/**
* Snap a position to the from→to route, resolving from/to booth containment
* first (domain logic kept out of the geometric `snapToRoute`):
* - inside the origin booth → route start, full distance remaining;
* - inside the destination booth → route end, zero remaining;
* - otherwise → geometric snap within `snapThreshold`.
*/
export declare function snapPositionToRoute(position: RoutePoint, routeLines: readonly RouteLine[], from: RouteEndpoint, to: RouteEndpoint, snapThreshold: number): RouteSnapResult;
//# sourceMappingURL=snapPositionToRoute.d.ts.map
import { pointInPolygon } from '../core/geometry/pointInPolygon.js';
import { getRouteLength, snapToRoute, } from '../core/index.js';
/** Whether `position` lies inside `endpoint`'s booth bounds on a matching layer. */
function insideBounds(position, endpoint, posLayer) {
if (!endpoint?.bounds)
return false;
const layerMatches = posLayer == null || !endpoint.layer || endpoint.layer === posLayer;
const containsPosition = pointInPolygon({ x: position.x, y: position.y }, endpoint.bounds);
return layerMatches && containsPosition;
}
/**
* Snap a position to the from→to route, resolving from/to booth containment
* first (domain logic kept out of the geometric `snapToRoute`):
* - inside the origin booth → route start, full distance remaining;
* - inside the destination booth → route end, zero remaining;
* - otherwise → geometric snap within `snapThreshold`.
*/
export function snapPositionToRoute(position, routeLines, from, to, snapThreshold) {
if (!routeLines.length)
return { snapped: false };
const posLayer = position.layer || null;
if (insideBounds(position, from, posLayer)) {
const first = routeLines[0];
return {
snapped: true,
snappedPoint: first.p0,
snappedLine: first,
distance: getRouteLength(routeLines),
};
}
if (insideBounds(position, to, posLayer)) {
const last = routeLines[routeLines.length - 1];
return { snapped: true, snappedPoint: last.p1, snappedLine: last, distance: 0 };
}
return snapToRoute(position, routeLines, { snapThreshold });
}