@camstack/types 1.2.392 → 1.2.394

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.
@@ -0,0 +1,104 @@
1
+ /**
2
+ * subject-path — the ONE geometry of "where did this subject go": the path a
3
+ * track's positions draw over a frame.
4
+ *
5
+ * Two surfaces draw it: the event detail of the debug views (admin-ui, an SVG
6
+ * over the hero frame) and the notification picture (addon-post-analysis, an
7
+ * SVG burnt into the JPEG by `sharp`, when a rule sets
8
+ * `media.showSubjectPath`). They must draw the SAME line for the same track,
9
+ * so the point list, its simplification and the mapping into a crop live here
10
+ * once. `@camstack/ui-library` re-exports this module for the UI side.
11
+ *
12
+ * It lives in `@camstack/types` and not in ui-library because the server
13
+ * addon that renders the notification picture cannot take ui-library (a React
14
+ * bundle for the browser), while `@camstack/types` is already the
15
+ * framework-free contract every side imports — the hub, the admin UI and the
16
+ * viewer (`@camstack/types/client`). No React, no DOM, no `sharp`: pure.
17
+ *
18
+ * ## Space
19
+ *
20
+ * Every input and output here is NORMALISED 0..1 in the frame the positions
21
+ * were captured in. A caller holding pixel positions divides by that frame
22
+ * first (the detection frame, never a native-res image it happens to show —
23
+ * the two share the aspect, which is what makes one normalised path land on
24
+ * both).
25
+ *
26
+ * ## Honesty rules (from real tracks, device 615, 2026-08-02)
27
+ *
28
+ * - A near-FULL-FRAME box is a motion placeholder, not a localisation: its
29
+ * centre is an artefact, so it is never a path point (it is counted).
30
+ * - A still subject repeats one box many times; consecutive samples closer
31
+ * than {@link SUBJECT_PATH_DWELL_EPS} collapse into one node carrying the
32
+ * count and the time span. The path always ENDS on the last sample.
33
+ * - A long track is downsampled to {@link SUBJECT_PATH_MAX_POINTS}, first and
34
+ * last kept.
35
+ * - Fewer than two nodes is no path: a caller draws nothing.
36
+ */
37
+ /** A rectangle in normalised 0..1 space. */
38
+ export interface SubjectPathRect {
39
+ readonly x: number;
40
+ readonly y: number;
41
+ readonly w: number;
42
+ readonly h: number;
43
+ }
44
+ /** A point in normalised 0..1 space. */
45
+ export interface SubjectPathPoint {
46
+ readonly x: number;
47
+ readonly y: number;
48
+ }
49
+ /** One position of the subject: its normalised CENTRE, the box extent when known, the instant. */
50
+ export interface SubjectPathSample {
51
+ readonly x: number;
52
+ readonly y: number;
53
+ /** Normalised box width — absent when only the centre is known. */
54
+ readonly w?: number;
55
+ /** Normalised box height — absent when only the centre is known. */
56
+ readonly h?: number;
57
+ /** Capture instant (ms). Absent = order of the input. */
58
+ readonly timestamp?: number;
59
+ }
60
+ /** One drawn node: a normalised centre and the samples that collapsed into it. */
61
+ export interface SubjectPathNode extends SubjectPathPoint {
62
+ readonly samples: number;
63
+ readonly firstTs: number;
64
+ readonly lastTs: number;
65
+ }
66
+ /** The drawable path of one track. */
67
+ export interface SubjectPath {
68
+ /** Oldest → newest. Fewer than 2 = nothing to draw. */
69
+ readonly nodes: readonly SubjectPathNode[];
70
+ /** Samples left out as motion placeholders (near-full-frame boxes). */
71
+ readonly unlocalized: number;
72
+ }
73
+ /** What to build the path from. */
74
+ export interface SubjectPathOptions {
75
+ /** Only samples at or before this instant (the frame the path is drawn on). */
76
+ readonly untilTimestamp?: number;
77
+ /** Downsample to at most this many nodes. Default {@link SUBJECT_PATH_MAX_POINTS}. */
78
+ readonly maxPoints?: number;
79
+ }
80
+ /** A box covering at least this fraction of BOTH frame sides is a placeholder. */
81
+ export declare const SUBJECT_PATH_FULL_FRAME_COVERAGE = 0.85;
82
+ /** Consecutive centres closer than this (normalised euclidean) are one dwell node. */
83
+ export declare const SUBJECT_PATH_DWELL_EPS = 0.005;
84
+ /** A path is downsampled to at most this many nodes. */
85
+ export declare const SUBJECT_PATH_MAX_POINTS = 120;
86
+ /** The sample of a normalised box: its centre, its extent. */
87
+ export declare function subjectPathSampleFromBox(bbox: SubjectPathRect, timestamp?: number): SubjectPathSample;
88
+ /**
89
+ * The path of one track from its samples (normalised, oldest → newest).
90
+ * Pure; never throws; malformed samples are skipped.
91
+ */
92
+ export declare function buildSubjectPath(samples: readonly SubjectPathSample[], opts?: SubjectPathOptions): SubjectPath;
93
+ /** True when the path has something to draw (two nodes or more). */
94
+ export declare function isDrawableSubjectPath(path: SubjectPath): boolean;
95
+ /**
96
+ * The path as seen through `view` — a normalised rectangle of the frame (the
97
+ * whole frame `{0,0,1,1}`, or the rectangle a crop cut). Points are re-mapped
98
+ * into the view's own 0..1 space and the line is CLIPPED to it: a path that
99
+ * leaves the crop and comes back is two runs. Runs of fewer than two points
100
+ * are dropped, so an empty result means "nothing to draw".
101
+ */
102
+ export declare function projectSubjectPath(points: readonly SubjectPathPoint[], view: SubjectPathRect): ReadonlyArray<readonly SubjectPathPoint[]>;
103
+ /** An SVG `points` attribute for a run in a `width` × `height` drawing ("x,y x,y …"). */
104
+ export declare function subjectPathSvgPoints(run: readonly SubjectPathPoint[], width: number, height: number): string;
package/dist/index.d.ts CHANGED
@@ -261,8 +261,9 @@ export { isAudioClassName, isBaseConditionKey, knownValues, NC_BASE_CONDITION_KE
261
261
  export { type PreparedAction, type PreparedAttachment, type PreparedNotification, prepareNotification, type ResolvedLevel, } from './notification/degrade-engine.js';
262
262
  export { htmlToText, markdownToHtmlLite, markdownToText, type NotificationFormat as NotificationBodyFormat, resolveFormat, textToHtml, transcodeBody, } from './notification/format-transcode.js';
263
263
  export { conditionExclusionReason, conditionVisibleForKind, defaultDeliveryForSection, droppedConditionsForKind, isAudioRule, isOccupancyRule, isSystemDelivery, isSystemRuleKind, kindsForDiscriminator, NC_AUDIO_SEED, NC_CAMERA_SCOPE_CAP_NAME, NC_OCCUPANCY_DEFAULTS, NC_RULE_EDITOR_SECTION_ORDER, NC_RULE_KIND_SPECS, NC_RULE_SECTIONS, NC_SYSTEM_DELIVERY, NC_SYSTEM_RULE_KINDS, type NcDroppedCondition, type NcKindVisibilitySubject, type NcRuleEditorSection, type NcRuleEditorSectionKey, type NcRuleKind, type NcRuleKindSpec, type NcRuleOwningSectionKey, type NcRuleSection, type NcRuleSectionKey, type NcRuleConditionsSubject, type NcRuleSectionSubject, type NcRuleSeed, parseRuleSection, ruleDeviceCompatCapNames, ruleDeviceCompatCapQueries, ruleEditorSectionsForKind, ruleGroupingApplies, ruleKindOf, ruleKindSpec, ruleMatchesSection, ruleSection, ruleSectionOf, ruleSeedForSection, systemFamilyOfKind, systemRuleKindOf, } from './notification/rule-kinds.js';
264
- export { imageModesForKind, legacyImageMode, NC_MEDIA_IMAGE_MODES, type NcMediaRuleSubject, ncMediaPolicyError, normalizeNcMediaPolicy, resolveNcMediaImage, ruleTriggerZoneIds, } from './notification/media-image.js';
264
+ export { imageModesForKind, legacyImageMode, NC_MEDIA_IMAGE_MODES, type NcMediaRuleSubject, ncMediaPolicyError, normalizeNcMediaPolicy, resolveNcMediaImage, ruleTriggerZoneIds, subjectHasPath, } from './notification/media-image.js';
265
265
  export { isScheduleActive } from './notification/schedule.js';
266
+ export { buildSubjectPath, isDrawableSubjectPath, projectSubjectPath, SUBJECT_PATH_DWELL_EPS, SUBJECT_PATH_FULL_FRAME_COVERAGE, SUBJECT_PATH_MAX_POINTS, type SubjectPath, type SubjectPathNode, type SubjectPathOptions, type SubjectPathPoint, type SubjectPathRect, type SubjectPathSample, subjectPathSampleFromBox, subjectPathSvgPoints, } from './geometry/subject-path.js';
266
267
  export { isAlarmControllableCap, NC_ALARM_CONTROLLABLE_CAPS, NC_ALARM_OPENING_EVENT_KINDS, NC_SENSOR_TRANSITION_NEVER_FIRES_WARNING, type NcAlarmControllableCap, type NcAlarmMembershipSubject, type NcAlarmOpeningEventKind, ruleAutoTriggersAlarm, ruleBelongsToAlarm, ruleGatedOnArmedState, sensorTransitionNeverFires, } from './notification/alarm-membership.js';
267
268
  export { alarmDeviceBlocksArming, alarmDeviceSecureOnArm, type AlarmDeviceBlocksInput, type AlarmDeviceSecureOnArmInput, type NcAlarmSecureActuation, secureActuationFor, } from './notification/alarm-controllable.js';
268
269
  export { alarmPatchLanded, applyBlockingEdit, applyModeDelaysEdits, applyModeDeviceListEdit, applyNonBlockingEdit, applySecureOnArmEdit, derivePostTriggeredAction, NC_ALARM_DEFAULT_TRIGGERED_DURATION_SEC, NC_ALARM_DELAY_FIELDS, NC_ALARM_NOTIFY_FIELDS, type NcAlarmDelayField, type NcAlarmDelaySettings, type NcAlarmDelaySource, type NcAlarmModeDeviceListEditResult, type NcAlarmModeNotifyPolicy, type NcAlarmNonBlockingEditResult, type NcAlarmNotifyField, type NcAlarmResolvedDelays, resolveModeDelays, resolveModeNotifyPolicy, } from './notification/alarm-settings.js';
package/dist/index.js CHANGED
@@ -14698,36 +14698,13 @@ var StorageKindSchema = zod.z.object({
14698
14698
  declaration: require_sleep.RecordKindDeclarationSchema
14699
14699
  });
14700
14700
  /**
14701
- * The collections retired by exact name by D870 (their data became keys of the
14702
- * camera's settings). No addon declares any of them any more; the engine still
14703
- * refuses one a running addon declared this boot. `dropCollection` may remove
14704
- * these, the per-addon scoped `addon-settings` / `addon-devices` / `sections`
14705
- * tables, and the tables of owners that are no longer installed - nothing else.
14706
- * The records-migration leftovers (D871 `stats-5m`, the phase-0 tables) were
14707
- * dropped by hand on the operator hubs and left this list with D900.
14708
- */
14709
- var STORAGE_ADMIN_MOVED_COLLECTIONS = [
14710
- "stream-broker:probe-snapshots",
14711
- "stream-broker:profile-map",
14712
- "stream-broker:rtsp-tokens",
14713
- "notification-center:device-mutes",
14714
- "recorder:recording-configs",
14715
- "pipeline-orchestrator:device-features"
14716
- ];
14717
- /** The per-addon blob tables a scoped name may carry. */
14718
- var STORAGE_ADMIN_SCOPED_BASES = [
14719
- "addon-settings",
14720
- "addon-devices",
14721
- "sections"
14722
- ];
14723
- /**
14724
- * storage-admin - the operator's maintenance reach into the settings database:
14725
- * list its tables, drop one that nothing owns any more (D870).
14701
+ * storage-admin - the operator's maintenance reach into the databases: list
14702
+ * their tables and record kinds, read a kind for diagnosis. Read-only: a table
14703
+ * the code no longer creates is dropped by hand, offline (D901).
14726
14704
  *
14727
14705
  * Hub-only, internal and `auth: 'admin'` explicitly (`internal` does not gate
14728
- * the mount, see `data-store-provider`). Both methods are LOGGED by the engine
14729
- * with the table and the counts. `dropCollection` is a ONE-SHOT of the D870
14730
- * migration and is marked for removal once the last hub has run it.
14706
+ * the mount, see `data-store-provider`). `listTables` is LOGGED by the engine
14707
+ * with the counts.
14731
14708
  *
14732
14709
  * `listTables` is READ-ONLY: it never creates a table, which `count` and `get`
14733
14710
  * on an undeclared scoped name used to do.
@@ -14743,26 +14720,6 @@ var storageAdminCapability = {
14743
14720
  * optionally only those owned by `ownerPrefix`. Reads only.
14744
14721
  */
14745
14722
  listTables: require_sleep.method(zod.z.object({ ownerPrefix: zod.z.string().min(1).optional() }), zod.z.object({ tables: zod.z.array(StorageTableSchema) }), { auth: "admin" }),
14746
- /**
14747
- * Drop one table. REFUSES when an addon declared it since the engine
14748
- * started (a running addon still owns it), when it is an engine or
14749
- * canonical table, when it does not exist, or when its row count is not
14750
- * `expectRows` - the caller states what it believes it is deleting. The
14751
- * engine also refuses anything but `<owner>:(addon-settings|addon-devices|
14752
- * sections)`, the retired collections by name, and tables of owners that
14753
- * are not installed (and refuses when it cannot read the installed list).
14754
- * Removed with the D870 migration script once the last hub ran it.
14755
- */
14756
- dropCollection: require_sleep.method(zod.z.object({
14757
- collection: zod.z.string().min(1),
14758
- expectRows: zod.z.number().int().nonnegative()
14759
- }), zod.z.object({
14760
- dropped: zod.z.literal(true),
14761
- rows: zod.z.number().int().nonnegative()
14762
- }), {
14763
- kind: "mutation",
14764
- auth: "admin"
14765
- }),
14766
14723
  /** Every record kind of both files with rows, a size estimate, class and retention (D881). */
14767
14724
  listKinds: require_sleep.method(zod.z.object({
14768
14725
  owner: zod.z.string().min(1).optional(),
@@ -44414,6 +44371,15 @@ var NcMediaPolicySchema = zod.z.object({
44414
44371
  * Replaces `zoneCrop`.
44415
44372
  */
44416
44373
  showTriggerZone: zod.z.boolean().optional(),
44374
+ /**
44375
+ * Draw the SUBJECT'S PATH on the picture — a line in the subject's colour
44376
+ * through the track's positions up to the frame shown, the same path the
44377
+ * debug views draw (`buildSubjectPath`, D903). Every picture mode a subject
44378
+ * is drawn on: a `crop` maps the path into the crop and clips it. Valid only
44379
+ * on a rule whose subject is a tracked object (refused on a sound rule);
44380
+ * dropped on `image: 'none'`. Absent = off.
44381
+ */
44382
+ showSubjectPath: zod.z.boolean().optional(),
44417
44383
  /** @deprecated D897 — accepted from an editor that predates `image`, folded into it on save. */
44418
44384
  attach: zod.z.enum([
44419
44385
  "best",
@@ -85053,12 +85019,6 @@ var METHOD_ACCESS_MAP = Object.freeze({
85053
85019
  addonId: null,
85054
85020
  access: "create"
85055
85021
  },
85056
- "storageAdmin.dropCollection": {
85057
- capName: "storage-admin",
85058
- capScope: "system",
85059
- addonId: null,
85060
- access: "delete"
85061
- },
85062
85022
  "storageAdmin.listKinds": {
85063
85023
  capName: "storage-admin",
85064
85024
  capScope: "system",
@@ -96356,22 +96316,35 @@ function resolveNcMediaImage(media, kind) {
96356
96316
  }
96357
96317
  /**
96358
96318
  * The stored shape of a media policy: `image` set, the legacy triple gone.
96319
+ * `showSubjectPath` survives on a kind with a tracked subject and a picture.
96359
96320
  * `showTriggerZone` survives only where it can be drawn — a zone trigger and a
96360
96321
  * mode other than `crop`/`none`; a legacy `zoneCrop` on a zone rule becomes it.
96361
96322
  * Every other field (gif, clip, rolls, speed, profile, linked cameras) is
96362
96323
  * carried through untouched. Pure; never throws.
96363
96324
  */
96364
96325
  function normalizeNcMediaPolicy(media, rule) {
96365
- const image = resolveNcMediaImage(media, ruleKindOf(rule));
96366
- const { attach: _attach, frame: _frame, zoneCrop, showTriggerZone, image: _image, ...rest } = media;
96326
+ const kind = ruleKindOf(rule);
96327
+ const image = resolveNcMediaImage(media, kind);
96328
+ const { attach: _attach, frame: _frame, zoneCrop, showTriggerZone, showSubjectPath, image: _image, ...rest } = media;
96367
96329
  const drawable = (showTriggerZone === true || media.image === void 0 && zoneCrop === true) && image !== "crop" && image !== "none" && ruleTriggerZoneIds(rule.conditions).length > 0;
96330
+ const pathDrawable = showSubjectPath === true && image !== "none" && subjectHasPath(kind);
96368
96331
  return {
96369
96332
  ...rest,
96370
96333
  image,
96371
- ...drawable ? { showTriggerZone: true } : {}
96334
+ ...drawable ? { showTriggerZone: true } : {},
96335
+ ...pathDrawable ? { showSubjectPath: true } : {}
96372
96336
  };
96373
96337
  }
96374
96338
  /**
96339
+ * True when a rule of this kind has a TRACKED subject whose path can be drawn
96340
+ * (`showSubjectPath`): the kinds whose subject owns a region of the frame. A
96341
+ * sound owns none, and a kind that carries no media has no picture.
96342
+ */
96343
+ function subjectHasPath(kind) {
96344
+ const spec = ruleKindSpec(kind);
96345
+ return spec.carriesMedia && spec.carriesSubjectCrop;
96346
+ }
96347
+ /**
96375
96348
  * Why a media policy cannot be SAVED on this rule, or null. Server-side and
96376
96349
  * editor alike: `showTriggerZone` on a rule whose trigger is not a zone, and a
96377
96350
  * mode the rule's kind cannot honour.
@@ -96383,6 +96356,7 @@ function ncMediaPolicyError(media, rule) {
96383
96356
  if (offered.length > 0 && !offered.includes(media.image)) return `media.image '${media.image}' is not available for a ${kind} rule (offered: ${offered.join(", ")})`;
96384
96357
  }
96385
96358
  if (media.showTriggerZone === true && ruleTriggerZoneIds(rule.conditions).length === 0) return "media.showTriggerZone needs a rule triggered by a zone (a zones condition or an occupancy zone)";
96359
+ if (media.showSubjectPath === true && !subjectHasPath(kind)) return `media.showSubjectPath needs a rule whose subject is a tracked object (not a ${kind} rule)`;
96386
96360
  return null;
96387
96361
  }
96388
96362
  //#endregion
@@ -96468,6 +96442,190 @@ function isScheduleActive(schedule, atMs) {
96468
96442
  return schedule.invert === true ? !inside : inside;
96469
96443
  }
96470
96444
  //#endregion
96445
+ //#region src/geometry/subject-path.ts
96446
+ /** A box covering at least this fraction of BOTH frame sides is a placeholder. */
96447
+ var SUBJECT_PATH_FULL_FRAME_COVERAGE = .85;
96448
+ /** Consecutive centres closer than this (normalised euclidean) are one dwell node. */
96449
+ var SUBJECT_PATH_DWELL_EPS = .005;
96450
+ /** A path is downsampled to at most this many nodes. */
96451
+ var SUBJECT_PATH_MAX_POINTS = 120;
96452
+ var clamp01 = (n) => n < 0 ? 0 : n > 1 ? 1 : n;
96453
+ /** The sample of a normalised box: its centre, its extent. */
96454
+ function subjectPathSampleFromBox(bbox, timestamp) {
96455
+ return {
96456
+ x: bbox.x + bbox.w / 2,
96457
+ y: bbox.y + bbox.h / 2,
96458
+ w: bbox.w,
96459
+ h: bbox.h,
96460
+ ...timestamp !== void 0 ? { timestamp } : {}
96461
+ };
96462
+ }
96463
+ function isPlaceholder(s) {
96464
+ return s.w !== void 0 && s.h !== void 0 && s.w >= .85 && s.h >= .85;
96465
+ }
96466
+ function downsample(xs, max) {
96467
+ if (xs.length <= max || max < 2) return xs;
96468
+ const step = (xs.length - 1) / (max - 1);
96469
+ const out = [];
96470
+ for (let i = 0; i < max - 1; i++) {
96471
+ const x = xs[Math.round(i * step)];
96472
+ if (x !== void 0) out.push(x);
96473
+ }
96474
+ const last = xs[xs.length - 1];
96475
+ if (last !== void 0) out.push(last);
96476
+ return out;
96477
+ }
96478
+ function isFiniteSample(s) {
96479
+ return Number.isFinite(s.x) && Number.isFinite(s.y);
96480
+ }
96481
+ /**
96482
+ * The path of one track from its samples (normalised, oldest → newest).
96483
+ * Pure; never throws; malformed samples are skipped.
96484
+ */
96485
+ function buildSubjectPath(samples, opts) {
96486
+ const until = opts?.untilTimestamp;
96487
+ const nodes = [];
96488
+ let unlocalized = 0;
96489
+ let tail = null;
96490
+ for (let index = 0; index < samples.length; index++) {
96491
+ const s = samples[index];
96492
+ if (s === void 0 || !isFiniteSample(s)) continue;
96493
+ if (until !== void 0 && s.timestamp !== void 0 && s.timestamp > until) continue;
96494
+ if (isPlaceholder(s)) {
96495
+ unlocalized += 1;
96496
+ continue;
96497
+ }
96498
+ const ts = s.timestamp !== void 0 && Number.isFinite(s.timestamp) ? s.timestamp : index;
96499
+ const x = clamp01(s.x);
96500
+ const y = clamp01(s.y);
96501
+ const prev = nodes[nodes.length - 1];
96502
+ if (prev !== void 0 && Math.hypot(x - prev.x, y - prev.y) < .005) {
96503
+ nodes[nodes.length - 1] = {
96504
+ ...prev,
96505
+ samples: prev.samples + 1,
96506
+ lastTs: Math.max(prev.lastTs, ts)
96507
+ };
96508
+ tail = {
96509
+ x,
96510
+ y,
96511
+ samples: 1,
96512
+ firstTs: ts,
96513
+ lastTs: ts
96514
+ };
96515
+ continue;
96516
+ }
96517
+ tail = null;
96518
+ nodes.push({
96519
+ x,
96520
+ y,
96521
+ samples: 1,
96522
+ firstTs: ts,
96523
+ lastTs: ts
96524
+ });
96525
+ }
96526
+ const end = nodes[nodes.length - 1];
96527
+ if (tail !== null && end !== void 0 && end.samples > 1 && (tail.x !== end.x || tail.y !== end.y)) {
96528
+ nodes[nodes.length - 1] = {
96529
+ ...end,
96530
+ samples: end.samples - 1
96531
+ };
96532
+ nodes.push(tail);
96533
+ }
96534
+ return {
96535
+ nodes: downsample(nodes, opts?.maxPoints ?? 120),
96536
+ unlocalized
96537
+ };
96538
+ }
96539
+ /** True when the path has something to draw (two nodes or more). */
96540
+ function isDrawableSubjectPath(path) {
96541
+ return path.nodes.length >= 2;
96542
+ }
96543
+ function inside(p) {
96544
+ return p.x >= 0 && p.x <= 1 && p.y >= 0 && p.y <= 1;
96545
+ }
96546
+ /** Liang–Barsky: the part of segment a→b inside the unit square, or null. */
96547
+ function clipSegment(a, b) {
96548
+ const dx = b.x - a.x;
96549
+ const dy = b.y - a.y;
96550
+ let t0 = 0;
96551
+ let t1 = 1;
96552
+ const edges = [
96553
+ [-dx, a.x],
96554
+ [dx, 1 - a.x],
96555
+ [-dy, a.y],
96556
+ [dy, 1 - a.y]
96557
+ ];
96558
+ for (const [p, q] of edges) {
96559
+ if (p === 0) {
96560
+ if (q < 0) return null;
96561
+ continue;
96562
+ }
96563
+ const r = q / p;
96564
+ if (p < 0) {
96565
+ if (r > t1) return null;
96566
+ if (r > t0) t0 = r;
96567
+ } else {
96568
+ if (r < t0) return null;
96569
+ if (r < t1) t1 = r;
96570
+ }
96571
+ }
96572
+ return [{
96573
+ x: a.x + t0 * dx,
96574
+ y: a.y + t0 * dy
96575
+ }, {
96576
+ x: a.x + t1 * dx,
96577
+ y: a.y + t1 * dy
96578
+ }];
96579
+ }
96580
+ function samePoint(a, b) {
96581
+ return Math.abs(a.x - b.x) < 1e-9 && Math.abs(a.y - b.y) < 1e-9;
96582
+ }
96583
+ /**
96584
+ * The path as seen through `view` — a normalised rectangle of the frame (the
96585
+ * whole frame `{0,0,1,1}`, or the rectangle a crop cut). Points are re-mapped
96586
+ * into the view's own 0..1 space and the line is CLIPPED to it: a path that
96587
+ * leaves the crop and comes back is two runs. Runs of fewer than two points
96588
+ * are dropped, so an empty result means "nothing to draw".
96589
+ */
96590
+ function projectSubjectPath(points, view) {
96591
+ if (points.length < 2 || !(view.w > 0) || !(view.h > 0)) return [];
96592
+ const local = points.map((p) => ({
96593
+ x: (p.x - view.x) / view.w,
96594
+ y: (p.y - view.y) / view.h
96595
+ }));
96596
+ const runs = [];
96597
+ let run = [];
96598
+ for (let i = 1; i < local.length; i++) {
96599
+ const a = local[i - 1];
96600
+ const b = local[i];
96601
+ if (a === void 0 || b === void 0) continue;
96602
+ const seg = clipSegment(a, b);
96603
+ if (seg === null) {
96604
+ if (run.length >= 2) runs.push(run);
96605
+ run = [];
96606
+ continue;
96607
+ }
96608
+ const [from, to] = seg;
96609
+ const last = run[run.length - 1];
96610
+ if (last === void 0 || !samePoint(last, from)) {
96611
+ if (run.length >= 2) runs.push(run);
96612
+ run = [from];
96613
+ }
96614
+ run.push(to);
96615
+ if (!inside(b)) {
96616
+ if (run.length >= 2) runs.push(run);
96617
+ run = [];
96618
+ }
96619
+ }
96620
+ if (run.length >= 2) runs.push(run);
96621
+ return runs;
96622
+ }
96623
+ /** An SVG `points` attribute for a run in a `width` × `height` drawing ("x,y x,y …"). */
96624
+ function subjectPathSvgPoints(run, width, height) {
96625
+ const fmt = (v) => String(Math.round(v * 100) / 100);
96626
+ return run.map((p) => `${fmt(p.x * width)},${fmt(p.y * height)}`).join(" ");
96627
+ }
96628
+ //#endregion
96471
96629
  //#region src/notification/alarm-membership.ts
96472
96630
  var ARMED_WORD_PREFIX = "armed_";
96473
96631
  var ALARM_PANEL_CAP = "alarm-panel";
@@ -105240,12 +105398,13 @@ exports.STATE_LABELS = STATE_LABELS;
105240
105398
  exports.STATE_LABEL_KEYS = STATE_LABEL_KEYS;
105241
105399
  exports.STATIONARY_OVERLAY_STYLE = STATIONARY_OVERLAY_STYLE;
105242
105400
  exports.STORAGE_ACCESS_FALLBACK = STORAGE_ACCESS_FALLBACK;
105243
- exports.STORAGE_ADMIN_MOVED_COLLECTIONS = STORAGE_ADMIN_MOVED_COLLECTIONS;
105244
- exports.STORAGE_ADMIN_SCOPED_BASES = STORAGE_ADMIN_SCOPED_BASES;
105245
105401
  exports.STORAGE_BYTES_PER_GB = STORAGE_BYTES_PER_GB;
105246
105402
  exports.STORAGE_LOCATION_MODES = STORAGE_LOCATION_MODES;
105247
105403
  exports.STREAM_PROFILE_META = STREAM_PROFILE_META;
105248
105404
  exports.STREAM_QUALITY_LABELS = STREAM_QUALITY_LABELS;
105405
+ exports.SUBJECT_PATH_DWELL_EPS = SUBJECT_PATH_DWELL_EPS;
105406
+ exports.SUBJECT_PATH_FULL_FRAME_COVERAGE = SUBJECT_PATH_FULL_FRAME_COVERAGE;
105407
+ exports.SUBJECT_PATH_MAX_POINTS = SUBJECT_PATH_MAX_POINTS;
105249
105408
  exports.SUB_DETECTION_TYPES = SUB_DETECTION_TYPES;
105250
105409
  exports.SYSTEM_CAP_NAMES = SYSTEM_CAP_NAMES;
105251
105410
  exports.SYSTEM_SCOPE_DEVICE_METHODS = SYSTEM_SCOPE_DEVICE_METHODS;
@@ -105637,6 +105796,7 @@ exports.buildNcTaxonomy = buildNcTaxonomy;
105637
105796
  exports.buildRoleScopes = buildRoleScopes;
105638
105797
  exports.buildSecretRef = require_sleep.buildSecretRef;
105639
105798
  exports.buildStreamParamsConfigSchema = buildStreamParamsConfigSchema;
105799
+ exports.buildSubjectPath = buildSubjectPath;
105640
105800
  exports.buildVideoArgs = require_composition.buildVideoArgs;
105641
105801
  exports.buildViewerLink = buildViewerLink;
105642
105802
  exports.builtCapabilityBlockers = builtCapabilityBlockers;
@@ -105958,6 +106118,7 @@ exports.isDeviceConfigCap = require_sleep.isDeviceConfigCap;
105958
106118
  exports.isDeviceScopedCap = require_sleep.isDeviceScopedCap;
105959
106119
  exports.isDoActionActuatable = isDoActionActuatable;
105960
106120
  exports.isDoPickerDeviceType = isDoPickerDeviceType;
106121
+ exports.isDrawableSubjectPath = isDrawableSubjectPath;
105961
106122
  exports.isEmbeddingModelFailed = isEmbeddingModelFailed;
105962
106123
  exports.isEvent = require_sleep.isEvent;
105963
106124
  exports.isEventKindLabelKey = isEventKindLabelKey;
@@ -106171,6 +106332,7 @@ exports.pressureSensorCapability = pressureSensorCapability;
106171
106332
  exports.principalMayReachAddon = principalMayReachAddon;
106172
106333
  exports.privacyMaskCapability = privacyMaskCapability;
106173
106334
  exports.procedureAuthKey = procedureAuthKey;
106335
+ exports.projectSubjectPath = projectSubjectPath;
106174
106336
  exports.providerMotionEvent = providerMotionEvent;
106175
106337
  exports.ptzAutotrackCapability = ptzAutotrackCapability;
106176
106338
  exports.ptzCapability = ptzCapability;
@@ -106350,6 +106512,9 @@ exports.streamSignalsCapability = streamSignalsCapability;
106350
106512
  exports.stripRetiredBandPreBufferSec = stripRetiredBandPreBufferSec;
106351
106513
  exports.strippedOfLegacyReadOnly = strippedOfLegacyReadOnly;
106352
106514
  exports.subKindsOf = subKindsOf;
106515
+ exports.subjectHasPath = subjectHasPath;
106516
+ exports.subjectPathSampleFromBox = subjectPathSampleFromBox;
106517
+ exports.subjectPathSvgPoints = subjectPathSvgPoints;
106353
106518
  exports.summarisePrivacyAudio = summarisePrivacyAudio;
106354
106519
  exports.summarizeEffectiveScope = summarizeEffectiveScope;
106355
106520
  exports.summarizeReview = summarizeReview;