mirror of
https://github.com/flash555588/ai-model-workbench.git
synced 2026-07-22 06:56:38 +00:00
fix: prepare 0.1.5 release
This commit is contained in:
parent
6c6c632493
commit
c0629c7d17
11 changed files with 26005 additions and 71 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -1,7 +1,5 @@
|
|||
node_modules/
|
||||
main.js
|
||||
main.js.map
|
||||
data.json
|
||||
package-lock.json
|
||||
_pr_work/
|
||||
test-annotation.md
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ No test runner is configured. The only verification gate is `typecheck`.
|
|||
|
||||
## Architecture
|
||||
|
||||
Entry point: `src/main.ts` → esbuild bundles to `main.js` (CJS, ES2018). `main.js` + `manifest.json` + `styles.css` are deployed to the Obsidian vault's `.obsidian/plugins/ai-3d-model-workbench/`.
|
||||
Entry point: `src/main.ts` → esbuild bundles to `main.js` (CJS, ES2018). `main.js` + `manifest.json` + `styles.css` are deployed to the Obsidian vault's `.obsidian/plugins/ai-three-d-model-workbench/`.
|
||||
|
||||
### Layer Layout
|
||||
|
||||
|
|
|
|||
16
README.md
16
README.md
|
|
@ -69,7 +69,7 @@ npm run build
|
|||
3. Create this folder inside the vault:
|
||||
|
||||
```text
|
||||
<your-vault>/.obsidian/plugins/ai-3d-model-workbench/
|
||||
<your-vault>/.obsidian/plugins/ai-three-d-model-workbench/
|
||||
```
|
||||
|
||||
4. Copy `main.js`, `manifest.json`, and `styles.css` into that folder.
|
||||
|
|
@ -99,7 +99,7 @@ Use [Quick Start](#quick-start) if you only want the fastest setup.
|
|||
- This plugin folder inside the vault:
|
||||
|
||||
```text
|
||||
<vault>/.obsidian/plugins/ai-3d-model-workbench/
|
||||
<vault>/.obsidian/plugins/ai-three-d-model-workbench/
|
||||
```
|
||||
|
||||
All install methods place the same three files in that folder:
|
||||
|
|
@ -123,27 +123,27 @@ npm install
|
|||
npm run build
|
||||
```
|
||||
|
||||
2. Create `<vault>/.obsidian/plugins/ai-3d-model-workbench/` if it does not exist.
|
||||
2. Create `<vault>/.obsidian/plugins/ai-three-d-model-workbench/` if it does not exist.
|
||||
3. Copy `main.js`, `manifest.json`, and `styles.css` into that folder.
|
||||
4. In Obsidian, enable `AI 3D Model Workbench` in `Settings > Community Plugins`.
|
||||
|
||||
### Option B: Download a Release
|
||||
|
||||
1. Download `main.js`, `manifest.json`, and `styles.css` from [Releases](https://github.com/flash555588/ai-3d-model-workbench/releases).
|
||||
2. Create `<vault>/.obsidian/plugins/ai-3d-model-workbench/` if it does not exist.
|
||||
2. Create `<vault>/.obsidian/plugins/ai-three-d-model-workbench/` if it does not exist.
|
||||
3. Put the three files in that folder.
|
||||
4. In Obsidian, enable `AI 3D Model Workbench` in `Settings > Community Plugins`.
|
||||
|
||||
### Option C: Symlink for Development
|
||||
|
||||
1. Make sure `<vault>/.obsidian/plugins/` already exists.
|
||||
2. Create a symlink named `ai-3d-model-workbench` that points to this repository.
|
||||
2. Create a symlink named `ai-three-d-model-workbench` that points to this repository.
|
||||
|
||||
Windows (PowerShell):
|
||||
|
||||
```powershell
|
||||
New-Item -ItemType SymbolicLink `
|
||||
-Path "C:\path\to\your-vault\.obsidian\plugins\ai-3d-model-workbench" `
|
||||
-Path "C:\path\to\your-vault\.obsidian\plugins\ai-three-d-model-workbench" `
|
||||
-Target "C:\path\to\ai-3d-model-workbench"
|
||||
```
|
||||
|
||||
|
|
@ -151,7 +151,7 @@ macOS / Linux:
|
|||
|
||||
```bash
|
||||
ln -s /path/to/ai-3d-model-workbench \
|
||||
/path/to/your-vault/.obsidian/plugins/ai-3d-model-workbench
|
||||
/path/to/your-vault/.obsidian/plugins/ai-three-d-model-workbench
|
||||
```
|
||||
|
||||
3. In this repository, run `npm install` once if needed.
|
||||
|
|
@ -569,7 +569,7 @@ npm run typecheck # TypeScript type check
|
|||
### Build Output
|
||||
|
||||
```
|
||||
ai-3d-model-workbench/
|
||||
ai-three-d-model-workbench/
|
||||
├── main.js # 1.7 MB (minified, Babylon.js core)
|
||||
├── manifest.json # Plugin manifest
|
||||
├── styles.css # Plugin styles
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ npm run build
|
|||
3. 在该 vault 里创建这个文件夹:
|
||||
|
||||
```text
|
||||
<your-vault>/.obsidian/plugins/ai-3d-model-workbench/
|
||||
<your-vault>/.obsidian/plugins/ai-three-d-model-workbench/
|
||||
```
|
||||
|
||||
4. 把 `main.js`、`manifest.json`、`styles.css` 复制到这个文件夹里。
|
||||
|
|
@ -99,7 +99,7 @@ npm run build
|
|||
- vault 里的插件目录:
|
||||
|
||||
```text
|
||||
<vault>/.obsidian/plugins/ai-3d-model-workbench/
|
||||
<vault>/.obsidian/plugins/ai-three-d-model-workbench/
|
||||
```
|
||||
|
||||
无论用哪种方式安装,最终都要把下面这三个文件放进这个目录:
|
||||
|
|
@ -123,27 +123,27 @@ npm install
|
|||
npm run build
|
||||
```
|
||||
|
||||
2. 如果 `<vault>/.obsidian/plugins/ai-3d-model-workbench/` 还不存在,先创建它。
|
||||
2. 如果 `<vault>/.obsidian/plugins/ai-three-d-model-workbench/` 还不存在,先创建它。
|
||||
3. 把 `main.js`、`manifest.json`、`styles.css` 复制到这个文件夹里。
|
||||
4. 在 Obsidian 的“设置 > 社区插件”中启用 `AI 3D Model Workbench`。
|
||||
|
||||
### 方式 B:下载发布版
|
||||
|
||||
1. 从 [Releases](https://github.com/flash555588/ai-3d-model-workbench/releases) 下载 `main.js`、`manifest.json` 和 `styles.css`。
|
||||
2. 如果 `<vault>/.obsidian/plugins/ai-3d-model-workbench/` 还不存在,先创建它。
|
||||
2. 如果 `<vault>/.obsidian/plugins/ai-three-d-model-workbench/` 还不存在,先创建它。
|
||||
3. 把这三个文件放进这个文件夹里。
|
||||
4. 在 Obsidian 的“设置 > 社区插件”中启用 `AI 3D Model Workbench`。
|
||||
|
||||
### 方式 C:开发用符号链接
|
||||
|
||||
1. 先确认 `<vault>/.obsidian/plugins/` 已经存在。
|
||||
2. 创建一个名为 `ai-3d-model-workbench` 的符号链接,指向当前仓库。
|
||||
2. 创建一个名为 `ai-three-d-model-workbench` 的符号链接,指向当前仓库。
|
||||
|
||||
Windows(PowerShell):
|
||||
|
||||
```powershell
|
||||
New-Item -ItemType SymbolicLink `
|
||||
-Path "C:\path\to\your-vault\.obsidian\plugins\ai-3d-model-workbench" `
|
||||
-Path "C:\path\to\your-vault\.obsidian\plugins\ai-three-d-model-workbench" `
|
||||
-Target "C:\path\to\ai-3d-model-workbench"
|
||||
```
|
||||
|
||||
|
|
@ -151,7 +151,7 @@ macOS / Linux:
|
|||
|
||||
```bash
|
||||
ln -s /path/to/ai-3d-model-workbench \
|
||||
/path/to/your-vault/.obsidian/plugins/ai-3d-model-workbench
|
||||
/path/to/your-vault/.obsidian/plugins/ai-three-d-model-workbench
|
||||
```
|
||||
|
||||
3. 如果还没装依赖,先在当前仓库运行一次 `npm install`。
|
||||
|
|
@ -228,7 +228,7 @@ ln -s /path/to/ai-3d-model-workbench \
|
|||
```3d model.glb
|
||||
```
|
||||
````
|
||||
|
||||
ai-three-d-model-workbench/
|
||||
**完整配置** — 相机、灯光、场景、多模型:
|
||||
|
||||
````markdown
|
||||
|
|
|
|||
20578
main.js
Normal file
20578
main.js
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -1,9 +1,9 @@
|
|||
{
|
||||
"id": "ai-3d-model-workbench",
|
||||
"id": "ai-three-d-model-workbench",
|
||||
"name": "AI 3D Model Workbench",
|
||||
"version": "0.1.4",
|
||||
"version": "0.1.5",
|
||||
"minAppVersion": "1.5.0",
|
||||
"description": "Turn 3D models into linked knowledge assets.",
|
||||
"description": "Turn 3D models into linked knowledge assets.",
|
||||
"author": "flash",
|
||||
"authorUrl": "https://github.com/flash555588",
|
||||
"isDesktopOnly": false
|
||||
|
|
|
|||
5258
package-lock.json
generated
Normal file
5258
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1,8 +1,8 @@
|
|||
{
|
||||
"name": "obsidian-ai-3d-model-workbench",
|
||||
"version": "0.1.4",
|
||||
"version": "0.1.5",
|
||||
"private": true,
|
||||
"description": "Obsidian plugin for turning 3D models into linked knowledge assets.",
|
||||
"description": "Obsidian plugin for turning 3D models into linked knowledge assets.",
|
||||
"scripts": {
|
||||
"dev": "node esbuild.config.mjs",
|
||||
"build": "node esbuild.config.mjs production",
|
||||
|
|
|
|||
162
src/main.ts
162
src/main.ts
|
|
@ -123,8 +123,12 @@ export default class AI3DModelWorkbench extends Plugin {
|
|||
".cm-header-4", ".cm-header-5", ".cm-header-6",
|
||||
].join(", ");
|
||||
|
||||
// Track bound elements for cleanup
|
||||
const boundEntries: { el: Element; handler: () => void }[] = [];
|
||||
type BoundHeadingEntry = {
|
||||
badge: HTMLSpanElement;
|
||||
handler: () => void;
|
||||
signature: string;
|
||||
};
|
||||
const boundEntries = new Map<Element, BoundHeadingEntry>();
|
||||
|
||||
// Build headingRef → [{ pinId, modelPath, color }] map from stored annotations.
|
||||
// Supports multiple pins/models per heading.
|
||||
|
|
@ -146,8 +150,10 @@ export default class AI3DModelWorkbench extends Plugin {
|
|||
for (const [modelPath, profile] of Object.entries(profiles)) {
|
||||
for (const pin of profile.annotations) {
|
||||
if (pin.headingRef && pin.id) {
|
||||
let arr = map.get(pin.headingRef);
|
||||
if (!arr) { arr = []; map.set(pin.headingRef, arr); }
|
||||
const headingKey = normalizeHeadingText(pin.headingRef);
|
||||
if (!headingKey) continue;
|
||||
let arr = map.get(headingKey);
|
||||
if (!arr) { arr = []; map.set(headingKey, arr); }
|
||||
arr.push({ pinId: pin.id, modelPath, color: pin.color });
|
||||
}
|
||||
}
|
||||
|
|
@ -155,24 +161,51 @@ export default class AI3DModelWorkbench extends Plugin {
|
|||
return map;
|
||||
};
|
||||
|
||||
// Normalize all headingRef keys for robust matching
|
||||
const normalizedMap = new Map<string, PinEntry[]>();
|
||||
const buildNormalizedMap = (headingMap: Map<string, PinEntry[]>): void => {
|
||||
normalizedMap.clear();
|
||||
for (const [key, entries] of headingMap) {
|
||||
normalizedMap.set(normalizeHeadingText(key), entries);
|
||||
}
|
||||
const buildEntriesSignature = (entries: PinEntry[]): string => entries
|
||||
.map((entry) => `${entry.pinId}:${entry.modelPath}:${entry.color}`)
|
||||
.sort()
|
||||
.join("|");
|
||||
|
||||
const buildHeadingMapSignature = (headingMap: Map<string, PinEntry[]>): string => Array
|
||||
.from(headingMap.entries())
|
||||
.map(([key, entries]) => `${key}=>${buildEntriesSignature(entries)}`)
|
||||
.sort()
|
||||
.join("||");
|
||||
|
||||
const getHeadingText = (el: Element): string => normalizeHeadingText(
|
||||
Array.from(el.childNodes)
|
||||
.map((node) => {
|
||||
if (node.instanceOf(Element) && node.classList.contains("ai3d-heading-pin-badge")) {
|
||||
return "";
|
||||
}
|
||||
return node.textContent ?? "";
|
||||
})
|
||||
.join(" "),
|
||||
);
|
||||
|
||||
const unbindHeading = (el: Element): void => {
|
||||
const existing = boundEntries.get(el);
|
||||
if (!existing) return;
|
||||
el.removeEventListener("mouseover", existing.handler);
|
||||
existing.badge.remove();
|
||||
delete (el as HTMLElement).dataset.pinBound;
|
||||
boundEntries.delete(el);
|
||||
};
|
||||
|
||||
const bindHeading = (el: Element) => {
|
||||
if ((el as HTMLElement).dataset.pinBound) return;
|
||||
const bindHeading = (el: Element, entries: PinEntry[]): void => {
|
||||
if (entries.length === 0) {
|
||||
unbindHeading(el);
|
||||
return;
|
||||
}
|
||||
|
||||
const text = el.textContent ?? "";
|
||||
const headingText = normalizeHeadingText(text);
|
||||
const entries = normalizedMap.get(headingText);
|
||||
const signature = buildEntriesSignature(entries);
|
||||
const existing = boundEntries.get(el);
|
||||
if (existing?.signature === signature) return;
|
||||
if (existing) {
|
||||
unbindHeading(el);
|
||||
}
|
||||
|
||||
if (!entries || entries.length === 0) return;
|
||||
(el as HTMLElement).dataset.pinBound = entries[0].pinId;
|
||||
(el as HTMLElement).dataset.pinBound = signature;
|
||||
|
||||
// Add pin badge: shows count if multiple, tooltip lists model sources
|
||||
// Create on heading element (in DOM) to inherit Obsidian CSS variables
|
||||
|
|
@ -216,32 +249,72 @@ export default class AI3DModelWorkbench extends Plugin {
|
|||
}
|
||||
};
|
||||
el.addEventListener("mouseover", handler);
|
||||
boundEntries.push({ el, handler });
|
||||
boundEntries.set(el, { badge, handler, signature });
|
||||
};
|
||||
|
||||
const processHeadings = (container: Element) => {
|
||||
const syncHeadingElement = (el: Element, headingMap: Map<string, PinEntry[]>): void => {
|
||||
const headingText = getHeadingText(el);
|
||||
bindHeading(el, headingMap.get(headingText) ?? []);
|
||||
};
|
||||
|
||||
const reconcileBoundHeadings = (headingMap: Map<string, PinEntry[]>): void => {
|
||||
for (const [el, entry] of Array.from(boundEntries.entries())) {
|
||||
if (!el.isConnected) {
|
||||
unbindHeading(el);
|
||||
continue;
|
||||
}
|
||||
const nextEntries = headingMap.get(getHeadingText(el)) ?? [];
|
||||
const nextSignature = buildEntriesSignature(nextEntries);
|
||||
if (nextEntries.length === 0 || entry.signature !== nextSignature) {
|
||||
bindHeading(el, nextEntries);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const processHeadings = (container: Element, headingMap: Map<string, PinEntry[]>): void => {
|
||||
container.querySelectorAll(headingSelector).forEach((el) => syncHeadingElement(el, headingMap));
|
||||
};
|
||||
|
||||
const scanAll = (): void => {
|
||||
const headingMap = buildHeadingMap();
|
||||
if (headingMap.size === 0) return;
|
||||
buildNormalizedMap(headingMap);
|
||||
container.querySelectorAll(headingSelector).forEach((el) => bindHeading(el));
|
||||
reconcileBoundHeadings(headingMap);
|
||||
const containers = activeDocument.querySelectorAll(markdownContainerSelector);
|
||||
containers.forEach((container) => processHeadings(container, headingMap));
|
||||
};
|
||||
|
||||
const scanAll = () => {
|
||||
const containers = activeDocument.querySelectorAll(markdownContainerSelector);
|
||||
containers.forEach(processHeadings);
|
||||
let lastHeadingMapSignature = buildHeadingMapSignature(buildHeadingMap());
|
||||
let scanTimer = 0;
|
||||
const scheduleScan = (delay = 0): void => {
|
||||
if (scanTimer) {
|
||||
activeWindow.clearTimeout(scanTimer);
|
||||
}
|
||||
scanTimer = activeWindow.setTimeout(() => {
|
||||
scanTimer = 0;
|
||||
scanAll();
|
||||
}, delay);
|
||||
};
|
||||
|
||||
const unsubscribeStore = this.ps.store.subscribe(() => {
|
||||
const nextHeadingMap = buildHeadingMap();
|
||||
const nextSignature = buildHeadingMapSignature(nextHeadingMap);
|
||||
if (nextSignature === lastHeadingMapSignature) return;
|
||||
lastHeadingMapSignature = nextSignature;
|
||||
scheduleScan();
|
||||
});
|
||||
|
||||
this.registerEvent(this.app.workspace.on("layout-change", () => {
|
||||
activeWindow.setTimeout(scanAll, 200);
|
||||
scheduleScan(200);
|
||||
}));
|
||||
|
||||
// Debounced MutationObserver: coalesce rapid DOM changes into a single scan
|
||||
const isRelevantAddedNode = (node: HTMLElement): boolean => {
|
||||
if (!node.isConnected) return false;
|
||||
const matchesRelevantNode = (node: HTMLElement): boolean => {
|
||||
if (node.matches(markdownContainerSelector) || node.matches(headingSelector)) return true;
|
||||
return !!node.querySelector(markdownContainerSelector) || !!node.querySelector(headingSelector);
|
||||
};
|
||||
|
||||
const isRelevantAddedNode = (node: HTMLElement): boolean => node.isConnected && matchesRelevantNode(node);
|
||||
const isRelevantRemovedNode = (node: HTMLElement): boolean => matchesRelevantNode(node);
|
||||
|
||||
let pendingNodes = new Set<HTMLElement>();
|
||||
let debounceTimer = 0;
|
||||
const flushPending = () => {
|
||||
|
|
@ -249,24 +322,35 @@ export default class AI3DModelWorkbench extends Plugin {
|
|||
pendingNodes.clear();
|
||||
debounceTimer = 0;
|
||||
const headingMap = buildHeadingMap();
|
||||
if (headingMap.size === 0) return;
|
||||
buildNormalizedMap(headingMap);
|
||||
reconcileBoundHeadings(headingMap);
|
||||
for (const node of nodes) {
|
||||
if (!node.isConnected) continue;
|
||||
if (node.matches?.(headingSelector)) bindHeading(node);
|
||||
node.querySelectorAll?.(headingSelector)?.forEach((el: Element) => bindHeading(el));
|
||||
if (node.matches?.(headingSelector)) syncHeadingElement(node, headingMap);
|
||||
node.querySelectorAll?.(headingSelector)?.forEach((el: Element) => syncHeadingElement(el, headingMap));
|
||||
if (node.matches?.(markdownContainerSelector)) {
|
||||
processHeadings(node, headingMap);
|
||||
}
|
||||
node.querySelectorAll?.(markdownContainerSelector)?.forEach((el: Element) => processHeadings(el, headingMap));
|
||||
}
|
||||
lastHeadingMapSignature = buildHeadingMapSignature(headingMap);
|
||||
};
|
||||
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
let shouldFlush = false;
|
||||
for (const m of mutations) {
|
||||
for (const node of Array.from(m.addedNodes)) {
|
||||
if (!node.instanceOf(HTMLElement)) continue;
|
||||
if (!isRelevantAddedNode(node)) continue;
|
||||
pendingNodes.add(node);
|
||||
shouldFlush = true;
|
||||
}
|
||||
for (const node of Array.from(m.removedNodes)) {
|
||||
if (!node.instanceOf(HTMLElement)) continue;
|
||||
if (!isRelevantRemovedNode(node)) continue;
|
||||
shouldFlush = true;
|
||||
}
|
||||
}
|
||||
if (pendingNodes.size > 0 && !debounceTimer) {
|
||||
if (shouldFlush && !debounceTimer) {
|
||||
debounceTimer = activeWindow.setTimeout(flushPending, 100);
|
||||
}
|
||||
});
|
||||
|
|
@ -274,17 +358,17 @@ export default class AI3DModelWorkbench extends Plugin {
|
|||
|
||||
// Cleanup: disconnect observer, remove all heading listeners
|
||||
this.register(() => {
|
||||
unsubscribeStore();
|
||||
observer.disconnect();
|
||||
if (debounceTimer) { activeWindow.clearTimeout(debounceTimer); debounceTimer = 0; }
|
||||
for (const { el, handler } of boundEntries) {
|
||||
el.removeEventListener("mouseover", handler);
|
||||
el.removeEventListener("click", handler);
|
||||
if (scanTimer) { activeWindow.clearTimeout(scanTimer); scanTimer = 0; }
|
||||
for (const el of Array.from(boundEntries.keys())) {
|
||||
unbindHeading(el);
|
||||
}
|
||||
boundEntries.length = 0;
|
||||
});
|
||||
|
||||
// Initial scan
|
||||
activeWindow.setTimeout(scanAll, 500);
|
||||
scheduleScan(500);
|
||||
}
|
||||
|
||||
private async activateView() {
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@ export class AnnotationManager {
|
|||
private static readonly _scratchRay = new Ray(Vector3.Zero(), Vector3.Zero(), 1);
|
||||
private hoverPopover: HTMLDivElement | null = null;
|
||||
private hoverTimeout: ReturnType<typeof activeWindow.setTimeout> | null = null;
|
||||
private hoverRequestId = 0;
|
||||
private _highlightHandler: ((e: Event) => void) | null = null;
|
||||
private _pulseTimeout: ReturnType<typeof activeWindow.setTimeout> | null = null;
|
||||
private _headingDropdown: HTMLDivElement | null = null;
|
||||
|
|
@ -471,22 +472,31 @@ export class AnnotationManager {
|
|||
await this.renderPreviewContent(el, content, notePath, "editor");
|
||||
}
|
||||
|
||||
private async showHoverPopover(pinEl: HTMLDivElement, pin: AnnotationPin): Promise<void> {
|
||||
private async showHoverPopover(requestId: number, pinEl: HTMLDivElement, pin: AnnotationPin): Promise<void> {
|
||||
if (!this.noteReader || !pin.notePath || !pin.headingRef) return;
|
||||
this.hideHoverPopover();
|
||||
|
||||
const content = await this.noteReader(pin.notePath, pin.headingRef);
|
||||
if (!content) return;
|
||||
if (!content || requestId !== this.hoverRequestId || !pinEl.isConnected || !this.hostEl.isConnected) return;
|
||||
|
||||
// Create on hostEl (in DOM) to inherit Obsidian CSS variables
|
||||
const popover = this.hostEl.createDiv({ cls: "ai3d-pin-popover" });
|
||||
const popover = activeDocument.createElement("div");
|
||||
popover.className = "ai3d-pin-popover";
|
||||
|
||||
const title = popover.createDiv({ cls: "ai3d-pin-popover-title" });
|
||||
const title = activeDocument.createElement("div");
|
||||
title.className = "ai3d-pin-popover-title";
|
||||
title.textContent = pin.headingRef;
|
||||
popover.appendChild(title);
|
||||
|
||||
const body = popover.createDiv({ cls: "ai3d-pin-popover-body" });
|
||||
const body = activeDocument.createElement("div");
|
||||
body.className = "ai3d-pin-popover-body";
|
||||
popover.appendChild(body);
|
||||
await this.renderPreviewContent(body, content, pin.notePath, "popover");
|
||||
|
||||
if (requestId !== this.hoverRequestId || !pinEl.isConnected || !this.hostEl.isConnected) {
|
||||
this.clearRenderedPreview(body);
|
||||
return;
|
||||
}
|
||||
|
||||
// Position relative to pin
|
||||
const rect = pinEl.getBoundingClientRect();
|
||||
popover.style.setProperty("--popover-left", `${rect.left + rect.width / 2}px`);
|
||||
|
|
@ -576,6 +586,7 @@ export class AnnotationManager {
|
|||
}
|
||||
|
||||
destroy(): void {
|
||||
this.hoverRequestId++;
|
||||
this.hideHoverPopover();
|
||||
if (this._pulseTimeout) { activeWindow.clearTimeout(this._pulseTimeout); this._pulseTimeout = null; }
|
||||
if (this._headingDebounce) { activeWindow.clearTimeout(this._headingDebounce); this._headingDebounce = null; }
|
||||
|
|
@ -629,12 +640,17 @@ export class AnnotationManager {
|
|||
// Hover popover for linked notes
|
||||
if (pin.notePath && pin.headingRef && this.noteReader) {
|
||||
el.addEventListener("mouseenter", () => {
|
||||
if (this.hoverTimeout) { activeWindow.clearTimeout(this.hoverTimeout); }
|
||||
const requestId = ++this.hoverRequestId;
|
||||
this.hoverTimeout = activeWindow.setTimeout(() => {
|
||||
void this.showHoverPopover(el, pin);
|
||||
this.hoverTimeout = null;
|
||||
if (requestId !== this.hoverRequestId) return;
|
||||
void this.showHoverPopover(requestId, el, pin);
|
||||
}, 300);
|
||||
});
|
||||
el.addEventListener("mouseleave", () => {
|
||||
if (this.hoverTimeout) { activeWindow.clearTimeout(this.hoverTimeout); this.hoverTimeout = null; }
|
||||
this.hoverRequestId++;
|
||||
this.hideHoverPopover();
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
{"0.0.1":"1.5.0","0.0.2":"1.5.0","0.0.3":"1.5.0","0.0.4":"1.5.0","0.0.5-beta.0":"1.5.0","0.1.1":"1.5.0","0.1.2":"1.5.0","0.1.4":"1.5.0"}
|
||||
{"0.0.1":"1.5.0","0.0.2":"1.5.0","0.0.3":"1.5.0","0.0.4":"1.5.0","0.0.5-beta.0":"1.5.0","0.1.1":"1.5.0","0.1.2":"1.5.0","0.1.4":"1.5.0","0.1.5":"1.5.0"}
|
||||
|
|
|
|||
Loading…
Reference in a new issue