refactor(important): change binđing geometry to bound_with(up side down tree)

This commit is contained in:
taDuc
2026-05-24 20:18:29 +07:00
parent a98e1ac5b0
commit 395eb3de47
31 changed files with 339 additions and 234 deletions
+57 -54
View File
@@ -30,10 +30,14 @@ import {
import { EditorMode } from "@/uhm/lib/editor/session/sessionTypes";
import {
getDefaultTypeIdForFeature,
normalizeFeatureBindingIds,
normalizeFeatureEntityIds,
uniqueEntityIds,
} from "@/uhm/lib/editor/snapshot/editorSnapshot";
import {
getDirectGeometryChildIds,
normalizeFeatureBoundWith,
wouldCreateGeometryBoundWithCycle,
} from "@/uhm/lib/editor/geometry/geometryBinding";
import {
buildClientEntityId,
mergeEntitySearchResults,
@@ -71,7 +75,7 @@ import {
isFeatureVisibleAtYear,
normalizeEntitiesForCompare,
normalizeEntityWikiLinksForCompare,
normalizeGeoSearchBindingIds,
normalizeGeoSearchBoundWith,
normalizeGeoSearchGeometry,
normalizeReplaysForCompare,
normalizeWikisForCompare,
@@ -630,11 +634,11 @@ function EditorPageContent() {
return rows;
}, [activeTimelineFilterEnabled, editor, mapRenderDraft.features]);
// Binding ids của geometry đại diện đang chọn.
const selectedGeometryBindingIds = useMemo(() => {
// Child ids bound to the selected geometry via child.properties.bound_with.
const selectedGeometryChildIds = useMemo(() => {
if (!selectedFeature) return [];
return normalizeFeatureBindingIds(selectedFeature);
}, [selectedFeature]);
return getDirectGeometryChildIds(editor.draft, selectedFeature.properties.id);
}, [editor.draft, selectedFeature]);
// Choices wiki dùng trong replay actions và binding panel.
const wikiChoices = useMemo(() => {
@@ -1317,7 +1321,6 @@ function EditorPageContent() {
type_key: "",
time_start: "",
time_end: "",
binding: "",
});
setEntityFormStatus(null);
lastSelectedFeatureIdRef.current = null;
@@ -1336,7 +1339,6 @@ function EditorPageContent() {
type_key: nextTypeKey,
time_start: timeStart != null ? String(timeStart) : "",
time_end: timeEnd != null ? String(timeEnd) : "",
binding: normalizeFeatureBindingIds(selectedFeature).join(", "),
});
// Only clear status when switching to a different geometry, not when patching metadata/bindings
// on the same selected geometry (otherwise messages will blink).
@@ -1518,58 +1520,55 @@ function EditorPageContent() {
setSelectedGeometryEntityIds,
]);
// Bind/unbind geometry id vào trường binding của selected geometry.
// Bind/unbind geometry con vào selected geometry qua field child.bound_with.
const handleToggleBindGeometryForSelectedGeometry = useCallback((geoId: string, nextChecked: boolean) => {
if (!selectedFeatures || selectedFeatures.length === 0) {
flashGeoBindingStatus("Chưa chọn geometry để bind.");
return;
}
if (selectedFeatures.length !== 1 || !selectedFeature) {
flashGeoBindingStatus("Chỉ bind geometry-geometry khi chọn đúng một geometry cha.");
return;
}
if (!isMultiEditValid) {
flashGeoBindingStatus("Không thể bind geometry cho nhiều geometry khác loại.");
return;
}
const id = String(geoId || "").trim();
if (!id) return;
if (selectedFeatures.some(f => String(f.properties.id) === id)) return;
const parentId = String(selectedFeature.properties.id);
if (parentId === id) return;
const childFeature = editor.draft.features.find((f) => String(f.properties.id) === id);
if (!childFeature) {
flashGeoBindingStatus("Không tìm thấy geometry con.");
return;
}
if (nextChecked && wouldCreateGeometryBoundWithCycle(editor.draft.features, id, parentId)) {
flashGeoBindingStatus("Không thể bind vì sẽ tạo vòng lặp bound_with.");
return;
}
setIsEntitySubmitting(true);
flashGeoBindingStatus(null, 0);
try {
const bindingPatches = selectedFeatures.map((feature) => {
const prevBindingIds = normalizeFeatureBindingIds(feature);
const has = prevBindingIds.includes(id);
const nextBindingIds = (() => {
if (nextChecked) {
if (has) return prevBindingIds;
return [...prevBindingIds, id];
}
if (!has) return prevBindingIds;
return prevBindingIds.filter((x) => x !== id);
})();
return {
id: feature.properties.id,
patch: { binding: nextBindingIds },
};
});
const currentParentId = normalizeFeatureBoundWith(childFeature);
const nextBoundWith = nextChecked
? parentId
: currentParentId === parentId
? null
: currentParentId;
editor.patchFeaturePropertiesBatch(
bindingPatches,
[{
id: childFeature.properties.id,
patch: { bound_with: nextBoundWith },
}],
nextChecked ? "Bind geometry vào GEO" : "Unbind geometry khỏi GEO"
);
// Assume selectedFeature (the first one) reflects the representative binding in UI
const firstFeaturePrevBindings = normalizeFeatureBindingIds(selectedFeatures[0]);
const firstFeatureHas = firstFeaturePrevBindings.includes(id);
const nextBindingIdsForUI = (() => {
if (nextChecked) return firstFeatureHas ? firstFeaturePrevBindings : [...firstFeaturePrevBindings, id];
return firstFeatureHas ? firstFeaturePrevBindings.filter(x => x !== id) : firstFeaturePrevBindings;
})();
setGeometryMetaForm((prev) => ({ ...prev, binding: nextBindingIdsForUI.join(", ") }));
flashGeoBindingStatus(
nextChecked
? "Đã bind geometry vào binding. Commit khi sẵn sàng."
: "Đã gỡ binding geometry. Commit khi sẵn sàng.",
? "Đã set bound_with cho geometry con. Commit khi sẵn sàng."
: "Đã gỡ bound_with khỏi geometry con. Commit khi sẵn sàng.",
3000
);
} finally {
@@ -1578,13 +1577,13 @@ function EditorPageContent() {
}, [
editor,
flashGeoBindingStatus,
selectedFeature,
selectedFeatures,
isMultiEditValid,
setGeometryMetaForm,
setIsEntitySubmitting,
]);
// Bind nhiều geometries vào target geometry.
// Bind nhiều geometries con vào target geometry.
const handleBindGeometries = useCallback((targetId: string | number, sourceIds: (string | number)[]) => {
const idStr = String(targetId).trim();
if (!idStr) return;
@@ -1595,23 +1594,27 @@ function EditorPageContent() {
return;
}
const prevBindingIds = normalizeFeatureBindingIds(targetFeature);
const sourceFeatures = sourceIds
.map((sourceId) => editor.draft.features.find((f) => String(f.properties.id) === String(sourceId)))
.filter((feature): feature is Feature => Boolean(feature))
.filter((feature) => String(feature.properties.id) !== idStr)
.filter((feature) => !wouldCreateGeometryBoundWithCycle(editor.draft.features, feature.properties.id, idStr));
// Merge prevBindingIds with sourceIds (which are strings of selected features)
// filter out targetId itself (we can't bind a geometry to itself)
const newSources = sourceIds.map(String).filter((x) => x !== idStr);
const merged = Array.from(new Set([...prevBindingIds, ...newSources]));
if (!sourceFeatures.length) {
flashGeoBindingStatus("Không có geometry con hợp lệ để bind.");
return;
}
editor.patchFeaturePropertiesBatch(
[{
id: targetFeature.properties.id,
patch: { binding: merged },
}],
sourceFeatures.map((feature) => ({
id: feature.properties.id,
patch: { bound_with: idStr },
})),
"Bind các geometry đã chọn vào GEO"
);
setSelectedFeatureIds([targetFeature.properties.id]);
flashGeoBindingStatus(`Đã bind ${newSources.length} geometry vào GEO này. Commit khi sẵn sàng.`, 3000);
flashGeoBindingStatus(`Đã set bound_with cho ${sourceFeatures.length} geometry con. Commit khi sẵn sàng.`, 3000);
}, [editor, flashGeoBindingStatus, setSelectedFeatureIds]);
// Focus/zoom tới geometry từ binding panel; nếu geo có time_start thì kéo year filter về năm đó.
@@ -1787,7 +1790,7 @@ function EditorPageContent() {
return;
}
const bindingIds = normalizeGeoSearchBindingIds(geo.binding);
const boundWith = normalizeGeoSearchBoundWith(geo.bound_with);
const typeKey = geo.type || null;
const feature: Feature = {
@@ -1797,7 +1800,7 @@ function EditorPageContent() {
type: typeKey,
time_start: normalizeTimelineYearValue(geo.time_start),
time_end: normalizeTimelineYearValue(geo.time_end),
binding: bindingIds.length ? bindingIds : undefined,
bound_with: boundWith,
entity_id: entityItem.entity_id,
entity_ids: [entityItem.entity_id],
entity_name: (entityItem.name || "").trim() || entityItem.entity_id,
@@ -2345,7 +2348,7 @@ function EditorPageContent() {
<GeometryBindingPanel
geometries={geometryChoices}
selectedGeometryId={selectedFeature ? String(selectedFeature.properties.id) : null}
selectedGeometryBindingIds={selectedGeometryBindingIds}
selectedGeometryChildIds={selectedGeometryChildIds}
onToggleBindGeometryForSelectedGeometry={handleToggleBindGeometryForSelectedGeometry}
onFocusGeometry={handleFocusGeometryFromBindingPanel}
/>