@sakibcoolz/core 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +16 -2
- package/dist/assembly.d.ts +27 -0
- package/dist/assembly.d.ts.map +1 -0
- package/dist/assembly.js +83 -0
- package/dist/assembly.js.map +1 -0
- package/dist/attach.d.ts +24 -0
- package/dist/attach.d.ts.map +1 -0
- package/dist/attach.js +33 -0
- package/dist/attach.js.map +1 -0
- package/dist/clipboard.d.ts +31 -0
- package/dist/clipboard.d.ts.map +1 -0
- package/dist/clipboard.js +76 -0
- package/dist/clipboard.js.map +1 -0
- package/dist/collision.d.ts +31 -0
- package/dist/collision.d.ts.map +1 -0
- package/dist/collision.js +73 -0
- package/dist/collision.js.map +1 -0
- package/dist/dataInput.d.ts +37 -0
- package/dist/dataInput.d.ts.map +1 -0
- package/dist/dataInput.js +83 -0
- package/dist/dataInput.js.map +1 -0
- package/dist/flow.d.ts +60 -0
- package/dist/flow.d.ts.map +1 -0
- package/dist/flow.js +57 -0
- package/dist/flow.js.map +1 -0
- package/dist/graph.d.ts +32 -0
- package/dist/graph.d.ts.map +1 -0
- package/dist/graph.js +94 -0
- package/dist/graph.js.map +1 -0
- package/dist/index.d.ts +38 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +34 -0
- package/dist/index.js.map +1 -0
- package/dist/joints.d.ts +48 -0
- package/dist/joints.d.ts.map +1 -0
- package/dist/joints.js +89 -0
- package/dist/joints.js.map +1 -0
- package/dist/observation.d.ts +24 -0
- package/dist/observation.d.ts.map +1 -0
- package/dist/observation.js +30 -0
- package/dist/observation.js.map +1 -0
- package/dist/quaternion.d.ts +13 -0
- package/dist/quaternion.d.ts.map +1 -0
- package/dist/quaternion.js +71 -0
- package/dist/quaternion.js.map +1 -0
- package/dist/rotation.d.ts +9 -0
- package/dist/rotation.d.ts.map +1 -0
- package/dist/rotation.js +17 -0
- package/dist/rotation.js.map +1 -0
- package/dist/routing.d.ts +60 -0
- package/dist/routing.d.ts.map +1 -0
- package/dist/routing.js +177 -0
- package/dist/routing.js.map +1 -0
- package/dist/scene.d.ts +115 -0
- package/dist/scene.d.ts.map +1 -0
- package/dist/scene.js +9 -0
- package/dist/scene.js.map +1 -0
- package/dist/serialization.d.ts +16 -0
- package/dist/serialization.d.ts.map +1 -0
- package/dist/serialization.js +67 -0
- package/dist/serialization.js.map +1 -0
- package/dist/snapping.d.ts +43 -0
- package/dist/snapping.d.ts.map +1 -0
- package/dist/snapping.js +87 -0
- package/dist/snapping.js.map +1 -0
- package/dist/telemetry.d.ts +66 -0
- package/dist/telemetry.d.ts.map +1 -0
- package/dist/telemetry.js +102 -0
- package/dist/telemetry.js.map +1 -0
- package/dist/touching.d.ts +49 -0
- package/dist/touching.d.ts.map +1 -0
- package/dist/touching.js +81 -0
- package/dist/touching.js.map +1 -0
- package/dist/transforms.d.ts +16 -0
- package/dist/transforms.d.ts.map +1 -0
- package/dist/transforms.js +29 -0
- package/dist/transforms.js.map +1 -0
- package/dist/units.d.ts +8 -0
- package/dist/units.d.ts.map +1 -0
- package/dist/units.js +23 -0
- package/dist/units.js.map +1 -0
- package/dist/upgrade.d.ts +24 -0
- package/dist/upgrade.d.ts.map +1 -0
- package/dist/upgrade.js +88 -0
- package/dist/upgrade.js.map +1 -0
- package/dist/validation.d.ts +57 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +513 -0
- package/dist/validation.js.map +1 -0
- package/dist/vectors.d.ts +6 -0
- package/dist/vectors.d.ts.map +1 -0
- package/dist/vectors.js +6 -0
- package/dist/vectors.js.map +1 -0
- package/package.json +47 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 sakibcoolz
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @sakibcoolz/core
|
|
2
2
|
|
|
3
|
-
|
|
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.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
npm install @sakibcoolz/core
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { parseScene, validateScene } from '@sakibcoolz/core';
|
|
11
|
+
```
|
|
12
|
+
|
|
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+.
|
|
14
|
+
|
|
15
|
+
The SDK visualises and simulates; it does not connect to or control PLCs or physical equipment.
|
|
16
|
+
|
|
17
|
+
MIT licence.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { QuaternionLike, ScadaScene, Vector3Like } from './scene.js';
|
|
2
|
+
/** Ids of the whole assembly containing `id` (its root and every descendant), in scene order. */
|
|
3
|
+
export declare function assemblyOf(scene: ScadaScene, id: string): string[];
|
|
4
|
+
/**
|
|
5
|
+
* Gives member `id` a new pose and applies the same rigid motion to the rest of its assembly:
|
|
6
|
+
* every member keeps its offset and relative rotation, so joined ports stay joined. A component
|
|
7
|
+
* outside any assembly simply moves on its own.
|
|
8
|
+
*/
|
|
9
|
+
export declare function moveAssembly(scene: ScadaScene, id: string, position: Vector3Like, rotation: QuaternionLike): ScadaScene;
|
|
10
|
+
export type AssemblyResult = {
|
|
11
|
+
ok: true;
|
|
12
|
+
scene: ScadaScene;
|
|
13
|
+
} | {
|
|
14
|
+
ok: false;
|
|
15
|
+
error: {
|
|
16
|
+
code: 'UNKNOWN_INSTANCE' | 'NOTHING_CONNECTED';
|
|
17
|
+
message: string;
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* 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
|
+
*/
|
|
24
|
+
export declare function lockConnectedAsChildren(scene: ScadaScene, id: string): AssemblyResult;
|
|
25
|
+
/** Frees `id` from its parent, and frees `id`'s own children, so neither moves with the other any more. */
|
|
26
|
+
export declare function unlockFromParent(scene: ScadaScene, id: string): ScadaScene;
|
|
27
|
+
//# sourceMappingURL=assembly.d.ts.map
|
|
@@ -0,0 +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"}
|
package/dist/assembly.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { reachableComponents } from './graph.js';
|
|
2
|
+
import { multiplyQuaternions, normalizeQuaternion, rotateVector } from './quaternion.js';
|
|
3
|
+
import { quaternionToTuple, tupleToQuaternion } from './vectors.js';
|
|
4
|
+
// Assemblies (ADR-0008): children carry `parentId`; a root and all its descendants move rigidly.
|
|
5
|
+
function rootOf(scene, id) {
|
|
6
|
+
const parents = new Map(scene.components.map((c) => [c.id, c.parentId]));
|
|
7
|
+
let root = id;
|
|
8
|
+
for (let steps = 0; parents.get(root) !== undefined && steps <= scene.components.length; steps++)
|
|
9
|
+
root = parents.get(root);
|
|
10
|
+
return root;
|
|
11
|
+
}
|
|
12
|
+
/** Ids of the whole assembly containing `id` (its root and every descendant), in scene order. */
|
|
13
|
+
export function assemblyOf(scene, id) {
|
|
14
|
+
const root = rootOf(scene, id);
|
|
15
|
+
const members = new Set([root]);
|
|
16
|
+
// Parents can appear after their children in the list, so repeat until nothing is added.
|
|
17
|
+
for (let added = true; added;) {
|
|
18
|
+
added = false;
|
|
19
|
+
for (const c of scene.components) {
|
|
20
|
+
if (c.parentId !== undefined && members.has(c.parentId) && !members.has(c.id)) {
|
|
21
|
+
members.add(c.id);
|
|
22
|
+
added = true;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return scene.components.filter((c) => members.has(c.id)).map((c) => c.id);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Gives member `id` a new pose and applies the same rigid motion to the rest of its assembly:
|
|
30
|
+
* every member keeps its offset and relative rotation, so joined ports stay joined. A component
|
|
31
|
+
* outside any assembly simply moves on its own.
|
|
32
|
+
*/
|
|
33
|
+
export function moveAssembly(scene, id, position, rotation) {
|
|
34
|
+
const moving = scene.components.find((c) => c.id === id);
|
|
35
|
+
if (!moving)
|
|
36
|
+
return scene;
|
|
37
|
+
const members = new Set(assemblyOf(scene, id));
|
|
38
|
+
const [x, y, z, w] = quaternionToTuple(moving.rotation);
|
|
39
|
+
const delta = normalizeQuaternion(multiplyQuaternions(quaternionToTuple(rotation), [-x, -y, -z, w]));
|
|
40
|
+
const place = (c) => {
|
|
41
|
+
if (c.id === id)
|
|
42
|
+
return { ...c, position, rotation };
|
|
43
|
+
const offset = rotateVector([c.position.x - moving.position.x, c.position.y - moving.position.y, c.position.z - moving.position.z], delta);
|
|
44
|
+
return {
|
|
45
|
+
...c,
|
|
46
|
+
position: { x: position.x + offset[0], y: position.y + offset[1], z: position.z + offset[2] },
|
|
47
|
+
rotation: tupleToQuaternion(normalizeQuaternion(multiplyQuaternions(delta, quaternionToTuple(c.rotation)))),
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
return { ...scene, components: scene.components.map((c) => (members.has(c.id) ? place(c) : c)) };
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
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.
|
|
55
|
+
*/
|
|
56
|
+
export function lockConnectedAsChildren(scene, id) {
|
|
57
|
+
if (!scene.components.some((c) => c.id === id))
|
|
58
|
+
return { ok: false, error: { code: 'UNKNOWN_INSTANCE', message: `No component "${id}".` } };
|
|
59
|
+
const connected = new Set(reachableComponents(scene, id).filter((other) => other !== id));
|
|
60
|
+
if (connected.size === 0)
|
|
61
|
+
return { ok: false, error: { code: 'NOTHING_CONNECTED', message: 'Nothing is connected to this component.' } };
|
|
62
|
+
const components = scene.components.map((c) => {
|
|
63
|
+
if (c.id === id) {
|
|
64
|
+
const { parentId: _, ...root } = c;
|
|
65
|
+
return root;
|
|
66
|
+
}
|
|
67
|
+
return connected.has(c.id) ? { ...c, parentId: id } : c;
|
|
68
|
+
});
|
|
69
|
+
return { ok: true, scene: { ...scene, components } };
|
|
70
|
+
}
|
|
71
|
+
/** Frees `id` from its parent, and frees `id`'s own children, so neither moves with the other any more. */
|
|
72
|
+
export function unlockFromParent(scene, id) {
|
|
73
|
+
return {
|
|
74
|
+
...scene,
|
|
75
|
+
components: scene.components.map((c) => {
|
|
76
|
+
if (c.id !== id && c.parentId !== id)
|
|
77
|
+
return c;
|
|
78
|
+
const { parentId: _, ...free } = c;
|
|
79
|
+
return free;
|
|
80
|
+
}),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
//# sourceMappingURL=assembly.js.map
|
|
@@ -0,0 +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"]}
|
package/dist/attach.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type ComponentInstance, type ComponentKind, type ConnectionPort, type ScadaScene, type SceneConnection } from './scene.js';
|
|
2
|
+
export interface AttachOptions {
|
|
3
|
+
resolvePorts: (type: ComponentKind, properties?: ComponentInstance['properties']) => readonly ConnectionPort[] | undefined;
|
|
4
|
+
createId: () => string;
|
|
5
|
+
}
|
|
6
|
+
export type AttachResult = {
|
|
7
|
+
ok: true;
|
|
8
|
+
scene: ScadaScene;
|
|
9
|
+
connection: SceneConnection; /** How far the attached port travelled, metres. */
|
|
10
|
+
moved: number;
|
|
11
|
+
} | {
|
|
12
|
+
ok: false;
|
|
13
|
+
error: {
|
|
14
|
+
code: 'UNKNOWN_INSTANCE' | 'SAME_COMPONENT' | 'NO_MATCHING_PORTS' | 'INVALID';
|
|
15
|
+
message: string;
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Attaches `movingId` to `fixedId`: picks the closest pair of free, compatible ports (moving part's
|
|
20
|
+
* port to the fixed part's port), connects them and seats the moving part (with its assembly,
|
|
21
|
+
* unless both share one) so the ports meet exactly and face each other. The input is not changed.
|
|
22
|
+
*/
|
|
23
|
+
export declare function attachComponents(scene: ScadaScene, fixedId: string, movingId: string, options: AttachOptions): AttachResult;
|
|
24
|
+
//# sourceMappingURL=attach.d.ts.map
|
|
@@ -0,0 +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"}
|
package/dist/attach.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { connect } from './graph.js';
|
|
2
|
+
import { seatOnConnection } from './joints.js';
|
|
3
|
+
import { portsCompatible } from './scene.js';
|
|
4
|
+
import { portToWorld } from './transforms.js';
|
|
5
|
+
/**
|
|
6
|
+
* Attaches `movingId` to `fixedId`: picks the closest pair of free, compatible ports (moving part's
|
|
7
|
+
* port to the fixed part's port), connects them and seats the moving part (with its assembly,
|
|
8
|
+
* unless both share one) so the ports meet exactly and face each other. The input is not changed.
|
|
9
|
+
*/
|
|
10
|
+
export function attachComponents(scene, fixedId, movingId, options) {
|
|
11
|
+
const byId = new Map(scene.components.map((c) => [c.id, c]));
|
|
12
|
+
const fixed = byId.get(fixedId), moving = byId.get(movingId);
|
|
13
|
+
if (!fixed || !moving)
|
|
14
|
+
return { ok: false, error: { code: 'UNKNOWN_INSTANCE', message: `No component "${!fixed ? fixedId : movingId}".` } };
|
|
15
|
+
if (fixedId === movingId)
|
|
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 }));
|
|
19
|
+
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
|
+
if (pairs.length === 0)
|
|
21
|
+
return { ok: false, error: { code: 'NO_MATCHING_PORTS', message: 'These parts have no free ports of the same type and size.' } };
|
|
22
|
+
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));
|
|
23
|
+
const best = pairs[0];
|
|
24
|
+
const connection = { id: options.createId(), from: { instanceId: movingId, portId: best.m.port.id }, to: { instanceId: fixedId, portId: best.f.port.id } };
|
|
25
|
+
const joined = connect(scene, connection, options);
|
|
26
|
+
if (!joined.ok)
|
|
27
|
+
return { ok: false, error: { code: 'INVALID', message: joined.error.message } };
|
|
28
|
+
const seated = seatOnConnection(joined.scene, connection.id, movingId, options);
|
|
29
|
+
if (!seated.ok)
|
|
30
|
+
return { ok: false, error: { code: 'INVALID', message: seated.error.message } };
|
|
31
|
+
return { ok: true, scene: seated.scene, connection, moved: seated.moved };
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=attach.js.map
|
|
@@ -0,0 +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"]}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { ComponentInstance, ComponentKind, ScadaScene, SceneConnection, Vector3Like, Vector3Tuple } from './scene.js';
|
|
2
|
+
/** A snapshot of copied components and the connections between them. */
|
|
3
|
+
export interface ComponentClipboard {
|
|
4
|
+
components: ComponentInstance[];
|
|
5
|
+
connections: SceneConnection[];
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Copies components `ids` (in scene order) with the connections whose both ends are copied. A
|
|
9
|
+
* parent link is kept only if the parent is copied too. Tag bindings are not copied: a copy must
|
|
10
|
+
* not show another part's live value. The result does not share objects with the scene.
|
|
11
|
+
*/
|
|
12
|
+
export declare function copyComponents(scene: ScadaScene, ids: readonly string[]): ComponentClipboard;
|
|
13
|
+
/** World X extent of the copied parts' boxes, used to paste beside the original without overlap. */
|
|
14
|
+
export declare function clipboardWidth(clipboard: ComponentClipboard, options: {
|
|
15
|
+
resolveBounds: (type: ComponentKind, properties?: ComponentInstance['properties']) => {
|
|
16
|
+
min: Vector3Tuple;
|
|
17
|
+
max: Vector3Tuple;
|
|
18
|
+
} | undefined;
|
|
19
|
+
}): number;
|
|
20
|
+
/**
|
|
21
|
+
* Adds the clipboard's components to the scene with new ids, shifted by `offset`, with their
|
|
22
|
+
* internal connections and parent links remapped to the new ids. Returns the new component ids.
|
|
23
|
+
*/
|
|
24
|
+
export declare function pasteComponents(scene: ScadaScene, clipboard: ComponentClipboard, options: {
|
|
25
|
+
createId: () => string;
|
|
26
|
+
offset: Vector3Like;
|
|
27
|
+
}): {
|
|
28
|
+
scene: ScadaScene;
|
|
29
|
+
pastedIds: string[];
|
|
30
|
+
};
|
|
31
|
+
//# sourceMappingURL=clipboard.d.ts.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { rotateVector } from './quaternion.js';
|
|
2
|
+
import { quaternionToTuple } from './vectors.js';
|
|
3
|
+
const cloneComponent = (c) => ({
|
|
4
|
+
...c,
|
|
5
|
+
position: { ...c.position },
|
|
6
|
+
rotation: { ...c.rotation },
|
|
7
|
+
scale: { ...c.scale },
|
|
8
|
+
properties: { ...c.properties },
|
|
9
|
+
});
|
|
10
|
+
/**
|
|
11
|
+
* Copies components `ids` (in scene order) with the connections whose both ends are copied. A
|
|
12
|
+
* parent link is kept only if the parent is copied too. Tag bindings are not copied: a copy must
|
|
13
|
+
* not show another part's live value. The result does not share objects with the scene.
|
|
14
|
+
*/
|
|
15
|
+
export function copyComponents(scene, ids) {
|
|
16
|
+
const set = new Set(ids);
|
|
17
|
+
const components = scene.components
|
|
18
|
+
.filter((c) => set.has(c.id))
|
|
19
|
+
.map((c) => {
|
|
20
|
+
const copy = cloneComponent(c);
|
|
21
|
+
if (copy.parentId !== undefined && !set.has(copy.parentId))
|
|
22
|
+
delete copy.parentId;
|
|
23
|
+
return copy;
|
|
24
|
+
});
|
|
25
|
+
const connections = scene.connections
|
|
26
|
+
.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 };
|
|
29
|
+
}
|
|
30
|
+
/** World X extent of the copied parts' boxes, used to paste beside the original without overlap. */
|
|
31
|
+
export function clipboardWidth(clipboard, options) {
|
|
32
|
+
let min = Infinity;
|
|
33
|
+
let max = -Infinity;
|
|
34
|
+
for (const c of clipboard.components) {
|
|
35
|
+
const b = options.resolveBounds(c.type, c.properties);
|
|
36
|
+
if (!b) {
|
|
37
|
+
min = Math.min(min, c.position.x);
|
|
38
|
+
max = Math.max(max, c.position.x);
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
const q = quaternionToTuple(c.rotation);
|
|
42
|
+
for (const x of [b.min[0], b.max[0]])
|
|
43
|
+
for (const y of [b.min[1], b.max[1]])
|
|
44
|
+
for (const z of [b.min[2], b.max[2]]) {
|
|
45
|
+
const corner = rotateVector([x * c.scale.x, y * c.scale.y, z * c.scale.z], q);
|
|
46
|
+
min = Math.min(min, c.position.x + corner[0]);
|
|
47
|
+
max = Math.max(max, c.position.x + corner[0]);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return Number.isFinite(min) ? max - min : 0;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Adds the clipboard's components to the scene with new ids, shifted by `offset`, with their
|
|
54
|
+
* internal connections and parent links remapped to the new ids. Returns the new component ids.
|
|
55
|
+
*/
|
|
56
|
+
export function pasteComponents(scene, clipboard, options) {
|
|
57
|
+
const ids = new Map(clipboard.components.map((c) => [c.id, options.createId()]));
|
|
58
|
+
const components = clipboard.components.map((c) => {
|
|
59
|
+
const copy = cloneComponent(c);
|
|
60
|
+
copy.id = ids.get(c.id);
|
|
61
|
+
if (c.parentId !== undefined)
|
|
62
|
+
copy.parentId = ids.get(c.parentId);
|
|
63
|
+
copy.position = { x: c.position.x + options.offset.x, y: c.position.y + options.offset.y, z: c.position.z + options.offset.z };
|
|
64
|
+
return copy;
|
|
65
|
+
});
|
|
66
|
+
const connections = clipboard.connections.map((c) => ({
|
|
67
|
+
id: options.createId(),
|
|
68
|
+
from: { instanceId: ids.get(c.from.instanceId), portId: c.from.portId },
|
|
69
|
+
to: { instanceId: ids.get(c.to.instanceId), portId: c.to.portId },
|
|
70
|
+
}));
|
|
71
|
+
return {
|
|
72
|
+
scene: { ...scene, components: [...scene.components, ...components], connections: [...scene.connections, ...connections] },
|
|
73
|
+
pastedIds: components.map((c) => c.id),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=clipboard.js.map
|
|
@@ -0,0 +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"]}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { ComponentInstance, ComponentKind, ScadaScene, Vector3Tuple } from './scene.js';
|
|
2
|
+
/**
|
|
3
|
+
* Solid-body overlap between components. Each component is treated as its catalog bounding box,
|
|
4
|
+
* placed by its position, rotation and scale (an oriented box). This is conservative: hollow or
|
|
5
|
+
* curved parts (an elbow's inside corner, a tank's rounded side) count as their full box.
|
|
6
|
+
*/
|
|
7
|
+
export interface CollisionOptions {
|
|
8
|
+
/** Local bounds of a component type; types without bounds are skipped. */
|
|
9
|
+
resolveBounds: (type: ComponentKind, properties?: ComponentInstance['properties']) => {
|
|
10
|
+
min: Vector3Tuple;
|
|
11
|
+
max: Vector3Tuple;
|
|
12
|
+
} | undefined;
|
|
13
|
+
/** Overlap up to this depth (metres) is treated as touching, so joined ports never collide. Default 0.001. */
|
|
14
|
+
tolerance?: number;
|
|
15
|
+
}
|
|
16
|
+
export interface CollisionPair {
|
|
17
|
+
/** Lower of the two component ids (string order). */
|
|
18
|
+
a: string;
|
|
19
|
+
b: string;
|
|
20
|
+
}
|
|
21
|
+
export interface CollisionResult {
|
|
22
|
+
/** Each colliding pair once, sorted by `a` then `b`. */
|
|
23
|
+
pairs: CollisionPair[];
|
|
24
|
+
collidingIds: Set<string>;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Pairs of components whose solid boxes overlap by more than the tolerance. Display information
|
|
28
|
+
* only: nothing is blocked or saved. A sweep along X skips pairs whose world boxes are disjoint.
|
|
29
|
+
*/
|
|
30
|
+
export declare function findCollisions(scene: ScadaScene, options: CollisionOptions): CollisionResult;
|
|
31
|
+
//# sourceMappingURL=collision.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"collision.d.ts","sourceRoot":"","sources":["../src/collision.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,aAAa,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAG7F;;;;GAIG;AACH,MAAM,WAAW,gBAAgB;IAC/B,0EAA0E;IAC1E,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,CAAC;IAC3I,8GAA8G;IAC9G,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,aAAa;IAC5B,qDAAqD;IACrD,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;CACX;AAED,MAAM,WAAW,eAAe;IAC9B,wDAAwD;IACxD,KAAK,EAAE,aAAa,EAAE,CAAC;IACvB,YAAY,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;CAC3B;AAmDD;;;GAGG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,UAAU,EAAE,OAAO,EAAE,gBAAgB,GAAG,eAAe,CAsB5F"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { rotateVector } from './quaternion.js';
|
|
2
|
+
import { quaternionToTuple } from './vectors.js';
|
|
3
|
+
const dot = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
|
|
4
|
+
const cross = (a, b) => [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]];
|
|
5
|
+
function orientedBox(component, local) {
|
|
6
|
+
const q = quaternionToTuple(component.rotation);
|
|
7
|
+
const s = [component.scale.x, component.scale.y, component.scale.z];
|
|
8
|
+
const localCentre = [0, 1, 2].map((i) => ((local.min[i] + local.max[i]) / 2) * s[i]);
|
|
9
|
+
const offset = rotateVector(localCentre, q);
|
|
10
|
+
const centre = [component.position.x + offset[0], component.position.y + offset[1], component.position.z + offset[2]];
|
|
11
|
+
const axes = [rotateVector([1, 0, 0], q), rotateVector([0, 1, 0], q), rotateVector([0, 0, 1], q)];
|
|
12
|
+
const half = [0, 1, 2].map((i) => ((local.max[i] - local.min[i]) / 2) * s[i]);
|
|
13
|
+
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]);
|
|
14
|
+
return {
|
|
15
|
+
id: component.id,
|
|
16
|
+
centre,
|
|
17
|
+
axes,
|
|
18
|
+
half,
|
|
19
|
+
min: [centre[0] - reach[0], centre[1] - reach[1], centre[2] - reach[2]],
|
|
20
|
+
max: [centre[0] + reach[0], centre[1] + reach[1], centre[2] + reach[2]],
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
/** Separating-axis test: the boxes collide only if they overlap by more than `tolerance` on all 15 axes. */
|
|
24
|
+
function overlaps(a, b, tolerance) {
|
|
25
|
+
const between = [b.centre[0] - a.centre[0], b.centre[1] - a.centre[1], b.centre[2] - a.centre[2]];
|
|
26
|
+
const candidates = [...a.axes, ...b.axes];
|
|
27
|
+
for (const u of a.axes)
|
|
28
|
+
for (const v of b.axes)
|
|
29
|
+
candidates.push(cross(u, v));
|
|
30
|
+
for (const axis of candidates) {
|
|
31
|
+
const length = Math.hypot(...axis);
|
|
32
|
+
if (length < 1e-9)
|
|
33
|
+
continue; // parallel edges: already covered by the face axes
|
|
34
|
+
const n = [axis[0] / length, axis[1] / length, axis[2] / length];
|
|
35
|
+
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));
|
|
36
|
+
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));
|
|
37
|
+
if (ra + rb - Math.abs(dot(between, n)) <= tolerance)
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Pairs of components whose solid boxes overlap by more than the tolerance. Display information
|
|
44
|
+
* only: nothing is blocked or saved. A sweep along X skips pairs whose world boxes are disjoint.
|
|
45
|
+
*/
|
|
46
|
+
export function findCollisions(scene, options) {
|
|
47
|
+
const tolerance = options.tolerance ?? 0.001;
|
|
48
|
+
const boxes = [];
|
|
49
|
+
for (const c of scene.components) {
|
|
50
|
+
const local = options.resolveBounds(c.type, c.properties);
|
|
51
|
+
if (local)
|
|
52
|
+
boxes.push(orientedBox(c, local));
|
|
53
|
+
}
|
|
54
|
+
boxes.sort((p, q) => p.min[0] - q.min[0]);
|
|
55
|
+
const pairs = [];
|
|
56
|
+
const active = [];
|
|
57
|
+
for (const box of boxes) {
|
|
58
|
+
// Drop boxes that end before this one starts along X.
|
|
59
|
+
for (let i = active.length - 1; i >= 0; i--)
|
|
60
|
+
if (active[i].max[0] <= box.min[0])
|
|
61
|
+
active.splice(i, 1);
|
|
62
|
+
for (const other of active) {
|
|
63
|
+
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])
|
|
64
|
+
continue;
|
|
65
|
+
if (overlaps(other, box, tolerance))
|
|
66
|
+
pairs.push(other.id < box.id ? { a: other.id, b: box.id } : { a: box.id, b: other.id });
|
|
67
|
+
}
|
|
68
|
+
active.push(box);
|
|
69
|
+
}
|
|
70
|
+
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));
|
|
71
|
+
return { pairs, collidingIds: new Set(pairs.flatMap((p) => [p.a, p.b])) };
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=collision.js.map
|
|
@@ -0,0 +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"]}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { Observation } from './telemetry.js';
|
|
2
|
+
/**
|
|
3
|
+
* Optional host-implemented source of observations. The SDK never opens a connection itself:
|
|
4
|
+
* the host decides where values come from (REST, WebSocket, MQTT bridge, file, simulator) and
|
|
5
|
+
* hands them over through these callbacks.
|
|
6
|
+
*/
|
|
7
|
+
export interface DataProvider {
|
|
8
|
+
/** Current values, read once when the provider is connected. */
|
|
9
|
+
getSnapshot?(): readonly Observation[] | Promise<readonly Observation[]>;
|
|
10
|
+
/** Pushes later batches; returns the function that stops them. */
|
|
11
|
+
subscribe?(onObservations: (observations: readonly Observation[]) => void): () => void;
|
|
12
|
+
}
|
|
13
|
+
/** Latest observation per tag and source, with change notification. */
|
|
14
|
+
export interface ObservationStore {
|
|
15
|
+
/** Adds a batch; returns how many observations changed the store. */
|
|
16
|
+
set(observations: readonly Observation[]): number;
|
|
17
|
+
/** Current observations; the same array until something changes. */
|
|
18
|
+
get(): readonly Observation[];
|
|
19
|
+
subscribe(listener: () => void): () => void;
|
|
20
|
+
clear(): void;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Keeps only the newest observation per (tag, source), so memory is bounded by the number of tags
|
|
24
|
+
* (`maxTags`, default 50,000; further new tags are refused) and late, older values are ignored.
|
|
25
|
+
*/
|
|
26
|
+
export declare function createObservationStore(options?: {
|
|
27
|
+
maxTags?: number;
|
|
28
|
+
}): ObservationStore;
|
|
29
|
+
/**
|
|
30
|
+
* Feeds a store from a provider: the snapshot (sync or async) and every pushed batch. Returns a
|
|
31
|
+
* disconnect function that unsubscribes exactly once; anything arriving after it is dropped.
|
|
32
|
+
* Provider failures go to `onError` (default: console.error) rather than throwing into the viewer.
|
|
33
|
+
*/
|
|
34
|
+
export declare function connectProvider(store: ObservationStore, provider: DataProvider, options?: {
|
|
35
|
+
onError?: (error: unknown) => void;
|
|
36
|
+
}): () => void;
|
|
37
|
+
//# sourceMappingURL=dataInput.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dataInput.d.ts","sourceRoot":"","sources":["../src/dataInput.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAElD;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC3B,gEAAgE;IAChE,WAAW,CAAC,IAAI,SAAS,WAAW,EAAE,GAAG,OAAO,CAAC,SAAS,WAAW,EAAE,CAAC,CAAC;IACzE,kEAAkE;IAClE,SAAS,CAAC,CAAC,cAAc,EAAE,CAAC,YAAY,EAAE,SAAS,WAAW,EAAE,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;CACxF;AAED,uEAAuE;AACvE,MAAM,WAAW,gBAAgB;IAC/B,qEAAqE;IACrE,GAAG,CAAC,YAAY,EAAE,SAAS,WAAW,EAAE,GAAG,MAAM,CAAC;IAClD,oEAAoE;IACpE,GAAG,IAAI,SAAS,WAAW,EAAE,CAAC;IAC9B,SAAS,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC;IAC5C,KAAK,IAAI,IAAI,CAAC;CACf;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,GAAE;IAAE,OAAO,CAAC,EAAE,MAAM,CAAA;CAAO,GAAG,gBAAgB,CAmC3F;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,gBAAgB,EACvB,QAAQ,EAAE,YAAY,EACtB,OAAO,GAAE;IAAE,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAA;CAAO,GACnD,MAAM,IAAI,CAwBZ"}
|