@expofp/wayfinding
Advanced tools
@@ -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; |
+2
-2
| { | ||
| "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 }); | ||
| } |
3454
0.09%153474
-0.57%108
-1.82%+ Added
- Removed
Updated