@7365admin1/core 3.83.0 → 3.84.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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@7365admin1/core",
3
3
  "license": "MIT",
4
- "version": "3.83.0",
4
+ "version": "3.84.0",
5
5
  "author": "7365admin1",
6
6
  "main": "dist/index.js",
7
7
  "module": "dist/index.mjs",
@@ -13,7 +13,7 @@
13
13
  "build": "tsup src/index.ts --format cjs,esm --dts && shx cp -r src/public dist && shx cp -r src/utils/handlebars dist",
14
14
  "release": "yarn run build && changeset publish",
15
15
  "lint": "tsc",
16
- "test": "tsup src/config.ts src/utils/robot-cloud.util.ts src/utils/robot-adapter.util.ts src/utils/robot-tracking.util.ts src/utils/robot-apps.util.ts src/utils/robot-permission.util.ts src/utils/lift-contract.util.ts src/utils/anpr-revoke.util.ts src/utils/anpr-plate.util.ts src/utils/vehicle-request.util.ts src/utils/camera-view.util.ts src/utils/camera-bulk-import.util.ts src/utils/dashboard-metrics.util.ts src/utils/camera-capability.util.ts src/utils/camera-playback.util.ts src/services/camera-device-http.service.ts src/utils/camera-alert.util.ts src/utils/dahua-protocol.util.ts src/utils/site-name.util.ts src/utils/service-provider-invite.util.ts src/utils/notification-category.util.ts src/utils/hid-card.util.ts src/utils/hid-json.util.ts src/utils/hid-access.util.ts src/models/customer-site.model.ts src/models/site-camera.model.ts src/models/member.model.ts src/models/subscription.model.ts src/utils/session-id.util.ts src/utils/occurrence-query.util.ts src/utils/occurrence-book-create.util.ts src/utils/update-result.util.ts src/utils/hid-amico-authz.util.ts src/utils/promo-code-currency.util.ts src/utils/console-audit.util.ts src/utils/org-suspension.util.ts src/utils/session-revoke.util.ts src/models/hid-amico.model.ts src/models/patrol-email.model.ts src/utils/patrol-email-pdf.util.ts src/utils/visitor-status.util.ts src/utils/self-service-email.util.ts src/utils/hid-secret.util.ts src/models/visitor-invite.model.ts src/models/guard-invite.model.ts src/utils/site-timezone.util.ts src/utils/bulletin-service-apps.util.ts src/models/bulletin-board.model.ts src/utils/vehicle-import.util.ts src/utils/expired-vehicle-sweep.util.ts src/models/vehicle.model.ts src/controllers/chat.controller.ts src/utils/file-owner.util.ts src/utils/console-permission.util.ts src/utils/role-scope.util.ts src/utils/org-default-roles.util.ts src/utils/org-modules.util.ts src/utils/site-modules.util.ts src/utils/resident-app-modules.util.ts src/utils/resident-default-role.util.ts src/utils/invite-role-scope.util.ts src/models/personal-emergency-chain.model.ts src/utils/self-signup-files.util.ts src/models/person.model.ts src/utils/module-gate.util.ts src/utils/member-lookup.util.ts src/utils/notification-access.util.ts src/utils/module-access.util.ts src/services/module-gate.service.ts src/controllers/site.controller.ts src/controllers/organization.controller.ts src/controllers/member.controller.ts src/utils/org-owner-repair.util.ts src/utils/socket-session.util.ts src/utils/people-create-gate.util.ts src/utils/service-app-type.util.ts --format esm --out-dir test/.build --no-dts --silent && node --test test/*.test.mjs",
16
+ "test": "tsup src/config.ts src/utils/building-map.util.ts src/utils/robot-cloud.util.ts src/utils/robot-adapter.util.ts src/utils/robot-tracking.util.ts src/utils/robot-apps.util.ts src/utils/robot-permission.util.ts src/utils/lift-contract.util.ts src/utils/anpr-revoke.util.ts src/utils/anpr-plate.util.ts src/utils/vehicle-request.util.ts src/utils/camera-view.util.ts src/utils/camera-bulk-import.util.ts src/utils/dashboard-metrics.util.ts src/utils/camera-capability.util.ts src/utils/camera-playback.util.ts src/services/camera-device-http.service.ts src/utils/camera-alert.util.ts src/utils/dahua-protocol.util.ts src/utils/site-name.util.ts src/utils/service-provider-invite.util.ts src/utils/notification-category.util.ts src/utils/hid-card.util.ts src/utils/hid-json.util.ts src/utils/hid-access.util.ts src/models/customer-site.model.ts src/models/site-camera.model.ts src/models/member.model.ts src/models/subscription.model.ts src/utils/session-id.util.ts src/utils/occurrence-query.util.ts src/utils/occurrence-book-create.util.ts src/utils/update-result.util.ts src/utils/hid-amico-authz.util.ts src/utils/promo-code-currency.util.ts src/utils/console-audit.util.ts src/utils/org-suspension.util.ts src/utils/session-revoke.util.ts src/models/hid-amico.model.ts src/models/patrol-email.model.ts src/utils/patrol-email-pdf.util.ts src/utils/visitor-status.util.ts src/utils/self-service-email.util.ts src/utils/hid-secret.util.ts src/models/visitor-invite.model.ts src/models/guard-invite.model.ts src/utils/site-timezone.util.ts src/utils/bulletin-service-apps.util.ts src/models/bulletin-board.model.ts src/utils/vehicle-import.util.ts src/utils/expired-vehicle-sweep.util.ts src/models/vehicle.model.ts src/controllers/chat.controller.ts src/utils/file-owner.util.ts src/utils/console-permission.util.ts src/utils/role-scope.util.ts src/utils/org-default-roles.util.ts src/utils/org-modules.util.ts src/utils/site-modules.util.ts src/utils/resident-app-modules.util.ts src/utils/resident-default-role.util.ts src/utils/invite-role-scope.util.ts src/models/personal-emergency-chain.model.ts src/utils/self-signup-files.util.ts src/models/person.model.ts src/utils/module-gate.util.ts src/utils/member-lookup.util.ts src/utils/notification-access.util.ts src/utils/module-access.util.ts src/services/module-gate.service.ts src/controllers/site.controller.ts src/controllers/organization.controller.ts src/controllers/member.controller.ts src/utils/org-owner-repair.util.ts src/utils/socket-session.util.ts src/utils/people-create-gate.util.ts src/utils/service-app-type.util.ts --format esm --out-dir test/.build --no-dts --silent && node --test test/*.test.mjs",
17
17
  "test:camera": "yarn test",
18
18
  "test:e2e": "yarn build && node --test --test-concurrency=1 --test-timeout=600000 \"test/e2e/*.test.mjs\""
19
19
  },
@@ -0,0 +1,305 @@
1
+ import assert from "node:assert/strict";
2
+ import { test } from "node:test";
3
+
4
+ import {
5
+ DEFAULT_SCALE_M_PER_PX,
6
+ WALL_HEIGHT_M,
7
+ WALL_THICKNESS_M,
8
+ buildingMapFromRobotMaps,
9
+ buildingSolids,
10
+ levelPixelToMetres,
11
+ levelScale,
12
+ levelSolids,
13
+ scaleFromMeasurement,
14
+ wallsFromOccupancyGrid,
15
+ } from "./.build/utils/building-map.util.mjs";
16
+
17
+ /* --------------------------------------------------------------------------
18
+ * A floor plan is pixels; everything downstream is metres. Get the scale wrong
19
+ * and a building is silently the wrong size - which is far harder to notice
20
+ * than an error, because every screen still renders.
21
+ *
22
+ * The shape and the defaults are Open-RMF's, taken from a real
23
+ * office.building.yaml in open-rmf/rmf_demos and traffic_editor's documented
24
+ * behaviour, so a map made here can be exported to RMF and one made there read
25
+ * here.
26
+ * ----------------------------------------------------------------------- */
27
+
28
+ const level = (over = {}) => ({
29
+ name: "L1",
30
+ elevationM: 0,
31
+ scaleMPerPx: 0.05,
32
+ originXM: 0,
33
+ originYM: 0,
34
+ vertices: [],
35
+ walls: [],
36
+ floors: [],
37
+ doors: [],
38
+ measurements: [],
39
+ ...over,
40
+ });
41
+
42
+ /* -------------------------------------------------------- RMF's defaults */
43
+
44
+ test("the wall defaults are traffic_editor's, not ours", () => {
45
+ // "By default, the walls are of thickness of 10cm and height 2.5m."
46
+ assert.equal(WALL_HEIGHT_M, 2.5);
47
+ assert.equal(WALL_THICKNESS_M, 0.1);
48
+ // traffic_editor's default before a measurement is taken.
49
+ assert.equal(DEFAULT_SCALE_M_PER_PX, 0.05);
50
+ });
51
+
52
+ /* -------------------------------------------------------------- the scale */
53
+
54
+ test("a measurement sets metres per pixel", () => {
55
+ // 200px across something known to be 10m.
56
+ assert.equal(scaleFromMeasurement(200, 10), 0.05);
57
+ assert.equal(scaleFromMeasurement(100, 10), 0.1);
58
+ });
59
+
60
+ test("a nonsense measurement is refused, not silently accepted", () => {
61
+ // A zero or negative scale makes every distance in the building wrong, and
62
+ // wrong-by-a-factor is much harder to spot than a thrown error.
63
+ for (const px of [0, -1, NaN, Infinity]) {
64
+ assert.throws(() => scaleFromMeasurement(px, 10), /pixel distance/);
65
+ }
66
+ for (const m of [0, -5, NaN]) {
67
+ assert.throws(() => scaleFromMeasurement(200, m), /real distance/);
68
+ }
69
+ });
70
+
71
+ test("a level's measurement beats its declared scale", () => {
72
+ const l = level({
73
+ scaleMPerPx: 0.5,
74
+ vertices: [{ x: 0, y: 0 }, { x: 200, y: 0 }],
75
+ measurements: [{ start: 0, end: 1, distanceM: 10 }],
76
+ });
77
+ assert.equal(levelScale(l), 0.05);
78
+ });
79
+
80
+ test("with no usable measurement it falls back, and never to zero", () => {
81
+ assert.equal(levelScale(level({ scaleMPerPx: 0.03 })), 0.03);
82
+ assert.equal(levelScale(level({ scaleMPerPx: 0 })), DEFAULT_SCALE_M_PER_PX);
83
+
84
+ // A measurement between two vertices at the same point is not usable.
85
+ const degenerate = level({
86
+ scaleMPerPx: 0.02,
87
+ vertices: [{ x: 5, y: 5 }, { x: 5, y: 5 }],
88
+ measurements: [{ start: 0, end: 1, distanceM: 10 }],
89
+ });
90
+ assert.equal(levelScale(degenerate), 0.02);
91
+ });
92
+
93
+ /* --------------------------------------------------------- the transform */
94
+
95
+ test("pixels become site metres, with the vertical flip", () => {
96
+ // Image origin is top-left, map origin bottom-left. Without the flip a
97
+ // building comes out mirrored, which reads as "the robot is in the wrong
98
+ // room" rather than as a transform bug.
99
+ const l = level({ originXM: -49.713, originYM: -35.846, scaleMPerPx: 0.05 });
100
+ const p = levelPixelToMetres(l, 100, 200, 800);
101
+
102
+ assert.equal(Number(p.x.toFixed(4)), Number((-49.713 + 100 * 0.05).toFixed(4)));
103
+ assert.equal(Number(p.y.toFixed(4)), Number((-35.846 + (800 - 200) * 0.05).toFixed(4)));
104
+ });
105
+
106
+ /* ------------------------------------------------------- 3D generation */
107
+
108
+ test("a wall becomes a box standing ON the floor", () => {
109
+ const l = level({
110
+ vertices: [{ x: 0, y: 0 }, { x: 200, y: 0 }],
111
+ walls: [{ start: 0, end: 1 }],
112
+ });
113
+ const { walls } = levelSolids(l, 100);
114
+
115
+ assert.equal(walls.length, 1);
116
+ const w = walls[0];
117
+ assert.equal(w.lengthM, 10); // 200px * 0.05
118
+ assert.equal(w.heightM, WALL_HEIGHT_M);
119
+ assert.equal(w.thicknessM, WALL_THICKNESS_M);
120
+ // Boxes are centred, so a wall on the floor sits at half its height.
121
+ assert.equal(w.centre.z, WALL_HEIGHT_M / 2);
122
+ assert.equal(w.centre.x, 5);
123
+ });
124
+
125
+ test("a wall on an upper level stands on THAT level's floor", () => {
126
+ const l = level({
127
+ elevationM: 6,
128
+ vertices: [{ x: 0, y: 0 }, { x: 100, y: 0 }],
129
+ walls: [{ start: 0, end: 1 }],
130
+ });
131
+ assert.equal(levelSolids(l, 100).walls[0].centre.z, 6 + WALL_HEIGHT_M / 2);
132
+ });
133
+
134
+ test("wall rotation follows the segment", () => {
135
+ const l = level({
136
+ vertices: [{ x: 0, y: 0 }, { x: 0, y: 100 }],
137
+ walls: [{ start: 0, end: 1 }],
138
+ });
139
+ // Pixel y grows downward and the transform flips it, so a wall drawn down the
140
+ // image runs in -y on the ground.
141
+ assert.equal(Math.abs(levelSolids(l, 100).walls[0].yaw), Math.PI / 2);
142
+ });
143
+
144
+ test("a zero-length wall is skipped, not emitted as a degenerate box", () => {
145
+ // Two annotations on one vertex would otherwise flicker like a graphics fault.
146
+ const l = level({
147
+ vertices: [{ x: 10, y: 10 }],
148
+ walls: [{ start: 0, end: 0 }, { start: 0, end: 9 }],
149
+ });
150
+ assert.deepEqual(levelSolids(l, 100).walls, []);
151
+ });
152
+
153
+ test("a floor polygon needs three points", () => {
154
+ const l = level({
155
+ vertices: [{ x: 0, y: 0 }, { x: 100, y: 0 }, { x: 100, y: 100 }],
156
+ floors: [{ vertices: [0, 1] }, { vertices: [0, 1, 2] }],
157
+ });
158
+ const { floors } = levelSolids(l, 100);
159
+ assert.equal(floors.length, 1);
160
+ assert.equal(floors[0].points.length, 3);
161
+ assert.equal(floors[0].hole, false);
162
+ });
163
+
164
+ test("a hole is kept as a hole — that is how a lift shaft is cut", () => {
165
+ const l = level({
166
+ vertices: [{ x: 0, y: 0 }, { x: 10, y: 0 }, { x: 10, y: 10 }],
167
+ floors: [{ vertices: [0, 1, 2], hole: true }],
168
+ });
169
+ assert.equal(levelSolids(l, 100).floors[0].hole, true);
170
+ });
171
+
172
+ test("every level is generated for a whole-building view", () => {
173
+ const map = {
174
+ name: "b",
175
+ lifts: [],
176
+ levels: [level({ name: "L1" }), level({ name: "L2", elevationM: 3 })],
177
+ };
178
+ const solids = buildingSolids(map, () => 100);
179
+ assert.deepEqual(solids.map((s) => s.level), ["L1", "L2"]);
180
+ assert.deepEqual(solids.map((s) => s.elevationM), [0, 3]);
181
+ });
182
+
183
+ /* ------------------------------------------ automatic wall extraction */
184
+
185
+ /** A grid where 0 is a wall and 255 is free space. */
186
+ function grid(rows) {
187
+ const height = rows.length;
188
+ const width = rows[0].length;
189
+ const data = new Uint8Array(width * height);
190
+ rows.forEach((row, y) =>
191
+ [...row].forEach((ch, x) => {
192
+ data[y * width + x] = ch === "#" ? 0 : 255;
193
+ }),
194
+ );
195
+ return { data, width, height };
196
+ }
197
+
198
+ test("a long dark run in a floor plan becomes ONE wall, not one per pixel", () => {
199
+ // The requirement: uploading a floor plan produces a layout without anyone
200
+ // drawing walls. A SLAM map already marks walls as dark pixels.
201
+ const { vertices, walls } = wallsFromOccupancyGrid(
202
+ grid(["..........", "##########", ".........."]),
203
+ { minRunPx: 4 },
204
+ );
205
+
206
+ assert.equal(walls.length, 1);
207
+ assert.equal(vertices[walls[0].start].x, 0);
208
+ assert.equal(vertices[walls[0].end].x, 9);
209
+ assert.equal(vertices[walls[0].start].y, 1);
210
+ });
211
+
212
+ test("vertical walls are found too", () => {
213
+ const { vertices, walls } = wallsFromOccupancyGrid(
214
+ grid(["..#..", "..#..", "..#..", "..#..", "..#.."]),
215
+ { minRunPx: 4 },
216
+ );
217
+ assert.equal(walls.length, 1);
218
+ assert.equal(vertices[walls[0].start].x, 2);
219
+ assert.equal(vertices[walls[0].end].y, 4);
220
+ });
221
+
222
+ test("a closed room yields its four walls", () => {
223
+ const { walls } = wallsFromOccupancyGrid(
224
+ grid([
225
+ "######",
226
+ "#....#",
227
+ "#....#",
228
+ "#....#",
229
+ "######",
230
+ ]),
231
+ { minRunPx: 3 },
232
+ );
233
+ // Two horizontal runs, and the two uprights left once those are consumed.
234
+ assert.equal(walls.length, 4);
235
+ });
236
+
237
+ test("speckle below the minimum run is ignored as noise", () => {
238
+ const { walls } = wallsFromOccupancyGrid(
239
+ grid(["#.#.#.#.#.", ".#.#.#.#.#", "#.#.#.#.#."]),
240
+ { minRunPx: 4 },
241
+ );
242
+ assert.deepEqual(walls, []);
243
+ });
244
+
245
+ test("corners are shared, so the map stays editable afterwards", () => {
246
+ const { vertices } = wallsFromOccupancyGrid(
247
+ grid(["#####", "#...#", "#####"]),
248
+ { minRunPx: 3 },
249
+ );
250
+ // De-duplicated: a vertex appears once however many walls meet there.
251
+ const keys = new Set(vertices.map((v) => `${v.x}:${v.y}`));
252
+ assert.equal(keys.size, vertices.length);
253
+ });
254
+
255
+ test("an unreadable grid returns nothing rather than throwing", () => {
256
+ assert.deepEqual(wallsFromOccupancyGrid({ data: [], width: 0, height: 0 }), {
257
+ vertices: [], walls: [],
258
+ });
259
+ assert.deepEqual(wallsFromOccupancyGrid({ data: [1, 2], width: 10, height: 10 }), {
260
+ vertices: [], walls: [],
261
+ });
262
+ });
263
+
264
+ /* ------------------------------------------- from the robot's own maps */
265
+
266
+ test("the robot's floor maps become a building, lowest floor first", () => {
267
+ const map = buildingMapFromRobotMaps("Tengfei Tower", [
268
+ { floorNo: 3, resolution: 0.05, originX: -10, originY: -20 },
269
+ { floorNo: 1, resolution: 0.05, originX: -49.713, originY: -35.846 },
270
+ ]);
271
+
272
+ assert.deepEqual(map.levels.map((l) => l.name), ["L1", "L3"]);
273
+ assert.deepEqual(map.levels.map((l) => l.elevationM), [0, 6]);
274
+ assert.equal(map.levels[0].originXM, -49.713);
275
+ assert.equal(map.levels[0].scaleMPerPx, 0.05);
276
+ });
277
+
278
+ test("a cleaning area becomes an RMF cleaning-zone waypoint", () => {
279
+ // Their areas ARE RMF's zones - the Clean Task's own marker.
280
+ const map = buildingMapFromRobotMaps("b", [
281
+ { floorNo: 1, areaList: [{ areaName: "AREA-FF0B0B83", x: 925, y: 604 }] },
282
+ ]);
283
+ const zone = map.levels[0].vertices.find((v) => v.isCleaningZone);
284
+ assert.equal(zone.name, "AREA-FF0B0B83");
285
+ assert.equal(zone.x, 925);
286
+ });
287
+
288
+ test("a dock at 0,0 on every floor is NOT planted as a charger", () => {
289
+ // The vendor's sample reports baseX/baseY as 0 for every floor. A charger at
290
+ // the origin of every level is worse than none.
291
+ const none = buildingMapFromRobotMaps("b", [{ floorNo: 1, baseX: 0, baseY: 0 }]);
292
+ assert.equal(none.levels[0].vertices.some((v) => v.isCharger), false);
293
+
294
+ const real = buildingMapFromRobotMaps("b", [
295
+ { floorNo: 1, baseX: 12.5, baseY: 3.25, baseTheta: 1.57 },
296
+ ]);
297
+ const charger = real.levels[0].vertices.find((v) => v.isCharger);
298
+ assert.equal(charger.x, 12.5);
299
+ assert.equal(charger.theta, 1.57);
300
+ });
301
+
302
+ test("a missing resolution falls back to traffic_editor's default", () => {
303
+ const map = buildingMapFromRobotMaps("b", [{ floorNo: 1 }]);
304
+ assert.equal(map.levels[0].scaleMPerPx, DEFAULT_SCALE_M_PER_PX);
305
+ });