refactoring van de cmap structuren bijna compleet

This commit is contained in:
2026-09-03 16:21:21 +02:00
parent 1abc84489f
commit bd1ef6bed0
75 changed files with 2547 additions and 1967 deletions
@@ -0,0 +1,147 @@
"use strict";
import { ConceptMapConnector } from "../model/concept-map.js";
import { debug, numberOr } from "../cmap-utils.js";
/** Own map-local connector mutations and linking-phrase membership. */
export class CmapRelationController {
constructor(editor) {
this.editor = editor;
}
get items() { return this.editor.items; }
get connectors() { return this.editor.connectors; }
connectWithPhrase(source, target, label = "?????", editImmediately = true) {
const a = this.editor.itemCenter(source);
const b = this.editor.itemCenter(target);
const parentSubmap = this.commonSubmapParent([source, target]);
const phrase = this.editor.addItem({
kind: "phrase",
label,
parentSubmap,
submapDepth: parentSubmap ? parentSubmap.submapDepth + 1 : 0,
x: ((a.x + b.x) / 2) - 72,
y: ((a.y + b.y) / 2) - 18,
backgroundColor: "#fbfbf8",
borderColor: "transparent"
});
this.editor.addConnector(source, phrase, false);
this.editor.addConnector(phrase, target, true);
this.reconcilePhraseMembership(phrase);
this.editor.selectItem(phrase);
if (editImmediately) this.editor.editPhraseInline(phrase);
return phrase;
}
submapChain(record) {
const result = [];
if (record.kind === "submap") result.push(record);
let parent = record.parentSubmap;
while (parent) {
result.push(parent);
parent = parent.parentSubmap;
}
return result;
}
commonSubmapParent(records) {
if (!records.length) return null;
const chains = records.map((record) => this.submapChain(record));
return chains[0]
.filter((candidate) => chains.every((chain) => chain.includes(candidate)))
.sort((a, b) => b.submapDepth - a.submapDepth)[0] || null;
}
reconcilePhraseMembership(phrase = null) {
const phrases = phrase ? [phrase] : this.items.filter((item) => item.kind === "phrase");
for (const item of phrases) {
const endpoints = this.connectors
.filter((connector) => connector.source === item || connector.target === item)
.map((connector) => connector.source === item ? connector.target : connector.source)
.filter((endpoint) => endpoint.kind !== "phrase");
if (!endpoints.length) continue;
const parent = this.commonSubmapParent(endpoints);
item.parentSubmap = parent;
item.submapDepth = parent ? parent.submapDepth + 1 : 0;
}
}
addConnector(source, target, hasArrow = true, options = {}) {
const sourceCenter = this.editor.itemCenter(source);
const targetCenter = this.editor.itemCenter(target);
const link = this.editor.view.createConnector({
content: "",
width: 1,
height: 1,
backgroundColor: "transparent",
borderColor: "transparent",
borderWidth: 0,
lineColor: options.lineColor || "#333",
lineWidth: numberOr(Number(options.lineWidth), 2),
hasArrow,
cx: (sourceCenter.x + targetCenter.x) / 2,
cy: (sourceCenter.y + targetCenter.y) / 2,
sourceX: sourceCenter.x,
sourceY: sourceCenter.y,
targetX: targetCenter.x,
targetY: targetCenter.y
});
link.sourceNode(source.node).targetNode(target.node);
link.straighten();
link.draggable(true);
const record = {
id: Number.isInteger(Number(options.id)) && Number(options.id) > 0 ?
Number(options.id) : this.editor.nextConnectorId,
link,
source,
target,
visualSource: source,
visualTarget: target,
hasArrow,
lineColor: options.lineColor || "#333",
lineWidth: numberOr(Number(options.lineWidth), 2)
};
this.editor.nextConnectorId = Math.max(this.editor.nextConnectorId, record.id + 1);
this.editor.documents.attachConnectorToModel(record);
this.connectors.push(record);
link.onRendered((_renderedLink, element) => this.editor.decorateConnector(record, element));
link.onConnectionChange((_changedLink, type, node) =>
this.editor.handleConnectorConnectionChange(record, type, node));
link.visible(this.editor.isItemVisible(source) && this.editor.isItemVisible(target));
this.editor.scheduleHistoryCommit();
return record;
}
handleConnectorConnectionChange(connector, type, node) {
if (!connector || (type !== "source" && type !== "target")) return false;
const endpoint = this.items.find((item) => item.node === node) || null;
const otherType = type === "source" ? "target" : "source";
if (!endpoint || endpoint === connector[otherType]) {
connector[`visual${type === "source" ? "Source" : "Target"}`] = null;
this.editor.applyConnectorVisualEndpoints(connector,
this.editor.connectorEndpoint(connector.source),
this.editor.connectorEndpoint(connector.target));
return false;
}
if (connector[type] === endpoint) {
connector[`visual${type === "source" ? "Source" : "Target"}`] = endpoint;
return false;
}
const previous = connector[type];
connector[type] = endpoint;
connector[`visual${type === "source" ? "Source" : "Target"}`] = endpoint;
this.reconcilePhraseMembership();
this.editor.refreshSubmapVisibility();
this.editor.refreshConnectorGeometry();
debug("connector endpoint changed", {
connectorId: connector.id,
type,
previousItemId: previous ? previous.id : null,
itemId: endpoint.id
});
this.editor.scheduleHistoryCommit();
return true;
}
}