@sakibcoolz/core 0.1.1 → 0.2.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.
Files changed (142) hide show
  1. package/README.md +67 -7
  2. package/dist/acquisition.d.ts +29 -0
  3. package/dist/acquisition.d.ts.map +1 -0
  4. package/dist/acquisition.js +30 -0
  5. package/dist/acquisition.js.map +1 -0
  6. package/dist/assembly.d.ts +5 -2
  7. package/dist/assembly.d.ts.map +1 -1
  8. package/dist/assembly.js +7 -4
  9. package/dist/assembly.js.map +1 -1
  10. package/dist/attach.d.ts +4 -2
  11. package/dist/attach.d.ts.map +1 -1
  12. package/dist/attach.js +5 -3
  13. package/dist/attach.js.map +1 -1
  14. package/dist/clipboard.d.ts +6 -4
  15. package/dist/clipboard.d.ts.map +1 -1
  16. package/dist/clipboard.js +25 -4
  17. package/dist/clipboard.js.map +1 -1
  18. package/dist/collision.d.ts +2 -2
  19. package/dist/collision.js.map +1 -1
  20. package/dist/digest.d.ts +9 -0
  21. package/dist/digest.d.ts.map +1 -0
  22. package/dist/digest.js +68 -0
  23. package/dist/digest.js.map +1 -0
  24. package/dist/flow.d.ts +3 -2
  25. package/dist/flow.d.ts.map +1 -1
  26. package/dist/flow.js.map +1 -1
  27. package/dist/graph.d.ts +67 -7
  28. package/dist/graph.d.ts.map +1 -1
  29. package/dist/graph.js +195 -10
  30. package/dist/graph.js.map +1 -1
  31. package/dist/index.d.ts +13 -6
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +7 -3
  34. package/dist/index.js.map +1 -1
  35. package/dist/observation.d.ts +1 -1
  36. package/dist/observation.d.ts.map +1 -1
  37. package/dist/observation.js +4 -1
  38. package/dist/observation.js.map +1 -1
  39. package/dist/routing.d.ts +6 -3
  40. package/dist/routing.d.ts.map +1 -1
  41. package/dist/routing.js +12 -2
  42. package/dist/routing.js.map +1 -1
  43. package/dist/scene.d.ts +245 -5
  44. package/dist/scene.d.ts.map +1 -1
  45. package/dist/scene.js +94 -3
  46. package/dist/scene.js.map +1 -1
  47. package/dist/serialization.d.ts +7 -7
  48. package/dist/serialization.d.ts.map +1 -1
  49. package/dist/serialization.js +120 -8
  50. package/dist/serialization.js.map +1 -1
  51. package/dist/snapping.d.ts +2 -2
  52. package/dist/snapping.d.ts.map +1 -1
  53. package/dist/snapping.js +10 -7
  54. package/dist/snapping.js.map +1 -1
  55. package/dist/status.d.ts +56 -0
  56. package/dist/status.d.ts.map +1 -0
  57. package/dist/status.js +114 -0
  58. package/dist/status.js.map +1 -0
  59. package/dist/tags.d.ts +52 -0
  60. package/dist/tags.d.ts.map +1 -0
  61. package/dist/tags.js +103 -0
  62. package/dist/tags.js.map +1 -0
  63. package/dist/telemetry.d.ts +42 -4
  64. package/dist/telemetry.d.ts.map +1 -1
  65. package/dist/telemetry.js +84 -8
  66. package/dist/telemetry.js.map +1 -1
  67. package/dist/touching.d.ts +2 -2
  68. package/dist/touching.d.ts.map +1 -1
  69. package/dist/touching.js +7 -4
  70. package/dist/touching.js.map +1 -1
  71. package/dist/units.js +1 -1
  72. package/dist/units.js.map +1 -1
  73. package/dist/upgrade.d.ts +2 -2
  74. package/dist/upgrade.js.map +1 -1
  75. package/dist/validation.d.ts +33 -6
  76. package/dist/validation.d.ts.map +1 -1
  77. package/dist/validation.js +806 -39
  78. package/dist/validation.js.map +1 -1
  79. package/package.json +4 -2
  80. package/schema/fixtures/invalid/association-without-relationship.scene.json +69 -0
  81. package/schema/fixtures/invalid/binding-kind.scene.json +14 -0
  82. package/schema/fixtures/invalid/command-zero-step.scene.json +105 -0
  83. package/schema/fixtures/invalid/empty-name.scene.json +30 -0
  84. package/schema/fixtures/invalid/expected.json +70 -0
  85. package/schema/fixtures/invalid/extension-not-namespaced.scene.json +10 -0
  86. package/schema/fixtures/invalid/extension-prototype-key.scene.json +12 -0
  87. package/schema/fixtures/invalid/missing-document-type.scene.json +6 -0
  88. package/schema/fixtures/invalid/non-string-label.scene.json +32 -0
  89. package/schema/fixtures/invalid/underlay-opacity.scene.json +17 -0
  90. package/schema/fixtures/invalid/unknown-component-kind.scene.json +29 -0
  91. package/schema/fixtures/invalid/unknown-relationship.scene.json +63 -0
  92. package/schema/fixtures/invalid/unknown-root-field.scene.json +8 -0
  93. package/schema/fixtures/invalid/unsupported-version.scene.json +7 -0
  94. package/schema/fixtures/invalid/view-rotation.scene.json +139 -0
  95. package/schema/fixtures/invalid/writable-string.scene.json +93 -0
  96. package/schema/fixtures/invalid/writable-without-limits.scene.json +100 -0
  97. package/schema/fixtures/invalid/zero-scale.scene.json +29 -0
  98. package/schema/fixtures/migration/README.md +18 -0
  99. package/schema/fixtures/migration/v1-demo.scene.json +160 -0
  100. package/schema/fixtures/migration/v1-demo.v4.json +198 -0
  101. package/schema/fixtures/migration/v2-demo.scene.json +244 -0
  102. package/schema/fixtures/migration/v2-demo.v4.json +245 -0
  103. package/schema/fixtures/migration/v2-generated-12.scene.json +473 -0
  104. package/schema/fixtures/migration/v2-generated-12.v4.json +474 -0
  105. package/schema/fixtures/migration/v3-demo.scene.json +244 -0
  106. package/schema/fixtures/migration/v3-demo.v4.json +245 -0
  107. package/schema/fixtures/migration/v3-generated-12.scene.json +473 -0
  108. package/schema/fixtures/migration/v3-generated-12.v4.json +474 -0
  109. package/schema/fixtures/migration/v3-mostly-connected-plant.scene.json +386 -0
  110. package/schema/fixtures/migration/v3-mostly-connected-plant.v4.json +387 -0
  111. package/schema/fixtures/migration/v3-open-joints-plant.scene.json +288 -0
  112. package/schema/fixtures/migration/v3-open-joints-plant.v4.json +289 -0
  113. package/schema/fixtures/migration/v3-unconnected-plant.scene.json +14 -0
  114. package/schema/fixtures/migration/v3-unconnected-plant.v4.json +276 -0
  115. package/schema/fixtures/projection/acquisition.plan.json +50 -0
  116. package/schema/fixtures/projection/control-line.plan.json +86 -0
  117. package/schema/fixtures/valid/acquisition.scene.json +117 -0
  118. package/schema/fixtures/valid/associations.scene.json +79 -0
  119. package/schema/fixtures/valid/commands.scene.json +155 -0
  120. package/schema/fixtures/valid/control-line.scene.json +210 -0
  121. package/schema/fixtures/valid/empty.scene.json +7 -0
  122. package/schema/fixtures/valid/identity-and-extensions.scene.json +96 -0
  123. package/schema/fixtures/valid/tank-valve-line.scene.json +89 -0
  124. package/schema/fixtures/valid/views.scene.json +139 -0
  125. package/schema/fixtures/validator-only/association-id-reuses-connection.scene.json +70 -0
  126. package/schema/fixtures/validator-only/association-unknown-component.scene.json +70 -0
  127. package/schema/fixtures/validator-only/command-max-below-min.scene.json +104 -0
  128. package/schema/fixtures/validator-only/command-out-of-type-range.scene.json +97 -0
  129. package/schema/fixtures/validator-only/connection-domain-mismatch.scene.json +63 -0
  130. package/schema/fixtures/validator-only/credential-in-connection.scene.json +66 -0
  131. package/schema/fixtures/validator-only/credential-in-extension.scene.json +14 -0
  132. package/schema/fixtures/validator-only/credential-label.scene.json +32 -0
  133. package/schema/fixtures/validator-only/duplicate-component-id.scene.json +50 -0
  134. package/schema/fixtures/validator-only/endpoint-with-credentials.scene.json +74 -0
  135. package/schema/fixtures/validator-only/expected.json +66 -0
  136. package/schema/fixtures/validator-only/modbus-coil-float.scene.json +100 -0
  137. package/schema/fixtures/validator-only/non-unit-rotation.scene.json +29 -0
  138. package/schema/fixtures/validator-only/unknown-data-source.scene.json +100 -0
  139. package/schema/fixtures/validator-only/unknown-port.scene.json +62 -0
  140. package/schema/fixtures/validator-only/unregistered-host-type.scene.json +29 -0
  141. package/schema/fixtures/validator-only/view-unknown-component.scene.json +143 -0
  142. package/schema/industrial-scene.v4.schema.json +365 -0
@@ -1,28 +1,138 @@
1
+ import { SCENE_DOCUMENT_TYPE, viewKinds } from './scene.js';
1
2
  import { validateScene } from './validation.js';
3
+ const byKey = ([a], [b]) => (a < b ? -1 : a > b ? 1 : 0);
4
+ /** An object with its keys sorted, as plain data (keys such as "__proto__" stay data). */
5
+ function sortedObject(record, map = (v) => v) {
6
+ const out = {};
7
+ for (const [key, value] of Object.entries(record).sort(byKey)) {
8
+ Object.defineProperty(out, key, { value: map(value), enumerable: true, writable: true, configurable: true });
9
+ }
10
+ return out;
11
+ }
12
+ /** Extension JSON with object keys sorted at every level; arrays keep their order. */
13
+ function canonicalJson(value) {
14
+ if (Array.isArray(value))
15
+ return value.map(canonicalJson);
16
+ if (value !== null && typeof value === 'object')
17
+ return sortedObject(value, canonicalJson);
18
+ return value;
19
+ }
20
+ /** Views in fixed order (process, electrical), with symbol and route ids sorted (ADR-0026). */
21
+ function canonicalViews(views) {
22
+ const out = {};
23
+ for (const kind of viewKinds) {
24
+ const layout = views[kind];
25
+ if (!layout)
26
+ continue;
27
+ out[kind] = {
28
+ ...(layout.symbols
29
+ ? { symbols: sortedObject(layout.symbols, (s) => ({ x: s.x, y: s.y, ...(s.rotation !== undefined ? { rotation: s.rotation } : {}), ...(s.hidden ? { hidden: true } : {}) })) }
30
+ : {}),
31
+ ...(layout.routes ? { routes: sortedObject(layout.routes, (r) => ({ points: r.points.map(([x, y]) => [x, y]) })) } : {}),
32
+ };
33
+ }
34
+ return out;
35
+ }
2
36
  /**
3
- * Canonical JSON for a scene: current schema version (3), vectors as named objects in x, y, z(, w)
4
- * order, fixed key order, property keys sorted,
5
- * two-space indent and a trailing newline. Only authored fields are written, so the same scene
6
- * always produces the same bytes. Array order (components, connections) is preserved.
37
+ * Canonical JSON for a scene: current schema version (4, ADR-0021), vectors as named objects in
38
+ * x, y, z(, w) order, fixed key order, keys of properties, labels, extensions and view entries sorted, two-space
39
+ * indent and a trailing newline. Only authored fields are written, so the same scene always
40
+ * produces the same bytes. Array order (components, connections) is preserved.
7
41
  */
8
42
  export function serializeScene(scene) {
43
+ const p = scene.project;
9
44
  const document = {
10
- schemaVersion: 3,
45
+ schemaVersion: 4,
46
+ documentType: SCENE_DOCUMENT_TYPE,
11
47
  units: 'm',
48
+ ...(p
49
+ ? {
50
+ project: {
51
+ ...(p.id !== undefined ? { id: p.id } : {}),
52
+ ...(p.name !== undefined ? { name: p.name } : {}),
53
+ ...(p.revision !== undefined ? { revision: p.revision } : {}),
54
+ ...(p.description !== undefined ? { description: p.description } : {}),
55
+ },
56
+ }
57
+ : {}),
12
58
  components: scene.components.map((c) => ({
13
59
  id: c.id,
60
+ ...(c.name !== undefined ? { name: c.name } : {}),
14
61
  type: c.type,
62
+ ...(c.typeVersion !== undefined ? { typeVersion: c.typeVersion } : {}),
63
+ ...(c.manufacturer !== undefined ? { manufacturer: c.manufacturer } : {}),
64
+ ...(c.model !== undefined ? { model: c.model } : {}),
15
65
  ...(c.parentId !== undefined ? { parentId: c.parentId } : {}),
16
66
  position: { x: c.position.x, y: c.position.y, z: c.position.z },
17
67
  rotation: { x: c.rotation.x, y: c.rotation.y, z: c.rotation.z, w: c.rotation.w },
18
68
  scale: { x: c.scale.x, y: c.scale.y, z: c.scale.z },
19
- properties: Object.fromEntries(Object.entries(c.properties).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))),
69
+ properties: sortedObject(c.properties),
70
+ ...(c.labels !== undefined ? { labels: sortedObject(c.labels) } : {}),
71
+ ...(c.extensions !== undefined ? { extensions: sortedObject(c.extensions, canonicalJson) } : {}),
20
72
  })),
21
73
  connections: scene.connections.map((c) => ({
22
74
  id: c.id,
75
+ ...(c.domain !== undefined ? { domain: c.domain } : {}),
76
+ ...(c.relationship !== undefined ? { relationship: c.relationship } : {}),
23
77
  from: { instanceId: c.from.instanceId, portId: c.from.portId },
24
78
  to: { instanceId: c.to.instanceId, portId: c.to.portId },
79
+ ...(c.properties !== undefined ? { properties: sortedObject(c.properties) } : {}),
25
80
  })),
81
+ ...(scene.associations
82
+ ? {
83
+ associations: scene.associations.map((a) => ({
84
+ id: a.id,
85
+ relationship: a.relationship,
86
+ from: a.from,
87
+ to: a.to,
88
+ ...(a.properties !== undefined ? { properties: sortedObject(a.properties) } : {}),
89
+ })),
90
+ }
91
+ : {}),
92
+ ...(scene.dataSources
93
+ ? {
94
+ dataSources: scene.dataSources.map((s) => ({
95
+ id: s.id,
96
+ ...(s.name !== undefined ? { name: s.name } : {}),
97
+ protocol: s.protocol,
98
+ ...(s.deviceId !== undefined ? { deviceId: s.deviceId } : {}),
99
+ ...(s.endpointRef !== undefined ? { endpointRef: s.endpointRef } : {}),
100
+ ...(s.properties !== undefined ? { properties: sortedObject(s.properties) } : {}),
101
+ })),
102
+ }
103
+ : {}),
104
+ ...(scene.runtimeVariables
105
+ ? {
106
+ runtimeVariables: scene.runtimeVariables.map((v) => ({
107
+ id: v.id,
108
+ ...(v.name !== undefined ? { name: v.name } : {}),
109
+ ...(v.componentId !== undefined ? { componentId: v.componentId } : {}),
110
+ dataType: v.dataType,
111
+ ...(v.unit !== undefined ? { unit: v.unit } : {}),
112
+ access: v.access,
113
+ ...(v.source
114
+ ? {
115
+ source: {
116
+ dataSourceId: v.source.dataSourceId,
117
+ ...(v.source.variable !== undefined ? { variable: v.source.variable } : {}),
118
+ ...(v.source.address !== undefined ? { address: canonicalJson(v.source.address) } : {}),
119
+ },
120
+ }
121
+ : {}),
122
+ ...(v.scaling ? { scaling: { kind: v.scaling.kind, rawMin: v.scaling.rawMin, rawMax: v.scaling.rawMax, min: v.scaling.min, max: v.scaling.max } } : {}),
123
+ ...(v.command
124
+ ? {
125
+ command: {
126
+ ...(v.command.min !== undefined ? { min: v.command.min } : {}),
127
+ ...(v.command.max !== undefined ? { max: v.command.max } : {}),
128
+ ...(v.command.step !== undefined ? { step: v.command.step } : {}),
129
+ ...(v.command.confirm !== undefined ? { confirm: v.command.confirm } : {}),
130
+ },
131
+ }
132
+ : {}),
133
+ })),
134
+ }
135
+ : {}),
26
136
  ...(scene.tagBindings
27
137
  ? {
28
138
  tagBindings: scene.tagBindings.map((b) => ({
@@ -45,13 +155,15 @@ export function serializeScene(scene) {
45
155
  },
46
156
  }
47
157
  : {}),
158
+ ...(scene.views !== undefined ? { views: canonicalViews(scene.views) } : {}),
159
+ ...(scene.extensions !== undefined ? { extensions: sortedObject(scene.extensions, canonicalJson) } : {}),
48
160
  };
49
161
  // JSON.stringify already writes -0 as 0.
50
162
  return `${JSON.stringify(document, null, 2)}\n`;
51
163
  }
52
- /** Parses scene JSON text: size check, JSON parse, then `validateScene` (which migrates version 1). */
164
+ /** Parses scene JSON text: size check, JSON parse, then `validateScene` (which migrates versions 1–3). */
53
165
  export function parseScene(text, options) {
54
- const maxLength = options.maxLength ?? 10_000_000;
166
+ const maxLength = options.maxLength ?? 32_000_000;
55
167
  if (text.length > maxLength) {
56
168
  return { ok: false, errors: [{ code: 'LIMIT_EXCEEDED', path: '', message: `Scene text is longer than ${maxLength} characters.` }] };
57
169
  }
@@ -1 +1 @@
1
- {"version":3,"file":"serialization.js","sourceRoot":"","sources":["../src/serialization.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAA2D,MAAM,iBAAiB,CAAC;AAOzG;;;;;GAKG;AACH,MAAM,UAAU,cAAc,CAAC,KAAiB;IAC9C,MAAM,QAAQ,GAAG;QACf,aAAa,EAAE,CAAC;QAChB,KAAK,EAAE,GAAG;QACV,UAAU,EAAE,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YACvC,EAAE,EAAE,CAAC,CAAC,EAAE;YACR,IAAI,EAAE,CAAC,CAAC,IAAI;YACZ,GAAG,CAAC,CAAC,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAC7D,QAAQ,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE;YAC/D,QAAQ,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE;YAChF,KAAK,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE;YACnD,UAAU,EAAE,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;SAC9G,CAAC,CAAC;QACH,WAAW,EAAE,KAAK,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YACzC,EAAE,EAAE,CAAC,CAAC,EAAE;YACR,IAAI,EAAE,EAAE,UAAU,EAAE,CAAC,CAAC,IAAI,CAAC,UAAU,EAAE,MAAM,EAAE,CAAC,CAAC,IAAI,CAAC,MAAM,EAAE;YAC9D,EAAE,EAAE,EAAE,UAAU,EAAE,CAAC,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,CAAC,CAAC,EAAE,CAAC,MAAM,EAAE;SACzD,CAAC,CAAC;QACH,GAAG,CAAC,KAAK,CAAC,WAAW;YACnB,CAAC,CAAC;gBACE,WAAW,EAAE,KAAK,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;oBACzC,EAAE,EAAE,CAAC,CAAC,EAAE;oBACR,KAAK,EAAE,CAAC,CAAC,KAAK;oBACd,IAAI,EAAE,CAAC,CAAC,IAAI;oBACZ,GAAG,CAAC,CAAC,CAAC,IAAI,KAAK,UAAU,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC,CAAC,UAAU,EAAE,QAAQ,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,YAAY,EAAE,CAAC,CAAC,YAAY,EAAE,CAAC;oBAClH,GAAG,CAAC,CAAC,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;iBAClD,CAAC,CAAC;aACJ;YACH,CAAC,CAAC,EAAE,CAAC;QACP,GAAG,CAAC,KAAK,CAAC,QAAQ;YAChB,CAAC,CAAC;gBACE,QAAQ,EAAE;oBACR,GAAG,EAAE,KAAK,CAAC,QAAQ,CAAC,GAAG;oBACvB,QAAQ,EAAE,EAAE,CAAC,EAAE,KAAK,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC,EAAE,KAAK,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,EAAE;oBACxE,KAAK,EAAE,KAAK,CAAC,QAAQ,CAAC,KAAK;oBAC3B,QAAQ,EAAE,KAAK,CAAC,QAAQ,CAAC,QAAQ;oBACjC,OAAO,EAAE,KAAK,CAAC,QAAQ,CAAC,OAAO;iBAChC;aACF;YACH,CAAC,CAAC,EAAE,CAAC;KACR,CAAC;IACF,yCAAyC;IACzC,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC;AAClD,CAAC;AAED,uGAAuG;AACvG,MAAM,UAAU,UAAU,CAAC,IAAY,EAAE,OAA0B;IACjE,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,IAAI,UAAU,CAAC;IAClD,IAAI,IAAI,CAAC,MAAM,GAAG,SAAS,EAAE,CAAC;QAC5B,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,EAAE,IAAI,EAAE,gBAAgB,EAAE,IAAI,EAAE,EAAE,EAAE,OAAO,EAAE,6BAA6B,SAAS,cAAc,EAAE,CAAC,EAAE,CAAC;IACtI,CAAC;IACD,IAAI,KAAc,CAAC;IACnB,IAAI,CAAC;QACH,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAC3B,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,EAAE,IAAI,EAAE,cAAc,EAAE,IAAI,EAAE,EAAE,EAAE,OAAO,EAAE,mBAAoB,KAAe,CAAC,OAAO,EAAE,EAAE,CAAC,EAAE,CAAC;IAC7H,CAAC;IACD,OAAO,aAAa,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;AACvC,CAAC","sourcesContent":["import type { ScadaScene } from './scene.js';\nimport { validateScene, type SceneValidationOptions, type SceneValidationResult } from './validation.js';\n\nexport interface ParseSceneOptions extends SceneValidationOptions {\n /** Largest accepted text length in characters, checked before parsing. Default 10,000,000. */\n maxLength?: number;\n}\n\n/**\n * Canonical JSON for a scene: current schema version (3), vectors as named objects in x, y, z(, w)\n * order, fixed key order, property keys sorted,\n * two-space indent and a trailing newline. Only authored fields are written, so the same scene\n * always produces the same bytes. Array order (components, connections) is preserved.\n */\nexport function serializeScene(scene: ScadaScene): string {\n const document = {\n schemaVersion: 3,\n units: 'm',\n components: scene.components.map((c) => ({\n id: c.id,\n type: c.type,\n ...(c.parentId !== undefined ? { parentId: c.parentId } : {}),\n position: { x: c.position.x, y: c.position.y, z: c.position.z },\n rotation: { x: c.rotation.x, y: c.rotation.y, z: c.rotation.z, w: c.rotation.w },\n scale: { x: c.scale.x, y: c.scale.y, z: c.scale.z },\n properties: Object.fromEntries(Object.entries(c.properties).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))),\n })),\n connections: scene.connections.map((c) => ({\n id: c.id,\n from: { instanceId: c.from.instanceId, portId: c.from.portId },\n to: { instanceId: c.to.instanceId, portId: c.to.portId },\n })),\n ...(scene.tagBindings\n ? {\n tagBindings: scene.tagBindings.map((b) => ({\n id: b.id,\n tagId: b.tagId,\n kind: b.kind,\n ...(b.kind === 'property' ? { instanceId: b.instanceId, property: b.property } : { connectionId: b.connectionId }),\n ...(b.unit !== undefined ? { unit: b.unit } : {}),\n })),\n }\n : {}),\n ...(scene.underlay\n ? {\n underlay: {\n svg: scene.underlay.svg,\n position: { x: scene.underlay.position.x, z: scene.underlay.position.z },\n width: scene.underlay.width,\n rotation: scene.underlay.rotation,\n opacity: scene.underlay.opacity,\n },\n }\n : {}),\n };\n // JSON.stringify already writes -0 as 0.\n return `${JSON.stringify(document, null, 2)}\\n`;\n}\n\n/** Parses scene JSON text: size check, JSON parse, then `validateScene` (which migrates version 1). */\nexport function parseScene(text: string, options: ParseSceneOptions): SceneValidationResult {\n const maxLength = options.maxLength ?? 10_000_000;\n if (text.length > maxLength) {\n return { ok: false, errors: [{ code: 'LIMIT_EXCEEDED', path: '', message: `Scene text is longer than ${maxLength} characters.` }] };\n }\n let input: unknown;\n try {\n input = JSON.parse(text);\n } catch (error) {\n return { ok: false, errors: [{ code: 'INVALID_JSON', path: '', message: `Not valid JSON: ${(error as Error).message}` }] };\n }\n return validateScene(input, options);\n}\n"]}
1
+ {"version":3,"file":"serialization.js","sourceRoot":"","sources":["../src/serialization.ts"],"names":[],"mappings":"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","sourcesContent":["import { SCENE_DOCUMENT_TYPE, viewKinds, type JsonValue, type ScadaScene, type SceneViews } from './scene.js';\nimport { validateScene, type SceneValidationOptions, type SceneValidationResult } from './validation.js';\n\nexport interface ParseSceneOptions extends SceneValidationOptions {\n /** Largest accepted text length in characters, checked before parsing. Default 32,000,000: a document at every default count limit is about 16 MB (T-095). */\n maxLength?: number;\n}\n\nconst byKey = ([a]: [string, unknown], [b]: [string, unknown]) => (a < b ? -1 : a > b ? 1 : 0);\n/** An object with its keys sorted, as plain data (keys such as \"__proto__\" stay data). */\nfunction sortedObject<T>(record: Record<string, T>, map: (value: T) => unknown = (v) => v): Record<string, unknown> {\n const out: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(record).sort(byKey)) {\n Object.defineProperty(out, key, { value: map(value), enumerable: true, writable: true, configurable: true });\n }\n return out;\n}\n/** Extension JSON with object keys sorted at every level; arrays keep their order. */\nfunction canonicalJson(value: JsonValue): unknown {\n if (Array.isArray(value)) return value.map(canonicalJson);\n if (value !== null && typeof value === 'object') return sortedObject(value, canonicalJson);\n return value;\n}\n\n/** Views in fixed order (process, electrical), with symbol and route ids sorted (ADR-0026). */\nfunction canonicalViews(views: SceneViews): Record<string, unknown> {\n const out: Record<string, unknown> = {};\n for (const kind of viewKinds) {\n const layout = views[kind];\n if (!layout) continue;\n out[kind] = {\n ...(layout.symbols\n ? { symbols: sortedObject(layout.symbols, (s) => ({ x: s.x, y: s.y, ...(s.rotation !== undefined ? { rotation: s.rotation } : {}), ...(s.hidden ? { hidden: true } : {}) })) }\n : {}),\n ...(layout.routes ? { routes: sortedObject(layout.routes, (r) => ({ points: r.points.map(([x, y]) => [x, y]) })) } : {}),\n };\n }\n return out;\n}\n\n/**\n * Canonical JSON for a scene: current schema version (4, ADR-0021), vectors as named objects in\n * x, y, z(, w) order, fixed key order, keys of properties, labels, extensions and view entries sorted, two-space\n * indent and a trailing newline. Only authored fields are written, so the same scene always\n * produces the same bytes. Array order (components, connections) is preserved.\n */\nexport function serializeScene(scene: ScadaScene): string {\n const p = scene.project;\n const document = {\n schemaVersion: 4,\n documentType: SCENE_DOCUMENT_TYPE,\n units: 'm',\n ...(p\n ? {\n project: {\n ...(p.id !== undefined ? { id: p.id } : {}),\n ...(p.name !== undefined ? { name: p.name } : {}),\n ...(p.revision !== undefined ? { revision: p.revision } : {}),\n ...(p.description !== undefined ? { description: p.description } : {}),\n },\n }\n : {}),\n components: scene.components.map((c) => ({\n id: c.id,\n ...(c.name !== undefined ? { name: c.name } : {}),\n type: c.type,\n ...(c.typeVersion !== undefined ? { typeVersion: c.typeVersion } : {}),\n ...(c.manufacturer !== undefined ? { manufacturer: c.manufacturer } : {}),\n ...(c.model !== undefined ? { model: c.model } : {}),\n ...(c.parentId !== undefined ? { parentId: c.parentId } : {}),\n position: { x: c.position.x, y: c.position.y, z: c.position.z },\n rotation: { x: c.rotation.x, y: c.rotation.y, z: c.rotation.z, w: c.rotation.w },\n scale: { x: c.scale.x, y: c.scale.y, z: c.scale.z },\n properties: sortedObject(c.properties),\n ...(c.labels !== undefined ? { labels: sortedObject(c.labels) } : {}),\n ...(c.extensions !== undefined ? { extensions: sortedObject(c.extensions, canonicalJson) } : {}),\n })),\n connections: scene.connections.map((c) => ({\n id: c.id,\n ...(c.domain !== undefined ? { domain: c.domain } : {}),\n ...(c.relationship !== undefined ? { relationship: c.relationship } : {}),\n from: { instanceId: c.from.instanceId, portId: c.from.portId },\n to: { instanceId: c.to.instanceId, portId: c.to.portId },\n ...(c.properties !== undefined ? { properties: sortedObject(c.properties) } : {}),\n })),\n ...(scene.associations\n ? {\n associations: scene.associations.map((a) => ({\n id: a.id,\n relationship: a.relationship,\n from: a.from,\n to: a.to,\n ...(a.properties !== undefined ? { properties: sortedObject(a.properties) } : {}),\n })),\n }\n : {}),\n ...(scene.dataSources\n ? {\n dataSources: scene.dataSources.map((s) => ({\n id: s.id,\n ...(s.name !== undefined ? { name: s.name } : {}),\n protocol: s.protocol,\n ...(s.deviceId !== undefined ? { deviceId: s.deviceId } : {}),\n ...(s.endpointRef !== undefined ? { endpointRef: s.endpointRef } : {}),\n ...(s.properties !== undefined ? { properties: sortedObject(s.properties) } : {}),\n })),\n }\n : {}),\n ...(scene.runtimeVariables\n ? {\n runtimeVariables: scene.runtimeVariables.map((v) => ({\n id: v.id,\n ...(v.name !== undefined ? { name: v.name } : {}),\n ...(v.componentId !== undefined ? { componentId: v.componentId } : {}),\n dataType: v.dataType,\n ...(v.unit !== undefined ? { unit: v.unit } : {}),\n access: v.access,\n ...(v.source\n ? {\n source: {\n dataSourceId: v.source.dataSourceId,\n ...(v.source.variable !== undefined ? { variable: v.source.variable } : {}),\n ...(v.source.address !== undefined ? { address: canonicalJson(v.source.address as JsonValue) } : {}),\n },\n }\n : {}),\n ...(v.scaling ? { scaling: { kind: v.scaling.kind, rawMin: v.scaling.rawMin, rawMax: v.scaling.rawMax, min: v.scaling.min, max: v.scaling.max } } : {}),\n ...(v.command\n ? {\n command: {\n ...(v.command.min !== undefined ? { min: v.command.min } : {}),\n ...(v.command.max !== undefined ? { max: v.command.max } : {}),\n ...(v.command.step !== undefined ? { step: v.command.step } : {}),\n ...(v.command.confirm !== undefined ? { confirm: v.command.confirm } : {}),\n },\n }\n : {}),\n })),\n }\n : {}),\n ...(scene.tagBindings\n ? {\n tagBindings: scene.tagBindings.map((b) => ({\n id: b.id,\n tagId: b.tagId,\n kind: b.kind,\n ...(b.kind === 'property' ? { instanceId: b.instanceId, property: b.property } : { connectionId: b.connectionId }),\n ...(b.unit !== undefined ? { unit: b.unit } : {}),\n })),\n }\n : {}),\n ...(scene.underlay\n ? {\n underlay: {\n svg: scene.underlay.svg,\n position: { x: scene.underlay.position.x, z: scene.underlay.position.z },\n width: scene.underlay.width,\n rotation: scene.underlay.rotation,\n opacity: scene.underlay.opacity,\n },\n }\n : {}),\n ...(scene.views !== undefined ? { views: canonicalViews(scene.views) } : {}),\n ...(scene.extensions !== undefined ? { extensions: sortedObject(scene.extensions, canonicalJson) } : {}),\n };\n // JSON.stringify already writes -0 as 0.\n return `${JSON.stringify(document, null, 2)}\\n`;\n}\n\n/** Parses scene JSON text: size check, JSON parse, then `validateScene` (which migrates versions 1–3). */\nexport function parseScene(text: string, options: ParseSceneOptions): SceneValidationResult {\n const maxLength = options.maxLength ?? 32_000_000;\n if (text.length > maxLength) {\n return { ok: false, errors: [{ code: 'LIMIT_EXCEEDED', path: '', message: `Scene text is longer than ${maxLength} characters.` }] };\n }\n let input: unknown;\n try {\n input = JSON.parse(text);\n } catch (error) {\n return { ok: false, errors: [{ code: 'INVALID_JSON', path: '', message: `Not valid JSON: ${(error as Error).message}` }] };\n }\n return validateScene(input, options);\n}\n"]}
@@ -1,4 +1,4 @@
1
- import { type ComponentInstance, type ComponentKind, type ConnectionPort, type QuaternionLike, type Vector3Like, type ScadaScene, type SceneConnection } from './scene.js';
1
+ import { type ComponentInstance, type ComponentType, type ConnectionPort, type QuaternionLike, type Vector3Like, type ScadaScene, type SceneConnection } from './scene.js';
2
2
  export type SnapStatus = 'ok' | 'incompatible' | 'occupied' | 'misaligned';
3
3
  export interface SnapCandidate {
4
4
  movingPortId: string;
@@ -10,7 +10,7 @@ export interface SnapCandidate {
10
10
  status: SnapStatus;
11
11
  }
12
12
  export interface SnapOptions {
13
- resolvePorts: (type: ComponentKind, properties?: ComponentInstance['properties']) => readonly ConnectionPort[] | undefined;
13
+ resolvePorts: (type: ComponentType, properties?: ComponentInstance['properties']) => readonly ConnectionPort[] | undefined;
14
14
  /** Search radius around each moving port, metres. Default 0.5. */
15
15
  maxDistance?: number;
16
16
  /** Largest allowed misalignment before snapping, radians. Default π/4. */
@@ -1 +1 @@
1
- {"version":3,"file":"snapping.d.ts","sourceRoot":"","sources":["../src/snapping.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,iBAAiB,EACtB,KAAK,aAAa,EAClB,KAAK,cAAc,EACnB,KAAK,cAAc,EACnB,KAAK,WAAW,EAChB,KAAK,UAAU,EACf,KAAK,eAAe,EAErB,MAAM,YAAY,CAAC;AAKpB,MAAM,MAAM,UAAU,GAAG,IAAI,GAAG,cAAc,GAAG,UAAU,GAAG,YAAY,CAAC;AAE3E,MAAM,WAAW,aAAa;IAC5B,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,eAAe,CAAC,IAAI,CAAC,CAAC;IAC9B,iEAAiE;IACjE,QAAQ,EAAE,MAAM,CAAC;IACjB,oFAAoF;IACpF,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,UAAU,CAAC;CACpB;AAED,MAAM,WAAW,WAAW;IAC1B,YAAY,EAAE,CAAC,IAAI,EAAE,aAAa,EAAE,UAAU,CAAC,EAAE,iBAAiB,CAAC,YAAY,CAAC,KAAK,SAAS,cAAc,EAAE,GAAG,SAAS,CAAC;IAC3H,kEAAkE;IAClE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,0EAA0E;IAC1E,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,MAAM,UAAU,GAClB;IACE,EAAE,EAAE,IAAI,CAAC;IACT,SAAS,EAAE,aAAa,CAAC;IACzB,8FAA8F;IAC9F,QAAQ,EAAE,WAAW,CAAC;IACtB,QAAQ,EAAE,cAAc,CAAC;IACzB,UAAU,EAAE,IAAI,CAAC,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC,CAAC;CAClD,GACD;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,cAAc,CAAC;IAAC,OAAO,CAAC,EAAE,aAAa,CAAA;CAAE,CAAC;AAsBnE;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,iBAAiB,EAAE,OAAO,EAAE,WAAW,GAAG,aAAa,EAAE,CA+BtH;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,iBAAiB,EAAE,OAAO,EAAE,WAAW,GAAG,UAAU,CAwB5G"}
1
+ {"version":3,"file":"snapping.d.ts","sourceRoot":"","sources":["../src/snapping.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,iBAAiB,EACtB,KAAK,aAAa,EAClB,KAAK,cAAc,EACnB,KAAK,cAAc,EACnB,KAAK,WAAW,EAChB,KAAK,UAAU,EACf,KAAK,eAAe,EAErB,MAAM,YAAY,CAAC;AAKpB,MAAM,MAAM,UAAU,GAAG,IAAI,GAAG,cAAc,GAAG,UAAU,GAAG,YAAY,CAAC;AAE3E,MAAM,WAAW,aAAa;IAC5B,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,eAAe,CAAC,IAAI,CAAC,CAAC;IAC9B,iEAAiE;IACjE,QAAQ,EAAE,MAAM,CAAC;IACjB,oFAAoF;IACpF,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,UAAU,CAAC;CACpB;AAED,MAAM,WAAW,WAAW;IAC1B,YAAY,EAAE,CAAC,IAAI,EAAE,aAAa,EAAE,UAAU,CAAC,EAAE,iBAAiB,CAAC,YAAY,CAAC,KAAK,SAAS,cAAc,EAAE,GAAG,SAAS,CAAC;IAC3H,kEAAkE;IAClE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,0EAA0E;IAC1E,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,MAAM,UAAU,GAClB;IACE,EAAE,EAAE,IAAI,CAAC;IACT,SAAS,EAAE,aAAa,CAAC;IACzB,8FAA8F;IAC9F,QAAQ,EAAE,WAAW,CAAC;IACtB,QAAQ,EAAE,cAAc,CAAC;IACzB,UAAU,EAAE,IAAI,CAAC,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC,CAAC;CAClD,GACD;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,cAAc,CAAC;IAAC,OAAO,CAAC,EAAE,aAAa,CAAA;CAAE,CAAC;AAyBnE;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,iBAAiB,EAAE,OAAO,EAAE,WAAW,GAAG,aAAa,EAAE,CA+BtH;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,iBAAiB,EAAE,OAAO,EAAE,WAAW,GAAG,UAAU,CAwB5G"}
package/dist/snapping.js CHANGED
@@ -1,4 +1,4 @@
1
- import { portsCompatible, } from './scene.js';
1
+ import { matesPhysically, portCapacity, portsCompatible, } from './scene.js';
2
2
  import { multiplyQuaternions, normalizeQuaternion, quaternionFromUnitVectors } from './quaternion.js';
3
3
  import { portToWorld } from './transforms.js';
4
4
  import { quaternionToTuple, tupleToQuaternion, tupleToVector } from './vectors.js';
@@ -9,11 +9,14 @@ function compareCandidates(a, b) {
9
9
  a.target.portId.localeCompare(b.target.portId) ||
10
10
  a.movingPortId.localeCompare(b.movingPortId));
11
11
  }
12
+ /** Connections per endpoint (`[instanceId, portId]` JSON). */
12
13
  function occupiedPorts(scene) {
13
- const occupied = new Set();
14
+ const occupied = new Map();
14
15
  for (const c of scene.connections) {
15
- occupied.add(JSON.stringify([c.from.instanceId, c.from.portId]));
16
- occupied.add(JSON.stringify([c.to.instanceId, c.to.portId]));
16
+ for (const e of [c.from, c.to]) {
17
+ const key = JSON.stringify([e.instanceId, e.portId]);
18
+ occupied.set(key, (occupied.get(key) ?? 0) + 1);
19
+ }
17
20
  }
18
21
  return occupied;
19
22
  }
@@ -26,12 +29,12 @@ export function findSnapCandidates(scene, moving, options) {
26
29
  const maxDistance = options.maxDistance ?? 0.5;
27
30
  const maxAngle = options.maxAngle ?? Math.PI / 4;
28
31
  const occupied = occupiedPorts(scene);
29
- const movingPorts = (options.resolvePorts(moving.type, moving.properties) ?? []).map((port) => ({ port, world: portToWorld(moving, port) }));
32
+ const movingPorts = (options.resolvePorts(moving.type, moving.properties) ?? []).filter(matesPhysically).map((port) => ({ port, world: portToWorld(moving, port) }));
30
33
  const candidates = [];
31
34
  for (const other of scene.components) {
32
35
  if (other.id === moving.id)
33
36
  continue;
34
- for (const targetPort of options.resolvePorts(other.type, other.properties) ?? []) {
37
+ for (const targetPort of (options.resolvePorts(other.type, other.properties) ?? []).filter(matesPhysically)) {
35
38
  const target = portToWorld(other, targetPort);
36
39
  for (const { port, world } of movingPorts) {
37
40
  const offset = [
@@ -47,7 +50,7 @@ export function findSnapCandidates(scene, moving, options) {
47
50
  let status = 'ok';
48
51
  if (!portsCompatible(port, targetPort))
49
52
  status = 'incompatible';
50
- else if (occupied.has(JSON.stringify([moving.id, port.id])) || occupied.has(JSON.stringify([other.id, targetPort.id])))
53
+ else if ((occupied.get(JSON.stringify([moving.id, port.id])) ?? 0) >= portCapacity(port) || (occupied.get(JSON.stringify([other.id, targetPort.id])) ?? 0) >= portCapacity(targetPort))
51
54
  status = 'occupied';
52
55
  else if (angle > maxAngle)
53
56
  status = 'misaligned';
@@ -1 +1 @@
1
- {"version":3,"file":"snapping.js","sourceRoot":"","sources":["../src/snapping.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n portsCompatible,\n type ComponentInstance,\n type ComponentKind,\n type ConnectionPort,\n type QuaternionLike,\n type Vector3Like,\n type ScadaScene,\n type SceneConnection,\n type Vector3Tuple,\n} from './scene.js';\nimport { multiplyQuaternions, normalizeQuaternion, quaternionFromUnitVectors } from './quaternion.js';\nimport { portToWorld, type WorldPort } from './transforms.js';\nimport { quaternionToTuple, tupleToQuaternion, tupleToVector } from './vectors.js';\n\nexport type SnapStatus = 'ok' | 'incompatible' | 'occupied' | 'misaligned';\n\nexport interface SnapCandidate {\n movingPortId: string;\n target: SceneConnection['to'];\n /** Distance between the two world-space port anchors, metres. */\n distance: number;\n /** Angle between the moving port normal and the reversed target normal, radians. */\n angle: number;\n status: SnapStatus;\n}\n\nexport interface SnapOptions {\n resolvePorts: (type: ComponentKind, properties?: ComponentInstance['properties']) => readonly ConnectionPort[] | undefined;\n /** Search radius around each moving port, metres. Default 0.5. */\n maxDistance?: number;\n /** Largest allowed misalignment before snapping, radians. Default π/4. */\n maxAngle?: number;\n}\n\nexport type SnapResult =\n | {\n ok: true;\n candidate: SnapCandidate;\n /** Transform for the moving component that makes the ports coincide with opposing normals. */\n position: Vector3Like;\n rotation: QuaternionLike;\n connection: Pick<SceneConnection, 'from' | 'to'>;\n }\n | { ok: false; reason: 'NO_CANDIDATE'; nearest?: SnapCandidate };\n\nconst dot = (a: Vector3Tuple, b: Vector3Tuple) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2];\n\nfunction compareCandidates(a: SnapCandidate, b: SnapCandidate): number {\n return (\n a.distance - b.distance ||\n a.target.instanceId.localeCompare(b.target.instanceId) ||\n a.target.portId.localeCompare(b.target.portId) ||\n a.movingPortId.localeCompare(b.movingPortId)\n );\n}\n\nfunction occupiedPorts(scene: ScadaScene): Set<string> {\n const occupied = new Set<string>();\n for (const c of scene.connections) {\n occupied.add(JSON.stringify([c.from.instanceId, c.from.portId]));\n occupied.add(JSON.stringify([c.to.instanceId, c.to.portId]));\n }\n return occupied;\n}\n\n/**\n * Lists every port pairing between `moving` and the rest of the scene within `maxDistance`,\n * nearest first with a stable tie-break, each marked with whether it can snap and why not.\n * A scene component with the same id as `moving` is treated as the one being dragged and skipped.\n */\nexport function findSnapCandidates(scene: ScadaScene, moving: ComponentInstance, options: SnapOptions): SnapCandidate[] {\n const maxDistance = options.maxDistance ?? 0.5;\n const maxAngle = options.maxAngle ?? Math.PI / 4;\n const occupied = occupiedPorts(scene);\n const movingPorts = (options.resolvePorts(moving.type, moving.properties) ?? []).map((port) => ({ port, world: portToWorld(moving, port) }));\n const candidates: SnapCandidate[] = [];\n\n for (const other of scene.components) {\n if (other.id === moving.id) continue;\n for (const targetPort of options.resolvePorts(other.type, other.properties) ?? []) {\n const target = portToWorld(other, targetPort);\n for (const { port, world } of movingPorts) {\n const offset: Vector3Tuple = [\n target.position[0] - world.position[0],\n target.position[1] - world.position[1],\n target.position[2] - world.position[2],\n ];\n const distance = Math.hypot(...offset);\n if (distance > maxDistance) continue;\n // Normals must oppose: compare the moving normal with the reversed target normal.\n const angle = Math.acos(Math.min(1, Math.max(-1, -dot(world.direction, target.direction))));\n let status: SnapStatus = 'ok';\n if (!portsCompatible(port, targetPort)) status = 'incompatible';\n else if (occupied.has(JSON.stringify([moving.id, port.id])) || occupied.has(JSON.stringify([other.id, targetPort.id])))\n status = 'occupied';\n else if (angle > maxAngle) status = 'misaligned';\n candidates.push({ movingPortId: port.id, target: { instanceId: other.id, portId: targetPort.id }, distance, angle, status });\n }\n }\n }\n return candidates.sort(compareCandidates);\n}\n\n/**\n * Snaps `moving` to the best eligible candidate: the smallest rotation that makes its port normal\n * oppose the target normal (roll about the port axis is kept), then a translation that makes the\n * anchors coincide. Returns the proposed transform and endpoints; the scene is not modified.\n */\nexport function snapComponent(scene: ScadaScene, moving: ComponentInstance, options: SnapOptions): SnapResult {\n const candidates = findSnapCandidates(scene, moving, options);\n const candidate = candidates.find((c) => c.status === 'ok');\n if (!candidate) return candidates.length > 0 ? { ok: false, reason: 'NO_CANDIDATE', nearest: candidates[0] } : { ok: false, reason: 'NO_CANDIDATE' };\n\n const targetInstance = scene.components.find((c) => c.id === candidate.target.instanceId)!;\n const targetPort = options.resolvePorts(targetInstance.type, targetInstance.properties)!.find((p) => p.id === candidate.target.portId)!;\n const movingPort = options.resolvePorts(moving.type, moving.properties)!.find((p) => p.id === candidate.movingPortId)!;\n const target: WorldPort = portToWorld(targetInstance, targetPort);\n const current = portToWorld(moving, movingPort);\n\n const wanted: Vector3Tuple = [-target.direction[0], -target.direction[1], -target.direction[2]];\n const rotation = tupleToQuaternion(\n normalizeQuaternion(multiplyQuaternions(quaternionFromUnitVectors(current.direction, wanted), quaternionToTuple(moving.rotation))),\n );\n const anchor = portToWorld({ position: { x: 0, y: 0, z: 0 }, rotation, scale: moving.scale }, movingPort).position;\n const position = tupleToVector([target.position[0] - anchor[0], target.position[1] - anchor[1], target.position[2] - anchor[2]]);\n return {\n ok: true,\n candidate,\n position,\n rotation,\n connection: { from: { instanceId: moving.id, portId: movingPort.id }, to: candidate.target },\n };\n}\n"]}
1
+ {"version":3,"file":"snapping.js","sourceRoot":"","sources":["../src/snapping.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n matesPhysically,\n portCapacity,\n portsCompatible,\n type ComponentInstance,\n type ComponentType,\n type ConnectionPort,\n type QuaternionLike,\n type Vector3Like,\n type ScadaScene,\n type SceneConnection,\n type Vector3Tuple,\n} from './scene.js';\nimport { multiplyQuaternions, normalizeQuaternion, quaternionFromUnitVectors } from './quaternion.js';\nimport { portToWorld, type WorldPort } from './transforms.js';\nimport { quaternionToTuple, tupleToQuaternion, tupleToVector } from './vectors.js';\n\nexport type SnapStatus = 'ok' | 'incompatible' | 'occupied' | 'misaligned';\n\nexport interface SnapCandidate {\n movingPortId: string;\n target: SceneConnection['to'];\n /** Distance between the two world-space port anchors, metres. */\n distance: number;\n /** Angle between the moving port normal and the reversed target normal, radians. */\n angle: number;\n status: SnapStatus;\n}\n\nexport interface SnapOptions {\n resolvePorts: (type: ComponentType, properties?: ComponentInstance['properties']) => readonly ConnectionPort[] | undefined;\n /** Search radius around each moving port, metres. Default 0.5. */\n maxDistance?: number;\n /** Largest allowed misalignment before snapping, radians. Default π/4. */\n maxAngle?: number;\n}\n\nexport type SnapResult =\n | {\n ok: true;\n candidate: SnapCandidate;\n /** Transform for the moving component that makes the ports coincide with opposing normals. */\n position: Vector3Like;\n rotation: QuaternionLike;\n connection: Pick<SceneConnection, 'from' | 'to'>;\n }\n | { ok: false; reason: 'NO_CANDIDATE'; nearest?: SnapCandidate };\n\nconst dot = (a: Vector3Tuple, b: Vector3Tuple) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2];\n\nfunction compareCandidates(a: SnapCandidate, b: SnapCandidate): number {\n return (\n a.distance - b.distance ||\n a.target.instanceId.localeCompare(b.target.instanceId) ||\n a.target.portId.localeCompare(b.target.portId) ||\n a.movingPortId.localeCompare(b.movingPortId)\n );\n}\n\n/** Connections per endpoint (`[instanceId, portId]` JSON). */\nfunction occupiedPorts(scene: ScadaScene): Map<string, number> {\n const occupied = new Map<string, number>();\n for (const c of scene.connections) {\n for (const e of [c.from, c.to]) {\n const key = JSON.stringify([e.instanceId, e.portId]);\n occupied.set(key, (occupied.get(key) ?? 0) + 1);\n }\n }\n return occupied;\n}\n\n/**\n * Lists every port pairing between `moving` and the rest of the scene within `maxDistance`,\n * nearest first with a stable tie-break, each marked with whether it can snap and why not.\n * A scene component with the same id as `moving` is treated as the one being dragged and skipped.\n */\nexport function findSnapCandidates(scene: ScadaScene, moving: ComponentInstance, options: SnapOptions): SnapCandidate[] {\n const maxDistance = options.maxDistance ?? 0.5;\n const maxAngle = options.maxAngle ?? Math.PI / 4;\n const occupied = occupiedPorts(scene);\n const movingPorts = (options.resolvePorts(moving.type, moving.properties) ?? []).filter(matesPhysically).map((port) => ({ port, world: portToWorld(moving, port) }));\n const candidates: SnapCandidate[] = [];\n\n for (const other of scene.components) {\n if (other.id === moving.id) continue;\n for (const targetPort of (options.resolvePorts(other.type, other.properties) ?? []).filter(matesPhysically)) {\n const target = portToWorld(other, targetPort);\n for (const { port, world } of movingPorts) {\n const offset: Vector3Tuple = [\n target.position[0] - world.position[0],\n target.position[1] - world.position[1],\n target.position[2] - world.position[2],\n ];\n const distance = Math.hypot(...offset);\n if (distance > maxDistance) continue;\n // Normals must oppose: compare the moving normal with the reversed target normal.\n const angle = Math.acos(Math.min(1, Math.max(-1, -dot(world.direction, target.direction))));\n let status: SnapStatus = 'ok';\n if (!portsCompatible(port, targetPort)) status = 'incompatible';\n else if ((occupied.get(JSON.stringify([moving.id, port.id])) ?? 0) >= portCapacity(port) || (occupied.get(JSON.stringify([other.id, targetPort.id])) ?? 0) >= portCapacity(targetPort))\n status = 'occupied';\n else if (angle > maxAngle) status = 'misaligned';\n candidates.push({ movingPortId: port.id, target: { instanceId: other.id, portId: targetPort.id }, distance, angle, status });\n }\n }\n }\n return candidates.sort(compareCandidates);\n}\n\n/**\n * Snaps `moving` to the best eligible candidate: the smallest rotation that makes its port normal\n * oppose the target normal (roll about the port axis is kept), then a translation that makes the\n * anchors coincide. Returns the proposed transform and endpoints; the scene is not modified.\n */\nexport function snapComponent(scene: ScadaScene, moving: ComponentInstance, options: SnapOptions): SnapResult {\n const candidates = findSnapCandidates(scene, moving, options);\n const candidate = candidates.find((c) => c.status === 'ok');\n if (!candidate) return candidates.length > 0 ? { ok: false, reason: 'NO_CANDIDATE', nearest: candidates[0] } : { ok: false, reason: 'NO_CANDIDATE' };\n\n const targetInstance = scene.components.find((c) => c.id === candidate.target.instanceId)!;\n const targetPort = options.resolvePorts(targetInstance.type, targetInstance.properties)!.find((p) => p.id === candidate.target.portId)!;\n const movingPort = options.resolvePorts(moving.type, moving.properties)!.find((p) => p.id === candidate.movingPortId)!;\n const target: WorldPort = portToWorld(targetInstance, targetPort);\n const current = portToWorld(moving, movingPort);\n\n const wanted: Vector3Tuple = [-target.direction[0], -target.direction[1], -target.direction[2]];\n const rotation = tupleToQuaternion(\n normalizeQuaternion(multiplyQuaternions(quaternionFromUnitVectors(current.direction, wanted), quaternionToTuple(moving.rotation))),\n );\n const anchor = portToWorld({ position: { x: 0, y: 0, z: 0 }, rotation, scale: moving.scale }, movingPort).position;\n const position = tupleToVector([target.position[0] - anchor[0], target.position[1] - anchor[1], target.position[2] - anchor[2]]);\n return {\n ok: true,\n candidate,\n position,\n rotation,\n connection: { from: { instanceId: moving.id, portId: movingPort.id }, to: candidate.target },\n };\n}\n"]}
@@ -0,0 +1,56 @@
1
+ import type { ScadaScene } from './scene.js';
2
+ import type { PropertyState, TelemetryUnknownReason } from './telemetry.js';
3
+ export type AlarmSeverity = 'critical' | 'high' | 'medium' | 'low';
4
+ export declare const alarmSeverities: readonly AlarmSeverity[];
5
+ /** An alarm from the host's alarm system, shown as given. */
6
+ export interface AlarmState {
7
+ id: string;
8
+ /** What it is about; a tag maps to components through property bindings, flow bindings and runtime variables. */
9
+ componentId?: string;
10
+ tagId?: string;
11
+ severity: AlarmSeverity;
12
+ message: string;
13
+ active: boolean;
14
+ acknowledged?: boolean;
15
+ /** ISO 8601, shown as given. */
16
+ since?: string;
17
+ }
18
+ export type ComponentStatusKind = 'alarm' | 'unknown' | 'uncertain' | 'ok' | 'unbound';
19
+ export interface ComponentStatus {
20
+ componentId: string;
21
+ /** Highest first: alarm, unknown, uncertain, ok, unbound. */
22
+ status: ComponentStatusKind;
23
+ /** The highest active alarm severity, when `status` is 'alarm'. */
24
+ severity?: AlarmSeverity;
25
+ /** Any of its active alarms is not acknowledged. */
26
+ unacknowledged: boolean;
27
+ /** Its active alarms: highest severity first, unacknowledged before acknowledged, then host order. */
28
+ alarms: AlarmState[];
29
+ /** Bound properties without a trusted value. */
30
+ unknown: {
31
+ property: string;
32
+ reason: TelemetryUnknownReason;
33
+ }[];
34
+ /** Bound properties whose value is trusted but UNCERTAIN. */
35
+ uncertain: string[];
36
+ /** Some of its values are simulated (labelled SIMULATED, never plant data). */
37
+ simulated: boolean;
38
+ }
39
+ export interface StatusResult {
40
+ /** One status per scene component. */
41
+ byComponent: ReadonlyMap<string, ComponentStatus>;
42
+ /** Components needing attention: alarms first (by severity, unacknowledged first), then unknown values; scene order within each. */
43
+ attention: ComponentStatus[];
44
+ /** Active alarms whose component or tag does not resolve: listed, never dropped. */
45
+ unlinked: AlarmState[];
46
+ counts: Record<AlarmSeverity | 'unknown' | 'uncertain' | 'unacknowledged' | 'unlinked', number>;
47
+ }
48
+ /**
49
+ * Folds host alarms and resolved telemetry into one status per component. Pure. Only active alarms
50
+ * count; acknowledged ones still count while active. An explicit `componentId` that exists wins;
51
+ * otherwise `tagId` maps through the scene's bindings and variables.
52
+ */
53
+ export declare function componentStatus(scene: ScadaScene, resolution: {
54
+ properties: readonly PropertyState[];
55
+ } | undefined, alarms?: readonly AlarmState[]): StatusResult;
56
+ //# sourceMappingURL=status.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../src/status.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAC7C,OAAO,KAAK,EAAE,aAAa,EAAE,sBAAsB,EAAE,MAAM,gBAAgB,CAAC;AAK5E,MAAM,MAAM,aAAa,GAAG,UAAU,GAAG,MAAM,GAAG,QAAQ,GAAG,KAAK,CAAC;AACnE,eAAO,MAAM,eAAe,EAAE,SAAS,aAAa,EAA0C,CAAC;AAE/F,6DAA6D;AAC7D,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,iHAAiH;IACjH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,aAAa,CAAC;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,OAAO,CAAC;IAChB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,gCAAgC;IAChC,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,MAAM,mBAAmB,GAAG,OAAO,GAAG,SAAS,GAAG,WAAW,GAAG,IAAI,GAAG,SAAS,CAAC;AAEvF,MAAM,WAAW,eAAe;IAC9B,WAAW,EAAE,MAAM,CAAC;IACpB,6DAA6D;IAC7D,MAAM,EAAE,mBAAmB,CAAC;IAC5B,mEAAmE;IACnE,QAAQ,CAAC,EAAE,aAAa,CAAC;IACzB,oDAAoD;IACpD,cAAc,EAAE,OAAO,CAAC;IACxB,sGAAsG;IACtG,MAAM,EAAE,UAAU,EAAE,CAAC;IACrB,gDAAgD;IAChD,OAAO,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,sBAAsB,CAAA;KAAE,EAAE,CAAC;IAChE,6DAA6D;IAC7D,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,+EAA+E;IAC/E,SAAS,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,YAAY;IAC3B,sCAAsC;IACtC,WAAW,EAAE,WAAW,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;IAClD,oIAAoI;IACpI,SAAS,EAAE,eAAe,EAAE,CAAC;IAC7B,oFAAoF;IACpF,QAAQ,EAAE,UAAU,EAAE,CAAC;IACvB,MAAM,EAAE,MAAM,CAAC,aAAa,GAAG,SAAS,GAAG,WAAW,GAAG,gBAAgB,GAAG,UAAU,EAAE,MAAM,CAAC,CAAC;CACjG;AA0BD;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE;IAAE,UAAU,EAAE,SAAS,aAAa,EAAE,CAAA;CAAE,GAAG,SAAS,EAAE,MAAM,GAAE,SAAS,UAAU,EAAO,GAAG,YAAY,CA4DrK"}
package/dist/status.js ADDED
@@ -0,0 +1,114 @@
1
+ export const alarmSeverities = ['critical', 'high', 'medium', 'low'];
2
+ const rank = (s) => alarmSeverities.indexOf(s);
3
+ /** Plain text from a host value; objects and symbols (whose conversion can throw) become ''. */
4
+ const text = (value) => (typeof value === 'string' ? value : typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint' ? String(value) : '');
5
+ /**
6
+ * Host data is untrusted: an unrecognised severity is shown as critical, never under-reported, and
7
+ * every field the views render becomes plain text or is dropped (T-096).
8
+ */
9
+ function normalise(alarm) {
10
+ const severity = alarmSeverities.includes(alarm.severity) ? alarm.severity : 'critical';
11
+ const { componentId, tagId, since } = alarm;
12
+ return {
13
+ id: text(alarm.id),
14
+ severity,
15
+ message: text(alarm.message),
16
+ active: true,
17
+ acknowledged: alarm.acknowledged === true,
18
+ ...(typeof componentId === 'string' ? { componentId } : {}),
19
+ ...(typeof tagId === 'string' ? { tagId } : {}),
20
+ ...(typeof since === 'string' ? { since } : {}),
21
+ };
22
+ }
23
+ /**
24
+ * Folds host alarms and resolved telemetry into one status per component. Pure. Only active alarms
25
+ * count; acknowledged ones still count while active. An explicit `componentId` that exists wins;
26
+ * otherwise `tagId` maps through the scene's bindings and variables.
27
+ */
28
+ export function componentStatus(scene, resolution, alarms = []) {
29
+ const ids = new Set(scene.components.map((c) => c.id));
30
+ const tagTargets = new Map();
31
+ const target = (tagId, componentId) => {
32
+ if (ids.has(componentId)) {
33
+ const set = tagTargets.get(tagId);
34
+ if (set)
35
+ set.add(componentId);
36
+ else
37
+ tagTargets.set(tagId, new Set([componentId]));
38
+ }
39
+ };
40
+ const connections = new Map(scene.connections.map((c) => [c.id, c]));
41
+ for (const b of scene.tagBindings ?? []) {
42
+ if (b.kind === 'property')
43
+ target(b.tagId, b.instanceId);
44
+ else {
45
+ const c = connections.get(b.connectionId);
46
+ if (c)
47
+ for (const end of [c.from.instanceId, c.to.instanceId])
48
+ target(b.tagId, end);
49
+ }
50
+ }
51
+ for (const v of scene.runtimeVariables ?? [])
52
+ if (v.componentId)
53
+ target(v.id, v.componentId);
54
+ const byAlarm = new Map();
55
+ const unlinked = [];
56
+ for (const raw of alarms) {
57
+ if (!raw || typeof raw !== 'object' || raw.active !== true)
58
+ continue;
59
+ const alarm = normalise(raw);
60
+ const targets = alarm.componentId !== undefined && ids.has(alarm.componentId) ? [alarm.componentId] : [...(alarm.tagId !== undefined ? (tagTargets.get(alarm.tagId) ?? []) : [])];
61
+ if (targets.length === 0)
62
+ unlinked.push(alarm);
63
+ // Pushed, not copied: folding must stay linear in the number of host alarms (T-096).
64
+ for (const id of targets) {
65
+ const list = byAlarm.get(id);
66
+ if (list)
67
+ list.push(alarm);
68
+ else
69
+ byAlarm.set(id, [alarm]);
70
+ }
71
+ }
72
+ const states = new Map();
73
+ for (const p of resolution?.properties ?? []) {
74
+ const list = states.get(p.instanceId);
75
+ if (list)
76
+ list.push(p);
77
+ else
78
+ states.set(p.instanceId, [p]);
79
+ }
80
+ const byComponent = new Map();
81
+ for (const { id } of scene.components) {
82
+ const active = (byAlarm.get(id) ?? []).map((a, i) => [a, i]).sort(([a, i], [b, j]) => rank(a.severity) - rank(b.severity) || Number(!!a.acknowledged) - Number(!!b.acknowledged) || i - j).map(([a]) => a);
83
+ const values = states.get(id) ?? [];
84
+ const unknown = values.flatMap((p) => (p.status === 'unknown' ? [{ property: p.property, reason: p.reason }] : []));
85
+ const uncertain = values.flatMap((p) => (p.status === 'ok' && p.uncertain ? [p.property] : []));
86
+ // A simulated value never clears an unknown measured one: unknown outranks ok whatever the source (ADR-0010).
87
+ const status = active.length ? 'alarm' : unknown.length ? 'unknown' : uncertain.length ? 'uncertain' : values.length ? 'ok' : 'unbound';
88
+ byComponent.set(id, {
89
+ componentId: id,
90
+ status,
91
+ ...(active.length ? { severity: active[0].severity } : {}),
92
+ unacknowledged: active.some((a) => !a.acknowledged),
93
+ alarms: active,
94
+ unknown,
95
+ uncertain,
96
+ simulated: values.some((p) => p.source === 'SIMULATED'),
97
+ });
98
+ }
99
+ const all = [...byComponent.values()];
100
+ const alarmed = all.filter((s) => s.status === 'alarm').map((s, i) => [s, i]).sort(([a, i], [b, j]) => rank(a.severity) - rank(b.severity) || Number(b.unacknowledged) - Number(a.unacknowledged) || i - j).map(([s]) => s);
101
+ const counts = { critical: 0, high: 0, medium: 0, low: 0, unknown: 0, uncertain: 0, unacknowledged: 0, unlinked: unlinked.length };
102
+ for (const s of all) {
103
+ if (s.severity)
104
+ counts[s.severity]++;
105
+ if (s.status === 'unknown')
106
+ counts.unknown++;
107
+ if (s.status === 'uncertain')
108
+ counts.uncertain++;
109
+ if (s.unacknowledged)
110
+ counts.unacknowledged++;
111
+ }
112
+ return { byComponent, attention: [...alarmed, ...all.filter((s) => s.status === 'unknown')], unlinked, counts };
113
+ }
114
+ //# sourceMappingURL=status.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"status.js","sourceRoot":"","sources":["../src/status.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ScadaScene } from './scene.js';\nimport type { PropertyState, TelemetryUnknownReason } from './telemetry.js';\n\n// Status per component (ADR-0028 §3–4): host-supplied alarms and the quality of bound values, folded\n// into one status each. Display only: the SDK never evaluates limits, raises, clears or acknowledges.\n\nexport type AlarmSeverity = 'critical' | 'high' | 'medium' | 'low';\nexport const alarmSeverities: readonly AlarmSeverity[] = ['critical', 'high', 'medium', 'low'];\n\n/** An alarm from the host's alarm system, shown as given. */\nexport interface AlarmState {\n id: string;\n /** What it is about; a tag maps to components through property bindings, flow bindings and runtime variables. */\n componentId?: string;\n tagId?: string;\n severity: AlarmSeverity;\n message: string;\n active: boolean;\n acknowledged?: boolean;\n /** ISO 8601, shown as given. */\n since?: string;\n}\n\nexport type ComponentStatusKind = 'alarm' | 'unknown' | 'uncertain' | 'ok' | 'unbound';\n\nexport interface ComponentStatus {\n componentId: string;\n /** Highest first: alarm, unknown, uncertain, ok, unbound. */\n status: ComponentStatusKind;\n /** The highest active alarm severity, when `status` is 'alarm'. */\n severity?: AlarmSeverity;\n /** Any of its active alarms is not acknowledged. */\n unacknowledged: boolean;\n /** Its active alarms: highest severity first, unacknowledged before acknowledged, then host order. */\n alarms: AlarmState[];\n /** Bound properties without a trusted value. */\n unknown: { property: string; reason: TelemetryUnknownReason }[];\n /** Bound properties whose value is trusted but UNCERTAIN. */\n uncertain: string[];\n /** Some of its values are simulated (labelled SIMULATED, never plant data). */\n simulated: boolean;\n}\n\nexport interface StatusResult {\n /** One status per scene component. */\n byComponent: ReadonlyMap<string, ComponentStatus>;\n /** Components needing attention: alarms first (by severity, unacknowledged first), then unknown values; scene order within each. */\n attention: ComponentStatus[];\n /** Active alarms whose component or tag does not resolve: listed, never dropped. */\n unlinked: AlarmState[];\n counts: Record<AlarmSeverity | 'unknown' | 'uncertain' | 'unacknowledged' | 'unlinked', number>;\n}\n\nconst rank = (s: AlarmSeverity) => alarmSeverities.indexOf(s);\n\n/** Plain text from a host value; objects and symbols (whose conversion can throw) become ''. */\nconst text = (value: unknown): string => (typeof value === 'string' ? value : typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint' ? String(value) : '');\n\n/**\n * Host data is untrusted: an unrecognised severity is shown as critical, never under-reported, and\n * every field the views render becomes plain text or is dropped (T-096).\n */\nfunction normalise(alarm: AlarmState): AlarmState {\n const severity = alarmSeverities.includes(alarm.severity) ? alarm.severity : 'critical';\n const { componentId, tagId, since } = alarm;\n return {\n id: text(alarm.id),\n severity,\n message: text(alarm.message),\n active: true,\n acknowledged: alarm.acknowledged === true,\n ...(typeof componentId === 'string' ? { componentId } : {}),\n ...(typeof tagId === 'string' ? { tagId } : {}),\n ...(typeof since === 'string' ? { since } : {}),\n };\n}\n\n/**\n * Folds host alarms and resolved telemetry into one status per component. Pure. Only active alarms\n * count; acknowledged ones still count while active. An explicit `componentId` that exists wins;\n * otherwise `tagId` maps through the scene's bindings and variables.\n */\nexport function componentStatus(scene: ScadaScene, resolution: { properties: readonly PropertyState[] } | undefined, alarms: readonly AlarmState[] = []): StatusResult {\n const ids = new Set(scene.components.map((c) => c.id));\n const tagTargets = new Map<string, Set<string>>();\n const target = (tagId: string, componentId: string) => {\n if (ids.has(componentId)) { const set = tagTargets.get(tagId); if (set) set.add(componentId); else tagTargets.set(tagId, new Set([componentId])); }\n };\n const connections = new Map(scene.connections.map((c) => [c.id, c]));\n for (const b of scene.tagBindings ?? []) {\n if (b.kind === 'property') target(b.tagId, b.instanceId);\n else {\n const c = connections.get(b.connectionId);\n if (c) for (const end of [c.from.instanceId, c.to.instanceId]) target(b.tagId, end);\n }\n }\n for (const v of scene.runtimeVariables ?? []) if (v.componentId) target(v.id, v.componentId);\n\n const byAlarm = new Map<string, AlarmState[]>();\n const unlinked: AlarmState[] = [];\n for (const raw of alarms) {\n if (!raw || typeof raw !== 'object' || raw.active !== true) continue;\n const alarm = normalise(raw);\n const targets = alarm.componentId !== undefined && ids.has(alarm.componentId) ? [alarm.componentId] : [...(alarm.tagId !== undefined ? (tagTargets.get(alarm.tagId) ?? []) : [])];\n if (targets.length === 0) unlinked.push(alarm);\n // Pushed, not copied: folding must stay linear in the number of host alarms (T-096).\n for (const id of targets) { const list = byAlarm.get(id); if (list) list.push(alarm); else byAlarm.set(id, [alarm]); }\n }\n\n const states = new Map<string, PropertyState[]>();\n for (const p of resolution?.properties ?? []) { const list = states.get(p.instanceId); if (list) list.push(p); else states.set(p.instanceId, [p]); }\n\n const byComponent = new Map<string, ComponentStatus>();\n for (const { id } of scene.components) {\n const active = (byAlarm.get(id) ?? []).map((a, i) => [a, i] as const).sort(([a, i], [b, j]) => rank(a.severity) - rank(b.severity) || Number(!!a.acknowledged) - Number(!!b.acknowledged) || i - j).map(([a]) => a);\n const values = states.get(id) ?? [];\n const unknown = values.flatMap((p) => (p.status === 'unknown' ? [{ property: p.property, reason: p.reason }] : []));\n const uncertain = values.flatMap((p) => (p.status === 'ok' && p.uncertain ? [p.property] : []));\n // A simulated value never clears an unknown measured one: unknown outranks ok whatever the source (ADR-0010).\n const status: ComponentStatusKind = active.length ? 'alarm' : unknown.length ? 'unknown' : uncertain.length ? 'uncertain' : values.length ? 'ok' : 'unbound';\n byComponent.set(id, {\n componentId: id,\n status,\n ...(active.length ? { severity: active[0].severity } : {}),\n unacknowledged: active.some((a) => !a.acknowledged),\n alarms: active,\n unknown,\n uncertain,\n simulated: values.some((p) => p.source === 'SIMULATED'),\n });\n }\n\n const all = [...byComponent.values()];\n const alarmed = all.filter((s) => s.status === 'alarm').map((s, i) => [s, i] as const).sort(([a, i], [b, j]) => rank(a.severity!) - rank(b.severity!) || Number(b.unacknowledged) - Number(a.unacknowledged) || i - j).map(([s]) => s);\n const counts: StatusResult['counts'] = { critical: 0, high: 0, medium: 0, low: 0, unknown: 0, uncertain: 0, unacknowledged: 0, unlinked: unlinked.length };\n for (const s of all) {\n if (s.severity) counts[s.severity]++;\n if (s.status === 'unknown') counts.unknown++;\n if (s.status === 'uncertain') counts.uncertain++;\n if (s.unacknowledged) counts.unacknowledged++;\n }\n return { byComponent, attention: [...alarmed, ...all.filter((s) => s.status === 'unknown')], unlinked, counts };\n}\n"]}
package/dist/tags.d.ts ADDED
@@ -0,0 +1,52 @@
1
+ import type { ComponentInstance, ComponentType, Quantity, RuntimeVariable, ScadaScene, TagBinding, VariableTemplate } from './scene.js';
2
+ import { type SceneValidationError, type SceneValidationOptions } from './validation.js';
3
+ type Resolvers = {
4
+ /** Bindable properties per type, as for `validateScene`. */
5
+ resolveBindable?: (type: ComponentType) => Readonly<Record<string, Quantity>> | undefined;
6
+ };
7
+ export interface SuggestOptions extends Resolvers {
8
+ /** Variable templates per type; a registry's `resolvers.resolveVariables`. */
9
+ resolveVariables: (type: ComponentType, properties?: ComponentInstance['properties']) => readonly VariableTemplate[] | undefined;
10
+ }
11
+ /** Proposed tags and bindings. Nothing is applied: a host or the editor applies the ones the user confirms. */
12
+ export interface TagSuggestions {
13
+ variables: RuntimeVariable[];
14
+ bindings: TagBinding[];
15
+ }
16
+ /**
17
+ * Proposes runtime variables and bindings from the component definitions' variable templates
18
+ * (ADR-0024): tag id `<componentId>.<name>`, read-only, no source (the host assigns addresses).
19
+ * Never guesses from names. Tags that already exist, and properties that are already bound, are
20
+ * left alone, so suggesting twice proposes nothing new.
21
+ */
22
+ export declare function suggestVariables(scene: ScadaScene, options: SuggestOptions): TagSuggestions;
23
+ export type ApplyResult = {
24
+ ok: true;
25
+ scene: ScadaScene;
26
+ } | {
27
+ ok: false;
28
+ errors: SceneValidationError[];
29
+ };
30
+ /**
31
+ * Adds the confirmed suggestions to the scene and validates the result, so a confirmed proposal can
32
+ * never leave an invalid document. The input is not changed.
33
+ */
34
+ export declare function applyTagSuggestions(scene: ScadaScene, picked: TagSuggestions, options: SceneValidationOptions): ApplyResult;
35
+ export type TagFindingCode = 'UNBOUND_COMPONENT' | 'UNUSED_VARIABLE' | 'UNDESCRIBED_TAG' | 'OVERLAPPING_REGISTERS';
36
+ /** Something worth a look; never an error. `path` points into the scene, as validation errors do. */
37
+ export interface TagFinding {
38
+ code: TagFindingCode;
39
+ path: string;
40
+ message: string;
41
+ }
42
+ /**
43
+ * Reviews a validated scene's tags (ADR-0024) and returns findings, in this order:
44
+ * - components with bindable properties and no binding (needs `resolveBindable`);
45
+ * - runtime variables no binding uses;
46
+ * - bindings to tags with no runtime variable, when the scene defines any;
47
+ * - variables on one Modbus source and unit whose register spans overlap (bits of one register may share it).
48
+ * Advisory only: `validateScene` stays the rule for what a document may contain.
49
+ */
50
+ export declare function reviewTags(scene: ScadaScene, options?: Resolvers): TagFinding[];
51
+ export {};
52
+ //# sourceMappingURL=tags.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tags.d.ts","sourceRoot":"","sources":["../src/tags.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,aAAa,EAAE,QAAQ,EAAE,eAAe,EAAE,UAAU,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AACxI,OAAO,EAAiB,KAAK,oBAAoB,EAAE,KAAK,sBAAsB,EAAE,MAAM,iBAAiB,CAAC;AAExG,KAAK,SAAS,GAAG;IACf,4DAA4D;IAC5D,eAAe,CAAC,EAAE,CAAC,IAAI,EAAE,aAAa,KAAK,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,GAAG,SAAS,CAAC;CAC3F,CAAC;AAEF,MAAM,WAAW,cAAe,SAAQ,SAAS;IAC/C,8EAA8E;IAC9E,gBAAgB,EAAE,CAAC,IAAI,EAAE,aAAa,EAAE,UAAU,CAAC,EAAE,iBAAiB,CAAC,YAAY,CAAC,KAAK,SAAS,gBAAgB,EAAE,GAAG,SAAS,CAAC;CAClI;AAED,+GAA+G;AAC/G,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,eAAe,EAAE,CAAC;IAC7B,QAAQ,EAAE,UAAU,EAAE,CAAC;CACxB;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,UAAU,EAAE,OAAO,EAAE,cAAc,GAAG,cAAc,CAsB3F;AAED,MAAM,MAAM,WAAW,GAAG;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,UAAU,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,oBAAoB,EAAE,CAAA;CAAE,CAAC;AAE1G;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,cAAc,EAAE,OAAO,EAAE,sBAAsB,GAAG,WAAW,CAQ3H;AAED,MAAM,MAAM,cAAc,GAAG,mBAAmB,GAAG,iBAAiB,GAAG,iBAAiB,GAAG,uBAAuB,CAAC;AAEnH,qGAAqG;AACrG,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,cAAc,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAID;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,UAAU,EAAE,OAAO,GAAE,SAAc,GAAG,UAAU,EAAE,CA2CnF"}