Spaces:
Running
Running
Cleanup - module dependencies removed, Perspective merged, UNDER THE HOOD and QUERY fixed
Browse files- js/app.js +635 -681
- js/i18n.js +70 -103
- js/renderers/mermaid-adapter.js +42 -81
- js/renderers/vis-adapter.js +75 -170
- js/scenarios/scenario-vanishing-crack.js +41 -69
- js/smim/bfo.js +82 -57
- js/smim/iso15926.js +112 -91
- js/smim/naive.js +92 -116
- js/smrm/actions.js +72 -40
- js/smrm/definitions.js +50 -3
- js/sms/lpg-generator.js +185 -36
- js/sms/rdf-generator.js +198 -146
- js/sms/sms-graph-generator.js +205 -66
- js/state.js +78 -41
- js/urmsm/bom-view.js +119 -70
- js/urmsm/smrm-view.js +179 -39
- styles/main.css +585 -98
js/app.js
CHANGED
|
@@ -1,29 +1,35 @@
|
|
| 1 |
/**
|
| 2 |
* APPLICATION ORCHESTRATOR (Vue 3)
|
| 3 |
* --------------------------------
|
| 4 |
-
*
|
| 5 |
-
*
|
| 6 |
-
*
|
| 7 |
*/
|
| 8 |
|
| 9 |
import { createApp, computed, watch, onMounted, ref, reactive, nextTick } from 'vue';
|
| 10 |
-
import { state, advanceTime, loadState } from './state.js';
|
| 11 |
import { i18n } from './i18n.js';
|
| 12 |
-
import {
|
| 13 |
-
createRegisterPartEvent,
|
| 14 |
-
createAssembleEvent,
|
| 15 |
-
createReportDefectEvent,
|
| 16 |
-
|
|
|
|
| 17 |
} from './smrm/actions.js';
|
| 18 |
-
import { ENTITY_CLASSES, RELATION_TYPES } from './smrm/definitions.js';
|
| 19 |
import * as bfoAdapter from './smim/bfo.js';
|
| 20 |
import * as isoAdapter from './smim/iso15926.js';
|
| 21 |
import * as naiveAdapter from './smim/naive.js';
|
| 22 |
import { initVisNetwork, updateVisNetwork } from './renderers/vis-adapter.js';
|
| 23 |
import { updateMermaidNetwork } from './renderers/mermaid-adapter.js';
|
| 24 |
-
import {
|
| 25 |
import { reduceGraphForSMRM } from './urmsm/smrm-view.js';
|
| 26 |
-
import {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
import { generateRdfStorage, generateRdfQuery } from './sms/rdf-generator.js';
|
| 28 |
import { generatePhysicalGraph } from './sms/sms-graph-generator.js';
|
| 29 |
import {
|
|
@@ -35,8 +41,8 @@ import {
|
|
| 35 |
/**
|
| 36 |
* RECURSIVE BOM TREE NODE COMPONENT
|
| 37 |
* ---------------------------------
|
| 38 |
-
*
|
| 39 |
-
*
|
| 40 |
*/
|
| 41 |
const BomNode = {
|
| 42 |
name: 'BomNode',
|
|
@@ -48,7 +54,15 @@ const BomNode = {
|
|
| 48 |
},
|
| 49 |
template: `
|
| 50 |
<li>
|
| 51 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
<ul v-if="node.children && node.children.length > 0" class="tree">
|
| 53 |
<bom-node
|
| 54 |
v-for="child in node.children"
|
|
@@ -65,96 +79,13 @@ const App = {
|
|
| 65 |
BomNode
|
| 66 |
},
|
| 67 |
setup() {
|
| 68 |
-
// 1. Local UI Settings (Isolated from domain state)
|
| 69 |
-
const settings = reactive({
|
| 70 |
-
language: 'sv', // Available options: 'sv', 'en', 'ru'
|
| 71 |
-
ontology: 'NAIVE_NODE', // SMIM Top-Level Ontology: 'BFO' | 'ISO15926' | 'NAIVE_NODE' | 'NAIVE_EDGE'
|
| 72 |
-
storage: 'LPG', // SMS Storage Paradigm: 'LPG' or 'RDF'
|
| 73 |
-
engine: 'vis', // Renderer: 'vis' (Vis.js) or 'mermaid'
|
| 74 |
-
perspective: 'SMRM' // Perspective: 'SMRM' | 'SMIM' | 'SMS'
|
| 75 |
-
});
|
| 76 |
-
|
| 77 |
-
// User Role State ('UNI_ENGINEER')
|
| 78 |
-
const userRole = ref('UNI_ENGINEER');
|
| 79 |
-
watch(userRole, (newRole) => {
|
| 80 |
-
state.userRole = newRole;
|
| 81 |
-
});
|
| 82 |
-
watch(() => state.userRole, (newRole) => {
|
| 83 |
-
if (newRole && userRole.value !== newRole) {
|
| 84 |
-
userRole.value = newRole;
|
| 85 |
-
}
|
| 86 |
-
});
|
| 87 |
-
|
| 88 |
-
// Synchronise reactive ontology between settings and domain state
|
| 89 |
-
watch(() => state.ontology, (newOnto) => {
|
| 90 |
-
if (newOnto === 'ISO_15926_4D' || newOnto === 'ISO15926') {
|
| 91 |
-
settings.ontology = 'ISO15926';
|
| 92 |
-
} else if (newOnto === 'NAIVE_NODE') {
|
| 93 |
-
settings.ontology = 'NAIVE_NODE';
|
| 94 |
-
} else if (newOnto === 'NAIVE_EDGE') {
|
| 95 |
-
settings.ontology = 'NAIVE_EDGE';
|
| 96 |
-
} else if (newOnto === 'NAIVE') {
|
| 97 |
-
settings.ontology = 'NAIVE_NODE';
|
| 98 |
-
} else if (newOnto === 'BFO') {
|
| 99 |
-
settings.ontology = 'BFO';
|
| 100 |
-
}
|
| 101 |
-
}, { immediate: true });
|
| 102 |
-
watch(() => settings.ontology, (newOnto) => {
|
| 103 |
-
if (state.ontology !== newOnto) {
|
| 104 |
-
state.ontology = newOnto;
|
| 105 |
-
}
|
| 106 |
-
});
|
| 107 |
-
|
| 108 |
-
// Synchronise perspective between settings and domain state
|
| 109 |
-
watch(() => state.perspective, (newPersp) => {
|
| 110 |
-
if (newPersp && settings.perspective !== newPersp) {
|
| 111 |
-
settings.perspective = newPersp;
|
| 112 |
-
}
|
| 113 |
-
}, { immediate: true });
|
| 114 |
-
watch(() => settings.perspective, (newPersp) => {
|
| 115 |
-
if (state.perspective !== newPersp) {
|
| 116 |
-
state.perspective = newPersp;
|
| 117 |
-
}
|
| 118 |
-
});
|
| 119 |
-
|
| 120 |
-
// Synchronise engine between settings and domain state
|
| 121 |
-
watch(() => state.graphEngine, (newEngine) => {
|
| 122 |
-
const lower = (newEngine || 'VIS').toLowerCase();
|
| 123 |
-
if (settings.engine !== lower) {
|
| 124 |
-
settings.engine = lower;
|
| 125 |
-
}
|
| 126 |
-
}, { immediate: true });
|
| 127 |
-
watch(() => settings.engine, (newEngine) => {
|
| 128 |
-
const upper = (newEngine || 'vis').toUpperCase();
|
| 129 |
-
if (state.graphEngine !== upper) {
|
| 130 |
-
state.graphEngine = upper;
|
| 131 |
-
}
|
| 132 |
-
});
|
| 133 |
-
|
| 134 |
// DOM References for renderers & file operations
|
| 135 |
const visContainer = ref(null);
|
| 136 |
const mermaidContainer = ref(null);
|
| 137 |
const fileInput = ref(null);
|
| 138 |
|
| 139 |
-
// Form inputs
|
| 140 |
-
const partNameInput = ref('');
|
| 141 |
-
const selectedParent = ref('System_A');
|
| 142 |
-
const selectedEdgeToRemove = ref('');
|
| 143 |
-
const showHistory = ref(true);
|
| 144 |
-
|
| 145 |
-
// New Sandbox & Domain Extension Form inputs
|
| 146 |
-
const selectedNodeClass = ref(ENTITY_CLASSES[0]);
|
| 147 |
-
const newNodeIdInput = ref('');
|
| 148 |
-
const selectedRelSource = ref('System_A');
|
| 149 |
-
const selectedRelType = ref(RELATION_TYPES[0]);
|
| 150 |
-
const selectedRelTarget = ref('');
|
| 151 |
-
const propTargetType = ref('NODE');
|
| 152 |
-
const propTargetId = ref('');
|
| 153 |
-
const propKeyInput = ref('');
|
| 154 |
-
const propValInput = ref('');
|
| 155 |
-
|
| 156 |
// Universal Engineer Business UI inputs
|
| 157 |
-
const bizRegType = ref('
|
| 158 |
const bizRegId = ref('');
|
| 159 |
const bizInstSource = ref('');
|
| 160 |
const bizInstTarget = ref('');
|
|
@@ -173,30 +104,14 @@ const App = {
|
|
| 173 |
const audioDuration = ref(0);
|
| 174 |
const showSubtitles = ref(true);
|
| 175 |
const currentSubtitleText = ref('');
|
| 176 |
-
const executedTimelineActions = ref(new Set());
|
| 177 |
|
| 178 |
// Layout State (Flex percentages for resizers)
|
| 179 |
-
const layout = reactive({ leftWidth:
|
| 180 |
-
|
| 181 |
-
// Active dictionary based on local settings
|
| 182 |
-
const t = computed(() => i18n[settings.language]);
|
| 183 |
|
| 184 |
-
/
|
| 185 |
-
|
| 186 |
-
*
|
| 187 |
-
* @param {string} nodeId - Identifier of the node.
|
| 188 |
-
* @returns {string} Domain entity class ('Vehicle' | 'Slot' | 'Part' | 'Defect' | 'Warehouse').
|
| 189 |
-
*/
|
| 190 |
-
function getNodeEntityClass(nodeId) {
|
| 191 |
-
if (!nodeId) return 'Part';
|
| 192 |
-
if (/^veh/i.test(nodeId) || /^sys/i.test(nodeId)) return 'Vehicle';
|
| 193 |
-
if (/^slot/i.test(nodeId)) return 'Slot';
|
| 194 |
-
if (/^ware|^wh/i.test(nodeId)) return 'Warehouse';
|
| 195 |
-
if (/^def/i.test(nodeId)) return 'Defect';
|
| 196 |
-
return 'Part';
|
| 197 |
-
}
|
| 198 |
|
| 199 |
-
//
|
| 200 |
const baseNodes = computed(() => {
|
| 201 |
const nodeMap = new Map();
|
| 202 |
|
|
@@ -204,27 +119,30 @@ const App = {
|
|
| 204 |
if (event.valid_from !== undefined && event.valid_from > state.currentTime) return;
|
| 205 |
|
| 206 |
if (event.type === 'REGISTER_PART' && event.partId) {
|
|
|
|
| 207 |
nodeMap.set(event.partId, {
|
| 208 |
id: event.partId,
|
| 209 |
label: event.partId,
|
| 210 |
-
type:
|
| 211 |
-
entityClass:
|
| 212 |
});
|
| 213 |
} else if (event.type === 'ASSEMBLE') {
|
| 214 |
if (event.parentId && !nodeMap.has(event.parentId)) {
|
|
|
|
| 215 |
nodeMap.set(event.parentId, {
|
| 216 |
id: event.parentId,
|
| 217 |
label: event.parentId,
|
| 218 |
-
type:
|
| 219 |
-
entityClass:
|
| 220 |
});
|
| 221 |
}
|
| 222 |
if (event.childId && !nodeMap.has(event.childId)) {
|
|
|
|
| 223 |
nodeMap.set(event.childId, {
|
| 224 |
id: event.childId,
|
| 225 |
label: event.childId,
|
| 226 |
-
type:
|
| 227 |
-
entityClass:
|
| 228 |
});
|
| 229 |
}
|
| 230 |
}
|
|
@@ -233,151 +151,140 @@ const App = {
|
|
| 233 |
return Array.from(nodeMap.values());
|
| 234 |
});
|
| 235 |
|
| 236 |
-
const availableNodes = computed(() =>
|
| 237 |
-
return baseNodes.value.map(n => n.id);
|
| 238 |
-
});
|
| 239 |
|
| 240 |
-
const
|
| 241 |
-
|
| 242 |
-
|
| 243 |
|
| 244 |
-
const
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
baseNodes.value,
|
| 248 |
-
state.currentTime
|
| 249 |
-
).edges.filter(e => e.properties?.isCurrent !== false);
|
| 250 |
-
});
|
| 251 |
|
| 252 |
-
const
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
const activeWarehouses = computed(() => availableNodes.value.filter(id => getNodeEntityClass(id) === 'Warehouse'));
|
| 256 |
|
| 257 |
-
const
|
| 258 |
-
|
| 259 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 260 |
|
| 261 |
-
|
| 262 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 263 |
});
|
| 264 |
|
|
|
|
|
|
|
|
|
|
| 265 |
const installedComponents = computed(() => {
|
| 266 |
const comps = new Set();
|
| 267 |
-
|
| 268 |
-
if (e.
|
| 269 |
-
|
|
|
|
|
|
|
|
|
|
| 270 |
}
|
| 271 |
});
|
| 272 |
return Array.from(comps).filter(id => !id.startsWith('Slot_'));
|
| 273 |
});
|
| 274 |
|
|
|
|
| 275 |
const activeDefectsList = computed(() => {
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
|
|
|
|
|
|
|
|
|
| 284 |
targetId: e.targetId,
|
| 285 |
defectName: e.defectName,
|
| 286 |
defectValue: e.defectValue,
|
| 287 |
label: `${e.targetId} • ${e.defectName}=${e.defectValue}`
|
| 288 |
-
};
|
| 289 |
-
})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 290 |
});
|
| 291 |
|
|
|
|
| 292 |
watch(bizInstallSources, (sources) => {
|
| 293 |
-
if (!sources.includes(bizInstSource.value))
|
| 294 |
-
bizInstSource.value = sources[0] || '';
|
| 295 |
-
}
|
| 296 |
}, { immediate: true });
|
| 297 |
|
| 298 |
watch(bizInstallTargets, (targets) => {
|
| 299 |
-
if (!targets.includes(bizInstTarget.value))
|
| 300 |
-
bizInstTarget.value = targets[0] || '';
|
| 301 |
-
}
|
| 302 |
}, { immediate: true });
|
| 303 |
|
| 304 |
watch(activeParts, (parts) => {
|
| 305 |
-
if (!parts.includes(bizStorePart.value))
|
| 306 |
-
bizStorePart.value = parts[0] || '';
|
| 307 |
-
}
|
| 308 |
}, { immediate: true });
|
| 309 |
|
| 310 |
watch(activeWarehouses, (warehouses) => {
|
| 311 |
-
if (!warehouses.includes(bizStoreTarget.value))
|
| 312 |
-
bizStoreTarget.value = warehouses[0] || '';
|
| 313 |
-
}
|
| 314 |
}, { immediate: true });
|
| 315 |
|
| 316 |
watch(installedComponents, (comps) => {
|
| 317 |
-
if (!comps.includes(bizDismPart.value))
|
| 318 |
-
bizDismPart.value = comps[0] || '';
|
| 319 |
-
}
|
| 320 |
}, { immediate: true });
|
| 321 |
|
| 322 |
watch(activeDefectsList, (defects) => {
|
| 323 |
-
if (!defects.some(d => d.id === bizRevDefect.value))
|
| 324 |
-
bizRevDefect.value = defects[0]?.id || '';
|
| 325 |
-
}
|
| 326 |
}, { immediate: true });
|
| 327 |
|
| 328 |
watch(availableNodes, (nodes) => {
|
| 329 |
-
if (!nodes.includes(bizDefTarget.value))
|
| 330 |
-
bizDefTarget.value = nodes[0] || '';
|
| 331 |
-
}
|
| 332 |
}, { immediate: true });
|
| 333 |
|
| 334 |
-
/
|
| 335 |
-
|
| 336 |
-
* ------------------------
|
| 337 |
-
* Receives domain action events, validates consistency, and mutates
|
| 338 |
-
* the event log strictly according to Event Sourcing principles.
|
| 339 |
-
*
|
| 340 |
-
* @param {Object} event - The domain event object to dispatch.
|
| 341 |
-
*/
|
| 342 |
-
/**
|
| 343 |
-
* Central Event Dispatcher (Pure Event Sourcing)
|
| 344 |
-
* ------------------------------------------------
|
| 345 |
-
* Directly appends immutable business facts to the event log.
|
| 346 |
-
*
|
| 347 |
-
* @param {Object} event - The domain event object to dispatch.
|
| 348 |
-
*/
|
| 349 |
-
function dispatch(event) {
|
| 350 |
-
if (!event) return;
|
| 351 |
-
state.eventLog.push(event);
|
| 352 |
-
}
|
| 353 |
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
if (settings.ontology === 'BFO') {
|
| 359 |
-
return bfoAdapter.generateSemanticGraph(eventsToProcess, baseNodes.value, state.currentTime);
|
| 360 |
-
} else if (settings.ontology === 'ISO15926') {
|
| 361 |
-
return isoAdapter.generateSemanticGraph(eventsToProcess, baseNodes.value, state.currentTime);
|
| 362 |
-
} else {
|
| 363 |
-
return naiveAdapter.generateSemanticGraph(eventsToProcess, baseNodes.value, state.currentTime, {
|
| 364 |
-
ontology: settings.ontology
|
| 365 |
-
});
|
| 366 |
}
|
|
|
|
| 367 |
});
|
| 368 |
|
| 369 |
-
// Derived Views for the Sidebar (URMSM & SMS)
|
| 370 |
-
const bomTree = computed(() => generateBomTree('System_A', activeEdges.value));
|
| 371 |
-
|
| 372 |
const lpgRecords = computed(() => {
|
| 373 |
-
if (
|
| 374 |
return generateLpgStorage(semanticGraph.value);
|
| 375 |
}
|
| 376 |
return [];
|
| 377 |
});
|
| 378 |
|
| 379 |
const rdfTtl = computed(() => {
|
| 380 |
-
if (
|
| 381 |
return generateRdfStorage(semanticGraph.value);
|
| 382 |
}
|
| 383 |
return '';
|
|
@@ -385,7 +292,11 @@ const App = {
|
|
| 385 |
|
| 386 |
function formatProperties(props) {
|
| 387 |
if (!props) return '';
|
| 388 |
-
const entries = Object.entries(props).filter(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 389 |
if (entries.length === 0) return '';
|
| 390 |
return entries.map(([k, v]) => {
|
| 391 |
const valStr = v === null ? 'null' : (typeof v === 'number' ? `T${v}` : `'${v}'`);
|
|
@@ -394,80 +305,102 @@ const App = {
|
|
| 394 |
}
|
| 395 |
|
| 396 |
const queryCode = computed(() => {
|
| 397 |
-
if (
|
| 398 |
-
return generateLpgQuery(semanticGraph.value, state.currentTime);
|
| 399 |
-
} else {
|
| 400 |
-
return generateRdfQuery(semanticGraph.value, state.currentTime);
|
| 401 |
}
|
|
|
|
| 402 |
});
|
| 403 |
|
| 404 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 405 |
const activeGraph = computed(() => {
|
| 406 |
-
if (
|
| 407 |
-
return generatePhysicalGraph(semanticGraph.value,
|
|
|
|
|
|
|
|
|
|
| 408 |
}
|
| 409 |
-
if (
|
| 410 |
-
return
|
| 411 |
}
|
| 412 |
return semanticGraph.value;
|
| 413 |
});
|
| 414 |
|
| 415 |
-
// Reactive Graph Caption
|
| 416 |
const graphCaption = computed(() => {
|
| 417 |
let ontoText = 'BFO (3D)';
|
| 418 |
-
if (
|
| 419 |
-
if (
|
| 420 |
-
if (
|
| 421 |
-
|
| 422 |
let viewText = t.value.viewLogicalCaption;
|
| 423 |
-
if (
|
| 424 |
-
if (
|
| 425 |
-
|
|
|
|
|
|
|
| 426 |
});
|
| 427 |
|
| 428 |
-
// Contextual Dynamic Legend
|
| 429 |
const graphLegendItems = computed(() => {
|
| 430 |
const nodes = [];
|
| 431 |
const edges = [];
|
| 432 |
const dict = t.value.legend;
|
| 433 |
|
| 434 |
-
|
|
|
|
| 435 |
nodes.push({ id: 'naiveVehicle', label: dict.naiveVehicle, swatch: 'swatch-box-vehicle' });
|
| 436 |
-
|
|
|
|
|
|
|
| 437 |
nodes.push({ id: 'naivePart', label: dict.naivePart, swatch: 'swatch-box-part' });
|
| 438 |
nodes.push({ id: 'naiveDefect', label: dict.naiveDefect, swatch: 'swatch-diamond-defect' });
|
| 439 |
nodes.push({ id: 'naiveWarehouse', label: dict.naiveWarehouse, swatch: 'swatch-box-warehouse' });
|
| 440 |
|
| 441 |
-
|
| 442 |
-
|
| 443 |
-
|
| 444 |
-
|
| 445 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 446 |
nodes.push({ id: 'bfoSys', label: dict.bfoSys, swatch: 'swatch-box-sys' });
|
| 447 |
nodes.push({ id: 'bfoElem', label: dict.bfoElem, swatch: 'swatch-box-elem' });
|
| 448 |
nodes.push({ id: 'bfoQuality', label: dict.bfoQuality, swatch: 'swatch-diamond' });
|
| 449 |
|
| 450 |
-
if (
|
| 451 |
nodes.push({ id: 'rdfStmt', label: dict.rdfStmt, swatch: 'swatch-diamond' });
|
| 452 |
nodes.push({ id: 'rdfLit', label: dict.rdfLit, swatch: 'swatch-literal' });
|
| 453 |
edges.push({ id: 'rdfPred', label: dict.rdfPred, swatch: 'swatch-line-past' });
|
| 454 |
} else {
|
| 455 |
edges.push({ id: 'bfoActive', label: dict.bfoActiveEdge, swatch: 'swatch-line-active' });
|
| 456 |
edges.push({ id: 'bfoInheres', label: dict.bfoInheres, swatch: 'swatch-line-transition' });
|
| 457 |
-
if (
|
| 458 |
edges.push({ id: 'bfoPast', label: dict.bfoPastEdge, swatch: 'swatch-line-past' });
|
| 459 |
}
|
| 460 |
}
|
| 461 |
} else {
|
| 462 |
-
// ISO 15926 (4D)
|
| 463 |
nodes.push({ id: 'iso4d', label: dict.iso4d, swatch: 'swatch-box-4d' });
|
| 464 |
nodes.push({ id: 'isoState', label: dict.isoState, swatch: 'swatch-box-state' });
|
| 465 |
nodes.push({ id: 'isoDefect', label: dict.isoDefect, swatch: 'swatch-diamond-defect' });
|
|
|
|
|
|
|
|
|
|
| 466 |
edges.push({ id: 'isoPart', label: dict.isoPartRel, swatch: 'swatch-line-active' });
|
| 467 |
edges.push({ id: 'isoSlice', label: dict.isoSliceRel, swatch: 'swatch-line-past' });
|
| 468 |
edges.push({ id: 'isoDefectRel', label: dict.isoDefectRel, swatch: 'swatch-line-defect' });
|
| 469 |
|
| 470 |
-
if (
|
| 471 |
edges.push({ id: 'isoTrans', label: dict.isoTransitionRel, swatch: 'swatch-line-transition' });
|
| 472 |
}
|
| 473 |
}
|
|
@@ -475,35 +408,51 @@ const App = {
|
|
| 475 |
return { nodes, edges };
|
| 476 |
});
|
| 477 |
|
| 478 |
-
//
|
| 479 |
-
watch([activeGraph, () =>
|
| 480 |
renderGraph();
|
| 481 |
}, { deep: true });
|
| 482 |
|
| 483 |
function renderGraph() {
|
| 484 |
if (state.currentView === 'MENU') return;
|
| 485 |
const graphToRender = activeGraph.value;
|
| 486 |
-
if (
|
| 487 |
-
const isComplex =
|
| 488 |
updateVisNetwork(graphToRender, isComplex);
|
| 489 |
} else if (mermaidContainer.value) {
|
| 490 |
updateMermaidNetwork(graphToRender, mermaidContainer.value);
|
| 491 |
}
|
| 492 |
}
|
| 493 |
|
| 494 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 495 |
watch(() => state.currentView, async (newView) => {
|
| 496 |
if (newView !== 'SCENARIO') {
|
| 497 |
-
stopAudio();
|
| 498 |
}
|
| 499 |
if (newView === 'SCENARIO') {
|
| 500 |
-
|
| 501 |
-
state.eventLog.
|
| 502 |
resetTimeline(0);
|
| 503 |
state.currentTime = 0;
|
| 504 |
state.ontology = 'NAIVE_NODE';
|
| 505 |
-
|
| 506 |
state.graphEngine = 'MERMAID';
|
|
|
|
|
|
|
| 507 |
}
|
| 508 |
if (newView !== 'MENU') {
|
| 509 |
await nextTick();
|
|
@@ -514,38 +463,22 @@ const App = {
|
|
| 514 |
}
|
| 515 |
});
|
| 516 |
|
| 517 |
-
// Lifecycle hooks
|
| 518 |
onMounted(() => {
|
| 519 |
-
|
| 520 |
-
dispatch(createRegisterPartEvent('System_A', state.currentTime));
|
| 521 |
-
dispatch(createRegisterPartEvent('SubAssembly_1', state.currentTime));
|
| 522 |
-
dispatch(createAssembleEvent('System_A', 'SubAssembly_1', 'has_part', state.currentTime));
|
| 523 |
-
dispatch(createRegisterPartEvent('Part_A', state.currentTime));
|
| 524 |
-
dispatch(createAssembleEvent('System_A', 'Part_A', 'has_part', state.currentTime));
|
| 525 |
-
dispatch(createRegisterPartEvent('Sensor_X', state.currentTime));
|
| 526 |
-
dispatch(createAssembleEvent('SubAssembly_1', 'Sensor_X', 'has_part', state.currentTime));
|
| 527 |
-
|
| 528 |
if (state.currentView !== 'MENU' && visContainer.value) {
|
| 529 |
initVisNetwork(visContainer.value);
|
| 530 |
renderGraph();
|
| 531 |
}
|
| 532 |
});
|
| 533 |
|
| 534 |
-
// UI Event Handlers
|
| 535 |
-
/**
|
| 536 |
-
* Resets the application state, stops audio playback, and returns to the main menu.
|
| 537 |
-
*/
|
| 538 |
function goHome() {
|
| 539 |
-
stopAudio();
|
| 540 |
state.currentView = 'MENU';
|
| 541 |
state.eventLog.splice(0, state.eventLog.length);
|
| 542 |
state.currentTime = 0;
|
| 543 |
state.edgeSequence = 1;
|
| 544 |
}
|
| 545 |
|
| 546 |
-
/**
|
| 547 |
-
* Starts audio playback.
|
| 548 |
-
*/
|
| 549 |
function playAudio() {
|
| 550 |
if (scenarioAudio.value) {
|
| 551 |
scenarioAudio.value.play().catch(err => {
|
|
@@ -554,18 +487,12 @@ const App = {
|
|
| 554 |
}
|
| 555 |
}
|
| 556 |
|
| 557 |
-
/**
|
| 558 |
-
* Pauses audio playback.
|
| 559 |
-
*/
|
| 560 |
function pauseAudio() {
|
| 561 |
if (scenarioAudio.value) {
|
| 562 |
scenarioAudio.value.pause();
|
| 563 |
}
|
| 564 |
}
|
| 565 |
|
| 566 |
-
/**
|
| 567 |
-
* Toggles audio playback between play and pause states.
|
| 568 |
-
*/
|
| 569 |
function togglePlayAudio() {
|
| 570 |
if (isPlaying.value) {
|
| 571 |
pauseAudio();
|
|
@@ -574,10 +501,7 @@ const App = {
|
|
| 574 |
}
|
| 575 |
}
|
| 576 |
|
| 577 |
-
|
| 578 |
-
* Stops audio playback, resets the timeline to zero, and clears the Event Sourcing log.
|
| 579 |
-
*/
|
| 580 |
-
function stopAudio() {
|
| 581 |
if (scenarioAudio.value) {
|
| 582 |
scenarioAudio.value.pause();
|
| 583 |
scenarioAudio.value.currentTime = 0;
|
|
@@ -586,61 +510,44 @@ const App = {
|
|
| 586 |
audioCurrentTime.value = 0;
|
| 587 |
currentSubtitleText.value = '';
|
| 588 |
|
| 589 |
-
|
| 590 |
-
|
| 591 |
-
|
| 592 |
-
|
| 593 |
-
|
| 594 |
-
|
| 595 |
-
|
| 596 |
-
|
| 597 |
-
|
|
|
|
|
|
|
| 598 |
}
|
| 599 |
|
| 600 |
-
/**
|
| 601 |
-
* Synchronises reactive audio time, subtitles, and timeline graph events with HTML5 audio playback.
|
| 602 |
-
*/
|
| 603 |
function onTimeUpdate() {
|
| 604 |
if (scenarioAudio.value) {
|
| 605 |
audioCurrentTime.value = scenarioAudio.value.currentTime;
|
| 606 |
}
|
| 607 |
-
|
| 608 |
const currentTime = audioCurrentTime.value;
|
| 609 |
|
| 610 |
-
// Subtitle synchronisation
|
| 611 |
const activeSubtitle = vanishingCrackScenario.subtitles.find(
|
| 612 |
sub => currentTime >= sub.start && currentTime <= sub.end
|
| 613 |
);
|
| 614 |
currentSubtitleText.value = activeSubtitle ? activeSubtitle.text : '';
|
| 615 |
|
| 616 |
-
// Scenario timeline dispatcher
|
| 617 |
dispatchScenarioTimeline(currentTime, state, renderGraph);
|
| 618 |
}
|
| 619 |
|
| 620 |
-
/**
|
| 621 |
-
* Initialises audio duration when metadata is loaded.
|
| 622 |
-
*/
|
| 623 |
function onLoadedMetadata() {
|
| 624 |
if (scenarioAudio.value) {
|
| 625 |
audioDuration.value = scenarioAudio.value.duration || 0;
|
| 626 |
}
|
| 627 |
}
|
| 628 |
|
| 629 |
-
/**
|
| 630 |
-
* Handles completion of audio playback.
|
| 631 |
-
* Crucial: On audio end, do NOT rewind or reset the state. Let the graph remain on screen!
|
| 632 |
-
*/
|
| 633 |
function onAudioEnded() {
|
| 634 |
isPlaying.value = false;
|
| 635 |
currentSubtitleText.value = '';
|
| 636 |
}
|
| 637 |
|
| 638 |
-
/**
|
| 639 |
-
* Seeks to a specific position in the audio timeline based on range input,
|
| 640 |
-
* resetting and re-evaluating the Event Sourcing state up to the new timestamp.
|
| 641 |
-
*
|
| 642 |
-
* @param {Event} event - The range slider input event.
|
| 643 |
-
*/
|
| 644 |
function onSeek(event) {
|
| 645 |
const targetTime = parseFloat(event.target.value);
|
| 646 |
if (scenarioAudio.value) {
|
|
@@ -648,23 +555,16 @@ const App = {
|
|
| 648 |
}
|
| 649 |
audioCurrentTime.value = targetTime;
|
| 650 |
|
| 651 |
-
|
| 652 |
-
state.eventLog.length = 0;
|
| 653 |
resetTimeline(0);
|
|
|
|
| 654 |
state.ontology = 'NAIVE_NODE';
|
| 655 |
-
|
| 656 |
state.graphEngine = 'MERMAID';
|
| 657 |
|
| 658 |
-
// Fast-forward timeline events and update subtitles to targetTime
|
| 659 |
onTimeUpdate();
|
| 660 |
}
|
| 661 |
|
| 662 |
-
/**
|
| 663 |
-
* Formats seconds into a human-readable mm:ss format.
|
| 664 |
-
*
|
| 665 |
-
* @param {number} seconds - Time duration in seconds.
|
| 666 |
-
* @returns {string} Formatted time string.
|
| 667 |
-
*/
|
| 668 |
function formatTime(seconds) {
|
| 669 |
if (!seconds || isNaN(seconds) || seconds < 0) return '00:00';
|
| 670 |
const mins = Math.floor(seconds / 60);
|
|
@@ -672,14 +572,14 @@ const App = {
|
|
| 672 |
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
|
| 673 |
}
|
| 674 |
|
| 675 |
-
/
|
| 676 |
-
* Universal Engineer Action 1: Register Asset
|
| 677 |
-
*/
|
| 678 |
function handleBizRegister() {
|
| 679 |
if (!bizRegId.value) return;
|
| 680 |
const event = createRegisterPartEvent(
|
| 681 |
bizRegId.value,
|
| 682 |
-
state.currentTime
|
|
|
|
|
|
|
| 683 |
);
|
| 684 |
if (event) {
|
| 685 |
dispatch(event);
|
|
@@ -687,36 +587,28 @@ const App = {
|
|
| 687 |
}
|
| 688 |
}
|
| 689 |
|
| 690 |
-
/**
|
| 691 |
-
* Universal Engineer Action 2: Install / Assemble
|
| 692 |
-
*/
|
| 693 |
function handleBizInstall() {
|
| 694 |
if (!bizInstSource.value || !bizInstTarget.value) return;
|
| 695 |
-
|
| 696 |
-
const parentId = bizInstTarget.value;
|
| 697 |
-
if (childId === parentId) return;
|
| 698 |
|
| 699 |
const event = createAssembleEvent(
|
| 700 |
-
|
| 701 |
-
|
| 702 |
'mounted_in',
|
| 703 |
-
state.currentTime
|
|
|
|
| 704 |
);
|
| 705 |
-
if (event)
|
| 706 |
-
dispatch(event);
|
| 707 |
-
}
|
| 708 |
}
|
| 709 |
|
| 710 |
-
/**
|
| 711 |
-
* Universal Engineer Action 3: Report Defect
|
| 712 |
-
*/
|
| 713 |
function handleBizReportDefect() {
|
| 714 |
if (!bizDefTarget.value || !bizDefType.value) return;
|
| 715 |
const event = createReportDefectEvent(
|
| 716 |
bizDefTarget.value,
|
| 717 |
bizDefType.value,
|
| 718 |
bizDefVal.value || 'present',
|
| 719 |
-
state.currentTime
|
|
|
|
| 720 |
);
|
| 721 |
if (event) {
|
| 722 |
dispatch(event);
|
|
@@ -725,26 +617,30 @@ const App = {
|
|
| 725 |
}
|
| 726 |
}
|
| 727 |
|
| 728 |
-
/**
|
| 729 |
-
* Universal Engineer Action 4: Revoke Defect
|
| 730 |
-
*/
|
| 731 |
function handleBizRevokeDefect() {
|
| 732 |
if (!bizRevDefect.value) return;
|
| 733 |
const item = activeDefectsList.value.find(d => d.id === bizRevDefect.value);
|
| 734 |
-
if (item
|
| 735 |
-
|
| 736 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 737 |
}
|
| 738 |
}
|
| 739 |
|
| 740 |
-
/**
|
| 741 |
-
* Universal Engineer Action 5: Dismantle
|
| 742 |
-
*/
|
| 743 |
function handleBizDismantle() {
|
| 744 |
if (!bizDismPart.value) return;
|
| 745 |
const event = createDismantleEvent(
|
| 746 |
bizDismPart.value,
|
| 747 |
-
state.currentTime
|
|
|
|
| 748 |
);
|
| 749 |
if (event) {
|
| 750 |
dispatch(event);
|
|
@@ -752,73 +648,79 @@ const App = {
|
|
| 752 |
}
|
| 753 |
}
|
| 754 |
|
| 755 |
-
/**
|
| 756 |
-
* Universal Engineer Action 6: Move to Storage
|
| 757 |
-
*/
|
| 758 |
function handleBizStore() {
|
| 759 |
if (!bizStorePart.value || !bizStoreTarget.value) return;
|
| 760 |
const event = createAssembleEvent(
|
| 761 |
bizStoreTarget.value,
|
| 762 |
bizStorePart.value,
|
| 763 |
'stored_at',
|
| 764 |
-
state.currentTime
|
|
|
|
| 765 |
);
|
| 766 |
-
if (event)
|
| 767 |
-
dispatch(event);
|
| 768 |
-
}
|
| 769 |
}
|
| 770 |
|
| 771 |
-
// Resizer
|
| 772 |
function startResizeH(e) {
|
|
|
|
|
|
|
| 773 |
const startX = e.clientX;
|
| 774 |
const startWidth = layout.leftWidth;
|
| 775 |
const containerWidth = document.querySelector('.main-container').getBoundingClientRect().width;
|
| 776 |
-
|
| 777 |
const onMouseMove = (evt) => {
|
| 778 |
const newWidth = startWidth + ((evt.clientX - startX) / containerWidth) * 100;
|
| 779 |
-
if (newWidth >
|
| 780 |
};
|
| 781 |
-
|
| 782 |
const onMouseUp = () => {
|
|
|
|
| 783 |
document.removeEventListener('mousemove', onMouseMove);
|
| 784 |
document.removeEventListener('mouseup', onMouseUp);
|
| 785 |
document.body.classList.remove('no-select');
|
| 786 |
};
|
| 787 |
-
|
| 788 |
document.addEventListener('mousemove', onMouseMove);
|
| 789 |
document.addEventListener('mouseup', onMouseUp);
|
| 790 |
document.body.classList.add('no-select');
|
| 791 |
}
|
| 792 |
|
| 793 |
function startResizeV(e, property, containerSelector) {
|
|
|
|
|
|
|
| 794 |
const startY = e.clientY;
|
| 795 |
const startHeight = layout[property];
|
| 796 |
const containerHeight = document.querySelector(containerSelector).getBoundingClientRect().height;
|
| 797 |
-
|
| 798 |
const onMouseMove = (evt) => {
|
| 799 |
const newHeight = startHeight + ((evt.clientY - startY) / containerHeight) * 100;
|
| 800 |
-
if (newHeight >
|
| 801 |
};
|
| 802 |
-
|
| 803 |
const onMouseUp = () => {
|
|
|
|
| 804 |
document.removeEventListener('mousemove', onMouseMove);
|
| 805 |
document.removeEventListener('mouseup', onMouseUp);
|
| 806 |
document.body.classList.remove('no-select');
|
| 807 |
};
|
| 808 |
-
|
| 809 |
document.addEventListener('mousemove', onMouseMove);
|
| 810 |
document.addEventListener('mouseup', onMouseUp);
|
| 811 |
document.body.classList.add('no-select');
|
| 812 |
}
|
| 813 |
|
| 814 |
-
/
|
| 815 |
-
* Serialises and exports the current Event Sourcing state to a JSON file.
|
| 816 |
-
*/
|
| 817 |
function saveJson() {
|
| 818 |
const exportData = {
|
| 819 |
-
version: '
|
| 820 |
savedAt: new Date().toISOString(),
|
| 821 |
-
userRole:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 822 |
currentTime: state.currentTime,
|
| 823 |
edgeSequence: state.edgeSequence,
|
| 824 |
eventLog: JSON.parse(JSON.stringify(state.eventLog))
|
|
@@ -834,9 +736,6 @@ const App = {
|
|
| 834 |
URL.revokeObjectURL(url);
|
| 835 |
}
|
| 836 |
|
| 837 |
-
/**
|
| 838 |
-
* Opens the native file dialogue for loading a saved state JSON file.
|
| 839 |
-
*/
|
| 840 |
function triggerLoadJson() {
|
| 841 |
if (fileInput.value) {
|
| 842 |
fileInput.value.value = '';
|
|
@@ -844,11 +743,6 @@ const App = {
|
|
| 844 |
}
|
| 845 |
}
|
| 846 |
|
| 847 |
-
/**
|
| 848 |
-
* Reads, deserialises, and restores the application domain state from a JSON file.
|
| 849 |
-
*
|
| 850 |
-
* @param {Event} e - HTML input change event.
|
| 851 |
-
*/
|
| 852 |
function onFileLoaded(e) {
|
| 853 |
const file = e.target.files?.[0];
|
| 854 |
if (!file) return;
|
|
@@ -861,9 +755,6 @@ const App = {
|
|
| 861 |
loadState({ eventLog: parsed });
|
| 862 |
} else if (parsed && typeof parsed === 'object') {
|
| 863 |
loadState(parsed);
|
| 864 |
-
if (parsed.userRole && (parsed.userRole === 'DB_ADMIN' || parsed.userRole === 'UNI_ENGINEER')) {
|
| 865 |
-
userRole.value = parsed.userRole;
|
| 866 |
-
}
|
| 867 |
}
|
| 868 |
nextTick(() => {
|
| 869 |
renderGraph();
|
|
@@ -877,44 +768,44 @@ const App = {
|
|
| 877 |
}
|
| 878 |
|
| 879 |
return {
|
| 880 |
-
state,
|
| 881 |
-
|
| 882 |
entityClasses: ENTITY_CLASSES,
|
| 883 |
relationTypes: RELATION_TYPES,
|
| 884 |
-
availableNodes, goHome,
|
| 885 |
-
|
| 886 |
-
activeVehicles, activeSlots, activeParts, activeWarehouses,
|
| 887 |
bizRegType, bizRegId, handleBizRegister,
|
| 888 |
bizInstSource, bizInstTarget, handleBizInstall, bizInstallSources, bizInstallTargets,
|
| 889 |
bizDefTarget, bizDefType, bizDefVal, handleBizReportDefect,
|
| 890 |
bizRevDefect, handleBizRevokeDefect, activeDefectsList,
|
| 891 |
bizDismPart, handleBizDismantle, installedComponents,
|
| 892 |
bizStorePart, bizStoreTarget, handleBizStore,
|
| 893 |
-
|
|
|
|
| 894 |
visContainer, mermaidContainer,
|
| 895 |
graphCaption, graphLegendItems,
|
| 896 |
startResizeH, startResizeV,
|
| 897 |
scenarioAudio, isPlaying, audioCurrentTime, audioDuration,
|
| 898 |
-
showSubtitles, currentSubtitleText,
|
| 899 |
playAudio, pauseAudio, togglePlayAudio, stopAudio,
|
| 900 |
onTimeUpdate, onLoadedMetadata, onAudioEnded, onSeek, formatTime
|
| 901 |
};
|
| 902 |
},
|
| 903 |
-
|
| 904 |
template: `
|
| 905 |
<!-- Main Menu (Landing View) -->
|
| 906 |
<div v-if="state.currentView === 'MENU'" class="landing-view">
|
| 907 |
<div class="menu-card">
|
| 908 |
<div class="menu-badge">IEC 63339 Workbench</div>
|
| 909 |
<h1 class="menu-title">Knowledge Architecture Workbench</h1>
|
| 910 |
-
<p class="menu-subtitle">Top-Level Ontologies (SMIM), Storage
|
| 911 |
-
|
| 912 |
<div class="menu-actions">
|
| 913 |
<button class="menu-btn menu-btn-primary" @click="state.currentView = 'SANDBOX'">
|
| 914 |
<span class="btn-main-text">Sandbox Mode</span>
|
| 915 |
<span class="btn-sub-text">Interactive exploration of 3D/4D ontologies & temporal graph models</span>
|
| 916 |
</button>
|
| 917 |
-
|
| 918 |
<button class="menu-btn menu-btn-scenario" @click="state.currentView = 'SCENARIO'">
|
| 919 |
<span class="btn-main-text">Scenario: The Vanishing Crack</span>
|
| 920 |
<span class="btn-sub-text">Guided demonstration of temporal defect tracing across lifecycle states</span>
|
|
@@ -923,331 +814,394 @@ const App = {
|
|
| 923 |
</div>
|
| 924 |
</div>
|
| 925 |
|
| 926 |
-
<!-- Main Sandbox Workspace -->
|
| 927 |
<div v-if="state.currentView !== 'MENU'" class="sandbox-workspace">
|
| 928 |
<div class="control-deck">
|
| 929 |
-
|
| 930 |
-
|
| 931 |
-
|
| 932 |
-
|
| 933 |
-
|
| 934 |
-
|
| 935 |
-
|
| 936 |
-
|
| 937 |
-
|
| 938 |
-
|
| 939 |
-
|
| 940 |
-
|
| 941 |
-
|
| 942 |
-
<div class="divider"></div>
|
| 943 |
-
|
| 944 |
-
<div class="section-label">
|
| 945 |
-
<span class="iec-badge">IEC 63339 / SMS<span class="iec-tooltip" v-html="t.tooltips.sms"></span></span>
|
| 946 |
-
<span>{{ t.graphTitle }}</span>
|
| 947 |
-
</div>
|
| 948 |
-
<div class="btn-group">
|
| 949 |
-
<button id="ctrl-storage-lpg" :class="{ active: settings.storage === 'LPG' }" @click="settings.storage = 'LPG'">LPG</button>
|
| 950 |
-
<button id="ctrl-storage-rdf" :class="{ active: settings.storage === 'RDF' }" @click="settings.storage = 'RDF'">RDF (W3C)</button>
|
| 951 |
-
</div>
|
| 952 |
-
|
| 953 |
-
<div class="divider"></div>
|
| 954 |
-
|
| 955 |
-
<div class="section-label">🎨 <span>{{ t.engineTitle }}</span></div>
|
| 956 |
-
<div class="btn-group">
|
| 957 |
-
<button id="ctrl-engine-vis" :class="{ active: settings.engine === 'vis' }" @click="settings.engine = 'vis'">🌐 Vis.js</button>
|
| 958 |
-
<button id="ctrl-engine-mermaid" :class="{ active: settings.engine === 'mermaid' }" @click="settings.engine = 'mermaid'">📊 Mermaid</button>
|
| 959 |
-
</div>
|
| 960 |
-
|
| 961 |
-
<div class="divider"></div>
|
| 962 |
-
|
| 963 |
-
<div class="section-label">👁️ <span>{{ t.viewTitle }}</span></div>
|
| 964 |
-
<div class="btn-group">
|
| 965 |
-
<button id="ctrl-view-smrm" :class="{ active: settings.perspective === 'SMRM' }" @click="settings.perspective = 'SMRM'">{{ t.viewSmrm }}</button>
|
| 966 |
-
<button id="ctrl-view-logical" :class="{ active: settings.perspective === 'SMIM' }" @click="settings.perspective = 'SMIM'">{{ t.viewLogical }}</button>
|
| 967 |
-
<button id="ctrl-view-physical" :class="{ active: settings.perspective === 'SMS' }" @click="settings.perspective = 'SMS'">{{ t.viewPhysical }}</button>
|
| 968 |
-
</div>
|
| 969 |
-
|
| 970 |
-
<div class="divider"></div>
|
| 971 |
-
|
| 972 |
-
<!-- JSON State Operations -->
|
| 973 |
-
<button id="ctrl-save-json" class="secondary" @click="saveJson" :title="t.saveBtn">{{ t.saveBtn }}</button>
|
| 974 |
-
<button id="ctrl-load-json" class="secondary" @click="triggerLoadJson" :title="t.loadBtn">{{ t.loadBtn }}</button>
|
| 975 |
-
<input ref="fileInput" type="file" accept=".json" style="display: none;" @change="onFileLoaded" />
|
| 976 |
-
|
| 977 |
-
<div style="margin-left: auto;" class="btn-group flag-group">
|
| 978 |
-
<button id="ctrl-lang-sv" :class="{ active: settings.language === 'sv' }" @click="settings.language = 'sv'" title="Svenska">🇸🇪</button>
|
| 979 |
-
<button id="ctrl-lang-en" :class="{ active: settings.language === 'en' }" @click="settings.language = 'en'" title="British English">🇬🇧</button>
|
| 980 |
-
<button id="ctrl-lang-ru" :class="{ active: settings.language === 'ru' }" @click="settings.language = 'ru'" title="Русский">🇷🇺</button>
|
| 981 |
-
</div>
|
| 982 |
-
</div>
|
| 983 |
-
|
| 984 |
-
<!-- Universal Engineer Workspace -->
|
| 985 |
-
<template v-if="userRole === 'UNI_ENGINEER'">
|
| 986 |
-
<!-- Row 1: Time, Register, Install, Dismantle -->
|
| 987 |
-
<div class="deck-row">
|
| 988 |
-
<span id="biz-time-badge" class="badge">T = {{ state.currentTime }}</span>
|
| 989 |
-
<button id="biz-step-btn" @click="advanceTime">{{ t.stepBtn }}</button>
|
| 990 |
-
|
| 991 |
-
<div class="divider"></div>
|
| 992 |
-
|
| 993 |
-
<!-- Action 1: Register Part -->
|
| 994 |
-
<label style="font-size: 11px; font-weight: 700; color: #0284c7;">1. Register Part:</label>
|
| 995 |
-
<input id="biz-reg-id" type="text" v-model="bizRegId" placeholder="Part ID" style="width: 110px;">
|
| 996 |
-
<button id="biz-reg-btn" class="secondary" @click="handleBizRegister">Register</button>
|
| 997 |
-
|
| 998 |
-
<div class="divider"></div>
|
| 999 |
-
|
| 1000 |
-
<!-- Action 2: Install / Assemble -->
|
| 1001 |
-
<label style="font-size: 11px; font-weight: 700; color: #0284c7;">2. Install:</label>
|
| 1002 |
-
<select id="biz-inst-source" v-model="bizInstSource">
|
| 1003 |
-
<option v-for="node in bizInstallSources" :key="'src_' + node" :value="node">{{ node }}</option>
|
| 1004 |
-
</select>
|
| 1005 |
-
<select id="biz-inst-target" v-model="bizInstTarget">
|
| 1006 |
-
<option v-for="node in bizInstallTargets" :key="'tgt_' + node" :value="node">{{ node }}</option>
|
| 1007 |
-
</select>
|
| 1008 |
-
<button id="biz-inst-btn" class="secondary" @click="handleBizInstall">Install</button>
|
| 1009 |
|
| 1010 |
<div class="divider"></div>
|
| 1011 |
|
| 1012 |
-
<!--
|
| 1013 |
-
<
|
| 1014 |
-
|
| 1015 |
-
<
|
| 1016 |
-
</
|
| 1017 |
-
<
|
| 1018 |
-
|
| 1019 |
-
|
| 1020 |
-
|
| 1021 |
-
|
| 1022 |
-
<
|
| 1023 |
-
<label style="font-size: 11px; font-weight: 700; color: #0284c7;">3. Defect:</label>
|
| 1024 |
-
|
| 1025 |
-
<select id="biz-def-target" v-model="bizDefTarget">
|
| 1026 |
-
<option v-for="node in availableNodes" :key="'dn_' + node" :value="node">{{ node }}</option>
|
| 1027 |
-
</select>
|
| 1028 |
-
<input id="biz-def-type" type="text" v-model="bizDefType" placeholder="Type (e.g. crack)" style="width: 110px;">
|
| 1029 |
-
<input id="biz-def-val" type="text" v-model="bizDefVal" placeholder="Value (e.g. 2mm)" style="width: 80px;">
|
| 1030 |
-
<button id="biz-def-btn" class="secondary" @click="handleBizReportDefect">Report Defect</button>
|
| 1031 |
|
| 1032 |
<div class="divider"></div>
|
| 1033 |
|
| 1034 |
-
<
|
| 1035 |
-
<
|
| 1036 |
-
|
| 1037 |
-
<
|
| 1038 |
-
</
|
| 1039 |
-
<button id="biz-rev-btn" class="danger" @click="handleBizRevokeDefect">Revoke</button>
|
| 1040 |
|
| 1041 |
<div class="divider"></div>
|
| 1042 |
|
| 1043 |
-
<!--
|
| 1044 |
-
<
|
| 1045 |
-
<
|
| 1046 |
-
|
| 1047 |
-
</select>
|
| 1048 |
-
<select id="biz-store-target" v-model="bizStoreTarget">
|
| 1049 |
-
<option v-for="wh in activeWarehouses" :key="'sw_' + wh" :value="wh">{{ wh }}</option>
|
| 1050 |
-
</select>
|
| 1051 |
-
<button id="biz-store-btn" class="secondary" @click="handleBizStore">Store</button>
|
| 1052 |
|
| 1053 |
-
<div class="
|
| 1054 |
-
|
| 1055 |
-
|
| 1056 |
-
|
| 1057 |
-
|
| 1058 |
-
</label>
|
| 1059 |
</div>
|
| 1060 |
-
</template>
|
| 1061 |
-
</div>
|
| 1062 |
|
| 1063 |
-
|
| 1064 |
-
|
| 1065 |
-
|
| 1066 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1067 |
|
| 1068 |
-
|
| 1069 |
-
|
| 1070 |
-
|
| 1071 |
-
|
| 1072 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1073 |
|
| 1074 |
-
|
| 1075 |
-
|
| 1076 |
-
|
| 1077 |
-
<span>{{
|
| 1078 |
</div>
|
| 1079 |
-
|
| 1080 |
-
|
| 1081 |
-
|
| 1082 |
-
|
| 1083 |
-
<span>{{
|
| 1084 |
</div>
|
| 1085 |
-
|
| 1086 |
-
|
| 1087 |
-
|
| 1088 |
-
|
| 1089 |
-
|
| 1090 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1091 |
</div>
|
| 1092 |
-
</div>
|
| 1093 |
-
</div>
|
| 1094 |
-
</div>
|
| 1095 |
-
|
| 1096 |
-
<!-- Horizontal Resizer -->
|
| 1097 |
-
<div class="resizer" id="panel-resizer" @mousedown="startResizeH"></div>
|
| 1098 |
-
|
| 1099 |
-
<div class="sidebar" style="flex: 1;">
|
| 1100 |
-
<!-- BOM View (URMSM) -->
|
| 1101 |
-
<div class="card" :style="{ flex: \`0 0 \${layout.bomHeight}%\` }">
|
| 1102 |
-
<h4>
|
| 1103 |
-
<span>{{ t.titleBom }}</span>
|
| 1104 |
-
<span class="iec-badge align-right">IEC 63339 / URMSM<span class="iec-tooltip" v-html="t.tooltips.urmsm"></span></span>
|
| 1105 |
-
</h4>
|
| 1106 |
-
<div id="tree-container" style="overflow-y: auto;">
|
| 1107 |
-
<template v-if="bomTree">
|
| 1108 |
-
<strong>{{ bomTree.id }}</strong>
|
| 1109 |
-
<ul v-if="bomTree.children && bomTree.children.length > 0" class="tree">
|
| 1110 |
-
<bom-node v-for="child in bomTree.children" :key="child.id" :node="child" />
|
| 1111 |
-
</ul>
|
| 1112 |
-
</template>
|
| 1113 |
</div>
|
| 1114 |
</div>
|
| 1115 |
|
| 1116 |
-
<
|
| 1117 |
-
|
| 1118 |
-
|
| 1119 |
-
<div class="
|
| 1120 |
-
<
|
| 1121 |
-
|
| 1122 |
-
<
|
| 1123 |
-
|
| 1124 |
-
|
| 1125 |
-
<
|
| 1126 |
-
|
| 1127 |
-
|
| 1128 |
-
|
| 1129 |
-
<
|
| 1130 |
-
|
| 1131 |
-
|
| 1132 |
-
|
| 1133 |
-
|
| 1134 |
-
</
|
| 1135 |
-
<tbody>
|
| 1136 |
-
<tr v-for="row in lpgRecords" :key="row.id" :class="row.isActive ? 'active-row' : 'closed-row'">
|
| 1137 |
-
<td>{{ row.from }}</td>
|
| 1138 |
-
<td><strong>{{ row.to }}</strong></td>
|
| 1139 |
-
<td><code>:{{ row.label }}</code></td>
|
| 1140 |
-
<td>
|
| 1141 |
-
<code v-if="formatProperties(row.properties)">{ {{ formatProperties(row.properties) }} }</code>
|
| 1142 |
-
<span v-else>—</span>
|
| 1143 |
-
</td>
|
| 1144 |
-
<td>{{ row.isActive ? t.activeStatus : t.closedStatus }}</td>
|
| 1145 |
-
</tr>
|
| 1146 |
-
</tbody>
|
| 1147 |
-
</table>
|
| 1148 |
-
<div v-else-if="settings.storage === 'LPG'" style="color: #94a3b8; padding: 8px;">
|
| 1149 |
-
No relationship records
|
| 1150 |
</div>
|
| 1151 |
-
<pre v-else class="code-block">{{ rdfTtl }}</pre>
|
| 1152 |
</div>
|
| 1153 |
-
</div>
|
| 1154 |
|
| 1155 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1156 |
|
| 1157 |
-
|
| 1158 |
-
|
| 1159 |
-
<
|
| 1160 |
-
|
| 1161 |
-
<
|
| 1162 |
-
|
| 1163 |
-
|
| 1164 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1165 |
</div>
|
| 1166 |
</div>
|
| 1167 |
</div>
|
| 1168 |
-
</div>
|
| 1169 |
|
| 1170 |
-
|
| 1171 |
-
|
| 1172 |
-
|
| 1173 |
-
|
| 1174 |
-
|
| 1175 |
-
|
| 1176 |
-
|
| 1177 |
-
|
| 1178 |
-
|
| 1179 |
-
|
| 1180 |
-
|
| 1181 |
-
|
| 1182 |
-
|
| 1183 |
-
|
| 1184 |
-
|
| 1185 |
-
|
| 1186 |
-
>
|
| 1187 |
-
{{ currentSubtitleText }}
|
| 1188 |
-
</div>
|
| 1189 |
-
|
| 1190 |
-
<!-- Floating Scenario Player Panel -->
|
| 1191 |
-
<div
|
| 1192 |
-
v-if="state.currentView === 'SCENARIO'"
|
| 1193 |
-
class="scenario-player-panel"
|
| 1194 |
-
>
|
| 1195 |
-
<button
|
| 1196 |
-
class="player-btn"
|
| 1197 |
-
@click="togglePlayAudio"
|
| 1198 |
-
:title="isPlaying ? 'Pause' : 'Play'"
|
| 1199 |
-
:aria-label="isPlaying ? 'Pause' : 'Play'"
|
| 1200 |
>
|
| 1201 |
-
|
| 1202 |
-
|
| 1203 |
-
</button>
|
| 1204 |
|
| 1205 |
-
<
|
| 1206 |
-
|
| 1207 |
-
|
| 1208 |
-
|
| 1209 |
-
aria-label="Stop"
|
| 1210 |
>
|
| 1211 |
-
<
|
| 1212 |
-
|
| 1213 |
-
|
| 1214 |
-
|
| 1215 |
-
|
| 1216 |
-
|
| 1217 |
-
|
| 1218 |
-
|
| 1219 |
-
|
| 1220 |
-
|
| 1221 |
-
|
| 1222 |
-
|
| 1223 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1224 |
|
| 1225 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1226 |
|
|
|
|
| 1227 |
<button
|
| 1228 |
-
|
| 1229 |
-
|
| 1230 |
-
@click="
|
| 1231 |
-
title="
|
| 1232 |
-
aria-label="
|
| 1233 |
>
|
| 1234 |
-
|
|
|
|
|
|
|
|
|
|
| 1235 |
</button>
|
| 1236 |
</div>
|
| 1237 |
-
|
| 1238 |
-
<!-- Floating Home Button -->
|
| 1239 |
-
<button
|
| 1240 |
-
v-if="state.currentView !== 'MENU'"
|
| 1241 |
-
class="floating-home-btn"
|
| 1242 |
-
@click="goHome"
|
| 1243 |
-
title="Return to Main Menu (Resets Sandbox)"
|
| 1244 |
-
aria-label="Return to Main Menu"
|
| 1245 |
-
>
|
| 1246 |
-
<svg viewBox="0 0 24 24" width="22" height="22" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
| 1247 |
-
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path>
|
| 1248 |
-
<polyline points="9 22 9 12 15 12 15 22"></polyline>
|
| 1249 |
-
</svg>
|
| 1250 |
-
</button>
|
| 1251 |
`
|
| 1252 |
};
|
| 1253 |
|
|
|
|
| 1 |
/**
|
| 2 |
* APPLICATION ORCHESTRATOR (Vue 3)
|
| 3 |
* --------------------------------
|
| 4 |
+
* Binds the single reactive state to the DOM, handles user inputs, and routes
|
| 5 |
+
* data between the Event Log, Domain Actions, Ontologies (SMIM), Storage (SMS),
|
| 6 |
+
* Viewpoints (URMSM), and Renderers.
|
| 7 |
*/
|
| 8 |
|
| 9 |
import { createApp, computed, watch, onMounted, ref, reactive, nextTick } from 'vue';
|
| 10 |
+
import { state, advanceTime, loadState, setPerspective, nextSequence } from './state.js';
|
| 11 |
import { i18n } from './i18n.js';
|
| 12 |
+
import {
|
| 13 |
+
createRegisterPartEvent,
|
| 14 |
+
createAssembleEvent,
|
| 15 |
+
createReportDefectEvent,
|
| 16 |
+
createRevokeDefectEvent,
|
| 17 |
+
createDismantleEvent
|
| 18 |
} from './smrm/actions.js';
|
| 19 |
+
import { ENTITY_CLASSES, RELATION_TYPES, INTERNAL_PROPERTY_KEYS, inferEntityClass } from './smrm/definitions.js';
|
| 20 |
import * as bfoAdapter from './smim/bfo.js';
|
| 21 |
import * as isoAdapter from './smim/iso15926.js';
|
| 22 |
import * as naiveAdapter from './smim/naive.js';
|
| 23 |
import { initVisNetwork, updateVisNetwork } from './renderers/vis-adapter.js';
|
| 24 |
import { updateMermaidNetwork } from './renderers/mermaid-adapter.js';
|
| 25 |
+
import { generateBomTrees } from './urmsm/bom-view.js';
|
| 26 |
import { reduceGraphForSMRM } from './urmsm/smrm-view.js';
|
| 27 |
+
import {
|
| 28 |
+
generateLpgNodeStorage,
|
| 29 |
+
generateLpgStorage,
|
| 30 |
+
generateLpgQuery,
|
| 31 |
+
evaluateLiveQuery
|
| 32 |
+
} from './sms/lpg-generator.js';
|
| 33 |
import { generateRdfStorage, generateRdfQuery } from './sms/rdf-generator.js';
|
| 34 |
import { generatePhysicalGraph } from './sms/sms-graph-generator.js';
|
| 35 |
import {
|
|
|
|
| 41 |
/**
|
| 42 |
* RECURSIVE BOM TREE NODE COMPONENT
|
| 43 |
* ---------------------------------
|
| 44 |
+
* Renders hierarchical Bill of Materials nodes with entity class badges
|
| 45 |
+
* and surviving defect indicators at time T.
|
| 46 |
*/
|
| 47 |
const BomNode = {
|
| 48 |
name: 'BomNode',
|
|
|
|
| 54 |
},
|
| 55 |
template: `
|
| 56 |
<li>
|
| 57 |
+
<span class="bom-node-row">
|
| 58 |
+
<strong>{{ node.id }}</strong>
|
| 59 |
+
<span class="bom-class-badge">{{ node.entityClass }}</span>
|
| 60 |
+
<span
|
| 61 |
+
v-for="def in (node.defects || [])"
|
| 62 |
+
:key="def"
|
| 63 |
+
class="bom-defect-badge"
|
| 64 |
+
>⚠️ {{ def }}</span>
|
| 65 |
+
</span>
|
| 66 |
<ul v-if="node.children && node.children.length > 0" class="tree">
|
| 67 |
<bom-node
|
| 68 |
v-for="child in node.children"
|
|
|
|
| 79 |
BomNode
|
| 80 |
},
|
| 81 |
setup() {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
// DOM References for renderers & file operations
|
| 83 |
const visContainer = ref(null);
|
| 84 |
const mermaidContainer = ref(null);
|
| 85 |
const fileInput = ref(null);
|
| 86 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
// Universal Engineer Business UI inputs
|
| 88 |
+
const bizRegType = ref('Part');
|
| 89 |
const bizRegId = ref('');
|
| 90 |
const bizInstSource = ref('');
|
| 91 |
const bizInstTarget = ref('');
|
|
|
|
| 104 |
const audioDuration = ref(0);
|
| 105 |
const showSubtitles = ref(true);
|
| 106 |
const currentSubtitleText = ref('');
|
|
|
|
| 107 |
|
| 108 |
// Layout State (Flex percentages for resizers)
|
| 109 |
+
const layout = reactive({ leftWidth: 64, bomHeight: 24, storageHeight: 42 });
|
|
|
|
|
|
|
|
|
|
| 110 |
|
| 111 |
+
// Active dictionary based on state.language
|
| 112 |
+
const t = computed(() => i18n[state.language] || i18n.en);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
|
| 114 |
+
// Reconstruct base registered entities up to state.currentTime
|
| 115 |
const baseNodes = computed(() => {
|
| 116 |
const nodeMap = new Map();
|
| 117 |
|
|
|
|
| 119 |
if (event.valid_from !== undefined && event.valid_from > state.currentTime) return;
|
| 120 |
|
| 121 |
if (event.type === 'REGISTER_PART' && event.partId) {
|
| 122 |
+
const cls = inferEntityClass(event.partId, event.entityClass);
|
| 123 |
nodeMap.set(event.partId, {
|
| 124 |
id: event.partId,
|
| 125 |
label: event.partId,
|
| 126 |
+
type: cls,
|
| 127 |
+
entityClass: cls
|
| 128 |
});
|
| 129 |
} else if (event.type === 'ASSEMBLE') {
|
| 130 |
if (event.parentId && !nodeMap.has(event.parentId)) {
|
| 131 |
+
const pCls = inferEntityClass(event.parentId);
|
| 132 |
nodeMap.set(event.parentId, {
|
| 133 |
id: event.parentId,
|
| 134 |
label: event.parentId,
|
| 135 |
+
type: pCls,
|
| 136 |
+
entityClass: pCls
|
| 137 |
});
|
| 138 |
}
|
| 139 |
if (event.childId && !nodeMap.has(event.childId)) {
|
| 140 |
+
const cCls = inferEntityClass(event.childId);
|
| 141 |
nodeMap.set(event.childId, {
|
| 142 |
id: event.childId,
|
| 143 |
label: event.childId,
|
| 144 |
+
type: cCls,
|
| 145 |
+
entityClass: cCls
|
| 146 |
});
|
| 147 |
}
|
| 148 |
}
|
|
|
|
| 151 |
return Array.from(nodeMap.values());
|
| 152 |
});
|
| 153 |
|
| 154 |
+
const availableNodes = computed(() => baseNodes.value.map(n => n.id));
|
|
|
|
|
|
|
| 155 |
|
| 156 |
+
const activeParts = computed(() =>
|
| 157 |
+
baseNodes.value.filter(n => n.entityClass !== 'Slot' && n.entityClass !== 'Warehouse').map(n => n.id)
|
| 158 |
+
);
|
| 159 |
|
| 160 |
+
const activeWarehouses = computed(() =>
|
| 161 |
+
baseNodes.value.filter(n => n.entityClass === 'Warehouse').map(n => n.id)
|
| 162 |
+
);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 163 |
|
| 164 |
+
const bizInstallSources = computed(() =>
|
| 165 |
+
baseNodes.value.filter(n => n.entityClass === 'Part').map(n => n.id)
|
| 166 |
+
);
|
|
|
|
| 167 |
|
| 168 |
+
const bizInstallTargets = computed(() =>
|
| 169 |
+
baseNodes.value.filter(n => n.entityClass === 'Vehicle' || n.entityClass === 'Part').map(n => n.id)
|
| 170 |
+
);
|
| 171 |
+
|
| 172 |
+
/**
|
| 173 |
+
* Central Event Dispatcher (Pure Event Sourcing)
|
| 174 |
+
* Appends immutable business facts to the event log.
|
| 175 |
+
*/
|
| 176 |
+
function dispatch(event) {
|
| 177 |
+
if (!event) return;
|
| 178 |
+
state.eventLog.push(event);
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
// Core Transformation Pipeline: Event Log -> SMIM Canonical Semantic Graph
|
| 182 |
+
const semanticGraph = computed(() => {
|
| 183 |
+
const opts = {
|
| 184 |
+
ontology: state.ontology,
|
| 185 |
+
showHistory: state.showHistory
|
| 186 |
+
};
|
| 187 |
|
| 188 |
+
if (state.ontology === 'BFO') {
|
| 189 |
+
return bfoAdapter.generateSemanticGraph(state.eventLog, baseNodes.value, state.currentTime, opts);
|
| 190 |
+
}
|
| 191 |
+
if (state.ontology === 'ISO15926') {
|
| 192 |
+
return isoAdapter.generateSemanticGraph(state.eventLog, baseNodes.value, state.currentTime, opts);
|
| 193 |
+
}
|
| 194 |
+
return naiveAdapter.generateSemanticGraph(state.eventLog, baseNodes.value, state.currentTime, opts);
|
| 195 |
});
|
| 196 |
|
| 197 |
+
// Engineer SMRM reduced graph (used for BOM, SMRM view, and installed component discovery)
|
| 198 |
+
const smrmReducedGraph = computed(() => reduceGraphForSMRM(semanticGraph.value));
|
| 199 |
+
|
| 200 |
const installedComponents = computed(() => {
|
| 201 |
const comps = new Set();
|
| 202 |
+
smrmReducedGraph.value.edges.forEach(e => {
|
| 203 |
+
if (e.properties?.isCurrent === false) return;
|
| 204 |
+
if (e.label === 'has_part') {
|
| 205 |
+
comps.add(e.to);
|
| 206 |
+
} else if (e.label === 'mounted_in' || e.label === 'stored_at') {
|
| 207 |
+
comps.add(e.from);
|
| 208 |
}
|
| 209 |
});
|
| 210 |
return Array.from(comps).filter(id => !id.startsWith('Slot_'));
|
| 211 |
});
|
| 212 |
|
| 213 |
+
// Active Defects List (respecting REVOKE_DEFECT events)
|
| 214 |
const activeDefectsList = computed(() => {
|
| 215 |
+
const activeMap = new Map();
|
| 216 |
+
|
| 217 |
+
state.eventLog.forEach((e, index) => {
|
| 218 |
+
if (e.valid_from !== undefined && e.valid_from > state.currentTime) return;
|
| 219 |
+
|
| 220 |
+
if (e.type === 'REPORT_DEFECT') {
|
| 221 |
+
if (e.valid_to !== null && e.valid_to !== undefined && e.valid_to <= state.currentTime) return;
|
| 222 |
+
const key = `${e.targetId}::${e.defectName}`;
|
| 223 |
+
activeMap.set(key, {
|
| 224 |
+
id: e.id || `def_${index}_${e.targetId}_${e.defectName}`,
|
| 225 |
+
logIndex: index,
|
| 226 |
targetId: e.targetId,
|
| 227 |
defectName: e.defectName,
|
| 228 |
defectValue: e.defectValue,
|
| 229 |
label: `${e.targetId} • ${e.defectName}=${e.defectValue}`
|
| 230 |
+
});
|
| 231 |
+
} else if (e.type === 'REVOKE_DEFECT') {
|
| 232 |
+
const key = `${e.targetId}::${e.defectName}`;
|
| 233 |
+
activeMap.delete(key);
|
| 234 |
+
}
|
| 235 |
+
});
|
| 236 |
+
|
| 237 |
+
return Array.from(activeMap.values());
|
| 238 |
});
|
| 239 |
|
| 240 |
+
// Keep select dropdown defaults synchronised with available options
|
| 241 |
watch(bizInstallSources, (sources) => {
|
| 242 |
+
if (!sources.includes(bizInstSource.value)) bizInstSource.value = sources[0] || '';
|
|
|
|
|
|
|
| 243 |
}, { immediate: true });
|
| 244 |
|
| 245 |
watch(bizInstallTargets, (targets) => {
|
| 246 |
+
if (!targets.includes(bizInstTarget.value)) bizInstTarget.value = targets[0] || '';
|
|
|
|
|
|
|
| 247 |
}, { immediate: true });
|
| 248 |
|
| 249 |
watch(activeParts, (parts) => {
|
| 250 |
+
if (!parts.includes(bizStorePart.value)) bizStorePart.value = parts[0] || '';
|
|
|
|
|
|
|
| 251 |
}, { immediate: true });
|
| 252 |
|
| 253 |
watch(activeWarehouses, (warehouses) => {
|
| 254 |
+
if (!warehouses.includes(bizStoreTarget.value)) bizStoreTarget.value = warehouses[0] || '';
|
|
|
|
|
|
|
| 255 |
}, { immediate: true });
|
| 256 |
|
| 257 |
watch(installedComponents, (comps) => {
|
| 258 |
+
if (!comps.includes(bizDismPart.value)) bizDismPart.value = comps[0] || '';
|
|
|
|
|
|
|
| 259 |
}, { immediate: true });
|
| 260 |
|
| 261 |
watch(activeDefectsList, (defects) => {
|
| 262 |
+
if (!defects.some(d => d.id === bizRevDefect.value)) bizRevDefect.value = defects[0]?.id || '';
|
|
|
|
|
|
|
| 263 |
}, { immediate: true });
|
| 264 |
|
| 265 |
watch(availableNodes, (nodes) => {
|
| 266 |
+
if (!nodes.includes(bizDefTarget.value)) bizDefTarget.value = nodes[0] || '';
|
|
|
|
|
|
|
| 267 |
}, { immediate: true });
|
| 268 |
|
| 269 |
+
// Derived Views for Sidebar (URMSM & SMS)
|
| 270 |
+
const bomTrees = computed(() => generateBomTrees(semanticGraph.value));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 271 |
|
| 272 |
+
const lpgNodeRecords = computed(() => {
|
| 273 |
+
if (state.storage === 'LPG') {
|
| 274 |
+
return generateLpgNodeStorage(semanticGraph.value);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 275 |
}
|
| 276 |
+
return [];
|
| 277 |
});
|
| 278 |
|
|
|
|
|
|
|
|
|
|
| 279 |
const lpgRecords = computed(() => {
|
| 280 |
+
if (state.storage === 'LPG') {
|
| 281 |
return generateLpgStorage(semanticGraph.value);
|
| 282 |
}
|
| 283 |
return [];
|
| 284 |
});
|
| 285 |
|
| 286 |
const rdfTtl = computed(() => {
|
| 287 |
+
if (state.storage === 'RDF') {
|
| 288 |
return generateRdfStorage(semanticGraph.value);
|
| 289 |
}
|
| 290 |
return '';
|
|
|
|
| 292 |
|
| 293 |
function formatProperties(props) {
|
| 294 |
if (!props) return '';
|
| 295 |
+
const entries = Object.entries(props).filter(
|
| 296 |
+
([key, val]) => !INTERNAL_PROPERTY_KEYS.includes(key) || key === 'validFrom' || key === 'validTo' || key === 'atTime'
|
| 297 |
+
? (key !== 'isCurrent' && val !== undefined)
|
| 298 |
+
: false
|
| 299 |
+
);
|
| 300 |
if (entries.length === 0) return '';
|
| 301 |
return entries.map(([k, v]) => {
|
| 302 |
const valStr = v === null ? 'null' : (typeof v === 'number' ? `T${v}` : `'${v}'`);
|
|
|
|
| 305 |
}
|
| 306 |
|
| 307 |
const queryCode = computed(() => {
|
| 308 |
+
if (state.storage === 'LPG') {
|
| 309 |
+
return generateLpgQuery(semanticGraph.value, state.currentTime, state.ontology);
|
|
|
|
|
|
|
| 310 |
}
|
| 311 |
+
return generateRdfQuery(semanticGraph.value, state.currentTime, state.ontology);
|
| 312 |
});
|
| 313 |
|
| 314 |
+
const liveQueryResult = computed(() =>
|
| 315 |
+
evaluateLiveQuery(semanticGraph.value, state.currentTime, state.ontology)
|
| 316 |
+
);
|
| 317 |
+
|
| 318 |
+
// Route active graph to canvas based on the 4 unified Perspectives
|
| 319 |
const activeGraph = computed(() => {
|
| 320 |
+
if (state.perspective === 'SMS_LPG') {
|
| 321 |
+
return generatePhysicalGraph(semanticGraph.value, 'LPG');
|
| 322 |
+
}
|
| 323 |
+
if (state.perspective === 'SMS_RDF') {
|
| 324 |
+
return generatePhysicalGraph(semanticGraph.value, 'RDF');
|
| 325 |
}
|
| 326 |
+
if (state.perspective === 'SMRM') {
|
| 327 |
+
return smrmReducedGraph.value;
|
| 328 |
}
|
| 329 |
return semanticGraph.value;
|
| 330 |
});
|
| 331 |
|
| 332 |
+
// Reactive Graph Caption (truthful to the active perspective)
|
| 333 |
const graphCaption = computed(() => {
|
| 334 |
let ontoText = 'BFO (3D)';
|
| 335 |
+
if (state.ontology === 'ISO15926') ontoText = 'ISO 15926 (4D)';
|
| 336 |
+
if (state.ontology === 'NAIVE_NODE') ontoText = t.value.ontoNaiveNode;
|
| 337 |
+
if (state.ontology === 'NAIVE_EDGE') ontoText = t.value.ontoNaiveEdge;
|
| 338 |
+
|
| 339 |
let viewText = t.value.viewLogicalCaption;
|
| 340 |
+
if (state.perspective === 'SMRM') viewText = t.value.viewSmrmCaption;
|
| 341 |
+
if (state.perspective === 'SMS_LPG') viewText = t.value.viewPhysicalLpgCaption;
|
| 342 |
+
if (state.perspective === 'SMS_RDF') viewText = t.value.viewPhysicalRdfCaption;
|
| 343 |
+
|
| 344 |
+
return `${ontoText} | ${viewText}`;
|
| 345 |
});
|
| 346 |
|
| 347 |
+
// Contextual Dynamic Legend matching the rendered canvas
|
| 348 |
const graphLegendItems = computed(() => {
|
| 349 |
const nodes = [];
|
| 350 |
const edges = [];
|
| 351 |
const dict = t.value.legend;
|
| 352 |
|
| 353 |
+
// In Engineer (SMRM) Perspective or Naive ontologies, domain entities are shown
|
| 354 |
+
if (state.perspective === 'SMRM' || state.ontology === 'NAIVE_NODE' || state.ontology === 'NAIVE_EDGE') {
|
| 355 |
nodes.push({ id: 'naiveVehicle', label: dict.naiveVehicle, swatch: 'swatch-box-vehicle' });
|
| 356 |
+
if (state.perspective !== 'SMRM') {
|
| 357 |
+
nodes.push({ id: 'naiveSlot', label: dict.naiveSlot, swatch: 'swatch-box-slot' });
|
| 358 |
+
}
|
| 359 |
nodes.push({ id: 'naivePart', label: dict.naivePart, swatch: 'swatch-box-part' });
|
| 360 |
nodes.push({ id: 'naiveDefect', label: dict.naiveDefect, swatch: 'swatch-diamond-defect' });
|
| 361 |
nodes.push({ id: 'naiveWarehouse', label: dict.naiveWarehouse, swatch: 'swatch-box-warehouse' });
|
| 362 |
|
| 363 |
+
if (state.perspective === 'SMS_RDF') {
|
| 364 |
+
nodes.push({ id: 'rdfStmt', label: dict.rdfStmt, swatch: 'swatch-diamond' });
|
| 365 |
+
nodes.push({ id: 'rdfLit', label: dict.rdfLit, swatch: 'swatch-literal' });
|
| 366 |
+
edges.push({ id: 'rdfPred', label: dict.rdfPred, swatch: 'swatch-line-past' });
|
| 367 |
+
} else {
|
| 368 |
+
if (state.perspective !== 'SMRM') {
|
| 369 |
+
edges.push({ id: 'relHasSlot', label: dict.relHasSlot, swatch: 'swatch-line-slot' });
|
| 370 |
+
}
|
| 371 |
+
edges.push({ id: 'relMountedIn', label: dict.relMountedIn, swatch: 'swatch-line-mount' });
|
| 372 |
+
edges.push({ id: 'relStoredAt', label: dict.relStoredAt, swatch: 'swatch-line-store' });
|
| 373 |
+
edges.push({ id: 'relHasDefect', label: dict.relHasDefect, swatch: 'swatch-line-defect' });
|
| 374 |
+
}
|
| 375 |
+
} else if (state.ontology === 'BFO') {
|
| 376 |
nodes.push({ id: 'bfoSys', label: dict.bfoSys, swatch: 'swatch-box-sys' });
|
| 377 |
nodes.push({ id: 'bfoElem', label: dict.bfoElem, swatch: 'swatch-box-elem' });
|
| 378 |
nodes.push({ id: 'bfoQuality', label: dict.bfoQuality, swatch: 'swatch-diamond' });
|
| 379 |
|
| 380 |
+
if (state.perspective === 'SMS_RDF') {
|
| 381 |
nodes.push({ id: 'rdfStmt', label: dict.rdfStmt, swatch: 'swatch-diamond' });
|
| 382 |
nodes.push({ id: 'rdfLit', label: dict.rdfLit, swatch: 'swatch-literal' });
|
| 383 |
edges.push({ id: 'rdfPred', label: dict.rdfPred, swatch: 'swatch-line-past' });
|
| 384 |
} else {
|
| 385 |
edges.push({ id: 'bfoActive', label: dict.bfoActiveEdge, swatch: 'swatch-line-active' });
|
| 386 |
edges.push({ id: 'bfoInheres', label: dict.bfoInheres, swatch: 'swatch-line-transition' });
|
| 387 |
+
if (state.showHistory) {
|
| 388 |
edges.push({ id: 'bfoPast', label: dict.bfoPastEdge, swatch: 'swatch-line-past' });
|
| 389 |
}
|
| 390 |
}
|
| 391 |
} else {
|
| 392 |
+
// ISO 15926 (4D) in SMIM / SMS_LPG / SMS_RDF
|
| 393 |
nodes.push({ id: 'iso4d', label: dict.iso4d, swatch: 'swatch-box-4d' });
|
| 394 |
nodes.push({ id: 'isoState', label: dict.isoState, swatch: 'swatch-box-state' });
|
| 395 |
nodes.push({ id: 'isoDefect', label: dict.isoDefect, swatch: 'swatch-diamond-defect' });
|
| 396 |
+
if (state.perspective === 'SMS_RDF') {
|
| 397 |
+
nodes.push({ id: 'rdfLit', label: dict.rdfLit, swatch: 'swatch-literal' });
|
| 398 |
+
}
|
| 399 |
edges.push({ id: 'isoPart', label: dict.isoPartRel, swatch: 'swatch-line-active' });
|
| 400 |
edges.push({ id: 'isoSlice', label: dict.isoSliceRel, swatch: 'swatch-line-past' });
|
| 401 |
edges.push({ id: 'isoDefectRel', label: dict.isoDefectRel, swatch: 'swatch-line-defect' });
|
| 402 |
|
| 403 |
+
if (state.perspective === 'SMS_LPG') {
|
| 404 |
edges.push({ id: 'isoTrans', label: dict.isoTransitionRel, swatch: 'swatch-line-transition' });
|
| 405 |
}
|
| 406 |
}
|
|
|
|
| 408 |
return { nodes, edges };
|
| 409 |
});
|
| 410 |
|
| 411 |
+
// Re-render canvas when activeGraph or engine changes
|
| 412 |
+
watch([activeGraph, () => state.graphEngine], () => {
|
| 413 |
renderGraph();
|
| 414 |
}, { deep: true });
|
| 415 |
|
| 416 |
function renderGraph() {
|
| 417 |
if (state.currentView === 'MENU') return;
|
| 418 |
const graphToRender = activeGraph.value;
|
| 419 |
+
if (state.graphEngine === 'VIS') {
|
| 420 |
+
const isComplex = state.ontology === 'ISO15926' || state.perspective.startsWith('SMS_');
|
| 421 |
updateVisNetwork(graphToRender, isComplex);
|
| 422 |
} else if (mermaidContainer.value) {
|
| 423 |
updateMermaidNetwork(graphToRender, mermaidContainer.value);
|
| 424 |
}
|
| 425 |
}
|
| 426 |
|
| 427 |
+
function seedInitialSandboxData() {
|
| 428 |
+
state.eventLog.splice(0, state.eventLog.length);
|
| 429 |
+
state.currentTime = 0;
|
| 430 |
+
state.edgeSequence = 1;
|
| 431 |
+
dispatch(createRegisterPartEvent('System_A', 0, 'Vehicle', nextSequence()));
|
| 432 |
+
dispatch(createRegisterPartEvent('SubAssembly_1', 0, 'Part', nextSequence()));
|
| 433 |
+
dispatch(createAssembleEvent('System_A', 'SubAssembly_1', 'mounted_in', 0, nextSequence()));
|
| 434 |
+
dispatch(createRegisterPartEvent('Part_A', 0, 'Part', nextSequence()));
|
| 435 |
+
dispatch(createAssembleEvent('System_A', 'Part_A', 'mounted_in', 0, nextSequence()));
|
| 436 |
+
dispatch(createRegisterPartEvent('Sensor_X', 0, 'Part', nextSequence()));
|
| 437 |
+
dispatch(createAssembleEvent('SubAssembly_1', 'Sensor_X', 'mounted_in', 0, nextSequence()));
|
| 438 |
+
dispatch(createRegisterPartEvent('Warehouse_1', 0, 'Warehouse', nextSequence()));
|
| 439 |
+
}
|
| 440 |
+
|
| 441 |
+
// Watch for view transitions (MENU vs SANDBOX vs SCENARIO)
|
| 442 |
watch(() => state.currentView, async (newView) => {
|
| 443 |
if (newView !== 'SCENARIO') {
|
| 444 |
+
stopAudio(false);
|
| 445 |
}
|
| 446 |
if (newView === 'SCENARIO') {
|
| 447 |
+
state.userRole = 'UNI_ENGINEER';
|
| 448 |
+
state.eventLog.splice(0, state.eventLog.length);
|
| 449 |
resetTimeline(0);
|
| 450 |
state.currentTime = 0;
|
| 451 |
state.ontology = 'NAIVE_NODE';
|
| 452 |
+
setPerspective('SMIM');
|
| 453 |
state.graphEngine = 'MERMAID';
|
| 454 |
+
} else if (newView === 'SANDBOX' && state.eventLog.length === 0) {
|
| 455 |
+
seedInitialSandboxData();
|
| 456 |
}
|
| 457 |
if (newView !== 'MENU') {
|
| 458 |
await nextTick();
|
|
|
|
| 463 |
}
|
| 464 |
});
|
| 465 |
|
|
|
|
| 466 |
onMounted(() => {
|
| 467 |
+
seedInitialSandboxData();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 468 |
if (state.currentView !== 'MENU' && visContainer.value) {
|
| 469 |
initVisNetwork(visContainer.value);
|
| 470 |
renderGraph();
|
| 471 |
}
|
| 472 |
});
|
| 473 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 474 |
function goHome() {
|
| 475 |
+
stopAudio(false);
|
| 476 |
state.currentView = 'MENU';
|
| 477 |
state.eventLog.splice(0, state.eventLog.length);
|
| 478 |
state.currentTime = 0;
|
| 479 |
state.edgeSequence = 1;
|
| 480 |
}
|
| 481 |
|
|
|
|
|
|
|
|
|
|
| 482 |
function playAudio() {
|
| 483 |
if (scenarioAudio.value) {
|
| 484 |
scenarioAudio.value.play().catch(err => {
|
|
|
|
| 487 |
}
|
| 488 |
}
|
| 489 |
|
|
|
|
|
|
|
|
|
|
| 490 |
function pauseAudio() {
|
| 491 |
if (scenarioAudio.value) {
|
| 492 |
scenarioAudio.value.pause();
|
| 493 |
}
|
| 494 |
}
|
| 495 |
|
|
|
|
|
|
|
|
|
|
| 496 |
function togglePlayAudio() {
|
| 497 |
if (isPlaying.value) {
|
| 498 |
pauseAudio();
|
|
|
|
| 501 |
}
|
| 502 |
}
|
| 503 |
|
| 504 |
+
function stopAudio(resetScenarioGraph = true) {
|
|
|
|
|
|
|
|
|
|
| 505 |
if (scenarioAudio.value) {
|
| 506 |
scenarioAudio.value.pause();
|
| 507 |
scenarioAudio.value.currentTime = 0;
|
|
|
|
| 510 |
audioCurrentTime.value = 0;
|
| 511 |
currentSubtitleText.value = '';
|
| 512 |
|
| 513 |
+
if (resetScenarioGraph) {
|
| 514 |
+
state.eventLog.splice(0, state.eventLog.length);
|
| 515 |
+
resetTimeline(0);
|
| 516 |
+
state.currentTime = 0;
|
| 517 |
+
state.ontology = 'NAIVE_NODE';
|
| 518 |
+
setPerspective('SMIM');
|
| 519 |
+
state.graphEngine = 'MERMAID';
|
| 520 |
+
nextTick(() => {
|
| 521 |
+
renderGraph();
|
| 522 |
+
});
|
| 523 |
+
}
|
| 524 |
}
|
| 525 |
|
|
|
|
|
|
|
|
|
|
| 526 |
function onTimeUpdate() {
|
| 527 |
if (scenarioAudio.value) {
|
| 528 |
audioCurrentTime.value = scenarioAudio.value.currentTime;
|
| 529 |
}
|
|
|
|
| 530 |
const currentTime = audioCurrentTime.value;
|
| 531 |
|
|
|
|
| 532 |
const activeSubtitle = vanishingCrackScenario.subtitles.find(
|
| 533 |
sub => currentTime >= sub.start && currentTime <= sub.end
|
| 534 |
);
|
| 535 |
currentSubtitleText.value = activeSubtitle ? activeSubtitle.text : '';
|
| 536 |
|
|
|
|
| 537 |
dispatchScenarioTimeline(currentTime, state, renderGraph);
|
| 538 |
}
|
| 539 |
|
|
|
|
|
|
|
|
|
|
| 540 |
function onLoadedMetadata() {
|
| 541 |
if (scenarioAudio.value) {
|
| 542 |
audioDuration.value = scenarioAudio.value.duration || 0;
|
| 543 |
}
|
| 544 |
}
|
| 545 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 546 |
function onAudioEnded() {
|
| 547 |
isPlaying.value = false;
|
| 548 |
currentSubtitleText.value = '';
|
| 549 |
}
|
| 550 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 551 |
function onSeek(event) {
|
| 552 |
const targetTime = parseFloat(event.target.value);
|
| 553 |
if (scenarioAudio.value) {
|
|
|
|
| 555 |
}
|
| 556 |
audioCurrentTime.value = targetTime;
|
| 557 |
|
| 558 |
+
state.eventLog.splice(0, state.eventLog.length);
|
|
|
|
| 559 |
resetTimeline(0);
|
| 560 |
+
state.currentTime = 0;
|
| 561 |
state.ontology = 'NAIVE_NODE';
|
| 562 |
+
setPerspective('SMIM');
|
| 563 |
state.graphEngine = 'MERMAID';
|
| 564 |
|
|
|
|
| 565 |
onTimeUpdate();
|
| 566 |
}
|
| 567 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 568 |
function formatTime(seconds) {
|
| 569 |
if (!seconds || isNaN(seconds) || seconds < 0) return '00:00';
|
| 570 |
const mins = Math.floor(seconds / 60);
|
|
|
|
| 572 |
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
|
| 573 |
}
|
| 574 |
|
| 575 |
+
// Engineer Domain Actions
|
|
|
|
|
|
|
| 576 |
function handleBizRegister() {
|
| 577 |
if (!bizRegId.value) return;
|
| 578 |
const event = createRegisterPartEvent(
|
| 579 |
bizRegId.value,
|
| 580 |
+
state.currentTime,
|
| 581 |
+
bizRegType.value,
|
| 582 |
+
nextSequence()
|
| 583 |
);
|
| 584 |
if (event) {
|
| 585 |
dispatch(event);
|
|
|
|
| 587 |
}
|
| 588 |
}
|
| 589 |
|
|
|
|
|
|
|
|
|
|
| 590 |
function handleBizInstall() {
|
| 591 |
if (!bizInstSource.value || !bizInstTarget.value) return;
|
| 592 |
+
if (bizInstSource.value === bizInstTarget.value) return;
|
|
|
|
|
|
|
| 593 |
|
| 594 |
const event = createAssembleEvent(
|
| 595 |
+
bizInstTarget.value,
|
| 596 |
+
bizInstSource.value,
|
| 597 |
'mounted_in',
|
| 598 |
+
state.currentTime,
|
| 599 |
+
nextSequence()
|
| 600 |
);
|
| 601 |
+
if (event) dispatch(event);
|
|
|
|
|
|
|
| 602 |
}
|
| 603 |
|
|
|
|
|
|
|
|
|
|
| 604 |
function handleBizReportDefect() {
|
| 605 |
if (!bizDefTarget.value || !bizDefType.value) return;
|
| 606 |
const event = createReportDefectEvent(
|
| 607 |
bizDefTarget.value,
|
| 608 |
bizDefType.value,
|
| 609 |
bizDefVal.value || 'present',
|
| 610 |
+
state.currentTime,
|
| 611 |
+
nextSequence()
|
| 612 |
);
|
| 613 |
if (event) {
|
| 614 |
dispatch(event);
|
|
|
|
| 617 |
}
|
| 618 |
}
|
| 619 |
|
|
|
|
|
|
|
|
|
|
| 620 |
function handleBizRevokeDefect() {
|
| 621 |
if (!bizRevDefect.value) return;
|
| 622 |
const item = activeDefectsList.value.find(d => d.id === bizRevDefect.value);
|
| 623 |
+
if (item) {
|
| 624 |
+
const event = createRevokeDefectEvent(
|
| 625 |
+
item.targetId,
|
| 626 |
+
item.defectName,
|
| 627 |
+
state.currentTime,
|
| 628 |
+
item.id,
|
| 629 |
+
nextSequence()
|
| 630 |
+
);
|
| 631 |
+
if (event) {
|
| 632 |
+
dispatch(event);
|
| 633 |
+
bizRevDefect.value = '';
|
| 634 |
+
}
|
| 635 |
}
|
| 636 |
}
|
| 637 |
|
|
|
|
|
|
|
|
|
|
| 638 |
function handleBizDismantle() {
|
| 639 |
if (!bizDismPart.value) return;
|
| 640 |
const event = createDismantleEvent(
|
| 641 |
bizDismPart.value,
|
| 642 |
+
state.currentTime,
|
| 643 |
+
nextSequence()
|
| 644 |
);
|
| 645 |
if (event) {
|
| 646 |
dispatch(event);
|
|
|
|
| 648 |
}
|
| 649 |
}
|
| 650 |
|
|
|
|
|
|
|
|
|
|
| 651 |
function handleBizStore() {
|
| 652 |
if (!bizStorePart.value || !bizStoreTarget.value) return;
|
| 653 |
const event = createAssembleEvent(
|
| 654 |
bizStoreTarget.value,
|
| 655 |
bizStorePart.value,
|
| 656 |
'stored_at',
|
| 657 |
+
state.currentTime,
|
| 658 |
+
nextSequence()
|
| 659 |
);
|
| 660 |
+
if (event) dispatch(event);
|
|
|
|
|
|
|
| 661 |
}
|
| 662 |
|
| 663 |
+
// Resizer Handlers with active .dragging visual feedback
|
| 664 |
function startResizeH(e) {
|
| 665 |
+
const resizerEl = e.currentTarget;
|
| 666 |
+
resizerEl?.classList.add('dragging');
|
| 667 |
const startX = e.clientX;
|
| 668 |
const startWidth = layout.leftWidth;
|
| 669 |
const containerWidth = document.querySelector('.main-container').getBoundingClientRect().width;
|
| 670 |
+
|
| 671 |
const onMouseMove = (evt) => {
|
| 672 |
const newWidth = startWidth + ((evt.clientX - startX) / containerWidth) * 100;
|
| 673 |
+
if (newWidth > 25 && newWidth < 78) layout.leftWidth = newWidth;
|
| 674 |
};
|
| 675 |
+
|
| 676 |
const onMouseUp = () => {
|
| 677 |
+
resizerEl?.classList.remove('dragging');
|
| 678 |
document.removeEventListener('mousemove', onMouseMove);
|
| 679 |
document.removeEventListener('mouseup', onMouseUp);
|
| 680 |
document.body.classList.remove('no-select');
|
| 681 |
};
|
| 682 |
+
|
| 683 |
document.addEventListener('mousemove', onMouseMove);
|
| 684 |
document.addEventListener('mouseup', onMouseUp);
|
| 685 |
document.body.classList.add('no-select');
|
| 686 |
}
|
| 687 |
|
| 688 |
function startResizeV(e, property, containerSelector) {
|
| 689 |
+
const resizerEl = e.currentTarget;
|
| 690 |
+
resizerEl?.classList.add('dragging');
|
| 691 |
const startY = e.clientY;
|
| 692 |
const startHeight = layout[property];
|
| 693 |
const containerHeight = document.querySelector(containerSelector).getBoundingClientRect().height;
|
| 694 |
+
|
| 695 |
const onMouseMove = (evt) => {
|
| 696 |
const newHeight = startHeight + ((evt.clientY - startY) / containerHeight) * 100;
|
| 697 |
+
if (newHeight > 12 && newHeight < 75) layout[property] = newHeight;
|
| 698 |
};
|
| 699 |
+
|
| 700 |
const onMouseUp = () => {
|
| 701 |
+
resizerEl?.classList.remove('dragging');
|
| 702 |
document.removeEventListener('mousemove', onMouseMove);
|
| 703 |
document.removeEventListener('mouseup', onMouseUp);
|
| 704 |
document.body.classList.remove('no-select');
|
| 705 |
};
|
| 706 |
+
|
| 707 |
document.addEventListener('mousemove', onMouseMove);
|
| 708 |
document.addEventListener('mouseup', onMouseUp);
|
| 709 |
document.body.classList.add('no-select');
|
| 710 |
}
|
| 711 |
|
| 712 |
+
// Full JSON State Export & Import
|
|
|
|
|
|
|
| 713 |
function saveJson() {
|
| 714 |
const exportData = {
|
| 715 |
+
version: '2.0',
|
| 716 |
savedAt: new Date().toISOString(),
|
| 717 |
+
userRole: state.userRole,
|
| 718 |
+
ontology: state.ontology,
|
| 719 |
+
perspective: state.perspective,
|
| 720 |
+
storage: state.storage,
|
| 721 |
+
graphEngine: state.graphEngine,
|
| 722 |
+
language: state.language,
|
| 723 |
+
showHistory: state.showHistory,
|
| 724 |
currentTime: state.currentTime,
|
| 725 |
edgeSequence: state.edgeSequence,
|
| 726 |
eventLog: JSON.parse(JSON.stringify(state.eventLog))
|
|
|
|
| 736 |
URL.revokeObjectURL(url);
|
| 737 |
}
|
| 738 |
|
|
|
|
|
|
|
|
|
|
| 739 |
function triggerLoadJson() {
|
| 740 |
if (fileInput.value) {
|
| 741 |
fileInput.value.value = '';
|
|
|
|
| 743 |
}
|
| 744 |
}
|
| 745 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 746 |
function onFileLoaded(e) {
|
| 747 |
const file = e.target.files?.[0];
|
| 748 |
if (!file) return;
|
|
|
|
| 755 |
loadState({ eventLog: parsed });
|
| 756 |
} else if (parsed && typeof parsed === 'object') {
|
| 757 |
loadState(parsed);
|
|
|
|
|
|
|
|
|
|
| 758 |
}
|
| 759 |
nextTick(() => {
|
| 760 |
renderGraph();
|
|
|
|
| 768 |
}
|
| 769 |
|
| 770 |
return {
|
| 771 |
+
state, t, layout, advanceTime, setPerspective, dispatch,
|
| 772 |
+
fileInput, saveJson, triggerLoadJson, onFileLoaded,
|
| 773 |
entityClasses: ENTITY_CLASSES,
|
| 774 |
relationTypes: RELATION_TYPES,
|
| 775 |
+
availableNodes, goHome,
|
| 776 |
+
activeParts, activeWarehouses,
|
|
|
|
| 777 |
bizRegType, bizRegId, handleBizRegister,
|
| 778 |
bizInstSource, bizInstTarget, handleBizInstall, bizInstallSources, bizInstallTargets,
|
| 779 |
bizDefTarget, bizDefType, bizDefVal, handleBizReportDefect,
|
| 780 |
bizRevDefect, handleBizRevokeDefect, activeDefectsList,
|
| 781 |
bizDismPart, handleBizDismantle, installedComponents,
|
| 782 |
bizStorePart, bizStoreTarget, handleBizStore,
|
| 783 |
+
bomTrees, lpgNodeRecords, lpgRecords, rdfTtl, formatProperties,
|
| 784 |
+
queryCode, liveQueryResult,
|
| 785 |
visContainer, mermaidContainer,
|
| 786 |
graphCaption, graphLegendItems,
|
| 787 |
startResizeH, startResizeV,
|
| 788 |
scenarioAudio, isPlaying, audioCurrentTime, audioDuration,
|
| 789 |
+
showSubtitles, currentSubtitleText,
|
| 790 |
playAudio, pauseAudio, togglePlayAudio, stopAudio,
|
| 791 |
onTimeUpdate, onLoadedMetadata, onAudioEnded, onSeek, formatTime
|
| 792 |
};
|
| 793 |
},
|
| 794 |
+
|
| 795 |
template: `
|
| 796 |
<!-- Main Menu (Landing View) -->
|
| 797 |
<div v-if="state.currentView === 'MENU'" class="landing-view">
|
| 798 |
<div class="menu-card">
|
| 799 |
<div class="menu-badge">IEC 63339 Workbench</div>
|
| 800 |
<h1 class="menu-title">Knowledge Architecture Workbench</h1>
|
| 801 |
+
<p class="menu-subtitle">Top-Level Ontologies (SMIM), Physical Storage (SMS) & Event Sourcing</p>
|
| 802 |
+
|
| 803 |
<div class="menu-actions">
|
| 804 |
<button class="menu-btn menu-btn-primary" @click="state.currentView = 'SANDBOX'">
|
| 805 |
<span class="btn-main-text">Sandbox Mode</span>
|
| 806 |
<span class="btn-sub-text">Interactive exploration of 3D/4D ontologies & temporal graph models</span>
|
| 807 |
</button>
|
| 808 |
+
|
| 809 |
<button class="menu-btn menu-btn-scenario" @click="state.currentView = 'SCENARIO'">
|
| 810 |
<span class="btn-main-text">Scenario: The Vanishing Crack</span>
|
| 811 |
<span class="btn-sub-text">Guided demonstration of temporal defect tracing across lifecycle states</span>
|
|
|
|
| 814 |
</div>
|
| 815 |
</div>
|
| 816 |
|
| 817 |
+
<!-- Main Sandbox & Scenario Workspace -->
|
| 818 |
<div v-if="state.currentView !== 'MENU'" class="sandbox-workspace">
|
| 819 |
<div class="control-deck">
|
| 820 |
+
<!-- Tier 1: Unified Architecture & Perspective Controls -->
|
| 821 |
+
<div class="deck-row deck-row-top">
|
| 822 |
+
<div class="section-label">
|
| 823 |
+
<span class="iec-badge">IEC 63339 / SMIM<span class="iec-tooltip" v-html="t.tooltips.smim"></span></span>
|
| 824 |
+
<span>{{ t.ontoTitle }}</span>
|
| 825 |
+
</div>
|
| 826 |
+
<div class="btn-group">
|
| 827 |
+
<button id="ctrl-onto-bfo" :class="{ active: state.ontology === 'BFO' }" @click="state.ontology = 'BFO'">BFO (3D)</button>
|
| 828 |
+
<button id="ctrl-onto-iso" :class="{ active: state.ontology === 'ISO15926' }" @click="state.ontology = 'ISO15926'">ISO 15926 (4D)</button>
|
| 829 |
+
<button id="ctrl-onto-naive-node" :class="{ active: state.ontology === 'NAIVE_NODE' }" @click="state.ontology = 'NAIVE_NODE'">{{ t.ontoNaiveNode }}</button>
|
| 830 |
+
<button id="ctrl-onto-naive-edge" :class="{ active: state.ontology === 'NAIVE_EDGE' }" @click="state.ontology = 'NAIVE_EDGE'">{{ t.ontoNaiveEdge }}</button>
|
| 831 |
+
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 832 |
|
| 833 |
<div class="divider"></div>
|
| 834 |
|
| 835 |
+
<!-- Unified 4-Perspective Switcher (SMRM | SMIM | SMS_LPG | SMS_RDF) -->
|
| 836 |
+
<div class="section-label">
|
| 837 |
+
<span class="iec-badge">IEC 63339<span class="iec-tooltip" v-html="t.tooltips.sms"></span></span>
|
| 838 |
+
<span>👁️ {{ t.viewTitle }}</span>
|
| 839 |
+
</div>
|
| 840 |
+
<div class="btn-group">
|
| 841 |
+
<button id="ctrl-view-smrm" :class="{ active: state.perspective === 'SMRM' }" @click="setPerspective('SMRM')">{{ t.viewSmrm }}</button>
|
| 842 |
+
<button id="ctrl-view-logical" :class="{ active: state.perspective === 'SMIM' }" @click="setPerspective('SMIM')">{{ t.viewLogical }}</button>
|
| 843 |
+
<button id="ctrl-view-physical-lpg" :class="{ active: state.perspective === 'SMS_LPG' }" @click="setPerspective('SMS_LPG')">{{ t.viewPhysicalLpg }}</button>
|
| 844 |
+
<button id="ctrl-view-physical-rdf" :class="{ active: state.perspective === 'SMS_RDF' }" @click="setPerspective('SMS_RDF')">{{ t.viewPhysicalRdf }}</button>
|
| 845 |
+
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 846 |
|
| 847 |
<div class="divider"></div>
|
| 848 |
|
| 849 |
+
<div class="section-label">🎨 <span>{{ t.engineTitle }}</span></div>
|
| 850 |
+
<div class="btn-group">
|
| 851 |
+
<button id="ctrl-engine-vis" :class="{ active: state.graphEngine === 'VIS' }" @click="state.graphEngine = 'VIS'">🌐 Vis.js</button>
|
| 852 |
+
<button id="ctrl-engine-mermaid" :class="{ active: state.graphEngine === 'MERMAID' }" @click="state.graphEngine = 'MERMAID'">📊 Mermaid</button>
|
| 853 |
+
</div>
|
|
|
|
| 854 |
|
| 855 |
<div class="divider"></div>
|
| 856 |
|
| 857 |
+
<!-- JSON State Operations -->
|
| 858 |
+
<button id="ctrl-save-json" class="secondary" @click="saveJson" :title="t.saveBtn">{{ t.saveBtn }}</button>
|
| 859 |
+
<button id="ctrl-load-json" class="secondary" @click="triggerLoadJson" :title="t.loadBtn">{{ t.loadBtn }}</button>
|
| 860 |
+
<input ref="fileInput" type="file" accept=".json" style="display: none;" @change="onFileLoaded" />
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 861 |
|
| 862 |
+
<div style="margin-left: auto;" class="btn-group flag-group">
|
| 863 |
+
<button id="ctrl-lang-sv" :class="{ active: state.language === 'sv' }" @click="state.language = 'sv'" title="Svenska">🇸🇪</button>
|
| 864 |
+
<button id="ctrl-lang-en" :class="{ active: state.language === 'en' }" @click="state.language = 'en'" title="British English">🇬🇧</button>
|
| 865 |
+
<button id="ctrl-lang-ru" :class="{ active: state.language === 'ru' }" @click="state.language = 'ru'" title="Русский">🇷🇺</button>
|
| 866 |
+
</div>
|
|
|
|
| 867 |
</div>
|
|
|
|
|
|
|
| 868 |
|
| 869 |
+
<!-- Tier 2: Engineer Lifecycle Actions -->
|
| 870 |
+
<template v-if="state.userRole === 'UNI_ENGINEER'">
|
| 871 |
+
<!-- Row 1: Time, Register, Install, Dismantle -->
|
| 872 |
+
<div class="deck-row">
|
| 873 |
+
<span id="biz-time-badge" class="badge">T = {{ state.currentTime }}</span>
|
| 874 |
+
<button id="biz-step-btn" @click="advanceTime">{{ t.stepBtn }}</button>
|
| 875 |
+
|
| 876 |
+
<div class="divider"></div>
|
| 877 |
+
|
| 878 |
+
<!-- Action 1: Register Entity -->
|
| 879 |
+
<label class="action-label">1. Register:</label>
|
| 880 |
+
<select id="biz-reg-type" v-model="bizRegType" title="Entity Class">
|
| 881 |
+
<option value="Vehicle">Vehicle</option>
|
| 882 |
+
<option value="Part">Part</option>
|
| 883 |
+
<option value="Warehouse">Warehouse</option>
|
| 884 |
+
</select>
|
| 885 |
+
<input id="biz-reg-id" type="text" v-model="bizRegId" placeholder="ID (e.g. Part_B)" style="width: 110px;" @keyup.enter="handleBizRegister">
|
| 886 |
+
<button id="biz-reg-btn" class="secondary" @click="handleBizRegister">Register</button>
|
| 887 |
+
|
| 888 |
+
<div class="divider"></div>
|
| 889 |
+
|
| 890 |
+
<!-- Action 2: Install / Assemble -->
|
| 891 |
+
<label class="action-label">2. Install:</label>
|
| 892 |
+
<select id="biz-inst-source" v-model="bizInstSource" title="Component to install">
|
| 893 |
+
<option v-for="node in bizInstallSources" :key="'src_' + node" :value="node">{{ node }}</option>
|
| 894 |
+
</select>
|
| 895 |
+
<span style="font-size: 11px; color: #64748b;">→</span>
|
| 896 |
+
<select id="biz-inst-target" v-model="bizInstTarget" title="Target Parent / System">
|
| 897 |
+
<option v-for="node in bizInstallTargets" :key="'tgt_' + node" :value="node">{{ node }}</option>
|
| 898 |
+
</select>
|
| 899 |
+
<button id="biz-inst-btn" class="secondary" @click="handleBizInstall">Install</button>
|
| 900 |
+
|
| 901 |
+
<div class="divider"></div>
|
| 902 |
+
|
| 903 |
+
<!-- Action 5: Dismantle -->
|
| 904 |
+
<label class="action-label">5. Dismantle:</label>
|
| 905 |
+
<select id="biz-dism-part" v-model="bizDismPart">
|
| 906 |
+
<option v-for="comp in installedComponents" :key="'comp_' + comp" :value="comp">{{ comp }}</option>
|
| 907 |
+
</select>
|
| 908 |
+
<button id="biz-dism-btn" class="danger" @click="handleBizDismantle">Dismantle</button>
|
| 909 |
+
</div>
|
| 910 |
|
| 911 |
+
<!-- Row 2: Defect, Revoke, Storage, History -->
|
| 912 |
+
<div class="deck-row" style="border-top: 1px solid #f1f5f9; background: #fafafa;">
|
| 913 |
+
<!-- Action 3: Report Defect -->
|
| 914 |
+
<label class="action-label">3. Defect:</label>
|
| 915 |
+
<select id="biz-def-target" v-model="bizDefTarget">
|
| 916 |
+
<option v-for="node in availableNodes" :key="'dn_' + node" :value="node">{{ node }}</option>
|
| 917 |
+
</select>
|
| 918 |
+
<input id="biz-def-type" type="text" v-model="bizDefType" placeholder="Type (e.g. crack)" style="width: 110px;">
|
| 919 |
+
<input id="biz-def-val" type="text" v-model="bizDefVal" placeholder="Value (e.g. 2mm)" style="width: 85px;">
|
| 920 |
+
<button id="biz-def-btn" class="secondary" @click="handleBizReportDefect">Report Defect</button>
|
| 921 |
+
|
| 922 |
+
<div class="divider"></div>
|
| 923 |
+
|
| 924 |
+
<!-- Action 4: Revoke Defect -->
|
| 925 |
+
<label class="action-label">4. Revoke:</label>
|
| 926 |
+
<select id="biz-rev-defect" v-model="bizRevDefect">
|
| 927 |
+
<option v-for="d in activeDefectsList" :key="d.id" :value="d.id">{{ d.label }}</option>
|
| 928 |
+
</select>
|
| 929 |
+
<button id="biz-rev-btn" class="danger" @click="handleBizRevokeDefect">Revoke</button>
|
| 930 |
+
|
| 931 |
+
<div class="divider"></div>
|
| 932 |
+
|
| 933 |
+
<!-- Action 6: Move to Storage -->
|
| 934 |
+
<label class="action-label">6. Storage:</label>
|
| 935 |
+
<select id="biz-store-part" v-model="bizStorePart">
|
| 936 |
+
<option v-for="part in activeParts" :key="'sp_' + part" :value="part">{{ part }}</option>
|
| 937 |
+
</select>
|
| 938 |
+
<span style="font-size: 11px; color: #64748b;">→</span>
|
| 939 |
+
<select id="biz-store-target" v-model="bizStoreTarget">
|
| 940 |
+
<option v-for="wh in activeWarehouses" :key="'sw_' + wh" :value="wh">{{ wh }}</option>
|
| 941 |
+
</select>
|
| 942 |
+
<button id="biz-store-btn" class="secondary" @click="handleBizStore">Store</button>
|
| 943 |
+
|
| 944 |
+
<div class="divider"></div>
|
| 945 |
+
|
| 946 |
+
<!-- History Checkbox -->
|
| 947 |
+
<label style="font-size: 11px; display: flex; align-items: center; gap: 4px; cursor: pointer;">
|
| 948 |
+
<input type="checkbox" v-model="state.showHistory"> {{ t.history }}
|
| 949 |
+
</label>
|
| 950 |
+
</div>
|
| 951 |
+
</template>
|
| 952 |
+
</div>
|
| 953 |
+
|
| 954 |
+
<div class="main-container">
|
| 955 |
+
<div id="canvas-container" :style="{ flex: \`0 0 \${layout.leftWidth}%\` }">
|
| 956 |
+
<div ref="visContainer" id="vis-container" v-show="state.graphEngine === 'VIS'"></div>
|
| 957 |
+
<div ref="mermaidContainer" id="mermaid-container" v-show="state.graphEngine === 'MERMAID'"></div>
|
| 958 |
|
| 959 |
+
<!-- Dynamic Graph Caption Overlay -->
|
| 960 |
+
<div class="graph-caption">
|
| 961 |
+
<span>📌</span>
|
| 962 |
+
<span>{{ graphCaption }}</span>
|
| 963 |
</div>
|
| 964 |
+
|
| 965 |
+
<!-- Contextual Dynamic Legend Overlay -->
|
| 966 |
+
<div class="graph-legend">
|
| 967 |
+
<div class="legend-title">
|
| 968 |
+
<span>{{ t.legend.title }}</span>
|
| 969 |
</div>
|
| 970 |
+
<div v-if="graphLegendItems.nodes.length > 0" class="legend-section">
|
| 971 |
+
<div class="legend-section-title">{{ t.legend.nodes }}</div>
|
| 972 |
+
<div v-for="item in graphLegendItems.nodes" :key="item.id" class="legend-item">
|
| 973 |
+
<div :class="item.swatch"></div>
|
| 974 |
+
<span>{{ item.label }}</span>
|
| 975 |
+
</div>
|
| 976 |
+
</div>
|
| 977 |
+
<div v-if="graphLegendItems.edges.length > 0" class="legend-section">
|
| 978 |
+
<div class="legend-section-title">{{ t.legend.edges }}</div>
|
| 979 |
+
<div v-for="item in graphLegendItems.edges" :key="item.id" class="legend-item">
|
| 980 |
+
<div :class="item.swatch"></div>
|
| 981 |
+
<span>{{ item.label }}</span>
|
| 982 |
+
</div>
|
| 983 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 984 |
</div>
|
| 985 |
</div>
|
| 986 |
|
| 987 |
+
<!-- Horizontal Resizer -->
|
| 988 |
+
<div class="resizer" id="panel-resizer" @mousedown="startResizeH"></div>
|
| 989 |
+
|
| 990 |
+
<div class="sidebar" style="flex: 1;">
|
| 991 |
+
<!-- 1. BOM View (URMSM) -->
|
| 992 |
+
<div class="card" :style="{ flex: \`0 0 \${layout.bomHeight}%\` }">
|
| 993 |
+
<h4>
|
| 994 |
+
<span>{{ t.titleBom }}</span>
|
| 995 |
+
<span class="iec-badge align-right">IEC 63339 / URMSM<span class="iec-tooltip" v-html="t.tooltips.urmsm"></span></span>
|
| 996 |
+
</h4>
|
| 997 |
+
<div id="tree-container" class="card-scroll-body">
|
| 998 |
+
<template v-if="bomTrees.length > 0">
|
| 999 |
+
<ul class="tree root-tree">
|
| 1000 |
+
<bom-node v-for="root in bomTrees" :key="root.id" :node="root" />
|
| 1001 |
+
</ul>
|
| 1002 |
+
</template>
|
| 1003 |
+
<div v-else class="empty-placeholder">
|
| 1004 |
+
{{ t.emptyBom }}{{ state.currentTime }}
|
| 1005 |
+
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1006 |
</div>
|
|
|
|
| 1007 |
</div>
|
|
|
|
| 1008 |
|
| 1009 |
+
<div class="resizer-h" @mousedown="(e) => startResizeV(e, 'bomHeight', '.sidebar')"></div>
|
| 1010 |
+
|
| 1011 |
+
<!-- 2. Storage View: UNDER THE BONNET (SMS) -->
|
| 1012 |
+
<div class="card" :style="{ flex: \`0 0 \${layout.storageHeight}%\` }">
|
| 1013 |
+
<h4>
|
| 1014 |
+
<span>{{ t.titleStorage }} <small style="color:#0284c7; font-weight: 700;">({{ state.ontology }})</small></span>
|
| 1015 |
+
<div class="btn-group mini-toggle">
|
| 1016 |
+
<button :class="{ active: state.storage === 'LPG' }" @click="state.storage = 'LPG'">LPG</button>
|
| 1017 |
+
<button :class="{ active: state.storage === 'RDF' }" @click="state.storage = 'RDF'">RDF (W3C)</button>
|
| 1018 |
+
</div>
|
| 1019 |
+
</h4>
|
| 1020 |
+
<div id="storage-content" class="card-scroll-body">
|
| 1021 |
+
<template v-if="state.storage === 'LPG'">
|
| 1022 |
+
<!-- Node Store Table -->
|
| 1023 |
+
<div class="store-sub-header">{{ t.nodeStoreTitle }} ({{ lpgNodeRecords.length }})</div>
|
| 1024 |
+
<table v-if="lpgNodeRecords.length > 0" class="storage-table">
|
| 1025 |
+
<thead>
|
| 1026 |
+
<tr>
|
| 1027 |
+
<th>Node ID</th>
|
| 1028 |
+
<th>Label / Class</th>
|
| 1029 |
+
<th>Properties</th>
|
| 1030 |
+
</tr>
|
| 1031 |
+
</thead>
|
| 1032 |
+
<tbody>
|
| 1033 |
+
<tr v-for="n in lpgNodeRecords" :key="'nd_' + n.id" :class="n.isActive ? 'active-row' : 'closed-row'">
|
| 1034 |
+
<td><strong>{{ n.id }}</strong></td>
|
| 1035 |
+
<td><code>:{{ n.type }}</code></td>
|
| 1036 |
+
<td>
|
| 1037 |
+
<code v-if="formatProperties(n.properties)">{ {{ formatProperties(n.properties) }} }</code>
|
| 1038 |
+
<span v-else>—</span>
|
| 1039 |
+
</td>
|
| 1040 |
+
</tr>
|
| 1041 |
+
</tbody>
|
| 1042 |
+
</table>
|
| 1043 |
+
<div v-else class="empty-placeholder">0 nodes</div>
|
| 1044 |
+
|
| 1045 |
+
<!-- Relationship Store Table -->
|
| 1046 |
+
<div class="store-sub-header" style="margin-top: 8px;">{{ t.edgeStoreTitle }} ({{ lpgRecords.length }})</div>
|
| 1047 |
+
<table v-if="lpgRecords.length > 0" class="storage-table">
|
| 1048 |
+
<thead>
|
| 1049 |
+
<tr>
|
| 1050 |
+
<th>{{ t.sourceCol }}</th>
|
| 1051 |
+
<th>{{ t.targetCol }}</th>
|
| 1052 |
+
<th>Type</th>
|
| 1053 |
+
<th>Properties</th>
|
| 1054 |
+
<th>{{ t.statusCol }}</th>
|
| 1055 |
+
</tr>
|
| 1056 |
+
</thead>
|
| 1057 |
+
<tbody>
|
| 1058 |
+
<tr v-for="row in lpgRecords" :key="row.id" :class="row.isActive ? 'active-row' : 'closed-row'">
|
| 1059 |
+
<td>{{ row.from }}</td>
|
| 1060 |
+
<td><strong>{{ row.to }}</strong></td>
|
| 1061 |
+
<td><code>:{{ row.label }}</code></td>
|
| 1062 |
+
<td>
|
| 1063 |
+
<code v-if="formatProperties(row.properties)">{ {{ formatProperties(row.properties) }} }</code>
|
| 1064 |
+
<span v-else>—</span>
|
| 1065 |
+
</td>
|
| 1066 |
+
<td>{{ row.isActive ? t.activeStatus : t.closedStatus }}</td>
|
| 1067 |
+
</tr>
|
| 1068 |
+
</tbody>
|
| 1069 |
+
</table>
|
| 1070 |
+
<div v-else class="empty-placeholder">0 relationships</div>
|
| 1071 |
+
</template>
|
| 1072 |
+
<pre v-else class="code-block">{{ rdfTtl }}</pre>
|
| 1073 |
+
</div>
|
| 1074 |
+
</div>
|
| 1075 |
|
| 1076 |
+
<div class="resizer-h" @mousedown="(e) => startResizeV(e, 'storageHeight', '.sidebar')"></div>
|
| 1077 |
+
|
| 1078 |
+
<!-- 3. Database Query & Live Execution Result -->
|
| 1079 |
+
<div class="card" style="flex: 1;">
|
| 1080 |
+
<h4>
|
| 1081 |
+
<span>{{ t.queryTitle }} ({{ state.storage === 'LPG' ? 'Cypher' : 'SPARQL' }})</span>
|
| 1082 |
+
<span style="font-weight: 600; text-transform: none; color: #0d9488;">{{ t.queryDesc }}{{ state.currentTime }})</span>
|
| 1083 |
+
</h4>
|
| 1084 |
+
<div id="query-content" class="card-scroll-body">
|
| 1085 |
+
<pre class="code-block query-code-block" :style="{ color: state.ontology === 'BFO' ? '#0369a1' : '#3730a3' }">{{ queryCode }}</pre>
|
| 1086 |
+
|
| 1087 |
+
<!-- Live Query Execution Result Box -->
|
| 1088 |
+
<div class="query-result-box">
|
| 1089 |
+
<div class="query-result-header">
|
| 1090 |
+
<span>{{ t.queryResultTitle }}</span>
|
| 1091 |
+
<span
|
| 1092 |
+
class="defect-counter-badge"
|
| 1093 |
+
:class="liveQueryResult.defectiveCount > 0 ? 'defect-alert-found' : 'defect-alert-zero'"
|
| 1094 |
+
>
|
| 1095 |
+
{{ t.queryDefectsFound }} {{ liveQueryResult.defectiveCount }}
|
| 1096 |
+
</span>
|
| 1097 |
+
</div>
|
| 1098 |
+
<table v-if="liveQueryResult.rows.length > 0" class="storage-table">
|
| 1099 |
+
<thead>
|
| 1100 |
+
<tr>
|
| 1101 |
+
<th>part</th>
|
| 1102 |
+
<th>location</th>
|
| 1103 |
+
<th>defect</th>
|
| 1104 |
+
</tr>
|
| 1105 |
+
</thead>
|
| 1106 |
+
<tbody>
|
| 1107 |
+
<tr v-for="(r, idx) in liveQueryResult.rows" :key="idx">
|
| 1108 |
+
<td><strong>{{ r.part }}</strong></td>
|
| 1109 |
+
<td>{{ r.location }}</td>
|
| 1110 |
+
<td>
|
| 1111 |
+
<span :class="r.hasDefect ? 'bom-defect-badge' : 'null-badge'">{{ r.defect }}</span>
|
| 1112 |
+
</td>
|
| 1113 |
+
</tr>
|
| 1114 |
+
</tbody>
|
| 1115 |
+
</table>
|
| 1116 |
+
<div v-else class="empty-placeholder">0 rows returned at T = {{ state.currentTime }}</div>
|
| 1117 |
+
</div>
|
| 1118 |
+
</div>
|
| 1119 |
</div>
|
| 1120 |
</div>
|
| 1121 |
</div>
|
|
|
|
| 1122 |
|
| 1123 |
+
<!-- Hidden Scenario Audio Element -->
|
| 1124 |
+
<audio
|
| 1125 |
+
ref="scenarioAudio"
|
| 1126 |
+
src="./narratives/Ontologies_-_Disappearing_Crack.mp3"
|
| 1127 |
+
preload="metadata"
|
| 1128 |
+
@timeupdate="onTimeUpdate"
|
| 1129 |
+
@loadedmetadata="onLoadedMetadata"
|
| 1130 |
+
@play="isPlaying = true"
|
| 1131 |
+
@pause="isPlaying = false"
|
| 1132 |
+
@ended="onAudioEnded"
|
| 1133 |
+
></audio>
|
| 1134 |
+
|
| 1135 |
+
<!-- Subtitles Overlay (Scenario Mode) -->
|
| 1136 |
+
<div
|
| 1137 |
+
v-if="state.currentView === 'SCENARIO' && showSubtitles && currentSubtitleText"
|
| 1138 |
+
class="scenario-subtitles"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1139 |
>
|
| 1140 |
+
{{ currentSubtitleText }}
|
| 1141 |
+
</div>
|
|
|
|
| 1142 |
|
| 1143 |
+
<!-- Floating Scenario Player Panel -->
|
| 1144 |
+
<div
|
| 1145 |
+
v-if="state.currentView === 'SCENARIO'"
|
| 1146 |
+
class="scenario-player-panel"
|
|
|
|
| 1147 |
>
|
| 1148 |
+
<button
|
| 1149 |
+
class="player-btn"
|
| 1150 |
+
@click="togglePlayAudio"
|
| 1151 |
+
:title="isPlaying ? 'Pause' : 'Play'"
|
| 1152 |
+
:aria-label="isPlaying ? 'Pause' : 'Play'"
|
| 1153 |
+
>
|
| 1154 |
+
<span v-if="isPlaying">⏸</span>
|
| 1155 |
+
<span v-else>▶</span>
|
| 1156 |
+
</button>
|
| 1157 |
+
|
| 1158 |
+
<button
|
| 1159 |
+
class="player-btn"
|
| 1160 |
+
@click="stopAudio(true)"
|
| 1161 |
+
title="Stop"
|
| 1162 |
+
aria-label="Stop"
|
| 1163 |
+
>
|
| 1164 |
+
<span>⏹</span>
|
| 1165 |
+
</button>
|
| 1166 |
+
|
| 1167 |
+
<input
|
| 1168 |
+
type="range"
|
| 1169 |
+
class="player-progress"
|
| 1170 |
+
min="0"
|
| 1171 |
+
:max="audioDuration || 100"
|
| 1172 |
+
step="0.1"
|
| 1173 |
+
:value="audioCurrentTime"
|
| 1174 |
+
@input="onSeek"
|
| 1175 |
+
aria-label="Audio Timeline"
|
| 1176 |
+
/>
|
| 1177 |
|
| 1178 |
+
<span class="player-time">{{ formatTime(audioCurrentTime) }} / {{ formatTime(audioDuration) }}</span>
|
| 1179 |
+
|
| 1180 |
+
<button
|
| 1181 |
+
class="player-btn player-btn-cc"
|
| 1182 |
+
:class="{ active: showSubtitles }"
|
| 1183 |
+
@click="showSubtitles = !showSubtitles"
|
| 1184 |
+
title="Toggle Subtitles"
|
| 1185 |
+
aria-label="Toggle Subtitles"
|
| 1186 |
+
>
|
| 1187 |
+
CC
|
| 1188 |
+
</button>
|
| 1189 |
+
</div>
|
| 1190 |
|
| 1191 |
+
<!-- Floating Home Button -->
|
| 1192 |
<button
|
| 1193 |
+
v-if="state.currentView !== 'MENU'"
|
| 1194 |
+
class="floating-home-btn"
|
| 1195 |
+
@click="goHome"
|
| 1196 |
+
title="Return to Main Menu"
|
| 1197 |
+
aria-label="Return to Main Menu"
|
| 1198 |
>
|
| 1199 |
+
<svg viewBox="0 0 24 24" width="22" height="22" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
| 1200 |
+
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path>
|
| 1201 |
+
<polyline points="9 22 9 12 15 12 15 22"></polyline>
|
| 1202 |
+
</svg>
|
| 1203 |
</button>
|
| 1204 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1205 |
`
|
| 1206 |
};
|
| 1207 |
|
js/i18n.js
CHANGED
|
@@ -1,78 +1,67 @@
|
|
| 1 |
/**
|
| 2 |
* INTERNATIONALISATION (i18n) DICTIONARIES
|
| 3 |
* ----------------------------------------
|
| 4 |
-
*
|
| 5 |
-
*
|
| 6 |
*/
|
| 7 |
export const i18n = {
|
| 8 |
sv: {
|
| 9 |
ontoTitle: "Ontologi:",
|
| 10 |
-
graphTitle: "Datamodell:",
|
| 11 |
engineTitle: "Grafmotor:",
|
| 12 |
viewTitle: "Perspektiv:",
|
| 13 |
viewSmrm: "Ingenjör (SMRM)",
|
| 14 |
viewLogical: "Logisk (SMIM)",
|
| 15 |
-
|
|
|
|
| 16 |
viewSmrmCaption: "Ingenjörsperspektiv (SMRM)",
|
| 17 |
viewLogicalCaption: "Logiskt perspektiv (SMIM)",
|
| 18 |
-
|
|
|
|
| 19 |
stepBtn: "Lås steg (T+1)",
|
| 20 |
-
parent: "Överordnad:",
|
| 21 |
-
partPlaceholder: "Komponentnamn",
|
| 22 |
-
addBtn: "+ Lägg till",
|
| 23 |
-
removeBtn: "Uteslut relation",
|
| 24 |
history: "Historik",
|
| 25 |
titleBom: "Struktur (BOM)",
|
|
|
|
| 26 |
titleStorage: "Under huven",
|
|
|
|
|
|
|
| 27 |
activeStatus: "Aktiv",
|
| 28 |
closedStatus: "Avslutad",
|
| 29 |
sourceCol: "Källa",
|
| 30 |
targetCol: "Mål",
|
| 31 |
statusCol: "Status",
|
| 32 |
-
smrmStatus: "Domänontologi (Låst: System/IDO)",
|
| 33 |
saveBtn: "💾 Spara JSON",
|
| 34 |
loadBtn: "📂 Ladda JSON",
|
| 35 |
queryTitle: "Databasfråga",
|
| 36 |
-
queryDesc: "
|
| 37 |
-
|
|
|
|
| 38 |
ontoNaiveNode: "Naiv Graf (Node)",
|
| 39 |
ontoNaiveEdge: "Naiv Graf (Edge)",
|
| 40 |
-
nodeClass: "Klass:",
|
| 41 |
-
nodeIdPlaceholder: "Nod-ID",
|
| 42 |
-
addNodeBtn: "+ Nod",
|
| 43 |
-
relTypeLabel: "Relation:",
|
| 44 |
-
addRelBtn: "+ Relation",
|
| 45 |
-
propTarget: "Mål:",
|
| 46 |
-
propKeyPlaceholder: "Nyckel (t.ex. crack)",
|
| 47 |
-
propValPlaceholder: "Värde (t.ex. 2mm)",
|
| 48 |
-
addPropBtn: "+ Egenskap",
|
| 49 |
-
targetNode: "Nod",
|
| 50 |
-
targetEdge: "Relation",
|
| 51 |
tooltips: {
|
| 52 |
-
urmsm: "<b>URMSM (Dimension & Aspect)</b><br/>Definierar intressentens perspektiv (ISO 42010 Viewpoint).
|
| 53 |
-
smrm: "<b>SMRM (Concept Repository)</b><br/>Domänontologi och gemensam begreppsbank (
|
| 54 |
-
smim: "<b>SMIM (Information Elements & Rules)</b><br/>Top-Level Ontology (TLO). Formella regler för
|
| 55 |
-
sms: "<b>SMS (Data in Tool)</b><br/>Den
|
| 56 |
},
|
| 57 |
legend: {
|
| 58 |
title: "Teckenförklaring",
|
| 59 |
nodes: "Noder",
|
| 60 |
edges: "Relationer",
|
| 61 |
-
bfoSys: "Continuant / System (:
|
| 62 |
-
bfoElem: "Continuant / Komponent (:
|
| 63 |
-
bfoActiveEdge: "Aktiv relation [giltig
|
| 64 |
bfoPastEdge: "Historisk relation (avslutad)",
|
| 65 |
-
bfoQuality: "Kvalitet /
|
| 66 |
bfoInheres: "inheres_in (egenskap i bärare)",
|
| 67 |
-
iso4d: "4D-individ (
|
| 68 |
-
isoState: "Temporal del / Tillstånd (State)",
|
| 69 |
-
isoPartRel: "
|
| 70 |
isoSliceRel: "temporalPartOf (del av 4D-individ)",
|
| 71 |
isoTransitionRel: "TRANSITION_TO (tillståndskedja i LPG)",
|
| 72 |
-
isoDefect: "Defekt (
|
| 73 |
isoDefectRel: "has_defect (tillståndsklassificering)",
|
| 74 |
rdfStmt: "Reifiering (_:statement)",
|
| 75 |
-
rdfLit: "Literal (
|
| 76 |
rdfPred: "RDF-predikat",
|
| 77 |
naiveVehicle: "Fordon (:Vehicle)",
|
| 78 |
naiveSlot: "Plats (:Slot)",
|
|
@@ -80,79 +69,68 @@ export const i18n = {
|
|
| 80 |
naiveDefect: "Defekt (:Defect)",
|
| 81 |
naiveWarehouse: "Lager (:Warehouse)",
|
| 82 |
relHasSlot: "has_slot (fordonsslot)",
|
| 83 |
-
relMountedIn: "mounted_in
|
| 84 |
relStoredAt: "stored_at (lagrad på)",
|
| 85 |
relHasDefect: "has_defect (identifierad defekt)"
|
| 86 |
}
|
| 87 |
},
|
| 88 |
en: {
|
| 89 |
ontoTitle: "Ontology:",
|
| 90 |
-
graphTitle: "Data Model:",
|
| 91 |
engineTitle: "Renderer:",
|
| 92 |
viewTitle: "Perspective:",
|
| 93 |
viewSmrm: "Engineer (SMRM)",
|
| 94 |
viewLogical: "Logical (SMIM)",
|
| 95 |
-
|
|
|
|
| 96 |
viewSmrmCaption: "Engineer Perspective (SMRM)",
|
| 97 |
viewLogicalCaption: "Logical Perspective (SMIM)",
|
| 98 |
-
|
|
|
|
| 99 |
stepBtn: "Commit step (T+1)",
|
| 100 |
-
parent: "Parent:",
|
| 101 |
-
partPlaceholder: "Component name",
|
| 102 |
-
addBtn: "+ Add",
|
| 103 |
-
removeBtn: "Sever relation",
|
| 104 |
history: "History",
|
| 105 |
titleBom: "BOM Tree Structure",
|
|
|
|
| 106 |
titleStorage: "Under the Bonnet",
|
|
|
|
|
|
|
| 107 |
activeStatus: "Active",
|
| 108 |
closedStatus: "Closed",
|
| 109 |
sourceCol: "Source",
|
| 110 |
targetCol: "Target",
|
| 111 |
statusCol: "Status",
|
| 112 |
-
smrmStatus: "Domain Ontology (Fixed: System/IDO)",
|
| 113 |
saveBtn: "💾 Save JSON",
|
| 114 |
loadBtn: "📂 Load JSON",
|
| 115 |
queryTitle: "Database Query",
|
| 116 |
-
queryDesc: "
|
| 117 |
-
|
|
|
|
| 118 |
ontoNaiveNode: "Naive Graph (Node)",
|
| 119 |
ontoNaiveEdge: "Naive Graph (Edge)",
|
| 120 |
-
nodeClass: "Class:",
|
| 121 |
-
nodeIdPlaceholder: "Node ID",
|
| 122 |
-
addNodeBtn: "+ Node",
|
| 123 |
-
relTypeLabel: "Relation:",
|
| 124 |
-
addRelBtn: "+ Relation",
|
| 125 |
-
propTarget: "Target:",
|
| 126 |
-
propKeyPlaceholder: "Key (e.g. crack)",
|
| 127 |
-
propValPlaceholder: "Value (e.g. 2mm)",
|
| 128 |
-
addPropBtn: "+ Property",
|
| 129 |
-
targetNode: "Node",
|
| 130 |
-
targetEdge: "Relation",
|
| 131 |
tooltips: {
|
| 132 |
-
urmsm: "<b>URMSM (Dimension & Aspect)</b><br/>Captures the stakeholder
|
| 133 |
-
smrm: "<b>SMRM (Concept Repository)</b><br/>Domain Ontology:
|
| 134 |
smim: "<b>SMIM (Information Elements & Rules)</b><br/>Top-Level Ontology (TLO). Axiomatic rules governing time and identity: 3D Continuants (BFO) vs 4D Spatio-Temporal Extents (ISO 15926).",
|
| 135 |
-
sms: "<b>SMS (Data in Tool)</b><br/>Low-level physical IT
|
| 136 |
},
|
| 137 |
legend: {
|
| 138 |
title: "Graph Legend",
|
| 139 |
nodes: "Node Types",
|
| 140 |
edges: "Relationships",
|
| 141 |
-
bfoSys: "Continuant / System (:
|
| 142 |
-
bfoElem: "Continuant / Component (:
|
| 143 |
-
bfoActiveEdge: "Active relation [valid
|
| 144 |
bfoPastEdge: "Historical relation (closed)",
|
| 145 |
bfoQuality: "Dependent Continuant (:Quality)",
|
| 146 |
bfoInheres: "inheres_in (Quality inherence)",
|
| 147 |
-
iso4d: "4D Individual (
|
| 148 |
-
isoState: "Temporal Part / State",
|
| 149 |
-
isoPartRel: "
|
| 150 |
isoSliceRel: "temporalPartOf (Extent link)",
|
| 151 |
isoTransitionRel: "TRANSITION_TO (State Chain in LPG)",
|
| 152 |
-
isoDefect: "Defect (
|
| 153 |
isoDefectRel: "has_defect (State Classification)",
|
| 154 |
rdfStmt: "Reification Node (_:statement)",
|
| 155 |
-
rdfLit: "Literal (xsd:
|
| 156 |
rdfPred: "RDF Predicate",
|
| 157 |
naiveVehicle: "Vehicle (:Vehicle)",
|
| 158 |
naiveSlot: "Slot (:Slot)",
|
|
@@ -160,79 +138,68 @@ export const i18n = {
|
|
| 160 |
naiveDefect: "Defect (:Defect)",
|
| 161 |
naiveWarehouse: "Warehouse (:Warehouse)",
|
| 162 |
relHasSlot: "has_slot (Vehicle Slot)",
|
| 163 |
-
relMountedIn: "mounted_in
|
| 164 |
relStoredAt: "stored_at (Stored in Warehouse)",
|
| 165 |
relHasDefect: "has_defect (Identified Defect)"
|
| 166 |
}
|
| 167 |
},
|
| 168 |
ru: {
|
| 169 |
ontoTitle: "Онтология:",
|
| 170 |
-
graphTitle: "Модель данных:",
|
| 171 |
engineTitle: "Движок:",
|
| 172 |
viewTitle: "Ракурс:",
|
| 173 |
viewSmrm: "Инженер (SMRM)",
|
| 174 |
viewLogical: "Логический (SMIM)",
|
| 175 |
-
|
|
|
|
| 176 |
viewSmrmCaption: "Инженерный ракурс (SMRM)",
|
| 177 |
viewLogicalCaption: "Логический ракурс (SMIM)",
|
| 178 |
-
|
|
|
|
| 179 |
stepBtn: "Зафиксировать шаг (T+1)",
|
| 180 |
-
parent: "Родитель:",
|
| 181 |
-
partPlaceholder: "Имя компонента",
|
| 182 |
-
addBtn: "+ Добавить",
|
| 183 |
-
removeBtn: "Исключить связь",
|
| 184 |
history: "История",
|
| 185 |
titleBom: "Состав изделия (BOM)",
|
|
|
|
| 186 |
titleStorage: "Под капотом",
|
|
|
|
|
|
|
| 187 |
activeStatus: "Активна",
|
| 188 |
closedStatus: "Закрыта",
|
| 189 |
sourceCol: "Источник",
|
| 190 |
targetCol: "Цель",
|
| 191 |
statusCol: "Статус",
|
| 192 |
-
smrmStatus: "Domain Ontology (Зафиксирована: System/IDO)",
|
| 193 |
saveBtn: "💾 Сохранить JSON",
|
| 194 |
loadBtn: "📂 Загрузить JSON",
|
| 195 |
queryTitle: "Запрос к БД",
|
| 196 |
-
queryDesc: "
|
| 197 |
-
|
|
|
|
| 198 |
ontoNaiveNode: "Наивный граф (Узел)",
|
| 199 |
ontoNaiveEdge: "Наивный граф (Ребро)",
|
| 200 |
-
nodeClass: "Класс:",
|
| 201 |
-
nodeIdPlaceholder: "ID узла",
|
| 202 |
-
addNodeBtn: "+ Узел",
|
| 203 |
-
relTypeLabel: "Связь:",
|
| 204 |
-
addRelBtn: "+ Связь",
|
| 205 |
-
propTarget: "Цель:",
|
| 206 |
-
propKeyPlaceholder: "Ключ (напр. crack)",
|
| 207 |
-
propValPlaceholder: "Значение (напр. 2mm)",
|
| 208 |
-
addPropBtn: "+ Свойство",
|
| 209 |
-
targetNode: "Узел",
|
| 210 |
-
targetEdge: "Связь",
|
| 211 |
tooltips: {
|
| 212 |
-
urmsm: "<b>URMSM (Dimension & Aspect)</b><br/>
|
| 213 |
-
smrm: "<b>SMRM (Concept Repository)</b><br/>Онтология предметной области
|
| 214 |
-
smim: "<b>SMIM (Information Elements & Rules)</b><br/>Верхнеуровневая онтология (TLO). Математические аксиомы
|
| 215 |
-
sms: "<b>SMS (Data in Tool)</b><br/>Физическое
|
| 216 |
},
|
| 217 |
legend: {
|
| 218 |
title: "Легенда графа",
|
| 219 |
nodes: "Типы узлов",
|
| 220 |
edges: "Типы связей",
|
| 221 |
-
bfoSys: "Континуант / Система (:
|
| 222 |
-
bfoElem: "Континуант / Компонент (:
|
| 223 |
bfoActiveEdge: "Активная связь [действует сейчас]",
|
| 224 |
bfoPastEdge: "Архивная связь (закрыта)",
|
| 225 |
bfoQuality: "Зависимый континуант (:Quality)",
|
| 226 |
bfoInheres: "inheres_in (присущ носителю)",
|
| 227 |
-
iso4d: "4D-индивид (
|
| 228 |
-
isoState: "Темпоральный срез / Состояние",
|
| 229 |
-
isoPartRel: "Вневременной состав
|
| 230 |
isoSliceRel: "temporalPartOf (связь с 4D-целым)",
|
| 231 |
isoTransitionRel: "TRANSITION_TO (цепочка состояний в LPG)",
|
| 232 |
-
isoDefect: "Дефект (
|
| 233 |
isoDefectRel: "has_defect (классификация среза)",
|
| 234 |
rdfStmt: "Узел реификации (_:statement)",
|
| 235 |
-
rdfLit: "Литерал (
|
| 236 |
rdfPred: "RDF-предикат",
|
| 237 |
naiveVehicle: "Транспорт (:Vehicle)",
|
| 238 |
naiveSlot: "Слот (:Slot)",
|
|
@@ -240,7 +207,7 @@ export const i18n = {
|
|
| 240 |
naiveDefect: "Дефект (:Defect)",
|
| 241 |
naiveWarehouse: "Склад (:Warehouse)",
|
| 242 |
relHasSlot: "has_slot (слот транспорта)",
|
| 243 |
-
relMountedIn: "mounted_in
|
| 244 |
relStoredAt: "stored_at (хранится на складе)",
|
| 245 |
relHasDefect: "has_defect (выявлен дефект)"
|
| 246 |
}
|
|
|
|
| 1 |
/**
|
| 2 |
* INTERNATIONALISATION (i18n) DICTIONARIES
|
| 3 |
* ----------------------------------------
|
| 4 |
+
* Exports UI strings, labels, and tooltips for Swedish ('sv'),
|
| 5 |
+
* British English ('en'), and Russian ('ru').
|
| 6 |
*/
|
| 7 |
export const i18n = {
|
| 8 |
sv: {
|
| 9 |
ontoTitle: "Ontologi:",
|
|
|
|
| 10 |
engineTitle: "Grafmotor:",
|
| 11 |
viewTitle: "Perspektiv:",
|
| 12 |
viewSmrm: "Ingenjör (SMRM)",
|
| 13 |
viewLogical: "Logisk (SMIM)",
|
| 14 |
+
viewPhysicalLpg: "Fysisk LPG (SMS)",
|
| 15 |
+
viewPhysicalRdf: "Fysisk RDF (SMS)",
|
| 16 |
viewSmrmCaption: "Ingenjörsperspektiv (SMRM)",
|
| 17 |
viewLogicalCaption: "Logiskt perspektiv (SMIM)",
|
| 18 |
+
viewPhysicalLpgCaption: "Fysiskt LPG-perspektiv (SMS)",
|
| 19 |
+
viewPhysicalRdfCaption: "Fysiskt RDF-perspektiv (SMS)",
|
| 20 |
stepBtn: "Lås steg (T+1)",
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
history: "Historik",
|
| 22 |
titleBom: "Struktur (BOM)",
|
| 23 |
+
emptyBom: "Inga aktiva enheter vid T = ",
|
| 24 |
titleStorage: "Under huven",
|
| 25 |
+
nodeStoreTitle: "Noder (Node Store)",
|
| 26 |
+
edgeStoreTitle: "Relationer (Relationship Store)",
|
| 27 |
activeStatus: "Aktiv",
|
| 28 |
closedStatus: "Avslutad",
|
| 29 |
sourceCol: "Källa",
|
| 30 |
targetCol: "Mål",
|
| 31 |
statusCol: "Status",
|
|
|
|
| 32 |
saveBtn: "💾 Spara JSON",
|
| 33 |
loadBtn: "📂 Ladda JSON",
|
| 34 |
queryTitle: "Databasfråga",
|
| 35 |
+
queryDesc: "Aktuellt tillstånd & defekter (T = ",
|
| 36 |
+
queryResultTitle: "Exekveringsresultat (Live):",
|
| 37 |
+
queryDefectsFound: "Defekta komponenter på nuvarande plats:",
|
| 38 |
ontoNaiveNode: "Naiv Graf (Node)",
|
| 39 |
ontoNaiveEdge: "Naiv Graf (Edge)",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
tooltips: {
|
| 41 |
+
urmsm: "<b>URMSM (Dimension & Aspect)</b><br/>Definierar intressentens perspektiv (ISO 42010 Viewpoint). Ingenjören ser produktnedbrytningen (BOM) och aktiva defekter vid tidpunkten T.",
|
| 42 |
+
smrm: "<b>SMRM (Concept Repository)</b><br/>Domänontologi och gemensam begreppsbank (Vehicle, Slot, Part, Warehouse, Defect).",
|
| 43 |
+
smim: "<b>SMIM (Information Elements & Rules)</b><br/>Top-Level Ontology (TLO). Formella regler för tid och identitet: 3D-kontinuanter (BFO) eller 4D-rumtidsindivider (ISO 15926).",
|
| 44 |
+
sms: "<b>SMS (Data in Tool)</b><br/>Den fysiska datalagringen: Labeled Property Graph (Neo4j / Cypher) kontra W3C RDF Triples (Turtle / SPARQL)."
|
| 45 |
},
|
| 46 |
legend: {
|
| 47 |
title: "Teckenförklaring",
|
| 48 |
nodes: "Noder",
|
| 49 |
edges: "Relationer",
|
| 50 |
+
bfoSys: "Continuant / System (:MaterialEntity)",
|
| 51 |
+
bfoElem: "Continuant / Komponent (:MaterialEntity)",
|
| 52 |
+
bfoActiveEdge: "Aktiv relation [giltig nu]",
|
| 53 |
bfoPastEdge: "Historisk relation (avslutad)",
|
| 54 |
+
bfoQuality: "Kvalitet / Defekt (:Quality)",
|
| 55 |
bfoInheres: "inheres_in (egenskap i bärare)",
|
| 56 |
+
iso4d: "4D-individ (:ActualIndividual)",
|
| 57 |
+
isoState: "Temporal del / Tillstånd (:State)",
|
| 58 |
+
isoPartRel: "Komposition / Montering",
|
| 59 |
isoSliceRel: "temporalPartOf (del av 4D-individ)",
|
| 60 |
isoTransitionRel: "TRANSITION_TO (tillståndskedja i LPG)",
|
| 61 |
+
isoDefect: "Defekt (:Defect)",
|
| 62 |
isoDefectRel: "has_defect (tillståndsklassificering)",
|
| 63 |
rdfStmt: "Reifiering (_:statement)",
|
| 64 |
+
rdfLit: "Literal (xsd:integer / xsd:string)",
|
| 65 |
rdfPred: "RDF-predikat",
|
| 66 |
naiveVehicle: "Fordon (:Vehicle)",
|
| 67 |
naiveSlot: "Plats (:Slot)",
|
|
|
|
| 69 |
naiveDefect: "Defekt (:Defect)",
|
| 70 |
naiveWarehouse: "Lager (:Warehouse)",
|
| 71 |
relHasSlot: "has_slot (fordonsslot)",
|
| 72 |
+
relMountedIn: "mounted_in / has_part",
|
| 73 |
relStoredAt: "stored_at (lagrad på)",
|
| 74 |
relHasDefect: "has_defect (identifierad defekt)"
|
| 75 |
}
|
| 76 |
},
|
| 77 |
en: {
|
| 78 |
ontoTitle: "Ontology:",
|
|
|
|
| 79 |
engineTitle: "Renderer:",
|
| 80 |
viewTitle: "Perspective:",
|
| 81 |
viewSmrm: "Engineer (SMRM)",
|
| 82 |
viewLogical: "Logical (SMIM)",
|
| 83 |
+
viewPhysicalLpg: "Physical LPG (SMS)",
|
| 84 |
+
viewPhysicalRdf: "Physical RDF (SMS)",
|
| 85 |
viewSmrmCaption: "Engineer Perspective (SMRM)",
|
| 86 |
viewLogicalCaption: "Logical Perspective (SMIM)",
|
| 87 |
+
viewPhysicalLpgCaption: "Physical LPG Perspective (SMS)",
|
| 88 |
+
viewPhysicalRdfCaption: "Physical RDF Perspective (SMS)",
|
| 89 |
stepBtn: "Commit step (T+1)",
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
history: "History",
|
| 91 |
titleBom: "BOM Tree Structure",
|
| 92 |
+
emptyBom: "No active assets at T = ",
|
| 93 |
titleStorage: "Under the Bonnet",
|
| 94 |
+
nodeStoreTitle: "Nodes (Node Store)",
|
| 95 |
+
edgeStoreTitle: "Relationships (Relationship Store)",
|
| 96 |
activeStatus: "Active",
|
| 97 |
closedStatus: "Closed",
|
| 98 |
sourceCol: "Source",
|
| 99 |
targetCol: "Target",
|
| 100 |
statusCol: "Status",
|
|
|
|
| 101 |
saveBtn: "💾 Save JSON",
|
| 102 |
loadBtn: "📂 Load JSON",
|
| 103 |
queryTitle: "Database Query",
|
| 104 |
+
queryDesc: "Current structure & defects (T = ",
|
| 105 |
+
queryResultTitle: "Live Query Output:",
|
| 106 |
+
queryDefectsFound: "Defective parts at current location:",
|
| 107 |
ontoNaiveNode: "Naive Graph (Node)",
|
| 108 |
ontoNaiveEdge: "Naive Graph (Edge)",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 109 |
tooltips: {
|
| 110 |
+
urmsm: "<b>URMSM (Dimension & Aspect)</b><br/>Captures the stakeholder viewpoint (ISO 42010). The engineer sees a clean BOM breakdown and active defects at time T, decoupled from graph mechanics.",
|
| 111 |
+
smrm: "<b>SMRM (Concept Repository)</b><br/>Domain Ontology: shared engineering lexicon (Vehicle, Slot, Part, Warehouse, Defect).",
|
| 112 |
smim: "<b>SMIM (Information Elements & Rules)</b><br/>Top-Level Ontology (TLO). Axiomatic rules governing time and identity: 3D Continuants (BFO) vs 4D Spatio-Temporal Extents (ISO 15926).",
|
| 113 |
+
sms: "<b>SMS (Data in Tool)</b><br/>Low-level physical IT storage: Labeled Property Graphs (Neo4j / Cypher) vs W3C RDF Triples (Turtle / SPARQL)."
|
| 114 |
},
|
| 115 |
legend: {
|
| 116 |
title: "Graph Legend",
|
| 117 |
nodes: "Node Types",
|
| 118 |
edges: "Relationships",
|
| 119 |
+
bfoSys: "Continuant / System (:MaterialEntity)",
|
| 120 |
+
bfoElem: "Continuant / Component (:MaterialEntity)",
|
| 121 |
+
bfoActiveEdge: "Active relation [valid now]",
|
| 122 |
bfoPastEdge: "Historical relation (closed)",
|
| 123 |
bfoQuality: "Dependent Continuant (:Quality)",
|
| 124 |
bfoInheres: "inheres_in (Quality inherence)",
|
| 125 |
+
iso4d: "4D Individual (:ActualIndividual)",
|
| 126 |
+
isoState: "Temporal Part / State (:State)",
|
| 127 |
+
isoPartRel: "Composition / Assembly",
|
| 128 |
isoSliceRel: "temporalPartOf (Extent link)",
|
| 129 |
isoTransitionRel: "TRANSITION_TO (State Chain in LPG)",
|
| 130 |
+
isoDefect: "Defect (:Defect)",
|
| 131 |
isoDefectRel: "has_defect (State Classification)",
|
| 132 |
rdfStmt: "Reification Node (_:statement)",
|
| 133 |
+
rdfLit: "Literal (xsd:integer / xsd:string)",
|
| 134 |
rdfPred: "RDF Predicate",
|
| 135 |
naiveVehicle: "Vehicle (:Vehicle)",
|
| 136 |
naiveSlot: "Slot (:Slot)",
|
|
|
|
| 138 |
naiveDefect: "Defect (:Defect)",
|
| 139 |
naiveWarehouse: "Warehouse (:Warehouse)",
|
| 140 |
relHasSlot: "has_slot (Vehicle Slot)",
|
| 141 |
+
relMountedIn: "mounted_in / has_part",
|
| 142 |
relStoredAt: "stored_at (Stored in Warehouse)",
|
| 143 |
relHasDefect: "has_defect (Identified Defect)"
|
| 144 |
}
|
| 145 |
},
|
| 146 |
ru: {
|
| 147 |
ontoTitle: "Онтология:",
|
|
|
|
| 148 |
engineTitle: "Движок:",
|
| 149 |
viewTitle: "Ракурс:",
|
| 150 |
viewSmrm: "Инженер (SMRM)",
|
| 151 |
viewLogical: "Логический (SMIM)",
|
| 152 |
+
viewPhysicalLpg: "Физический LPG (SMS)",
|
| 153 |
+
viewPhysicalRdf: "Физический RDF (SMS)",
|
| 154 |
viewSmrmCaption: "Инженерный ракурс (SMRM)",
|
| 155 |
viewLogicalCaption: "Логический ракурс (SMIM)",
|
| 156 |
+
viewPhysicalLpgCaption: "Физический ракурс LPG (SMS)",
|
| 157 |
+
viewPhysicalRdfCaption: "Физический ракурс RDF (SMS)",
|
| 158 |
stepBtn: "Зафиксировать шаг (T+1)",
|
|
|
|
|
|
|
|
|
|
|
|
|
| 159 |
history: "История",
|
| 160 |
titleBom: "Состав изделия (BOM)",
|
| 161 |
+
emptyBom: "Нет активных объектов при T = ",
|
| 162 |
titleStorage: "Под капотом",
|
| 163 |
+
nodeStoreTitle: "Узлы (Node Store)",
|
| 164 |
+
edgeStoreTitle: "Связи (Relationship Store)",
|
| 165 |
activeStatus: "Активна",
|
| 166 |
closedStatus: "Закрыта",
|
| 167 |
sourceCol: "Источник",
|
| 168 |
targetCol: "Цель",
|
| 169 |
statusCol: "Статус",
|
|
|
|
| 170 |
saveBtn: "💾 Сохранить JSON",
|
| 171 |
loadBtn: "📂 Загрузить JSON",
|
| 172 |
queryTitle: "Запрос к БД",
|
| 173 |
+
queryDesc: "Текущий состав и дефекты (T = ",
|
| 174 |
+
queryResultTitle: "Результат выполнения запроса (Live):",
|
| 175 |
+
queryDefectsFound: "Дефектных деталей на текущей позиции:",
|
| 176 |
ontoNaiveNode: "Наивный граф (Узел)",
|
| 177 |
ontoNaiveEdge: "Наивный граф (Ребро)",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 178 |
tooltips: {
|
| 179 |
+
urmsm: "<b>URMSM (Dimension & Aspect)</b><br/>Точка зрения заинтересованного лица (ISO 42010). Инженер видит чистый срез BOM и активные дефекты на момент T без деталей реализации БД.",
|
| 180 |
+
smrm: "<b>SMRM (Concept Repository)</b><br/>Онтология предметной области: общий инженерный словарь (Vehicle, Slot, Part, Warehouse, Defect).",
|
| 181 |
+
smim: "<b>SMIM (Information Elements & Rules)</b><br/>Верхнеуровневая онтология (TLO). Математические аксиомы времени и идентичности: 3D-континуанты (BFO) или 4D-экстенты (ISO 15926).",
|
| 182 |
+
sms: "<b>SMS (Data in Tool)</b><br/>Физическое хранение данных в СУБД: граф со свойствами на ребрах (LPG / Cypher) против атомарных триплетов W3C RDF (Turtle / SPARQL)."
|
| 183 |
},
|
| 184 |
legend: {
|
| 185 |
title: "Легенда графа",
|
| 186 |
nodes: "Типы узлов",
|
| 187 |
edges: "Типы связей",
|
| 188 |
+
bfoSys: "Континуант / Система (:MaterialEntity)",
|
| 189 |
+
bfoElem: "Континуант / Компонент (:MaterialEntity)",
|
| 190 |
bfoActiveEdge: "Активная связь [действует сейчас]",
|
| 191 |
bfoPastEdge: "Архивная связь (закрыта)",
|
| 192 |
bfoQuality: "Зависимый континуант (:Quality)",
|
| 193 |
bfoInheres: "inheres_in (присущ носителю)",
|
| 194 |
+
iso4d: "4D-индивид (:ActualIndividual)",
|
| 195 |
+
isoState: "Темпоральный срез / Состояние (:State)",
|
| 196 |
+
isoPartRel: "Вневременной состав / Монтаж",
|
| 197 |
isoSliceRel: "temporalPartOf (связь с 4D-целым)",
|
| 198 |
isoTransitionRel: "TRANSITION_TO (цепочка состояний в LPG)",
|
| 199 |
+
isoDefect: "Дефект (:Defect)",
|
| 200 |
isoDefectRel: "has_defect (классификация среза)",
|
| 201 |
rdfStmt: "Узел реификации (_:statement)",
|
| 202 |
+
rdfLit: "Литерал (xsd:integer / xsd:string)",
|
| 203 |
rdfPred: "RDF-предикат",
|
| 204 |
naiveVehicle: "Транспорт (:Vehicle)",
|
| 205 |
naiveSlot: "Слот (:Slot)",
|
|
|
|
| 207 |
naiveDefect: "Дефект (:Defect)",
|
| 208 |
naiveWarehouse: "Склад (:Warehouse)",
|
| 209 |
relHasSlot: "has_slot (слот транспорта)",
|
| 210 |
+
relMountedIn: "mounted_in / has_part",
|
| 211 |
relStoredAt: "stored_at (хранится на складе)",
|
| 212 |
relHasDefect: "has_defect (выявлен дефект)"
|
| 213 |
}
|
js/renderers/mermaid-adapter.js
CHANGED
|
@@ -2,79 +2,64 @@
|
|
| 2 |
* MERMAID RENDERER ADAPTER
|
| 3 |
* ------------------------
|
| 4 |
* Translates the abstract Semantic Graph into Mermaid flowchart syntax
|
| 5 |
-
* and renders it to the DOM
|
| 6 |
-
* on the abstract node/edge types, decoupled from the domain logic.
|
| 7 |
*/
|
| 8 |
|
| 9 |
-
|
|
|
|
| 10 |
let isRendering = false;
|
| 11 |
let pendingDefinition = null;
|
| 12 |
let renderCounter = 0;
|
| 13 |
|
| 14 |
/**
|
| 15 |
* Updates the Mermaid graph in the specified container.
|
| 16 |
-
*
|
| 17 |
* @param {Object} semanticGraph - { nodes: [], edges: [] }
|
| 18 |
* @param {HTMLElement} container - The DOM element to host the SVG.
|
| 19 |
*/
|
| 20 |
export async function updateMermaidNetwork(semanticGraph, container) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
const chartDefinition = buildMermaidDefinition(semanticGraph);
|
| 22 |
|
| 23 |
-
// If a render is already in progress, queue this definition and exit
|
| 24 |
if (isRendering) {
|
| 25 |
pendingDefinition = chartDefinition;
|
| 26 |
return;
|
| 27 |
}
|
| 28 |
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
try {
|
| 32 |
-
const renderId = 'mermaid_svg_' + (++renderCounter);
|
| 33 |
-
// We assume 'mermaid' is globally available from the CDN in index.html
|
| 34 |
-
const { svg } = await mermaid.render(renderId, chartDefinition);
|
| 35 |
-
container.innerHTML = svg;
|
| 36 |
-
} catch (err) {
|
| 37 |
-
console.error("[Mermaid Adapter] Render Error:", err);
|
| 38 |
-
} finally {
|
| 39 |
-
isRendering = false;
|
| 40 |
-
// If a new graph was requested while we were rendering, process it now
|
| 41 |
-
if (pendingDefinition) {
|
| 42 |
-
const nextDefinition = pendingDefinition;
|
| 43 |
-
pendingDefinition = null;
|
| 44 |
-
// We pass a dummy object here because the definition string is already built,
|
| 45 |
-
// but to keep the signature clean for this recursive pattern, it's better
|
| 46 |
-
// to extract the render call. For simplicity, we just rebuild it:
|
| 47 |
-
executePendingRender(nextDefinition, container);
|
| 48 |
-
}
|
| 49 |
-
}
|
| 50 |
}
|
| 51 |
|
| 52 |
-
async function
|
| 53 |
isRendering = true;
|
| 54 |
try {
|
| 55 |
-
const
|
|
|
|
| 56 |
container.innerHTML = svg;
|
| 57 |
-
} catch (
|
| 58 |
-
console.error(
|
| 59 |
} finally {
|
| 60 |
isRendering = false;
|
| 61 |
if (pendingDefinition) {
|
| 62 |
const next = pendingDefinition;
|
| 63 |
pendingDefinition = null;
|
| 64 |
-
|
| 65 |
}
|
| 66 |
}
|
| 67 |
}
|
| 68 |
|
| 69 |
/**
|
| 70 |
* Builds the string definition for the Mermaid flowchart.
|
| 71 |
-
*
|
| 72 |
-
*
|
| 73 |
* @param {Object} semanticGraph - Standardised Semantic Graph.
|
| 74 |
* @returns {string} Mermaid chart definition string.
|
| 75 |
*/
|
| 76 |
function buildMermaidDefinition(semanticGraph) {
|
| 77 |
-
// 1. Initialise the graph direction and styling classes
|
| 78 |
let def = 'flowchart TD\n';
|
| 79 |
def += ' classDef default fill:#f8fafc,stroke:#cbd5e1,color:#334155,stroke-width:1px\n';
|
| 80 |
def += ' classDef bfoSys fill:#0284c7,stroke:#0369a1,color:#ffffff,stroke-width:2px\n';
|
|
@@ -84,26 +69,28 @@ function buildMermaidDefinition(semanticGraph) {
|
|
| 84 |
def += ' classDef rdfLit fill:#f1f5f9,stroke:#cbd5e1,color:#334155,stroke-dasharray:3 3\n';
|
| 85 |
def += ' classDef iso4d fill:#4f46e5,stroke:#3730a3,color:#ffffff,stroke-width:2px\n';
|
| 86 |
def += ' classDef isoState fill:#0d9488,stroke:#0f766e,color:#ffffff,stroke-width:1px\n';
|
|
|
|
| 87 |
def += ' classDef naiveVehicle fill:#0284c7,stroke:#0369a1,color:#ffffff,stroke-width:2px\n';
|
| 88 |
def += ' classDef naiveSlot fill:#64748b,stroke:#475569,color:#ffffff,stroke-width:1px\n';
|
| 89 |
def += ' classDef naivePart fill:#10b981,stroke:#059669,color:#ffffff,stroke-width:2px\n';
|
| 90 |
def += ' classDef naiveDefect fill:#ef4444,stroke:#dc2626,color:#ffffff,stroke-width:2px\n';
|
| 91 |
def += ' classDef naiveWarehouse fill:#f97316,stroke:#ea580c,color:#ffffff,stroke-width:2px\n\n';
|
| 92 |
|
| 93 |
-
// 2. Add Nodes directly from the Semantic Graph (pure renderer)
|
| 94 |
(semanticGraph.nodes || []).forEach(node => {
|
| 95 |
const safeId = sanitizeId(node.id);
|
| 96 |
const styleClass = getNodeClass(node);
|
| 97 |
-
let labelText = node.label ||
|
|
|
|
| 98 |
if (node.properties) {
|
| 99 |
const customProps = Object.entries(node.properties).filter(
|
| 100 |
-
([k]) => !
|
| 101 |
);
|
| 102 |
if (customProps.length > 0) {
|
| 103 |
const propStr = customProps.map(([k, v]) => `${k}: ${v}`).join(', ');
|
| 104 |
labelText = `${labelText} {${propStr}}`;
|
| 105 |
}
|
| 106 |
}
|
|
|
|
| 107 |
const safeLabel = labelText
|
| 108 |
.replace(/\n/g, '<br/>')
|
| 109 |
.replace(/"/g, '"');
|
|
@@ -113,10 +100,10 @@ function buildMermaidDefinition(semanticGraph) {
|
|
| 113 |
|
| 114 |
const entityType = node.type || node.properties?.entityClass;
|
| 115 |
if (entityType === 'TemporalPartMembership' || entityType === 'RelationalQuality' || entityType === 'Defect' || /^def/i.test(node.id)) {
|
| 116 |
-
shapeOpen = '{{"';
|
| 117 |
shapeClose = '"}}';
|
| 118 |
} else if (entityType === 'Literal' || entityType === 'Quality' || entityType === 'DependentContinuant' || /^qual/i.test(node.id)) {
|
| 119 |
-
shapeOpen = '(["';
|
| 120 |
shapeClose = '"])';
|
| 121 |
}
|
| 122 |
|
|
@@ -125,7 +112,6 @@ function buildMermaidDefinition(semanticGraph) {
|
|
| 125 |
|
| 126 |
def += '\n';
|
| 127 |
|
| 128 |
-
// 4. Add Edges and collect explicit linkStyle declarations
|
| 129 |
const linkStyles = [];
|
| 130 |
(semanticGraph.edges || []).forEach((edge, idx) => {
|
| 131 |
const safeFrom = sanitizeId(edge.from);
|
|
@@ -134,16 +120,15 @@ function buildMermaidDefinition(semanticGraph) {
|
|
| 134 |
|
| 135 |
let linkArrow = '-->';
|
| 136 |
if (edge.type === 'transition') {
|
| 137 |
-
linkArrow = '==>';
|
| 138 |
} else if (!isCurrent || edge.type === 'classification' || edge.type === 'reification' || edge.type === 'predicate') {
|
| 139 |
-
linkArrow = '-.->';
|
| 140 |
}
|
| 141 |
|
| 142 |
-
// Format edge label and append custom properties (e.g. defect observations) if present
|
| 143 |
let rawLabel = edge.label || '';
|
| 144 |
if (edge.properties) {
|
| 145 |
const customEdgeProps = Object.entries(edge.properties).filter(
|
| 146 |
-
([k]) => !
|
| 147 |
);
|
| 148 |
if (customEdgeProps.length > 0) {
|
| 149 |
const propStr = customEdgeProps.map(([k, v]) => `${k}: ${v}`).join(', ');
|
|
@@ -151,12 +136,9 @@ function buildMermaidDefinition(semanticGraph) {
|
|
| 151 |
}
|
| 152 |
}
|
| 153 |
|
| 154 |
-
// Escape quotes in the label
|
| 155 |
const safeEdgeLabel = rawLabel.replace(/"/g, "'");
|
| 156 |
-
|
| 157 |
def += ` ${safeFrom} ${linkArrow}|"${safeEdgeLabel}"| ${safeTo}\n`;
|
| 158 |
|
| 159 |
-
// Calculate edge colour and line styling to match the legend
|
| 160 |
const style = getMermaidEdgeStyle(edge);
|
| 161 |
let styleDecl = `stroke:${style.stroke},stroke-width:${style.width}px`;
|
| 162 |
if (style.dash) {
|
|
@@ -172,13 +154,6 @@ function buildMermaidDefinition(semanticGraph) {
|
|
| 172 |
return def;
|
| 173 |
}
|
| 174 |
|
| 175 |
-
/**
|
| 176 |
-
* Determines the stroke colour, width, and dash style for a Mermaid edge.
|
| 177 |
-
* Perfectly synchronises canvas edge rendering with the UI legend colours.
|
| 178 |
-
*
|
| 179 |
-
* @param {Object} edge - Canonical semantic edge.
|
| 180 |
-
* @returns {{ stroke: string, width: number, dash?: string }} Styling descriptor.
|
| 181 |
-
*/
|
| 182 |
function getMermaidEdgeStyle(edge) {
|
| 183 |
const isCurrent = edge.properties?.isCurrent !== false;
|
| 184 |
|
|
@@ -190,33 +165,23 @@ function getMermaidEdgeStyle(edge) {
|
|
| 190 |
const label = (edge.label || '').toLowerCase();
|
| 191 |
|
| 192 |
if (rel === 'has_defect' || label === 'has_defect' || label === 'defect') {
|
| 193 |
-
return { stroke: '#ef4444', width: 2 };
|
| 194 |
}
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
return { stroke: '#f59e0b', width: 2 }; // Amber
|
| 198 |
}
|
| 199 |
-
|
| 200 |
if (rel === 'has_slot' || label === 'has_slot') {
|
| 201 |
-
return { stroke: '#0284c7', width: 2 };
|
| 202 |
}
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
return { stroke: '#10b981', width: 2 }; // Green
|
| 206 |
}
|
| 207 |
-
|
| 208 |
if (rel === 'stored_at' || label === 'stored_at') {
|
| 209 |
-
return { stroke: '#f97316', width: 2 };
|
| 210 |
-
}
|
| 211 |
-
|
| 212 |
-
if (rel === 'composition' || rel === 'has_part' || label === 'has_part' || label === 'haspart' || label === 'partof') {
|
| 213 |
-
return { stroke: '#10b981', width: 2 }; // Emerald green
|
| 214 |
}
|
| 215 |
-
|
| 216 |
if (rel === 'transition') {
|
| 217 |
-
return { stroke: '#f59e0b', width: 2 };
|
| 218 |
}
|
| 219 |
-
|
| 220 |
if (rel === 'classification' || rel === 'reification' || rel === 'predicate' || label === 'temporalpartof') {
|
| 221 |
return { stroke: '#94a3b8', width: 1, dash: '3 3' };
|
| 222 |
}
|
|
@@ -224,9 +189,6 @@ function getMermaidEdgeStyle(edge) {
|
|
| 224 |
return { stroke: '#94a3b8', width: 1 };
|
| 225 |
}
|
| 226 |
|
| 227 |
-
/**
|
| 228 |
-
* Maps ontological node types to Mermaid CSS classes.
|
| 229 |
-
*/
|
| 230 |
function getNodeClass(node) {
|
| 231 |
const entityType = node.type || node.properties?.entityClass || node.properties?.entityType;
|
| 232 |
|
|
@@ -242,7 +204,9 @@ function getNodeClass(node) {
|
|
| 242 |
case 'Warehouse':
|
| 243 |
return 'naiveWarehouse';
|
| 244 |
case 'MaterialEntity':
|
| 245 |
-
return node.properties?.entityType === 'system' ?
|
|
|
|
|
|
|
| 246 |
case 'Quality':
|
| 247 |
case 'DependentContinuant':
|
| 248 |
return 'bfoQuality';
|
|
@@ -254,7 +218,7 @@ function getNodeClass(node) {
|
|
| 254 |
case 'ActualIndividual':
|
| 255 |
return 'iso4d';
|
| 256 |
case 'State':
|
| 257 |
-
return 'isoState';
|
| 258 |
default:
|
| 259 |
if (/^def/i.test(node.id)) return 'naiveDefect';
|
| 260 |
if (/^qual/i.test(node.id)) return 'bfoQuality';
|
|
@@ -262,9 +226,6 @@ function getNodeClass(node) {
|
|
| 262 |
}
|
| 263 |
}
|
| 264 |
|
| 265 |
-
/**
|
| 266 |
-
* Removes unsupported characters from IDs for Mermaid compatibility.
|
| 267 |
-
*/
|
| 268 |
function sanitizeId(id) {
|
| 269 |
-
return id.replace(/[^a-zA-Z0-9_]/g, '_');
|
| 270 |
}
|
|
|
|
| 2 |
* MERMAID RENDERER ADAPTER
|
| 3 |
* ------------------------
|
| 4 |
* Translates the abstract Semantic Graph into Mermaid flowchart syntax
|
| 5 |
+
* and renders it to the DOM without leaking internal metadata properties.
|
|
|
|
| 6 |
*/
|
| 7 |
|
| 8 |
+
import { INTERNAL_PROPERTY_KEYS } from '../smrm/definitions.js';
|
| 9 |
+
|
| 10 |
let isRendering = false;
|
| 11 |
let pendingDefinition = null;
|
| 12 |
let renderCounter = 0;
|
| 13 |
|
| 14 |
/**
|
| 15 |
* Updates the Mermaid graph in the specified container.
|
| 16 |
+
*
|
| 17 |
* @param {Object} semanticGraph - { nodes: [], edges: [] }
|
| 18 |
* @param {HTMLElement} container - The DOM element to host the SVG.
|
| 19 |
*/
|
| 20 |
export async function updateMermaidNetwork(semanticGraph, container) {
|
| 21 |
+
if (!container) return;
|
| 22 |
+
|
| 23 |
+
if (!semanticGraph || !Array.isArray(semanticGraph.nodes) || semanticGraph.nodes.length === 0) {
|
| 24 |
+
container.innerHTML = `<div style="margin: auto; color: #94a3b8; font-size: 13px; font-weight: 500;">Empty Graph Canvas (T = 0)</div>`;
|
| 25 |
+
return;
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
const chartDefinition = buildMermaidDefinition(semanticGraph);
|
| 29 |
|
|
|
|
| 30 |
if (isRendering) {
|
| 31 |
pendingDefinition = chartDefinition;
|
| 32 |
return;
|
| 33 |
}
|
| 34 |
|
| 35 |
+
await executeRender(chartDefinition, container);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
}
|
| 37 |
|
| 38 |
+
async function executeRender(definition, container) {
|
| 39 |
isRendering = true;
|
| 40 |
try {
|
| 41 |
+
const renderId = 'mermaid_svg_' + (++renderCounter);
|
| 42 |
+
const { svg } = await mermaid.render(renderId, definition);
|
| 43 |
container.innerHTML = svg;
|
| 44 |
+
} catch (err) {
|
| 45 |
+
console.error('[Mermaid Adapter] Render Error:', err);
|
| 46 |
} finally {
|
| 47 |
isRendering = false;
|
| 48 |
if (pendingDefinition) {
|
| 49 |
const next = pendingDefinition;
|
| 50 |
pendingDefinition = null;
|
| 51 |
+
await executeRender(next, container);
|
| 52 |
}
|
| 53 |
}
|
| 54 |
}
|
| 55 |
|
| 56 |
/**
|
| 57 |
* Builds the string definition for the Mermaid flowchart.
|
| 58 |
+
*
|
|
|
|
| 59 |
* @param {Object} semanticGraph - Standardised Semantic Graph.
|
| 60 |
* @returns {string} Mermaid chart definition string.
|
| 61 |
*/
|
| 62 |
function buildMermaidDefinition(semanticGraph) {
|
|
|
|
| 63 |
let def = 'flowchart TD\n';
|
| 64 |
def += ' classDef default fill:#f8fafc,stroke:#cbd5e1,color:#334155,stroke-width:1px\n';
|
| 65 |
def += ' classDef bfoSys fill:#0284c7,stroke:#0369a1,color:#ffffff,stroke-width:2px\n';
|
|
|
|
| 69 |
def += ' classDef rdfLit fill:#f1f5f9,stroke:#cbd5e1,color:#334155,stroke-dasharray:3 3\n';
|
| 70 |
def += ' classDef iso4d fill:#4f46e5,stroke:#3730a3,color:#ffffff,stroke-width:2px\n';
|
| 71 |
def += ' classDef isoState fill:#0d9488,stroke:#0f766e,color:#ffffff,stroke-width:1px\n';
|
| 72 |
+
def += ' classDef isoStatePast fill:#94a3b8,stroke:#64748b,color:#ffffff,stroke-width:1px,stroke-dasharray:3 3\n';
|
| 73 |
def += ' classDef naiveVehicle fill:#0284c7,stroke:#0369a1,color:#ffffff,stroke-width:2px\n';
|
| 74 |
def += ' classDef naiveSlot fill:#64748b,stroke:#475569,color:#ffffff,stroke-width:1px\n';
|
| 75 |
def += ' classDef naivePart fill:#10b981,stroke:#059669,color:#ffffff,stroke-width:2px\n';
|
| 76 |
def += ' classDef naiveDefect fill:#ef4444,stroke:#dc2626,color:#ffffff,stroke-width:2px\n';
|
| 77 |
def += ' classDef naiveWarehouse fill:#f97316,stroke:#ea580c,color:#ffffff,stroke-width:2px\n\n';
|
| 78 |
|
|
|
|
| 79 |
(semanticGraph.nodes || []).forEach(node => {
|
| 80 |
const safeId = sanitizeId(node.id);
|
| 81 |
const styleClass = getNodeClass(node);
|
| 82 |
+
let labelText = node.label || node.id;
|
| 83 |
+
|
| 84 |
if (node.properties) {
|
| 85 |
const customProps = Object.entries(node.properties).filter(
|
| 86 |
+
([k, v]) => !INTERNAL_PROPERTY_KEYS.includes(k) && v !== undefined && v !== null
|
| 87 |
);
|
| 88 |
if (customProps.length > 0) {
|
| 89 |
const propStr = customProps.map(([k, v]) => `${k}: ${v}`).join(', ');
|
| 90 |
labelText = `${labelText} {${propStr}}`;
|
| 91 |
}
|
| 92 |
}
|
| 93 |
+
|
| 94 |
const safeLabel = labelText
|
| 95 |
.replace(/\n/g, '<br/>')
|
| 96 |
.replace(/"/g, '"');
|
|
|
|
| 100 |
|
| 101 |
const entityType = node.type || node.properties?.entityClass;
|
| 102 |
if (entityType === 'TemporalPartMembership' || entityType === 'RelationalQuality' || entityType === 'Defect' || /^def/i.test(node.id)) {
|
| 103 |
+
shapeOpen = '{{"';
|
| 104 |
shapeClose = '"}}';
|
| 105 |
} else if (entityType === 'Literal' || entityType === 'Quality' || entityType === 'DependentContinuant' || /^qual/i.test(node.id)) {
|
| 106 |
+
shapeOpen = '(["';
|
| 107 |
shapeClose = '"])';
|
| 108 |
}
|
| 109 |
|
|
|
|
| 112 |
|
| 113 |
def += '\n';
|
| 114 |
|
|
|
|
| 115 |
const linkStyles = [];
|
| 116 |
(semanticGraph.edges || []).forEach((edge, idx) => {
|
| 117 |
const safeFrom = sanitizeId(edge.from);
|
|
|
|
| 120 |
|
| 121 |
let linkArrow = '-->';
|
| 122 |
if (edge.type === 'transition') {
|
| 123 |
+
linkArrow = '==>';
|
| 124 |
} else if (!isCurrent || edge.type === 'classification' || edge.type === 'reification' || edge.type === 'predicate') {
|
| 125 |
+
linkArrow = '-.->';
|
| 126 |
}
|
| 127 |
|
|
|
|
| 128 |
let rawLabel = edge.label || '';
|
| 129 |
if (edge.properties) {
|
| 130 |
const customEdgeProps = Object.entries(edge.properties).filter(
|
| 131 |
+
([k, v]) => !INTERNAL_PROPERTY_KEYS.includes(k) && v !== undefined && v !== null
|
| 132 |
);
|
| 133 |
if (customEdgeProps.length > 0) {
|
| 134 |
const propStr = customEdgeProps.map(([k, v]) => `${k}: ${v}`).join(', ');
|
|
|
|
| 136 |
}
|
| 137 |
}
|
| 138 |
|
|
|
|
| 139 |
const safeEdgeLabel = rawLabel.replace(/"/g, "'");
|
|
|
|
| 140 |
def += ` ${safeFrom} ${linkArrow}|"${safeEdgeLabel}"| ${safeTo}\n`;
|
| 141 |
|
|
|
|
| 142 |
const style = getMermaidEdgeStyle(edge);
|
| 143 |
let styleDecl = `stroke:${style.stroke},stroke-width:${style.width}px`;
|
| 144 |
if (style.dash) {
|
|
|
|
| 154 |
return def;
|
| 155 |
}
|
| 156 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 157 |
function getMermaidEdgeStyle(edge) {
|
| 158 |
const isCurrent = edge.properties?.isCurrent !== false;
|
| 159 |
|
|
|
|
| 165 |
const label = (edge.label || '').toLowerCase();
|
| 166 |
|
| 167 |
if (rel === 'has_defect' || label === 'has_defect' || label === 'defect') {
|
| 168 |
+
return { stroke: '#ef4444', width: 2 };
|
| 169 |
}
|
| 170 |
+
if (rel === 'inherence' || label === 'inheres_in') {
|
| 171 |
+
return { stroke: '#f59e0b', width: 2 };
|
|
|
|
| 172 |
}
|
|
|
|
| 173 |
if (rel === 'has_slot' || label === 'has_slot') {
|
| 174 |
+
return { stroke: '#0284c7', width: 2 };
|
| 175 |
}
|
| 176 |
+
if (rel === 'mounted_in' || label === 'mounted_in' || rel === 'composition' || rel === 'has_part' || label === 'has_part') {
|
| 177 |
+
return { stroke: '#10b981', width: 2 };
|
|
|
|
| 178 |
}
|
|
|
|
| 179 |
if (rel === 'stored_at' || label === 'stored_at') {
|
| 180 |
+
return { stroke: '#f97316', width: 2 };
|
|
|
|
|
|
|
|
|
|
|
|
|
| 181 |
}
|
|
|
|
| 182 |
if (rel === 'transition') {
|
| 183 |
+
return { stroke: '#f59e0b', width: 2 };
|
| 184 |
}
|
|
|
|
| 185 |
if (rel === 'classification' || rel === 'reification' || rel === 'predicate' || label === 'temporalpartof') {
|
| 186 |
return { stroke: '#94a3b8', width: 1, dash: '3 3' };
|
| 187 |
}
|
|
|
|
| 189 |
return { stroke: '#94a3b8', width: 1 };
|
| 190 |
}
|
| 191 |
|
|
|
|
|
|
|
|
|
|
| 192 |
function getNodeClass(node) {
|
| 193 |
const entityType = node.type || node.properties?.entityClass || node.properties?.entityType;
|
| 194 |
|
|
|
|
| 204 |
case 'Warehouse':
|
| 205 |
return 'naiveWarehouse';
|
| 206 |
case 'MaterialEntity':
|
| 207 |
+
return (node.properties?.entityType === 'system' || node.properties?.entityClass === 'Vehicle')
|
| 208 |
+
? 'bfoSys'
|
| 209 |
+
: 'bfoElem';
|
| 210 |
case 'Quality':
|
| 211 |
case 'DependentContinuant':
|
| 212 |
return 'bfoQuality';
|
|
|
|
| 218 |
case 'ActualIndividual':
|
| 219 |
return 'iso4d';
|
| 220 |
case 'State':
|
| 221 |
+
return node.properties?.isCurrent === false ? 'isoStatePast' : 'isoState';
|
| 222 |
default:
|
| 223 |
if (/^def/i.test(node.id)) return 'naiveDefect';
|
| 224 |
if (/^qual/i.test(node.id)) return 'bfoQuality';
|
|
|
|
| 226 |
}
|
| 227 |
}
|
| 228 |
|
|
|
|
|
|
|
|
|
|
| 229 |
function sanitizeId(id) {
|
| 230 |
+
return String(id).replace(/[^a-zA-Z0-9_]/g, '_');
|
| 231 |
}
|
js/renderers/vis-adapter.js
CHANGED
|
@@ -1,58 +1,54 @@
|
|
| 1 |
/**
|
| 2 |
* VIS.JS RENDERER ADAPTER
|
| 3 |
* -----------------------
|
| 4 |
-
*
|
| 5 |
-
*
|
| 6 |
-
* It strictly handles styling (colours, shapes, physics) based on node/edge types.
|
| 7 |
*/
|
| 8 |
|
| 9 |
-
|
|
|
|
| 10 |
let network = null;
|
| 11 |
const visNodes = new vis.DataSet();
|
| 12 |
const visEdges = new vis.DataSet();
|
| 13 |
|
| 14 |
/**
|
| 15 |
* Initialises the Vis.js network instance in the specified DOM container.
|
| 16 |
-
*
|
| 17 |
* @param {HTMLElement} container - The DOM element to host the canvas.
|
| 18 |
*/
|
| 19 |
export function initVisNetwork(container) {
|
|
|
|
| 20 |
if (network) {
|
| 21 |
network.destroy();
|
| 22 |
network = null;
|
| 23 |
}
|
|
|
|
|
|
|
| 24 |
const data = { nodes: visNodes, edges: visEdges };
|
| 25 |
network = new vis.Network(container, data, getBaseOptions(false));
|
| 26 |
}
|
| 27 |
|
| 28 |
/**
|
| 29 |
-
* Updates the network with a new Semantic Graph.
|
| 30 |
-
*
|
| 31 |
* @param {Object} semanticGraph - { nodes: [], edges: [] }
|
| 32 |
-
* @param {boolean} isComplex - Flag to adjust physics for denser graphs
|
| 33 |
*/
|
| 34 |
export function updateVisNetwork(semanticGraph, isComplex = false) {
|
| 35 |
-
if (!network)
|
| 36 |
-
console.error("[Vis Adapter] Network not initialised. Call initVisNetwork first.");
|
| 37 |
-
return;
|
| 38 |
-
}
|
| 39 |
|
| 40 |
-
// Update physics options based on graph complexity
|
| 41 |
network.setOptions(getBaseOptions(isComplex));
|
| 42 |
|
| 43 |
-
//
|
| 44 |
-
|
| 45 |
-
visEdges.clear();
|
| 46 |
-
|
| 47 |
-
// 1. Map and style Nodes directly from the Semantic Graph (pure renderer)
|
| 48 |
const styledNodes = (semanticGraph.nodes || []).map(node => {
|
|
|
|
| 49 |
const style = getNodeStyle(node);
|
| 50 |
-
|
| 51 |
-
// Display custom properties directly on the node label if present
|
| 52 |
let displayLabel = node.label || node.id;
|
| 53 |
if (node.properties) {
|
| 54 |
const customProps = Object.entries(node.properties).filter(
|
| 55 |
-
([k]) => !
|
| 56 |
);
|
| 57 |
if (customProps.length > 0) {
|
| 58 |
const propStr = customProps.map(([k, v]) => `${k}: ${v}`).join(', ');
|
|
@@ -78,34 +74,39 @@ export function updateVisNetwork(semanticGraph, isComplex = false) {
|
|
| 78 |
|
| 79 |
return styledNode;
|
| 80 |
});
|
| 81 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
|
| 83 |
// 2. Map and style Edges
|
| 84 |
-
// We need to handle overlapping edges (multiple events between same parent/child)
|
| 85 |
const pairMap = {};
|
| 86 |
-
semanticGraph.edges.forEach(edge => {
|
| 87 |
const pairKey = `${edge.from}->${edge.to}`;
|
| 88 |
if (!pairMap[pairKey]) pairMap[pairKey] = [];
|
| 89 |
pairMap[pairKey].push(edge);
|
| 90 |
});
|
| 91 |
|
|
|
|
| 92 |
const styledEdges = [];
|
|
|
|
| 93 |
Object.values(pairMap).forEach(edgesGroup => {
|
| 94 |
const count = edgesGroup.length;
|
| 95 |
-
|
| 96 |
edgesGroup.forEach((edge, idx) => {
|
| 97 |
-
|
| 98 |
-
const smoothConfig = count === 1
|
| 99 |
? { type: 'continuous', roundness: 0.2 }
|
| 100 |
: { type: 'curvedCW', roundness: Math.min(0.14 + (idx * 0.14), 0.85) };
|
| 101 |
|
| 102 |
const style = getEdgeStyle(edge);
|
| 103 |
|
| 104 |
-
|
| 105 |
-
let displayEdgeLabel = edge.label;
|
| 106 |
if (edge.properties) {
|
| 107 |
const customEdgeProps = Object.entries(edge.properties).filter(
|
| 108 |
-
([k]) => !
|
| 109 |
);
|
| 110 |
if (customEdgeProps.length > 0) {
|
| 111 |
const propStr = customEdgeProps.map(([k, v]) => `${k}: ${v}`).join(', ');
|
|
@@ -127,110 +128,59 @@ export function updateVisNetwork(semanticGraph, isComplex = false) {
|
|
| 127 |
});
|
| 128 |
});
|
| 129 |
|
| 130 |
-
visEdges.
|
|
|
|
|
|
|
|
|
|
| 131 |
}
|
| 132 |
|
| 133 |
-
/**
|
| 134 |
-
* Determines the visual style of a node based on its ontological type.
|
| 135 |
-
* Only handles canonical 3D (BFO) and 4D (ISO 15926) domain entities.
|
| 136 |
-
*
|
| 137 |
-
* @param {Object} node - Canonical semantic node.
|
| 138 |
-
* @returns {Object} Vis.js node styling object.
|
| 139 |
-
*/
|
| 140 |
function getNodeStyle(node) {
|
| 141 |
const entityType = node.type || node.properties?.entityClass || node.properties?.entityType;
|
| 142 |
|
| 143 |
switch (entityType) {
|
| 144 |
-
// Specific Manufacturing Domain Classes (Naive Mode)
|
| 145 |
case 'Vehicle':
|
| 146 |
-
return {
|
| 147 |
-
shape: 'box',
|
| 148 |
-
color: '#0284c7', // Blue
|
| 149 |
-
font: { color: '#ffffff', bold: true }
|
| 150 |
-
};
|
| 151 |
-
|
| 152 |
case 'Slot':
|
| 153 |
-
return {
|
| 154 |
-
shape: 'box',
|
| 155 |
-
color: '#64748b', // Grey
|
| 156 |
-
font: { color: '#ffffff' }
|
| 157 |
-
};
|
| 158 |
-
|
| 159 |
case 'Part':
|
| 160 |
-
return {
|
| 161 |
-
shape: 'box',
|
| 162 |
-
color: '#10b981', // Green
|
| 163 |
-
font: { color: '#ffffff', bold: true }
|
| 164 |
-
};
|
| 165 |
-
|
| 166 |
case 'Defect':
|
| 167 |
-
return {
|
| 168 |
-
shape: 'diamond',
|
| 169 |
-
size: 25,
|
| 170 |
-
color: '#ef4444', // Red
|
| 171 |
-
font: { color: '#ffffff', bold: true }
|
| 172 |
-
};
|
| 173 |
-
|
| 174 |
case 'Warehouse':
|
| 175 |
-
return {
|
| 176 |
-
shape: 'box',
|
| 177 |
-
color: '#f97316', // Orange
|
| 178 |
-
font: { color: '#ffffff', bold: true }
|
| 179 |
-
};
|
| 180 |
-
|
| 181 |
-
// BFO 3D Continuants
|
| 182 |
case 'MaterialEntity': {
|
| 183 |
-
const isSystem = node.properties?.entityType === 'system';
|
| 184 |
return {
|
| 185 |
shape: 'box',
|
| 186 |
-
color: isSystem ? '#0284c7' : '#64748b',
|
| 187 |
font: { color: '#ffffff', bold: isSystem }
|
| 188 |
};
|
| 189 |
}
|
| 190 |
-
|
| 191 |
case 'Quality':
|
| 192 |
case 'DependentContinuant':
|
| 193 |
-
return {
|
| 194 |
-
shape: 'diamond',
|
| 195 |
-
size: 25,
|
| 196 |
-
color: '#f59e0b', // Amber / orange for Dependent Continuant
|
| 197 |
-
font: { color: '#000000', bold: true }
|
| 198 |
-
};
|
| 199 |
-
|
| 200 |
-
// W3C RDF Reification and Literals (SMS physical layer)
|
| 201 |
case 'RelationalQuality':
|
| 202 |
case 'TemporalPartMembership': {
|
| 203 |
const isStmtActive = node.properties?.isCurrent !== false;
|
| 204 |
return {
|
| 205 |
shape: 'diamond',
|
| 206 |
-
size:
|
| 207 |
-
color: isStmtActive ? '#f59e0b' : '#e2e8f0',
|
| 208 |
font: { size: 10, color: isStmtActive ? '#000000' : '#64748b', bold: true }
|
| 209 |
};
|
| 210 |
}
|
| 211 |
-
|
| 212 |
case 'Literal':
|
| 213 |
-
return {
|
| 214 |
-
shape: 'ellipse', // Oval shape
|
| 215 |
-
color: '#f8fafc',
|
| 216 |
-
font: { size: 8, color: '#334155' }
|
| 217 |
-
};
|
| 218 |
-
|
| 219 |
-
// ISO 15926 4D Perdurants
|
| 220 |
case 'ActualIndividual':
|
|
|
|
|
|
|
|
|
|
| 221 |
return {
|
| 222 |
shape: 'box',
|
| 223 |
-
color: '#
|
| 224 |
-
font: { color: '#ffffff', bold: true }
|
| 225 |
-
};
|
| 226 |
-
|
| 227 |
-
case 'State':
|
| 228 |
-
return {
|
| 229 |
-
shape: 'box',
|
| 230 |
-
color: '#0d9488', // Teal
|
| 231 |
font: { color: '#ffffff' }
|
| 232 |
};
|
| 233 |
-
|
| 234 |
default: {
|
| 235 |
if (/^def/i.test(node.id)) {
|
| 236 |
return { shape: 'diamond', color: '#ef4444', font: { color: '#ffffff', bold: true } };
|
|
@@ -243,94 +193,49 @@ function getNodeStyle(node) {
|
|
| 243 |
}
|
| 244 |
}
|
| 245 |
|
| 246 |
-
/**
|
| 247 |
-
* Determines the visual style of an edge based on its ontological type and temporal validity.
|
| 248 |
-
*
|
| 249 |
-
* @param {Object} edge - Canonical semantic edge with properties.
|
| 250 |
-
* @returns {Object} Vis.js edge styling object.
|
| 251 |
-
*/
|
| 252 |
function getEdgeStyle(edge) {
|
| 253 |
const isCurrent = edge.properties?.isCurrent !== false;
|
|
|
|
| 254 |
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
color: isCurrent ? { color: '#0284c7' } : { color: '#cbd5e1' }, // Blue
|
| 259 |
-
dashes: !isCurrent,
|
| 260 |
-
width: isCurrent ? 2 : 1
|
| 261 |
-
};
|
| 262 |
|
|
|
|
|
|
|
|
|
|
| 263 |
case 'mounted_in':
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
width: isCurrent ? 2 : 1
|
| 268 |
-
};
|
| 269 |
-
|
| 270 |
case 'stored_at':
|
| 271 |
-
return {
|
| 272 |
-
color: isCurrent ? { color: '#f97316' } : { color: '#cbd5e1' }, // Orange
|
| 273 |
-
dashes: !isCurrent,
|
| 274 |
-
width: isCurrent ? 2 : 1
|
| 275 |
-
};
|
| 276 |
-
|
| 277 |
case 'has_defect':
|
| 278 |
-
return {
|
| 279 |
-
color: isCurrent ? { color: '#ef4444' } : { color: '#cbd5e1' }, // Red
|
| 280 |
-
dashes: !isCurrent,
|
| 281 |
-
width: 2
|
| 282 |
-
};
|
| 283 |
-
|
| 284 |
case 'inherence':
|
| 285 |
-
return {
|
| 286 |
-
color: isCurrent ? { color: '#f59e0b' } : { color: '#cbd5e1' }, // Amber for inheres_in
|
| 287 |
-
dashes: !isCurrent,
|
| 288 |
-
width: 2
|
| 289 |
-
};
|
| 290 |
-
|
| 291 |
-
case 'composition':
|
| 292 |
-
case 'has_part':
|
| 293 |
-
return {
|
| 294 |
-
color: isCurrent ? { color: '#10b981' } : { color: '#cbd5e1' }, // Emerald active, Slate past
|
| 295 |
-
dashes: !isCurrent,
|
| 296 |
-
width: isCurrent ? 2 : 1
|
| 297 |
-
};
|
| 298 |
-
|
| 299 |
case 'transition':
|
| 300 |
-
return {
|
| 301 |
-
color: { color: '#f59e0b' }, // Amber for LPG state transitions
|
| 302 |
-
dashes: false,
|
| 303 |
-
width: 2
|
| 304 |
-
};
|
| 305 |
-
|
| 306 |
case 'classification':
|
| 307 |
case 'reification':
|
| 308 |
case 'predicate':
|
| 309 |
-
return {
|
| 310 |
-
color: { color: '#94a3b8' }, // Subtle slate for structural 4D or RDF links
|
| 311 |
-
dashes: !isCurrent,
|
| 312 |
-
width: 1
|
| 313 |
-
};
|
| 314 |
-
|
| 315 |
default:
|
| 316 |
-
return { color: { color: '#94a3b8' }, dashes:
|
| 317 |
}
|
| 318 |
}
|
| 319 |
|
| 320 |
-
/**
|
| 321 |
-
* Generates the physics configuration for the Vis.js canvas.
|
| 322 |
-
*/
|
| 323 |
function getBaseOptions(isComplex) {
|
| 324 |
return {
|
| 325 |
physics: {
|
| 326 |
-
stabilization: { iterations:
|
| 327 |
-
barnesHut: {
|
| 328 |
-
gravitationalConstant: isComplex ? -
|
| 329 |
-
centralGravity: 0.25,
|
| 330 |
-
springLength: isComplex ?
|
| 331 |
-
springConstant: 0.04,
|
| 332 |
-
damping: 0.
|
| 333 |
-
avoidOverlap:
|
| 334 |
}
|
| 335 |
},
|
| 336 |
edges: { arrows: 'to' }
|
|
|
|
| 1 |
/**
|
| 2 |
* VIS.JS RENDERER ADAPTER
|
| 3 |
* -----------------------
|
| 4 |
+
* Bridges the abstract Semantic Graph with Vis.js Network.
|
| 5 |
+
* Uses incremental DataSet diffing so the graph layout stays stable across time steps.
|
|
|
|
| 6 |
*/
|
| 7 |
|
| 8 |
+
import { INTERNAL_PROPERTY_KEYS } from '../smrm/definitions.js';
|
| 9 |
+
|
| 10 |
let network = null;
|
| 11 |
const visNodes = new vis.DataSet();
|
| 12 |
const visEdges = new vis.DataSet();
|
| 13 |
|
| 14 |
/**
|
| 15 |
* Initialises the Vis.js network instance in the specified DOM container.
|
| 16 |
+
*
|
| 17 |
* @param {HTMLElement} container - The DOM element to host the canvas.
|
| 18 |
*/
|
| 19 |
export function initVisNetwork(container) {
|
| 20 |
+
if (!container) return;
|
| 21 |
if (network) {
|
| 22 |
network.destroy();
|
| 23 |
network = null;
|
| 24 |
}
|
| 25 |
+
visNodes.clear();
|
| 26 |
+
visEdges.clear();
|
| 27 |
const data = { nodes: visNodes, edges: visEdges };
|
| 28 |
network = new vis.Network(container, data, getBaseOptions(false));
|
| 29 |
}
|
| 30 |
|
| 31 |
/**
|
| 32 |
+
* Updates the network with a new Semantic Graph using non-destructive DataSet diffing.
|
| 33 |
+
*
|
| 34 |
* @param {Object} semanticGraph - { nodes: [], edges: [] }
|
| 35 |
+
* @param {boolean} isComplex - Flag to adjust physics for denser graphs.
|
| 36 |
*/
|
| 37 |
export function updateVisNetwork(semanticGraph, isComplex = false) {
|
| 38 |
+
if (!network || !semanticGraph) return;
|
|
|
|
|
|
|
|
|
|
| 39 |
|
|
|
|
| 40 |
network.setOptions(getBaseOptions(isComplex));
|
| 41 |
|
| 42 |
+
// 1. Map and style Nodes
|
| 43 |
+
const incomingNodeIds = new Set();
|
|
|
|
|
|
|
|
|
|
| 44 |
const styledNodes = (semanticGraph.nodes || []).map(node => {
|
| 45 |
+
incomingNodeIds.add(node.id);
|
| 46 |
const style = getNodeStyle(node);
|
| 47 |
+
|
|
|
|
| 48 |
let displayLabel = node.label || node.id;
|
| 49 |
if (node.properties) {
|
| 50 |
const customProps = Object.entries(node.properties).filter(
|
| 51 |
+
([k, v]) => !INTERNAL_PROPERTY_KEYS.includes(k) && v !== undefined && v !== null
|
| 52 |
);
|
| 53 |
if (customProps.length > 0) {
|
| 54 |
const propStr = customProps.map(([k, v]) => `${k}: ${v}`).join(', ');
|
|
|
|
| 74 |
|
| 75 |
return styledNode;
|
| 76 |
});
|
| 77 |
+
|
| 78 |
+
// Remove obsolete nodes and update/add current nodes
|
| 79 |
+
const existingNodeIds = visNodes.getIds();
|
| 80 |
+
const nodesToRemove = existingNodeIds.filter(id => !incomingNodeIds.has(id));
|
| 81 |
+
if (nodesToRemove.length > 0) visNodes.remove(nodesToRemove);
|
| 82 |
+
if (styledNodes.length > 0) visNodes.update(styledNodes);
|
| 83 |
|
| 84 |
// 2. Map and style Edges
|
|
|
|
| 85 |
const pairMap = {};
|
| 86 |
+
(semanticGraph.edges || []).forEach(edge => {
|
| 87 |
const pairKey = `${edge.from}->${edge.to}`;
|
| 88 |
if (!pairMap[pairKey]) pairMap[pairKey] = [];
|
| 89 |
pairMap[pairKey].push(edge);
|
| 90 |
});
|
| 91 |
|
| 92 |
+
const incomingEdgeIds = new Set();
|
| 93 |
const styledEdges = [];
|
| 94 |
+
|
| 95 |
Object.values(pairMap).forEach(edgesGroup => {
|
| 96 |
const count = edgesGroup.length;
|
| 97 |
+
|
| 98 |
edgesGroup.forEach((edge, idx) => {
|
| 99 |
+
incomingEdgeIds.add(edge.id);
|
| 100 |
+
const smoothConfig = count === 1
|
| 101 |
? { type: 'continuous', roundness: 0.2 }
|
| 102 |
: { type: 'curvedCW', roundness: Math.min(0.14 + (idx * 0.14), 0.85) };
|
| 103 |
|
| 104 |
const style = getEdgeStyle(edge);
|
| 105 |
|
| 106 |
+
let displayEdgeLabel = edge.label || '';
|
|
|
|
| 107 |
if (edge.properties) {
|
| 108 |
const customEdgeProps = Object.entries(edge.properties).filter(
|
| 109 |
+
([k, v]) => !INTERNAL_PROPERTY_KEYS.includes(k) && v !== undefined && v !== null
|
| 110 |
);
|
| 111 |
if (customEdgeProps.length > 0) {
|
| 112 |
const propStr = customEdgeProps.map(([k, v]) => `${k}: ${v}`).join(', ');
|
|
|
|
| 128 |
});
|
| 129 |
});
|
| 130 |
|
| 131 |
+
const existingEdgeIds = visEdges.getIds();
|
| 132 |
+
const edgesToRemove = existingEdgeIds.filter(id => !incomingEdgeIds.has(id));
|
| 133 |
+
if (edgesToRemove.length > 0) visEdges.remove(edgesToRemove);
|
| 134 |
+
if (styledEdges.length > 0) visEdges.update(styledEdges);
|
| 135 |
}
|
| 136 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 137 |
function getNodeStyle(node) {
|
| 138 |
const entityType = node.type || node.properties?.entityClass || node.properties?.entityType;
|
| 139 |
|
| 140 |
switch (entityType) {
|
|
|
|
| 141 |
case 'Vehicle':
|
| 142 |
+
return { shape: 'box', color: '#0284c7', font: { color: '#ffffff', bold: true } };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 143 |
case 'Slot':
|
| 144 |
+
return { shape: 'box', color: '#64748b', font: { color: '#ffffff' } };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 145 |
case 'Part':
|
| 146 |
+
return { shape: 'box', color: '#10b981', font: { color: '#ffffff', bold: true } };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 147 |
case 'Defect':
|
| 148 |
+
return { shape: 'diamond', size: 25, color: '#ef4444', font: { color: '#ffffff', bold: true } };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 149 |
case 'Warehouse':
|
| 150 |
+
return { shape: 'box', color: '#f97316', font: { color: '#ffffff', bold: true } };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 151 |
case 'MaterialEntity': {
|
| 152 |
+
const isSystem = node.properties?.entityType === 'system' || node.properties?.entityClass === 'Vehicle';
|
| 153 |
return {
|
| 154 |
shape: 'box',
|
| 155 |
+
color: isSystem ? '#0284c7' : '#64748b',
|
| 156 |
font: { color: '#ffffff', bold: isSystem }
|
| 157 |
};
|
| 158 |
}
|
|
|
|
| 159 |
case 'Quality':
|
| 160 |
case 'DependentContinuant':
|
| 161 |
+
return { shape: 'diamond', size: 25, color: '#f59e0b', font: { color: '#000000', bold: true } };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 162 |
case 'RelationalQuality':
|
| 163 |
case 'TemporalPartMembership': {
|
| 164 |
const isStmtActive = node.properties?.isCurrent !== false;
|
| 165 |
return {
|
| 166 |
shape: 'diamond',
|
| 167 |
+
size: 16,
|
| 168 |
+
color: isStmtActive ? '#f59e0b' : '#e2e8f0',
|
| 169 |
font: { size: 10, color: isStmtActive ? '#000000' : '#64748b', bold: true }
|
| 170 |
};
|
| 171 |
}
|
|
|
|
| 172 |
case 'Literal':
|
| 173 |
+
return { shape: 'ellipse', color: '#f8fafc', font: { size: 9, color: '#334155' } };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 174 |
case 'ActualIndividual':
|
| 175 |
+
return { shape: 'box', color: '#4f46e5', font: { color: '#ffffff', bold: true } };
|
| 176 |
+
case 'State': {
|
| 177 |
+
const isStateActive = node.properties?.isCurrent !== false;
|
| 178 |
return {
|
| 179 |
shape: 'box',
|
| 180 |
+
color: isStateActive ? '#0d9488' : '#94a3b8',
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 181 |
font: { color: '#ffffff' }
|
| 182 |
};
|
| 183 |
+
}
|
| 184 |
default: {
|
| 185 |
if (/^def/i.test(node.id)) {
|
| 186 |
return { shape: 'diamond', color: '#ef4444', font: { color: '#ffffff', bold: true } };
|
|
|
|
| 193 |
}
|
| 194 |
}
|
| 195 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 196 |
function getEdgeStyle(edge) {
|
| 197 |
const isCurrent = edge.properties?.isCurrent !== false;
|
| 198 |
+
const rel = edge.type || edge.label;
|
| 199 |
|
| 200 |
+
if (!isCurrent) {
|
| 201 |
+
return { color: { color: '#cbd5e1' }, dashes: true, width: 1 };
|
| 202 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 203 |
|
| 204 |
+
switch (rel) {
|
| 205 |
+
case 'has_slot':
|
| 206 |
+
return { color: { color: '#0284c7' }, dashes: false, width: 2 };
|
| 207 |
case 'mounted_in':
|
| 208 |
+
case 'composition':
|
| 209 |
+
case 'has_part':
|
| 210 |
+
return { color: { color: '#10b981' }, dashes: false, width: 2 };
|
|
|
|
|
|
|
|
|
|
| 211 |
case 'stored_at':
|
| 212 |
+
return { color: { color: '#f97316' }, dashes: false, width: 2 };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 213 |
case 'has_defect':
|
| 214 |
+
return { color: { color: '#ef4444' }, dashes: false, width: 2 };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 215 |
case 'inherence':
|
| 216 |
+
return { color: { color: '#f59e0b' }, dashes: false, width: 2 };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 217 |
case 'transition':
|
| 218 |
+
return { color: { color: '#f59e0b' }, dashes: false, width: 2 };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 219 |
case 'classification':
|
| 220 |
case 'reification':
|
| 221 |
case 'predicate':
|
| 222 |
+
return { color: { color: '#94a3b8' }, dashes: true, width: 1 };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 223 |
default:
|
| 224 |
+
return { color: { color: '#94a3b8' }, dashes: false, width: 1 };
|
| 225 |
}
|
| 226 |
}
|
| 227 |
|
|
|
|
|
|
|
|
|
|
| 228 |
function getBaseOptions(isComplex) {
|
| 229 |
return {
|
| 230 |
physics: {
|
| 231 |
+
stabilization: { iterations: 80, fit: true },
|
| 232 |
+
barnesHut: {
|
| 233 |
+
gravitationalConstant: isComplex ? -22000 : -10000,
|
| 234 |
+
centralGravity: 0.25,
|
| 235 |
+
springLength: isComplex ? 200 : 140,
|
| 236 |
+
springConstant: 0.04,
|
| 237 |
+
damping: 0.12,
|
| 238 |
+
avoidOverlap: 0.9
|
| 239 |
}
|
| 240 |
},
|
| 241 |
edges: { arrows: 'to' }
|
js/scenarios/scenario-vanishing-crack.js
CHANGED
|
@@ -1,10 +1,8 @@
|
|
| 1 |
/**
|
| 2 |
* SCENARIO: THE VANISHING CRACK
|
| 3 |
* -----------------------------
|
| 4 |
-
*
|
| 5 |
* synchronised with audio playback.
|
| 6 |
-
*
|
| 7 |
-
* Timestamps are represented in seconds (float).
|
| 8 |
*/
|
| 9 |
|
| 10 |
import {
|
|
@@ -13,11 +11,11 @@ import {
|
|
| 13 |
createReportDefectEvent,
|
| 14 |
createDismantleEvent
|
| 15 |
} from '../smrm/actions.js';
|
|
|
|
| 16 |
|
| 17 |
/**
|
| 18 |
* Highlights specified UI elements by IDs with a temporary glowing effect.
|
| 19 |
-
*
|
| 20 |
-
*
|
| 21 |
* @param {string[]} elementIds - Array of DOM element IDs to highlight.
|
| 22 |
* @param {number} [duration=1200] - Duration of the highlight effect in milliseconds.
|
| 23 |
*/
|
|
@@ -78,162 +76,144 @@ const subtitles = [
|
|
| 78 |
export const vanishingCrackScenario = {
|
| 79 |
subtitles,
|
| 80 |
timeline: [
|
| 81 |
-
// 1. t = 1.0: Initialize Naive Node mode,
|
| 82 |
{
|
| 83 |
id: 'step_1_0_init_naive',
|
| 84 |
time: 1.0,
|
| 85 |
executed: false,
|
| 86 |
-
action(state
|
| 87 |
state.ontology = 'NAIVE_NODE';
|
| 88 |
-
|
| 89 |
state.graphEngine = 'MERMAID';
|
| 90 |
}
|
| 91 |
},
|
| 92 |
|
| 93 |
-
// 2. t = 15.5:
|
| 94 |
{
|
| 95 |
id: 'step_15_5_reg_vehicle',
|
| 96 |
time: 15.5,
|
| 97 |
executed: false,
|
| 98 |
-
action(state
|
| 99 |
highlightUI(['biz-reg-id', 'biz-reg-btn']);
|
| 100 |
state.currentTime = 15.5;
|
| 101 |
-
const event = createRegisterPartEvent('System_A', state.currentTime);
|
| 102 |
-
if (event)
|
| 103 |
-
state.eventLog.push(event);
|
| 104 |
-
}
|
| 105 |
}
|
| 106 |
},
|
| 107 |
|
| 108 |
-
// 3. t = 18.0:
|
| 109 |
-
// The smart adapters implicitly generate the contextual slot!
|
| 110 |
{
|
| 111 |
id: 'step_18_0_reg_part_install',
|
| 112 |
time: 18.0,
|
| 113 |
executed: false,
|
| 114 |
-
action(state
|
| 115 |
highlightUI(['biz-reg-id', 'biz-inst-btn']);
|
| 116 |
state.currentTime = 18.0;
|
| 117 |
-
const regEvent = createRegisterPartEvent('Part_M01', state.currentTime);
|
| 118 |
-
if (regEvent)
|
| 119 |
-
state.eventLog.push(regEvent);
|
| 120 |
-
}
|
| 121 |
const asmEvent = createAssembleEvent('System_A', 'Part_M01', 'mounted_in', state.currentTime);
|
| 122 |
-
if (asmEvent)
|
| 123 |
-
state.eventLog.push(asmEvent);
|
| 124 |
-
}
|
| 125 |
}
|
| 126 |
},
|
| 127 |
|
| 128 |
-
// 4. t = 43.0:
|
| 129 |
{
|
| 130 |
id: 'step_43_0_attach_crack_node',
|
| 131 |
time: 43.0,
|
| 132 |
executed: false,
|
| 133 |
-
action(state
|
| 134 |
highlightUI(['biz-def-target', 'biz-def-btn']);
|
| 135 |
state.currentTime = 43.0;
|
| 136 |
const event = createReportDefectEvent('Part_M01', 'crack', '2mm', state.currentTime);
|
| 137 |
-
if (event)
|
| 138 |
-
state.eventLog.push(event);
|
| 139 |
-
}
|
| 140 |
}
|
| 141 |
},
|
| 142 |
|
| 143 |
-
// 5. t = 56.5:
|
| 144 |
{
|
| 145 |
id: 'step_56_5_revoke_crack_node',
|
| 146 |
time: 56.5,
|
| 147 |
executed: false,
|
| 148 |
-
action(state
|
| 149 |
highlightUI(['biz-rev-defect', 'biz-rev-btn']);
|
| 150 |
state.currentTime = 56.5;
|
| 151 |
const idx = state.eventLog.findIndex(e => e.type === 'REPORT_DEFECT' && e.targetId === 'Part_M01');
|
| 152 |
-
if (idx !== -1)
|
| 153 |
-
state.eventLog.splice(idx, 1);
|
| 154 |
-
}
|
| 155 |
}
|
| 156 |
},
|
| 157 |
|
| 158 |
-
// 6. t = 58.5: Switch
|
| 159 |
{
|
| 160 |
id: 'step_58_5_switch_strategy_edge',
|
| 161 |
time: 58.5,
|
| 162 |
executed: false,
|
| 163 |
-
action(state
|
| 164 |
highlightUI(['ctrl-onto-naive-edge']);
|
| 165 |
state.ontology = 'NAIVE_EDGE';
|
| 166 |
}
|
| 167 |
},
|
| 168 |
|
| 169 |
-
// 7. t = 61.0:
|
| 170 |
{
|
| 171 |
id: 'step_61_0_attach_crack_edge',
|
| 172 |
time: 61.0,
|
| 173 |
executed: false,
|
| 174 |
-
action(state
|
| 175 |
highlightUI(['biz-def-target', 'biz-def-btn']);
|
| 176 |
state.currentTime = 61.0;
|
| 177 |
const event = createReportDefectEvent('Part_M01', 'crack', '2mm', state.currentTime);
|
| 178 |
-
if (event)
|
| 179 |
-
state.eventLog.push(event);
|
| 180 |
-
}
|
| 181 |
}
|
| 182 |
},
|
| 183 |
|
| 184 |
-
// 8. t = 78.0:
|
| 185 |
{
|
| 186 |
id: 'step_78_0_dismantle_part',
|
| 187 |
time: 78.0,
|
| 188 |
executed: false,
|
| 189 |
-
action(state
|
| 190 |
highlightUI(['biz-dism-part', 'biz-dism-btn']);
|
| 191 |
state.currentTime = 78.0;
|
| 192 |
const event = createDismantleEvent('Part_M01', state.currentTime);
|
| 193 |
-
if (event)
|
| 194 |
-
state.eventLog.push(event);
|
| 195 |
-
}
|
| 196 |
}
|
| 197 |
},
|
| 198 |
|
| 199 |
-
// 9. t = 82.0:
|
| 200 |
{
|
| 201 |
id: 'step_82_0_reg_warehouse',
|
| 202 |
time: 82.0,
|
| 203 |
executed: false,
|
| 204 |
-
action(state
|
| 205 |
highlightUI(['biz-reg-id', 'biz-reg-btn']);
|
| 206 |
state.currentTime = 82.0;
|
| 207 |
-
const event = createRegisterPartEvent('Warehouse_1', state.currentTime);
|
| 208 |
-
if (event)
|
| 209 |
-
state.eventLog.push(event);
|
| 210 |
-
}
|
| 211 |
}
|
| 212 |
},
|
| 213 |
|
| 214 |
-
// 10. t = 84.0:
|
| 215 |
{
|
| 216 |
id: 'step_84_0_store_part',
|
| 217 |
time: 84.0,
|
| 218 |
executed: false,
|
| 219 |
-
action(state
|
| 220 |
highlightUI(['biz-store-part', 'biz-store-target', 'biz-store-btn']);
|
| 221 |
state.currentTime = 84.0;
|
| 222 |
const event = createAssembleEvent('Warehouse_1', 'Part_M01', 'stored_at', state.currentTime);
|
| 223 |
-
if (event)
|
| 224 |
-
state.eventLog.push(event);
|
| 225 |
-
}
|
| 226 |
}
|
| 227 |
},
|
| 228 |
|
| 229 |
-
// 11. t = 122.0:
|
| 230 |
{
|
| 231 |
id: 'step_122_0_switch_iso',
|
| 232 |
time: 122.0,
|
| 233 |
executed: false,
|
| 234 |
-
action(state
|
| 235 |
-
highlightUI(['ctrl-onto-iso']);
|
| 236 |
state.ontology = 'ISO15926';
|
|
|
|
| 237 |
}
|
| 238 |
}
|
| 239 |
]
|
|
@@ -241,18 +221,12 @@ export const vanishingCrackScenario = {
|
|
| 241 |
|
| 242 |
/**
|
| 243 |
* Dispatches pending timeline actions based on current playback time.
|
| 244 |
-
* Resolves float chunking using `>= step.time && !step.executed`.
|
| 245 |
-
*
|
| 246 |
-
* @param {number} currentTime - Current audio playback time in seconds.
|
| 247 |
-
* @param {Object} state - The reactive domain state.
|
| 248 |
-
* @param {Function} [renderGraph] - Function to trigger canvas redraw.
|
| 249 |
*/
|
| 250 |
export function onTimeUpdate(currentTime, state, renderGraph) {
|
| 251 |
if (!Array.isArray(vanishingCrackScenario.timeline)) return;
|
| 252 |
|
| 253 |
vanishingCrackScenario.timeline.forEach(step => {
|
| 254 |
if (currentTime >= step.time && !step.executed) {
|
| 255 |
-
console.log('Executing step:', step.time);
|
| 256 |
step.executed = true;
|
| 257 |
const fn = step.action || step.execute;
|
| 258 |
if (typeof fn === 'function') {
|
|
@@ -267,8 +241,6 @@ export function onTimeUpdate(currentTime, state, renderGraph) {
|
|
| 267 |
|
| 268 |
/**
|
| 269 |
* Resets execution flags on timeline steps when seeking, stopping, or rewinding.
|
| 270 |
-
*
|
| 271 |
-
* @param {number} [targetTime=0] - Audio target time. Steps with step.time >= targetTime are reset to unexecuted.
|
| 272 |
*/
|
| 273 |
export function resetTimeline(targetTime = 0) {
|
| 274 |
if (!Array.isArray(vanishingCrackScenario.timeline)) return;
|
|
@@ -277,4 +249,4 @@ export function resetTimeline(targetTime = 0) {
|
|
| 277 |
step.executed = false;
|
| 278 |
}
|
| 279 |
});
|
| 280 |
-
}
|
|
|
|
| 1 |
/**
|
| 2 |
* SCENARIO: THE VANISHING CRACK
|
| 3 |
* -----------------------------
|
| 4 |
+
* Narrative subtitle data and chronological action timeline
|
| 5 |
* synchronised with audio playback.
|
|
|
|
|
|
|
| 6 |
*/
|
| 7 |
|
| 8 |
import {
|
|
|
|
| 11 |
createReportDefectEvent,
|
| 12 |
createDismantleEvent
|
| 13 |
} from '../smrm/actions.js';
|
| 14 |
+
import { setPerspective } from '../state.js';
|
| 15 |
|
| 16 |
/**
|
| 17 |
* Highlights specified UI elements by IDs with a temporary glowing effect.
|
| 18 |
+
*
|
|
|
|
| 19 |
* @param {string[]} elementIds - Array of DOM element IDs to highlight.
|
| 20 |
* @param {number} [duration=1200] - Duration of the highlight effect in milliseconds.
|
| 21 |
*/
|
|
|
|
| 76 |
export const vanishingCrackScenario = {
|
| 77 |
subtitles,
|
| 78 |
timeline: [
|
| 79 |
+
// 1. t = 1.0: Initialize Naive Node mode, Logical (SMIM) perspective so slot & mounted_in edge are visible
|
| 80 |
{
|
| 81 |
id: 'step_1_0_init_naive',
|
| 82 |
time: 1.0,
|
| 83 |
executed: false,
|
| 84 |
+
action(state) {
|
| 85 |
state.ontology = 'NAIVE_NODE';
|
| 86 |
+
setPerspective('SMIM');
|
| 87 |
state.graphEngine = 'MERMAID';
|
| 88 |
}
|
| 89 |
},
|
| 90 |
|
| 91 |
+
// 2. t = 15.5: Register Vehicle System_A
|
| 92 |
{
|
| 93 |
id: 'step_15_5_reg_vehicle',
|
| 94 |
time: 15.5,
|
| 95 |
executed: false,
|
| 96 |
+
action(state) {
|
| 97 |
highlightUI(['biz-reg-id', 'biz-reg-btn']);
|
| 98 |
state.currentTime = 15.5;
|
| 99 |
+
const event = createRegisterPartEvent('System_A', state.currentTime, 'Vehicle');
|
| 100 |
+
if (event) state.eventLog.push(event);
|
|
|
|
|
|
|
| 101 |
}
|
| 102 |
},
|
| 103 |
|
| 104 |
+
// 3. t = 18.0: Register Part_M01 and install in System_A (creates Slot_System_A_Part_M01)
|
|
|
|
| 105 |
{
|
| 106 |
id: 'step_18_0_reg_part_install',
|
| 107 |
time: 18.0,
|
| 108 |
executed: false,
|
| 109 |
+
action(state) {
|
| 110 |
highlightUI(['biz-reg-id', 'biz-inst-btn']);
|
| 111 |
state.currentTime = 18.0;
|
| 112 |
+
const regEvent = createRegisterPartEvent('Part_M01', state.currentTime, 'Part');
|
| 113 |
+
if (regEvent) state.eventLog.push(regEvent);
|
|
|
|
|
|
|
| 114 |
const asmEvent = createAssembleEvent('System_A', 'Part_M01', 'mounted_in', state.currentTime);
|
| 115 |
+
if (asmEvent) state.eventLog.push(asmEvent);
|
|
|
|
|
|
|
| 116 |
}
|
| 117 |
},
|
| 118 |
|
| 119 |
+
// 4. t = 43.0: Report Defect crack=2mm on Part_M01 (attached to NODE)
|
| 120 |
{
|
| 121 |
id: 'step_43_0_attach_crack_node',
|
| 122 |
time: 43.0,
|
| 123 |
executed: false,
|
| 124 |
+
action(state) {
|
| 125 |
highlightUI(['biz-def-target', 'biz-def-btn']);
|
| 126 |
state.currentTime = 43.0;
|
| 127 |
const event = createReportDefectEvent('Part_M01', 'crack', '2mm', state.currentTime);
|
| 128 |
+
if (event) state.eventLog.push(event);
|
|
|
|
|
|
|
| 129 |
}
|
| 130 |
},
|
| 131 |
|
| 132 |
+
// 5. t = 56.5: Rewind thought-experiment defect before switching strategy to NAIVE_EDGE
|
| 133 |
{
|
| 134 |
id: 'step_56_5_revoke_crack_node',
|
| 135 |
time: 56.5,
|
| 136 |
executed: false,
|
| 137 |
+
action(state) {
|
| 138 |
highlightUI(['biz-rev-defect', 'biz-rev-btn']);
|
| 139 |
state.currentTime = 56.5;
|
| 140 |
const idx = state.eventLog.findIndex(e => e.type === 'REPORT_DEFECT' && e.targetId === 'Part_M01');
|
| 141 |
+
if (idx !== -1) state.eventLog.splice(idx, 1);
|
|
|
|
|
|
|
| 142 |
}
|
| 143 |
},
|
| 144 |
|
| 145 |
+
// 6. t = 58.5: Switch to NAIVE_EDGE
|
| 146 |
{
|
| 147 |
id: 'step_58_5_switch_strategy_edge',
|
| 148 |
time: 58.5,
|
| 149 |
executed: false,
|
| 150 |
+
action(state) {
|
| 151 |
highlightUI(['ctrl-onto-naive-edge']);
|
| 152 |
state.ontology = 'NAIVE_EDGE';
|
| 153 |
}
|
| 154 |
},
|
| 155 |
|
| 156 |
+
// 7. t = 61.0: Report Defect crack=2mm on Part_M01 (attached to mounted_in EDGE)
|
| 157 |
{
|
| 158 |
id: 'step_61_0_attach_crack_edge',
|
| 159 |
time: 61.0,
|
| 160 |
executed: false,
|
| 161 |
+
action(state) {
|
| 162 |
highlightUI(['biz-def-target', 'biz-def-btn']);
|
| 163 |
state.currentTime = 61.0;
|
| 164 |
const event = createReportDefectEvent('Part_M01', 'crack', '2mm', state.currentTime);
|
| 165 |
+
if (event) state.eventLog.push(event);
|
|
|
|
|
|
|
| 166 |
}
|
| 167 |
},
|
| 168 |
|
| 169 |
+
// 8. t = 78.0: Dismantle Part_M01
|
| 170 |
{
|
| 171 |
id: 'step_78_0_dismantle_part',
|
| 172 |
time: 78.0,
|
| 173 |
executed: false,
|
| 174 |
+
action(state) {
|
| 175 |
highlightUI(['biz-dism-part', 'biz-dism-btn']);
|
| 176 |
state.currentTime = 78.0;
|
| 177 |
const event = createDismantleEvent('Part_M01', state.currentTime);
|
| 178 |
+
if (event) state.eventLog.push(event);
|
|
|
|
|
|
|
| 179 |
}
|
| 180 |
},
|
| 181 |
|
| 182 |
+
// 9. t = 82.0: Register Warehouse_1
|
| 183 |
{
|
| 184 |
id: 'step_82_0_reg_warehouse',
|
| 185 |
time: 82.0,
|
| 186 |
executed: false,
|
| 187 |
+
action(state) {
|
| 188 |
highlightUI(['biz-reg-id', 'biz-reg-btn']);
|
| 189 |
state.currentTime = 82.0;
|
| 190 |
+
const event = createRegisterPartEvent('Warehouse_1', state.currentTime, 'Warehouse');
|
| 191 |
+
if (event) state.eventLog.push(event);
|
|
|
|
|
|
|
| 192 |
}
|
| 193 |
},
|
| 194 |
|
| 195 |
+
// 10. t = 84.0: Move Part_M01 to Warehouse_1
|
| 196 |
{
|
| 197 |
id: 'step_84_0_store_part',
|
| 198 |
time: 84.0,
|
| 199 |
executed: false,
|
| 200 |
+
action(state) {
|
| 201 |
highlightUI(['biz-store-part', 'biz-store-target', 'biz-store-btn']);
|
| 202 |
state.currentTime = 84.0;
|
| 203 |
const event = createAssembleEvent('Warehouse_1', 'Part_M01', 'stored_at', state.currentTime);
|
| 204 |
+
if (event) state.eventLog.push(event);
|
|
|
|
|
|
|
| 205 |
}
|
| 206 |
},
|
| 207 |
|
| 208 |
+
// 11. t = 122.0: Switch to ISO 15926 (4D) in Logical (SMIM) Perspective
|
| 209 |
{
|
| 210 |
id: 'step_122_0_switch_iso',
|
| 211 |
time: 122.0,
|
| 212 |
executed: false,
|
| 213 |
+
action(state) {
|
| 214 |
+
highlightUI(['ctrl-onto-iso', 'ctrl-view-logical']);
|
| 215 |
state.ontology = 'ISO15926';
|
| 216 |
+
setPerspective('SMIM');
|
| 217 |
}
|
| 218 |
}
|
| 219 |
]
|
|
|
|
| 221 |
|
| 222 |
/**
|
| 223 |
* Dispatches pending timeline actions based on current playback time.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 224 |
*/
|
| 225 |
export function onTimeUpdate(currentTime, state, renderGraph) {
|
| 226 |
if (!Array.isArray(vanishingCrackScenario.timeline)) return;
|
| 227 |
|
| 228 |
vanishingCrackScenario.timeline.forEach(step => {
|
| 229 |
if (currentTime >= step.time && !step.executed) {
|
|
|
|
| 230 |
step.executed = true;
|
| 231 |
const fn = step.action || step.execute;
|
| 232 |
if (typeof fn === 'function') {
|
|
|
|
| 241 |
|
| 242 |
/**
|
| 243 |
* Resets execution flags on timeline steps when seeking, stopping, or rewinding.
|
|
|
|
|
|
|
| 244 |
*/
|
| 245 |
export function resetTimeline(targetTime = 0) {
|
| 246 |
if (!Array.isArray(vanishingCrackScenario.timeline)) return;
|
|
|
|
| 249 |
step.executed = false;
|
| 250 |
}
|
| 251 |
});
|
| 252 |
+
}
|
js/smim/bfo.js
CHANGED
|
@@ -2,44 +2,50 @@
|
|
| 2 |
* BFO ONTOLOGY ADAPTER (3D Continuant)
|
| 3 |
* ------------------------------------
|
| 4 |
* Maps Domain Business Events into a canonical 3D BFO Semantic Graph.
|
| 5 |
-
*
|
| 6 |
* Ontological Axioms:
|
| 7 |
-
* -
|
| 8 |
-
*
|
| 9 |
-
*
|
| 10 |
-
*
|
| 11 |
-
* - Relationships between continuants (mounted_in, stored_at, etc.) hold directly
|
| 12 |
-
* between the continuants at a given time, carrying temporal validity intervals.
|
| 13 |
-
* - Both the Quality node and the 'inheres_in' edge record validFrom: timestamp.
|
| 14 |
-
* - NEVER generates 'State' or 'Temporal Part' nodes.
|
| 15 |
-
*
|
| 16 |
-
* @param {Array} eventLog - The projection of temporal business events.
|
| 17 |
-
* @param {Array} [baseNodes=[]] - The projection of base continuant entities.
|
| 18 |
-
* @param {number|null} [currentTime=null] - Global time to evaluate current validity.
|
| 19 |
-
* @returns {{ nodes: Array, edges: Array }} The canonical Semantic Graph.
|
| 20 |
*/
|
| 21 |
|
| 22 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
const nodeMap = new Map();
|
| 24 |
const edges = [];
|
|
|
|
| 25 |
|
| 26 |
function ensureContinuantNode(partId, overrideClass = null) {
|
| 27 |
if (!nodeMap.has(partId)) {
|
|
|
|
| 28 |
nodeMap.set(partId, {
|
| 29 |
id: partId,
|
| 30 |
label: partId,
|
| 31 |
type: 'MaterialEntity',
|
| 32 |
properties: {
|
| 33 |
originalId: partId,
|
| 34 |
-
entityClass
|
|
|
|
| 35 |
}
|
| 36 |
});
|
| 37 |
}
|
|
|
|
| 38 |
}
|
| 39 |
|
| 40 |
-
// 1. Initialise base continuant entities
|
| 41 |
(baseNodes || []).forEach(bn => {
|
| 42 |
-
ensureContinuantNode(bn.id);
|
| 43 |
});
|
| 44 |
|
| 45 |
// 2. Filter and sort business events by timestamp
|
|
@@ -48,12 +54,11 @@ export function generateSemanticGraph(eventLog = [], baseNodes = [], currentTime
|
|
| 48 |
.slice()
|
| 49 |
.sort((a, b) => (a.valid_from || 0) - (b.valid_from || 0));
|
| 50 |
|
| 51 |
-
|
| 52 |
// 3. Process business events in temporal order
|
| 53 |
eventsToProcess.forEach(event => {
|
| 54 |
switch (event.type) {
|
| 55 |
case 'REGISTER_PART': {
|
| 56 |
-
ensureContinuantNode(event.partId);
|
| 57 |
break;
|
| 58 |
}
|
| 59 |
|
|
@@ -84,12 +89,11 @@ export function generateSemanticGraph(eventLog = [], baseNodes = [], currentTime
|
|
| 84 |
break;
|
| 85 |
}
|
| 86 |
|
| 87 |
-
//
|
| 88 |
const slotId = `Slot_${parentId}_${childId}`;
|
| 89 |
ensureContinuantNode(slotId, 'Slot');
|
| 90 |
|
| 91 |
-
|
| 92 |
-
const slotEdgeId = `edge_slot_${parentId}_${slotId}_${timestamp}`;
|
| 93 |
edges.push({
|
| 94 |
id: slotEdgeId,
|
| 95 |
from: parentId,
|
|
@@ -97,7 +101,7 @@ export function generateSemanticGraph(eventLog = [], baseNodes = [], currentTime
|
|
| 97 |
label: 'has_slot',
|
| 98 |
type: 'has_slot',
|
| 99 |
childId: slotId,
|
| 100 |
-
parentId
|
| 101 |
properties: {
|
| 102 |
validFrom: timestamp,
|
| 103 |
validTo: null,
|
|
@@ -105,7 +109,6 @@ export function generateSemanticGraph(eventLog = [], baseNodes = [], currentTime
|
|
| 105 |
}
|
| 106 |
});
|
| 107 |
|
| 108 |
-
// Link 2: Child -> Slot (mounted_in)
|
| 109 |
const mountEdgeId = event.id ? `edge_${event.id}` : `edge_${childId}_${slotId}_${timestamp}`;
|
| 110 |
edges.push({
|
| 111 |
id: mountEdgeId,
|
|
@@ -113,7 +116,7 @@ export function generateSemanticGraph(eventLog = [], baseNodes = [], currentTime
|
|
| 113 |
to: slotId,
|
| 114 |
label: 'mounted_in',
|
| 115 |
type: 'composition',
|
| 116 |
-
childId
|
| 117 |
parentId: slotId,
|
| 118 |
properties: {
|
| 119 |
validFrom: timestamp,
|
|
@@ -128,7 +131,6 @@ export function generateSemanticGraph(eventLog = [], baseNodes = [], currentTime
|
|
| 128 |
const { childId } = event;
|
| 129 |
const dismantleTime = event.valid_from;
|
| 130 |
|
| 131 |
-
// Locate active assembly relationship for the child component
|
| 132 |
const activeEdge = edges.find(
|
| 133 |
e => (e.childId === childId || e.from === childId) &&
|
| 134 |
(e.label === 'mounted_in' || e.label === 'stored_at') &&
|
|
@@ -136,11 +138,17 @@ export function generateSemanticGraph(eventLog = [], baseNodes = [], currentTime
|
|
| 136 |
);
|
| 137 |
if (activeEdge) {
|
| 138 |
activeEdge.properties.validTo = dismantleTime;
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 144 |
}
|
| 145 |
break;
|
| 146 |
}
|
|
@@ -154,7 +162,6 @@ export function generateSemanticGraph(eventLog = [], baseNodes = [], currentTime
|
|
| 154 |
const qualityId = `qual_${targetId}_${defectName}`;
|
| 155 |
const qualityLabel = `${capitalize(defectName)}: ${defectValue}`;
|
| 156 |
|
| 157 |
-
// Specifically Dependent Continuant (Quality) node recording validFrom
|
| 158 |
nodeMap.set(qualityId, {
|
| 159 |
id: qualityId,
|
| 160 |
label: qualityLabel,
|
|
@@ -165,11 +172,12 @@ export function generateSemanticGraph(eventLog = [], baseNodes = [], currentTime
|
|
| 165 |
value: defectValue,
|
| 166 |
bearerId: targetId,
|
| 167 |
entityClass: 'Quality',
|
| 168 |
-
validFrom: timestamp
|
|
|
|
|
|
|
| 169 |
}
|
| 170 |
});
|
| 171 |
|
| 172 |
-
// Canonical BFO relation: Quality inheres_in Independent Continuant (with validFrom)
|
| 173 |
const edgeId = `inh_${targetId}_${defectName}_${timestamp}`;
|
| 174 |
edges.push({
|
| 175 |
id: edgeId,
|
|
@@ -186,6 +194,24 @@ export function generateSemanticGraph(eventLog = [], baseNodes = [], currentTime
|
|
| 186 |
break;
|
| 187 |
}
|
| 188 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 189 |
default:
|
| 190 |
break;
|
| 191 |
}
|
|
@@ -197,31 +223,30 @@ export function generateSemanticGraph(eventLog = [], baseNodes = [], currentTime
|
|
| 197 |
(currentTime !== null && currentTime !== undefined ? edge.properties.validTo > currentTime : false);
|
| 198 |
});
|
| 199 |
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
}
|
|
|
|
| 205 |
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
return
|
|
|
|
|
|
|
|
|
|
| 217 |
}
|
| 218 |
|
| 219 |
-
/**
|
| 220 |
-
* Capitalises the first letter of a string.
|
| 221 |
-
* @param {string} str
|
| 222 |
-
* @returns {string}
|
| 223 |
-
*/
|
| 224 |
function capitalize(str) {
|
| 225 |
if (!str) return '';
|
| 226 |
return str.charAt(0).toUpperCase() + str.slice(1);
|
| 227 |
-
}
|
|
|
|
| 2 |
* BFO ONTOLOGY ADAPTER (3D Continuant)
|
| 3 |
* ------------------------------------
|
| 4 |
* Maps Domain Business Events into a canonical 3D BFO Semantic Graph.
|
| 5 |
+
*
|
| 6 |
* Ontological Axioms:
|
| 7 |
+
* - Physical entities (Vehicle, Part, Warehouse, Slot) are Independent Continuants ('MaterialEntity').
|
| 8 |
+
* - Defect observations are Specifically Dependent Continuants ('Quality') inhering in their
|
| 9 |
+
* bearer via 'inheres_in'. Because the Quality inheres in the Part itself (not the mounting slot),
|
| 10 |
+
* moving the Part to a Warehouse preserves its Quality!
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
*/
|
| 12 |
|
| 13 |
+
import { inferEntityClass } from '../smrm/definitions.js';
|
| 14 |
+
|
| 15 |
+
/**
|
| 16 |
+
* Generates the 3D BFO Semantic Graph from the event log up to currentTime.
|
| 17 |
+
*
|
| 18 |
+
* @param {Array} eventLog - Temporal business events.
|
| 19 |
+
* @param {Array} [baseNodes=[]] - Initial base entities.
|
| 20 |
+
* @param {number|null} [currentTime=null] - Global evaluation time T.
|
| 21 |
+
* @param {Object} [options={}] - { showHistory: boolean }.
|
| 22 |
+
* @returns {{ nodes: Array, edges: Array }}
|
| 23 |
+
*/
|
| 24 |
+
export function generateSemanticGraph(eventLog = [], baseNodes = [], currentTime = null, options = {}) {
|
| 25 |
const nodeMap = new Map();
|
| 26 |
const edges = [];
|
| 27 |
+
const showHistory = options.showHistory !== undefined ? Boolean(options.showHistory) : true;
|
| 28 |
|
| 29 |
function ensureContinuantNode(partId, overrideClass = null) {
|
| 30 |
if (!nodeMap.has(partId)) {
|
| 31 |
+
const entityClass = inferEntityClass(partId, overrideClass);
|
| 32 |
nodeMap.set(partId, {
|
| 33 |
id: partId,
|
| 34 |
label: partId,
|
| 35 |
type: 'MaterialEntity',
|
| 36 |
properties: {
|
| 37 |
originalId: partId,
|
| 38 |
+
entityClass,
|
| 39 |
+
entityType: entityClass === 'Vehicle' ? 'system' : 'element'
|
| 40 |
}
|
| 41 |
});
|
| 42 |
}
|
| 43 |
+
return nodeMap.get(partId);
|
| 44 |
}
|
| 45 |
|
| 46 |
+
// 1. Initialise base continuant entities
|
| 47 |
(baseNodes || []).forEach(bn => {
|
| 48 |
+
ensureContinuantNode(bn.id, bn.entityClass || bn.type);
|
| 49 |
});
|
| 50 |
|
| 51 |
// 2. Filter and sort business events by timestamp
|
|
|
|
| 54 |
.slice()
|
| 55 |
.sort((a, b) => (a.valid_from || 0) - (b.valid_from || 0));
|
| 56 |
|
|
|
|
| 57 |
// 3. Process business events in temporal order
|
| 58 |
eventsToProcess.forEach(event => {
|
| 59 |
switch (event.type) {
|
| 60 |
case 'REGISTER_PART': {
|
| 61 |
+
ensureContinuantNode(event.partId, event.entityClass);
|
| 62 |
break;
|
| 63 |
}
|
| 64 |
|
|
|
|
| 89 |
break;
|
| 90 |
}
|
| 91 |
|
| 92 |
+
// Contextual Slot generation: Slot_${parentId}_${childId}
|
| 93 |
const slotId = `Slot_${parentId}_${childId}`;
|
| 94 |
ensureContinuantNode(slotId, 'Slot');
|
| 95 |
|
| 96 |
+
const slotEdgeId = `edge_slot_${event.id || `${parentId}_${slotId}_${timestamp}`}`;
|
|
|
|
| 97 |
edges.push({
|
| 98 |
id: slotEdgeId,
|
| 99 |
from: parentId,
|
|
|
|
| 101 |
label: 'has_slot',
|
| 102 |
type: 'has_slot',
|
| 103 |
childId: slotId,
|
| 104 |
+
parentId,
|
| 105 |
properties: {
|
| 106 |
validFrom: timestamp,
|
| 107 |
validTo: null,
|
|
|
|
| 109 |
}
|
| 110 |
});
|
| 111 |
|
|
|
|
| 112 |
const mountEdgeId = event.id ? `edge_${event.id}` : `edge_${childId}_${slotId}_${timestamp}`;
|
| 113 |
edges.push({
|
| 114 |
id: mountEdgeId,
|
|
|
|
| 116 |
to: slotId,
|
| 117 |
label: 'mounted_in',
|
| 118 |
type: 'composition',
|
| 119 |
+
childId,
|
| 120 |
parentId: slotId,
|
| 121 |
properties: {
|
| 122 |
validFrom: timestamp,
|
|
|
|
| 131 |
const { childId } = event;
|
| 132 |
const dismantleTime = event.valid_from;
|
| 133 |
|
|
|
|
| 134 |
const activeEdge = edges.find(
|
| 135 |
e => (e.childId === childId || e.from === childId) &&
|
| 136 |
(e.label === 'mounted_in' || e.label === 'stored_at') &&
|
|
|
|
| 138 |
);
|
| 139 |
if (activeEdge) {
|
| 140 |
activeEdge.properties.validTo = dismantleTime;
|
| 141 |
+
|
| 142 |
+
if (activeEdge.to && activeEdge.to.startsWith('Slot_')) {
|
| 143 |
+
const slotEdge = edges.find(
|
| 144 |
+
e => e.to === activeEdge.to &&
|
| 145 |
+
e.label === 'has_slot' &&
|
| 146 |
+
e.properties.validTo === null
|
| 147 |
+
);
|
| 148 |
+
if (slotEdge) {
|
| 149 |
+
slotEdge.properties.validTo = dismantleTime;
|
| 150 |
+
}
|
| 151 |
+
}
|
| 152 |
}
|
| 153 |
break;
|
| 154 |
}
|
|
|
|
| 162 |
const qualityId = `qual_${targetId}_${defectName}`;
|
| 163 |
const qualityLabel = `${capitalize(defectName)}: ${defectValue}`;
|
| 164 |
|
|
|
|
| 165 |
nodeMap.set(qualityId, {
|
| 166 |
id: qualityId,
|
| 167 |
label: qualityLabel,
|
|
|
|
| 172 |
value: defectValue,
|
| 173 |
bearerId: targetId,
|
| 174 |
entityClass: 'Quality',
|
| 175 |
+
validFrom: timestamp,
|
| 176 |
+
validTo: null,
|
| 177 |
+
isCurrent: true
|
| 178 |
}
|
| 179 |
});
|
| 180 |
|
|
|
|
| 181 |
const edgeId = `inh_${targetId}_${defectName}_${timestamp}`;
|
| 182 |
edges.push({
|
| 183 |
id: edgeId,
|
|
|
|
| 194 |
break;
|
| 195 |
}
|
| 196 |
|
| 197 |
+
case 'REVOKE_DEFECT': {
|
| 198 |
+
const { targetId, defectName } = event;
|
| 199 |
+
const revokeTime = event.valid_from;
|
| 200 |
+
const qualityId = `qual_${targetId}_${defectName}`;
|
| 201 |
+
|
| 202 |
+
const activeInherence = edges.find(
|
| 203 |
+
e => e.from === qualityId && e.to === targetId && e.label === 'inheres_in' && e.properties.validTo === null
|
| 204 |
+
);
|
| 205 |
+
if (activeInherence) {
|
| 206 |
+
activeInherence.properties.validTo = revokeTime;
|
| 207 |
+
}
|
| 208 |
+
const qNode = nodeMap.get(qualityId);
|
| 209 |
+
if (qNode && qNode.properties) {
|
| 210 |
+
qNode.properties.validTo = revokeTime;
|
| 211 |
+
}
|
| 212 |
+
break;
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
default:
|
| 216 |
break;
|
| 217 |
}
|
|
|
|
| 223 |
(currentTime !== null && currentTime !== undefined ? edge.properties.validTo > currentTime : false);
|
| 224 |
});
|
| 225 |
|
| 226 |
+
nodeMap.forEach(node => {
|
| 227 |
+
if (node.type === 'Quality' && node.properties) {
|
| 228 |
+
node.properties.isCurrent = node.properties.validTo === null ||
|
| 229 |
+
(currentTime !== null && currentTime !== undefined ? node.properties.validTo > currentTime : false);
|
| 230 |
+
}
|
| 231 |
+
});
|
| 232 |
|
| 233 |
+
const filteredEdges = showHistory
|
| 234 |
+
? edges
|
| 235 |
+
: edges.filter(e => e.properties.isCurrent !== false);
|
| 236 |
+
|
| 237 |
+
const filteredNodes = Array.from(nodeMap.values()).filter(n => {
|
| 238 |
+
if (showHistory) return true;
|
| 239 |
+
if (n.type === 'Quality') return n.properties?.isCurrent !== false;
|
| 240 |
+
return true;
|
| 241 |
+
});
|
| 242 |
+
|
| 243 |
+
return {
|
| 244 |
+
nodes: filteredNodes,
|
| 245 |
+
edges: filteredEdges
|
| 246 |
+
};
|
| 247 |
}
|
| 248 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 249 |
function capitalize(str) {
|
| 250 |
if (!str) return '';
|
| 251 |
return str.charAt(0).toUpperCase() + str.slice(1);
|
| 252 |
+
}
|
js/smim/iso15926.js
CHANGED
|
@@ -2,26 +2,31 @@
|
|
| 2 |
* ISO 15926 ONTOLOGY ADAPTER (4D Perdurant)
|
| 3 |
* -----------------------------------------
|
| 4 |
* Maps Domain Business Events into a canonical 4D ISO 15926 Semantic Graph.
|
| 5 |
-
*
|
| 6 |
* Ontological Axioms:
|
| 7 |
-
* - Physical entities (Vehicle, Part, Warehouse) are 4D individuals ('ActualIndividual')
|
| 8 |
* with spatio-temporal extents across their entire lifetime.
|
| 9 |
-
* - Relationships
|
| 10 |
-
*
|
| 11 |
-
* -
|
| 12 |
-
* - A 'Defect' is an independent condition/classification entity that classifies a specific
|
| 13 |
-
* temporal part ('State') of an entity during a specific time slice, NOT the root 4D individual.
|
| 14 |
-
*
|
| 15 |
-
* @param {Array} eventLog - The projection of temporal business events.
|
| 16 |
-
* @param {Array} [baseNodes=[]] - The projection of base entities.
|
| 17 |
-
* @param {number|null} [currentTime=null] - Global time to evaluate current validity.
|
| 18 |
-
* @returns {{ nodes: Array, edges: Array }} The canonical Semantic Graph.
|
| 19 |
*/
|
| 20 |
|
| 21 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
const nodeMap = new Map();
|
| 23 |
const edges = [];
|
| 24 |
const stateNodesByOwner = new Map();
|
|
|
|
| 25 |
|
| 26 |
function registerStateNode(ownerId, stateNode) {
|
| 27 |
if (!stateNodesByOwner.has(ownerId)) {
|
|
@@ -33,22 +38,23 @@ export function generateSemanticGraph(eventLog = [], baseNodes = [], currentTime
|
|
| 33 |
function ensureExtentNode(partId, overrideClass = null) {
|
| 34 |
const extentId = `4d_${partId}`;
|
| 35 |
if (!nodeMap.has(extentId)) {
|
|
|
|
| 36 |
nodeMap.set(extentId, {
|
| 37 |
id: extentId,
|
| 38 |
label: `${partId}_4D`,
|
| 39 |
type: 'ActualIndividual',
|
| 40 |
properties: {
|
| 41 |
originalId: partId,
|
| 42 |
-
entityClass
|
| 43 |
}
|
| 44 |
});
|
| 45 |
}
|
| 46 |
return extentId;
|
| 47 |
}
|
| 48 |
|
| 49 |
-
// 1. Initialise base 4D extent nodes
|
| 50 |
(baseNodes || []).forEach(bn => {
|
| 51 |
-
ensureExtentNode(bn.id);
|
| 52 |
});
|
| 53 |
|
| 54 |
// 2. Filter and sort business events by timestamp
|
|
@@ -57,12 +63,11 @@ export function generateSemanticGraph(eventLog = [], baseNodes = [], currentTime
|
|
| 57 |
.slice()
|
| 58 |
.sort((a, b) => (a.valid_from || 0) - (b.valid_from || 0));
|
| 59 |
|
| 60 |
-
|
| 61 |
// 3. Process business events in temporal order
|
| 62 |
eventsToProcess.forEach(event => {
|
| 63 |
switch (event.type) {
|
| 64 |
case 'REGISTER_PART': {
|
| 65 |
-
ensureExtentNode(event.partId);
|
| 66 |
break;
|
| 67 |
}
|
| 68 |
|
|
@@ -123,12 +128,14 @@ export function generateSemanticGraph(eventLog = [], baseNodes = [], currentTime
|
|
| 123 |
type: 'composition',
|
| 124 |
childId,
|
| 125 |
parentId,
|
|
|
|
|
|
|
| 126 |
properties: { validFrom: timestamp, validTo: null, isCurrent: true }
|
| 127 |
});
|
| 128 |
break;
|
| 129 |
}
|
| 130 |
|
| 131 |
-
//
|
| 132 |
const slotId = `Slot_${parentId}_${childId}`;
|
| 133 |
const slotExtentId = ensureExtentNode(slotId, 'Slot');
|
| 134 |
|
|
@@ -140,12 +147,7 @@ export function generateSemanticGraph(eventLog = [], baseNodes = [], currentTime
|
|
| 140 |
id: parentStateId,
|
| 141 |
label: `${parentId}_State`,
|
| 142 |
type: 'State',
|
| 143 |
-
properties: {
|
| 144 |
-
ownerId: parentId,
|
| 145 |
-
validFrom: timestamp,
|
| 146 |
-
validTo: null,
|
| 147 |
-
isCurrent: true
|
| 148 |
-
}
|
| 149 |
};
|
| 150 |
nodeMap.set(parentStateId, parentState);
|
| 151 |
registerStateNode(parentId, parentState);
|
|
@@ -154,12 +156,7 @@ export function generateSemanticGraph(eventLog = [], baseNodes = [], currentTime
|
|
| 154 |
id: slotStateId,
|
| 155 |
label: `${slotId}_State`,
|
| 156 |
type: 'State',
|
| 157 |
-
properties: {
|
| 158 |
-
ownerId: slotId,
|
| 159 |
-
validFrom: timestamp,
|
| 160 |
-
validTo: null,
|
| 161 |
-
isCurrent: true
|
| 162 |
-
}
|
| 163 |
};
|
| 164 |
nodeMap.set(slotStateId, slotState);
|
| 165 |
registerStateNode(slotId, slotState);
|
|
@@ -168,17 +165,11 @@ export function generateSemanticGraph(eventLog = [], baseNodes = [], currentTime
|
|
| 168 |
id: childStateId,
|
| 169 |
label: `${childId}_State`,
|
| 170 |
type: 'State',
|
| 171 |
-
properties: {
|
| 172 |
-
ownerId: childId,
|
| 173 |
-
validFrom: timestamp,
|
| 174 |
-
validTo: null,
|
| 175 |
-
isCurrent: true
|
| 176 |
-
}
|
| 177 |
};
|
| 178 |
nodeMap.set(childStateId, childState);
|
| 179 |
registerStateNode(childId, childState);
|
| 180 |
|
| 181 |
-
// Link temporal states to their 4D extents
|
| 182 |
edges.push({
|
| 183 |
id: `tpo_${parentStateId}`,
|
| 184 |
from: parentStateId,
|
|
@@ -215,14 +206,10 @@ export function generateSemanticGraph(eventLog = [], baseNodes = [], currentTime
|
|
| 215 |
label: 'has_slot',
|
| 216 |
type: 'has_slot',
|
| 217 |
childId: slotId,
|
| 218 |
-
parentId
|
| 219 |
childStateId: slotStateId,
|
| 220 |
-
parentStateId
|
| 221 |
-
properties: {
|
| 222 |
-
validFrom: timestamp,
|
| 223 |
-
validTo: null,
|
| 224 |
-
isCurrent: true
|
| 225 |
-
}
|
| 226 |
});
|
| 227 |
|
| 228 |
// Link 2: Child State -> Slot State (mounted_in)
|
|
@@ -237,11 +224,7 @@ export function generateSemanticGraph(eventLog = [], baseNodes = [], currentTime
|
|
| 237 |
parentId: slotId,
|
| 238 |
childStateId,
|
| 239 |
parentStateId: slotStateId,
|
| 240 |
-
properties: {
|
| 241 |
-
validFrom: timestamp,
|
| 242 |
-
validTo: null,
|
| 243 |
-
isCurrent: true
|
| 244 |
-
}
|
| 245 |
});
|
| 246 |
break;
|
| 247 |
}
|
|
@@ -250,7 +233,6 @@ export function generateSemanticGraph(eventLog = [], baseNodes = [], currentTime
|
|
| 250 |
const { childId } = event;
|
| 251 |
const dismantleTime = event.valid_from;
|
| 252 |
|
| 253 |
-
// Find active assembly or storage edge for this child
|
| 254 |
const activeAssembly = edges.find(
|
| 255 |
e => (e.childId === childId || e.from.startsWith(`st_${childId}_`)) &&
|
| 256 |
(e.label === 'mounted_in' || e.label === 'stored_at') &&
|
|
@@ -258,25 +240,43 @@ export function generateSemanticGraph(eventLog = [], baseNodes = [], currentTime
|
|
| 258 |
);
|
| 259 |
if (activeAssembly) {
|
| 260 |
activeAssembly.properties.validTo = dismantleTime;
|
| 261 |
-
activeAssembly.properties.isCurrent = (currentTime !== null && currentTime !== undefined)
|
| 262 |
-
? dismantleTime > currentTime
|
| 263 |
-
: false;
|
| 264 |
|
| 265 |
-
// Close
|
| 266 |
const childState = nodeMap.get(activeAssembly.childStateId);
|
| 267 |
if (childState) {
|
| 268 |
childState.properties.validTo = dismantleTime;
|
| 269 |
-
childState.properties.isCurrent = activeAssembly.properties.isCurrent;
|
| 270 |
}
|
| 271 |
-
|
| 272 |
-
// Close the child's temporalPartOf link
|
| 273 |
const tpoChild = edges.find(e => e.id === `tpo_${activeAssembly.childStateId}`);
|
| 274 |
if (tpoChild) {
|
| 275 |
tpoChild.properties.validTo = dismantleTime;
|
| 276 |
-
tpoChild.properties.isCurrent = activeAssembly.properties.isCurrent;
|
| 277 |
}
|
| 278 |
-
|
| 279 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 280 |
}
|
| 281 |
break;
|
| 282 |
}
|
|
@@ -287,9 +287,8 @@ export function generateSemanticGraph(eventLog = [], baseNodes = [], currentTime
|
|
| 287 |
|
| 288 |
const targetExtentId = ensureExtentNode(targetId);
|
| 289 |
|
| 290 |
-
// Find current active State of the target part
|
| 291 |
const existingStates = stateNodesByOwner.get(targetId) || [];
|
| 292 |
-
let targetState = existingStates.find(st => st.properties?.
|
| 293 |
existingStates[existingStates.length - 1];
|
| 294 |
|
| 295 |
if (!targetState) {
|
|
@@ -331,7 +330,9 @@ export function generateSemanticGraph(eventLog = [], baseNodes = [], currentTime
|
|
| 331 |
value: defectValue,
|
| 332 |
targetId,
|
| 333 |
entityClass: 'Defect',
|
| 334 |
-
validFrom: timestamp
|
|
|
|
|
|
|
| 335 |
}
|
| 336 |
});
|
| 337 |
|
|
@@ -345,49 +346,69 @@ export function generateSemanticGraph(eventLog = [], baseNodes = [], currentTime
|
|
| 345 |
properties: {
|
| 346 |
validFrom: timestamp,
|
| 347 |
validTo: null,
|
| 348 |
-
isCurrent:
|
| 349 |
}
|
| 350 |
});
|
| 351 |
break;
|
| 352 |
}
|
| 353 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 354 |
default:
|
| 355 |
break;
|
| 356 |
}
|
| 357 |
});
|
| 358 |
|
| 359 |
-
// 4. Synchronise final temporal currentness flags
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 360 |
edges.forEach(edge => {
|
| 361 |
-
edge.properties.isCurrent = edge.properties.validTo
|
| 362 |
-
(currentTime !== null && currentTime !== undefined ? edge.properties.validTo > currentTime : false);
|
| 363 |
});
|
| 364 |
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 370 |
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
* @returns {string} Inferred entity class name.
|
| 376 |
-
*/
|
| 377 |
-
function inferEntityType(id) {
|
| 378 |
-
if (/^veh/i.test(id)) return 'Vehicle';
|
| 379 |
-
if (/^slot/i.test(id)) return 'Slot';
|
| 380 |
-
if (/^def/i.test(id)) return 'Defect';
|
| 381 |
-
if (/^ware|^wh/i.test(id)) return 'Warehouse';
|
| 382 |
-
return 'Part';
|
| 383 |
}
|
| 384 |
|
| 385 |
-
/**
|
| 386 |
-
* Capitalises the first letter of a string.
|
| 387 |
-
* @param {string} str
|
| 388 |
-
* @returns {string}
|
| 389 |
-
*/
|
| 390 |
function capitalize(str) {
|
| 391 |
if (!str) return '';
|
| 392 |
return str.charAt(0).toUpperCase() + str.slice(1);
|
| 393 |
-
}
|
|
|
|
| 2 |
* ISO 15926 ONTOLOGY ADAPTER (4D Perdurant)
|
| 3 |
* -----------------------------------------
|
| 4 |
* Maps Domain Business Events into a canonical 4D ISO 15926 Semantic Graph.
|
| 5 |
+
*
|
| 6 |
* Ontological Axioms:
|
| 7 |
+
* - Physical entities (Vehicle, Part, Warehouse, Slot) are 4D individuals ('ActualIndividual')
|
| 8 |
* with spatio-temporal extents across their entire lifetime.
|
| 9 |
+
* - Relationships connect temporal slices ('State') of those entities during specific validity periods.
|
| 10 |
+
* - Each 'State' is linked to its 4D extent via 'temporalPartOf'.
|
| 11 |
+
* - A 'Defect' classifies the temporal 'State' of the entity.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
*/
|
| 13 |
|
| 14 |
+
import { inferEntityClass } from '../smrm/definitions.js';
|
| 15 |
+
|
| 16 |
+
/**
|
| 17 |
+
* Generates the 4D ISO 15926 Semantic Graph from the event log up to currentTime.
|
| 18 |
+
*
|
| 19 |
+
* @param {Array} eventLog - Temporal business events.
|
| 20 |
+
* @param {Array} [baseNodes=[]] - Initial base entities.
|
| 21 |
+
* @param {number|null} [currentTime=null] - Global evaluation time T.
|
| 22 |
+
* @param {Object} [options={}] - { showHistory: boolean }.
|
| 23 |
+
* @returns {{ nodes: Array, edges: Array }}
|
| 24 |
+
*/
|
| 25 |
+
export function generateSemanticGraph(eventLog = [], baseNodes = [], currentTime = null, options = {}) {
|
| 26 |
const nodeMap = new Map();
|
| 27 |
const edges = [];
|
| 28 |
const stateNodesByOwner = new Map();
|
| 29 |
+
const showHistory = options.showHistory !== undefined ? Boolean(options.showHistory) : true;
|
| 30 |
|
| 31 |
function registerStateNode(ownerId, stateNode) {
|
| 32 |
if (!stateNodesByOwner.has(ownerId)) {
|
|
|
|
| 38 |
function ensureExtentNode(partId, overrideClass = null) {
|
| 39 |
const extentId = `4d_${partId}`;
|
| 40 |
if (!nodeMap.has(extentId)) {
|
| 41 |
+
const entityClass = inferEntityClass(partId, overrideClass);
|
| 42 |
nodeMap.set(extentId, {
|
| 43 |
id: extentId,
|
| 44 |
label: `${partId}_4D`,
|
| 45 |
type: 'ActualIndividual',
|
| 46 |
properties: {
|
| 47 |
originalId: partId,
|
| 48 |
+
entityClass
|
| 49 |
}
|
| 50 |
});
|
| 51 |
}
|
| 52 |
return extentId;
|
| 53 |
}
|
| 54 |
|
| 55 |
+
// 1. Initialise base 4D extent nodes
|
| 56 |
(baseNodes || []).forEach(bn => {
|
| 57 |
+
ensureExtentNode(bn.id, bn.entityClass || bn.type);
|
| 58 |
});
|
| 59 |
|
| 60 |
// 2. Filter and sort business events by timestamp
|
|
|
|
| 63 |
.slice()
|
| 64 |
.sort((a, b) => (a.valid_from || 0) - (b.valid_from || 0));
|
| 65 |
|
|
|
|
| 66 |
// 3. Process business events in temporal order
|
| 67 |
eventsToProcess.forEach(event => {
|
| 68 |
switch (event.type) {
|
| 69 |
case 'REGISTER_PART': {
|
| 70 |
+
ensureExtentNode(event.partId, event.entityClass);
|
| 71 |
break;
|
| 72 |
}
|
| 73 |
|
|
|
|
| 128 |
type: 'composition',
|
| 129 |
childId,
|
| 130 |
parentId,
|
| 131 |
+
childStateId,
|
| 132 |
+
parentStateId,
|
| 133 |
properties: { validFrom: timestamp, validTo: null, isCurrent: true }
|
| 134 |
});
|
| 135 |
break;
|
| 136 |
}
|
| 137 |
|
| 138 |
+
// Contextual Slot generation: Slot_${parentId}_${childId}
|
| 139 |
const slotId = `Slot_${parentId}_${childId}`;
|
| 140 |
const slotExtentId = ensureExtentNode(slotId, 'Slot');
|
| 141 |
|
|
|
|
| 147 |
id: parentStateId,
|
| 148 |
label: `${parentId}_State`,
|
| 149 |
type: 'State',
|
| 150 |
+
properties: { ownerId: parentId, validFrom: timestamp, validTo: null, isCurrent: true }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 151 |
};
|
| 152 |
nodeMap.set(parentStateId, parentState);
|
| 153 |
registerStateNode(parentId, parentState);
|
|
|
|
| 156 |
id: slotStateId,
|
| 157 |
label: `${slotId}_State`,
|
| 158 |
type: 'State',
|
| 159 |
+
properties: { ownerId: slotId, validFrom: timestamp, validTo: null, isCurrent: true }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 160 |
};
|
| 161 |
nodeMap.set(slotStateId, slotState);
|
| 162 |
registerStateNode(slotId, slotState);
|
|
|
|
| 165 |
id: childStateId,
|
| 166 |
label: `${childId}_State`,
|
| 167 |
type: 'State',
|
| 168 |
+
properties: { ownerId: childId, validFrom: timestamp, validTo: null, isCurrent: true }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 169 |
};
|
| 170 |
nodeMap.set(childStateId, childState);
|
| 171 |
registerStateNode(childId, childState);
|
| 172 |
|
|
|
|
| 173 |
edges.push({
|
| 174 |
id: `tpo_${parentStateId}`,
|
| 175 |
from: parentStateId,
|
|
|
|
| 206 |
label: 'has_slot',
|
| 207 |
type: 'has_slot',
|
| 208 |
childId: slotId,
|
| 209 |
+
parentId,
|
| 210 |
childStateId: slotStateId,
|
| 211 |
+
parentStateId,
|
| 212 |
+
properties: { validFrom: timestamp, validTo: null, isCurrent: true }
|
|
|
|
|
|
|
|
|
|
|
|
|
| 213 |
});
|
| 214 |
|
| 215 |
// Link 2: Child State -> Slot State (mounted_in)
|
|
|
|
| 224 |
parentId: slotId,
|
| 225 |
childStateId,
|
| 226 |
parentStateId: slotStateId,
|
| 227 |
+
properties: { validFrom: timestamp, validTo: null, isCurrent: true }
|
|
|
|
|
|
|
|
|
|
|
|
|
| 228 |
});
|
| 229 |
break;
|
| 230 |
}
|
|
|
|
| 233 |
const { childId } = event;
|
| 234 |
const dismantleTime = event.valid_from;
|
| 235 |
|
|
|
|
| 236 |
const activeAssembly = edges.find(
|
| 237 |
e => (e.childId === childId || e.from.startsWith(`st_${childId}_`)) &&
|
| 238 |
(e.label === 'mounted_in' || e.label === 'stored_at') &&
|
|
|
|
| 240 |
);
|
| 241 |
if (activeAssembly) {
|
| 242 |
activeAssembly.properties.validTo = dismantleTime;
|
|
|
|
|
|
|
|
|
|
| 243 |
|
| 244 |
+
// Close child state slice and its temporalPartOf link
|
| 245 |
const childState = nodeMap.get(activeAssembly.childStateId);
|
| 246 |
if (childState) {
|
| 247 |
childState.properties.validTo = dismantleTime;
|
|
|
|
| 248 |
}
|
|
|
|
|
|
|
| 249 |
const tpoChild = edges.find(e => e.id === `tpo_${activeAssembly.childStateId}`);
|
| 250 |
if (tpoChild) {
|
| 251 |
tpoChild.properties.validTo = dismantleTime;
|
|
|
|
| 252 |
}
|
| 253 |
+
|
| 254 |
+
// Close contextual slot state, its has_slot edge, and the parent state slice
|
| 255 |
+
const slotStateId = activeAssembly.parentStateId;
|
| 256 |
+
if (slotStateId && slotStateId.startsWith('st_Slot_')) {
|
| 257 |
+
const slotState = nodeMap.get(slotStateId);
|
| 258 |
+
if (slotState) {
|
| 259 |
+
slotState.properties.validTo = dismantleTime;
|
| 260 |
+
}
|
| 261 |
+
const tpoSlot = edges.find(e => e.id === `tpo_${slotStateId}`);
|
| 262 |
+
if (tpoSlot) {
|
| 263 |
+
tpoSlot.properties.validTo = dismantleTime;
|
| 264 |
+
}
|
| 265 |
+
const hasSlotEdge = edges.find(
|
| 266 |
+
e => e.to === slotStateId && e.label === 'has_slot' && e.properties.validTo === null
|
| 267 |
+
);
|
| 268 |
+
if (hasSlotEdge) {
|
| 269 |
+
hasSlotEdge.properties.validTo = dismantleTime;
|
| 270 |
+
const parentState = nodeMap.get(hasSlotEdge.parentStateId);
|
| 271 |
+
if (parentState) {
|
| 272 |
+
parentState.properties.validTo = dismantleTime;
|
| 273 |
+
}
|
| 274 |
+
const tpoParent = edges.find(e => e.id === `tpo_${hasSlotEdge.parentStateId}`);
|
| 275 |
+
if (tpoParent) {
|
| 276 |
+
tpoParent.properties.validTo = dismantleTime;
|
| 277 |
+
}
|
| 278 |
+
}
|
| 279 |
+
}
|
| 280 |
}
|
| 281 |
break;
|
| 282 |
}
|
|
|
|
| 287 |
|
| 288 |
const targetExtentId = ensureExtentNode(targetId);
|
| 289 |
|
|
|
|
| 290 |
const existingStates = stateNodesByOwner.get(targetId) || [];
|
| 291 |
+
let targetState = existingStates.find(st => st.properties?.validTo === null) ||
|
| 292 |
existingStates[existingStates.length - 1];
|
| 293 |
|
| 294 |
if (!targetState) {
|
|
|
|
| 330 |
value: defectValue,
|
| 331 |
targetId,
|
| 332 |
entityClass: 'Defect',
|
| 333 |
+
validFrom: timestamp,
|
| 334 |
+
validTo: null,
|
| 335 |
+
isCurrent: true
|
| 336 |
}
|
| 337 |
});
|
| 338 |
|
|
|
|
| 346 |
properties: {
|
| 347 |
validFrom: timestamp,
|
| 348 |
validTo: null,
|
| 349 |
+
isCurrent: true
|
| 350 |
}
|
| 351 |
});
|
| 352 |
break;
|
| 353 |
}
|
| 354 |
|
| 355 |
+
case 'REVOKE_DEFECT': {
|
| 356 |
+
const { targetId, defectName } = event;
|
| 357 |
+
const revokeTime = event.valid_from;
|
| 358 |
+
const defectNodeId = `def_${targetId}_${defectName}`;
|
| 359 |
+
|
| 360 |
+
edges.forEach(e => {
|
| 361 |
+
if (e.to === defectNodeId && e.label === 'has_defect' && e.properties.validTo === null) {
|
| 362 |
+
e.properties.validTo = revokeTime;
|
| 363 |
+
}
|
| 364 |
+
});
|
| 365 |
+
const dNode = nodeMap.get(defectNodeId);
|
| 366 |
+
if (dNode && dNode.properties) {
|
| 367 |
+
dNode.properties.validTo = revokeTime;
|
| 368 |
+
}
|
| 369 |
+
break;
|
| 370 |
+
}
|
| 371 |
+
|
| 372 |
default:
|
| 373 |
break;
|
| 374 |
}
|
| 375 |
});
|
| 376 |
|
| 377 |
+
// 4. Synchronise final temporal currentness flags across States, Defects, and Edges
|
| 378 |
+
const isTimeActive = (validTo) =>
|
| 379 |
+
validTo === null || validTo === undefined ||
|
| 380 |
+
(currentTime !== null && currentTime !== undefined ? validTo > currentTime : false);
|
| 381 |
+
|
| 382 |
+
nodeMap.forEach(node => {
|
| 383 |
+
if ((node.type === 'State' || node.type === 'Defect') && node.properties) {
|
| 384 |
+
node.properties.isCurrent = isTimeActive(node.properties.validTo);
|
| 385 |
+
}
|
| 386 |
+
});
|
| 387 |
+
|
| 388 |
edges.forEach(edge => {
|
| 389 |
+
edge.properties.isCurrent = isTimeActive(edge.properties.validTo);
|
|
|
|
| 390 |
});
|
| 391 |
|
| 392 |
+
if (!showHistory) {
|
| 393 |
+
const activeEdges = edges.filter(e => e.properties.isCurrent !== false);
|
| 394 |
+
const activeNodeIds = new Set();
|
| 395 |
+
activeEdges.forEach(e => {
|
| 396 |
+
activeNodeIds.add(e.from);
|
| 397 |
+
activeNodeIds.add(e.to);
|
| 398 |
+
});
|
| 399 |
+
const activeNodes = Array.from(nodeMap.values()).filter(
|
| 400 |
+
n => n.type === 'ActualIndividual' || n.properties?.isCurrent !== false || activeNodeIds.has(n.id)
|
| 401 |
+
);
|
| 402 |
+
return { nodes: activeNodes, edges: activeEdges };
|
| 403 |
+
}
|
| 404 |
|
| 405 |
+
return {
|
| 406 |
+
nodes: Array.from(nodeMap.values()),
|
| 407 |
+
edges
|
| 408 |
+
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 409 |
}
|
| 410 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 411 |
function capitalize(str) {
|
| 412 |
if (!str) return '';
|
| 413 |
return str.charAt(0).toUpperCase() + str.slice(1);
|
| 414 |
+
}
|
js/smim/naive.js
CHANGED
|
@@ -1,44 +1,51 @@
|
|
| 1 |
/**
|
| 2 |
* NAIVE GRAPH ADAPTER (Direct Property Graph)
|
| 3 |
* --------------------------------------------
|
| 4 |
-
*
|
| 5 |
-
*
|
| 6 |
-
*
|
| 7 |
-
* In
|
| 8 |
-
* -
|
| 9 |
-
* - Assembly events create direct relationships (e.g. 'mounted_in', 'stored_at')
|
| 10 |
-
* directed from child component to parent entity.
|
| 11 |
-
* - Dismantling events locate the active assembly relationship and close its temporal validity.
|
| 12 |
-
* - Defect reports are attached directly to Node properties (if naiveDefectTarget === 'NODE')
|
| 13 |
-
* or to the active assembly Edge properties (if naiveDefectTarget === 'EDGE').
|
| 14 |
-
*
|
| 15 |
-
* Decoupled from database storage (SMS) and UI concerns, strictly adhering
|
| 16 |
-
* to the SemanticGraph data contract ({ nodes: [], edges: [] }).
|
| 17 |
-
*
|
| 18 |
-
* @param {Array} eventLog - The projection of temporal business events.
|
| 19 |
-
* @param {Array} [baseNodes=[]] - The projection of base entities.
|
| 20 |
-
* @param {number|null} [currentTime=null] - Global time to evaluate current validity.
|
| 21 |
-
* @param {Object} [options={}] - Optional configuration overrides.
|
| 22 |
-
* @returns {{ nodes: Array, edges: Array }} The canonical Semantic Graph.
|
| 23 |
*/
|
| 24 |
|
| 25 |
-
import {
|
| 26 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
export function generateSemanticGraph(eventLog = [], baseNodes = [], currentTime = null, options = {}) {
|
| 28 |
const nodeMap = new Map();
|
| 29 |
const edges = [];
|
| 30 |
|
| 31 |
-
const ontologyMode = options.ontology ||
|
| 32 |
-
const defectTargetMode = ontologyMode === 'NAIVE_EDGE' ? 'EDGE' :
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
|
| 34 |
-
// 1. Initialise base nodes
|
| 35 |
(baseNodes || []).forEach(bn => {
|
| 36 |
-
|
| 37 |
-
id: bn.id,
|
| 38 |
-
label: bn.label || bn.id,
|
| 39 |
-
type: bn.entityClass || bn.type || inferEntityType(bn.id),
|
| 40 |
-
properties: { ...(bn.properties || {}) }
|
| 41 |
-
});
|
| 42 |
});
|
| 43 |
|
| 44 |
// 2. Filter and sort business events by timestamp
|
|
@@ -47,48 +54,24 @@ export function generateSemanticGraph(eventLog = [], baseNodes = [], currentTime
|
|
| 47 |
.slice()
|
| 48 |
.sort((a, b) => (a.valid_from || 0) - (b.valid_from || 0));
|
| 49 |
|
| 50 |
-
|
| 51 |
-
// 3. Process business events in temporal order
|
| 52 |
eventsToProcess.forEach(event => {
|
| 53 |
switch (event.type) {
|
| 54 |
case 'REGISTER_PART': {
|
| 55 |
-
|
| 56 |
-
if (!nodeMap.has(partId)) {
|
| 57 |
-
nodeMap.set(partId, {
|
| 58 |
-
id: partId,
|
| 59 |
-
label: partId,
|
| 60 |
-
type: inferEntityType(partId),
|
| 61 |
-
properties: {}
|
| 62 |
-
});
|
| 63 |
-
}
|
| 64 |
break;
|
| 65 |
}
|
| 66 |
|
| 67 |
case 'ASSEMBLE': {
|
| 68 |
const { parentId, childId } = event;
|
| 69 |
const relType = event.relationType || 'mounted_in';
|
|
|
|
| 70 |
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
nodeMap.set(parentId, {
|
| 74 |
-
id: parentId,
|
| 75 |
-
label: parentId,
|
| 76 |
-
type: inferEntityType(parentId),
|
| 77 |
-
properties: {}
|
| 78 |
-
});
|
| 79 |
-
}
|
| 80 |
-
if (!nodeMap.has(childId)) {
|
| 81 |
-
nodeMap.set(childId, {
|
| 82 |
-
id: childId,
|
| 83 |
-
label: childId,
|
| 84 |
-
type: inferEntityType(childId),
|
| 85 |
-
properties: {}
|
| 86 |
-
});
|
| 87 |
-
}
|
| 88 |
|
| 89 |
-
// Warehouse storage links child directly to warehouse
|
| 90 |
if (relType === 'stored_at') {
|
| 91 |
-
const edgeId = event.id ? `edge_${event.id}` : `edge_${childId}_${parentId}_${
|
| 92 |
edges.push({
|
| 93 |
id: edgeId,
|
| 94 |
from: childId,
|
|
@@ -98,7 +81,7 @@ export function generateSemanticGraph(eventLog = [], baseNodes = [], currentTime
|
|
| 98 |
childId,
|
| 99 |
parentId,
|
| 100 |
properties: {
|
| 101 |
-
validFrom:
|
| 102 |
validTo: null,
|
| 103 |
isCurrent: true
|
| 104 |
}
|
|
@@ -106,19 +89,11 @@ export function generateSemanticGraph(eventLog = [], baseNodes = [], currentTime
|
|
| 106 |
break;
|
| 107 |
}
|
| 108 |
|
| 109 |
-
//
|
| 110 |
const slotId = `Slot_${parentId}_${childId}`;
|
| 111 |
-
|
| 112 |
-
nodeMap.set(slotId, {
|
| 113 |
-
id: slotId,
|
| 114 |
-
label: slotId,
|
| 115 |
-
type: 'Slot',
|
| 116 |
-
properties: { entityClass: 'Slot' }
|
| 117 |
-
});
|
| 118 |
-
}
|
| 119 |
|
| 120 |
-
|
| 121 |
-
const slotEdgeId = `edge_slot_${parentId}_${slotId}_${event.valid_from}`;
|
| 122 |
edges.push({
|
| 123 |
id: slotEdgeId,
|
| 124 |
from: parentId,
|
|
@@ -126,16 +101,15 @@ export function generateSemanticGraph(eventLog = [], baseNodes = [], currentTime
|
|
| 126 |
label: 'has_slot',
|
| 127 |
type: 'has_slot',
|
| 128 |
childId: slotId,
|
| 129 |
-
parentId
|
| 130 |
properties: {
|
| 131 |
-
validFrom:
|
| 132 |
validTo: null,
|
| 133 |
isCurrent: true
|
| 134 |
}
|
| 135 |
});
|
| 136 |
|
| 137 |
-
|
| 138 |
-
const mountEdgeId = event.id ? `edge_${event.id}` : `edge_${childId}_${slotId}_${event.valid_from}`;
|
| 139 |
edges.push({
|
| 140 |
id: mountEdgeId,
|
| 141 |
from: childId,
|
|
@@ -145,7 +119,7 @@ export function generateSemanticGraph(eventLog = [], baseNodes = [], currentTime
|
|
| 145 |
childId,
|
| 146 |
parentId: slotId,
|
| 147 |
properties: {
|
| 148 |
-
validFrom:
|
| 149 |
validTo: null,
|
| 150 |
isCurrent: true
|
| 151 |
}
|
|
@@ -157,7 +131,7 @@ export function generateSemanticGraph(eventLog = [], baseNodes = [], currentTime
|
|
| 157 |
const { childId } = event;
|
| 158 |
const dismantleTime = event.valid_from;
|
| 159 |
|
| 160 |
-
//
|
| 161 |
const activeEdge = edges.find(
|
| 162 |
e => (e.childId === childId || e.from === childId) &&
|
| 163 |
(e.label === 'mounted_in' || e.label === 'stored_at') &&
|
|
@@ -165,11 +139,18 @@ export function generateSemanticGraph(eventLog = [], baseNodes = [], currentTime
|
|
| 165 |
);
|
| 166 |
if (activeEdge) {
|
| 167 |
activeEdge.properties.validTo = dismantleTime;
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 173 |
}
|
| 174 |
break;
|
| 175 |
}
|
|
@@ -178,61 +159,56 @@ export function generateSemanticGraph(eventLog = [], baseNodes = [], currentTime
|
|
| 178 |
const { targetId, defectName, defectValue } = event;
|
| 179 |
|
| 180 |
if (defectTargetMode === 'NODE') {
|
| 181 |
-
|
| 182 |
-
if (!targetNode) {
|
| 183 |
-
targetNode = {
|
| 184 |
-
id: targetId,
|
| 185 |
-
label: targetId,
|
| 186 |
-
type: inferEntityType(targetId),
|
| 187 |
-
properties: {}
|
| 188 |
-
};
|
| 189 |
-
nodeMap.set(targetId, targetNode);
|
| 190 |
-
}
|
| 191 |
targetNode.properties[defectName] = defectValue;
|
| 192 |
-
} else
|
| 193 |
-
//
|
| 194 |
const activeEdge = edges.find(
|
| 195 |
e => (e.childId === targetId || e.from === targetId) &&
|
| 196 |
-
(e.label === 'mounted_in' || e.
|
| 197 |
(e.properties.validTo === null || e.properties.validTo >= event.valid_from)
|
| 198 |
);
|
| 199 |
if (activeEdge) {
|
| 200 |
activeEdge.properties[defectName] = defectValue;
|
| 201 |
-
} else {
|
| 202 |
-
console.warn(`[SMIM Naive] No active edge found for defect report on part: ${targetId}`);
|
| 203 |
}
|
| 204 |
}
|
| 205 |
break;
|
| 206 |
}
|
| 207 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 208 |
default:
|
| 209 |
break;
|
| 210 |
}
|
| 211 |
});
|
| 212 |
|
| 213 |
-
// 4. Synchronise
|
| 214 |
edges.forEach(edge => {
|
| 215 |
-
edge.properties.isCurrent = edge.properties.validTo === null ||
|
| 216 |
(currentTime !== null && currentTime !== undefined ? edge.properties.validTo > currentTime : false);
|
| 217 |
});
|
| 218 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 219 |
return {
|
| 220 |
nodes: Array.from(nodeMap.values()),
|
| 221 |
-
edges
|
| 222 |
};
|
| 223 |
-
}
|
| 224 |
-
|
| 225 |
-
/**
|
| 226 |
-
* Fallback helper to infer entity type from common identifier naming conventions.
|
| 227 |
-
*
|
| 228 |
-
* @param {string} id - Node identifier.
|
| 229 |
-
* @returns {string} Inferred entity class name.
|
| 230 |
-
*/
|
| 231 |
-
function inferEntityType(id) {
|
| 232 |
-
if (/^veh/i.test(id)) return 'Vehicle';
|
| 233 |
-
if (/^slot/i.test(id)) return 'Slot';
|
| 234 |
-
if (/^def/i.test(id)) return 'Defect';
|
| 235 |
-
if (/^ware|^wh/i.test(id)) return 'Warehouse';
|
| 236 |
-
return 'Part';
|
| 237 |
-
}
|
| 238 |
-
|
|
|
|
| 1 |
/**
|
| 2 |
* NAIVE GRAPH ADAPTER (Direct Property Graph)
|
| 3 |
* --------------------------------------------
|
| 4 |
+
* Pure function mapping Domain Business Events into an unconstrained Property Graph
|
| 5 |
+
* without 3D Continuant (BFO) or 4D Perdurant (ISO 15926) formalisms.
|
| 6 |
+
*
|
| 7 |
+
* - In 'NAIVE_NODE' mode: Defect reports mutate node properties directly (overwriting history).
|
| 8 |
+
* - In 'NAIVE_EDGE' mode: Defect reports attach to the active assembly edge (vanishing upon dismantle).
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
*/
|
| 10 |
|
| 11 |
+
import { inferEntityClass } from '../smrm/definitions.js';
|
| 12 |
|
| 13 |
+
/**
|
| 14 |
+
* Generates the Naive Semantic Graph from the event log up to currentTime.
|
| 15 |
+
*
|
| 16 |
+
* @param {Array} eventLog - Temporal business events.
|
| 17 |
+
* @param {Array} [baseNodes=[]] - Initial base entities.
|
| 18 |
+
* @param {number|null} [currentTime=null] - Global evaluation time T.
|
| 19 |
+
* @param {Object} [options={}] - { ontology: 'NAIVE_NODE' | 'NAIVE_EDGE', showHistory: boolean }.
|
| 20 |
+
* @returns {{ nodes: Array, edges: Array }}
|
| 21 |
+
*/
|
| 22 |
export function generateSemanticGraph(eventLog = [], baseNodes = [], currentTime = null, options = {}) {
|
| 23 |
const nodeMap = new Map();
|
| 24 |
const edges = [];
|
| 25 |
|
| 26 |
+
const ontologyMode = options.ontology || 'NAIVE_NODE';
|
| 27 |
+
const defectTargetMode = ontologyMode === 'NAIVE_EDGE' ? 'EDGE' : 'NODE';
|
| 28 |
+
const showHistory = options.showHistory !== undefined ? Boolean(options.showHistory) : true;
|
| 29 |
+
|
| 30 |
+
function ensureNode(id, explicitClass = null, extraProps = {}) {
|
| 31 |
+
if (!nodeMap.has(id)) {
|
| 32 |
+
const resolvedClass = inferEntityClass(id, explicitClass);
|
| 33 |
+
nodeMap.set(id, {
|
| 34 |
+
id,
|
| 35 |
+
label: id,
|
| 36 |
+
type: resolvedClass,
|
| 37 |
+
properties: {
|
| 38 |
+
entityClass: resolvedClass,
|
| 39 |
+
...extraProps
|
| 40 |
+
}
|
| 41 |
+
});
|
| 42 |
+
}
|
| 43 |
+
return nodeMap.get(id);
|
| 44 |
+
}
|
| 45 |
|
| 46 |
+
// 1. Initialise base nodes
|
| 47 |
(baseNodes || []).forEach(bn => {
|
| 48 |
+
ensureNode(bn.id, bn.entityClass || bn.type, bn.properties || {});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
});
|
| 50 |
|
| 51 |
// 2. Filter and sort business events by timestamp
|
|
|
|
| 54 |
.slice()
|
| 55 |
.sort((a, b) => (a.valid_from || 0) - (b.valid_from || 0));
|
| 56 |
|
| 57 |
+
// 3. Process business events in chronological order
|
|
|
|
| 58 |
eventsToProcess.forEach(event => {
|
| 59 |
switch (event.type) {
|
| 60 |
case 'REGISTER_PART': {
|
| 61 |
+
ensureNode(event.partId, event.entityClass);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
break;
|
| 63 |
}
|
| 64 |
|
| 65 |
case 'ASSEMBLE': {
|
| 66 |
const { parentId, childId } = event;
|
| 67 |
const relType = event.relationType || 'mounted_in';
|
| 68 |
+
const timestamp = event.valid_from;
|
| 69 |
|
| 70 |
+
ensureNode(parentId);
|
| 71 |
+
ensureNode(childId);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
|
|
|
|
| 73 |
if (relType === 'stored_at') {
|
| 74 |
+
const edgeId = event.id ? `edge_${event.id}` : `edge_${childId}_${parentId}_${timestamp}`;
|
| 75 |
edges.push({
|
| 76 |
id: edgeId,
|
| 77 |
from: childId,
|
|
|
|
| 81 |
childId,
|
| 82 |
parentId,
|
| 83 |
properties: {
|
| 84 |
+
validFrom: timestamp,
|
| 85 |
validTo: null,
|
| 86 |
isCurrent: true
|
| 87 |
}
|
|
|
|
| 89 |
break;
|
| 90 |
}
|
| 91 |
|
| 92 |
+
// Contextual Slot generation: Slot_${parentId}_${childId}
|
| 93 |
const slotId = `Slot_${parentId}_${childId}`;
|
| 94 |
+
ensureNode(slotId, 'Slot');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 95 |
|
| 96 |
+
const slotEdgeId = `edge_slot_${event.id || `${parentId}_${slotId}_${timestamp}`}`;
|
|
|
|
| 97 |
edges.push({
|
| 98 |
id: slotEdgeId,
|
| 99 |
from: parentId,
|
|
|
|
| 101 |
label: 'has_slot',
|
| 102 |
type: 'has_slot',
|
| 103 |
childId: slotId,
|
| 104 |
+
parentId,
|
| 105 |
properties: {
|
| 106 |
+
validFrom: timestamp,
|
| 107 |
validTo: null,
|
| 108 |
isCurrent: true
|
| 109 |
}
|
| 110 |
});
|
| 111 |
|
| 112 |
+
const mountEdgeId = event.id ? `edge_${event.id}` : `edge_${childId}_${slotId}_${timestamp}`;
|
|
|
|
| 113 |
edges.push({
|
| 114 |
id: mountEdgeId,
|
| 115 |
from: childId,
|
|
|
|
| 119 |
childId,
|
| 120 |
parentId: slotId,
|
| 121 |
properties: {
|
| 122 |
+
validFrom: timestamp,
|
| 123 |
validTo: null,
|
| 124 |
isCurrent: true
|
| 125 |
}
|
|
|
|
| 131 |
const { childId } = event;
|
| 132 |
const dismantleTime = event.valid_from;
|
| 133 |
|
| 134 |
+
// Close active mounted_in or stored_at edge for this child
|
| 135 |
const activeEdge = edges.find(
|
| 136 |
e => (e.childId === childId || e.from === childId) &&
|
| 137 |
(e.label === 'mounted_in' || e.label === 'stored_at') &&
|
|
|
|
| 139 |
);
|
| 140 |
if (activeEdge) {
|
| 141 |
activeEdge.properties.validTo = dismantleTime;
|
| 142 |
+
|
| 143 |
+
// Also close the contextual has_slot edge if mounted in a Slot
|
| 144 |
+
if (activeEdge.to && activeEdge.to.startsWith('Slot_')) {
|
| 145 |
+
const slotEdge = edges.find(
|
| 146 |
+
e => e.to === activeEdge.to &&
|
| 147 |
+
e.label === 'has_slot' &&
|
| 148 |
+
e.properties.validTo === null
|
| 149 |
+
);
|
| 150 |
+
if (slotEdge) {
|
| 151 |
+
slotEdge.properties.validTo = dismantleTime;
|
| 152 |
+
}
|
| 153 |
+
}
|
| 154 |
}
|
| 155 |
break;
|
| 156 |
}
|
|
|
|
| 159 |
const { targetId, defectName, defectValue } = event;
|
| 160 |
|
| 161 |
if (defectTargetMode === 'NODE') {
|
| 162 |
+
const targetNode = ensureNode(targetId);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 163 |
targetNode.properties[defectName] = defectValue;
|
| 164 |
+
} else {
|
| 165 |
+
// EDGE mode: attach defect to currently active assembly/storage edge
|
| 166 |
const activeEdge = edges.find(
|
| 167 |
e => (e.childId === targetId || e.from === targetId) &&
|
| 168 |
+
(e.label === 'mounted_in' || e.label === 'stored_at') &&
|
| 169 |
(e.properties.validTo === null || e.properties.validTo >= event.valid_from)
|
| 170 |
);
|
| 171 |
if (activeEdge) {
|
| 172 |
activeEdge.properties[defectName] = defectValue;
|
|
|
|
|
|
|
| 173 |
}
|
| 174 |
}
|
| 175 |
break;
|
| 176 |
}
|
| 177 |
|
| 178 |
+
case 'REVOKE_DEFECT': {
|
| 179 |
+
const { targetId, defectName } = event;
|
| 180 |
+
if (defectTargetMode === 'NODE') {
|
| 181 |
+
const targetNode = nodeMap.get(targetId);
|
| 182 |
+
if (targetNode && targetNode.properties) {
|
| 183 |
+
delete targetNode.properties[defectName];
|
| 184 |
+
}
|
| 185 |
+
} else {
|
| 186 |
+
edges.forEach(e => {
|
| 187 |
+
if ((e.childId === targetId || e.from === targetId) && e.properties && defectName in e.properties) {
|
| 188 |
+
delete e.properties[defectName];
|
| 189 |
+
}
|
| 190 |
+
});
|
| 191 |
+
}
|
| 192 |
+
break;
|
| 193 |
+
}
|
| 194 |
+
|
| 195 |
default:
|
| 196 |
break;
|
| 197 |
}
|
| 198 |
});
|
| 199 |
|
| 200 |
+
// 4. Synchronise temporal currentness flags
|
| 201 |
edges.forEach(edge => {
|
| 202 |
+
edge.properties.isCurrent = edge.properties.validTo === null ||
|
| 203 |
(currentTime !== null && currentTime !== undefined ? edge.properties.validTo > currentTime : false);
|
| 204 |
});
|
| 205 |
|
| 206 |
+
const filteredEdges = showHistory
|
| 207 |
+
? edges
|
| 208 |
+
: edges.filter(e => e.properties.isCurrent !== false);
|
| 209 |
+
|
| 210 |
return {
|
| 211 |
nodes: Array.from(nodeMap.values()),
|
| 212 |
+
edges: filteredEdges
|
| 213 |
};
|
| 214 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
js/smrm/actions.js
CHANGED
|
@@ -3,57 +3,62 @@
|
|
| 3 |
* ------------------------------------
|
| 4 |
* This module contains pure factory functions responsible for creating
|
| 5 |
* immutable business Domain Events for the Event Sourcing architecture.
|
| 6 |
-
*
|
| 7 |
-
* In accordance with Domain-Driven Event Sourcing:
|
| 8 |
-
* - Events represent pure business facts, NOT low-level graph mutations.
|
| 9 |
-
* - No graph concepts (e.g. 'NODE', 'EDGE') exist at this layer.
|
| 10 |
-
*
|
| 11 |
* Strict Architectural Rule:
|
| 12 |
* Functions here MUST NOT import the singleton state or directly mutate data.
|
| 13 |
* All functions are pure factories returning event descriptors to be dispatched.
|
| 14 |
*/
|
| 15 |
|
| 16 |
-
import { RELATION_TYPES } from './definitions.js';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
|
| 18 |
/**
|
| 19 |
-
* Creates a business event representing the registration/creation of a domain
|
| 20 |
-
*
|
| 21 |
-
*
|
| 22 |
-
* @param {
|
| 23 |
-
* @param {
|
|
|
|
| 24 |
* @returns {Object|null} The created registration business event, or null if validation fails.
|
| 25 |
*/
|
| 26 |
-
export function createRegisterPartEvent(partId, timestamp = 0) {
|
| 27 |
if (!partId) return null;
|
| 28 |
const trimmedId = String(partId).trim();
|
| 29 |
if (!trimmedId) return null;
|
| 30 |
|
|
|
|
|
|
|
| 31 |
return {
|
| 32 |
-
id: `evt_reg_${trimmedId}`,
|
| 33 |
type: 'REGISTER_PART',
|
| 34 |
partId: trimmedId,
|
|
|
|
| 35 |
valid_from: timestamp,
|
| 36 |
valid_to: null
|
| 37 |
};
|
| 38 |
}
|
| 39 |
|
| 40 |
/**
|
| 41 |
-
* Creates a business event representing the assembly or
|
| 42 |
-
*
|
| 43 |
-
*
|
| 44 |
-
* @param {string}
|
| 45 |
-
* @param {string}
|
| 46 |
-
* @param {
|
| 47 |
-
* @param {number} [
|
| 48 |
* @returns {Object|null} The created assembly business event, or null if validation fails.
|
| 49 |
*/
|
| 50 |
-
export function createAssembleEvent(parentId, childId, relationType = 'mounted_in', timestamp = 0) {
|
| 51 |
if (!parentId || !childId) return null;
|
| 52 |
|
| 53 |
const trimmedParent = String(parentId).trim();
|
| 54 |
const trimmedChild = String(childId).trim();
|
| 55 |
|
| 56 |
-
// Rule 1: An entity cannot be assembled into itself
|
| 57 |
if (!trimmedParent || !trimmedChild || trimmedParent === trimmedChild) {
|
| 58 |
return null;
|
| 59 |
}
|
|
@@ -63,7 +68,7 @@ export function createAssembleEvent(parentId, childId, relationType = 'mounted_i
|
|
| 63 |
: (relationType ? String(relationType).trim() : 'mounted_in');
|
| 64 |
|
| 65 |
return {
|
| 66 |
-
id: `evt_asm_${trimmedChild}_${timestamp}`,
|
| 67 |
type: 'ASSEMBLE',
|
| 68 |
parentId: trimmedParent,
|
| 69 |
childId: trimmedChild,
|
|
@@ -75,15 +80,15 @@ export function createAssembleEvent(parentId, childId, relationType = 'mounted_i
|
|
| 75 |
|
| 76 |
/**
|
| 77 |
* Creates a business event reporting a physical defect or quality observation on an entity.
|
| 78 |
-
*
|
| 79 |
-
*
|
| 80 |
-
* @param {string}
|
| 81 |
-
* @param {string}
|
| 82 |
-
* @param {
|
| 83 |
-
* @param {number} [
|
| 84 |
* @returns {Object|null} The created defect report business event, or null if validation fails.
|
| 85 |
*/
|
| 86 |
-
export function createReportDefectEvent(targetId, defectName, defectValue, timestamp = 0) {
|
| 87 |
if (!targetId || !defectName) return null;
|
| 88 |
|
| 89 |
const trimmedTarget = String(targetId).trim();
|
|
@@ -94,35 +99,62 @@ export function createReportDefectEvent(targetId, defectName, defectValue, times
|
|
| 94 |
}
|
| 95 |
|
| 96 |
return {
|
| 97 |
-
id: `evt_def_${trimmedTarget}_${trimmedDefect}_${timestamp}`,
|
| 98 |
type: 'REPORT_DEFECT',
|
| 99 |
targetId: trimmedTarget,
|
| 100 |
defectName: trimmedDefect,
|
| 101 |
-
defectValue: defectValue,
|
| 102 |
valid_from: timestamp,
|
| 103 |
valid_to: null
|
| 104 |
};
|
| 105 |
}
|
| 106 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
/**
|
| 108 |
* Creates a business event representing the dismantling or removal of a component.
|
| 109 |
-
*
|
| 110 |
-
*
|
| 111 |
-
* @param {
|
| 112 |
-
* @param {number} [
|
| 113 |
* @returns {Object|null} The created dismantling business event, or null if validation fails.
|
| 114 |
*/
|
| 115 |
-
export function createDismantleEvent(childId, timestamp = 0) {
|
| 116 |
if (!childId) return null;
|
| 117 |
|
| 118 |
const trimmedChild = String(childId).trim();
|
| 119 |
if (!trimmedChild) return null;
|
| 120 |
|
| 121 |
return {
|
| 122 |
-
id: `evt_dsm_${trimmedChild}_${timestamp}`,
|
| 123 |
type: 'DISMANTLE',
|
| 124 |
childId: trimmedChild,
|
| 125 |
valid_from: timestamp
|
| 126 |
};
|
| 127 |
-
}
|
| 128 |
-
|
|
|
|
| 3 |
* ------------------------------------
|
| 4 |
* This module contains pure factory functions responsible for creating
|
| 5 |
* immutable business Domain Events for the Event Sourcing architecture.
|
| 6 |
+
*
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
* Strict Architectural Rule:
|
| 8 |
* Functions here MUST NOT import the singleton state or directly mutate data.
|
| 9 |
* All functions are pure factories returning event descriptors to be dispatched.
|
| 10 |
*/
|
| 11 |
|
| 12 |
+
import { RELATION_TYPES, inferEntityClass } from './definitions.js';
|
| 13 |
+
|
| 14 |
+
let localCounter = 1;
|
| 15 |
+
|
| 16 |
+
function nextIdSuffix(seq) {
|
| 17 |
+
return seq !== null && seq !== undefined ? seq : localCounter++;
|
| 18 |
+
}
|
| 19 |
|
| 20 |
/**
|
| 21 |
+
* Creates a business event representing the registration/creation of a domain entity.
|
| 22 |
+
*
|
| 23 |
+
* @param {string} partId - Unique business identifier of the entity.
|
| 24 |
+
* @param {number} [timestamp=0] - Discrete system timestamp of registration.
|
| 25 |
+
* @param {string|null} [entityClass=null] - Optional explicit domain class ('Vehicle' | 'Part' | 'Warehouse').
|
| 26 |
+
* @param {number|null} [seq=null] - Optional monotonic sequence number.
|
| 27 |
* @returns {Object|null} The created registration business event, or null if validation fails.
|
| 28 |
*/
|
| 29 |
+
export function createRegisterPartEvent(partId, timestamp = 0, entityClass = null, seq = null) {
|
| 30 |
if (!partId) return null;
|
| 31 |
const trimmedId = String(partId).trim();
|
| 32 |
if (!trimmedId) return null;
|
| 33 |
|
| 34 |
+
const resolvedClass = inferEntityClass(trimmedId, entityClass);
|
| 35 |
+
|
| 36 |
return {
|
| 37 |
+
id: `evt_reg_${trimmedId}_${timestamp}_${nextIdSuffix(seq)}`,
|
| 38 |
type: 'REGISTER_PART',
|
| 39 |
partId: trimmedId,
|
| 40 |
+
entityClass: resolvedClass,
|
| 41 |
valid_from: timestamp,
|
| 42 |
valid_to: null
|
| 43 |
};
|
| 44 |
}
|
| 45 |
|
| 46 |
/**
|
| 47 |
+
* Creates a business event representing the assembly or storage of a child part into a parent entity.
|
| 48 |
+
*
|
| 49 |
+
* @param {string} parentId - Unique business identifier of the parent entity/container.
|
| 50 |
+
* @param {string} childId - Unique business identifier of the assembled child component.
|
| 51 |
+
* @param {string} [relationType='mounted_in'] - Relationship category ('mounted_in' | 'stored_at' | 'has_part').
|
| 52 |
+
* @param {number} [timestamp=0] - Discrete system timestamp of the assembly.
|
| 53 |
+
* @param {number|null} [seq=null] - Optional monotonic sequence number.
|
| 54 |
* @returns {Object|null} The created assembly business event, or null if validation fails.
|
| 55 |
*/
|
| 56 |
+
export function createAssembleEvent(parentId, childId, relationType = 'mounted_in', timestamp = 0, seq = null) {
|
| 57 |
if (!parentId || !childId) return null;
|
| 58 |
|
| 59 |
const trimmedParent = String(parentId).trim();
|
| 60 |
const trimmedChild = String(childId).trim();
|
| 61 |
|
|
|
|
| 62 |
if (!trimmedParent || !trimmedChild || trimmedParent === trimmedChild) {
|
| 63 |
return null;
|
| 64 |
}
|
|
|
|
| 68 |
: (relationType ? String(relationType).trim() : 'mounted_in');
|
| 69 |
|
| 70 |
return {
|
| 71 |
+
id: `evt_asm_${trimmedChild}_${timestamp}_${nextIdSuffix(seq)}`,
|
| 72 |
type: 'ASSEMBLE',
|
| 73 |
parentId: trimmedParent,
|
| 74 |
childId: trimmedChild,
|
|
|
|
| 80 |
|
| 81 |
/**
|
| 82 |
* Creates a business event reporting a physical defect or quality observation on an entity.
|
| 83 |
+
*
|
| 84 |
+
* @param {string} targetId - Unique identifier of the part or vehicle bearing the defect.
|
| 85 |
+
* @param {string} defectName - Defect category (e.g. 'crack', 'dent', 'wear').
|
| 86 |
+
* @param {string|number} defectValue - Recorded measurement or severity value (e.g. '2mm').
|
| 87 |
+
* @param {number} [timestamp=0] - Discrete system timestamp of the observation.
|
| 88 |
+
* @param {number|null} [seq=null] - Optional monotonic sequence number.
|
| 89 |
* @returns {Object|null} The created defect report business event, or null if validation fails.
|
| 90 |
*/
|
| 91 |
+
export function createReportDefectEvent(targetId, defectName, defectValue, timestamp = 0, seq = null) {
|
| 92 |
if (!targetId || !defectName) return null;
|
| 93 |
|
| 94 |
const trimmedTarget = String(targetId).trim();
|
|
|
|
| 99 |
}
|
| 100 |
|
| 101 |
return {
|
| 102 |
+
id: `evt_def_${trimmedTarget}_${trimmedDefect}_${timestamp}_${nextIdSuffix(seq)}`,
|
| 103 |
type: 'REPORT_DEFECT',
|
| 104 |
targetId: trimmedTarget,
|
| 105 |
defectName: trimmedDefect,
|
| 106 |
+
defectValue: defectValue || 'present',
|
| 107 |
valid_from: timestamp,
|
| 108 |
valid_to: null
|
| 109 |
};
|
| 110 |
}
|
| 111 |
|
| 112 |
+
/**
|
| 113 |
+
* Creates a business event representing the revocation/repair of a previously reported defect.
|
| 114 |
+
* Preserves Event Sourcing immutability instead of splicing the event log.
|
| 115 |
+
*
|
| 116 |
+
* @param {string} targetId - Identifier of the entity bearing the defect.
|
| 117 |
+
* @param {string} defectName - Defect category being revoked/repaired.
|
| 118 |
+
* @param {number} [timestamp=0] - Discrete system timestamp when revocation occurred.
|
| 119 |
+
* @param {string|null} [targetEventId=null] - Optional specific REPORT_DEFECT event ID to close.
|
| 120 |
+
* @param {number|null} [seq=null] - Optional monotonic sequence number.
|
| 121 |
+
* @returns {Object|null} The created defect revocation business event.
|
| 122 |
+
*/
|
| 123 |
+
export function createRevokeDefectEvent(targetId, defectName, timestamp = 0, targetEventId = null, seq = null) {
|
| 124 |
+
if (!targetId || !defectName) return null;
|
| 125 |
+
|
| 126 |
+
const trimmedTarget = String(targetId).trim();
|
| 127 |
+
const trimmedDefect = String(defectName).trim();
|
| 128 |
+
if (!trimmedTarget || !trimmedDefect) return null;
|
| 129 |
+
|
| 130 |
+
return {
|
| 131 |
+
id: `evt_rev_${trimmedTarget}_${trimmedDefect}_${timestamp}_${nextIdSuffix(seq)}`,
|
| 132 |
+
type: 'REVOKE_DEFECT',
|
| 133 |
+
targetId: trimmedTarget,
|
| 134 |
+
defectName: trimmedDefect,
|
| 135 |
+
targetEventId: targetEventId || null,
|
| 136 |
+
valid_from: timestamp
|
| 137 |
+
};
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
/**
|
| 141 |
* Creates a business event representing the dismantling or removal of a component.
|
| 142 |
+
*
|
| 143 |
+
* @param {string} childId - Unique business identifier of the dismantled component.
|
| 144 |
+
* @param {number} [timestamp=0] - Discrete system timestamp when dismantling occurred.
|
| 145 |
+
* @param {number|null} [seq=null] - Optional monotonic sequence number.
|
| 146 |
* @returns {Object|null} The created dismantling business event, or null if validation fails.
|
| 147 |
*/
|
| 148 |
+
export function createDismantleEvent(childId, timestamp = 0, seq = null) {
|
| 149 |
if (!childId) return null;
|
| 150 |
|
| 151 |
const trimmedChild = String(childId).trim();
|
| 152 |
if (!trimmedChild) return null;
|
| 153 |
|
| 154 |
return {
|
| 155 |
+
id: `evt_dsm_${trimmedChild}_${timestamp}_${nextIdSuffix(seq)}`,
|
| 156 |
type: 'DISMANTLE',
|
| 157 |
childId: trimmedChild,
|
| 158 |
valid_from: timestamp
|
| 159 |
};
|
| 160 |
+
}
|
|
|
js/smrm/definitions.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
| 1 |
/**
|
| 2 |
* SMRM DEFINITIONS & DOMAIN VOCABULARY
|
| 3 |
* ------------------------------------
|
| 4 |
-
* Standard domain vocabulary
|
| 5 |
-
* IEC 63339 Knowledge Architecture Workbench.
|
| 6 |
*/
|
| 7 |
|
| 8 |
/**
|
|
@@ -25,6 +25,53 @@ export const RELATION_TYPES = Object.freeze([
|
|
| 25 |
'has_slot',
|
| 26 |
'mounted_in',
|
| 27 |
'stored_at',
|
| 28 |
-
'has_defect'
|
|
|
|
| 29 |
]);
|
| 30 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
/**
|
| 2 |
* SMRM DEFINITIONS & DOMAIN VOCABULARY
|
| 3 |
* ------------------------------------
|
| 4 |
+
* Standard domain vocabulary, reference model definitions, and shared
|
| 5 |
+
* classification helpers for the IEC 63339 Knowledge Architecture Workbench.
|
| 6 |
*/
|
| 7 |
|
| 8 |
/**
|
|
|
|
| 25 |
'has_slot',
|
| 26 |
'mounted_in',
|
| 27 |
'stored_at',
|
| 28 |
+
'has_defect',
|
| 29 |
+
'has_part'
|
| 30 |
]);
|
| 31 |
|
| 32 |
+
/**
|
| 33 |
+
* Internal metadata property keys that should be hidden when rendering
|
| 34 |
+
* domain/custom property payloads on graph nodes, edges, and tables.
|
| 35 |
+
* @type {ReadonlyArray<string>}
|
| 36 |
+
*/
|
| 37 |
+
export const INTERNAL_PROPERTY_KEYS = Object.freeze([
|
| 38 |
+
'isCurrent',
|
| 39 |
+
'originalId',
|
| 40 |
+
'entityType',
|
| 41 |
+
'entityClass',
|
| 42 |
+
'validFrom',
|
| 43 |
+
'validTo',
|
| 44 |
+
'ownerId',
|
| 45 |
+
'childId',
|
| 46 |
+
'parentId',
|
| 47 |
+
'childStateId',
|
| 48 |
+
'parentStateId',
|
| 49 |
+
'qualityType',
|
| 50 |
+
'bearerId',
|
| 51 |
+
'defectType',
|
| 52 |
+
'targetId',
|
| 53 |
+
'value',
|
| 54 |
+
'atTime',
|
| 55 |
+
'originalEdgeId'
|
| 56 |
+
]);
|
| 57 |
+
|
| 58 |
+
/**
|
| 59 |
+
* Infers the manufacturing domain entity class from an explicit override
|
| 60 |
+
* or identifier naming conventions.
|
| 61 |
+
*
|
| 62 |
+
* @param {string} id - Node or part identifier.
|
| 63 |
+
* @param {string|null} [explicitClass=null] - Optional explicit class override.
|
| 64 |
+
* @returns {string} Resolved entity class ('Vehicle' | 'Slot' | 'Part' | 'Defect' | 'Warehouse').
|
| 65 |
+
*/
|
| 66 |
+
export function inferEntityClass(id, explicitClass = null) {
|
| 67 |
+
if (explicitClass && ENTITY_CLASSES.includes(explicitClass)) {
|
| 68 |
+
return explicitClass;
|
| 69 |
+
}
|
| 70 |
+
if (!id) return 'Part';
|
| 71 |
+
const cleanId = String(id).replace(/^(4d_|st_)/i, '');
|
| 72 |
+
if (/^(veh|sys|car)/i.test(cleanId)) return 'Vehicle';
|
| 73 |
+
if (/^slot/i.test(cleanId)) return 'Slot';
|
| 74 |
+
if (/^(ware|wh|stor)/i.test(cleanId)) return 'Warehouse';
|
| 75 |
+
if (/^(def|qual)/i.test(cleanId)) return 'Defect';
|
| 76 |
+
return 'Part';
|
| 77 |
+
}
|
js/sms/lpg-generator.js
CHANGED
|
@@ -1,23 +1,79 @@
|
|
| 1 |
/**
|
| 2 |
-
* SMS LAYER: LPG GENERATOR
|
| 3 |
-
* ------------------------
|
| 4 |
* Simulates how the Semantic Graph is persisted in a Labeled Property Graph
|
| 5 |
-
* database (
|
| 6 |
-
* Pure storage generator decoupled from ontologies and presentation markup.
|
| 7 |
*/
|
| 8 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
/**
|
| 10 |
-
* Generates an array of LPG edge storage records from a Semantic Graph.
|
|
|
|
| 11 |
*
|
| 12 |
-
* @param {Object} semanticGraph - Standardised Semantic Graph
|
| 13 |
-
* @returns {Array<Object>}
|
| 14 |
*/
|
| 15 |
export function generateLpgStorage(semanticGraph) {
|
| 16 |
if (!semanticGraph || !Array.isArray(semanticGraph.edges)) {
|
| 17 |
return [];
|
| 18 |
}
|
| 19 |
|
| 20 |
-
|
|
|
|
|
|
|
| 21 |
const props = edge.properties ? { ...edge.properties } : {};
|
| 22 |
const isActive = props.isCurrent !== undefined
|
| 23 |
? Boolean(props.isCurrent)
|
|
@@ -35,43 +91,136 @@ export function generateLpgStorage(semanticGraph) {
|
|
| 35 |
}
|
| 36 |
|
| 37 |
/**
|
| 38 |
-
* Generates
|
| 39 |
*
|
| 40 |
-
* @param {Object} semanticGraph - Standardised Semantic Graph
|
| 41 |
-
* @param {number} currentTime - Current discrete
|
|
|
|
| 42 |
* @returns {string} Cypher query string.
|
| 43 |
*/
|
| 44 |
-
export function generateLpgQuery(semanticGraph, currentTime) {
|
| 45 |
-
|
|
|
|
|
|
|
| 46 |
|
| 47 |
-
const nodes = semanticGraph.nodes || [];
|
| 48 |
-
|
| 49 |
-
const is4D = nodes.some(n => n.type === 'State');
|
| 50 |
if (is4D) {
|
| 51 |
-
return `// Cypher (Neo4j)
|
| 52 |
-
MATCH (
|
| 53 |
-
|
|
|
|
|
|
|
|
|
|
| 54 |
}
|
| 55 |
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
MATCH (
|
| 61 |
-
|
|
|
|
| 62 |
}
|
| 63 |
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
MATCH (n)-[r]->(m)
|
| 71 |
-
RETURN n, r, m`;
|
| 72 |
}
|
| 73 |
|
| 74 |
-
return `// Cypher (Neo4j)
|
| 75 |
-
|
| 76 |
-
|
|
|
|
|
|
|
| 77 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
/**
|
| 2 |
+
* SMS LAYER: LPG GENERATOR & LIVE QUERY ENGINE
|
| 3 |
+
* --------------------------------------------
|
| 4 |
* Simulates how the Semantic Graph is persisted in a Labeled Property Graph
|
| 5 |
+
* database (Neo4j) and executes live temporal queries at time T.
|
|
|
|
| 6 |
*/
|
| 7 |
|
| 8 |
+
import { INTERNAL_PROPERTY_KEYS } from '../smrm/definitions.js';
|
| 9 |
+
import { generatePhysicalGraph } from './sms-graph-generator.js';
|
| 10 |
+
import { reduceGraphForSMRM } from '../urmsm/smrm-view.js';
|
| 11 |
+
|
| 12 |
+
/**
|
| 13 |
+
* Generates an array of LPG node records (Node Store) from a Semantic Graph.
|
| 14 |
+
* Crucial for showing node-level properties (such as crack: '2mm' in NAIVE_NODE mode).
|
| 15 |
+
*
|
| 16 |
+
* @param {Object} semanticGraph - Standardised Semantic Graph.
|
| 17 |
+
* @returns {Array<{ id: string, label: string, type: string, properties: Object, isActive: boolean }>}
|
| 18 |
+
*/
|
| 19 |
+
export function generateLpgNodeStorage(semanticGraph) {
|
| 20 |
+
if (!semanticGraph || !Array.isArray(semanticGraph.nodes)) {
|
| 21 |
+
return [];
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
const physGraph = generatePhysicalGraph(semanticGraph, 'LPG');
|
| 25 |
+
|
| 26 |
+
return physGraph.nodes.map(node => {
|
| 27 |
+
const rawProps = node.properties ? { ...node.properties } : {};
|
| 28 |
+
const displayProps = {};
|
| 29 |
+
|
| 30 |
+
Object.entries(rawProps).forEach(([k, v]) => {
|
| 31 |
+
if (!INTERNAL_PROPERTY_KEYS.includes(k) && v !== undefined && v !== null) {
|
| 32 |
+
displayProps[k] = v;
|
| 33 |
+
}
|
| 34 |
+
});
|
| 35 |
+
|
| 36 |
+
if (node.type === 'Quality' || node.type === 'Defect') {
|
| 37 |
+
const dKey = rawProps.qualityType || rawProps.defectType || 'defect';
|
| 38 |
+
if (rawProps.value !== undefined) displayProps[dKey] = rawProps.value;
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
if (rawProps.validFrom !== undefined && rawProps.validFrom !== null) {
|
| 42 |
+
displayProps.validFrom = rawProps.validFrom;
|
| 43 |
+
}
|
| 44 |
+
if (rawProps.validTo !== undefined && rawProps.validTo !== null) {
|
| 45 |
+
displayProps.validTo = rawProps.validTo;
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
const isActive = rawProps.isCurrent !== undefined
|
| 49 |
+
? Boolean(rawProps.isCurrent)
|
| 50 |
+
: (rawProps.validTo === null || rawProps.validTo === undefined);
|
| 51 |
+
|
| 52 |
+
return {
|
| 53 |
+
id: node.id,
|
| 54 |
+
label: node.label || node.id,
|
| 55 |
+
type: node.type || rawProps.entityClass || 'Node',
|
| 56 |
+
properties: displayProps,
|
| 57 |
+
isActive
|
| 58 |
+
};
|
| 59 |
+
});
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
/**
|
| 63 |
+
* Generates an array of LPG edge storage records (Relationship Store) from a Semantic Graph.
|
| 64 |
+
* Uses the physical LPG graph so that 4D TRANSITION_TO chains are included.
|
| 65 |
*
|
| 66 |
+
* @param {Object} semanticGraph - Standardised Semantic Graph.
|
| 67 |
+
* @returns {Array<Object>} Relationship records for LPG storage table.
|
| 68 |
*/
|
| 69 |
export function generateLpgStorage(semanticGraph) {
|
| 70 |
if (!semanticGraph || !Array.isArray(semanticGraph.edges)) {
|
| 71 |
return [];
|
| 72 |
}
|
| 73 |
|
| 74 |
+
const physGraph = generatePhysicalGraph(semanticGraph, 'LPG');
|
| 75 |
+
|
| 76 |
+
return physGraph.edges.map(edge => {
|
| 77 |
const props = edge.properties ? { ...edge.properties } : {};
|
| 78 |
const isActive = props.isCurrent !== undefined
|
| 79 |
? Boolean(props.isCurrent)
|
|
|
|
| 91 |
}
|
| 92 |
|
| 93 |
/**
|
| 94 |
+
* Generates an ontology-aware temporal Cypher query for time T.
|
| 95 |
*
|
| 96 |
+
* @param {Object} semanticGraph - Standardised Semantic Graph.
|
| 97 |
+
* @param {number} currentTime - Current discrete timestamp T.
|
| 98 |
+
* @param {string} [ontology='NAIVE_NODE'] - Active ontology identifier.
|
| 99 |
* @returns {string} Cypher query string.
|
| 100 |
*/
|
| 101 |
+
export function generateLpgQuery(semanticGraph, currentTime = 0, ontology = 'NAIVE_NODE') {
|
| 102 |
+
const t = currentTime ?? 0;
|
| 103 |
+
const is4D = ontology === 'ISO15926' || semanticGraph?.nodes?.some(n => n.type === 'State');
|
| 104 |
+
const isBFO = ontology === 'BFO' || semanticGraph?.nodes?.some(n => n.type === 'MaterialEntity');
|
| 105 |
|
|
|
|
|
|
|
|
|
|
| 106 |
if (is4D) {
|
| 107 |
+
return `// Cypher (Neo4j) — ISO 15926 (4D) State & Defect Audit at T = ${t}
|
| 108 |
+
MATCH (item:ActualIndividual)<-[:temporalPartOf]-(st:State)-[rel:mounted_in|stored_at]->(locState:State)-[:temporalPartOf]->(loc:ActualIndividual)
|
| 109 |
+
WHERE st.validFrom <= ${t} AND (st.validTo IS NULL OR st.validTo > ${t})
|
| 110 |
+
OPTIONAL MATCH (item)<-[:temporalPartOf]-(anyState:State)-[hd:has_defect]->(def:Defect)
|
| 111 |
+
WHERE hd.validFrom <= ${t} AND (hd.validTo IS NULL OR hd.validTo > ${t})
|
| 112 |
+
RETURN item.originalId AS part, loc.originalId AS location, def.label AS defect`;
|
| 113 |
}
|
| 114 |
|
| 115 |
+
if (isBFO) {
|
| 116 |
+
return `// Cypher (Neo4j) — BFO (3D) Continuant & Quality Inherence at T = ${t}
|
| 117 |
+
MATCH (part:MaterialEntity)-[rel:mounted_in|stored_at]->(target:MaterialEntity)
|
| 118 |
+
WHERE rel.validFrom <= ${t} AND (rel.validTo IS NULL OR rel.validTo > ${t})
|
| 119 |
+
OPTIONAL MATCH (q:Quality)-[inh:inheres_in]->(part)
|
| 120 |
+
WHERE inh.validFrom <= ${t} AND (inh.validTo IS NULL OR inh.validTo > ${t})
|
| 121 |
+
RETURN part.id AS part, target.id AS location, q.label AS defect`;
|
| 122 |
}
|
| 123 |
|
| 124 |
+
if (ontology === 'NAIVE_EDGE') {
|
| 125 |
+
return `// Cypher (Neo4j) — Naive Graph (Edge) Active Structure at T = ${t}
|
| 126 |
+
// WARNING: Reads defect property from active relationship r!
|
| 127 |
+
MATCH (part:Part)-[r:mounted_in|stored_at]->(target)
|
| 128 |
+
WHERE r.validFrom <= ${t} AND (r.validTo IS NULL OR r.validTo > ${t})
|
| 129 |
+
RETURN part.id AS part, target.id AS location, r.crack AS defect`;
|
|
|
|
|
|
|
| 130 |
}
|
| 131 |
|
| 132 |
+
return `// Cypher (Neo4j) — Naive Graph (Node) Active Structure at T = ${t}
|
| 133 |
+
// WARNING: Node properties have no temporal history (overwritten in-place)!
|
| 134 |
+
MATCH (part:Part)-[r:mounted_in|stored_at]->(target)
|
| 135 |
+
WHERE r.validFrom <= ${t} AND (r.validTo IS NULL OR r.validTo > ${t})
|
| 136 |
+
RETURN part.id AS part, target.id AS location, part.crack AS defect`;
|
| 137 |
}
|
| 138 |
+
|
| 139 |
+
/**
|
| 140 |
+
* Executes the active temporal query against the current Semantic Graph in memory.
|
| 141 |
+
* Demonstrates why NAIVE_EDGE loses the defect upon moving to the warehouse,
|
| 142 |
+
* while BFO and ISO 15926 preserve it.
|
| 143 |
+
*
|
| 144 |
+
* @param {Object} semanticGraph - Current SMIM Semantic Graph.
|
| 145 |
+
* @param {number} currentTime - Current system time T.
|
| 146 |
+
* @param {string} [ontology='NAIVE_NODE'] - Active ontology mode.
|
| 147 |
+
* @returns {{ rows: Array<{ part: string, location: string, defect: string, hasDefect: boolean }>, defectiveCount: number }}
|
| 148 |
+
*/
|
| 149 |
+
export function evaluateLiveQuery(semanticGraph, currentTime = 0, ontology = 'NAIVE_NODE') {
|
| 150 |
+
if (!semanticGraph || !Array.isArray(semanticGraph.nodes) || semanticGraph.nodes.length === 0) {
|
| 151 |
+
return { rows: [], defectiveCount: 0 };
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
const smrm = reduceGraphForSMRM(semanticGraph);
|
| 155 |
+
const activeEdges = smrm.edges.filter(e => e.properties?.isCurrent !== false);
|
| 156 |
+
const rows = [];
|
| 157 |
+
|
| 158 |
+
// Map active defects in SMRM projection
|
| 159 |
+
const defectMap = new Map();
|
| 160 |
+
const addDefect = (id, text) => {
|
| 161 |
+
if (!defectMap.has(id)) defectMap.set(id, []);
|
| 162 |
+
defectMap.get(id).push(text);
|
| 163 |
+
};
|
| 164 |
+
|
| 165 |
+
smrm.nodes.forEach(n => {
|
| 166 |
+
if (n.type === 'Defect') return;
|
| 167 |
+
if (n.properties) {
|
| 168 |
+
Object.entries(n.properties).forEach(([k, v]) => {
|
| 169 |
+
if (!INTERNAL_PROPERTY_KEYS.includes(k) && v !== undefined && v !== null) {
|
| 170 |
+
addDefect(n.id, `${k}: ${v}`);
|
| 171 |
+
}
|
| 172 |
+
});
|
| 173 |
+
}
|
| 174 |
+
});
|
| 175 |
+
|
| 176 |
+
activeEdges.forEach(e => {
|
| 177 |
+
if (e.label === 'has_defect' || e.type === 'has_defect') {
|
| 178 |
+
const defNode = smrm.nodes.find(n => n.id === e.to);
|
| 179 |
+
if (defNode) {
|
| 180 |
+
const dType = defNode.properties?.defectType || defNode.properties?.qualityType || 'defect';
|
| 181 |
+
const dVal = defNode.properties?.value || 'present';
|
| 182 |
+
addDefect(e.from, `${dType}: ${dVal}`);
|
| 183 |
+
}
|
| 184 |
+
return;
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
const childId = e.childId || (e.label === 'stored_at' || e.label === 'mounted_in' ? e.from : e.to);
|
| 188 |
+
if (e.properties && childId) {
|
| 189 |
+
Object.entries(e.properties).forEach(([k, v]) => {
|
| 190 |
+
if (!INTERNAL_PROPERTY_KEYS.includes(k) && v !== undefined && v !== null) {
|
| 191 |
+
addDefect(childId, `${k}: ${v}`);
|
| 192 |
+
}
|
| 193 |
+
});
|
| 194 |
+
}
|
| 195 |
+
});
|
| 196 |
+
|
| 197 |
+
// Collect active placements (has_part / stored_at)
|
| 198 |
+
activeEdges.forEach(e => {
|
| 199 |
+
if (e.label === 'has_defect' || e.type === 'has_defect') return;
|
| 200 |
+
|
| 201 |
+
let partId = null;
|
| 202 |
+
let locationId = null;
|
| 203 |
+
|
| 204 |
+
if (e.label === 'has_part') {
|
| 205 |
+
locationId = e.from;
|
| 206 |
+
partId = e.to;
|
| 207 |
+
} else if (e.label === 'stored_at' || e.label === 'mounted_in') {
|
| 208 |
+
partId = e.from;
|
| 209 |
+
locationId = e.to;
|
| 210 |
+
}
|
| 211 |
+
|
| 212 |
+
if (partId && locationId) {
|
| 213 |
+
const defs = defectMap.get(partId) || [];
|
| 214 |
+
const hasDefect = defs.length > 0;
|
| 215 |
+
rows.push({
|
| 216 |
+
part: partId,
|
| 217 |
+
location: locationId,
|
| 218 |
+
defect: hasDefect ? defs.join(', ') : 'NULL (0 defects)',
|
| 219 |
+
hasDefect
|
| 220 |
+
});
|
| 221 |
+
}
|
| 222 |
+
});
|
| 223 |
+
|
| 224 |
+
const defectiveCount = rows.filter(r => r.hasDefect).length;
|
| 225 |
+
return { rows, defectiveCount };
|
| 226 |
+
}
|
js/sms/rdf-generator.js
CHANGED
|
@@ -2,229 +2,281 @@
|
|
| 2 |
* SMS LAYER: RDF GENERATOR
|
| 3 |
* ------------------------
|
| 4 |
* Simulates how the Semantic Graph is persisted in a W3C RDF Triplestore.
|
| 5 |
-
* Generates pure Turtle (.ttl) serialisation and SPARQL queries.
|
| 6 |
-
* Pure storage generator decoupled from ontologies and presentation markup.
|
| 7 |
*/
|
| 8 |
|
|
|
|
|
|
|
| 9 |
/**
|
| 10 |
* Generates a W3C RDF Turtle (.ttl) serialisation string from a Semantic Graph.
|
| 11 |
-
* If the graph contains 3D continuant entities, performs autonomous W3C Reification
|
| 12 |
-
* for temporal binary relations. If the graph contains 4D perdurants, serialises
|
| 13 |
-
* standard binary triples for temporal parts and states.
|
| 14 |
*
|
| 15 |
* @param {Object} semanticGraph - Standardised Semantic Graph with nodes and edges.
|
| 16 |
* @returns {string} Pure Turtle (.ttl) serialisation text.
|
| 17 |
*/
|
| 18 |
export function generateRdfStorage(semanticGraph) {
|
| 19 |
-
if (!semanticGraph) {
|
| 20 |
-
return '';
|
| 21 |
}
|
| 22 |
|
| 23 |
-
const is4D = semanticGraph.nodes
|
|
|
|
| 24 |
let ttl = '';
|
| 25 |
|
|
|
|
| 26 |
if (is4D) {
|
| 27 |
ttl += `@prefix iso: <http://example.org/iso15926/> .\n`;
|
| 28 |
ttl += `@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .\n\n`;
|
| 29 |
ttl += `# 4D Perdurant Graph persisted as Pure Binary RDF Triples\n`;
|
| 30 |
|
| 31 |
-
const individuals =
|
| 32 |
individuals.forEach(ind => {
|
| 33 |
-
|
|
|
|
| 34 |
});
|
| 35 |
|
| 36 |
-
const defectNodes =
|
| 37 |
defectNodes.forEach(d => {
|
| 38 |
-
|
|
|
|
|
|
|
| 39 |
});
|
| 40 |
|
| 41 |
-
const states =
|
| 42 |
states.forEach(st => {
|
| 43 |
-
const
|
| 44 |
-
e => e.from === st.id && (e.label === 'temporalPartOf' || e.type === 'classification') && !e.to.startsWith('def')
|
| 45 |
-
);
|
| 46 |
-
const partOfEdges = (semanticGraph.edges || []).filter(
|
| 47 |
-
e => e.from === st.id && (e.label === 'mounted_in' || e.type === 'mounted_in' || e.label === 'partOf')
|
| 48 |
-
);
|
| 49 |
-
const hasPartEdges = (semanticGraph.edges || []).filter(
|
| 50 |
-
e => e.from === st.id && (e.label === 'hasPart' || (e.type === 'composition' && e.label !== 'mounted_in' && e.type !== 'mounted_in'))
|
| 51 |
-
);
|
| 52 |
-
const hasSlotEdges = (semanticGraph.edges || []).filter(
|
| 53 |
-
e => e.from === st.id && (e.label === 'has_slot' || e.type === 'has_slot')
|
| 54 |
-
);
|
| 55 |
-
const storedAtEdges = (semanticGraph.edges || []).filter(
|
| 56 |
-
e => e.from === st.id && (e.label === 'stored_at' || e.type === 'stored_at')
|
| 57 |
-
);
|
| 58 |
-
const hasDefectEdges = (semanticGraph.edges || []).filter(
|
| 59 |
-
e => e.from === st.id && (e.label === 'has_defect' || e.to.startsWith('def'))
|
| 60 |
-
);
|
| 61 |
-
|
| 62 |
const vFrom = st.properties?.validFrom;
|
| 63 |
const vTo = st.properties?.validTo;
|
| 64 |
|
| 65 |
ttl += `\n:${st.id} a iso:State`;
|
| 66 |
|
| 67 |
-
|
| 68 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
});
|
| 70 |
|
| 71 |
-
|
| 72 |
-
ttl += ` ;\n iso:
|
| 73 |
-
}
|
|
|
|
|
|
|
|
|
|
| 74 |
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
});
|
| 78 |
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
});
|
| 82 |
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
|
|
|
|
|
|
| 86 |
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
}
|
|
|
|
| 90 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 91 |
if (vFrom !== undefined && vFrom !== null) {
|
| 92 |
-
ttl += ` ;\n
|
| 93 |
}
|
| 94 |
if (vTo !== undefined && vTo !== null) {
|
| 95 |
-
ttl += ` ;\n
|
| 96 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 97 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 98 |
ttl += ` .\n`;
|
| 99 |
});
|
| 100 |
-
} else {
|
| 101 |
-
const domainClasses = ['Vehicle', 'Slot', 'Part', 'Defect', 'Warehouse'];
|
| 102 |
-
const hasDomainNodes = (semanticGraph.nodes || []).some(n => domainClasses.includes(n.type));
|
| 103 |
|
| 104 |
-
|
| 105 |
-
|
| 106 |
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
});
|
| 120 |
}
|
| 121 |
-
ttl += ` .\n`;
|
| 122 |
});
|
|
|
|
|
|
|
|
|
|
| 123 |
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
});
|
| 129 |
-
} else {
|
| 130 |
-
ttl += `# 3D Continuant Graph persisted via W3C Reification & Quality Inherence\n`;
|
| 131 |
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
ttl += ` ;\n :inheres_in :${inh.to}`;
|
| 143 |
-
}
|
| 144 |
-
ttl += ` .\n`;
|
| 145 |
-
});
|
| 146 |
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
const parentId = isMountedIn ? edge.to : edge.from;
|
| 158 |
-
const childId = isMountedIn ? edge.from : edge.to;
|
| 159 |
-
|
| 160 |
-
ttl += `\n_:${edge.id} a :RelationalQuality ;\n`;
|
| 161 |
-
ttl += ` :hasParent :${parentId} ;\n`;
|
| 162 |
-
ttl += ` :hasChild :${childId}`;
|
| 163 |
-
|
| 164 |
-
if (vFrom !== undefined && vFrom !== null) {
|
| 165 |
-
ttl += ` ;\n :validFrom "${vFrom}"^^xsd:integer`;
|
| 166 |
-
}
|
| 167 |
-
if (vTo !== undefined && vTo !== null) {
|
| 168 |
-
ttl += ` ;\n :validTo "${vTo}"^^xsd:integer`;
|
| 169 |
}
|
| 170 |
-
|
| 171 |
-
ttl += ` .\n`;
|
| 172 |
});
|
| 173 |
}
|
| 174 |
-
|
|
|
|
| 175 |
|
| 176 |
return ttl;
|
| 177 |
}
|
| 178 |
|
| 179 |
/**
|
| 180 |
-
* Generates
|
| 181 |
*
|
| 182 |
-
* @param {Object} semanticGraph - Standardised Semantic Graph
|
| 183 |
-
* @param {number} currentTime - Current discrete
|
|
|
|
| 184 |
* @returns {string} SPARQL query string.
|
| 185 |
*/
|
| 186 |
-
export function generateRdfQuery(semanticGraph, currentTime) {
|
| 187 |
-
const
|
|
|
|
|
|
|
| 188 |
|
| 189 |
if (is4D) {
|
| 190 |
-
return `# SPARQL (W3C RDF)
|
| 191 |
PREFIX iso: <http://example.org/iso15926/>
|
| 192 |
-
|
| 193 |
-
SELECT ?subject ?predicate ?object
|
| 194 |
WHERE {
|
| 195 |
-
?
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 196 |
}`;
|
| 197 |
}
|
| 198 |
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
|
| 204 |
-
SELECT ?subject ?predicate ?object
|
| 205 |
WHERE {
|
| 206 |
-
?
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 207 |
}`;
|
| 208 |
}
|
| 209 |
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
|
| 217 |
-
SELECT ?subject ?predicate ?object
|
| 218 |
WHERE {
|
| 219 |
-
?
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 220 |
}`;
|
| 221 |
}
|
| 222 |
|
| 223 |
-
return `# SPARQL (W3C RDF)
|
| 224 |
-
|
| 225 |
-
PREFIX
|
| 226 |
-
|
|
|
|
| 227 |
WHERE {
|
| 228 |
-
?
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 229 |
}`;
|
| 230 |
-
}
|
|
|
|
| 2 |
* SMS LAYER: RDF GENERATOR
|
| 3 |
* ------------------------
|
| 4 |
* Simulates how the Semantic Graph is persisted in a W3C RDF Triplestore.
|
| 5 |
+
* Generates pure Turtle (.ttl) serialisation and ontology-specific temporal SPARQL queries.
|
|
|
|
| 6 |
*/
|
| 7 |
|
| 8 |
+
import { INTERNAL_PROPERTY_KEYS } from '../smrm/definitions.js';
|
| 9 |
+
|
| 10 |
/**
|
| 11 |
* Generates a W3C RDF Turtle (.ttl) serialisation string from a Semantic Graph.
|
|
|
|
|
|
|
|
|
|
| 12 |
*
|
| 13 |
* @param {Object} semanticGraph - Standardised Semantic Graph with nodes and edges.
|
| 14 |
* @returns {string} Pure Turtle (.ttl) serialisation text.
|
| 15 |
*/
|
| 16 |
export function generateRdfStorage(semanticGraph) {
|
| 17 |
+
if (!semanticGraph || !Array.isArray(semanticGraph.nodes) || semanticGraph.nodes.length === 0) {
|
| 18 |
+
return '# Empty RDF Graph (0 triples)';
|
| 19 |
}
|
| 20 |
|
| 21 |
+
const is4D = semanticGraph.nodes.some(n => n.type === 'State' || n.type === 'ActualIndividual');
|
| 22 |
+
const isBFO = semanticGraph.nodes.some(n => n.type === 'MaterialEntity' || n.type === 'Quality');
|
| 23 |
let ttl = '';
|
| 24 |
|
| 25 |
+
// 1. 4D ISO 15926 Turtle Serialisation
|
| 26 |
if (is4D) {
|
| 27 |
ttl += `@prefix iso: <http://example.org/iso15926/> .\n`;
|
| 28 |
ttl += `@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .\n\n`;
|
| 29 |
ttl += `# 4D Perdurant Graph persisted as Pure Binary RDF Triples\n`;
|
| 30 |
|
| 31 |
+
const individuals = semanticGraph.nodes.filter(n => n.type === 'ActualIndividual');
|
| 32 |
individuals.forEach(ind => {
|
| 33 |
+
const cls = ind.properties?.entityClass || 'Part';
|
| 34 |
+
ttl += `:${ind.id} a iso:ActualIndividual , iso:${cls} .\n`;
|
| 35 |
});
|
| 36 |
|
| 37 |
+
const defectNodes = semanticGraph.nodes.filter(n => n.type === 'Defect');
|
| 38 |
defectNodes.forEach(d => {
|
| 39 |
+
const dType = d.properties?.defectType || 'defect';
|
| 40 |
+
const dVal = d.properties?.value || 'present';
|
| 41 |
+
ttl += `:${d.id} a iso:DefectClass ;\n iso:defectType "${dType}" ;\n iso:severity "${dVal}" .\n`;
|
| 42 |
});
|
| 43 |
|
| 44 |
+
const states = semanticGraph.nodes.filter(n => n.type === 'State');
|
| 45 |
states.forEach(st => {
|
| 46 |
+
const outgoing = (semanticGraph.edges || []).filter(e => e.from === st.id);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
const vFrom = st.properties?.validFrom;
|
| 48 |
const vTo = st.properties?.validTo;
|
| 49 |
|
| 50 |
ttl += `\n:${st.id} a iso:State`;
|
| 51 |
|
| 52 |
+
outgoing.forEach(e => {
|
| 53 |
+
if (e.label === 'temporalPartOf') {
|
| 54 |
+
ttl += ` ;\n iso:temporalPartOf :${e.to}`;
|
| 55 |
+
} else if (e.label === 'mounted_in') {
|
| 56 |
+
ttl += ` ;\n iso:mountedIn :${e.to}`;
|
| 57 |
+
} else if (e.label === 'has_slot') {
|
| 58 |
+
ttl += ` ;\n iso:hasSlot :${e.to}`;
|
| 59 |
+
} else if (e.label === 'stored_at') {
|
| 60 |
+
ttl += ` ;\n iso:storedAt :${e.to}`;
|
| 61 |
+
} else if (e.label === 'has_defect') {
|
| 62 |
+
ttl += ` ;\n iso:hasDefect :${e.to}`;
|
| 63 |
+
}
|
| 64 |
});
|
| 65 |
|
| 66 |
+
if (vFrom !== undefined && vFrom !== null) {
|
| 67 |
+
ttl += ` ;\n iso:validFrom "${vFrom}"^^xsd:integer`;
|
| 68 |
+
}
|
| 69 |
+
if (vTo !== undefined && vTo !== null) {
|
| 70 |
+
ttl += ` ;\n iso:validTo "${vTo}"^^xsd:integer`;
|
| 71 |
+
}
|
| 72 |
|
| 73 |
+
ttl += ` .\n`;
|
| 74 |
+
});
|
|
|
|
| 75 |
|
| 76 |
+
return ttl;
|
| 77 |
+
}
|
|
|
|
| 78 |
|
| 79 |
+
// 2. 3D BFO Turtle Serialisation (with W3C Reification)
|
| 80 |
+
if (isBFO) {
|
| 81 |
+
ttl += `@prefix : <http://example.org/bfo/> .\n`;
|
| 82 |
+
ttl += `@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .\n\n`;
|
| 83 |
+
ttl += `# 3D Continuant Graph persisted via W3C Reification & Quality Inherence\n`;
|
| 84 |
|
| 85 |
+
semanticGraph.nodes.filter(n => n.type === 'MaterialEntity').forEach(n => {
|
| 86 |
+
const cls = n.properties?.entityClass || 'Part';
|
| 87 |
+
ttl += `:${n.id} a :MaterialEntity , :${cls} .\n`;
|
| 88 |
+
});
|
| 89 |
|
| 90 |
+
semanticGraph.nodes.filter(n => n.type === 'Quality').forEach(q => {
|
| 91 |
+
const inh = (semanticGraph.edges || []).find(e => e.from === q.id && e.label === 'inheres_in');
|
| 92 |
+
const vFrom = q.properties?.validFrom;
|
| 93 |
+
const vTo = q.properties?.validTo;
|
| 94 |
+
ttl += `\n:${q.id} a :Quality`;
|
| 95 |
+
if (q.properties?.qualityType) {
|
| 96 |
+
ttl += ` ;\n :qualityType "${q.properties.qualityType}"`;
|
| 97 |
+
}
|
| 98 |
+
if (q.properties?.value) {
|
| 99 |
+
ttl += ` ;\n :value "${q.properties.value}"`;
|
| 100 |
+
}
|
| 101 |
+
if (inh) {
|
| 102 |
+
ttl += ` ;\n :inheres_in :${inh.to}`;
|
| 103 |
+
}
|
| 104 |
if (vFrom !== undefined && vFrom !== null) {
|
| 105 |
+
ttl += ` ;\n :validFrom "${vFrom}"^^xsd:integer`;
|
| 106 |
}
|
| 107 |
if (vTo !== undefined && vTo !== null) {
|
| 108 |
+
ttl += ` ;\n :validTo "${vTo}"^^xsd:integer`;
|
| 109 |
}
|
| 110 |
+
ttl += ` .\n`;
|
| 111 |
+
});
|
| 112 |
+
|
| 113 |
+
(semanticGraph.edges || []).filter(e => e.type !== 'inherence').forEach(edge => {
|
| 114 |
+
const vFrom = edge.properties?.validFrom;
|
| 115 |
+
const vTo = edge.properties?.validTo;
|
| 116 |
+
const rel = edge.label || edge.type;
|
| 117 |
+
const isChildToParent = rel === 'mounted_in' || rel === 'stored_at';
|
| 118 |
+
const parentId = isChildToParent ? edge.to : edge.from;
|
| 119 |
+
const childId = isChildToParent ? edge.from : edge.to;
|
| 120 |
|
| 121 |
+
ttl += `\n_:${edge.id} a :RelationalQuality ;\n`;
|
| 122 |
+
ttl += ` :relationType :${rel} ;\n`;
|
| 123 |
+
ttl += ` :hasParent :${parentId} ;\n`;
|
| 124 |
+
ttl += ` :hasChild :${childId}`;
|
| 125 |
+
|
| 126 |
+
if (vFrom !== undefined && vFrom !== null) {
|
| 127 |
+
ttl += ` ;\n :validFrom "${vFrom}"^^xsd:integer`;
|
| 128 |
+
}
|
| 129 |
+
if (vTo !== undefined && vTo !== null) {
|
| 130 |
+
ttl += ` ;\n :validTo "${vTo}"^^xsd:integer`;
|
| 131 |
+
}
|
| 132 |
ttl += ` .\n`;
|
| 133 |
});
|
|
|
|
|
|
|
|
|
|
| 134 |
|
| 135 |
+
return ttl;
|
| 136 |
+
}
|
| 137 |
|
| 138 |
+
// 3. Naive Graph (Node / Edge) Turtle Serialisation with Reified Edge Properties
|
| 139 |
+
ttl += `@prefix : <http://example.org/naive/> .\n`;
|
| 140 |
+
ttl += `@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .\n`;
|
| 141 |
+
ttl += `@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .\n\n`;
|
| 142 |
+
ttl += `# Naive Domain Graph persisted as RDF Triples & Reified Statements\n`;
|
| 143 |
+
|
| 144 |
+
semanticGraph.nodes.forEach(n => {
|
| 145 |
+
ttl += `:${n.id} a :${n.type || 'Part'}`;
|
| 146 |
+
if (n.properties) {
|
| 147 |
+
Object.entries(n.properties).forEach(([k, v]) => {
|
| 148 |
+
if (!INTERNAL_PROPERTY_KEYS.includes(k) && v !== undefined && v !== null) {
|
| 149 |
+
ttl += ` ;\n :${k} "${v}"`;
|
|
|
|
| 150 |
}
|
|
|
|
| 151 |
});
|
| 152 |
+
}
|
| 153 |
+
ttl += ` .\n`;
|
| 154 |
+
});
|
| 155 |
|
| 156 |
+
(semanticGraph.edges || []).forEach(edge => {
|
| 157 |
+
const edgeRel = edge.label || 'has_part';
|
| 158 |
+
const vFrom = edge.properties?.validFrom;
|
| 159 |
+
const vTo = edge.properties?.validTo;
|
|
|
|
|
|
|
|
|
|
| 160 |
|
| 161 |
+
// Emit direct triple if currently active
|
| 162 |
+
if (edge.properties?.isCurrent !== false) {
|
| 163 |
+
ttl += `\n:${edge.from} :${edgeRel} :${edge.to} .\n`;
|
| 164 |
+
}
|
| 165 |
|
| 166 |
+
// Emit W3C Statement reification to hold validFrom/validTo and edge-attached defects
|
| 167 |
+
ttl += `_:${edge.id} a rdf:Statement ;\n`;
|
| 168 |
+
ttl += ` rdf:subject :${edge.from} ;\n`;
|
| 169 |
+
ttl += ` rdf:predicate :${edgeRel} ;\n`;
|
| 170 |
+
ttl += ` rdf:object :${edge.to}`;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 171 |
|
| 172 |
+
if (vFrom !== undefined && vFrom !== null) {
|
| 173 |
+
ttl += ` ;\n :validFrom "${vFrom}"^^xsd:integer`;
|
| 174 |
+
}
|
| 175 |
+
if (vTo !== undefined && vTo !== null) {
|
| 176 |
+
ttl += ` ;\n :validTo "${vTo}"^^xsd:integer`;
|
| 177 |
+
}
|
| 178 |
+
if (edge.properties) {
|
| 179 |
+
Object.entries(edge.properties).forEach(([k, v]) => {
|
| 180 |
+
if (!INTERNAL_PROPERTY_KEYS.includes(k) && v !== undefined && v !== null) {
|
| 181 |
+
ttl += ` ;\n :${k} "${v}"`;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 182 |
}
|
|
|
|
|
|
|
| 183 |
});
|
| 184 |
}
|
| 185 |
+
ttl += ` .\n`;
|
| 186 |
+
});
|
| 187 |
|
| 188 |
return ttl;
|
| 189 |
}
|
| 190 |
|
| 191 |
/**
|
| 192 |
+
* Generates an ontology-aware temporal SPARQL query for time T.
|
| 193 |
*
|
| 194 |
+
* @param {Object} semanticGraph - Standardised Semantic Graph.
|
| 195 |
+
* @param {number} currentTime - Current discrete timestamp T.
|
| 196 |
+
* @param {string} [ontology='NAIVE_NODE'] - Active ontology identifier.
|
| 197 |
* @returns {string} SPARQL query string.
|
| 198 |
*/
|
| 199 |
+
export function generateRdfQuery(semanticGraph, currentTime = 0, ontology = 'NAIVE_NODE') {
|
| 200 |
+
const t = currentTime ?? 0;
|
| 201 |
+
const is4D = ontology === 'ISO15926' || semanticGraph?.nodes?.some(n => n.type === 'State');
|
| 202 |
+
const isBFO = ontology === 'BFO' || semanticGraph?.nodes?.some(n => n.type === 'MaterialEntity');
|
| 203 |
|
| 204 |
if (is4D) {
|
| 205 |
+
return `# SPARQL (W3C RDF) — ISO 15926 (4D) State & Defect Audit at T = ${t}
|
| 206 |
PREFIX iso: <http://example.org/iso15926/>
|
| 207 |
+
SELECT ?part ?location ?defectType ?severity
|
|
|
|
| 208 |
WHERE {
|
| 209 |
+
?st iso:temporalPartOf ?part ;
|
| 210 |
+
iso:mountedIn|iso:storedAt ?locState ;
|
| 211 |
+
iso:validFrom ?vf .
|
| 212 |
+
?locState iso:temporalPartOf ?location .
|
| 213 |
+
OPTIONAL { ?st iso:validTo ?vt }
|
| 214 |
+
FILTER (?vf <= ${t} && (!BOUND(?vt) || ?vt > ${t}))
|
| 215 |
+
|
| 216 |
+
OPTIONAL {
|
| 217 |
+
?defState iso:temporalPartOf ?part ;
|
| 218 |
+
iso:hasDefect ?def .
|
| 219 |
+
?def iso:defectType ?defectType ;
|
| 220 |
+
iso:severity ?severity .
|
| 221 |
+
}
|
| 222 |
}`;
|
| 223 |
}
|
| 224 |
|
| 225 |
+
if (isBFO) {
|
| 226 |
+
return `# SPARQL (W3C RDF) — BFO (3D) Reified Placement & Quality at T = ${t}
|
| 227 |
+
PREFIX : <http://example.org/bfo/>
|
| 228 |
+
SELECT ?part ?location ?qualityType ?value
|
|
|
|
|
|
|
| 229 |
WHERE {
|
| 230 |
+
?stmt a :RelationalQuality ;
|
| 231 |
+
:hasChild ?part ;
|
| 232 |
+
:hasParent ?location ;
|
| 233 |
+
:validFrom ?vf .
|
| 234 |
+
OPTIONAL { ?stmt :validTo ?vt }
|
| 235 |
+
FILTER (?vf <= ${t} && (!BOUND(?vt) || ?vt > ${t}))
|
| 236 |
+
|
| 237 |
+
OPTIONAL {
|
| 238 |
+
?qual a :Quality ;
|
| 239 |
+
:inheres_in ?part ;
|
| 240 |
+
:qualityType ?qualityType ;
|
| 241 |
+
:value ?value ;
|
| 242 |
+
:validFrom ?qvf .
|
| 243 |
+
OPTIONAL { ?qual :validTo ?qvt }
|
| 244 |
+
FILTER (?qvf <= ${t} && (!BOUND(?qvt) || ?qvt > ${t}))
|
| 245 |
+
}
|
| 246 |
}`;
|
| 247 |
}
|
| 248 |
|
| 249 |
+
if (ontology === 'NAIVE_EDGE') {
|
| 250 |
+
return `# SPARQL (W3C RDF) — Naive Graph (Edge) Reified Statement Query at T = ${t}
|
| 251 |
+
# WARNING: Reads :crack from active placement statement ?stmt!
|
| 252 |
+
PREFIX : <http://example.org/naive/>
|
| 253 |
+
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
|
| 254 |
+
SELECT ?part ?location ?crack
|
|
|
|
|
|
|
| 255 |
WHERE {
|
| 256 |
+
?stmt a rdf:Statement ;
|
| 257 |
+
rdf:subject ?part ;
|
| 258 |
+
rdf:predicate ?pred ;
|
| 259 |
+
rdf:object ?location ;
|
| 260 |
+
:validFrom ?vf .
|
| 261 |
+
OPTIONAL { ?stmt :validTo ?vt }
|
| 262 |
+
OPTIONAL { ?stmt :crack ?crack }
|
| 263 |
+
FILTER (?pred IN (:mounted_in, :stored_at))
|
| 264 |
+
FILTER (?vf <= ${t} && (!BOUND(?vt) || ?vt > ${t}))
|
| 265 |
}`;
|
| 266 |
}
|
| 267 |
|
| 268 |
+
return `# SPARQL (W3C RDF) — Naive Graph (Node) Query at T = ${t}
|
| 269 |
+
# WARNING: :crack is attached directly to ?part without temporal bounds!
|
| 270 |
+
PREFIX : <http://example.org/naive/>
|
| 271 |
+
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
|
| 272 |
+
SELECT ?part ?location ?crack
|
| 273 |
WHERE {
|
| 274 |
+
?stmt a rdf:Statement ;
|
| 275 |
+
rdf:subject ?part ;
|
| 276 |
+
rdf:object ?location ;
|
| 277 |
+
:validFrom ?vf .
|
| 278 |
+
OPTIONAL { ?stmt :validTo ?vt }
|
| 279 |
+
OPTIONAL { ?part :crack ?crack }
|
| 280 |
+
FILTER (?vf <= ${t} && (!BOUND(?vt) || ?vt > ${t}))
|
| 281 |
}`;
|
| 282 |
+
}
|
js/sms/sms-graph-generator.js
CHANGED
|
@@ -2,15 +2,18 @@
|
|
| 2 |
* SMS LAYER: PHYSICAL GRAPH GENERATOR
|
| 3 |
* -----------------------------------
|
| 4 |
* Transforms a canonical Semantic Graph (SMIM) into physical storage (SMS) per IEC 63339.
|
| 5 |
-
* RDF: Reifies
|
| 6 |
-
*
|
|
|
|
| 7 |
*/
|
| 8 |
|
|
|
|
|
|
|
| 9 |
/**
|
| 10 |
* Generates a physical graph projection from a canonical semantic graph.
|
| 11 |
*
|
| 12 |
* @param {Object} semanticGraph - Standardised Semantic Graph ({ nodes: Array, edges: Array }).
|
| 13 |
-
* @param {
|
| 14 |
* @returns {{ nodes: Array, edges: Array }} Transformed physical graph.
|
| 15 |
*/
|
| 16 |
export function generatePhysicalGraph(semanticGraph, storageMode) {
|
|
@@ -18,7 +21,6 @@ export function generatePhysicalGraph(semanticGraph, storageMode) {
|
|
| 18 |
return { nodes: [], edges: [] };
|
| 19 |
}
|
| 20 |
|
| 21 |
-
// Deep clone nodes and edges to maintain immutability
|
| 22 |
const physicalNodes = semanticGraph.nodes.map(node => ({
|
| 23 |
...node,
|
| 24 |
properties: node.properties ? { ...node.properties } : {}
|
|
@@ -29,97 +31,233 @@ export function generatePhysicalGraph(semanticGraph, storageMode) {
|
|
| 29 |
properties: edge.properties ? { ...edge.properties } : {}
|
| 30 |
}));
|
| 31 |
|
| 32 |
-
const is4D = physicalNodes.some(n => n.type === 'State');
|
| 33 |
|
| 34 |
-
//
|
|
|
|
|
|
|
| 35 |
if (storageMode === 'RDF') {
|
| 36 |
-
if (
|
| 37 |
-
|
|
|
|
|
|
|
|
|
|
| 38 |
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
edge.properties.validFrom !== undefined ||
|
| 42 |
-
edge.properties.validTo !== undefined
|
| 43 |
-
);
|
| 44 |
|
| 45 |
-
if (
|
| 46 |
-
const
|
| 47 |
-
const
|
|
|
|
| 48 |
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
finalEdges.push({
|
| 57 |
-
id: `
|
| 58 |
from: stmtId,
|
| 59 |
-
to:
|
| 60 |
-
label: ':
|
| 61 |
type: 'reification',
|
| 62 |
properties: { isCurrent }
|
| 63 |
});
|
|
|
|
| 64 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
finalEdges.push({
|
| 66 |
-
id: `
|
| 67 |
from: stmtId,
|
| 68 |
-
to:
|
| 69 |
-
label: ':
|
| 70 |
type: 'reification',
|
| 71 |
-
properties: { isCurrent }
|
| 72 |
});
|
|
|
|
| 73 |
|
| 74 |
-
|
| 75 |
-
|
|
|
|
|
|
|
| 76 |
physicalNodes.push({
|
| 77 |
-
id:
|
| 78 |
-
label: `"${
|
| 79 |
type: 'Literal',
|
| 80 |
-
properties: { value:
|
| 81 |
});
|
| 82 |
-
|
| 83 |
finalEdges.push({
|
| 84 |
-
id: `
|
| 85 |
from: stmtId,
|
| 86 |
-
to:
|
| 87 |
-
label:
|
| 88 |
type: 'reification',
|
| 89 |
properties: { isCurrent }
|
| 90 |
});
|
| 91 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 92 |
|
| 93 |
-
|
| 94 |
-
const litToId = `lit_vt_${edge.id}`;
|
| 95 |
-
physicalNodes.push({
|
| 96 |
-
id: litToId,
|
| 97 |
-
label: `"${edge.properties.validTo}"^^xsd:integer`,
|
| 98 |
-
type: 'Literal',
|
| 99 |
-
properties: { value: edge.properties.validTo }
|
| 100 |
-
});
|
| 101 |
-
|
| 102 |
-
finalEdges.push({
|
| 103 |
-
id: `rel_vt_${edge.id}`,
|
| 104 |
-
from: stmtId,
|
| 105 |
-
to: litToId,
|
| 106 |
-
label: ':validTo',
|
| 107 |
-
type: 'reification',
|
| 108 |
-
properties: { isCurrent: false }
|
| 109 |
-
});
|
| 110 |
-
}
|
| 111 |
-
} else {
|
| 112 |
-
finalEdges.push(edge);
|
| 113 |
-
}
|
| 114 |
-
});
|
| 115 |
-
|
| 116 |
-
return { nodes: physicalNodes, edges: finalEdges };
|
| 117 |
-
}
|
| 118 |
-
|
| 119 |
-
return { nodes: physicalNodes, edges: physicalEdges };
|
| 120 |
}
|
| 121 |
|
| 122 |
-
//
|
|
|
|
|
|
|
| 123 |
if (storageMode === 'LPG' && is4D) {
|
| 124 |
const stateNodes = physicalNodes.filter(n => n.type === 'State');
|
| 125 |
const entityStates = new Map();
|
|
@@ -158,6 +296,7 @@ export function generatePhysicalGraph(semanticGraph, storageMode) {
|
|
| 158 |
type: 'transition',
|
| 159 |
properties: {
|
| 160 |
atTime: nextState.properties?.validFrom,
|
|
|
|
| 161 |
isCurrent: nextState.properties?.isCurrent ?? true
|
| 162 |
}
|
| 163 |
});
|
|
@@ -169,4 +308,4 @@ export function generatePhysicalGraph(semanticGraph, storageMode) {
|
|
| 169 |
}
|
| 170 |
|
| 171 |
return { nodes: physicalNodes, edges: physicalEdges };
|
| 172 |
-
}
|
|
|
|
| 2 |
* SMS LAYER: PHYSICAL GRAPH GENERATOR
|
| 3 |
* -----------------------------------
|
| 4 |
* Transforms a canonical Semantic Graph (SMIM) into physical storage (SMS) per IEC 63339.
|
| 5 |
+
* - RDF Mode: Reifies binary relations carrying temporal/custom properties into W3C
|
| 6 |
+
* statement nodes and extracts scalar properties into explicit RDF Literal nodes.
|
| 7 |
+
* - LPG Mode: Links sequential 4D states of the same entity via TRANSITION_TO edges.
|
| 8 |
*/
|
| 9 |
|
| 10 |
+
import { INTERNAL_PROPERTY_KEYS } from '../smrm/definitions.js';
|
| 11 |
+
|
| 12 |
/**
|
| 13 |
* Generates a physical graph projection from a canonical semantic graph.
|
| 14 |
*
|
| 15 |
* @param {Object} semanticGraph - Standardised Semantic Graph ({ nodes: Array, edges: Array }).
|
| 16 |
+
* @param {'LPG'|'RDF'} storageMode - Physical storage paradigm ('LPG' or 'RDF').
|
| 17 |
* @returns {{ nodes: Array, edges: Array }} Transformed physical graph.
|
| 18 |
*/
|
| 19 |
export function generatePhysicalGraph(semanticGraph, storageMode) {
|
|
|
|
| 21 |
return { nodes: [], edges: [] };
|
| 22 |
}
|
| 23 |
|
|
|
|
| 24 |
const physicalNodes = semanticGraph.nodes.map(node => ({
|
| 25 |
...node,
|
| 26 |
properties: node.properties ? { ...node.properties } : {}
|
|
|
|
| 31 |
properties: edge.properties ? { ...edge.properties } : {}
|
| 32 |
}));
|
| 33 |
|
| 34 |
+
const is4D = physicalNodes.some(n => n.type === 'State' || n.type === 'ActualIndividual');
|
| 35 |
|
| 36 |
+
// =========================================================================
|
| 37 |
+
// CASE 1: W3C RDF Mode (Triples, Statement Reification & Explicit Literals)
|
| 38 |
+
// =========================================================================
|
| 39 |
if (storageMode === 'RDF') {
|
| 40 |
+
if (is4D) {
|
| 41 |
+
// In 4D RDF, States and Defects cannot hold internal property maps;
|
| 42 |
+
// their timestamps and values are persisted as explicit RDF Literal nodes.
|
| 43 |
+
const rdf4dNodes = [];
|
| 44 |
+
const rdf4dEdges = [...physicalEdges];
|
| 45 |
|
| 46 |
+
physicalNodes.forEach(node => {
|
| 47 |
+
rdf4dNodes.push(node);
|
|
|
|
|
|
|
|
|
|
| 48 |
|
| 49 |
+
if (node.type === 'State') {
|
| 50 |
+
const vFrom = node.properties?.validFrom;
|
| 51 |
+
const vTo = node.properties?.validTo;
|
| 52 |
+
const isCurrent = node.properties?.isCurrent !== false;
|
| 53 |
|
| 54 |
+
if (vFrom !== undefined && vFrom !== null) {
|
| 55 |
+
const litFromId = `lit_vf_${node.id}`;
|
| 56 |
+
rdf4dNodes.push({
|
| 57 |
+
id: litFromId,
|
| 58 |
+
label: `"${vFrom}"^^xsd:integer`,
|
| 59 |
+
type: 'Literal',
|
| 60 |
+
properties: { value: vFrom }
|
| 61 |
+
});
|
| 62 |
+
rdf4dEdges.push({
|
| 63 |
+
id: `edge_vf_${node.id}`,
|
| 64 |
+
from: node.id,
|
| 65 |
+
to: litFromId,
|
| 66 |
+
label: 'iso:validFrom',
|
| 67 |
+
type: 'predicate',
|
| 68 |
+
properties: { isCurrent }
|
| 69 |
+
});
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
if (vTo !== undefined && vTo !== null) {
|
| 73 |
+
const litToId = `lit_vt_${node.id}`;
|
| 74 |
+
rdf4dNodes.push({
|
| 75 |
+
id: litToId,
|
| 76 |
+
label: `"${vTo}"^^xsd:integer`,
|
| 77 |
+
type: 'Literal',
|
| 78 |
+
properties: { value: vTo }
|
| 79 |
+
});
|
| 80 |
+
rdf4dEdges.push({
|
| 81 |
+
id: `edge_vt_${node.id}`,
|
| 82 |
+
from: node.id,
|
| 83 |
+
to: litToId,
|
| 84 |
+
label: 'iso:validTo',
|
| 85 |
+
type: 'predicate',
|
| 86 |
+
properties: { isCurrent: false }
|
| 87 |
+
});
|
| 88 |
+
}
|
| 89 |
+
} else if (node.type === 'Defect' && node.properties?.value) {
|
| 90 |
+
const litValId = `lit_val_${node.id}`;
|
| 91 |
+
rdf4dNodes.push({
|
| 92 |
+
id: litValId,
|
| 93 |
+
label: `"${node.properties.value}"^^xsd:string`,
|
| 94 |
+
type: 'Literal',
|
| 95 |
+
properties: { value: node.properties.value }
|
| 96 |
});
|
| 97 |
+
rdf4dEdges.push({
|
| 98 |
+
id: `edge_val_${node.id}`,
|
| 99 |
+
from: node.id,
|
| 100 |
+
to: litValId,
|
| 101 |
+
label: 'iso:severity',
|
| 102 |
+
type: 'predicate',
|
| 103 |
+
properties: { isCurrent: node.properties?.isCurrent !== false }
|
| 104 |
+
});
|
| 105 |
+
}
|
| 106 |
+
});
|
| 107 |
+
|
| 108 |
+
return { nodes: rdf4dNodes, edges: rdf4dEdges };
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
// 3D BFO and Naive Graph RDF Reification
|
| 112 |
+
const finalEdges = [];
|
| 113 |
+
|
| 114 |
+
// Extract custom properties on nodes (e.g. Naive Node crack="2mm") into RDF Literals
|
| 115 |
+
physicalNodes.slice().forEach(node => {
|
| 116 |
+
if (node.properties) {
|
| 117 |
+
Object.entries(node.properties).forEach(([k, v]) => {
|
| 118 |
+
if (!INTERNAL_PROPERTY_KEYS.includes(k) && v !== undefined && v !== null) {
|
| 119 |
+
const litId = `lit_node_${node.id}_${k}`;
|
| 120 |
+
physicalNodes.push({
|
| 121 |
+
id: litId,
|
| 122 |
+
label: `"${v}"^^xsd:string`,
|
| 123 |
+
type: 'Literal',
|
| 124 |
+
properties: { value: v }
|
| 125 |
+
});
|
| 126 |
+
finalEdges.push({
|
| 127 |
+
id: `rel_lit_${node.id}_${k}`,
|
| 128 |
+
from: node.id,
|
| 129 |
+
to: litId,
|
| 130 |
+
label: `:${k}`,
|
| 131 |
+
type: 'predicate',
|
| 132 |
+
properties: { isCurrent: true }
|
| 133 |
+
});
|
| 134 |
+
}
|
| 135 |
+
});
|
| 136 |
+
}
|
| 137 |
+
});
|
| 138 |
+
|
| 139 |
+
physicalEdges.forEach(edge => {
|
| 140 |
+
const hasTemporalProps = edge.properties && (
|
| 141 |
+
edge.properties.validFrom !== undefined ||
|
| 142 |
+
edge.properties.validTo !== undefined
|
| 143 |
+
);
|
| 144 |
+
const isReifiableRelation = [
|
| 145 |
+
'composition',
|
| 146 |
+
'mounted_in',
|
| 147 |
+
'has_slot',
|
| 148 |
+
'stored_at',
|
| 149 |
+
'has_part'
|
| 150 |
+
].includes(edge.type) || [
|
| 151 |
+
'mounted_in',
|
| 152 |
+
'has_slot',
|
| 153 |
+
'stored_at',
|
| 154 |
+
'has_part'
|
| 155 |
+
].includes(edge.label);
|
| 156 |
+
|
| 157 |
+
if (isReifiableRelation && hasTemporalProps) {
|
| 158 |
+
const stmtId = `stmt_${edge.id}`;
|
| 159 |
+
const isCurrent = edge.properties.isCurrent !== false;
|
| 160 |
+
const rel = edge.label || edge.type;
|
| 161 |
|
| 162 |
+
// Correct semantic directionality:
|
| 163 |
+
// For mounted_in / stored_at, edge.from is the Child (Part) and edge.to is the Parent (Slot/Warehouse).
|
| 164 |
+
// For has_slot / has_part, edge.from is the Parent and edge.to is the Child.
|
| 165 |
+
const isChildToParent = rel === 'mounted_in' || rel === 'stored_at';
|
| 166 |
+
const parentNodeId = isChildToParent ? edge.to : edge.from;
|
| 167 |
+
const childNodeId = isChildToParent ? edge.from : edge.to;
|
| 168 |
+
|
| 169 |
+
physicalNodes.push({
|
| 170 |
+
id: stmtId,
|
| 171 |
+
label: `_:${edge.id}`,
|
| 172 |
+
type: 'RelationalQuality',
|
| 173 |
+
properties: { isCurrent, originalEdgeId: edge.id }
|
| 174 |
+
});
|
| 175 |
+
|
| 176 |
+
finalEdges.push({
|
| 177 |
+
id: `rel_p_${edge.id}`,
|
| 178 |
+
from: stmtId,
|
| 179 |
+
to: parentNodeId,
|
| 180 |
+
label: ':hasParent',
|
| 181 |
+
type: 'reification',
|
| 182 |
+
properties: { isCurrent }
|
| 183 |
+
});
|
| 184 |
+
|
| 185 |
+
finalEdges.push({
|
| 186 |
+
id: `rel_c_${edge.id}`,
|
| 187 |
+
from: stmtId,
|
| 188 |
+
to: childNodeId,
|
| 189 |
+
label: ':hasChild',
|
| 190 |
+
type: 'reification',
|
| 191 |
+
properties: { isCurrent }
|
| 192 |
+
});
|
| 193 |
+
|
| 194 |
+
if (edge.properties.validFrom !== undefined && edge.properties.validFrom !== null) {
|
| 195 |
+
const litFromId = `lit_vf_${edge.id}`;
|
| 196 |
+
physicalNodes.push({
|
| 197 |
+
id: litFromId,
|
| 198 |
+
label: `"${edge.properties.validFrom}"^^xsd:integer`,
|
| 199 |
+
type: 'Literal',
|
| 200 |
+
properties: { value: edge.properties.validFrom }
|
| 201 |
+
});
|
| 202 |
finalEdges.push({
|
| 203 |
+
id: `rel_vf_${edge.id}`,
|
| 204 |
from: stmtId,
|
| 205 |
+
to: litFromId,
|
| 206 |
+
label: ':validFrom',
|
| 207 |
type: 'reification',
|
| 208 |
properties: { isCurrent }
|
| 209 |
});
|
| 210 |
+
}
|
| 211 |
|
| 212 |
+
if (edge.properties.validTo !== undefined && edge.properties.validTo !== null) {
|
| 213 |
+
const litToId = `lit_vt_${edge.id}`;
|
| 214 |
+
physicalNodes.push({
|
| 215 |
+
id: litToId,
|
| 216 |
+
label: `"${edge.properties.validTo}"^^xsd:integer`,
|
| 217 |
+
type: 'Literal',
|
| 218 |
+
properties: { value: edge.properties.validTo }
|
| 219 |
+
});
|
| 220 |
finalEdges.push({
|
| 221 |
+
id: `rel_vt_${edge.id}`,
|
| 222 |
from: stmtId,
|
| 223 |
+
to: litToId,
|
| 224 |
+
label: ':validTo',
|
| 225 |
type: 'reification',
|
| 226 |
+
properties: { isCurrent: false }
|
| 227 |
});
|
| 228 |
+
}
|
| 229 |
|
| 230 |
+
// Also reify custom edge properties (e.g. crack="2mm" in NAIVE_EDGE mode)
|
| 231 |
+
Object.entries(edge.properties).forEach(([k, v]) => {
|
| 232 |
+
if (!INTERNAL_PROPERTY_KEYS.includes(k) && v !== undefined && v !== null) {
|
| 233 |
+
const litPropId = `lit_prop_${edge.id}_${k}`;
|
| 234 |
physicalNodes.push({
|
| 235 |
+
id: litPropId,
|
| 236 |
+
label: `"${v}"^^xsd:string`,
|
| 237 |
type: 'Literal',
|
| 238 |
+
properties: { value: v }
|
| 239 |
});
|
|
|
|
| 240 |
finalEdges.push({
|
| 241 |
+
id: `rel_prop_${edge.id}_${k}`,
|
| 242 |
from: stmtId,
|
| 243 |
+
to: litPropId,
|
| 244 |
+
label: `:${k}`,
|
| 245 |
type: 'reification',
|
| 246 |
properties: { isCurrent }
|
| 247 |
});
|
| 248 |
}
|
| 249 |
+
});
|
| 250 |
+
} else {
|
| 251 |
+
finalEdges.push(edge);
|
| 252 |
+
}
|
| 253 |
+
});
|
| 254 |
|
| 255 |
+
return { nodes: physicalNodes, edges: finalEdges };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 256 |
}
|
| 257 |
|
| 258 |
+
// =========================================================================
|
| 259 |
+
// CASE 2: Labeled Property Graph (LPG) Mode
|
| 260 |
+
// =========================================================================
|
| 261 |
if (storageMode === 'LPG' && is4D) {
|
| 262 |
const stateNodes = physicalNodes.filter(n => n.type === 'State');
|
| 263 |
const entityStates = new Map();
|
|
|
|
| 296 |
type: 'transition',
|
| 297 |
properties: {
|
| 298 |
atTime: nextState.properties?.validFrom,
|
| 299 |
+
validFrom: nextState.properties?.validFrom,
|
| 300 |
isCurrent: nextState.properties?.isCurrent ?? true
|
| 301 |
}
|
| 302 |
});
|
|
|
|
| 308 |
}
|
| 309 |
|
| 310 |
return { nodes: physicalNodes, edges: physicalEdges };
|
| 311 |
+
}
|
js/state.js
CHANGED
|
@@ -1,18 +1,16 @@
|
|
| 1 |
import { reactive } from 'vue';
|
| 2 |
-
export { ENTITY_CLASSES, RELATION_TYPES } from './smrm/definitions.js';
|
| 3 |
|
| 4 |
/**
|
| 5 |
-
* SINGLE SOURCE OF TRUTH (Event Sourcing)
|
| 6 |
-
* ---------------------------------------
|
| 7 |
-
*
|
| 8 |
-
*
|
| 9 |
-
*
|
| 10 |
-
*
|
| 11 |
-
*
|
| 12 |
-
* -
|
| 13 |
-
* -
|
| 14 |
-
* - Edge/Event sequence counter
|
| 15 |
-
* - Domain settings (e.g. Naive defect target variant)
|
| 16 |
*/
|
| 17 |
export const state = reactive({
|
| 18 |
// 1. Navigation View ('MENU' | 'SANDBOX' | 'SCENARIO')
|
|
@@ -27,65 +25,104 @@ export const state = reactive({
|
|
| 27 |
// 4. Current Domain Ontology ('NAIVE_NODE' | 'NAIVE_EDGE' | 'BFO' | 'ISO15926')
|
| 28 |
ontology: 'NAIVE_NODE',
|
| 29 |
|
| 30 |
-
// 5.
|
| 31 |
-
|
| 32 |
-
eventLog: [],
|
| 33 |
|
| 34 |
-
// 6.
|
| 35 |
-
|
|
|
|
| 36 |
|
| 37 |
-
// 7.
|
| 38 |
-
|
|
|
|
|
|
|
|
|
|
| 39 |
|
| 40 |
-
//
|
| 41 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
});
|
| 43 |
|
| 44 |
/**
|
| 45 |
* Advances the global system time by one unit (T = T + 1).
|
| 46 |
-
* Because 'state' is reactive, calling this will automatically trigger UI updates.
|
| 47 |
*/
|
| 48 |
export function advanceTime() {
|
| 49 |
state.currentTime++;
|
| 50 |
}
|
| 51 |
|
| 52 |
/**
|
| 53 |
-
*
|
| 54 |
-
*
|
| 55 |
-
*
|
| 56 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
*/
|
| 58 |
export function loadState(newState) {
|
|
|
|
|
|
|
| 59 |
if (newState.currentView !== undefined) {
|
| 60 |
state.currentView = newState.currentView;
|
| 61 |
}
|
| 62 |
-
|
| 63 |
if (newState.userRole !== undefined) {
|
| 64 |
state.userRole = newState.userRole;
|
| 65 |
}
|
| 66 |
-
|
| 67 |
if (newState.ontology !== undefined) {
|
| 68 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
}
|
| 70 |
-
|
| 71 |
if (newState.currentTime !== undefined) {
|
| 72 |
state.currentTime = newState.currentTime;
|
| 73 |
}
|
| 74 |
-
|
| 75 |
-
if (newState.eventLog) {
|
| 76 |
state.eventLog.splice(0, state.eventLog.length, ...newState.eventLog);
|
| 77 |
}
|
| 78 |
-
|
| 79 |
if (newState.edgeSequence !== undefined) {
|
| 80 |
state.edgeSequence = newState.edgeSequence;
|
| 81 |
}
|
| 82 |
-
|
| 83 |
-
if (newState.perspective !== undefined) {
|
| 84 |
-
state.perspective = newState.perspective;
|
| 85 |
-
}
|
| 86 |
-
|
| 87 |
if (newState.graphEngine !== undefined) {
|
| 88 |
-
state.graphEngine = newState.graphEngine;
|
| 89 |
}
|
| 90 |
-
|
| 91 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import { reactive } from 'vue';
|
| 2 |
+
export { ENTITY_CLASSES, RELATION_TYPES, inferEntityClass } from './smrm/definitions.js';
|
| 3 |
|
| 4 |
/**
|
| 5 |
+
* SINGLE SOURCE OF TRUTH (Event Sourcing & Workbench State)
|
| 6 |
+
* ---------------------------------------------------------
|
| 7 |
+
* Central reactive store eliminating duplicate state/settings watchers.
|
| 8 |
+
*
|
| 9 |
+
* Perspective values (4 unified viewpoints):
|
| 10 |
+
* - 'SMRM' : Engineer Perspective (domain hierarchy, slots/states collapsed)
|
| 11 |
+
* - 'SMIM' : Logical Perspective (canonical 3D BFO / 4D ISO 15926 / Naive topology)
|
| 12 |
+
* - 'SMS_LPG' : Physical LPG Perspective (Property Graph storage & state transitions)
|
| 13 |
+
* - 'SMS_RDF' : Physical RDF Perspective (W3C RDF triples, reification & literals)
|
|
|
|
|
|
|
| 14 |
*/
|
| 15 |
export const state = reactive({
|
| 16 |
// 1. Navigation View ('MENU' | 'SANDBOX' | 'SCENARIO')
|
|
|
|
| 25 |
// 4. Current Domain Ontology ('NAIVE_NODE' | 'NAIVE_EDGE' | 'BFO' | 'ISO15926')
|
| 26 |
ontology: 'NAIVE_NODE',
|
| 27 |
|
| 28 |
+
// 5. Unified Perspective ('SMRM' | 'SMIM' | 'SMS_LPG' | 'SMS_RDF')
|
| 29 |
+
perspective: 'SMRM',
|
|
|
|
| 30 |
|
| 31 |
+
// 6. Active Physical Storage Paradigm ('LPG' | 'RDF')
|
| 32 |
+
// Automatically switched when selecting 'SMS_LPG' or 'SMS_RDF'
|
| 33 |
+
storage: 'LPG',
|
| 34 |
|
| 35 |
+
// 7. Visual Graph Engine ('VIS' | 'MERMAID')
|
| 36 |
+
graphEngine: 'VIS',
|
| 37 |
+
|
| 38 |
+
// 8. UI Locale ('sv' | 'en' | 'ru')
|
| 39 |
+
language: 'en',
|
| 40 |
|
| 41 |
+
// 9. Toggle visibility of historical (closed) relationships
|
| 42 |
+
showHistory: true,
|
| 43 |
+
|
| 44 |
+
// 10. The Event Log (Pure business events)
|
| 45 |
+
eventLog: [],
|
| 46 |
+
|
| 47 |
+
// 11. Monotonic counter for unique event IDs
|
| 48 |
+
edgeSequence: 1
|
| 49 |
});
|
| 50 |
|
| 51 |
/**
|
| 52 |
* Advances the global system time by one unit (T = T + 1).
|
|
|
|
| 53 |
*/
|
| 54 |
export function advanceTime() {
|
| 55 |
state.currentTime++;
|
| 56 |
}
|
| 57 |
|
| 58 |
/**
|
| 59 |
+
* Returns the next monotonic sequence integer and increments the counter.
|
| 60 |
+
* @returns {number}
|
| 61 |
+
*/
|
| 62 |
+
export function nextSequence() {
|
| 63 |
+
return state.edgeSequence++;
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
/**
|
| 67 |
+
* Sets the active perspective and synchronises the physical storage mode ('LPG' | 'RDF')
|
| 68 |
+
* so that UNDER THE BONNET and DATABASE QUERY always match the selected Physical view.
|
| 69 |
+
*
|
| 70 |
+
* @param {'SMRM'|'SMIM'|'SMS_LPG'|'SMS_RDF'|'SMS'} newPerspective
|
| 71 |
+
*/
|
| 72 |
+
export function setPerspective(newPerspective) {
|
| 73 |
+
if (newPerspective === 'SMS_LPG') {
|
| 74 |
+
state.perspective = 'SMS_LPG';
|
| 75 |
+
state.storage = 'LPG';
|
| 76 |
+
} else if (newPerspective === 'SMS_RDF') {
|
| 77 |
+
state.perspective = 'SMS_RDF';
|
| 78 |
+
state.storage = 'RDF';
|
| 79 |
+
} else if (newPerspective === 'SMS') {
|
| 80 |
+
state.perspective = state.storage === 'RDF' ? 'SMS_RDF' : 'SMS_LPG';
|
| 81 |
+
} else if (newPerspective === 'SMIM' || newPerspective === 'SMRM') {
|
| 82 |
+
state.perspective = newPerspective;
|
| 83 |
+
}
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
/**
|
| 87 |
+
* Completely replaces or updates the current state from a saved JSON object or scenario.
|
| 88 |
+
*
|
| 89 |
+
* @param {Object} newState - State payload to inject.
|
| 90 |
*/
|
| 91 |
export function loadState(newState) {
|
| 92 |
+
if (!newState || typeof newState !== 'object') return;
|
| 93 |
+
|
| 94 |
if (newState.currentView !== undefined) {
|
| 95 |
state.currentView = newState.currentView;
|
| 96 |
}
|
|
|
|
| 97 |
if (newState.userRole !== undefined) {
|
| 98 |
state.userRole = newState.userRole;
|
| 99 |
}
|
|
|
|
| 100 |
if (newState.ontology !== undefined) {
|
| 101 |
+
const onto = newState.ontology;
|
| 102 |
+
state.ontology = onto === 'ISO_15926_4D' ? 'ISO15926' : (onto === 'NAIVE' ? 'NAIVE_NODE' : onto);
|
| 103 |
+
}
|
| 104 |
+
if (newState.storage === 'LPG' || newState.storage === 'RDF') {
|
| 105 |
+
state.storage = newState.storage;
|
| 106 |
+
}
|
| 107 |
+
if (newState.perspective !== undefined) {
|
| 108 |
+
setPerspective(newState.perspective);
|
| 109 |
}
|
|
|
|
| 110 |
if (newState.currentTime !== undefined) {
|
| 111 |
state.currentTime = newState.currentTime;
|
| 112 |
}
|
| 113 |
+
if (Array.isArray(newState.eventLog)) {
|
|
|
|
| 114 |
state.eventLog.splice(0, state.eventLog.length, ...newState.eventLog);
|
| 115 |
}
|
|
|
|
| 116 |
if (newState.edgeSequence !== undefined) {
|
| 117 |
state.edgeSequence = newState.edgeSequence;
|
| 118 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 119 |
if (newState.graphEngine !== undefined) {
|
| 120 |
+
state.graphEngine = String(newState.graphEngine).toUpperCase() === 'MERMAID' ? 'MERMAID' : 'VIS';
|
| 121 |
}
|
| 122 |
+
if (newState.language !== undefined) {
|
| 123 |
+
state.language = newState.language;
|
| 124 |
+
}
|
| 125 |
+
if (newState.showHistory !== undefined) {
|
| 126 |
+
state.showHistory = Boolean(newState.showHistory);
|
| 127 |
+
}
|
| 128 |
+
}
|
js/urmsm/bom-view.js
CHANGED
|
@@ -1,88 +1,137 @@
|
|
| 1 |
/**
|
| 2 |
* URMSM LAYER: BOM VIEW (Stakeholder Viewpoint)
|
| 3 |
* ---------------------------------------------
|
| 4 |
-
*
|
| 5 |
-
*
|
| 6 |
-
*
|
| 7 |
-
*
|
| 8 |
-
*
|
| 9 |
-
*
|
|
|
|
| 10 |
*/
|
| 11 |
|
| 12 |
-
/
|
| 13 |
-
|
| 14 |
-
*
|
| 15 |
-
* @param {string} rootId - The ID of the top-level system.
|
| 16 |
-
* @param {Array<Object>} activeEdges - Array of currently active edges (filtered for Time = T).
|
| 17 |
-
* @returns {Object} Hierarchical node structure: { id: string, children: Array<Object> }.
|
| 18 |
-
*/
|
| 19 |
-
export function generateBomTree(rootId, activeEdges) {
|
| 20 |
-
return buildTreeRecursive(rootId, activeEdges || [], new Set());
|
| 21 |
-
}
|
| 22 |
|
| 23 |
/**
|
| 24 |
-
*
|
| 25 |
-
*
|
| 26 |
-
*
|
| 27 |
-
*
|
| 28 |
-
* - When at a Slot: traverses incoming mounted_in edges to locate the mounted Child component.
|
| 29 |
-
* - Under no circumstances does a Slot traverse backwards to its Parent, or a Child backwards to its Slot.
|
| 30 |
-
*
|
| 31 |
-
* @param {string} nodeId - Current node identifier.
|
| 32 |
-
* @param {Array<Object>} activeEdges - Array of active relationship edges.
|
| 33 |
-
* @param {Set<string>} visited - Set of previously visited node identifiers to avoid cycles.
|
| 34 |
-
* @returns {Object} Tree node object.
|
| 35 |
*/
|
| 36 |
-
function
|
| 37 |
-
if (
|
| 38 |
-
return
|
| 39 |
}
|
| 40 |
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
const childIds = new Set();
|
| 45 |
-
const edges = activeEdges || [];
|
| 46 |
|
| 47 |
-
//
|
| 48 |
-
const
|
| 49 |
-
|
| 50 |
-
|
|
|
|
|
|
|
| 51 |
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
if (!
|
| 58 |
-
|
| 59 |
}
|
| 60 |
-
}
|
| 61 |
-
}
|
| 62 |
-
}
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
if (
|
| 70 |
-
|
| 71 |
-
childIds.add(e.to);
|
| 72 |
-
}
|
| 73 |
-
} else if (rel === 'has_part' || e.label === 'has_part') {
|
| 74 |
-
if (!nextVisited.has(e.to)) {
|
| 75 |
-
childIds.add(e.to);
|
| 76 |
-
}
|
| 77 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
}
|
| 79 |
-
}
|
| 80 |
-
}
|
| 81 |
|
| 82 |
-
|
|
|
|
|
|
|
| 83 |
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 88 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
/**
|
| 2 |
* URMSM LAYER: BOM VIEW (Stakeholder Viewpoint)
|
| 3 |
* ---------------------------------------------
|
| 4 |
+
* Generates the user-facing Bill of Materials (BOM) hierarchy for the active
|
| 5 |
+
* ontology at time T:
|
| 6 |
+
* - Dynamically discovers all active root containers (Vehicles, Warehouses).
|
| 7 |
+
* - Never renders a phantom 'System_A' when the graph is empty.
|
| 8 |
+
* - Collapses technical Slot nodes via SMRM reduction.
|
| 9 |
+
* - Attaches active defect indicators (e.g. 'crack: 2mm') to components if and only if
|
| 10 |
+
* the defect survives in the active ontology at time T.
|
| 11 |
*/
|
| 12 |
|
| 13 |
+
import { INTERNAL_PROPERTY_KEYS } from '../smrm/definitions.js';
|
| 14 |
+
import { reduceGraphForSMRM } from './smrm-view.js';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
|
| 16 |
/**
|
| 17 |
+
* Generates an array of root BOM trees from the current canonical Semantic Graph.
|
| 18 |
+
*
|
| 19 |
+
* @param {Object} semanticGraph - Canonical SMIM Semantic Graph ({ nodes, edges }).
|
| 20 |
+
* @returns {Array<{ id: string, entityClass: string, defects: Array<string>, children: Array }>}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
*/
|
| 22 |
+
export function generateBomTrees(semanticGraph) {
|
| 23 |
+
if (!semanticGraph || !Array.isArray(semanticGraph.nodes) || semanticGraph.nodes.length === 0) {
|
| 24 |
+
return [];
|
| 25 |
}
|
| 26 |
|
| 27 |
+
// Reduce to Engineer SMRM graph so Slots, 4D Extents, and Qualities are normalised
|
| 28 |
+
const smrmGraph = reduceGraphForSMRM(semanticGraph);
|
| 29 |
+
const activeEdges = smrmGraph.edges.filter(e => e.properties?.isCurrent !== false);
|
|
|
|
|
|
|
| 30 |
|
| 31 |
+
// Map active defects per component ID
|
| 32 |
+
const defectsByNode = new Map();
|
| 33 |
+
const addDefect = (nodeId, label) => {
|
| 34 |
+
if (!defectsByNode.has(nodeId)) defectsByNode.set(nodeId, new Set());
|
| 35 |
+
defectsByNode.get(nodeId).add(label);
|
| 36 |
+
};
|
| 37 |
|
| 38 |
+
// 1a. Check custom properties on nodes (Naive Node mode)
|
| 39 |
+
smrmGraph.nodes.forEach(node => {
|
| 40 |
+
if (node.type === 'Defect') return;
|
| 41 |
+
if (node.properties) {
|
| 42 |
+
Object.entries(node.properties).forEach(([k, v]) => {
|
| 43 |
+
if (!INTERNAL_PROPERTY_KEYS.includes(k) && v !== undefined && v !== null) {
|
| 44 |
+
addDefect(node.id, `${k}: ${v}`);
|
| 45 |
}
|
| 46 |
+
});
|
| 47 |
+
}
|
| 48 |
+
});
|
| 49 |
+
|
| 50 |
+
// 1b. Check custom properties on active structural edges (Naive Edge mode)
|
| 51 |
+
activeEdges.forEach(edge => {
|
| 52 |
+
const childId = edge.childId || (edge.label === 'stored_at' || edge.label === 'mounted_in' ? edge.from : edge.to);
|
| 53 |
+
if (edge.properties && childId) {
|
| 54 |
+
Object.entries(edge.properties).forEach(([k, v]) => {
|
| 55 |
+
if (!INTERNAL_PROPERTY_KEYS.includes(k) && v !== undefined && v !== null) {
|
| 56 |
+
addDefect(childId, `${k}: ${v}`);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
}
|
| 58 |
+
});
|
| 59 |
+
}
|
| 60 |
+
});
|
| 61 |
+
|
| 62 |
+
// 1c. Check explicit Defect nodes linked via active 'has_defect' edges (BFO & ISO 15926)
|
| 63 |
+
activeEdges.forEach(edge => {
|
| 64 |
+
if (edge.label === 'has_defect' || edge.type === 'has_defect') {
|
| 65 |
+
const defectNode = smrmGraph.nodes.find(n => n.id === edge.to);
|
| 66 |
+
if (defectNode) {
|
| 67 |
+
const dType = defectNode.properties?.defectType || defectNode.properties?.qualityType || 'defect';
|
| 68 |
+
const dVal = defectNode.properties?.value || 'present';
|
| 69 |
+
addDefect(edge.from, `${dType}: ${dVal}`);
|
| 70 |
}
|
| 71 |
+
}
|
| 72 |
+
});
|
| 73 |
|
| 74 |
+
// Build parent -> children adjacency for active structural relations
|
| 75 |
+
const childrenMap = new Map();
|
| 76 |
+
const hasParentSet = new Set();
|
| 77 |
|
| 78 |
+
activeEdges.forEach(edge => {
|
| 79 |
+
if (edge.label === 'has_defect' || edge.type === 'has_defect') return;
|
| 80 |
+
|
| 81 |
+
let parentId = null;
|
| 82 |
+
let childId = null;
|
| 83 |
+
|
| 84 |
+
if (edge.label === 'has_part' || edge.label === 'has_slot') {
|
| 85 |
+
parentId = edge.from;
|
| 86 |
+
childId = edge.to;
|
| 87 |
+
} else if (edge.label === 'mounted_in' || edge.label === 'stored_at') {
|
| 88 |
+
parentId = edge.to;
|
| 89 |
+
childId = edge.from;
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
if (parentId && childId && parentId !== childId) {
|
| 93 |
+
if (!childrenMap.has(parentId)) childrenMap.set(parentId, new Set());
|
| 94 |
+
childrenMap.get(parentId).add(childId);
|
| 95 |
+
hasParentSet.add(childId);
|
| 96 |
+
}
|
| 97 |
+
});
|
| 98 |
+
|
| 99 |
+
// Identify root nodes (Vehicles, Warehouses, or top-level assemblies without a parent)
|
| 100 |
+
const candidateRoots = smrmGraph.nodes.filter(n => {
|
| 101 |
+
if (n.type === 'Defect' || n.type === 'Slot') return false;
|
| 102 |
+
if (hasParentSet.has(n.id)) return false;
|
| 103 |
+
return n.type === 'Vehicle' || n.type === 'Warehouse' || childrenMap.has(n.id);
|
| 104 |
+
});
|
| 105 |
+
|
| 106 |
+
function buildNode(nodeId, visited = new Set()) {
|
| 107 |
+
if (visited.has(nodeId)) {
|
| 108 |
+
return { id: nodeId, defects: [], children: [] };
|
| 109 |
+
}
|
| 110 |
+
const nextVisited = new Set(visited);
|
| 111 |
+
nextVisited.add(nodeId);
|
| 112 |
+
|
| 113 |
+
const nodeObj = smrmGraph.nodes.find(n => n.id === nodeId);
|
| 114 |
+
const childIds = Array.from(childrenMap.get(nodeId) || []);
|
| 115 |
+
const defects = Array.from(defectsByNode.get(nodeId) || []);
|
| 116 |
+
|
| 117 |
+
return {
|
| 118 |
+
id: nodeId,
|
| 119 |
+
entityClass: nodeObj?.type || 'Part',
|
| 120 |
+
defects,
|
| 121 |
+
children: childIds.map(cid => buildNode(cid, nextVisited))
|
| 122 |
+
};
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
return candidateRoots.map(root => buildNode(root.id));
|
| 126 |
}
|
| 127 |
+
|
| 128 |
+
/**
|
| 129 |
+
* Backwards-compatible single-root helper.
|
| 130 |
+
*/
|
| 131 |
+
export function generateBomTree(rootId, activeEdges) {
|
| 132 |
+
const trees = generateBomTrees({
|
| 133 |
+
nodes: [{ id: rootId, type: 'Vehicle', properties: {} }],
|
| 134 |
+
edges: activeEdges || []
|
| 135 |
+
});
|
| 136 |
+
return trees[0] || null;
|
| 137 |
+
}
|
js/urmsm/smrm-view.js
CHANGED
|
@@ -1,33 +1,187 @@
|
|
| 1 |
/**
|
| 2 |
* URMSM LAYER: SMRM GRAPH REDUCTION VIEWPOINT (ISO 42010)
|
| 3 |
* -------------------------------------------------------
|
| 4 |
-
* Transforms
|
| 5 |
-
*
|
| 6 |
-
*
|
| 7 |
-
*
|
| 8 |
-
*
|
| 9 |
-
*
|
| 10 |
-
*
|
| 11 |
-
* Adheres strictly to the Rule of Isolation:
|
| 12 |
-
* Pure graph-to-graph reduction middleware without DOM or Vue coupling.
|
| 13 |
-
*
|
| 14 |
-
* @param {Object} semanticGraph - Canonical Semantic Graph ({ nodes: Array, edges: Array }).
|
| 15 |
-
* @returns {Object} Reduced Semantic Graph ({ nodes: Array, edges: Array }) for SMRM rendering.
|
| 16 |
*/
|
| 17 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
export function reduceGraphForSMRM(semanticGraph) {
|
| 19 |
if (!semanticGraph || !Array.isArray(semanticGraph.nodes) || !Array.isArray(semanticGraph.edges)) {
|
| 20 |
return { nodes: [], edges: [] };
|
| 21 |
}
|
| 22 |
|
| 23 |
-
|
| 24 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
semanticGraph.nodes.forEach(node => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
const isSlot = node.type === 'Slot' ||
|
| 27 |
node.properties?.entityClass === 'Slot' ||
|
| 28 |
-
node.id.startsWith('Slot_')
|
| 29 |
-
node.id.startsWith('4d_Slot_') ||
|
| 30 |
-
node.id.startsWith('st_Slot_');
|
| 31 |
if (isSlot) {
|
| 32 |
slotNodeIds.add(node.id);
|
| 33 |
}
|
|
@@ -35,24 +189,19 @@ export function reduceGraphForSMRM(semanticGraph) {
|
|
| 35 |
|
| 36 |
if (slotNodeIds.size === 0) {
|
| 37 |
return {
|
| 38 |
-
nodes:
|
| 39 |
-
edges:
|
| 40 |
};
|
| 41 |
}
|
| 42 |
|
| 43 |
-
const nodesToRemove = new Set(slotNodeIds);
|
| 44 |
const edgesToRemove = new Set();
|
| 45 |
const directTransitiveEdges = [];
|
| 46 |
|
| 47 |
-
// 2. Locate and collapse [Parent] -[has_slot]-> [Slot] <-[mounted_in]- [Child]
|
| 48 |
slotNodeIds.forEach(slotId => {
|
| 49 |
-
|
| 50 |
-
const hasSlotEdges = semanticGraph.edges.filter(
|
| 51 |
e => e.to === slotId && (e.label === 'has_slot' || e.type === 'has_slot')
|
| 52 |
);
|
| 53 |
-
|
| 54 |
-
// Find mounted_in edges from Child to Slot
|
| 55 |
-
const mountedInEdges = semanticGraph.edges.filter(
|
| 56 |
e => e.to === slotId && (e.label === 'mounted_in' || e.type === 'mounted_in' || e.type === 'composition')
|
| 57 |
);
|
| 58 |
|
|
@@ -63,8 +212,6 @@ export function reduceGraphForSMRM(semanticGraph) {
|
|
| 63 |
mountedInEdges.forEach(mountedInEdge => {
|
| 64 |
edgesToRemove.add(mountedInEdge.id);
|
| 65 |
const childNodeId = mountedInEdge.from;
|
| 66 |
-
|
| 67 |
-
// Formulate direct transitive edge: Parent -> Child
|
| 68 |
const directEdgeId = `smrm_${parentNodeId}_${childNodeId}_${mountedInEdge.properties?.validFrom ?? 0}`;
|
| 69 |
|
| 70 |
directTransitiveEdges.push({
|
|
@@ -81,20 +228,13 @@ export function reduceGraphForSMRM(semanticGraph) {
|
|
| 81 |
});
|
| 82 |
});
|
| 83 |
});
|
| 84 |
-
|
| 85 |
-
// Also remove temporalPartOf edges linking slot state slices to 4D extents
|
| 86 |
-
semanticGraph.edges.forEach(e => {
|
| 87 |
-
if ((e.from === slotId || e.to === slotId) && e.label === 'temporalPartOf') {
|
| 88 |
-
edgesToRemove.add(e.id);
|
| 89 |
-
}
|
| 90 |
-
});
|
| 91 |
});
|
| 92 |
|
| 93 |
-
const remainingNodes =
|
| 94 |
-
.filter(n => !
|
| 95 |
.map(n => ({ ...n, properties: { ...(n.properties || {}) } }));
|
| 96 |
|
| 97 |
-
const remainingEdges =
|
| 98 |
.filter(e => !edgesToRemove.has(e.id))
|
| 99 |
.map(e => ({ ...e, properties: { ...(e.properties || {}) } }))
|
| 100 |
.concat(directTransitiveEdges);
|
|
@@ -103,4 +243,4 @@ export function reduceGraphForSMRM(semanticGraph) {
|
|
| 103 |
nodes: remainingNodes,
|
| 104 |
edges: remainingEdges
|
| 105 |
};
|
| 106 |
-
}
|
|
|
|
| 1 |
/**
|
| 2 |
* URMSM LAYER: SMRM GRAPH REDUCTION VIEWPOINT (ISO 42010)
|
| 3 |
* -------------------------------------------------------
|
| 4 |
+
* Transforms any canonical SMIM Semantic Graph (Naive, 3D BFO, or 4D ISO 15926)
|
| 5 |
+
* into the Engineer's SMRM Perspective:
|
| 6 |
+
* - Collapses intermediate contextual Slot nodes into direct 'has_part' relationships.
|
| 7 |
+
* - Abstracts away 4D Spatio-Temporal Extents ('ActualIndividual') and 'State' slices
|
| 8 |
+
* into clean domain entities ('Vehicle', 'Part', 'Warehouse', 'Defect'), showing
|
| 9 |
+
* the engineer purely what domain knowledge survives at time T.
|
| 10 |
+
* - Maps 3D BFO 'MaterialEntity' and 'Quality' nodes into engineering domain classes.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
*/
|
| 12 |
|
| 13 |
+
import { inferEntityClass } from '../smrm/definitions.js';
|
| 14 |
+
|
| 15 |
+
/**
|
| 16 |
+
* Reduces a canonical SMIM graph into the Engineer (SMRM) viewpoint.
|
| 17 |
+
*
|
| 18 |
+
* @param {Object} semanticGraph - Canonical Semantic Graph ({ nodes: Array, edges: Array }).
|
| 19 |
+
* @returns {{ nodes: Array, edges: Array }}
|
| 20 |
+
*/
|
| 21 |
export function reduceGraphForSMRM(semanticGraph) {
|
| 22 |
if (!semanticGraph || !Array.isArray(semanticGraph.nodes) || !Array.isArray(semanticGraph.edges)) {
|
| 23 |
return { nodes: [], edges: [] };
|
| 24 |
}
|
| 25 |
|
| 26 |
+
const is4D = semanticGraph.nodes.some(n => n.type === 'ActualIndividual' || n.type === 'State');
|
| 27 |
+
const isBFO = semanticGraph.nodes.some(n => n.type === 'MaterialEntity' || n.type === 'Quality');
|
| 28 |
+
|
| 29 |
+
// Case A: 4D ISO 15926 Graph -> Project onto domain entities
|
| 30 |
+
if (is4D) {
|
| 31 |
+
return reduce4DToSMRM(semanticGraph);
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
// Case B: 3D BFO Graph -> Normalize MaterialEntity/Quality & collapse Slots
|
| 35 |
+
if (isBFO) {
|
| 36 |
+
const normalizedNodes = semanticGraph.nodes.map(n => {
|
| 37 |
+
if (n.type === 'MaterialEntity') {
|
| 38 |
+
const cls = inferEntityClass(n.id, n.properties?.entityClass);
|
| 39 |
+
return {
|
| 40 |
+
...n,
|
| 41 |
+
type: cls,
|
| 42 |
+
properties: { ...(n.properties || {}), entityClass: cls }
|
| 43 |
+
};
|
| 44 |
+
}
|
| 45 |
+
if (n.type === 'Quality') {
|
| 46 |
+
return {
|
| 47 |
+
...n,
|
| 48 |
+
type: 'Defect',
|
| 49 |
+
properties: { ...(n.properties || {}), entityClass: 'Defect' }
|
| 50 |
+
};
|
| 51 |
+
}
|
| 52 |
+
return { ...n, properties: { ...(n.properties || {}) } };
|
| 53 |
+
});
|
| 54 |
+
|
| 55 |
+
const normalizedEdges = semanticGraph.edges.map(e => {
|
| 56 |
+
if (e.label === 'inheres_in' || e.type === 'inherence') {
|
| 57 |
+
// Flip Quality -> Part inheres_in into Part -> Defect has_defect for Engineer View
|
| 58 |
+
return {
|
| 59 |
+
...e,
|
| 60 |
+
from: e.to,
|
| 61 |
+
to: e.from,
|
| 62 |
+
label: 'has_defect',
|
| 63 |
+
type: 'has_defect',
|
| 64 |
+
properties: { ...(e.properties || {}) }
|
| 65 |
+
};
|
| 66 |
+
}
|
| 67 |
+
return { ...e, properties: { ...(e.properties || {}) } };
|
| 68 |
+
});
|
| 69 |
+
|
| 70 |
+
return collapseSlots({ nodes: normalizedNodes, edges: normalizedEdges });
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
// Case C: Naive Graph -> Collapse Slots directly while transferring edge properties
|
| 74 |
+
return collapseSlots(semanticGraph);
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
/**
|
| 78 |
+
* Projects a 4D ISO 15926 Semantic Graph into a clean domain graph for the Engineer View,
|
| 79 |
+
* collapsing 4D Extents, States, and Slots while preserving surviving defects.
|
| 80 |
+
*/
|
| 81 |
+
function reduce4DToSMRM(semanticGraph) {
|
| 82 |
+
const domainNodes = new Map();
|
| 83 |
+
const stateToOwner = new Map();
|
| 84 |
+
|
| 85 |
semanticGraph.nodes.forEach(node => {
|
| 86 |
+
if (node.type === 'ActualIndividual') {
|
| 87 |
+
const origId = node.properties?.originalId || node.id.replace(/^4d_/, '');
|
| 88 |
+
const cls = inferEntityClass(origId, node.properties?.entityClass);
|
| 89 |
+
if (cls !== 'Slot') {
|
| 90 |
+
domainNodes.set(origId, {
|
| 91 |
+
id: origId,
|
| 92 |
+
label: origId,
|
| 93 |
+
type: cls,
|
| 94 |
+
properties: { entityClass: cls }
|
| 95 |
+
});
|
| 96 |
+
}
|
| 97 |
+
} else if (node.type === 'State') {
|
| 98 |
+
const ownerId = node.properties?.ownerId || node.id.replace(/^st_/, '').split('_evt_')[0];
|
| 99 |
+
stateToOwner.set(node.id, ownerId);
|
| 100 |
+
} else if (node.type === 'Defect') {
|
| 101 |
+
domainNodes.set(node.id, {
|
| 102 |
+
...node,
|
| 103 |
+
type: 'Defect',
|
| 104 |
+
properties: { ...(node.properties || {}), entityClass: 'Defect' }
|
| 105 |
+
});
|
| 106 |
+
}
|
| 107 |
+
});
|
| 108 |
+
|
| 109 |
+
const projectedEdges = [];
|
| 110 |
+
|
| 111 |
+
semanticGraph.edges.forEach(edge => {
|
| 112 |
+
if (edge.label === 'temporalPartOf') return;
|
| 113 |
+
|
| 114 |
+
const fromOwner = stateToOwner.get(edge.from) || edge.from;
|
| 115 |
+
const toOwner = stateToOwner.get(edge.to) || edge.to;
|
| 116 |
+
|
| 117 |
+
if (edge.label === 'has_defect') {
|
| 118 |
+
projectedEdges.push({
|
| 119 |
+
id: `smrm_def_${fromOwner}_${edge.to}`,
|
| 120 |
+
from: fromOwner,
|
| 121 |
+
to: edge.to,
|
| 122 |
+
label: 'has_defect',
|
| 123 |
+
type: 'has_defect',
|
| 124 |
+
properties: {
|
| 125 |
+
...(edge.properties || {}),
|
| 126 |
+
isCurrent: edge.properties?.validTo === null
|
| 127 |
+
}
|
| 128 |
+
});
|
| 129 |
+
return;
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
if (edge.label === 'stored_at') {
|
| 133 |
+
projectedEdges.push({
|
| 134 |
+
id: `smrm_store_${fromOwner}_${toOwner}_${edge.properties?.validFrom ?? 0}`,
|
| 135 |
+
from: fromOwner,
|
| 136 |
+
to: toOwner,
|
| 137 |
+
label: 'stored_at',
|
| 138 |
+
type: 'stored_at',
|
| 139 |
+
childId: fromOwner,
|
| 140 |
+
parentId: toOwner,
|
| 141 |
+
properties: { ...(edge.properties || {}) }
|
| 142 |
+
});
|
| 143 |
+
return;
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
if (edge.label === 'has_slot' || edge.label === 'mounted_in') {
|
| 147 |
+
// Ensure intermediate slot exists temporarily so collapseSlots can merge the pair cleanly
|
| 148 |
+
if (toOwner.startsWith('Slot_') && !domainNodes.has(toOwner)) {
|
| 149 |
+
domainNodes.set(toOwner, {
|
| 150 |
+
id: toOwner,
|
| 151 |
+
label: toOwner,
|
| 152 |
+
type: 'Slot',
|
| 153 |
+
properties: { entityClass: 'Slot' }
|
| 154 |
+
});
|
| 155 |
+
}
|
| 156 |
+
projectedEdges.push({
|
| 157 |
+
id: edge.id,
|
| 158 |
+
from: fromOwner,
|
| 159 |
+
to: toOwner,
|
| 160 |
+
label: edge.label,
|
| 161 |
+
type: edge.label,
|
| 162 |
+
childId: edge.childId,
|
| 163 |
+
parentId: edge.parentId,
|
| 164 |
+
properties: { ...(edge.properties || {}) }
|
| 165 |
+
});
|
| 166 |
+
}
|
| 167 |
+
});
|
| 168 |
+
|
| 169 |
+
return collapseSlots({
|
| 170 |
+
nodes: Array.from(domainNodes.values()),
|
| 171 |
+
edges: projectedEdges
|
| 172 |
+
});
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
/**
|
| 176 |
+
* Collapses [Parent] -[has_slot]-> [Slot] <-[mounted_in]- [Child] into
|
| 177 |
+
* a direct [Parent] -[has_part]-> [Child] edge, preserving mounting properties.
|
| 178 |
+
*/
|
| 179 |
+
function collapseSlots(graph) {
|
| 180 |
+
const slotNodeIds = new Set();
|
| 181 |
+
graph.nodes.forEach(node => {
|
| 182 |
const isSlot = node.type === 'Slot' ||
|
| 183 |
node.properties?.entityClass === 'Slot' ||
|
| 184 |
+
node.id.startsWith('Slot_');
|
|
|
|
|
|
|
| 185 |
if (isSlot) {
|
| 186 |
slotNodeIds.add(node.id);
|
| 187 |
}
|
|
|
|
| 189 |
|
| 190 |
if (slotNodeIds.size === 0) {
|
| 191 |
return {
|
| 192 |
+
nodes: graph.nodes.map(n => ({ ...n, properties: { ...(n.properties || {}) } })),
|
| 193 |
+
edges: graph.edges.map(e => ({ ...e, properties: { ...(e.properties || {}) } }))
|
| 194 |
};
|
| 195 |
}
|
| 196 |
|
|
|
|
| 197 |
const edgesToRemove = new Set();
|
| 198 |
const directTransitiveEdges = [];
|
| 199 |
|
|
|
|
| 200 |
slotNodeIds.forEach(slotId => {
|
| 201 |
+
const hasSlotEdges = graph.edges.filter(
|
|
|
|
| 202 |
e => e.to === slotId && (e.label === 'has_slot' || e.type === 'has_slot')
|
| 203 |
);
|
| 204 |
+
const mountedInEdges = graph.edges.filter(
|
|
|
|
|
|
|
| 205 |
e => e.to === slotId && (e.label === 'mounted_in' || e.type === 'mounted_in' || e.type === 'composition')
|
| 206 |
);
|
| 207 |
|
|
|
|
| 212 |
mountedInEdges.forEach(mountedInEdge => {
|
| 213 |
edgesToRemove.add(mountedInEdge.id);
|
| 214 |
const childNodeId = mountedInEdge.from;
|
|
|
|
|
|
|
| 215 |
const directEdgeId = `smrm_${parentNodeId}_${childNodeId}_${mountedInEdge.properties?.validFrom ?? 0}`;
|
| 216 |
|
| 217 |
directTransitiveEdges.push({
|
|
|
|
| 228 |
});
|
| 229 |
});
|
| 230 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 231 |
});
|
| 232 |
|
| 233 |
+
const remainingNodes = graph.nodes
|
| 234 |
+
.filter(n => !slotNodeIds.has(n.id))
|
| 235 |
.map(n => ({ ...n, properties: { ...(n.properties || {}) } }));
|
| 236 |
|
| 237 |
+
const remainingEdges = graph.edges
|
| 238 |
.filter(e => !edgesToRemove.has(e.id))
|
| 239 |
.map(e => ({ ...e, properties: { ...(e.properties || {}) } }))
|
| 240 |
.concat(directTransitiveEdges);
|
|
|
|
| 243 |
nodes: remainingNodes,
|
| 244 |
edges: remainingEdges
|
| 245 |
};
|
| 246 |
+
}
|
styles/main.css
CHANGED
|
@@ -5,65 +5,399 @@
|
|
| 5 |
font-style: normal;
|
| 6 |
}
|
| 7 |
|
| 8 |
-
* {
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
.btn-group.flag-group button {
|
| 39 |
font-family: 'Twemoji', system-ui, sans-serif;
|
| 40 |
-
font-size:
|
| 41 |
-
padding: 3px
|
| 42 |
}
|
| 43 |
|
| 44 |
-
|
| 45 |
-
|
|
|
|
|
|
|
|
|
|
| 46 |
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
|
|
|
|
|
|
| 52 |
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
|
|
|
| 59 |
|
| 60 |
-
|
| 61 |
-
.
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
|
|
|
| 66 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
.swatch-box-sys { width: 13px; height: 11px; background: #0284c7; border: 1px solid #0369a1; border-radius: 2px; margin-right: 8px; flex-shrink: 0; }
|
| 68 |
.swatch-box-elem { width: 13px; height: 11px; background: #64748b; border: 1px solid #475569; border-radius: 2px; margin-right: 8px; flex-shrink: 0; }
|
| 69 |
.swatch-box-4d { width: 13px; height: 11px; background: #4f46e5; border: 1px solid #3730a3; border-radius: 2px; margin-right: 8px; flex-shrink: 0; }
|
|
@@ -83,38 +417,215 @@ body.no-select { user-select: none; }
|
|
| 83 |
.swatch-line-store { width: 16px; height: 2px; background: #f97316; margin-right: 8px; flex-shrink: 0; }
|
| 84 |
.swatch-line-defect { width: 16px; height: 2px; background: #ef4444; margin-right: 8px; flex-shrink: 0; }
|
| 85 |
|
| 86 |
-
|
| 87 |
-
.sidebar {
|
| 88 |
-
|
| 89 |
-
.card h4 { margin: 0 0 8px 0; font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em; color: #64748b; display: flex; justify-content: space-between; align-items: center; }
|
| 90 |
-
ul.tree, ul.tree ul { list-style-type: none; padding-left: 16px; margin: 4px 0; }
|
| 91 |
-
ul.tree li { position: relative; padding: 2px 0; font-size: 13px; }
|
| 92 |
-
ul.tree li::before { content: "├─ "; color: #94a3b8; }
|
| 93 |
-
ul.tree li:last-child::before { content: "└─ "; color: #94a3b8; }
|
| 94 |
-
table { width: 100%; border-collapse: collapse; font-size: 11px; }
|
| 95 |
-
th, td { text-align: left; padding: 5px 6px; border-bottom: 1px solid #f1f5f9; }
|
| 96 |
-
th { color: #64748b; font-weight: 600; }
|
| 97 |
-
.active-row { background: #f0fdf4; }
|
| 98 |
-
.closed-row { color: #94a3b8; text-decoration: line-through; }
|
| 99 |
-
pre.code-block { margin: 0; font-family: monospace; font-size: 11px; color: #334155; line-height: 1.4; background: #f8fafc; padding: 8px; border-radius: 4px; border: 1px solid #e2e8f0; overflow-y: auto; flex: 1; }
|
| 100 |
-
|
| 101 |
-
/* Fix for Vue #app wrapper to fill the viewport */
|
| 102 |
-
#app {
|
| 103 |
display: flex;
|
| 104 |
flex-direction: column;
|
| 105 |
-
|
| 106 |
-
|
| 107 |
min-height: 0;
|
| 108 |
}
|
| 109 |
|
| 110 |
-
|
| 111 |
-
|
|
|
|
|
|
|
|
|
|
| 112 |
display: flex;
|
| 113 |
flex-direction: column;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 114 |
flex: 1;
|
|
|
|
|
|
|
| 115 |
min-height: 0;
|
| 116 |
}
|
| 117 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 118 |
/* Main Menu / Landing View */
|
| 119 |
.landing-view {
|
| 120 |
display: flex;
|
|
@@ -186,6 +697,7 @@ pre.code-block { margin: 0; font-family: monospace; font-size: 11px; color: #334
|
|
| 186 |
cursor: pointer;
|
| 187 |
transition: all 0.2s ease-in-out;
|
| 188 |
width: 100%;
|
|
|
|
| 189 |
}
|
| 190 |
|
| 191 |
.menu-btn .btn-main-text {
|
|
@@ -378,31 +890,6 @@ pre.code-block { margin: 0; font-family: monospace; font-size: 11px; color: #334
|
|
| 378 |
cursor: pointer;
|
| 379 |
}
|
| 380 |
|
| 381 |
-
/* Role Switcher Styling */
|
| 382 |
-
.btn-group.role-group button {
|
| 383 |
-
font-weight: 600;
|
| 384 |
-
display: inline-flex;
|
| 385 |
-
align-items: center;
|
| 386 |
-
gap: 5px;
|
| 387 |
-
}
|
| 388 |
-
|
| 389 |
-
.btn-group.role-group button.active {
|
| 390 |
-
background: #4f46e5;
|
| 391 |
-
color: #ffffff;
|
| 392 |
-
}
|
| 393 |
-
|
| 394 |
-
/* Universal Engineer Workspace Placeholder */
|
| 395 |
-
.uni-engineer-placeholder {
|
| 396 |
-
min-height: 52px;
|
| 397 |
-
background: #f8fafc;
|
| 398 |
-
border-top: 1px solid #e2e8f0;
|
| 399 |
-
border-bottom-left-radius: 7px;
|
| 400 |
-
border-bottom-right-radius: 7px;
|
| 401 |
-
display: flex;
|
| 402 |
-
align-items: center;
|
| 403 |
-
padding: 12px 16px;
|
| 404 |
-
}
|
| 405 |
-
|
| 406 |
.player-time {
|
| 407 |
font-size: 12px;
|
| 408 |
font-variant-numeric: tabular-nums;
|
|
@@ -417,4 +904,4 @@ pre.code-block { margin: 0; font-family: monospace; font-size: 11px; color: #334
|
|
| 417 |
box-shadow: 0 0 10px 3px rgba(255, 215, 0, 0.8) !important;
|
| 418 |
background-color: rgba(255, 255, 200, 0.3) !important;
|
| 419 |
transition: all 0.3s ease;
|
| 420 |
-
}
|
|
|
|
| 5 |
font-style: normal;
|
| 6 |
}
|
| 7 |
|
| 8 |
+
* {
|
| 9 |
+
box-sizing: border-box;
|
| 10 |
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
body {
|
| 14 |
+
margin: 0;
|
| 15 |
+
padding: 10px 12px;
|
| 16 |
+
background: #f8fafc;
|
| 17 |
+
color: #0f172a;
|
| 18 |
+
display: flex;
|
| 19 |
+
flex-direction: column;
|
| 20 |
+
height: 100vh;
|
| 21 |
+
overflow: hidden;
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
#app {
|
| 25 |
+
display: flex;
|
| 26 |
+
flex-direction: column;
|
| 27 |
+
flex: 1;
|
| 28 |
+
height: 100%;
|
| 29 |
+
min-height: 0;
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
.sandbox-workspace {
|
| 33 |
+
display: flex;
|
| 34 |
+
flex-direction: column;
|
| 35 |
+
flex: 1;
|
| 36 |
+
min-height: 0;
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
/* Top Control Deck */
|
| 40 |
+
.control-deck {
|
| 41 |
+
background: #ffffff;
|
| 42 |
+
border: 1px solid #cbd5e1;
|
| 43 |
+
border-radius: 8px;
|
| 44 |
+
margin-bottom: 8px;
|
| 45 |
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
| 46 |
+
flex-shrink: 0;
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
.deck-row {
|
| 50 |
+
display: flex;
|
| 51 |
+
gap: 8px;
|
| 52 |
+
align-items: center;
|
| 53 |
+
flex-wrap: wrap;
|
| 54 |
+
padding: 6px 12px;
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
.deck-row-top {
|
| 58 |
+
background: #f1f5f9;
|
| 59 |
+
border-bottom: 1px solid #e2e8f0;
|
| 60 |
+
border-top-left-radius: 7px;
|
| 61 |
+
border-top-right-radius: 7px;
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
.action-label {
|
| 65 |
+
font-size: 11px;
|
| 66 |
+
font-weight: 700;
|
| 67 |
+
color: #0284c7;
|
| 68 |
+
white-space: nowrap;
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
.badge {
|
| 72 |
+
background: #0284c7;
|
| 73 |
+
color: #ffffff;
|
| 74 |
+
font-weight: bold;
|
| 75 |
+
padding: 4px 9px;
|
| 76 |
+
border-radius: 6px;
|
| 77 |
+
font-size: 12px;
|
| 78 |
+
white-space: nowrap;
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
.section-label {
|
| 82 |
+
font-size: 11px;
|
| 83 |
+
font-weight: 700;
|
| 84 |
+
text-transform: uppercase;
|
| 85 |
+
letter-spacing: 0.04em;
|
| 86 |
+
color: #475569;
|
| 87 |
+
display: flex;
|
| 88 |
+
align-items: center;
|
| 89 |
+
gap: 4px;
|
| 90 |
+
white-space: nowrap;
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
/* IEC 63339 Badges & Tooltips */
|
| 94 |
+
.iec-badge {
|
| 95 |
+
display: inline-flex;
|
| 96 |
+
align-items: center;
|
| 97 |
+
gap: 3px;
|
| 98 |
+
background: #e0f2fe;
|
| 99 |
+
color: #0369a1;
|
| 100 |
+
border: 1px solid #7dd3fc;
|
| 101 |
+
padding: 2px 6px;
|
| 102 |
+
border-radius: 4px;
|
| 103 |
+
font-size: 10px;
|
| 104 |
+
font-weight: 700;
|
| 105 |
+
letter-spacing: 0.03em;
|
| 106 |
+
cursor: help;
|
| 107 |
+
position: relative;
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
.iec-badge:hover .iec-tooltip {
|
| 111 |
+
visibility: visible;
|
| 112 |
+
opacity: 1;
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
.iec-tooltip {
|
| 116 |
+
visibility: hidden;
|
| 117 |
+
opacity: 0;
|
| 118 |
+
position: absolute;
|
| 119 |
+
top: 130%;
|
| 120 |
+
left: 0;
|
| 121 |
+
z-index: 200;
|
| 122 |
+
background: #0f172a;
|
| 123 |
+
color: #f8fafc;
|
| 124 |
+
padding: 8px 12px;
|
| 125 |
+
border-radius: 6px;
|
| 126 |
+
font-size: 11px;
|
| 127 |
+
font-weight: 400;
|
| 128 |
+
text-transform: none;
|
| 129 |
+
line-height: 1.35;
|
| 130 |
+
min-width: 250px;
|
| 131 |
+
max-width: 320px;
|
| 132 |
+
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.25);
|
| 133 |
+
transition: opacity 0.15s ease-in-out;
|
| 134 |
+
pointer-events: none;
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
.iec-tooltip::after {
|
| 138 |
+
content: "";
|
| 139 |
+
position: absolute;
|
| 140 |
+
bottom: 100%;
|
| 141 |
+
left: 14px;
|
| 142 |
+
border-width: 5px;
|
| 143 |
+
border-style: solid;
|
| 144 |
+
border-color: transparent transparent #0f172a transparent;
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
.iec-badge.align-right .iec-tooltip {
|
| 148 |
+
left: auto;
|
| 149 |
+
right: -5px;
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
.iec-badge.align-right .iec-tooltip::after {
|
| 153 |
+
left: auto;
|
| 154 |
+
right: 20px;
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
/* Buttons & Inputs */
|
| 158 |
+
button {
|
| 159 |
+
background: #0f172a;
|
| 160 |
+
color: #ffffff;
|
| 161 |
+
border: none;
|
| 162 |
+
padding: 5px 10px;
|
| 163 |
+
border-radius: 6px;
|
| 164 |
+
cursor: pointer;
|
| 165 |
+
font-size: 12px;
|
| 166 |
+
font-weight: 500;
|
| 167 |
+
white-space: nowrap;
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
button:hover {
|
| 171 |
+
background: #334155;
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
button.secondary {
|
| 175 |
+
background: #e2e8f0;
|
| 176 |
+
color: #0f172a;
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
button.secondary:hover {
|
| 180 |
+
background: #cbd5e1;
|
| 181 |
+
}
|
| 182 |
+
|
| 183 |
+
button.danger {
|
| 184 |
+
background: #ef4444;
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
button.danger:hover {
|
| 188 |
+
background: #dc2626;
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
.btn-group {
|
| 192 |
+
display: flex;
|
| 193 |
+
border: 1px solid #cbd5e1;
|
| 194 |
+
border-radius: 6px;
|
| 195 |
+
overflow: hidden;
|
| 196 |
+
background: #ffffff;
|
| 197 |
+
flex-shrink: 0;
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
.btn-group button {
|
| 201 |
+
border-radius: 0;
|
| 202 |
+
background: #ffffff;
|
| 203 |
+
color: #475569;
|
| 204 |
+
padding: 4px 9px;
|
| 205 |
+
border-right: 1px solid #cbd5e1;
|
| 206 |
+
font-size: 11.5px;
|
| 207 |
+
font-weight: 600;
|
| 208 |
+
line-height: 1.2;
|
| 209 |
+
}
|
| 210 |
+
|
| 211 |
+
.btn-group button:last-child {
|
| 212 |
+
border-right: none;
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
.btn-group button.active {
|
| 216 |
+
background: #0284c7;
|
| 217 |
+
color: #ffffff;
|
| 218 |
+
}
|
| 219 |
+
|
| 220 |
.btn-group.flag-group button {
|
| 221 |
font-family: 'Twemoji', system-ui, sans-serif;
|
| 222 |
+
font-size: 14px;
|
| 223 |
+
padding: 3px 7px;
|
| 224 |
}
|
| 225 |
|
| 226 |
+
.btn-group.mini-toggle button {
|
| 227 |
+
font-size: 10px;
|
| 228 |
+
padding: 2px 7px;
|
| 229 |
+
text-transform: uppercase;
|
| 230 |
+
}
|
| 231 |
|
| 232 |
+
input,
|
| 233 |
+
select {
|
| 234 |
+
padding: 4px 7px;
|
| 235 |
+
border: 1px solid #cbd5e1;
|
| 236 |
+
border-radius: 6px;
|
| 237 |
+
font-size: 12px;
|
| 238 |
+
}
|
| 239 |
|
| 240 |
+
.divider {
|
| 241 |
+
height: 18px;
|
| 242 |
+
width: 1px;
|
| 243 |
+
background: #cbd5e1;
|
| 244 |
+
margin: 0 2px;
|
| 245 |
+
flex-shrink: 0;
|
| 246 |
+
}
|
| 247 |
|
| 248 |
+
/* Main Split Workspace & Graph Canvas */
|
| 249 |
+
.main-container {
|
| 250 |
+
display: flex;
|
| 251 |
+
flex: 1;
|
| 252 |
+
gap: 8px;
|
| 253 |
+
min-height: 0;
|
| 254 |
+
}
|
| 255 |
|
| 256 |
+
#canvas-container {
|
| 257 |
+
flex: 2;
|
| 258 |
+
background: #ffffff;
|
| 259 |
+
border: 1px solid #e2e8f0;
|
| 260 |
+
border-radius: 8px;
|
| 261 |
+
position: relative;
|
| 262 |
+
overflow: hidden;
|
| 263 |
+
min-width: 25%;
|
| 264 |
+
}
|
| 265 |
+
|
| 266 |
+
#vis-container {
|
| 267 |
+
width: 100%;
|
| 268 |
+
height: 100%;
|
| 269 |
+
position: absolute;
|
| 270 |
+
top: 0;
|
| 271 |
+
left: 0;
|
| 272 |
+
}
|
| 273 |
+
|
| 274 |
+
#mermaid-container {
|
| 275 |
+
width: 100%;
|
| 276 |
+
height: 100%;
|
| 277 |
+
position: absolute;
|
| 278 |
+
top: 0;
|
| 279 |
+
left: 0;
|
| 280 |
+
overflow: auto;
|
| 281 |
+
padding: 24px 24px 140px 24px;
|
| 282 |
+
display: flex;
|
| 283 |
+
justify-content: center;
|
| 284 |
+
align-items: flex-start;
|
| 285 |
+
background: #ffffff;
|
| 286 |
+
}
|
| 287 |
+
|
| 288 |
+
#mermaid-container svg {
|
| 289 |
+
width: auto;
|
| 290 |
+
height: auto;
|
| 291 |
+
max-width: none;
|
| 292 |
+
}
|
| 293 |
+
|
| 294 |
+
/* Resizers */
|
| 295 |
+
.resizer {
|
| 296 |
+
width: 7px;
|
| 297 |
+
background-color: #f1f5f9;
|
| 298 |
+
border: 1px solid #e2e8f0;
|
| 299 |
+
border-radius: 4px;
|
| 300 |
+
cursor: col-resize;
|
| 301 |
+
transition: background-color 0.2s, border-color 0.2s;
|
| 302 |
+
flex-shrink: 0;
|
| 303 |
+
}
|
| 304 |
+
|
| 305 |
+
.resizer:hover,
|
| 306 |
+
.resizer.dragging {
|
| 307 |
+
background-color: #94a3b8;
|
| 308 |
+
border-color: #64748b;
|
| 309 |
+
}
|
| 310 |
+
|
| 311 |
+
.resizer-h {
|
| 312 |
+
height: 7px;
|
| 313 |
+
background-color: #f1f5f9;
|
| 314 |
+
border: 1px solid #e2e8f0;
|
| 315 |
+
border-radius: 4px;
|
| 316 |
+
cursor: row-resize;
|
| 317 |
+
transition: background-color 0.2s, border-color 0.2s;
|
| 318 |
+
flex-shrink: 0;
|
| 319 |
+
}
|
| 320 |
+
|
| 321 |
+
.resizer-h:hover,
|
| 322 |
+
.resizer-h.dragging {
|
| 323 |
+
background-color: #94a3b8;
|
| 324 |
+
border-color: #64748b;
|
| 325 |
+
}
|
| 326 |
+
|
| 327 |
+
body.no-select {
|
| 328 |
+
user-select: none;
|
| 329 |
+
}
|
| 330 |
+
|
| 331 |
+
/* Canvas Overlays: Caption & Legend */
|
| 332 |
+
.graph-caption {
|
| 333 |
+
position: absolute;
|
| 334 |
+
top: 12px;
|
| 335 |
+
left: 12px;
|
| 336 |
+
background: rgba(255, 255, 255, 0.94);
|
| 337 |
+
backdrop-filter: blur(4px);
|
| 338 |
+
border: 1px solid #cbd5e1;
|
| 339 |
+
border-radius: 6px;
|
| 340 |
+
padding: 6px 12px;
|
| 341 |
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
| 342 |
+
font-size: 11px;
|
| 343 |
+
font-weight: 700;
|
| 344 |
+
color: #1e293b;
|
| 345 |
+
z-index: 20;
|
| 346 |
+
display: flex;
|
| 347 |
+
align-items: center;
|
| 348 |
+
gap: 6px;
|
| 349 |
+
letter-spacing: 0.02em;
|
| 350 |
+
pointer-events: none;
|
| 351 |
+
}
|
| 352 |
+
|
| 353 |
+
.graph-legend {
|
| 354 |
+
position: absolute;
|
| 355 |
+
bottom: 12px;
|
| 356 |
+
left: 12px;
|
| 357 |
+
background: rgba(255, 255, 255, 0.96);
|
| 358 |
+
backdrop-filter: blur(4px);
|
| 359 |
+
border: 1px solid #cbd5e1;
|
| 360 |
+
border-radius: 8px;
|
| 361 |
+
padding: 10px 14px;
|
| 362 |
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
| 363 |
+
font-size: 11px;
|
| 364 |
+
color: #334155;
|
| 365 |
+
z-index: 20;
|
| 366 |
+
max-width: 330px;
|
| 367 |
+
}
|
| 368 |
+
|
| 369 |
+
.legend-title {
|
| 370 |
+
font-weight: 700;
|
| 371 |
+
text-transform: uppercase;
|
| 372 |
+
letter-spacing: 0.05em;
|
| 373 |
+
color: #475569;
|
| 374 |
+
margin-bottom: 6px;
|
| 375 |
+
display: flex;
|
| 376 |
+
justify-content: space-between;
|
| 377 |
+
border-bottom: 1px solid #e2e8f0;
|
| 378 |
+
padding-bottom: 4px;
|
| 379 |
+
}
|
| 380 |
+
|
| 381 |
+
.legend-section {
|
| 382 |
+
margin-top: 5px;
|
| 383 |
+
}
|
| 384 |
+
|
| 385 |
+
.legend-section-title {
|
| 386 |
+
font-weight: 600;
|
| 387 |
+
color: #64748b;
|
| 388 |
+
font-size: 10px;
|
| 389 |
+
margin-bottom: 3px;
|
| 390 |
+
text-transform: uppercase;
|
| 391 |
+
}
|
| 392 |
+
|
| 393 |
+
.legend-item {
|
| 394 |
+
display: flex;
|
| 395 |
+
align-items: center;
|
| 396 |
+
margin-bottom: 3px;
|
| 397 |
+
line-height: 1.25;
|
| 398 |
+
}
|
| 399 |
+
|
| 400 |
+
/* Legend Swatches */
|
| 401 |
.swatch-box-sys { width: 13px; height: 11px; background: #0284c7; border: 1px solid #0369a1; border-radius: 2px; margin-right: 8px; flex-shrink: 0; }
|
| 402 |
.swatch-box-elem { width: 13px; height: 11px; background: #64748b; border: 1px solid #475569; border-radius: 2px; margin-right: 8px; flex-shrink: 0; }
|
| 403 |
.swatch-box-4d { width: 13px; height: 11px; background: #4f46e5; border: 1px solid #3730a3; border-radius: 2px; margin-right: 8px; flex-shrink: 0; }
|
|
|
|
| 417 |
.swatch-line-store { width: 16px; height: 2px; background: #f97316; margin-right: 8px; flex-shrink: 0; }
|
| 418 |
.swatch-line-defect { width: 16px; height: 2px; background: #ef4444; margin-right: 8px; flex-shrink: 0; }
|
| 419 |
|
| 420 |
+
/* Right Sidebar Cards */
|
| 421 |
+
.sidebar {
|
| 422 |
+
flex: 1;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 423 |
display: flex;
|
| 424 |
flex-direction: column;
|
| 425 |
+
gap: 6px;
|
| 426 |
+
min-width: 360px;
|
| 427 |
min-height: 0;
|
| 428 |
}
|
| 429 |
|
| 430 |
+
.card {
|
| 431 |
+
background: #ffffff;
|
| 432 |
+
border: 1px solid #e2e8f0;
|
| 433 |
+
border-radius: 8px;
|
| 434 |
+
padding: 10px 12px;
|
| 435 |
display: flex;
|
| 436 |
flex-direction: column;
|
| 437 |
+
min-height: 0;
|
| 438 |
+
position: relative;
|
| 439 |
+
}
|
| 440 |
+
|
| 441 |
+
.card h4 {
|
| 442 |
+
margin: 0 0 6px 0;
|
| 443 |
+
font-size: 11px;
|
| 444 |
+
text-transform: uppercase;
|
| 445 |
+
letter-spacing: 0.05em;
|
| 446 |
+
color: #64748b;
|
| 447 |
+
display: flex;
|
| 448 |
+
justify-content: space-between;
|
| 449 |
+
align-items: center;
|
| 450 |
+
flex-shrink: 0;
|
| 451 |
+
}
|
| 452 |
+
|
| 453 |
+
.card-scroll-body {
|
| 454 |
+
overflow-y: auto;
|
| 455 |
flex: 1;
|
| 456 |
+
display: flex;
|
| 457 |
+
flex-direction: column;
|
| 458 |
min-height: 0;
|
| 459 |
}
|
| 460 |
|
| 461 |
+
/* BOM Tree & Badges */
|
| 462 |
+
ul.tree,
|
| 463 |
+
ul.tree ul {
|
| 464 |
+
list-style-type: none;
|
| 465 |
+
padding-left: 16px;
|
| 466 |
+
margin: 2px 0;
|
| 467 |
+
}
|
| 468 |
+
|
| 469 |
+
ul.tree.root-tree {
|
| 470 |
+
padding-left: 4px;
|
| 471 |
+
}
|
| 472 |
+
|
| 473 |
+
ul.tree li {
|
| 474 |
+
position: relative;
|
| 475 |
+
padding: 2px 0;
|
| 476 |
+
font-size: 12.5px;
|
| 477 |
+
}
|
| 478 |
+
|
| 479 |
+
ul.tree ul li::before {
|
| 480 |
+
content: "├─ ";
|
| 481 |
+
color: #94a3b8;
|
| 482 |
+
}
|
| 483 |
+
|
| 484 |
+
ul.tree ul li:last-child::before {
|
| 485 |
+
content: "└─ ";
|
| 486 |
+
color: #94a3b8;
|
| 487 |
+
}
|
| 488 |
+
|
| 489 |
+
.bom-node-row {
|
| 490 |
+
display: inline-flex;
|
| 491 |
+
align-items: center;
|
| 492 |
+
gap: 6px;
|
| 493 |
+
flex-wrap: wrap;
|
| 494 |
+
}
|
| 495 |
+
|
| 496 |
+
.bom-class-badge {
|
| 497 |
+
background: #f1f5f9;
|
| 498 |
+
color: #475569;
|
| 499 |
+
border: 1px solid #cbd5e1;
|
| 500 |
+
border-radius: 4px;
|
| 501 |
+
padding: 1px 5px;
|
| 502 |
+
font-size: 10px;
|
| 503 |
+
font-weight: 600;
|
| 504 |
+
}
|
| 505 |
+
|
| 506 |
+
.bom-defect-badge {
|
| 507 |
+
background: #fef2f2;
|
| 508 |
+
color: #dc2626;
|
| 509 |
+
border: 1px solid #fecaca;
|
| 510 |
+
border-radius: 4px;
|
| 511 |
+
padding: 1px 6px;
|
| 512 |
+
font-size: 10.5px;
|
| 513 |
+
font-weight: 700;
|
| 514 |
+
}
|
| 515 |
+
|
| 516 |
+
.null-badge {
|
| 517 |
+
color: #94a3b8;
|
| 518 |
+
font-style: italic;
|
| 519 |
+
font-size: 10.5px;
|
| 520 |
+
}
|
| 521 |
+
|
| 522 |
+
/* Under the Bonnet & Live Query Tables */
|
| 523 |
+
.store-sub-header {
|
| 524 |
+
font-size: 10px;
|
| 525 |
+
font-weight: 700;
|
| 526 |
+
text-transform: uppercase;
|
| 527 |
+
color: #475569;
|
| 528 |
+
background: #f8fafc;
|
| 529 |
+
padding: 3px 6px;
|
| 530 |
+
border-left: 3px solid #0284c7;
|
| 531 |
+
margin-bottom: 4px;
|
| 532 |
+
}
|
| 533 |
+
|
| 534 |
+
.storage-table {
|
| 535 |
+
width: 100%;
|
| 536 |
+
border-collapse: collapse;
|
| 537 |
+
font-size: 11px;
|
| 538 |
+
margin-bottom: 4px;
|
| 539 |
+
}
|
| 540 |
+
|
| 541 |
+
th,
|
| 542 |
+
td {
|
| 543 |
+
text-align: left;
|
| 544 |
+
padding: 4px 6px;
|
| 545 |
+
border-bottom: 1px solid #f1f5f9;
|
| 546 |
+
}
|
| 547 |
+
|
| 548 |
+
th {
|
| 549 |
+
color: #64748b;
|
| 550 |
+
font-weight: 600;
|
| 551 |
+
background: #fafafa;
|
| 552 |
+
}
|
| 553 |
+
|
| 554 |
+
.active-row {
|
| 555 |
+
background: #f0fdf4;
|
| 556 |
+
}
|
| 557 |
+
|
| 558 |
+
.closed-row {
|
| 559 |
+
color: #94a3b8;
|
| 560 |
+
text-decoration: line-through;
|
| 561 |
+
}
|
| 562 |
+
|
| 563 |
+
.empty-placeholder {
|
| 564 |
+
color: #94a3b8;
|
| 565 |
+
font-size: 11.5px;
|
| 566 |
+
padding: 6px 4px;
|
| 567 |
+
font-style: italic;
|
| 568 |
+
}
|
| 569 |
+
|
| 570 |
+
pre.code-block {
|
| 571 |
+
margin: 0;
|
| 572 |
+
font-family: ui-monospace, SFMono-Regular, Consolas, monospace;
|
| 573 |
+
font-size: 11px;
|
| 574 |
+
color: #334155;
|
| 575 |
+
line-height: 1.38;
|
| 576 |
+
background: #f8fafc;
|
| 577 |
+
padding: 8px;
|
| 578 |
+
border-radius: 4px;
|
| 579 |
+
border: 1px solid #e2e8f0;
|
| 580 |
+
overflow-x: auto;
|
| 581 |
+
white-space: pre-wrap;
|
| 582 |
+
}
|
| 583 |
+
|
| 584 |
+
pre.query-code-block {
|
| 585 |
+
flex-shrink: 0;
|
| 586 |
+
margin-bottom: 8px;
|
| 587 |
+
}
|
| 588 |
+
|
| 589 |
+
.query-result-box {
|
| 590 |
+
border: 1px solid #e2e8f0;
|
| 591 |
+
border-radius: 6px;
|
| 592 |
+
padding: 6px 8px;
|
| 593 |
+
background: #ffffff;
|
| 594 |
+
display: flex;
|
| 595 |
+
flex-direction: column;
|
| 596 |
+
gap: 4px;
|
| 597 |
+
}
|
| 598 |
+
|
| 599 |
+
.query-result-header {
|
| 600 |
+
display: flex;
|
| 601 |
+
justify-content: space-between;
|
| 602 |
+
align-items: center;
|
| 603 |
+
font-size: 10.5px;
|
| 604 |
+
font-weight: 700;
|
| 605 |
+
color: #334155;
|
| 606 |
+
border-bottom: 1px solid #f1f5f9;
|
| 607 |
+
padding-bottom: 4px;
|
| 608 |
+
}
|
| 609 |
+
|
| 610 |
+
.defect-counter-badge {
|
| 611 |
+
padding: 2px 7px;
|
| 612 |
+
border-radius: 999px;
|
| 613 |
+
font-size: 10px;
|
| 614 |
+
font-weight: 700;
|
| 615 |
+
}
|
| 616 |
+
|
| 617 |
+
.defect-alert-zero {
|
| 618 |
+
background: #fee2e2;
|
| 619 |
+
color: #b91c1c;
|
| 620 |
+
border: 1px solid #fca5a5;
|
| 621 |
+
}
|
| 622 |
+
|
| 623 |
+
.defect-alert-found {
|
| 624 |
+
background: #dcfce7;
|
| 625 |
+
color: #15803d;
|
| 626 |
+
border: 1px solid #86efac;
|
| 627 |
+
}
|
| 628 |
+
|
| 629 |
/* Main Menu / Landing View */
|
| 630 |
.landing-view {
|
| 631 |
display: flex;
|
|
|
|
| 697 |
cursor: pointer;
|
| 698 |
transition: all 0.2s ease-in-out;
|
| 699 |
width: 100%;
|
| 700 |
+
white-space: normal;
|
| 701 |
}
|
| 702 |
|
| 703 |
.menu-btn .btn-main-text {
|
|
|
|
| 890 |
cursor: pointer;
|
| 891 |
}
|
| 892 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 893 |
.player-time {
|
| 894 |
font-size: 12px;
|
| 895 |
font-variant-numeric: tabular-nums;
|
|
|
|
| 904 |
box-shadow: 0 0 10px 3px rgba(255, 215, 0, 0.8) !important;
|
| 905 |
background-color: rgba(255, 255, 200, 0.3) !important;
|
| 906 |
transition: all 0.3s ease;
|
| 907 |
+
}
|