Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 78 additions & 50 deletions etc/js/components/widgets/inspector/entity-inspector-module.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,56 +6,50 @@
</template>
<template v-slot:detail>
<!-- Components -->
<template v-for="elem in item.value.components">
<template v-for="elem in filtered.components" :key="elem.fullName">
<entity-inspector-component
:conn="conn"
:entity="entity"
:name="elem.name"
:fullName="elem.fullName"
:key="elem.fullName"
icon_src="symbol-field"
:value="elem.value"
:type="type_info[elem.fullName]"
:base="elem.base"
v-model:loading="loading"
@selectEntity="onSelectEntity"
@removeComponent="emit('removeComponent', elem.fullName)"
v-if="matchesFilter(elem)">
@removeComponent="emit('removeComponent', elem.fullName)">
</entity-inspector-component>
</template>

<!-- Pairs -->
<template v-for="elem in item.value.pairs">
<template v-for="elem in filtered.pairs" :key="elem.fullName">
<entity-inspector-component
:conn="conn"
:entity="entity"
:name="elem.name"
:fullName="elem.fullName"
:key="elem.fullName"
icon_src="symbol-interface"
:targets="elem.value"
:base="elem.base"
v-model:loading="loading"
@selectEntity="onSelectEntity"
@removeComponent="emit('removeComponent', elem.fullName)"
v-if="matchesFilter(elem)">
@removeComponent="emit('removeComponent', elem.fullName)">
</entity-inspector-component>
</template>

<!-- Tags -->
<template v-for="elem in item.value.tags">
<template v-for="elem in filtered.tags" :key="elem.fullName">
<entity-inspector-component
:conn="conn"
:entity="entity"
:name="elem.name"
:fullName="elem.fullName"
:key="elem.fullName"
:base="elem.base"
icon_src="tag"
v-model:loading="loading"
@selectEntity="onSelectEntity"
@removeComponent="emit('removeComponent', elem.fullName)"
v-if="matchesFilter(elem)">
@removeComponent="emit('removeComponent', elem.fullName)">
</entity-inspector-component>
</template>
</template>
Expand Down Expand Up @@ -93,57 +87,91 @@ function onSelectEntity(evt) {
emit("selectEntity", evt);
}

function matchesFilter(elem) {
if (!props.filter) {
return true;
}
return elem.fullName.includes(props.filter);
function maxDist(len) {
if (len <= 2) return 0;
if (len <= 4) return 1;
if (len <= 8) return 2;
return Math.floor(len * 0.3);
}

const matchCount = computed(() => {
const item = props.item.value;
if (!props.filter) {
let count = 0;
if (item.components) {
count += Object.keys(item.components).length;
}
if (item.pairs) {
count += Object.keys(item.pairs).length;
// Efficient thresholded Levenshtein (Ukkonen's banded algorithm)
function levWithin(a, b, limit) {
const n = a.length, m = b.length;
if (Math.abs(n - m) > limit) return limit + 1;
const prev = new Array(m + 1);
for (let j = 0; j <= m; j++) prev[j] = j;
const curr = new Array(m + 1);

for (let i = 1; i <= n; i++) {
const lo = Math.max(1, i - limit);
const hi = Math.min(m, i + limit);
curr[0] = i;
let rowBest = Infinity;

for (let j = lo; j <= hi; j++) {
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
const del = prev[j] + 1;
const ins = curr[j - 1] + 1;
const sub = prev[j - 1] + cost;
const v = Math.min(del, ins, sub);
curr[j] = v;
if (v < rowBest) rowBest = v;
}
if (item.tags) {
count += Object.keys(item.tags).length;
}
return count;
}

let count = 0;
if (item.components) {
for (let elem of Object.values(item.components)) {
if (matchesFilter(elem)) {
count++;
}
}
if (rowBest > limit) return limit + 1;
for (let j = 0; j <= m; j++) prev[j] = curr[j] ?? Infinity;
}

if (item.pairs) {
for (let elem of Object.values(item.pairs)) {
if (matchesFilter(elem)) {
count++;
}
}
return prev[m] ?? limit + 1;
}

// Core fuzzy matching
function fuzzy(text, query) {
if (!query) return true;
if (text.includes(query)) return true;

const limit = maxDist(query.length);
const qLen = query.length;
const minLen = Math.max(1, qLen - limit);
const maxLen = qLen + limit;

// Token heuristic
for (const t of text.split(/[^a-z0-9]+/)) {
if (!t) continue;
if (t.includes(query) || levWithin(t, query, limit) <= limit) return true;
}

if (item.tags) {
for (let elem of Object.values(item.tags)) {
if (matchesFilter(elem)) {
count++;
}
// Sliding windows
for (let len = minLen; len <= Math.min(maxLen, text.length); len++) {
for (let i = 0; i + len <= text.length; i++) {
if (levWithin(text.slice(i, i + len), query, limit) <= limit) return true;
}
}

return count;
return false;
}

const normalizedFilter = computed(() => (props.filter || "").trim().toLowerCase());

const filtered = computed(() => {
const f = normalizedFilter.value;
const norm = s => (s ?? "").toLowerCase();
const keep = e => !f || norm(e.fullName).includes(f) || fuzzy(norm(e.fullName), f);

const toArr = x => Array.isArray(x) ? x : Object.values(x || {});
return {
components: toArr(props.item.value.components).filter(keep),
pairs: toArr(props.item.value.pairs).filter(keep),
tags: toArr(props.item.value.tags).filter(keep),
};
});

const matchCount = computed(() =>
filtered.value.components.length +
filtered.value.pairs.length +
filtered.value.tags.length
);

</script>

<style scoped>
Expand Down