@sakibcoolz/core 0.1.2 → 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
package/README.md CHANGED
@@ -1,17 +1,77 @@
1
1
  # @sakibcoolz/core
2
2
 
3
- Framework-neutral scene model for 3D SCADA scenes: typed component ports, connections, validation, schema migration, snapping, orthogonal pipe routing, open-joint and collision checks, telemetry bindings. No rendering, no network.
3
+ The **framework-neutral scene model** of the [3D SCADA SDK](https://github.com/sakibcoolz/3d-scada-sdk): the industrial scene document (schema version 4), typed ports in five domains, validation and migration, snapping and attach, orthogonal pipe routing, open-joint, touching-port and collision checks, connection-graph queries, assemblies, copy/paste, acquisition descriptions, tag bindings and read-only telemetry. No React, no Three.js, no DOM, no network — it runs in browsers, Node.js and workers.
4
4
 
5
5
  ```sh
6
- npm install @sakibcoolz/core
6
+ npm install @sakibcoolz/core @sakibcoolz/components
7
7
  ```
8
+ ESM only · Node.js 20+ · TypeScript types included · MIT
8
9
 
9
- ```ts
10
- import { parseScene, validateScene } from '@sakibcoolz/core';
10
+ Core knows nothing about specific equipment: you pass catalog lookups. [`@sakibcoolz/components`](https://www.npmjs.com/package/@sakibcoolz/components) provides them as `catalogResolvers`, or as `registry.resolvers` when you register your own types.
11
+
12
+ ## Quick start
13
+
14
+ ```js
15
+ import { parseScene, serializeScene, findOpenJoints, findCollisions, routeScene, neighbours, projectAcquisition, sceneDigest } from '@sakibcoolz/core';
16
+ import { catalogResolvers } from '@sakibcoolz/components';
17
+
18
+ const at = (id, type, x, properties = {}) => ({
19
+ id, type, position: { x, y: 0.084, z: 0 }, rotation: { x: 0, y: 0, z: 0, w: 1 },
20
+ scale: { x: 1, y: 1, z: 1 }, properties,
21
+ });
22
+ const scene = {
23
+ schemaVersion: 4, documentType: 'industrial.scene', units: 'm',
24
+ project: { id: 'line-2', name: 'Transfer line', revision: '1' },
25
+ components: [at('a', 'pipe', 0, { length: 2 }), at('b', 'pipe', 2, { length: 2 })],
26
+ connections: [{ id: 'j', from: { instanceId: 'a', portId: 'out' }, to: { instanceId: 'b', portId: 'in' } }],
27
+ dataSources: [{ id: 'plc1', protocol: 'modbus-tcp', endpointRef: 'host:plc-1' }], // a reference, never credentials
28
+ runtimeVariables: [{ id: 'FT-101', dataType: 'float32', unit: 'm³/h', access: 'read',
29
+ source: { dataSourceId: 'plc1', address: { area: 'holding', address: 100, unitId: 1 } } }],
30
+ tagBindings: [{ id: 'b1', tagId: 'FT-101', kind: 'flow', connectionId: 'j' }],
31
+ };
32
+
33
+ const result = parseScene(JSON.stringify(scene), catalogResolvers); // validates; v1–v3 files migrate on load
34
+ if (!result.ok) throw new Error(result.errors.map((e) => e.message).join('\n'));
35
+
36
+ findOpenJoints(result.scene, catalogResolvers); // [] — connected ports meet face to face
37
+ findCollisions(result.scene, catalogResolvers); // overlapping solid components
38
+ routeScene(result.scene, catalogResolvers); // pipe centrelines for connections that do not touch
39
+ neighbours(result.scene, 'a'); // ['b'] — one edge away
40
+ projectAcquisition(result.scene); // what the host's gateway should read; sent nowhere
41
+ sceneDigest(result.scene); // SHA-256 of the canonical form, for approval records
42
+ serializeScene(result.scene); // canonical JSON for saving (always version 4)
11
43
  ```
12
44
 
13
- Part of the [3D SCADA SDK](https://github.com/sakibcoolz/3d-scada-sdk). API reference: [docs/API.md](https://github.com/sakibcoolz/3d-scada-sdk/blob/develop/docs/API.md). ESM only, Node.js 20+.
45
+ ## Scene format (schema v4, `documentType: "industrial.scene"`)
46
+ - `project`: optional `{ id?, name?, revision?, description? }`.
47
+ - `components[]`: `id`, optional `name`, `type` (a built-in kind or a namespaced host type such as `com.example.press`), `position {x,y,z}` (metres, Y up, right-handed), `rotation {x,y,z,w}` (unit quaternion), `scale`, `properties`, optional `parentId` (assemblies), `labels` (string map) and `extensions`.
48
+ - `connections[]`: `{ id, domain?, relationship?, from: { instanceId, portId }, to: { instanceId, portId }, properties? }` between typed ports (`fluid`, `mechanical`, `power`, `signal`, `network`) — explicit; nothing is connected implicitly.
49
+ - `associations[]`: directed, port-less relationships between components (e.g. a PLC `controls` a drive); never drawn as wiring or routed.
50
+ - `dataSources[]` and `runtimeVariables[]`: descriptions of how a host acquires tags (protocol, opaque `endpointRef`, typed Modbus / OPC UA address, scaling). `access` is `'read'` or `'readWrite'`; a `readWrite` variable requires `command` limits.
51
+ - Optional `tagBindings[]` (tag → property / connection flow), `views` (2D process and electrical layouts), `underlay` (sanitized SVG drawing) and namespaced `extensions`.
52
+ - Files never hold live values, code or credentials; credential-like keys are rejected (`SECRET_FIELD`). Versions 1–3 migrate on load and report `migratedFrom`. JSON Schema: `@sakibcoolz/core/schema/industrial-scene.v4.schema.json`.
53
+
54
+ ## API overview
55
+
56
+ | Area | Functions |
57
+ |---|---|
58
+ | Scene model | `createEmptyScene`, `componentKinds`, `isBuiltInKind`, `isNamespacedType`, `portKinds`, `checkPorts`, `portsCompatible`, `relationshipKinds`, `SCENE_SCHEMA_VERSION`, `SCENE_DOCUMENT_TYPE` |
59
+ | Validation and files | `validateScene`, `parseScene`, `serializeScene`, `defaultSceneLimits`, `sceneDigest`, `sha256Hex` |
60
+ | Units | `canonicalUnits`, `isKnownUnit`, `toCanonical` |
61
+ | Transforms | `portToWorld`, `rotateVector`, `quaternionFromEuler`, `quaternionToEuler` |
62
+ | Placing parts | `findSnapCandidates`, `snapComponent`, `attachComponents`, `seatOnConnection` |
63
+ | Graph | `connect`, `disconnect`, `removeComponent`, `addAssociation`, `removeAssociation`, `connectionsOf`, `neighbours`, `relatedComponents`, `pathBetween`, `reachableComponents`, `connectedGroups` |
64
+ | Routing | `routeConnection`, `routeScene`, `sampleRouteCenterline` |
65
+ | Checks | `findOpenJoints`, `findTouchingPorts`, `connectTouchingPorts`, `findCollisions` |
66
+ | Assemblies, clipboard | `assemblyOf`, `moveAssembly`, `lockConnectedAsChildren`, `unlockFromParent`, `copyComponents`, `pasteComponents` |
67
+ | Tags and acquisition | `projectAcquisition`, `suggestVariables`, `reviewTags`, `applyTagSuggestions`, `dataTypes`, `protocols`, `modbusAreas` |
68
+ | Older files | `upgradeDimensions(scene, { ...catalogResolvers, ...legacyCatalogResolvers })` |
69
+ | Live data (read-only) | `resolveTelemetry`, `resolveVariableValues`, `valueFitsDataType`, `resolveFlow`, `displayScene`, `createObservationStore`, `connectProvider` |
70
+ | Status | `componentStatus`, `alarmSeverities` |
71
+
72
+ Functions are pure: they return new scenes or result objects and never mutate their input. Time (`now`) is passed in, so results are deterministic.
14
73
 
15
- The SDK visualises and simulates; it does not connect to or control PLCs or physical equipment.
74
+ ## Safety
75
+ Core is **read-only with respect to plants**: it never writes to devices, opens network connections or holds credentials. Live values and alarms come from the host. Data sources and runtime variables only *describe* acquisition; the host's gateway does the reading. Writable (`readWrite`) variables are describe-and-request only (ADR-0029): the SDK can raise a confirmed command request, and the host authorizes, audits, enforces limits and writes. Validation is an engineering aid; it does not verify physical wiring, safe operating limits or functional safety.
16
76
 
17
- MIT licence.
77
+ Full API reference: [docs/API.md](https://github.com/sakibcoolz/3d-scada-sdk/blob/develop/docs/API.md) · Scene format: [docs/INDUSTRIAL_SCENE_SPEC.md](https://github.com/sakibcoolz/3d-scada-sdk/blob/develop/docs/INDUSTRIAL_SCENE_SPEC.md) · Host integration: [docs/RUNTIME_INTEGRATION.md](https://github.com/sakibcoolz/3d-scada-sdk/blob/develop/docs/RUNTIME_INTEGRATION.md) · Licence: MIT
@@ -0,0 +1,29 @@
1
+ import type { DataType, RuntimeVariable, ScadaScene, SceneDataSource, VariableAddress } from './scene.js';
2
+ /** One tag a host's gateway acquires (ADR-0024). */
3
+ export interface AcquisitionVariable {
4
+ tagId: string;
5
+ dataSourceId: string;
6
+ dataType: DataType;
7
+ unit?: string;
8
+ variable?: string;
9
+ address?: VariableAddress;
10
+ scaling?: RuntimeVariable['scaling'];
11
+ access: RuntimeVariable['access'];
12
+ /** A writable variable's limits (ADR-0029). The gateway must enforce them itself. */
13
+ command?: RuntimeVariable['command'];
14
+ }
15
+ /** Everything a gateway needs to acquire a scene's tags, and nothing it does not: no geometry, views or labels. */
16
+ export interface AcquisitionPlan {
17
+ /** The scene's `project.revision`, so a host can tie a deployed configuration to an approved document. */
18
+ sourceRevision?: string;
19
+ dataSources: SceneDataSource[];
20
+ /** Variables with a source, in scene order. */
21
+ variables: AcquisitionVariable[];
22
+ }
23
+ /**
24
+ * The acquisition description of a validated scene (ADR-0024). Pure and deterministic: the same
25
+ * scene always gives the same plan, in authored order. The SDK never sends it anywhere; the host
26
+ * approves, signs and deploys it, and owns connectivity and credentials.
27
+ */
28
+ export declare function projectAcquisition(scene: ScadaScene): AcquisitionPlan;
29
+ //# sourceMappingURL=acquisition.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"acquisition.d.ts","sourceRoot":"","sources":["../src/acquisition.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,eAAe,EAAE,UAAU,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAE1G,oDAAoD;AACpD,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,QAAQ,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,eAAe,CAAC;IAC1B,OAAO,CAAC,EAAE,eAAe,CAAC,SAAS,CAAC,CAAC;IACrC,MAAM,EAAE,eAAe,CAAC,QAAQ,CAAC,CAAC;IAClC,qFAAqF;IACrF,OAAO,CAAC,EAAE,eAAe,CAAC,SAAS,CAAC,CAAC;CACtC;AAED,mHAAmH;AACnH,MAAM,WAAW,eAAe;IAC9B,0GAA0G;IAC1G,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,WAAW,EAAE,eAAe,EAAE,CAAC;IAC/B,+CAA+C;IAC/C,SAAS,EAAE,mBAAmB,EAAE,CAAC;CAClC;AAID;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,UAAU,GAAG,eAAe,CAqBrE"}
@@ -0,0 +1,30 @@
1
+ const copy = (value) => JSON.parse(JSON.stringify(value));
2
+ /**
3
+ * The acquisition description of a validated scene (ADR-0024). Pure and deterministic: the same
4
+ * scene always gives the same plan, in authored order. The SDK never sends it anywhere; the host
5
+ * approves, signs and deploys it, and owns connectivity and credentials.
6
+ */
7
+ export function projectAcquisition(scene) {
8
+ const variables = [];
9
+ for (const v of scene.runtimeVariables ?? []) {
10
+ if (!v.source)
11
+ continue;
12
+ variables.push({
13
+ tagId: v.id,
14
+ dataSourceId: v.source.dataSourceId,
15
+ dataType: v.dataType,
16
+ ...(v.unit !== undefined ? { unit: v.unit } : {}),
17
+ ...(v.source.variable !== undefined ? { variable: v.source.variable } : {}),
18
+ ...(v.source.address !== undefined ? { address: copy(v.source.address) } : {}),
19
+ ...(v.scaling ? { scaling: { ...v.scaling } } : {}),
20
+ access: v.access,
21
+ ...(v.command ? { command: { ...v.command } } : {}),
22
+ });
23
+ }
24
+ return {
25
+ ...(scene.project?.revision !== undefined ? { sourceRevision: scene.project.revision } : {}),
26
+ dataSources: (scene.dataSources ?? []).map(copy),
27
+ variables,
28
+ };
29
+ }
30
+ //# sourceMappingURL=acquisition.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"acquisition.js","sourceRoot":"","sources":["../src/acquisition.ts"],"names":[],"mappings":"AAyBA,MAAM,IAAI,GAAG,CAAI,KAAQ,EAAK,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAM,CAAC;AAExE;;;;GAIG;AACH,MAAM,UAAU,kBAAkB,CAAC,KAAiB;IAClD,MAAM,SAAS,GAA0B,EAAE,CAAC;IAC5C,KAAK,MAAM,CAAC,IAAI,KAAK,CAAC,gBAAgB,IAAI,EAAE,EAAE,CAAC;QAC7C,IAAI,CAAC,CAAC,CAAC,MAAM;YAAE,SAAS;QACxB,SAAS,CAAC,IAAI,CAAC;YACb,KAAK,EAAE,CAAC,CAAC,EAAE;YACX,YAAY,EAAE,CAAC,CAAC,MAAM,CAAC,YAAY;YACnC,QAAQ,EAAE,CAAC,CAAC,QAAQ;YACpB,GAAG,CAAC,CAAC,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACjD,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAC3E,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAC9E,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,EAAE,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACnD,MAAM,EAAE,CAAC,CAAC,MAAM;YAChB,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,EAAE,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACpD,CAAC,CAAC;IACL,CAAC;IACD,OAAO;QACL,GAAG,CAAC,KAAK,CAAC,OAAO,EAAE,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,cAAc,EAAE,KAAK,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC5F,WAAW,EAAE,CAAC,KAAK,CAAC,WAAW,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC;QAChD,SAAS;KACV,CAAC;AACJ,CAAC","sourcesContent":["import type { DataType, RuntimeVariable, ScadaScene, SceneDataSource, VariableAddress } from './scene.js';\n\n/** One tag a host's gateway acquires (ADR-0024). */\nexport interface AcquisitionVariable {\n tagId: string;\n dataSourceId: string;\n dataType: DataType;\n unit?: string;\n variable?: string;\n address?: VariableAddress;\n scaling?: RuntimeVariable['scaling'];\n access: RuntimeVariable['access'];\n /** A writable variable's limits (ADR-0029). The gateway must enforce them itself. */\n command?: RuntimeVariable['command'];\n}\n\n/** Everything a gateway needs to acquire a scene's tags, and nothing it does not: no geometry, views or labels. */\nexport interface AcquisitionPlan {\n /** The scene's `project.revision`, so a host can tie a deployed configuration to an approved document. */\n sourceRevision?: string;\n dataSources: SceneDataSource[];\n /** Variables with a source, in scene order. */\n variables: AcquisitionVariable[];\n}\n\nconst copy = <T>(value: T): T => JSON.parse(JSON.stringify(value)) as T;\n\n/**\n * The acquisition description of a validated scene (ADR-0024). Pure and deterministic: the same\n * scene always gives the same plan, in authored order. The SDK never sends it anywhere; the host\n * approves, signs and deploys it, and owns connectivity and credentials.\n */\nexport function projectAcquisition(scene: ScadaScene): AcquisitionPlan {\n const variables: AcquisitionVariable[] = [];\n for (const v of scene.runtimeVariables ?? []) {\n if (!v.source) continue;\n variables.push({\n tagId: v.id,\n dataSourceId: v.source.dataSourceId,\n dataType: v.dataType,\n ...(v.unit !== undefined ? { unit: v.unit } : {}),\n ...(v.source.variable !== undefined ? { variable: v.source.variable } : {}),\n ...(v.source.address !== undefined ? { address: copy(v.source.address) } : {}),\n ...(v.scaling ? { scaling: { ...v.scaling } } : {}),\n access: v.access,\n ...(v.command ? { command: { ...v.command } } : {}),\n });\n }\n return {\n ...(scene.project?.revision !== undefined ? { sourceRevision: scene.project.revision } : {}),\n dataSources: (scene.dataSources ?? []).map(copy),\n variables,\n };\n}\n"]}
@@ -1,3 +1,4 @@
1
+ import { type ConnectionFilter } from './graph.js';
1
2
  import type { QuaternionLike, ScadaScene, Vector3Like } from './scene.js';
2
3
  /** Ids of the whole assembly containing `id` (its root and every descendant), in scene order. */
3
4
  export declare function assemblyOf(scene: ScadaScene, id: string): string[];
@@ -19,9 +20,11 @@ export type AssemblyResult = {
19
20
  };
20
21
  /**
21
22
  * Makes `id` an assembly root and locks every component connected to it (through any path of
22
- * connections) as its child. Earlier locks among those components are replaced.
23
+ * connections) as its child. Earlier locks among those components are replaced. With
24
+ * `resolvePorts`, only physical (fluid and mechanical) connections count: a part wired to another
25
+ * by a power, signal or network cable is not bolted to it (ADR-0023).
23
26
  */
24
- export declare function lockConnectedAsChildren(scene: ScadaScene, id: string): AssemblyResult;
27
+ export declare function lockConnectedAsChildren(scene: ScadaScene, id: string, options?: Pick<ConnectionFilter, 'resolvePorts'>): AssemblyResult;
25
28
  /** Frees `id` from its parent, and frees `id`'s own children, so neither moves with the other any more. */
26
29
  export declare function unlockFromParent(scene: ScadaScene, id: string): ScadaScene;
27
30
  //# sourceMappingURL=assembly.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"assembly.d.ts","sourceRoot":"","sources":["../src/assembly.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAqB,cAAc,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAY7F,iGAAiG;AACjG,wBAAgB,UAAU,CAAC,KAAK,EAAE,UAAU,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM,EAAE,CAclE;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,UAAU,EAAE,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,WAAW,EAAE,QAAQ,EAAE,cAAc,GAAG,UAAU,CAgBvH;AAED,MAAM,MAAM,cAAc,GAAG;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,UAAU,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE;QAAE,IAAI,EAAE,kBAAkB,GAAG,mBAAmB,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,CAAC;AAEzJ;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,UAAU,EAAE,EAAE,EAAE,MAAM,GAAG,cAAc,CAYrF;AAED,2GAA2G;AAC3G,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,UAAU,EAAE,EAAE,EAAE,MAAM,GAAG,UAAU,CAS1E"}
1
+ {"version":3,"file":"assembly.d.ts","sourceRoot":"","sources":["../src/assembly.ts"],"names":[],"mappings":"AAAA,OAAO,EAAwC,KAAK,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAEzF,OAAO,KAAK,EAAqB,cAAc,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAY7F,iGAAiG;AACjG,wBAAgB,UAAU,CAAC,KAAK,EAAE,UAAU,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM,EAAE,CAclE;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,UAAU,EAAE,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,WAAW,EAAE,QAAQ,EAAE,cAAc,GAAG,UAAU,CAgBvH;AAED,MAAM,MAAM,cAAc,GAAG;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,UAAU,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE;QAAE,IAAI,EAAE,kBAAkB,GAAG,mBAAmB,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,CAAC;AAEzJ;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,UAAU,EAAE,EAAE,EAAE,MAAM,EAAE,OAAO,GAAE,IAAI,CAAC,gBAAgB,EAAE,cAAc,CAAM,GAAG,cAAc,CAa3I;AAED,2GAA2G;AAC3G,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,UAAU,EAAE,EAAE,EAAE,MAAM,GAAG,UAAU,CAS1E"}
package/dist/assembly.js CHANGED
@@ -1,4 +1,4 @@
1
- import { reachableComponents } from './graph.js';
1
+ import { physicalDomains, reachableComponents } from './graph.js';
2
2
  import { multiplyQuaternions, normalizeQuaternion, rotateVector } from './quaternion.js';
3
3
  import { quaternionToTuple, tupleToQuaternion } from './vectors.js';
4
4
  // Assemblies (ADR-0008): children carry `parentId`; a root and all its descendants move rigidly.
@@ -51,12 +51,15 @@ export function moveAssembly(scene, id, position, rotation) {
51
51
  }
52
52
  /**
53
53
  * Makes `id` an assembly root and locks every component connected to it (through any path of
54
- * connections) as its child. Earlier locks among those components are replaced.
54
+ * connections) as its child. Earlier locks among those components are replaced. With
55
+ * `resolvePorts`, only physical (fluid and mechanical) connections count: a part wired to another
56
+ * by a power, signal or network cable is not bolted to it (ADR-0023).
55
57
  */
56
- export function lockConnectedAsChildren(scene, id) {
58
+ export function lockConnectedAsChildren(scene, id, options = {}) {
57
59
  if (!scene.components.some((c) => c.id === id))
58
60
  return { ok: false, error: { code: 'UNKNOWN_INSTANCE', message: `No component "${id}".` } };
59
- const connected = new Set(reachableComponents(scene, id).filter((other) => other !== id));
61
+ const filter = options.resolvePorts ? { domains: physicalDomains, resolvePorts: options.resolvePorts } : {};
62
+ const connected = new Set(reachableComponents(scene, id, filter).filter((other) => other !== id));
60
63
  if (connected.size === 0)
61
64
  return { ok: false, error: { code: 'NOTHING_CONNECTED', message: 'Nothing is connected to this component.' } };
62
65
  const components = scene.components.map((c) => {
@@ -1 +1 @@
1
- {"version":3,"file":"assembly.js","sourceRoot":"","sources":["../src/assembly.ts"],"names":[],"mappings":"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","sourcesContent":["import { reachableComponents } from './graph.js';\nimport { multiplyQuaternions, normalizeQuaternion, rotateVector } from './quaternion.js';\nimport type { ComponentInstance, QuaternionLike, ScadaScene, Vector3Like } from './scene.js';\nimport { quaternionToTuple, tupleToQuaternion } from './vectors.js';\n\n// Assemblies (ADR-0008): children carry `parentId`; a root and all its descendants move rigidly.\n\nfunction rootOf(scene: ScadaScene, id: string): string {\n const parents = new Map(scene.components.map((c) => [c.id, c.parentId]));\n let root = id;\n for (let steps = 0; parents.get(root) !== undefined && steps <= scene.components.length; steps++) root = parents.get(root)!;\n return root;\n}\n\n/** Ids of the whole assembly containing `id` (its root and every descendant), in scene order. */\nexport function assemblyOf(scene: ScadaScene, id: string): string[] {\n const root = rootOf(scene, id);\n const members = new Set([root]);\n // Parents can appear after their children in the list, so repeat until nothing is added.\n for (let added = true; added; ) {\n added = false;\n for (const c of scene.components) {\n if (c.parentId !== undefined && members.has(c.parentId) && !members.has(c.id)) {\n members.add(c.id);\n added = true;\n }\n }\n }\n return scene.components.filter((c) => members.has(c.id)).map((c) => c.id);\n}\n\n/**\n * Gives member `id` a new pose and applies the same rigid motion to the rest of its assembly:\n * every member keeps its offset and relative rotation, so joined ports stay joined. A component\n * outside any assembly simply moves on its own.\n */\nexport function moveAssembly(scene: ScadaScene, id: string, position: Vector3Like, rotation: QuaternionLike): ScadaScene {\n const moving = scene.components.find((c) => c.id === id);\n if (!moving) return scene;\n const members = new Set(assemblyOf(scene, id));\n const [x, y, z, w] = quaternionToTuple(moving.rotation);\n const delta = normalizeQuaternion(multiplyQuaternions(quaternionToTuple(rotation), [-x, -y, -z, w]));\n const place = (c: ComponentInstance): ComponentInstance => {\n if (c.id === id) return { ...c, position, rotation };\n const offset = rotateVector([c.position.x - moving.position.x, c.position.y - moving.position.y, c.position.z - moving.position.z], delta);\n return {\n ...c,\n position: { x: position.x + offset[0], y: position.y + offset[1], z: position.z + offset[2] },\n rotation: tupleToQuaternion(normalizeQuaternion(multiplyQuaternions(delta, quaternionToTuple(c.rotation)))),\n };\n };\n return { ...scene, components: scene.components.map((c) => (members.has(c.id) ? place(c) : c)) };\n}\n\nexport type AssemblyResult = { ok: true; scene: ScadaScene } | { ok: false; error: { code: 'UNKNOWN_INSTANCE' | 'NOTHING_CONNECTED'; message: string } };\n\n/**\n * Makes `id` an assembly root and locks every component connected to it (through any path of\n * connections) as its child. Earlier locks among those components are replaced.\n */\nexport function lockConnectedAsChildren(scene: ScadaScene, id: string): AssemblyResult {\n if (!scene.components.some((c) => c.id === id)) return { ok: false, error: { code: 'UNKNOWN_INSTANCE', message: `No component \"${id}\".` } };\n const connected = new Set(reachableComponents(scene, id).filter((other) => other !== id));\n if (connected.size === 0) return { ok: false, error: { code: 'NOTHING_CONNECTED', message: 'Nothing is connected to this component.' } };\n const components = scene.components.map((c) => {\n if (c.id === id) {\n const { parentId: _, ...root } = c;\n return root;\n }\n return connected.has(c.id) ? { ...c, parentId: id } : c;\n });\n return { ok: true, scene: { ...scene, components } };\n}\n\n/** Frees `id` from its parent, and frees `id`'s own children, so neither moves with the other any more. */\nexport function unlockFromParent(scene: ScadaScene, id: string): ScadaScene {\n return {\n ...scene,\n components: scene.components.map((c) => {\n if (c.id !== id && c.parentId !== id) return c;\n const { parentId: _, ...free } = c;\n return free;\n }),\n };\n}\n"]}
1
+ {"version":3,"file":"assembly.js","sourceRoot":"","sources":["../src/assembly.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,mBAAmB,EAAyB,MAAM,YAAY,CAAC;AACzF,OAAO,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAEzF,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAEpE,iGAAiG;AAEjG,SAAS,MAAM,CAAC,KAAiB,EAAE,EAAU;IAC3C,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;IACzE,IAAI,IAAI,GAAG,EAAE,CAAC;IACd,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,SAAS,IAAI,KAAK,IAAI,KAAK,CAAC,UAAU,CAAC,MAAM,EAAE,KAAK,EAAE;QAAE,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,CAAE,CAAC;IAC5H,OAAO,IAAI,CAAC;AACd,CAAC;AAED,iGAAiG;AACjG,MAAM,UAAU,UAAU,CAAC,KAAiB,EAAE,EAAU;IACtD,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IAC/B,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;IAChC,yFAAyF;IACzF,KAAK,IAAI,KAAK,GAAG,IAAI,EAAE,KAAK,GAAI,CAAC;QAC/B,KAAK,GAAG,KAAK,CAAC;QACd,KAAK,MAAM,CAAC,IAAI,KAAK,CAAC,UAAU,EAAE,CAAC;YACjC,IAAI,CAAC,CAAC,QAAQ,KAAK,SAAS,IAAI,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC;gBAC9E,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;gBAClB,KAAK,GAAG,IAAI,CAAC;YACf,CAAC;QACH,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;AAC5E,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,YAAY,CAAC,KAAiB,EAAE,EAAU,EAAE,QAAqB,EAAE,QAAwB;IACzG,MAAM,MAAM,GAAG,KAAK,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC;IACzD,IAAI,CAAC,MAAM;QAAE,OAAO,KAAK,CAAC;IAC1B,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,UAAU,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,CAAC;IAC/C,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,GAAG,iBAAiB,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;IACxD,MAAM,KAAK,GAAG,mBAAmB,CAAC,mBAAmB,CAAC,iBAAiB,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;IACrG,MAAM,KAAK,GAAG,CAAC,CAAoB,EAAqB,EAAE;QACxD,IAAI,CAAC,CAAC,EAAE,KAAK,EAAE;YAAE,OAAO,EAAE,GAAG,CAAC,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC;QACrD,MAAM,MAAM,GAAG,YAAY,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC;QAC3I,OAAO;YACL,GAAG,CAAC;YACJ,QAAQ,EAAE,EAAE,CAAC,EAAE,QAAQ,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,QAAQ,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,QAAQ,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,EAAE;YAC7F,QAAQ,EAAE,iBAAiB,CAAC,mBAAmB,CAAC,mBAAmB,CAAC,KAAK,EAAE,iBAAiB,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;SAC5G,CAAC;IACJ,CAAC,CAAC;IACF,OAAO,EAAE,GAAG,KAAK,EAAE,UAAU,EAAE,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;AACnG,CAAC;AAID;;;;;GAKG;AACH,MAAM,UAAU,uBAAuB,CAAC,KAAiB,EAAE,EAAU,EAAE,UAAkD,EAAE;IACzH,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,kBAAkB,EAAE,OAAO,EAAE,iBAAiB,EAAE,IAAI,EAAE,EAAE,CAAC;IAC5I,MAAM,MAAM,GAAqB,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,eAAe,EAAE,YAAY,EAAE,OAAO,CAAC,YAAY,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IAC9H,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC,mBAAmB,CAAC,KAAK,EAAE,EAAE,EAAE,MAAM,CAAC,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,KAAK,EAAE,CAAC,CAAC,CAAC;IAClG,IAAI,SAAS,CAAC,IAAI,KAAK,CAAC;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,mBAAmB,EAAE,OAAO,EAAE,yCAAyC,EAAE,EAAE,CAAC;IACzI,MAAM,UAAU,GAAG,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE;QAC5C,IAAI,CAAC,CAAC,EAAE,KAAK,EAAE,EAAE,CAAC;YAChB,MAAM,EAAE,QAAQ,EAAE,CAAC,EAAE,GAAG,IAAI,EAAE,GAAG,CAAC,CAAC;YACnC,OAAO,IAAI,CAAC;QACd,CAAC;QACD,OAAO,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;IAC1D,CAAC,CAAC,CAAC;IACH,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,EAAE,GAAG,KAAK,EAAE,UAAU,EAAE,EAAE,CAAC;AACvD,CAAC;AAED,2GAA2G;AAC3G,MAAM,UAAU,gBAAgB,CAAC,KAAiB,EAAE,EAAU;IAC5D,OAAO;QACL,GAAG,KAAK;QACR,UAAU,EAAE,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE;YACrC,IAAI,CAAC,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC,QAAQ,KAAK,EAAE;gBAAE,OAAO,CAAC,CAAC;YAC/C,MAAM,EAAE,QAAQ,EAAE,CAAC,EAAE,GAAG,IAAI,EAAE,GAAG,CAAC,CAAC;YACnC,OAAO,IAAI,CAAC;QACd,CAAC,CAAC;KACH,CAAC;AACJ,CAAC","sourcesContent":["import { physicalDomains, reachableComponents, type ConnectionFilter } from './graph.js';\nimport { multiplyQuaternions, normalizeQuaternion, rotateVector } from './quaternion.js';\nimport type { ComponentInstance, QuaternionLike, ScadaScene, Vector3Like } from './scene.js';\nimport { quaternionToTuple, tupleToQuaternion } from './vectors.js';\n\n// Assemblies (ADR-0008): children carry `parentId`; a root and all its descendants move rigidly.\n\nfunction rootOf(scene: ScadaScene, id: string): string {\n const parents = new Map(scene.components.map((c) => [c.id, c.parentId]));\n let root = id;\n for (let steps = 0; parents.get(root) !== undefined && steps <= scene.components.length; steps++) root = parents.get(root)!;\n return root;\n}\n\n/** Ids of the whole assembly containing `id` (its root and every descendant), in scene order. */\nexport function assemblyOf(scene: ScadaScene, id: string): string[] {\n const root = rootOf(scene, id);\n const members = new Set([root]);\n // Parents can appear after their children in the list, so repeat until nothing is added.\n for (let added = true; added; ) {\n added = false;\n for (const c of scene.components) {\n if (c.parentId !== undefined && members.has(c.parentId) && !members.has(c.id)) {\n members.add(c.id);\n added = true;\n }\n }\n }\n return scene.components.filter((c) => members.has(c.id)).map((c) => c.id);\n}\n\n/**\n * Gives member `id` a new pose and applies the same rigid motion to the rest of its assembly:\n * every member keeps its offset and relative rotation, so joined ports stay joined. A component\n * outside any assembly simply moves on its own.\n */\nexport function moveAssembly(scene: ScadaScene, id: string, position: Vector3Like, rotation: QuaternionLike): ScadaScene {\n const moving = scene.components.find((c) => c.id === id);\n if (!moving) return scene;\n const members = new Set(assemblyOf(scene, id));\n const [x, y, z, w] = quaternionToTuple(moving.rotation);\n const delta = normalizeQuaternion(multiplyQuaternions(quaternionToTuple(rotation), [-x, -y, -z, w]));\n const place = (c: ComponentInstance): ComponentInstance => {\n if (c.id === id) return { ...c, position, rotation };\n const offset = rotateVector([c.position.x - moving.position.x, c.position.y - moving.position.y, c.position.z - moving.position.z], delta);\n return {\n ...c,\n position: { x: position.x + offset[0], y: position.y + offset[1], z: position.z + offset[2] },\n rotation: tupleToQuaternion(normalizeQuaternion(multiplyQuaternions(delta, quaternionToTuple(c.rotation)))),\n };\n };\n return { ...scene, components: scene.components.map((c) => (members.has(c.id) ? place(c) : c)) };\n}\n\nexport type AssemblyResult = { ok: true; scene: ScadaScene } | { ok: false; error: { code: 'UNKNOWN_INSTANCE' | 'NOTHING_CONNECTED'; message: string } };\n\n/**\n * Makes `id` an assembly root and locks every component connected to it (through any path of\n * connections) as its child. Earlier locks among those components are replaced. With\n * `resolvePorts`, only physical (fluid and mechanical) connections count: a part wired to another\n * by a power, signal or network cable is not bolted to it (ADR-0023).\n */\nexport function lockConnectedAsChildren(scene: ScadaScene, id: string, options: Pick<ConnectionFilter, 'resolvePorts'> = {}): AssemblyResult {\n if (!scene.components.some((c) => c.id === id)) return { ok: false, error: { code: 'UNKNOWN_INSTANCE', message: `No component \"${id}\".` } };\n const filter: ConnectionFilter = options.resolvePorts ? { domains: physicalDomains, resolvePorts: options.resolvePorts } : {};\n const connected = new Set(reachableComponents(scene, id, filter).filter((other) => other !== id));\n if (connected.size === 0) return { ok: false, error: { code: 'NOTHING_CONNECTED', message: 'Nothing is connected to this component.' } };\n const components = scene.components.map((c) => {\n if (c.id === id) {\n const { parentId: _, ...root } = c;\n return root;\n }\n return connected.has(c.id) ? { ...c, parentId: id } : c;\n });\n return { ok: true, scene: { ...scene, components } };\n}\n\n/** Frees `id` from its parent, and frees `id`'s own children, so neither moves with the other any more. */\nexport function unlockFromParent(scene: ScadaScene, id: string): ScadaScene {\n return {\n ...scene,\n components: scene.components.map((c) => {\n if (c.id !== id && c.parentId !== id) return c;\n const { parentId: _, ...free } = c;\n return free;\n }),\n };\n}\n"]}
package/dist/attach.d.ts CHANGED
@@ -1,6 +1,8 @@
1
- import { type ComponentInstance, type ComponentKind, type ConnectionPort, type ScadaScene, type SceneConnection } from './scene.js';
1
+ import { type ComponentInstance, type ComponentType, type ConnectionPort, type ScadaScene, type SceneConnection } from './scene.js';
2
2
  export interface AttachOptions {
3
- resolvePorts: (type: ComponentKind, properties?: ComponentInstance['properties']) => readonly ConnectionPort[] | undefined;
3
+ resolvePorts: (type: ComponentType, properties?: ComponentInstance['properties']) => readonly ConnectionPort[] | undefined;
4
+ /** Whether the host knows a type (ADR-0022); without it only built-in kinds connect. */
5
+ resolveType?: (type: ComponentType) => boolean;
4
6
  createId: () => string;
5
7
  }
6
8
  export type AttachResult = {
@@ -1 +1 @@
1
- {"version":3,"file":"attach.d.ts","sourceRoot":"","sources":["../src/attach.ts"],"names":[],"mappings":"AAEA,OAAO,EAAmB,KAAK,iBAAiB,EAAE,KAAK,aAAa,EAAE,KAAK,cAAc,EAAE,KAAK,UAAU,EAAE,KAAK,eAAe,EAAE,MAAM,YAAY,CAAC;AAGrJ,MAAM,WAAW,aAAa;IAC5B,YAAY,EAAE,CAAC,IAAI,EAAE,aAAa,EAAE,UAAU,CAAC,EAAE,iBAAiB,CAAC,YAAY,CAAC,KAAK,SAAS,cAAc,EAAE,GAAG,SAAS,CAAC;IAC3H,QAAQ,EAAE,MAAM,MAAM,CAAC;CACxB;AAED,MAAM,MAAM,YAAY,GACpB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,UAAU,CAAC;IAAC,UAAU,EAAE,eAAe,CAAC,CAAC,mDAAmD;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAC/H;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE;QAAE,IAAI,EAAE,kBAAkB,GAAG,gBAAgB,GAAG,mBAAmB,GAAG,SAAS,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,CAAC;AAE7H;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,aAAa,GAAG,YAAY,CAiB3H"}
1
+ {"version":3,"file":"attach.d.ts","sourceRoot":"","sources":["../src/attach.ts"],"names":[],"mappings":"AAEA,OAAO,EAAkD,KAAK,iBAAiB,EAAE,KAAK,aAAa,EAAE,KAAK,cAAc,EAAE,KAAK,UAAU,EAAE,KAAK,eAAe,EAAE,MAAM,YAAY,CAAC;AAGpL,MAAM,WAAW,aAAa;IAC5B,YAAY,EAAE,CAAC,IAAI,EAAE,aAAa,EAAE,UAAU,CAAC,EAAE,iBAAiB,CAAC,YAAY,CAAC,KAAK,SAAS,cAAc,EAAE,GAAG,SAAS,CAAC;IAC3H,wFAAwF;IACxF,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,aAAa,KAAK,OAAO,CAAC;IAC/C,QAAQ,EAAE,MAAM,MAAM,CAAC;CACxB;AAED,MAAM,MAAM,YAAY,GACpB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,UAAU,CAAC;IAAC,UAAU,EAAE,eAAe,CAAC,CAAC,mDAAmD;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAC/H;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE;QAAE,IAAI,EAAE,kBAAkB,GAAG,gBAAgB,GAAG,mBAAmB,GAAG,SAAS,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,CAAC;AAE7H;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,aAAa,GAAG,YAAY,CAkB3H"}
package/dist/attach.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { connect } from './graph.js';
2
2
  import { seatOnConnection } from './joints.js';
3
- import { portsCompatible } from './scene.js';
3
+ import { matesPhysically, portCapacity, portsCompatible } from './scene.js';
4
4
  import { portToWorld } from './transforms.js';
5
5
  /**
6
6
  * Attaches `movingId` to `fixedId`: picks the closest pair of free, compatible ports (moving part's
@@ -14,8 +14,10 @@ export function attachComponents(scene, fixedId, movingId, options) {
14
14
  return { ok: false, error: { code: 'UNKNOWN_INSTANCE', message: `No component "${!fixed ? fixedId : movingId}".` } };
15
15
  if (fixedId === movingId)
16
16
  return { ok: false, error: { code: 'SAME_COMPONENT', message: 'A part cannot be attached to itself.' } };
17
- const used = new Set(scene.connections.flatMap((c) => [c.from, c.to]).map((e) => `${e.instanceId}\u0000${e.portId}`));
18
- const free = (c) => (options.resolvePorts(c.type, c.properties) ?? []).filter((p) => !used.has(`${c.id}\u0000${p.id}`)).map((p) => ({ port: p, at: portToWorld(c, p).position }));
17
+ const used = new Map();
18
+ for (const e of scene.connections.flatMap((c) => [c.from, c.to]))
19
+ used.set(`${e.instanceId}\u0000${e.portId}`, (used.get(`${e.instanceId}\u0000${e.portId}`) ?? 0) + 1);
20
+ const free = (c) => (options.resolvePorts(c.type, c.properties) ?? []).filter((p) => matesPhysically(p) && (used.get(`${c.id}\u0000${p.id}`) ?? 0) < portCapacity(p)).map((p) => ({ port: p, at: portToWorld(c, p).position }));
19
21
  const pairs = free(moving).flatMap((m) => free(fixed).filter((f) => portsCompatible(m.port, f.port)).map((f) => ({ m, f, distance: Math.hypot(...m.at.map((n, i) => n - f.at[i])) })));
20
22
  if (pairs.length === 0)
21
23
  return { ok: false, error: { code: 'NO_MATCHING_PORTS', message: 'These parts have no free ports of the same type and size.' } };
@@ -1 +1 @@
1
- {"version":3,"file":"attach.js","sourceRoot":"","sources":["../src/attach.ts"],"names":[],"mappings":"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","sourcesContent":["import { connect } from './graph.js';\nimport { seatOnConnection } from './joints.js';\nimport { portsCompatible, type ComponentInstance, type ComponentKind, type ConnectionPort, type ScadaScene, type SceneConnection } from './scene.js';\nimport { portToWorld } from './transforms.js';\n\nexport interface AttachOptions {\n resolvePorts: (type: ComponentKind, properties?: ComponentInstance['properties']) => readonly ConnectionPort[] | undefined;\n createId: () => string;\n}\n\nexport type AttachResult =\n | { ok: true; scene: ScadaScene; connection: SceneConnection; /** How far the attached port travelled, metres. */ moved: number }\n | { ok: false; error: { code: 'UNKNOWN_INSTANCE' | 'SAME_COMPONENT' | 'NO_MATCHING_PORTS' | 'INVALID'; message: string } };\n\n/**\n * Attaches `movingId` to `fixedId`: picks the closest pair of free, compatible ports (moving part's\n * port to the fixed part's port), connects them and seats the moving part (with its assembly,\n * unless both share one) so the ports meet exactly and face each other. The input is not changed.\n */\nexport function attachComponents(scene: ScadaScene, fixedId: string, movingId: string, options: AttachOptions): AttachResult {\n const byId = new Map(scene.components.map((c) => [c.id, c]));\n const fixed = byId.get(fixedId), moving = byId.get(movingId);\n if (!fixed || !moving) return { ok: false, error: { code: 'UNKNOWN_INSTANCE', message: `No component \"${!fixed ? fixedId : movingId}\".` } };\n if (fixedId === movingId) return { ok: false, error: { code: 'SAME_COMPONENT', message: 'A part cannot be attached to itself.' } };\n const used = new Set(scene.connections.flatMap((c) => [c.from, c.to]).map((e) => `${e.instanceId}\\u0000${e.portId}`));\n const free = (c: ComponentInstance) => (options.resolvePorts(c.type, c.properties) ?? []).filter((p) => !used.has(`${c.id}\\u0000${p.id}`)).map((p) => ({ port: p, at: portToWorld(c, p).position }));\n const pairs = free(moving).flatMap((m) => free(fixed).filter((f) => portsCompatible(m.port, f.port)).map((f) => ({ m, f, distance: Math.hypot(...m.at.map((n, i) => n - f.at[i])) })));\n if (pairs.length === 0) return { ok: false, error: { code: 'NO_MATCHING_PORTS', message: 'These parts have no free ports of the same type and size.' } };\n pairs.sort((a, b) => a.distance - b.distance || a.m.port.id.localeCompare(b.m.port.id) || a.f.port.id.localeCompare(b.f.port.id));\n const best = pairs[0];\n const connection: SceneConnection = { id: options.createId(), from: { instanceId: movingId, portId: best.m.port.id }, to: { instanceId: fixedId, portId: best.f.port.id } };\n const joined = connect(scene, connection, options);\n if (!joined.ok) return { ok: false, error: { code: 'INVALID', message: joined.error.message } };\n const seated = seatOnConnection(joined.scene, connection.id, movingId, options);\n if (!seated.ok) return { ok: false, error: { code: 'INVALID', message: seated.error.message } };\n return { ok: true, scene: seated.scene, connection, moved: seated.moved };\n}\n"]}
1
+ {"version":3,"file":"attach.js","sourceRoot":"","sources":["../src/attach.ts"],"names":[],"mappings":"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","sourcesContent":["import { connect } from './graph.js';\nimport { seatOnConnection } from './joints.js';\nimport { matesPhysically, portCapacity, portsCompatible, type ComponentInstance, type ComponentType, type ConnectionPort, type ScadaScene, type SceneConnection } from './scene.js';\nimport { portToWorld } from './transforms.js';\n\nexport interface AttachOptions {\n resolvePorts: (type: ComponentType, properties?: ComponentInstance['properties']) => readonly ConnectionPort[] | undefined;\n /** Whether the host knows a type (ADR-0022); without it only built-in kinds connect. */\n resolveType?: (type: ComponentType) => boolean;\n createId: () => string;\n}\n\nexport type AttachResult =\n | { ok: true; scene: ScadaScene; connection: SceneConnection; /** How far the attached port travelled, metres. */ moved: number }\n | { ok: false; error: { code: 'UNKNOWN_INSTANCE' | 'SAME_COMPONENT' | 'NO_MATCHING_PORTS' | 'INVALID'; message: string } };\n\n/**\n * Attaches `movingId` to `fixedId`: picks the closest pair of free, compatible ports (moving part's\n * port to the fixed part's port), connects them and seats the moving part (with its assembly,\n * unless both share one) so the ports meet exactly and face each other. The input is not changed.\n */\nexport function attachComponents(scene: ScadaScene, fixedId: string, movingId: string, options: AttachOptions): AttachResult {\n const byId = new Map(scene.components.map((c) => [c.id, c]));\n const fixed = byId.get(fixedId), moving = byId.get(movingId);\n if (!fixed || !moving) return { ok: false, error: { code: 'UNKNOWN_INSTANCE', message: `No component \"${!fixed ? fixedId : movingId}\".` } };\n if (fixedId === movingId) return { ok: false, error: { code: 'SAME_COMPONENT', message: 'A part cannot be attached to itself.' } };\n const used = new Map<string, number>();\n for (const e of scene.connections.flatMap((c) => [c.from, c.to])) used.set(`${e.instanceId}\\u0000${e.portId}`, (used.get(`${e.instanceId}\\u0000${e.portId}`) ?? 0) + 1);\n const free = (c: ComponentInstance) => (options.resolvePorts(c.type, c.properties) ?? []).filter((p) => matesPhysically(p) && (used.get(`${c.id}\\u0000${p.id}`) ?? 0) < portCapacity(p)).map((p) => ({ port: p, at: portToWorld(c, p).position }));\n const pairs = free(moving).flatMap((m) => free(fixed).filter((f) => portsCompatible(m.port, f.port)).map((f) => ({ m, f, distance: Math.hypot(...m.at.map((n, i) => n - f.at[i])) })));\n if (pairs.length === 0) return { ok: false, error: { code: 'NO_MATCHING_PORTS', message: 'These parts have no free ports of the same type and size.' } };\n pairs.sort((a, b) => a.distance - b.distance || a.m.port.id.localeCompare(b.m.port.id) || a.f.port.id.localeCompare(b.f.port.id));\n const best = pairs[0];\n const connection: SceneConnection = { id: options.createId(), from: { instanceId: movingId, portId: best.m.port.id }, to: { instanceId: fixedId, portId: best.f.port.id } };\n const joined = connect(scene, connection, options);\n if (!joined.ok) return { ok: false, error: { code: 'INVALID', message: joined.error.message } };\n const seated = seatOnConnection(joined.scene, connection.id, movingId, options);\n if (!seated.ok) return { ok: false, error: { code: 'INVALID', message: seated.error.message } };\n return { ok: true, scene: seated.scene, connection, moved: seated.moved };\n}\n"]}
@@ -1,18 +1,20 @@
1
- import type { ComponentInstance, ComponentKind, ScadaScene, SceneConnection, Vector3Like, Vector3Tuple } from './scene.js';
2
- /** A snapshot of copied components and the connections between them. */
1
+ import type { ComponentInstance, ComponentType, ScadaScene, SceneAssociation, SceneConnection, Vector3Like, Vector3Tuple } from './scene.js';
2
+ /** A snapshot of copied components and the connections and associations between them. */
3
3
  export interface ComponentClipboard {
4
4
  components: ComponentInstance[];
5
5
  connections: SceneConnection[];
6
+ /** Associations whose both ends are copied (ADR-0023); absent in clipboards made before them. */
7
+ associations?: SceneAssociation[];
6
8
  }
7
9
  /**
8
- * Copies components `ids` (in scene order) with the connections whose both ends are copied. A
10
+ * Copies components `ids` (in scene order) with the connections and associations whose both ends are copied. A
9
11
  * parent link is kept only if the parent is copied too. Tag bindings are not copied: a copy must
10
12
  * not show another part's live value. The result does not share objects with the scene.
11
13
  */
12
14
  export declare function copyComponents(scene: ScadaScene, ids: readonly string[]): ComponentClipboard;
13
15
  /** World X extent of the copied parts' boxes, used to paste beside the original without overlap. */
14
16
  export declare function clipboardWidth(clipboard: ComponentClipboard, options: {
15
- resolveBounds: (type: ComponentKind, properties?: ComponentInstance['properties']) => {
17
+ resolveBounds: (type: ComponentType, properties?: ComponentInstance['properties']) => {
16
18
  min: Vector3Tuple;
17
19
  max: Vector3Tuple;
18
20
  } | undefined;
@@ -1 +1 @@
1
- {"version":3,"file":"clipboard.d.ts","sourceRoot":"","sources":["../src/clipboard.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,aAAa,EAAE,UAAU,EAAE,eAAe,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAG3H,wEAAwE;AACxE,MAAM,WAAW,kBAAkB;IACjC,UAAU,EAAE,iBAAiB,EAAE,CAAC;IAChC,WAAW,EAAE,eAAe,EAAE,CAAC;CAChC;AAUD;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,UAAU,EAAE,GAAG,EAAE,SAAS,MAAM,EAAE,GAAG,kBAAkB,CAa5F;AAED,oGAAoG;AACpG,wBAAgB,cAAc,CAC5B,SAAS,EAAE,kBAAkB,EAC7B,OAAO,EAAE;IAAE,aAAa,EAAE,CAAC,IAAI,EAAE,aAAa,EAAE,UAAU,CAAC,EAAE,iBAAiB,CAAC,YAAY,CAAC,KAAK;QAAE,GAAG,EAAE,YAAY,CAAC;QAAC,GAAG,EAAE,YAAY,CAAA;KAAE,GAAG,SAAS,CAAA;CAAE,GACtJ,MAAM,CAoBR;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,UAAU,EACjB,SAAS,EAAE,kBAAkB,EAC7B,OAAO,EAAE;IAAE,QAAQ,EAAE,MAAM,MAAM,CAAC;IAAC,MAAM,EAAE,WAAW,CAAA;CAAE,GACvD;IAAE,KAAK,EAAE,UAAU,CAAC;IAAC,SAAS,EAAE,MAAM,EAAE,CAAA;CAAE,CAkB5C"}
1
+ {"version":3,"file":"clipboard.d.ts","sourceRoot":"","sources":["../src/clipboard.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,aAAa,EAAE,UAAU,EAAE,gBAAgB,EAAE,eAAe,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAG7I,yFAAyF;AACzF,MAAM,WAAW,kBAAkB;IACjC,UAAU,EAAE,iBAAiB,EAAE,CAAC;IAChC,WAAW,EAAE,eAAe,EAAE,CAAC;IAC/B,iGAAiG;IACjG,YAAY,CAAC,EAAE,gBAAgB,EAAE,CAAC;CACnC;AAaD;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,UAAU,EAAE,GAAG,EAAE,SAAS,MAAM,EAAE,GAAG,kBAAkB,CAgB5F;AAED,oGAAoG;AACpG,wBAAgB,cAAc,CAC5B,SAAS,EAAE,kBAAkB,EAC7B,OAAO,EAAE;IAAE,aAAa,EAAE,CAAC,IAAI,EAAE,aAAa,EAAE,UAAU,CAAC,EAAE,iBAAiB,CAAC,YAAY,CAAC,KAAK;QAAE,GAAG,EAAE,YAAY,CAAC;QAAC,GAAG,EAAE,YAAY,CAAA;KAAE,GAAG,SAAS,CAAA;CAAE,GACtJ,MAAM,CAoBR;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,UAAU,EACjB,SAAS,EAAE,kBAAkB,EAC7B,OAAO,EAAE;IAAE,QAAQ,EAAE,MAAM,MAAM,CAAC;IAAC,MAAM,EAAE,WAAW,CAAA;CAAE,GACvD;IAAE,KAAK,EAAE,UAAU,CAAC;IAAC,SAAS,EAAE,MAAM,EAAE,CAAA;CAAE,CAiC5C"}
package/dist/clipboard.js CHANGED
@@ -6,9 +6,12 @@ const cloneComponent = (c) => ({
6
6
  rotation: { ...c.rotation },
7
7
  scale: { ...c.scale },
8
8
  properties: { ...c.properties },
9
+ // Deep copies, so a pasted component never shares nested data with the original (T-096).
10
+ ...(c.labels ? { labels: { ...c.labels } } : {}),
11
+ ...(c.extensions ? { extensions: structuredClone(c.extensions) } : {}),
9
12
  });
10
13
  /**
11
- * Copies components `ids` (in scene order) with the connections whose both ends are copied. A
14
+ * Copies components `ids` (in scene order) with the connections and associations whose both ends are copied. A
12
15
  * parent link is kept only if the parent is copied too. Tag bindings are not copied: a copy must
13
16
  * not show another part's live value. The result does not share objects with the scene.
14
17
  */
@@ -24,8 +27,11 @@ export function copyComponents(scene, ids) {
24
27
  });
25
28
  const connections = scene.connections
26
29
  .filter((c) => set.has(c.from.instanceId) && set.has(c.to.instanceId))
27
- .map((c) => ({ id: c.id, from: { ...c.from }, to: { ...c.to } }));
28
- return { components, connections };
30
+ .map((c) => ({ ...c, from: { ...c.from }, to: { ...c.to }, ...(c.properties ? { properties: { ...c.properties } } : {}) }));
31
+ const associations = (scene.associations ?? [])
32
+ .filter((a) => set.has(a.from) && set.has(a.to))
33
+ .map((a) => ({ ...a, ...(a.properties ? { properties: { ...a.properties } } : {}) }));
34
+ return { components, connections, ...(associations.length > 0 ? { associations } : {}) };
29
35
  }
30
36
  /** World X extent of the copied parts' boxes, used to paste beside the original without overlap. */
31
37
  export function clipboardWidth(clipboard, options) {
@@ -64,12 +70,27 @@ export function pasteComponents(scene, clipboard, options) {
64
70
  return copy;
65
71
  });
66
72
  const connections = clipboard.connections.map((c) => ({
73
+ ...c,
74
+ ...(c.properties ? { properties: { ...c.properties } } : {}),
67
75
  id: options.createId(),
68
76
  from: { instanceId: ids.get(c.from.instanceId), portId: c.from.portId },
69
77
  to: { instanceId: ids.get(c.to.instanceId), portId: c.to.portId },
70
78
  }));
79
+ const associations = (clipboard.associations ?? []).map((a) => ({
80
+ ...a,
81
+ ...(a.properties ? { properties: { ...a.properties } } : {}),
82
+ id: options.createId(),
83
+ from: ids.get(a.from),
84
+ to: ids.get(a.to),
85
+ }));
86
+ const allAssociations = [...(scene.associations ?? []), ...associations];
71
87
  return {
72
- scene: { ...scene, components: [...scene.components, ...components], connections: [...scene.connections, ...connections] },
88
+ scene: {
89
+ ...scene,
90
+ components: [...scene.components, ...components],
91
+ connections: [...scene.connections, ...connections],
92
+ ...(allAssociations.length > 0 ? { associations: allAssociations } : {}),
93
+ },
73
94
  pastedIds: components.map((c) => c.id),
74
95
  };
75
96
  }
@@ -1 +1 @@
1
- {"version":3,"file":"clipboard.js","sourceRoot":"","sources":["../src/clipboard.ts"],"names":[],"mappings":"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","sourcesContent":["import { rotateVector } from './quaternion.js';\nimport type { ComponentInstance, ComponentKind, ScadaScene, SceneConnection, Vector3Like, Vector3Tuple } from './scene.js';\nimport { quaternionToTuple } from './vectors.js';\n\n/** A snapshot of copied components and the connections between them. */\nexport interface ComponentClipboard {\n components: ComponentInstance[];\n connections: SceneConnection[];\n}\n\nconst cloneComponent = (c: ComponentInstance): ComponentInstance => ({\n ...c,\n position: { ...c.position },\n rotation: { ...c.rotation },\n scale: { ...c.scale },\n properties: { ...c.properties },\n});\n\n/**\n * Copies components `ids` (in scene order) with the connections whose both ends are copied. A\n * parent link is kept only if the parent is copied too. Tag bindings are not copied: a copy must\n * not show another part's live value. The result does not share objects with the scene.\n */\nexport function copyComponents(scene: ScadaScene, ids: readonly string[]): ComponentClipboard {\n const set = new Set(ids);\n const components = scene.components\n .filter((c) => set.has(c.id))\n .map((c) => {\n const copy = cloneComponent(c);\n if (copy.parentId !== undefined && !set.has(copy.parentId)) delete copy.parentId;\n return copy;\n });\n const connections = scene.connections\n .filter((c) => set.has(c.from.instanceId) && set.has(c.to.instanceId))\n .map((c) => ({ id: c.id, from: { ...c.from }, to: { ...c.to } }));\n return { components, connections };\n}\n\n/** World X extent of the copied parts' boxes, used to paste beside the original without overlap. */\nexport function clipboardWidth(\n clipboard: ComponentClipboard,\n options: { resolveBounds: (type: ComponentKind, properties?: ComponentInstance['properties']) => { min: Vector3Tuple; max: Vector3Tuple } | undefined },\n): number {\n let min = Infinity;\n let max = -Infinity;\n for (const c of clipboard.components) {\n const b = options.resolveBounds(c.type, c.properties);\n if (!b) {\n min = Math.min(min, c.position.x);\n max = Math.max(max, c.position.x);\n continue;\n }\n const q = quaternionToTuple(c.rotation);\n for (const x of [b.min[0], b.max[0]])\n for (const y of [b.min[1], b.max[1]])\n for (const z of [b.min[2], b.max[2]]) {\n const corner = rotateVector([x * c.scale.x, y * c.scale.y, z * c.scale.z], q);\n min = Math.min(min, c.position.x + corner[0]);\n max = Math.max(max, c.position.x + corner[0]);\n }\n }\n return Number.isFinite(min) ? max - min : 0;\n}\n\n/**\n * Adds the clipboard's components to the scene with new ids, shifted by `offset`, with their\n * internal connections and parent links remapped to the new ids. Returns the new component ids.\n */\nexport function pasteComponents(\n scene: ScadaScene,\n clipboard: ComponentClipboard,\n options: { createId: () => string; offset: Vector3Like },\n): { scene: ScadaScene; pastedIds: string[] } {\n const ids = new Map(clipboard.components.map((c) => [c.id, options.createId()]));\n const components = clipboard.components.map((c) => {\n const copy = cloneComponent(c);\n copy.id = ids.get(c.id)!;\n if (c.parentId !== undefined) copy.parentId = ids.get(c.parentId);\n copy.position = { x: c.position.x + options.offset.x, y: c.position.y + options.offset.y, z: c.position.z + options.offset.z };\n return copy;\n });\n const connections = clipboard.connections.map((c) => ({\n id: options.createId(),\n from: { instanceId: ids.get(c.from.instanceId)!, portId: c.from.portId },\n to: { instanceId: ids.get(c.to.instanceId)!, portId: c.to.portId },\n }));\n return {\n scene: { ...scene, components: [...scene.components, ...components], connections: [...scene.connections, ...connections] },\n pastedIds: components.map((c) => c.id),\n };\n}\n"]}
1
+ {"version":3,"file":"clipboard.js","sourceRoot":"","sources":["../src/clipboard.ts"],"names":[],"mappings":"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","sourcesContent":["import { rotateVector } from './quaternion.js';\nimport type { ComponentInstance, ComponentType, ScadaScene, SceneAssociation, SceneConnection, Vector3Like, Vector3Tuple } from './scene.js';\nimport { quaternionToTuple } from './vectors.js';\n\n/** A snapshot of copied components and the connections and associations between them. */\nexport interface ComponentClipboard {\n components: ComponentInstance[];\n connections: SceneConnection[];\n /** Associations whose both ends are copied (ADR-0023); absent in clipboards made before them. */\n associations?: SceneAssociation[];\n}\n\nconst cloneComponent = (c: ComponentInstance): ComponentInstance => ({\n ...c,\n position: { ...c.position },\n rotation: { ...c.rotation },\n scale: { ...c.scale },\n properties: { ...c.properties },\n // Deep copies, so a pasted component never shares nested data with the original (T-096).\n ...(c.labels ? { labels: { ...c.labels } } : {}),\n ...(c.extensions ? { extensions: structuredClone(c.extensions) } : {}),\n});\n\n/**\n * Copies components `ids` (in scene order) with the connections and associations whose both ends are copied. A\n * parent link is kept only if the parent is copied too. Tag bindings are not copied: a copy must\n * not show another part's live value. The result does not share objects with the scene.\n */\nexport function copyComponents(scene: ScadaScene, ids: readonly string[]): ComponentClipboard {\n const set = new Set(ids);\n const components = scene.components\n .filter((c) => set.has(c.id))\n .map((c) => {\n const copy = cloneComponent(c);\n if (copy.parentId !== undefined && !set.has(copy.parentId)) delete copy.parentId;\n return copy;\n });\n const connections = scene.connections\n .filter((c) => set.has(c.from.instanceId) && set.has(c.to.instanceId))\n .map((c) => ({ ...c, from: { ...c.from }, to: { ...c.to }, ...(c.properties ? { properties: { ...c.properties } } : {}) }));\n const associations = (scene.associations ?? [])\n .filter((a) => set.has(a.from) && set.has(a.to))\n .map((a) => ({ ...a, ...(a.properties ? { properties: { ...a.properties } } : {}) }));\n return { components, connections, ...(associations.length > 0 ? { associations } : {}) };\n}\n\n/** World X extent of the copied parts' boxes, used to paste beside the original without overlap. */\nexport function clipboardWidth(\n clipboard: ComponentClipboard,\n options: { resolveBounds: (type: ComponentType, properties?: ComponentInstance['properties']) => { min: Vector3Tuple; max: Vector3Tuple } | undefined },\n): number {\n let min = Infinity;\n let max = -Infinity;\n for (const c of clipboard.components) {\n const b = options.resolveBounds(c.type, c.properties);\n if (!b) {\n min = Math.min(min, c.position.x);\n max = Math.max(max, c.position.x);\n continue;\n }\n const q = quaternionToTuple(c.rotation);\n for (const x of [b.min[0], b.max[0]])\n for (const y of [b.min[1], b.max[1]])\n for (const z of [b.min[2], b.max[2]]) {\n const corner = rotateVector([x * c.scale.x, y * c.scale.y, z * c.scale.z], q);\n min = Math.min(min, c.position.x + corner[0]);\n max = Math.max(max, c.position.x + corner[0]);\n }\n }\n return Number.isFinite(min) ? max - min : 0;\n}\n\n/**\n * Adds the clipboard's components to the scene with new ids, shifted by `offset`, with their\n * internal connections and parent links remapped to the new ids. Returns the new component ids.\n */\nexport function pasteComponents(\n scene: ScadaScene,\n clipboard: ComponentClipboard,\n options: { createId: () => string; offset: Vector3Like },\n): { scene: ScadaScene; pastedIds: string[] } {\n const ids = new Map(clipboard.components.map((c) => [c.id, options.createId()]));\n const components = clipboard.components.map((c) => {\n const copy = cloneComponent(c);\n copy.id = ids.get(c.id)!;\n if (c.parentId !== undefined) copy.parentId = ids.get(c.parentId);\n copy.position = { x: c.position.x + options.offset.x, y: c.position.y + options.offset.y, z: c.position.z + options.offset.z };\n return copy;\n });\n const connections = clipboard.connections.map((c) => ({\n ...c,\n ...(c.properties ? { properties: { ...c.properties } } : {}),\n id: options.createId(),\n from: { instanceId: ids.get(c.from.instanceId)!, portId: c.from.portId },\n to: { instanceId: ids.get(c.to.instanceId)!, portId: c.to.portId },\n }));\n const associations = (clipboard.associations ?? []).map((a) => ({\n ...a,\n ...(a.properties ? { properties: { ...a.properties } } : {}),\n id: options.createId(),\n from: ids.get(a.from)!,\n to: ids.get(a.to)!,\n }));\n const allAssociations = [...(scene.associations ?? []), ...associations];\n return {\n scene: {\n ...scene,\n components: [...scene.components, ...components],\n connections: [...scene.connections, ...connections],\n ...(allAssociations.length > 0 ? { associations: allAssociations } : {}),\n },\n pastedIds: components.map((c) => c.id),\n };\n}\n"]}
@@ -1,4 +1,4 @@
1
- import type { ComponentInstance, ComponentKind, ScadaScene, Vector3Tuple } from './scene.js';
1
+ import type { ComponentInstance, ComponentType, ScadaScene, Vector3Tuple } from './scene.js';
2
2
  /**
3
3
  * Solid-body overlap between components. Each component is treated as its catalog bounding box,
4
4
  * placed by its position, rotation and scale (an oriented box). This is conservative: hollow or
@@ -6,7 +6,7 @@ import type { ComponentInstance, ComponentKind, ScadaScene, Vector3Tuple } from
6
6
  */
7
7
  export interface CollisionOptions {
8
8
  /** Local bounds of a component type; types without bounds are skipped. */
9
- resolveBounds: (type: ComponentKind, properties?: ComponentInstance['properties']) => {
9
+ resolveBounds: (type: ComponentType, properties?: ComponentInstance['properties']) => {
10
10
  min: Vector3Tuple;
11
11
  max: Vector3Tuple;
12
12
  } | undefined;
@@ -1 +1 @@
1
- {"version":3,"file":"collision.js","sourceRoot":"","sources":["../src/collision.ts"],"names":[],"mappings":"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","sourcesContent":["import { rotateVector } from './quaternion.js';\nimport type { ComponentInstance, ComponentKind, ScadaScene, Vector3Tuple } from './scene.js';\nimport { quaternionToTuple } from './vectors.js';\n\n/**\n * Solid-body overlap between components. Each component is treated as its catalog bounding box,\n * placed by its position, rotation and scale (an oriented box). This is conservative: hollow or\n * curved parts (an elbow's inside corner, a tank's rounded side) count as their full box.\n */\nexport interface CollisionOptions {\n /** Local bounds of a component type; types without bounds are skipped. */\n resolveBounds: (type: ComponentKind, properties?: ComponentInstance['properties']) => { min: Vector3Tuple; max: Vector3Tuple } | undefined;\n /** Overlap up to this depth (metres) is treated as touching, so joined ports never collide. Default 0.001. */\n tolerance?: number;\n}\n\nexport interface CollisionPair {\n /** Lower of the two component ids (string order). */\n a: string;\n b: string;\n}\n\nexport interface CollisionResult {\n /** Each colliding pair once, sorted by `a` then `b`. */\n pairs: CollisionPair[];\n collidingIds: Set<string>;\n}\n\ninterface Box {\n id: string;\n centre: Vector3Tuple;\n /** Unit axes of the box in world space. */\n axes: [Vector3Tuple, Vector3Tuple, Vector3Tuple];\n half: Vector3Tuple;\n /** World-aligned extent, for the broad phase. */\n min: Vector3Tuple;\n max: Vector3Tuple;\n}\n\nconst dot = (a: Vector3Tuple, b: Vector3Tuple) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2];\nconst cross = (a: Vector3Tuple, b: Vector3Tuple): Vector3Tuple => [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]];\n\nfunction orientedBox(component: ScadaScene['components'][number], local: { min: Vector3Tuple; max: Vector3Tuple }): Box {\n const q = quaternionToTuple(component.rotation);\n const s = [component.scale.x, component.scale.y, component.scale.z];\n const localCentre = [0, 1, 2].map((i) => ((local.min[i] + local.max[i]) / 2) * s[i]) as Vector3Tuple;\n const offset = rotateVector(localCentre, q);\n const centre: Vector3Tuple = [component.position.x + offset[0], component.position.y + offset[1], component.position.z + offset[2]];\n const axes: Box['axes'] = [rotateVector([1, 0, 0], q), rotateVector([0, 1, 0], q), rotateVector([0, 0, 1], q)];\n const half = [0, 1, 2].map((i) => ((local.max[i] - local.min[i]) / 2) * s[i]) as Vector3Tuple;\n const reach = [0, 1, 2].map((w) => Math.abs(axes[0][w]) * half[0] + Math.abs(axes[1][w]) * half[1] + Math.abs(axes[2][w]) * half[2]);\n return {\n id: component.id,\n centre,\n axes,\n half,\n min: [centre[0] - reach[0], centre[1] - reach[1], centre[2] - reach[2]],\n max: [centre[0] + reach[0], centre[1] + reach[1], centre[2] + reach[2]],\n };\n}\n\n/** Separating-axis test: the boxes collide only if they overlap by more than `tolerance` on all 15 axes. */\nfunction overlaps(a: Box, b: Box, tolerance: number): boolean {\n const between: Vector3Tuple = [b.centre[0] - a.centre[0], b.centre[1] - a.centre[1], b.centre[2] - a.centre[2]];\n const candidates: Vector3Tuple[] = [...a.axes, ...b.axes];\n for (const u of a.axes) for (const v of b.axes) candidates.push(cross(u, v));\n for (const axis of candidates) {\n const length = Math.hypot(...axis);\n if (length < 1e-9) continue; // parallel edges: already covered by the face axes\n const n: Vector3Tuple = [axis[0] / length, axis[1] / length, axis[2] / length];\n const ra = a.half[0] * Math.abs(dot(a.axes[0], n)) + a.half[1] * Math.abs(dot(a.axes[1], n)) + a.half[2] * Math.abs(dot(a.axes[2], n));\n const rb = b.half[0] * Math.abs(dot(b.axes[0], n)) + b.half[1] * Math.abs(dot(b.axes[1], n)) + b.half[2] * Math.abs(dot(b.axes[2], n));\n if (ra + rb - Math.abs(dot(between, n)) <= tolerance) return false;\n }\n return true;\n}\n\n/**\n * Pairs of components whose solid boxes overlap by more than the tolerance. Display information\n * only: nothing is blocked or saved. A sweep along X skips pairs whose world boxes are disjoint.\n */\nexport function findCollisions(scene: ScadaScene, options: CollisionOptions): CollisionResult {\n const tolerance = options.tolerance ?? 0.001;\n const boxes: Box[] = [];\n for (const c of scene.components) {\n const local = options.resolveBounds(c.type, c.properties);\n if (local) boxes.push(orientedBox(c, local));\n }\n boxes.sort((p, q) => p.min[0] - q.min[0]);\n\n const pairs: CollisionPair[] = [];\n const active: Box[] = [];\n for (const box of boxes) {\n // Drop boxes that end before this one starts along X.\n for (let i = active.length - 1; i >= 0; i--) if (active[i].max[0] <= box.min[0]) active.splice(i, 1);\n for (const other of active) {\n if (other.max[1] <= box.min[1] || box.max[1] <= other.min[1] || other.max[2] <= box.min[2] || box.max[2] <= other.min[2]) continue;\n if (overlaps(other, box, tolerance)) pairs.push(other.id < box.id ? { a: other.id, b: box.id } : { a: box.id, b: other.id });\n }\n active.push(box);\n }\n pairs.sort((p, q) => (p.a < q.a ? -1 : p.a > q.a ? 1 : p.b < q.b ? -1 : p.b > q.b ? 1 : 0));\n return { pairs, collidingIds: new Set(pairs.flatMap((p) => [p.a, p.b])) };\n}\n"]}
1
+ {"version":3,"file":"collision.js","sourceRoot":"","sources":["../src/collision.ts"],"names":[],"mappings":"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","sourcesContent":["import { rotateVector } from './quaternion.js';\nimport type { ComponentInstance, ComponentType, ScadaScene, Vector3Tuple } from './scene.js';\nimport { quaternionToTuple } from './vectors.js';\n\n/**\n * Solid-body overlap between components. Each component is treated as its catalog bounding box,\n * placed by its position, rotation and scale (an oriented box). This is conservative: hollow or\n * curved parts (an elbow's inside corner, a tank's rounded side) count as their full box.\n */\nexport interface CollisionOptions {\n /** Local bounds of a component type; types without bounds are skipped. */\n resolveBounds: (type: ComponentType, properties?: ComponentInstance['properties']) => { min: Vector3Tuple; max: Vector3Tuple } | undefined;\n /** Overlap up to this depth (metres) is treated as touching, so joined ports never collide. Default 0.001. */\n tolerance?: number;\n}\n\nexport interface CollisionPair {\n /** Lower of the two component ids (string order). */\n a: string;\n b: string;\n}\n\nexport interface CollisionResult {\n /** Each colliding pair once, sorted by `a` then `b`. */\n pairs: CollisionPair[];\n collidingIds: Set<string>;\n}\n\ninterface Box {\n id: string;\n centre: Vector3Tuple;\n /** Unit axes of the box in world space. */\n axes: [Vector3Tuple, Vector3Tuple, Vector3Tuple];\n half: Vector3Tuple;\n /** World-aligned extent, for the broad phase. */\n min: Vector3Tuple;\n max: Vector3Tuple;\n}\n\nconst dot = (a: Vector3Tuple, b: Vector3Tuple) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2];\nconst cross = (a: Vector3Tuple, b: Vector3Tuple): Vector3Tuple => [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]];\n\nfunction orientedBox(component: ScadaScene['components'][number], local: { min: Vector3Tuple; max: Vector3Tuple }): Box {\n const q = quaternionToTuple(component.rotation);\n const s = [component.scale.x, component.scale.y, component.scale.z];\n const localCentre = [0, 1, 2].map((i) => ((local.min[i] + local.max[i]) / 2) * s[i]) as Vector3Tuple;\n const offset = rotateVector(localCentre, q);\n const centre: Vector3Tuple = [component.position.x + offset[0], component.position.y + offset[1], component.position.z + offset[2]];\n const axes: Box['axes'] = [rotateVector([1, 0, 0], q), rotateVector([0, 1, 0], q), rotateVector([0, 0, 1], q)];\n const half = [0, 1, 2].map((i) => ((local.max[i] - local.min[i]) / 2) * s[i]) as Vector3Tuple;\n const reach = [0, 1, 2].map((w) => Math.abs(axes[0][w]) * half[0] + Math.abs(axes[1][w]) * half[1] + Math.abs(axes[2][w]) * half[2]);\n return {\n id: component.id,\n centre,\n axes,\n half,\n min: [centre[0] - reach[0], centre[1] - reach[1], centre[2] - reach[2]],\n max: [centre[0] + reach[0], centre[1] + reach[1], centre[2] + reach[2]],\n };\n}\n\n/** Separating-axis test: the boxes collide only if they overlap by more than `tolerance` on all 15 axes. */\nfunction overlaps(a: Box, b: Box, tolerance: number): boolean {\n const between: Vector3Tuple = [b.centre[0] - a.centre[0], b.centre[1] - a.centre[1], b.centre[2] - a.centre[2]];\n const candidates: Vector3Tuple[] = [...a.axes, ...b.axes];\n for (const u of a.axes) for (const v of b.axes) candidates.push(cross(u, v));\n for (const axis of candidates) {\n const length = Math.hypot(...axis);\n if (length < 1e-9) continue; // parallel edges: already covered by the face axes\n const n: Vector3Tuple = [axis[0] / length, axis[1] / length, axis[2] / length];\n const ra = a.half[0] * Math.abs(dot(a.axes[0], n)) + a.half[1] * Math.abs(dot(a.axes[1], n)) + a.half[2] * Math.abs(dot(a.axes[2], n));\n const rb = b.half[0] * Math.abs(dot(b.axes[0], n)) + b.half[1] * Math.abs(dot(b.axes[1], n)) + b.half[2] * Math.abs(dot(b.axes[2], n));\n if (ra + rb - Math.abs(dot(between, n)) <= tolerance) return false;\n }\n return true;\n}\n\n/**\n * Pairs of components whose solid boxes overlap by more than the tolerance. Display information\n * only: nothing is blocked or saved. A sweep along X skips pairs whose world boxes are disjoint.\n */\nexport function findCollisions(scene: ScadaScene, options: CollisionOptions): CollisionResult {\n const tolerance = options.tolerance ?? 0.001;\n const boxes: Box[] = [];\n for (const c of scene.components) {\n const local = options.resolveBounds(c.type, c.properties);\n if (local) boxes.push(orientedBox(c, local));\n }\n boxes.sort((p, q) => p.min[0] - q.min[0]);\n\n const pairs: CollisionPair[] = [];\n const active: Box[] = [];\n for (const box of boxes) {\n // Drop boxes that end before this one starts along X.\n for (let i = active.length - 1; i >= 0; i--) if (active[i].max[0] <= box.min[0]) active.splice(i, 1);\n for (const other of active) {\n if (other.max[1] <= box.min[1] || box.max[1] <= other.min[1] || other.max[2] <= box.min[2] || box.max[2] <= other.min[2]) continue;\n if (overlaps(other, box, tolerance)) pairs.push(other.id < box.id ? { a: other.id, b: box.id } : { a: box.id, b: other.id });\n }\n active.push(box);\n }\n pairs.sort((p, q) => (p.a < q.a ? -1 : p.a > q.a ? 1 : p.b < q.b ? -1 : p.b > q.b ? 1 : 0));\n return { pairs, collidingIds: new Set(pairs.flatMap((p) => [p.a, p.b])) };\n}\n"]}
@@ -0,0 +1,9 @@
1
+ import type { ScadaScene } from './scene.js';
2
+ /** SHA-256 of a string's UTF-8 bytes, as 64 lower-case hex digits. */
3
+ export declare function sha256Hex(text: string): string;
4
+ /**
5
+ * The digest a host records when it approves a revision: SHA-256 of `serializeScene(scene)`, so
6
+ * whitespace and key order in the stored file do not matter. `ScadaRuntime` compares it before drawing.
7
+ */
8
+ export declare function sceneDigest(scene: ScadaScene): string;
9
+ //# sourceMappingURL=digest.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"digest.d.ts","sourceRoot":"","sources":["../src/digest.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAmB7C,sEAAsE;AACtE,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAqC9C;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,CAErD"}