@7365admin1/layer-common 4.49.0 → 4.51.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.
@@ -14,12 +14,19 @@
14
14
  <path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3" />
15
15
  </svg>
16
16
  </v-btn>
17
+ <button v-if="hasRoutes" type="button" class="hero-mini__toggle" :aria-pressed="showRoutes" title="Show the robots' lanes and waypoints" @click="showRoutes = !showRoutes">Routes</button>
17
18
  <select v-if="floors.length" :value="floor" class="hero-input hero-select hero-select--sm" aria-label="Floor" @change="chosen = Number(($event.target as HTMLSelectElement).value)">
18
19
  <option v-for="f in floors" :key="f" :value="f">{{ floorName(f) }}</option>
19
20
  </select>
20
21
  </div>
21
22
 
22
23
  <RobotFloorPlan ref="plan">
24
+ <template v-if="showRoutes && overlay.lanes.length + overlay.waypoints.length">
25
+ <svg class="hero-plan__svg" viewBox="0 0 100 100" preserveAspectRatio="none" aria-hidden="true">
26
+ <line v-for="(l, i) in overlay.lanes" :key="`l${i}`" class="hero-mini__lane" :class="{ 'hero-mini__lane--oneway': l.oneWay }" :x1="l.a.x" :y1="l.a.y" :x2="l.b.x" :y2="l.b.y" vector-effect="non-scaling-stroke" />
27
+ </svg>
28
+ <span v-for="(w, i) in overlay.waypoints" :key="`w${i}`" class="hero-mini__wp" :class="`hero-mini__wp--${w.kind}`" :style="{ left: `${w.x}%`, top: `${w.y}%` }" :title="w.name" aria-hidden="true" />
29
+ </template>
23
30
  <RobotPatrolRoutes v-if="type === 'security' && floorRoutes.length" :routes="floorRoutes" />
24
31
  <button
25
32
  v-for="r in robots"
@@ -40,6 +47,7 @@
40
47
  <div class="hero-mini__foot">
41
48
  <span>Floor plan from robot (placeholder)</span>
42
49
  <span v-if="type === 'security'"><span class="hero-mini__legend" aria-hidden="true" />Patrol routes</span>
50
+ <span v-if="showRoutes && hasRoutes"><span class="hero-mini__legend hero-mini__legend--lane" aria-hidden="true" />Robot lanes</span>
43
51
  <span v-if="floor !== null" class="hero-mini__count">{{ robots.length }} on {{ floorName(floor) }}</span>
44
52
  </div>
45
53
  <div v-if="unplaced" class="hero-notice">{{ unplaced }}</div>
@@ -48,6 +56,7 @@
48
56
 
49
57
  <script setup lang="ts">
50
58
  import { computed, ref } from "vue";
59
+ import { routeOverlay } from "../../utils/rmf-editor";
51
60
  import RobotFloorPlan from "./FloorPlan.vue";
52
61
  import RobotPatrolRoutes from "./PatrolRoutes.vue";
53
62
  import RobotReconnecting from "./Reconnecting.vue";
@@ -79,8 +88,10 @@ const props = withDefaults(
79
88
  brands?: Array<{ brand?: unknown; displayName?: unknown }>;
80
89
  /** One telemetry tick; positions glide across it. */
81
90
  tickMs?: number;
91
+ /** The site's building (saved map or live HERO Fleet Core building) - its robot lanes and waypoints can be shown. */
92
+ map?: Record<string, any> | null;
82
93
  }>(),
83
- { type: "cleaning", routes: () => ({}), connected: true, brands: () => [], tickMs: 4000 },
94
+ { type: "cleaning", routes: () => ({}), connected: true, brands: () => [], tickMs: 4000, map: null },
84
95
  );
85
96
  const emit = defineEmits<{ open: [id: string] }>();
86
97
 
@@ -90,6 +101,10 @@ const floors = computed(() => mapFloors(ids.value, props.states));
90
101
  const chosen = ref<number | null>(null);
91
102
  const floor = computed(() => pickFloor(floors.value, chosen.value, ids.value, props.states));
92
103
  const robots = computed(() => mapRobots(props.rows, props.states, floor.value));
104
+ /** The robots' lanes and waypoints on the floor shown (toggle; on by default once the building has any). */
105
+ const overlay = computed(() => (floor.value === null ? { lanes: [], waypoints: [], fiducials: [] } : routeOverlay(props.map, floor.value)));
106
+ const hasRoutes = computed(() => !!props.map && (props.map.rmf?.levels ?? props.map.levels ?? []).some((l: any) => (l.lanes ?? []).length));
107
+ const showRoutes = ref(true);
93
108
  const floorName = (f: number) => floorLabel({ floorNo: f, floorKnown: true });
94
109
 
95
110
  /** The routes walked on this floor, once each. A route with no floor is drawn on its robot's floor. */
@@ -72,6 +72,7 @@
72
72
  :routes="routes"
73
73
  :connected="connected"
74
74
  :brands="brands"
75
+ :map="routeMap"
75
76
  @open="onMapOpen"
76
77
  />
77
78
  </aside>
@@ -126,7 +127,26 @@ const emit = defineEmits<{
126
127
  status: [status: { connected: boolean; faults: number; siteName: string; clientName: string }];
127
128
  }>();
128
129
 
129
- const { getSiteFleet, getSiteSummary, getRobotBrands, getPatrolRoutes } = useRobots();
130
+ const { getSiteFleet, getSiteSummary, getRobotBrands, getPatrolRoutes, listSiteBuildingMaps, getBuildingMap, getFleetCoreBuilding } = useRobots();
131
+
132
+ /**
133
+ * The site's building for the mini-map's robot lanes: the live HERO Fleet
134
+ * Core building at a fleet site, else the newest saved map. Read once per
135
+ * site (lanes do not move); a failure just leaves the lanes off.
136
+ */
137
+ const routeMap = ref<Record<string, any> | null>(null);
138
+ async function loadRouteMap() {
139
+ const site = props.site;
140
+ try {
141
+ const fleetSite = rows.value.some((r) => r?.brand === "fleet-core");
142
+ const found = fleetSite
143
+ ? await getFleetCoreBuilding(site).catch(() => null)
144
+ : await listSiteBuildingMaps(site, { limit: 1 }).then((res: any) => (res?.items?.[0]?._id ? getBuildingMap(String(res.items[0]._id)) : null));
145
+ if (site === props.site) routeMap.value = found ?? null;
146
+ } catch {
147
+ if (site === props.site) routeMap.value = null;
148
+ }
149
+ }
130
150
 
131
151
  const headers = [
132
152
  { title: "Name", key: "name", sortable: false },
@@ -253,6 +273,8 @@ watch(
253
273
  { immediate: true },
254
274
  );
255
275
  watch(ids, loadRoutes);
276
+ // The mini-map's lanes: once per site, again if the site turns out to have fleet robots.
277
+ watch([() => props.site, () => rows.value.some((r) => r?.brand === "fleet-core")], loadRouteMap, { immediate: true });
256
278
  getRobotBrands().then(
257
279
  (res) => (brands.value = res?.items ?? []),
258
280
  () => undefined,
@@ -69,10 +69,13 @@ export default function useMember() {
69
69
  type = "",
70
70
  status = "active",
71
71
  siteId = "",
72
+ roleState = "",
72
73
  } = {}) {
73
74
  const query: Record<string, any> = { page, limit, type, status };
74
75
 
75
76
  if (search) query.search = search;
77
+ // "other-site": only members holding a role pinned to another site.
78
+ if (roleState) query.roleState = roleState;
76
79
  if (user) query.user = user;
77
80
  if (org) query.org = org;
78
81
  if (siteId) query.siteId = siteId;
@@ -385,6 +385,19 @@ export default function useRobots() {
385
385
  });
386
386
  }
387
387
 
388
+ /**
389
+ * Read a building file (traffic-editor / rmf_site building.yaml) or one
390
+ * route (nav graph) file into the editor. Nothing is saved: the result is
391
+ * the map's `rmf` part, which the editor shows until it is saved.
392
+ */
393
+ function importBuildingFile(body: { format: "building" | "nav"; text: string; graph?: number }) {
394
+ return useNuxtApp().$api<Record<string, any>>("/api/robots/building-maps/import", {
395
+ method: "POST",
396
+ query: { appSlug },
397
+ body,
398
+ });
399
+ }
400
+
388
401
  /**
389
402
  * One floor's plan image. A Blob, not a URL: the image sits behind the
390
403
  * session like the map itself, and an <img src> cannot carry the header.
@@ -400,7 +413,7 @@ export default function useRobots() {
400
413
  function saveFloorPlan(
401
414
  mapId: string,
402
415
  floorNo: number,
403
- body: { version: number; image?: string; widthPx: number; heightPx: number; metersPerPixel: number; tx: number; ty: number },
416
+ body: { version: number; image?: string; widthPx: number; heightPx: number; metersPerPixel: number; tx: number; ty: number; yawRad?: number; space?: "world" },
404
417
  ) {
405
418
  return useNuxtApp().$api<Record<string, any>>(`/api/robots/building-maps/${mapId}/floor-plans/${floorNo}`, {
406
419
  method: "PUT",
@@ -467,6 +480,7 @@ export default function useRobots() {
467
480
  deleteBuildingMap,
468
481
  getFleetCoreBuildingData,
469
482
  exportBuildingMap,
483
+ importBuildingFile,
470
484
  getFloorPlanImage,
471
485
  saveFloorPlan,
472
486
  removeFloorPlan,
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@7365admin1/layer-common",
3
3
  "license": "MIT",
4
4
  "type": "module",
5
- "version": "4.49.0",
5
+ "version": "4.51.0",
6
6
  "author": "7365admin1",
7
7
  "main": "./nuxt.config.ts",
8
8
  "//files": "What a consumer extending this layer actually loads. Without this npm ships the whole working tree - the changesets, the CI workflows, the render harness in tools/ and any scratch directory that happened to exist at publish time. Nuxt resolves a layer by directory, so every runtime directory below has to stay listed; adding a new top-level runtime directory means adding it here too.",
@@ -36,7 +36,7 @@
36
36
  "build": "nuxt build .playground",
37
37
  "generate": "nuxt generate .playground",
38
38
  "preview": "nuxt preview .playground",
39
- "test": "esbuild composables/useANPRSettings.ts --format=esm --outfile=test/.build/useANPRSettings.mjs --log-level=error && esbuild composables/useVisitorSocket.ts --format=esm --outfile=test/.build/useVisitorSocket.mjs --log-level=error && esbuild composables/usePlateToastWindow.ts --format=esm --outfile=test/.build/usePlateToastWindow.mjs --log-level=error && esbuild utils/client-invite-modules.ts --bundle --platform=node --format=esm --outfile=test/.build/client-invite-modules.mjs --log-level=error && esbuild utils/robot-display.ts --bundle --platform=node --format=esm --outfile=test/.build/robot-display.mjs --log-level=error && esbuild utils/building-map.ts --bundle --platform=node --format=esm --outfile=test/.build/building-map.mjs --log-level=error && esbuild utils/robot-building.ts --bundle --platform=node --format=esm --outfile=test/.build/robot-building.mjs --log-level=error && esbuild utils/robot-fleet-core.ts --bundle --platform=node --format=esm --outfile=test/.build/robot-fleet-core.mjs --log-level=error && node --experimental-strip-types --test \"test/*.test.mjs\" && yarn test:units",
39
+ "test": "esbuild composables/useANPRSettings.ts --format=esm --outfile=test/.build/useANPRSettings.mjs --log-level=error && esbuild composables/useVisitorSocket.ts --format=esm --outfile=test/.build/useVisitorSocket.mjs --log-level=error && esbuild composables/usePlateToastWindow.ts --format=esm --outfile=test/.build/usePlateToastWindow.mjs --log-level=error && esbuild utils/client-invite-modules.ts --bundle --platform=node --format=esm --outfile=test/.build/client-invite-modules.mjs --log-level=error && esbuild utils/robot-display.ts --bundle --platform=node --format=esm --outfile=test/.build/robot-display.mjs --log-level=error && esbuild utils/building-map.ts --bundle --platform=node --format=esm --outfile=test/.build/building-map.mjs --log-level=error && esbuild utils/robot-building.ts --bundle --platform=node --format=esm --outfile=test/.build/robot-building.mjs --log-level=error && esbuild utils/robot-fleet-core.ts --bundle --platform=node --format=esm --outfile=test/.build/robot-fleet-core.mjs --log-level=error && esbuild utils/rmf-editor.ts --bundle --platform=node --format=esm --outfile=test/.build/rmf-editor.mjs --log-level=error && node --experimental-strip-types --test \"test/*.test.mjs\" && yarn test:units",
40
40
  "test:units": "node --experimental-strip-types --test \"utils/*.test.ts\"",
41
41
  "release": "yarn run build && changeset publish"
42
42
  },