Orientation widget
diff --git a/webviews/viewer/src/components/sidebar/ActionButtons.svelte b/webviews/viewer/src/components/sidebar/ActionButtons.svelte
new file mode 100644
index 0000000..f76723a
--- /dev/null
+++ b/webviews/viewer/src/components/sidebar/ActionButtons.svelte
@@ -0,0 +1,44 @@
+
+
+
diff --git a/webviews/viewer/src/components/sidebar/GroupButton.svelte b/webviews/viewer/src/components/sidebar/GroupButton.svelte
new file mode 100644
index 0000000..6c4a82e
--- /dev/null
+++ b/webviews/viewer/src/components/sidebar/GroupButton.svelte
@@ -0,0 +1,53 @@
+
+
+{#if !isHidden}
+
+
+ {#if isFace}
+
+ {:else}
+
+ {/if}
+
+ {groupName}
+
+{/if}
diff --git a/webviews/viewer/src/components/sidebar/ObjectSection.svelte b/webviews/viewer/src/components/sidebar/ObjectSection.svelte
new file mode 100644
index 0000000..cc78274
--- /dev/null
+++ b/webviews/viewer/src/components/sidebar/ObjectSection.svelte
@@ -0,0 +1,142 @@
+
+
+{#if contextMenu}
+
{
+ e.preventDefault();
+ closeContextMenu();
+ }}
+ role="presentation"
+ >
+
+
+ Hide all other objects
+
+
+{/if}
+
+
+
+ e.key === 'Enter' && toggleCollapsed()}
+ role="button"
+ tabindex="0"
+ >
+ {objectName}
+
+
+ {#if isHidden}
+
+ {:else}
+
+ {/if}
+
+
+
+{#if isHidden}
+
+ {groupCount} groups
+
+{:else if !collapsed}
+
+ {#each faces as groupName}
+
+ {/each}
+ {#each nodes as groupName}
+
+ {/each}
+ {#if hiddenGroupCount > 0}
+
+ {hiddenGroupCount} hidden
+
+ {/if}
+
+{:else}
+
+ {groupCount} groups
+
+{/if}
diff --git a/webviews/viewer/src/components/ui/Dropdown.svelte b/webviews/viewer/src/components/ui/Dropdown.svelte
new file mode 100644
index 0000000..8a79d62
--- /dev/null
+++ b/webviews/viewer/src/components/ui/Dropdown.svelte
@@ -0,0 +1,92 @@
+
+
+
(open = false)} />
+
+
+ {@render children()}
+
+
+{#if open}
+ e.stopPropagation()}
+ onkeydown={(e) => e.stopPropagation()}
+ role="menu"
+ tabindex="-1"
+ >
+ {#each options as { value: val, label }}
+ {@const isSelected = val === value}
+
select(val)}
+ onkeydown={(e) => (e.key === 'Enter' || e.key === ' ') && select(val)}
+ >
+ {#if value !== null}
+ ✓
+ {/if}
+ {label}
+
+ {/each}
+
+{/if}
diff --git a/webviews/viewer/src/components/ui/Toggle.svelte b/webviews/viewer/src/components/ui/Toggle.svelte
new file mode 100644
index 0000000..97560bf
--- /dev/null
+++ b/webviews/viewer/src/components/ui/Toggle.svelte
@@ -0,0 +1,42 @@
+
+
+
+
+
diff --git a/webviews/viewer/src/components/viewer/AxisButtons.svelte b/webviews/viewer/src/components/viewer/AxisButtons.svelte
new file mode 100644
index 0000000..f9629f0
--- /dev/null
+++ b/webviews/viewer/src/components/viewer/AxisButtons.svelte
@@ -0,0 +1,27 @@
+
+
+
+ CameraManager.Instance.setCameraAxis('x')}
+ >
+ X
+
+ CameraManager.Instance.setCameraAxis('y')}
+ >
+ Y
+
+ CameraManager.Instance.setCameraAxis('z')}
+ >
+ Z
+
+
diff --git a/webviews/viewer/src/components/viewer/ZoomWidget.svelte b/webviews/viewer/src/components/viewer/ZoomWidget.svelte
new file mode 100644
index 0000000..0206a30
--- /dev/null
+++ b/webviews/viewer/src/components/viewer/ZoomWidget.svelte
@@ -0,0 +1,55 @@
+
+
+
diff --git a/webviews/viewer/src/lib/Controller.ts b/webviews/viewer/src/lib/Controller.ts
index fe7c124..22daf27 100644
--- a/webviews/viewer/src/lib/Controller.ts
+++ b/webviews/viewer/src/lib/Controller.ts
@@ -2,7 +2,7 @@ import { VtkApp } from './core/VtkApp';
import { CreateGroups } from './data/CreateGroups';
import { VisibilityManager } from './commands/VisibilityManager';
import { CameraManager } from './interaction/CameraManager';
-import { groupHierarchy as groupHierarchyStore } from './state';
+import { groupHierarchy as groupHierarchyStore, loadingProgress, loadingMessage } from './state';
import type { Group } from './data/Group';
export class Controller {
@@ -33,20 +33,41 @@ export class Controller {
return this._vsCodeApi;
}
- loadFiles(fileContexts: string[], fileNames: string[]): void {
+ async loadFiles(fileContexts: string[], fileNames: string[]): Promise {
+ if (this._groups) {
+ return;
+ }
+ loadingProgress.set(0);
+ loadingMessage.set('');
const lfr = new CreateGroups(fileContexts, fileNames);
- lfr.do();
+ await lfr.do(
+ (progress) => loadingProgress.set(progress),
+ (message) => loadingMessage.set(message)
+ );
this._vsCodeApi.postMessage({
type: 'groups',
groupList: this.getGroupNames(),
+ objectList: this.getObjectNames(),
});
}
saveGroups(groups: Record, groupHierarchy: Record): void {
this._groups = groups;
- this._groupHierarchy = groupHierarchy;
- groupHierarchyStore.set(groupHierarchy);
+ const naturalSort = (a: string, b: string) =>
+ a.localeCompare(b, undefined, { numeric: true, sensitivity: 'base' });
+
+ const sortedHierarchy: Record = {};
+ for (const key of Object.keys(groupHierarchy).sort(naturalSort)) {
+ sortedHierarchy[key] = {
+ ...groupHierarchy[key],
+ faces: [...groupHierarchy[key].faces].sort(naturalSort),
+ nodes: [...groupHierarchy[key].nodes].sort(naturalSort),
+ };
+ }
+
+ this._groupHierarchy = sortedHierarchy;
+ groupHierarchyStore.set(sortedHierarchy);
this.initManagers();
@@ -75,4 +96,11 @@ export class Controller {
}
return Object.keys(this._groups).filter((key) => key.includes('::'));
}
+
+ getObjectNames(): string[] {
+ if (!this._groupHierarchy) {
+ return [];
+ }
+ return Object.keys(this._groupHierarchy);
+ }
}
diff --git a/webviews/viewer/src/lib/commands/VisibilityManager.ts b/webviews/viewer/src/lib/commands/VisibilityManager.ts
index afb7f3d..39c0cb9 100644
--- a/webviews/viewer/src/lib/commands/VisibilityManager.ts
+++ b/webviews/viewer/src/lib/commands/VisibilityManager.ts
@@ -156,6 +156,19 @@ export class VisibilityManager {
VtkApp.Instance.getRenderWindow().render();
}
+ showOnlyObjects(objects: string[]): void {
+ const objectSet = new Set(objects);
+ for (const key in this.hiddenObjects) {
+ const shouldBeVisible = objects.length === 0 || objectSet.has(key);
+ const isCurrentlyHidden = this.hiddenObjects[key];
+ if (shouldBeVisible && isCurrentlyHidden) {
+ this.toggleObjectVisibility(key);
+ } else if (!shouldBeVisible && !isCurrentlyHidden) {
+ this.toggleObjectVisibility(key);
+ }
+ }
+ }
+
hideAllOthers(object: string): void {
for (const key in this.hiddenObjects) {
if (key === object) continue;
diff --git a/webviews/viewer/src/lib/data/CreateGroups.ts b/webviews/viewer/src/lib/data/CreateGroups.ts
index 9b51c7c..a30e108 100644
--- a/webviews/viewer/src/lib/data/CreateGroups.ts
+++ b/webviews/viewer/src/lib/data/CreateGroups.ts
@@ -16,8 +16,16 @@ export class CreateGroups {
this.fileNames = fileNames;
}
- do(): void {
- const result = ObjLoader.loadFiles(this.fileContexts, this.fileNames);
+ async do(
+ onProgress: (progress: number) => void,
+ onMessage: (message: string) => void
+ ): Promise {
+ const result = await ObjLoader.loadFiles(
+ this.fileContexts,
+ this.fileNames,
+ onProgress,
+ onMessage
+ );
const post = (text: string) => {
Controller.Instance.getVSCodeAPI().postMessage({ type: 'debugPanel', text });
};
@@ -40,7 +48,12 @@ export class CreateGroups {
const faceActorCreator = new FaceActorCreator(vertices, cells, cellIndexToGroup);
const nodeActorCreator = new NodeActorCreator(vertices, nodes, nodeIndexToGroup);
- for (const fileGroup in groupHierarchy) {
+ const groupKeys = Object.keys(groupHierarchy);
+ const yield_ = () => new Promise((r) => setTimeout(r, 0));
+
+ onMessage('Building scene...');
+ for (let gi = 0; gi < groupKeys.length; gi++) {
+ const fileGroup = groupKeys[gi];
const groupId = faceGroups.indexOf(fileGroup);
const _oc = GlobalSettings.Instance.objectColors;
@@ -104,6 +117,9 @@ export class CreateGroups {
);
this.groups[`${fileGroup}::${nodeGroup}::node`] = subGroup;
}
+
+ onProgress(0.9 + ((gi + 1) / groupKeys.length) * 0.1);
+ await yield_();
}
VtkApp.Instance.getRenderer().resetCamera();
diff --git a/webviews/viewer/src/lib/data/ObjLoader.ts b/webviews/viewer/src/lib/data/ObjLoader.ts
index ee3d42f..866fb4b 100644
--- a/webviews/viewer/src/lib/data/ObjLoader.ts
+++ b/webviews/viewer/src/lib/data/ObjLoader.ts
@@ -11,8 +11,15 @@ export interface ObjLoaderResult {
groupHierarchy: Record;
}
+const YIELD_EVERY_LINES = 5_000;
+
export class ObjLoader {
- static loadFiles(fileContexts: string[], fileNames: string[]): ObjLoaderResult {
+ static async loadFiles(
+ fileContexts: string[],
+ fileNames: string[],
+ onProgress: (progress: number) => void,
+ onMessage: (message: string) => void
+ ): Promise {
const vertices: { x: number; y: number; z: number }[] = [];
const cells: number[][] = [];
const cellIndexToGroup: number[] = [];
@@ -26,6 +33,8 @@ export class ObjLoader {
let groupId = -1;
let nodeGroupId = -1;
+ const yield_ = () => new Promise((r) => setTimeout(r, 0));
+
for (let i = 0; i < fileContexts.length; i++) {
try {
groupId++;
@@ -35,9 +44,17 @@ export class ObjLoader {
faceGroups.push(skinName);
nbVertices = vertices.length;
+ onMessage(`Parsing ${fileNames[i]}...`);
const lines = fileContexts[i].split('\n').map((l) => l.replace('\r', ''));
+ const totalLines = lines.length;
+
+ for (let lineIdx = 0; lineIdx < totalLines; lineIdx++) {
+ if (lineIdx % YIELD_EVERY_LINES === 0 && lineIdx > 0) {
+ const fileProgress = (i + lineIdx / totalLines) / fileContexts.length;
+ onProgress(fileProgress * 0.9);
+ await yield_();
+ }
- for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
const line = lines[lineIdx];
const ss = line.split(' ').filter((p) => p.length !== 0);
if (ss.length === 0) {
@@ -84,6 +101,9 @@ export class ObjLoader {
}
}
}
+
+ onProgress(((i + 1) / fileContexts.length) * 0.9);
+ await yield_();
} catch (fileError: any) {
Controller.Instance.getVSCodeAPI().postMessage({
type: 'debugPanel',
diff --git a/webviews/viewer/src/lib/state.ts b/webviews/viewer/src/lib/state.ts
index f1cc5f5..ce95f2b 100644
--- a/webviews/viewer/src/lib/state.ts
+++ b/webviews/viewer/src/lib/state.ts
@@ -1,4 +1,6 @@
import { writable } from 'svelte/store';
+import { tweened } from 'svelte/motion';
+import { cubicOut } from 'svelte/easing';
export type GroupHierarchy = Record;
export type HighlightedGroups = Map;
@@ -29,3 +31,6 @@ export const settings = writable({
// Map> — groups NOT shown in sidebar (hidden)
export const sidebarHiddenGroups = writable>>(new Map());
+
+export const loadingProgress = tweened(0, { duration: 300, easing: cubicOut });
+export const loadingMessage = writable('');
diff --git a/webviews/viewer/src/main.ts b/webviews/viewer/src/main.ts
index 08f6f9e..b7fd10f 100644
--- a/webviews/viewer/src/main.ts
+++ b/webviews/viewer/src/main.ts
@@ -1,5 +1,5 @@
import { mount } from 'svelte';
-import App from './components/App.svelte';
+import App from './components/layout/App.svelte';
import { Controller } from './lib/Controller';
import { VisibilityManager } from './lib/commands/VisibilityManager';
import { CameraManager } from './lib/interaction/CameraManager';
@@ -57,6 +57,10 @@ window.addEventListener('message', async (e) => {
case 'displayGroup':
VisibilityManager.Instance.setVisibility(body.group, body.visible);
break;
+
+ case 'showOnlyObjects':
+ VisibilityManager.Instance.showOnlyObjects(body.objects);
+ break;
}
});