Skip to content

Commit e98015f

Browse files
Merge branch 'main' into dev
2 parents 71c0007 + beee500 commit e98015f

11 files changed

Lines changed: 2398 additions & 273 deletions

package-lock.json

Lines changed: 1810 additions & 273 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<script lang="ts">
2+
import { VisibilityManager } from '../lib/commands/VisibilityManager';
3+
import FilterIcon from '../icons/FilterIcon.svelte';
4+
import ClearIcon from '../icons/ClearIcon.svelte';
5+
6+
let { onOpenGroups }: { onOpenGroups: () => void } = $props();
7+
8+
function clear() {
9+
VisibilityManager.Instance.clear();
10+
}
11+
</script>
12+
13+
<div
14+
id="sidebarActions"
15+
class="absolute left-full top-1 flex flex-col items-center rounded-r-sm border-l bg-ui-muted border-ui-border"
16+
>
17+
<div class="group flex items-center">
18+
<button
19+
class="size-6 p-1 flex items-center justify-center cursor-pointer hover:bg-ui-elem"
20+
onclick={onOpenGroups}
21+
>
22+
<FilterIcon class="size-3.5" />
23+
</button>
24+
<span
25+
class="absolute left-full pl-1.5 hidden group-hover:inline whitespace-nowrap text-ui-text-secondary"
26+
>
27+
Select groups to show in sidebar
28+
</span>
29+
</div>
30+
<div class="w-3/4 h-px mx-auto bg-ui-border"></div>
31+
<div class="group flex items-center">
32+
<button
33+
class="size-6 p-1 flex items-center justify-center cursor-pointer hover:bg-ui-elem"
34+
onclick={clear}
35+
>
36+
<ClearIcon class="size-3.5" />
37+
</button>
38+
<span
39+
class="absolute left-full pl-1.5 hidden group-hover:inline whitespace-nowrap text-ui-text-secondary"
40+
>
41+
Clear highlighted groups
42+
</span>
43+
</div>
44+
</div>
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
<script lang="ts">
2+
import { groupHierarchy } from '../lib/state';
3+
import Sidebar from './Sidebar.svelte';
4+
import TopActions from './TopActions.svelte';
5+
import ZoomWidget from './ZoomWidget.svelte';
6+
import Popup from './popups/Popup.svelte';
7+
import HelpPopup from './popups/HelpPopup.svelte';
8+
import SettingsPopup from './popups/SettingsPopup.svelte';
9+
import GroupsPopup from './popups/GroupsPopup.svelte';
10+
import LoadingScreen from './LoadingScreen.svelte';
11+
12+
type PopupType = 'help' | 'settings' | 'groups' | null;
13+
let openPopup: PopupType = $state(null);
14+
15+
let hasData = $derived(Object.keys($groupHierarchy).length > 0);
16+
</script>
17+
18+
{#if hasData}
19+
<Sidebar
20+
onOpenGroups={() => {
21+
openPopup = 'groups';
22+
}}
23+
/>
24+
{:else}
25+
<LoadingScreen />
26+
{/if}
27+
28+
{#if hasData}
29+
<TopActions
30+
onOpenSettings={() => {
31+
openPopup = 'settings';
32+
}}
33+
onOpenHelp={() => {
34+
openPopup = 'help';
35+
}}
36+
/>
37+
38+
<ZoomWidget />
39+
{/if}
40+
41+
{#if openPopup}
42+
<Popup
43+
onclose={() => {
44+
openPopup = null;
45+
}}
46+
>
47+
{#if openPopup === 'help'}
48+
<HelpPopup
49+
onclose={() => {
50+
openPopup = null;
51+
}}
52+
/>
53+
{:else if openPopup === 'settings'}
54+
<SettingsPopup
55+
onclose={() => {
56+
openPopup = null;
57+
}}
58+
/>
59+
{:else if openPopup === 'groups'}
60+
<GroupsPopup
61+
onclose={() => {
62+
openPopup = null;
63+
}}
64+
/>
65+
{/if}
66+
</Popup>
67+
{/if}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<script lang="ts">
2+
import { CameraManager } from '../lib/interaction/CameraManager';
3+
</script>
4+
5+
<div class="flex justify-center space-x-1">
6+
<button
7+
title="Rotate camera to x-axis"
8+
class="flex items-center justify-center size-6 text-[0.75rem] font-bold text-red-500 cursor-pointer hover:bg-red-500/15 rounded-sm"
9+
onclick={() => CameraManager.Instance.setCameraAxis('x')}
10+
>
11+
X
12+
</button>
13+
<button
14+
title="Rotate camera to y-axis"
15+
class="flex items-center justify-center size-6 text-[0.75rem] font-bold text-green-500 cursor-pointer hover:bg-green-500/15 rounded-sm"
16+
onclick={() => CameraManager.Instance.setCameraAxis('y')}
17+
>
18+
Y
19+
</button>
20+
<button
21+
title="Rotate camera to z-axis"
22+
class="flex items-center justify-center size-6 text-[0.75rem] font-bold text-blue-500 cursor-pointer hover:bg-blue-500/15 rounded-sm"
23+
onclick={() => CameraManager.Instance.setCameraAxis('z')}
24+
>
25+
Z
26+
</button>
27+
</div>
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<script lang="ts">
2+
import { highlightedGroups, sidebarHiddenGroups } from '../lib/state';
3+
import { VisibilityManager } from '../lib/commands/VisibilityManager';
4+
import FaceIcon from '../icons/FaceIcon.svelte';
5+
import NodeIcon from '../icons/NodeIcon.svelte';
6+
7+
let {
8+
objectKey,
9+
groupName,
10+
isFace,
11+
}: {
12+
objectKey: string;
13+
groupName: string;
14+
isFace: boolean;
15+
} = $props();
16+
17+
let highlight = $derived(
18+
$highlightedGroups.get(`${objectKey}::${groupName}::${isFace ? 'face' : 'node'}`)
19+
);
20+
let isHidden = $derived($sidebarHiddenGroups.get(objectKey)?.has(groupName) ?? false);
21+
22+
let bgStyle = $derived(
23+
highlight ? `rgb(${highlight[0] * 255}, ${highlight[1] * 255}, ${highlight[2] * 255}, 0.8)` : ''
24+
);
25+
let colorStyle = $derived(highlight ? 'var(--ui-highlight-text)' : 'var(--ui-text-primary)');
26+
27+
function handleClick() {
28+
VisibilityManager.Instance.setVisibility(
29+
`${objectKey}::${groupName}::${isFace ? 'face' : 'node'}`
30+
);
31+
}
32+
</script>
33+
34+
{#if !isHidden}
35+
<button
36+
class="relative flex items-center justify-center rounded-sm text-xs px-2 pt-0.75 pb-1.25 w-full cursor-pointer {!highlight
37+
? 'bg-ui-elem hover:bg-ui-elem-hover'
38+
: ''}"
39+
style="{highlight ? `background: ${bgStyle};` : ''} color: {colorStyle}; {highlight
40+
? 'font-weight: 600;'
41+
: ''}"
42+
onclick={handleClick}
43+
>
44+
<span class="absolute left-1.5 top-1">
45+
{#if isFace}
46+
<FaceIcon class="size-4" />
47+
{:else}
48+
<NodeIcon class="size-4" />
49+
{/if}
50+
</span>
51+
<span class="pl-5">{groupName}</span>
52+
</button>
53+
{/if}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<script lang="ts"></script>
2+
3+
<div
4+
class="fixed inset-0 flex flex-col items-center justify-center gap-3 pointer-events-none text-ui-text-secondary"
5+
>
6+
<div
7+
class="size-7 rounded-full border-2 border-transparent border-t-ui-text-muted animate-spin"
8+
></div>
9+
<span class="text-xs">Loading meshes...</span>
10+
</div>
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
<script lang="ts">
2+
import { hiddenObjects, sidebarHiddenGroups } from '../lib/state';
3+
import { VisibilityManager } from '../lib/commands/VisibilityManager';
4+
import GroupButton from './GroupButton.svelte';
5+
import ObjectIcon from '../icons/ObjectIcon.svelte';
6+
import EyeIcon from '../icons/EyeIcon.svelte';
7+
import EyeOffIcon from '../icons/EyeOffIcon.svelte';
8+
9+
let {
10+
objectKey,
11+
faces,
12+
nodes,
13+
color,
14+
}: {
15+
objectKey: string;
16+
faces: string[];
17+
nodes: string[];
18+
color: number[];
19+
} = $props();
20+
21+
let objectName = $derived(objectKey.replace('all_', '').replace('.obj', ''));
22+
let colorCss = $derived(
23+
`rgb(${Math.round(color[0] * 255)},${Math.round(color[1] * 255)},${Math.round(color[2] * 255)})`
24+
);
25+
let isHidden = $derived($hiddenObjects.has(objectKey));
26+
let collapsed = $state(false);
27+
28+
let hiddenGroupCount = $derived.by(() => {
29+
const hidden = $sidebarHiddenGroups.get(objectKey);
30+
if (!hidden) return 0;
31+
return [...faces, ...nodes].filter((g) => hidden.has(g)).length;
32+
});
33+
34+
let allGroupsHiddenFromSidebar = $derived.by(() => {
35+
const hidden = $sidebarHiddenGroups.get(objectKey);
36+
if (!hidden) return false;
37+
return [...faces, ...nodes].every((g) => hidden.has(g));
38+
});
39+
40+
let groupCount = $derived(faces.length + nodes.length);
41+
42+
function toggleVisibility() {
43+
VisibilityManager.Instance.toggleObjectVisibility(objectKey);
44+
}
45+
46+
function toggleCollapsed() {
47+
if (isHidden || allGroupsHiddenFromSidebar) return;
48+
collapsed = !collapsed;
49+
}
50+
51+
let contextMenu: { x: number; y: number } | null = $state(null);
52+
53+
function onContextMenu(e: MouseEvent) {
54+
e.preventDefault();
55+
contextMenu = { x: e.clientX, y: e.clientY };
56+
}
57+
58+
function closeContextMenu() {
59+
contextMenu = null;
60+
}
61+
62+
function hideAllOthers() {
63+
VisibilityManager.Instance.hideAllOthers(objectKey);
64+
closeContextMenu();
65+
}
66+
</script>
67+
68+
{#if contextMenu}
69+
<div
70+
class="fixed inset-0 z-40"
71+
onclick={closeContextMenu}
72+
oncontextmenu={(e) => {
73+
e.preventDefault();
74+
closeContextMenu();
75+
}}
76+
role="presentation"
77+
></div>
78+
<div
79+
class="fixed z-[9999] bg-ui-popup-bg border border-ui-border rounded shadow-[0_4px_16px_rgba(0,0,0,0.25)] py-[3px] overflow-hidden"
80+
style="left: {contextMenu.x}px; top: {contextMenu.y}px"
81+
>
82+
<button
83+
class="flex items-center w-full px-[10px] py-[5px] text-xs cursor-pointer text-ui-fg whitespace-nowrap bg-transparent border-0 text-left hover:bg-ui-elem-hover"
84+
onclick={hideAllOthers}
85+
>
86+
Hide all other objects
87+
</button>
88+
</div>
89+
{/if}
90+
91+
<span
92+
role="group"
93+
class="h-4.5 w-full self-stretch flex items-center gap-1 pl-1.25 pr-0.5 mb-2 not-nth-of-type-[1]:mt-1 text-xs font-bold text-ui-text-primary"
94+
oncontextmenu={onContextMenu}
95+
>
96+
<span style="color: {colorCss}"><ObjectIcon class="size-[18px]" /></span>
97+
<span
98+
class="flex-1 truncate text-center px-2 select-none"
99+
style="cursor: {isHidden || allGroupsHiddenFromSidebar ? 'default' : 'pointer'}"
100+
onclick={toggleCollapsed}
101+
onkeydown={(e) => e.key === 'Enter' && toggleCollapsed()}
102+
role="button"
103+
tabindex="0"
104+
>
105+
{objectName}
106+
</span>
107+
<button
108+
class="size-4.5 shrink-0 flex items-center justify-center cursor-pointer opacity-40 hover:opacity-90"
109+
title="Hide/show mesh"
110+
onclick={toggleVisibility}
111+
>
112+
{#if isHidden}
113+
<EyeOffIcon class="size-4" />
114+
{:else}
115+
<EyeIcon class="size-4" />
116+
{/if}
117+
</button>
118+
</span>
119+
120+
{#if isHidden}
121+
<div class="w-full text-center text-[0.7rem] font-normal mb-2 text-ui-text-muted">
122+
{groupCount} groups
123+
</div>
124+
{:else if !collapsed}
125+
<div class="w-full flex flex-col items-center space-y-1">
126+
{#each faces as groupName}
127+
<GroupButton {objectKey} {groupName} isFace={true} />
128+
{/each}
129+
{#each nodes as groupName}
130+
<GroupButton {objectKey} {groupName} isFace={false} />
131+
{/each}
132+
{#if hiddenGroupCount > 0}
133+
<div class="w-full text-center text-[0.7rem] pb-1 text-ui-text-muted">
134+
{hiddenGroupCount} hidden
135+
</div>
136+
{/if}
137+
</div>
138+
{:else}
139+
<div class="w-full text-center text-[0.7rem] font-normal mb-2 text-ui-text-muted">
140+
{groupCount} groups
141+
</div>
142+
{/if}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<script lang="ts">
2+
import { onMount } from 'svelte';
3+
import { groupHierarchy } from '../lib/state';
4+
import { VtkApp } from '../lib/core/VtkApp';
5+
import ObjectSection from './ObjectSection.svelte';
6+
import ActionButtons from './ActionButtons.svelte';
7+
import AxisButtons from './AxisButtons.svelte';
8+
9+
let { onOpenGroups }: { onOpenGroups: () => void } = $props();
10+
11+
onMount(() => {
12+
VtkApp.Instance.updateCameraOffset();
13+
});
14+
</script>
15+
16+
<div id="controls" class="relative h-full flex flex-col z-10 p-2 bg-ui-muted">
17+
<div id="sidebarGroups" class="flex flex-col items-center space-y-1 grow overflow-y-auto">
18+
{#each Object.entries($groupHierarchy) as [key, data]}
19+
<ObjectSection
20+
objectKey={key}
21+
faces={data.faces}
22+
nodes={data.nodes}
23+
color={(data as any).color ?? [0.537, 0.529, 0.529]}
24+
/>
25+
{/each}
26+
</div>
27+
28+
<ActionButtons {onOpenGroups} />
29+
30+
<div class="w-3/4 h-px my-2 mx-auto bg-ui-border"></div>
31+
32+
<AxisButtons />
33+
</div>

0 commit comments

Comments
 (0)