@selvajs/ui 4.6.1 → 4.6.2-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/viewer/SceneManager.svelte +8 -3
- package/dist/components/viewer/Viewer.svelte +165 -47
- package/dist/components/viewer/Viewer.svelte.d.ts +1 -0
- package/dist/public.d.ts +12 -0
- package/dist/public.js +35 -0
- package/package.json +11 -6
- package/src/lib/components/viewer/SceneManager.svelte +8 -3
- package/src/lib/components/viewer/Viewer.svelte +165 -47
- package/src/lib/public.ts +55 -0
|
@@ -80,11 +80,16 @@
|
|
|
80
80
|
}
|
|
81
81
|
};
|
|
82
82
|
|
|
83
|
+
const prettyType = (type: string) =>
|
|
84
|
+
type
|
|
85
|
+
.replace(/^Line(Segments)?2$/, 'Curve')
|
|
86
|
+
.replace('Mesh', '')
|
|
87
|
+
.replace('Object3D', 'Obj') || type;
|
|
88
|
+
|
|
83
89
|
const getObjectLabel = (object: THREE.Object3D) =>
|
|
84
|
-
object.userData?.name || object.userData?.fileName || object.name || object.type;
|
|
90
|
+
object.userData?.name || object.userData?.fileName || object.name || prettyType(object.type);
|
|
85
91
|
|
|
86
|
-
const getTypeLabel = (object: THREE.Object3D) =>
|
|
87
|
-
object.type.replace('Mesh', '').replace('Object3D', 'Obj') || object.type;
|
|
92
|
+
const getTypeLabel = (object: THREE.Object3D) => prettyType(object.type);
|
|
88
93
|
|
|
89
94
|
let searchQuery = $state('');
|
|
90
95
|
|
|
@@ -4,9 +4,26 @@
|
|
|
4
4
|
import {
|
|
5
5
|
initThree,
|
|
6
6
|
updateScene,
|
|
7
|
-
type ThreeInitializerOptions
|
|
7
|
+
type ThreeInitializerOptions,
|
|
8
|
+
type CameraController,
|
|
9
|
+
type CameraProjection,
|
|
10
|
+
type MeasureTool,
|
|
11
|
+
type ViewPreset
|
|
8
12
|
} from '@selvajs/compute/visualization';
|
|
9
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
Maximize,
|
|
15
|
+
Minimize,
|
|
16
|
+
Camera,
|
|
17
|
+
Layers,
|
|
18
|
+
Settings2,
|
|
19
|
+
Box,
|
|
20
|
+
Square,
|
|
21
|
+
Frame,
|
|
22
|
+
Ruler,
|
|
23
|
+
Check,
|
|
24
|
+
ChevronRight
|
|
25
|
+
} from '@lucide/svelte';
|
|
26
|
+
import { DropdownMenu } from 'bits-ui';
|
|
10
27
|
import type * as THREE from 'three';
|
|
11
28
|
import type { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
|
|
12
29
|
import SceneManager from './SceneManager.svelte';
|
|
@@ -17,6 +34,7 @@
|
|
|
17
34
|
showScreenshotButton?: boolean;
|
|
18
35
|
showFullscreenButton?: boolean;
|
|
19
36
|
showSceneManager?: boolean;
|
|
37
|
+
showToolsMenu?: boolean;
|
|
20
38
|
enableMeshClick?: boolean;
|
|
21
39
|
backgroundColor?: string;
|
|
22
40
|
}
|
|
@@ -34,6 +52,7 @@
|
|
|
34
52
|
showScreenshotButton: true,
|
|
35
53
|
showFullscreenButton: true,
|
|
36
54
|
showSceneManager: true,
|
|
55
|
+
showToolsMenu: true,
|
|
37
56
|
enableMeshClick: true,
|
|
38
57
|
backgroundColor: '#E6E6E6'
|
|
39
58
|
};
|
|
@@ -53,13 +72,28 @@
|
|
|
53
72
|
let scene: THREE.Scene | null = $state(null);
|
|
54
73
|
let camera: THREE.PerspectiveCamera | null = null;
|
|
55
74
|
let controls: OrbitControls | null = null;
|
|
75
|
+
let cameraController: CameraController | null = null;
|
|
76
|
+
let measureTool: MeasureTool | null = null;
|
|
77
|
+
let fitToView: (() => void) | null = null;
|
|
56
78
|
let viewerInitialized = false;
|
|
57
79
|
let sceneVersion = $state(0);
|
|
58
80
|
let hideButton = $state(false);
|
|
59
81
|
let sceneManagerOpen = $state(false);
|
|
82
|
+
let projection: CameraProjection = $state('perspective');
|
|
83
|
+
let measureActive = $state(false);
|
|
60
84
|
let selectedMeshMetadata: Record<string, any> | null = $state(null);
|
|
61
85
|
let selectedMeshName: string | null = $state(null);
|
|
62
86
|
|
|
87
|
+
const VIEW_PRESETS: { preset: ViewPreset; label: string }[] = [
|
|
88
|
+
{ preset: 'top', label: 'Top' },
|
|
89
|
+
{ preset: 'front', label: 'Front' },
|
|
90
|
+
{ preset: 'right', label: 'Right' },
|
|
91
|
+
{ preset: 'back', label: 'Back' },
|
|
92
|
+
{ preset: 'left', label: 'Left' },
|
|
93
|
+
{ preset: 'bottom', label: 'Bottom' },
|
|
94
|
+
{ preset: 'iso', label: 'Isometric' }
|
|
95
|
+
];
|
|
96
|
+
|
|
63
97
|
// Hide button instantly when expanding, show after animation when collapsing
|
|
64
98
|
$effect(() => {
|
|
65
99
|
if (drawerOpen) {
|
|
@@ -78,6 +112,9 @@
|
|
|
78
112
|
const opts: ThreeInitializerOptions = {
|
|
79
113
|
environment: { backgroundColor: config.backgroundColor },
|
|
80
114
|
controls: {},
|
|
115
|
+
grid: { enabled: config.showToolsMenu },
|
|
116
|
+
gizmo: { enabled: false },
|
|
117
|
+
measure: { enabled: config.showToolsMenu },
|
|
81
118
|
events: {
|
|
82
119
|
onMeshMetadataClicked: config.enableMeshClick
|
|
83
120
|
? (metadata: Record<string, string>) => {
|
|
@@ -94,12 +131,31 @@
|
|
|
94
131
|
scene = init.scene;
|
|
95
132
|
camera = init.camera;
|
|
96
133
|
controls = init.controls;
|
|
134
|
+
cameraController = init.cameraController;
|
|
135
|
+
measureTool = init.measureTool;
|
|
136
|
+
fitToView = init.fitToView;
|
|
137
|
+
projection = init.cameraController.getProjection();
|
|
97
138
|
|
|
98
139
|
return () => {
|
|
99
140
|
init.dispose();
|
|
100
141
|
};
|
|
101
142
|
});
|
|
102
143
|
|
|
144
|
+
function toggleProjection() {
|
|
145
|
+
if (!cameraController) return;
|
|
146
|
+
projection = cameraController.toggleProjection();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function setView(preset: ViewPreset) {
|
|
150
|
+
cameraController?.setView(preset);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function toggleMeasure() {
|
|
154
|
+
if (!measureTool) return;
|
|
155
|
+
measureActive = !measureActive;
|
|
156
|
+
measureTool.setEnabled(measureActive);
|
|
157
|
+
}
|
|
158
|
+
|
|
103
159
|
$effect(() => {
|
|
104
160
|
if (scene && camera && controls) {
|
|
105
161
|
updateScene(scene, meshes, camera, controls, viewerInitialized);
|
|
@@ -179,58 +235,120 @@
|
|
|
179
235
|
></div>
|
|
180
236
|
{/if}
|
|
181
237
|
|
|
182
|
-
{#if config.
|
|
238
|
+
{#if config.showToolsMenu}
|
|
239
|
+
{@const itemClass =
|
|
240
|
+
'gap-2 px-2 py-1.5 text-sm flex cursor-pointer select-none items-center rounded-sm outline-none transition-colors hover:bg-muted focus:bg-muted'}
|
|
183
241
|
<div
|
|
184
|
-
class="
|
|
242
|
+
class="left-4 {isFullscreen
|
|
185
243
|
? 'bottom-4'
|
|
186
|
-
: 'bottom-16 sm:bottom-4'}
|
|
244
|
+
: 'bottom-16 sm:bottom-4'} absolute z-50 flex items-center"
|
|
187
245
|
style={hideButton
|
|
188
246
|
? 'opacity: 0; pointer-events: none;'
|
|
189
247
|
: 'opacity: 1; pointer-events: auto;'}
|
|
190
248
|
>
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
title="Download screenshot"
|
|
197
|
-
aria-label="Download screenshot"
|
|
198
|
-
>
|
|
199
|
-
<Camera class="h-5 w-5 text-card-foreground" />
|
|
200
|
-
</button>
|
|
201
|
-
{/if}
|
|
202
|
-
|
|
203
|
-
<!-- Fullscreen Toggle -->
|
|
204
|
-
{#if config.showFullscreenButton}
|
|
205
|
-
<button
|
|
206
|
-
class="h-10 w-10 shadow-lg hover:shadow-xl flex items-center justify-center rounded-lg border border-border bg-card/90 transition-all hover:bg-card active:scale-95"
|
|
207
|
-
onclick={toggleFullscreen}
|
|
208
|
-
title={isFullscreen ? 'Exit fullscreen' : 'Enter fullscreen'}
|
|
209
|
-
aria-label={isFullscreen ? 'Exit fullscreen' : 'Enter fullscreen'}
|
|
210
|
-
>
|
|
211
|
-
{#if isFullscreen}
|
|
212
|
-
<Minimize class="h-5 w-5 text-card-foreground" />
|
|
213
|
-
{:else}
|
|
214
|
-
<Maximize class="h-5 w-5 text-card-foreground" />
|
|
215
|
-
{/if}
|
|
216
|
-
</button>
|
|
217
|
-
{/if}
|
|
218
|
-
|
|
219
|
-
<!-- Scene Manager Toggle -->
|
|
220
|
-
{#if config.showSceneManager}
|
|
221
|
-
<button
|
|
222
|
-
class="h-10 w-10 shadow-lg hover:shadow-xl flex items-center justify-center rounded-lg border transition-all active:scale-95 {sceneManagerOpen
|
|
223
|
-
? 'border-muted-foreground/40 bg-secondary/40 hover:bg-secondary/60'
|
|
224
|
-
: 'border-muted-foreground/50 bg-card/90 hover:bg-secondary/80'}"
|
|
225
|
-
onclick={() => (sceneManagerOpen = !sceneManagerOpen)}
|
|
226
|
-
title={sceneManagerOpen ? 'Hide scene manager' : 'Show scene manager'}
|
|
227
|
-
aria-label="Toggle scene manager"
|
|
249
|
+
<DropdownMenu.Root>
|
|
250
|
+
<DropdownMenu.Trigger
|
|
251
|
+
class="h-10 w-10 shadow-lg hover:shadow-xl flex items-center justify-center rounded-lg border border-border bg-card/90 transition-all hover:bg-card active:scale-95 data-[state=open]:bg-secondary/60"
|
|
252
|
+
title="Viewer tools"
|
|
253
|
+
aria-label="Viewer tools"
|
|
228
254
|
>
|
|
229
|
-
<
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
255
|
+
<Settings2 class="h-5 w-5 text-card-foreground" />
|
|
256
|
+
</DropdownMenu.Trigger>
|
|
257
|
+
<DropdownMenu.Portal>
|
|
258
|
+
<DropdownMenu.Content
|
|
259
|
+
sideOffset={6}
|
|
260
|
+
align="start"
|
|
261
|
+
class="data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 min-w-44 p-1 shadow-md z-10001 rounded-md border bg-popover text-popover-foreground"
|
|
262
|
+
>
|
|
263
|
+
<!-- Camera -->
|
|
264
|
+
<DropdownMenu.Item class={itemClass} onSelect={toggleProjection}>
|
|
265
|
+
{#if projection === 'perspective'}
|
|
266
|
+
<Square class="h-4 w-4" />
|
|
267
|
+
Switch to 2D
|
|
268
|
+
{:else}
|
|
269
|
+
<Box class="h-4 w-4" />
|
|
270
|
+
Switch to 3D
|
|
271
|
+
{/if}
|
|
272
|
+
</DropdownMenu.Item>
|
|
273
|
+
|
|
274
|
+
<DropdownMenu.Item class={itemClass} onSelect={() => fitToView?.()}>
|
|
275
|
+
<Frame class="h-4 w-4" />
|
|
276
|
+
Fit to view
|
|
277
|
+
</DropdownMenu.Item>
|
|
278
|
+
|
|
279
|
+
<DropdownMenu.Sub>
|
|
280
|
+
<DropdownMenu.SubTrigger class="{itemClass} data-[state=open]:bg-muted">
|
|
281
|
+
<Box class="h-4 w-4" />
|
|
282
|
+
<span class="flex-1">Views</span>
|
|
283
|
+
<ChevronRight class="h-4 w-4 text-muted-foreground" />
|
|
284
|
+
</DropdownMenu.SubTrigger>
|
|
285
|
+
<DropdownMenu.SubContent
|
|
286
|
+
sideOffset={4}
|
|
287
|
+
class="min-w-32 p-1 shadow-md z-10001 rounded-md border bg-popover text-popover-foreground"
|
|
288
|
+
>
|
|
289
|
+
{#each VIEW_PRESETS as { preset, label } (preset)}
|
|
290
|
+
<DropdownMenu.Item
|
|
291
|
+
class="px-2 py-1.5 text-sm flex cursor-pointer items-center rounded-sm transition-colors outline-none select-none hover:bg-muted focus:bg-muted"
|
|
292
|
+
onSelect={() => setView(preset)}
|
|
293
|
+
>
|
|
294
|
+
{label}
|
|
295
|
+
</DropdownMenu.Item>
|
|
296
|
+
{/each}
|
|
297
|
+
</DropdownMenu.SubContent>
|
|
298
|
+
</DropdownMenu.Sub>
|
|
299
|
+
|
|
300
|
+
<DropdownMenu.Item
|
|
301
|
+
closeOnSelect={false}
|
|
302
|
+
class="{itemClass} {measureActive ? 'text-primary' : ''}"
|
|
303
|
+
onSelect={toggleMeasure}
|
|
304
|
+
>
|
|
305
|
+
<Ruler class="h-4 w-4" />
|
|
306
|
+
<span class="flex-1">Measure</span>
|
|
307
|
+
{#if measureActive}
|
|
308
|
+
<Check class="h-4 w-4" />
|
|
309
|
+
{/if}
|
|
310
|
+
</DropdownMenu.Item>
|
|
311
|
+
|
|
312
|
+
<!-- Scene tools -->
|
|
313
|
+
{#if config.showSceneManager || config.showScreenshotButton || config.showFullscreenButton}
|
|
314
|
+
<DropdownMenu.Separator class="my-1 h-px bg-border" />
|
|
315
|
+
{/if}
|
|
316
|
+
|
|
317
|
+
{#if config.showSceneManager}
|
|
318
|
+
<DropdownMenu.Item
|
|
319
|
+
closeOnSelect={false}
|
|
320
|
+
class="{itemClass} {sceneManagerOpen ? 'text-primary' : ''}"
|
|
321
|
+
onSelect={() => (sceneManagerOpen = !sceneManagerOpen)}
|
|
322
|
+
>
|
|
323
|
+
<Layers class="h-4 w-4" />
|
|
324
|
+
<span class="flex-1">Scene manager</span>
|
|
325
|
+
{#if sceneManagerOpen}
|
|
326
|
+
<Check class="h-4 w-4" />
|
|
327
|
+
{/if}
|
|
328
|
+
</DropdownMenu.Item>
|
|
329
|
+
{/if}
|
|
330
|
+
|
|
331
|
+
{#if config.showScreenshotButton}
|
|
332
|
+
<DropdownMenu.Item class={itemClass} onSelect={downloadScreenshot}>
|
|
333
|
+
<Camera class="h-4 w-4" />
|
|
334
|
+
Screenshot
|
|
335
|
+
</DropdownMenu.Item>
|
|
336
|
+
{/if}
|
|
337
|
+
|
|
338
|
+
{#if config.showFullscreenButton}
|
|
339
|
+
<DropdownMenu.Item class={itemClass} onSelect={toggleFullscreen}>
|
|
340
|
+
{#if isFullscreen}
|
|
341
|
+
<Minimize class="h-4 w-4" />
|
|
342
|
+
Exit fullscreen
|
|
343
|
+
{:else}
|
|
344
|
+
<Maximize class="h-4 w-4" />
|
|
345
|
+
Fullscreen
|
|
346
|
+
{/if}
|
|
347
|
+
</DropdownMenu.Item>
|
|
348
|
+
{/if}
|
|
349
|
+
</DropdownMenu.Content>
|
|
350
|
+
</DropdownMenu.Portal>
|
|
351
|
+
</DropdownMenu.Root>
|
|
234
352
|
</div>
|
|
235
353
|
{/if}
|
|
236
354
|
</div>
|
package/dist/public.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export { default as AppLayout } from './components/compute/AppLayout.svelte';
|
|
2
|
+
export { default as ComputeApp } from './components/compute/ComputeApp.svelte';
|
|
3
|
+
export { default as ErrorScreen } from './components/ErrorScreen.svelte';
|
|
4
|
+
export { createSolveSession, createRequestResponseDriver, type SolveSession, type SolveSessionArgs, type SolveDriver, type SolveReporter } from './compute/createSolveSession.svelte';
|
|
5
|
+
export type { ClientSlotArgs, ClientSlot } from './contexts/clientSlotContext.svelte';
|
|
6
|
+
export * from './external/storage';
|
|
7
|
+
export * from './schema/defaults';
|
|
8
|
+
export * from './schema/traversal';
|
|
9
|
+
export * from './schema/dynamic-value-list';
|
|
10
|
+
export type { ActionButton } from './types/actionButton';
|
|
11
|
+
export type { SolveFn, SolveResult } from './types/solveFn';
|
|
12
|
+
export { DEFAULT_PRESET_LABELS, type PresetLabels } from './types/presetLabels';
|
package/dist/public.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// Published public API of @selvajs/ui
|
|
3
|
+
// ============================================================================
|
|
4
|
+
//
|
|
5
|
+
// This is the ONLY surface npm consumers of @selvajs/ui see (package.json maps
|
|
6
|
+
// the "." export's `svelte`/`types` conditions at this file's build output).
|
|
7
|
+
//
|
|
8
|
+
// Scope: the compute-app SDK — everything an external host app needs to embed a
|
|
9
|
+
// Grasshopper-driven app (ComputeApp), drive solves, and wire pre-step
|
|
10
|
+
// producers. Verified against real consumers (parafa, parapet): they import
|
|
11
|
+
// ComputeApp + its types, the solve seam, and external/storage. Nothing else.
|
|
12
|
+
//
|
|
13
|
+
// Deliberately NOT public: design-system primitives (Button, Card, Dialog, …),
|
|
14
|
+
// page-chrome layout (AppShell, SideNav, …), toast/Toaster, ThemeSwitcher,
|
|
15
|
+
// DataTable, contexts/composables, cn/randomId. These remain importable from
|
|
16
|
+
// the full barrel (./index.ts) INSIDE the monorepo via the "@selvajs/source"
|
|
17
|
+
// export condition, but never ship to npm. If an external consumer ever needs a
|
|
18
|
+
// primitive directly, promote it here explicitly rather than re-exporting the
|
|
19
|
+
// whole primitives barrel.
|
|
20
|
+
// Compute app (schema + viewer + solve controls composed into a runnable app)
|
|
21
|
+
export { default as AppLayout } from './components/compute/AppLayout.svelte';
|
|
22
|
+
export { default as ComputeApp } from './components/compute/ComputeApp.svelte';
|
|
23
|
+
// Full-screen states a host app renders
|
|
24
|
+
export { default as ErrorScreen } from './components/ErrorScreen.svelte';
|
|
25
|
+
// Solve Session seam (transport-agnostic value/lifecycle state machine + its
|
|
26
|
+
// driver interface). Exported so transports outside this package can satisfy
|
|
27
|
+
// SolveDriver and drive a session. See CONTEXT.md.
|
|
28
|
+
export { createSolveSession, createRequestResponseDriver } from './compute/createSolveSession.svelte';
|
|
29
|
+
// Pre-step producer transit storage (parafa wires producers via these).
|
|
30
|
+
export * from './external/storage';
|
|
31
|
+
// Schema utilities a ComputeApp host reasonably needs to read/shape values.
|
|
32
|
+
export * from './schema/defaults';
|
|
33
|
+
export * from './schema/traversal';
|
|
34
|
+
export * from './schema/dynamic-value-list';
|
|
35
|
+
export { DEFAULT_PRESET_LABELS } from './types/presetLabels';
|
package/package.json
CHANGED
|
@@ -1,18 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@selvajs/ui",
|
|
3
|
-
"version": "4.6.1",
|
|
3
|
+
"version": "4.6.2-beta.1",
|
|
4
4
|
"description": "Shared UI components and utilities for Selva applications",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"publishConfig": {
|
|
7
7
|
"access": "public"
|
|
8
8
|
},
|
|
9
9
|
"type": "module",
|
|
10
|
-
"svelte": "./dist/
|
|
11
|
-
"types": "./dist/
|
|
10
|
+
"svelte": "./dist/public.js",
|
|
11
|
+
"types": "./dist/public.d.ts",
|
|
12
12
|
"exports": {
|
|
13
13
|
".": {
|
|
14
|
-
"
|
|
15
|
-
|
|
14
|
+
"source": {
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"svelte": "./dist/index.js"
|
|
17
|
+
},
|
|
18
|
+
"types": "./dist/public.d.ts",
|
|
19
|
+
"svelte": "./dist/public.js"
|
|
16
20
|
},
|
|
17
21
|
"./external": {
|
|
18
22
|
"types": "./dist/external/storage.d.ts",
|
|
@@ -33,7 +37,7 @@
|
|
|
33
37
|
"**/*.css"
|
|
34
38
|
],
|
|
35
39
|
"peerDependencies": {
|
|
36
|
-
"@selvajs/compute": "^2.0.
|
|
40
|
+
"@selvajs/compute": "^2.1.0-beta.7",
|
|
37
41
|
"@sveltejs/kit": "^2",
|
|
38
42
|
"bits-ui": "^2.18.0",
|
|
39
43
|
"svelte": "^5",
|
|
@@ -63,6 +67,7 @@
|
|
|
63
67
|
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
|
64
68
|
"@types/three": "^0.184.0",
|
|
65
69
|
"bits-ui": "^2.18.0",
|
|
70
|
+
"rhino3dm": "8.17.0",
|
|
66
71
|
"rimraf": "^6.0.1",
|
|
67
72
|
"svelte": "5.55.5",
|
|
68
73
|
"tailwind-variants": "^3.2.2",
|
|
@@ -80,11 +80,16 @@
|
|
|
80
80
|
}
|
|
81
81
|
};
|
|
82
82
|
|
|
83
|
+
const prettyType = (type: string) =>
|
|
84
|
+
type
|
|
85
|
+
.replace(/^Line(Segments)?2$/, 'Curve')
|
|
86
|
+
.replace('Mesh', '')
|
|
87
|
+
.replace('Object3D', 'Obj') || type;
|
|
88
|
+
|
|
83
89
|
const getObjectLabel = (object: THREE.Object3D) =>
|
|
84
|
-
object.userData?.name || object.userData?.fileName || object.name || object.type;
|
|
90
|
+
object.userData?.name || object.userData?.fileName || object.name || prettyType(object.type);
|
|
85
91
|
|
|
86
|
-
const getTypeLabel = (object: THREE.Object3D) =>
|
|
87
|
-
object.type.replace('Mesh', '').replace('Object3D', 'Obj') || object.type;
|
|
92
|
+
const getTypeLabel = (object: THREE.Object3D) => prettyType(object.type);
|
|
88
93
|
|
|
89
94
|
let searchQuery = $state('');
|
|
90
95
|
|
|
@@ -4,9 +4,26 @@
|
|
|
4
4
|
import {
|
|
5
5
|
initThree,
|
|
6
6
|
updateScene,
|
|
7
|
-
type ThreeInitializerOptions
|
|
7
|
+
type ThreeInitializerOptions,
|
|
8
|
+
type CameraController,
|
|
9
|
+
type CameraProjection,
|
|
10
|
+
type MeasureTool,
|
|
11
|
+
type ViewPreset
|
|
8
12
|
} from '@selvajs/compute/visualization';
|
|
9
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
Maximize,
|
|
15
|
+
Minimize,
|
|
16
|
+
Camera,
|
|
17
|
+
Layers,
|
|
18
|
+
Settings2,
|
|
19
|
+
Box,
|
|
20
|
+
Square,
|
|
21
|
+
Frame,
|
|
22
|
+
Ruler,
|
|
23
|
+
Check,
|
|
24
|
+
ChevronRight
|
|
25
|
+
} from '@lucide/svelte';
|
|
26
|
+
import { DropdownMenu } from 'bits-ui';
|
|
10
27
|
import type * as THREE from 'three';
|
|
11
28
|
import type { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
|
|
12
29
|
import SceneManager from './SceneManager.svelte';
|
|
@@ -17,6 +34,7 @@
|
|
|
17
34
|
showScreenshotButton?: boolean;
|
|
18
35
|
showFullscreenButton?: boolean;
|
|
19
36
|
showSceneManager?: boolean;
|
|
37
|
+
showToolsMenu?: boolean;
|
|
20
38
|
enableMeshClick?: boolean;
|
|
21
39
|
backgroundColor?: string;
|
|
22
40
|
}
|
|
@@ -34,6 +52,7 @@
|
|
|
34
52
|
showScreenshotButton: true,
|
|
35
53
|
showFullscreenButton: true,
|
|
36
54
|
showSceneManager: true,
|
|
55
|
+
showToolsMenu: true,
|
|
37
56
|
enableMeshClick: true,
|
|
38
57
|
backgroundColor: '#E6E6E6'
|
|
39
58
|
};
|
|
@@ -53,13 +72,28 @@
|
|
|
53
72
|
let scene: THREE.Scene | null = $state(null);
|
|
54
73
|
let camera: THREE.PerspectiveCamera | null = null;
|
|
55
74
|
let controls: OrbitControls | null = null;
|
|
75
|
+
let cameraController: CameraController | null = null;
|
|
76
|
+
let measureTool: MeasureTool | null = null;
|
|
77
|
+
let fitToView: (() => void) | null = null;
|
|
56
78
|
let viewerInitialized = false;
|
|
57
79
|
let sceneVersion = $state(0);
|
|
58
80
|
let hideButton = $state(false);
|
|
59
81
|
let sceneManagerOpen = $state(false);
|
|
82
|
+
let projection: CameraProjection = $state('perspective');
|
|
83
|
+
let measureActive = $state(false);
|
|
60
84
|
let selectedMeshMetadata: Record<string, any> | null = $state(null);
|
|
61
85
|
let selectedMeshName: string | null = $state(null);
|
|
62
86
|
|
|
87
|
+
const VIEW_PRESETS: { preset: ViewPreset; label: string }[] = [
|
|
88
|
+
{ preset: 'top', label: 'Top' },
|
|
89
|
+
{ preset: 'front', label: 'Front' },
|
|
90
|
+
{ preset: 'right', label: 'Right' },
|
|
91
|
+
{ preset: 'back', label: 'Back' },
|
|
92
|
+
{ preset: 'left', label: 'Left' },
|
|
93
|
+
{ preset: 'bottom', label: 'Bottom' },
|
|
94
|
+
{ preset: 'iso', label: 'Isometric' }
|
|
95
|
+
];
|
|
96
|
+
|
|
63
97
|
// Hide button instantly when expanding, show after animation when collapsing
|
|
64
98
|
$effect(() => {
|
|
65
99
|
if (drawerOpen) {
|
|
@@ -78,6 +112,9 @@
|
|
|
78
112
|
const opts: ThreeInitializerOptions = {
|
|
79
113
|
environment: { backgroundColor: config.backgroundColor },
|
|
80
114
|
controls: {},
|
|
115
|
+
grid: { enabled: config.showToolsMenu },
|
|
116
|
+
gizmo: { enabled: false },
|
|
117
|
+
measure: { enabled: config.showToolsMenu },
|
|
81
118
|
events: {
|
|
82
119
|
onMeshMetadataClicked: config.enableMeshClick
|
|
83
120
|
? (metadata: Record<string, string>) => {
|
|
@@ -94,12 +131,31 @@
|
|
|
94
131
|
scene = init.scene;
|
|
95
132
|
camera = init.camera;
|
|
96
133
|
controls = init.controls;
|
|
134
|
+
cameraController = init.cameraController;
|
|
135
|
+
measureTool = init.measureTool;
|
|
136
|
+
fitToView = init.fitToView;
|
|
137
|
+
projection = init.cameraController.getProjection();
|
|
97
138
|
|
|
98
139
|
return () => {
|
|
99
140
|
init.dispose();
|
|
100
141
|
};
|
|
101
142
|
});
|
|
102
143
|
|
|
144
|
+
function toggleProjection() {
|
|
145
|
+
if (!cameraController) return;
|
|
146
|
+
projection = cameraController.toggleProjection();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function setView(preset: ViewPreset) {
|
|
150
|
+
cameraController?.setView(preset);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function toggleMeasure() {
|
|
154
|
+
if (!measureTool) return;
|
|
155
|
+
measureActive = !measureActive;
|
|
156
|
+
measureTool.setEnabled(measureActive);
|
|
157
|
+
}
|
|
158
|
+
|
|
103
159
|
$effect(() => {
|
|
104
160
|
if (scene && camera && controls) {
|
|
105
161
|
updateScene(scene, meshes, camera, controls, viewerInitialized);
|
|
@@ -179,58 +235,120 @@
|
|
|
179
235
|
></div>
|
|
180
236
|
{/if}
|
|
181
237
|
|
|
182
|
-
{#if config.
|
|
238
|
+
{#if config.showToolsMenu}
|
|
239
|
+
{@const itemClass =
|
|
240
|
+
'gap-2 px-2 py-1.5 text-sm flex cursor-pointer select-none items-center rounded-sm outline-none transition-colors hover:bg-muted focus:bg-muted'}
|
|
183
241
|
<div
|
|
184
|
-
class="
|
|
242
|
+
class="left-4 {isFullscreen
|
|
185
243
|
? 'bottom-4'
|
|
186
|
-
: 'bottom-16 sm:bottom-4'}
|
|
244
|
+
: 'bottom-16 sm:bottom-4'} absolute z-50 flex items-center"
|
|
187
245
|
style={hideButton
|
|
188
246
|
? 'opacity: 0; pointer-events: none;'
|
|
189
247
|
: 'opacity: 1; pointer-events: auto;'}
|
|
190
248
|
>
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
title="Download screenshot"
|
|
197
|
-
aria-label="Download screenshot"
|
|
198
|
-
>
|
|
199
|
-
<Camera class="h-5 w-5 text-card-foreground" />
|
|
200
|
-
</button>
|
|
201
|
-
{/if}
|
|
202
|
-
|
|
203
|
-
<!-- Fullscreen Toggle -->
|
|
204
|
-
{#if config.showFullscreenButton}
|
|
205
|
-
<button
|
|
206
|
-
class="h-10 w-10 shadow-lg hover:shadow-xl flex items-center justify-center rounded-lg border border-border bg-card/90 transition-all hover:bg-card active:scale-95"
|
|
207
|
-
onclick={toggleFullscreen}
|
|
208
|
-
title={isFullscreen ? 'Exit fullscreen' : 'Enter fullscreen'}
|
|
209
|
-
aria-label={isFullscreen ? 'Exit fullscreen' : 'Enter fullscreen'}
|
|
210
|
-
>
|
|
211
|
-
{#if isFullscreen}
|
|
212
|
-
<Minimize class="h-5 w-5 text-card-foreground" />
|
|
213
|
-
{:else}
|
|
214
|
-
<Maximize class="h-5 w-5 text-card-foreground" />
|
|
215
|
-
{/if}
|
|
216
|
-
</button>
|
|
217
|
-
{/if}
|
|
218
|
-
|
|
219
|
-
<!-- Scene Manager Toggle -->
|
|
220
|
-
{#if config.showSceneManager}
|
|
221
|
-
<button
|
|
222
|
-
class="h-10 w-10 shadow-lg hover:shadow-xl flex items-center justify-center rounded-lg border transition-all active:scale-95 {sceneManagerOpen
|
|
223
|
-
? 'border-muted-foreground/40 bg-secondary/40 hover:bg-secondary/60'
|
|
224
|
-
: 'border-muted-foreground/50 bg-card/90 hover:bg-secondary/80'}"
|
|
225
|
-
onclick={() => (sceneManagerOpen = !sceneManagerOpen)}
|
|
226
|
-
title={sceneManagerOpen ? 'Hide scene manager' : 'Show scene manager'}
|
|
227
|
-
aria-label="Toggle scene manager"
|
|
249
|
+
<DropdownMenu.Root>
|
|
250
|
+
<DropdownMenu.Trigger
|
|
251
|
+
class="h-10 w-10 shadow-lg hover:shadow-xl flex items-center justify-center rounded-lg border border-border bg-card/90 transition-all hover:bg-card active:scale-95 data-[state=open]:bg-secondary/60"
|
|
252
|
+
title="Viewer tools"
|
|
253
|
+
aria-label="Viewer tools"
|
|
228
254
|
>
|
|
229
|
-
<
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
255
|
+
<Settings2 class="h-5 w-5 text-card-foreground" />
|
|
256
|
+
</DropdownMenu.Trigger>
|
|
257
|
+
<DropdownMenu.Portal>
|
|
258
|
+
<DropdownMenu.Content
|
|
259
|
+
sideOffset={6}
|
|
260
|
+
align="start"
|
|
261
|
+
class="data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 min-w-44 p-1 shadow-md z-10001 rounded-md border bg-popover text-popover-foreground"
|
|
262
|
+
>
|
|
263
|
+
<!-- Camera -->
|
|
264
|
+
<DropdownMenu.Item class={itemClass} onSelect={toggleProjection}>
|
|
265
|
+
{#if projection === 'perspective'}
|
|
266
|
+
<Square class="h-4 w-4" />
|
|
267
|
+
Switch to 2D
|
|
268
|
+
{:else}
|
|
269
|
+
<Box class="h-4 w-4" />
|
|
270
|
+
Switch to 3D
|
|
271
|
+
{/if}
|
|
272
|
+
</DropdownMenu.Item>
|
|
273
|
+
|
|
274
|
+
<DropdownMenu.Item class={itemClass} onSelect={() => fitToView?.()}>
|
|
275
|
+
<Frame class="h-4 w-4" />
|
|
276
|
+
Fit to view
|
|
277
|
+
</DropdownMenu.Item>
|
|
278
|
+
|
|
279
|
+
<DropdownMenu.Sub>
|
|
280
|
+
<DropdownMenu.SubTrigger class="{itemClass} data-[state=open]:bg-muted">
|
|
281
|
+
<Box class="h-4 w-4" />
|
|
282
|
+
<span class="flex-1">Views</span>
|
|
283
|
+
<ChevronRight class="h-4 w-4 text-muted-foreground" />
|
|
284
|
+
</DropdownMenu.SubTrigger>
|
|
285
|
+
<DropdownMenu.SubContent
|
|
286
|
+
sideOffset={4}
|
|
287
|
+
class="min-w-32 p-1 shadow-md z-10001 rounded-md border bg-popover text-popover-foreground"
|
|
288
|
+
>
|
|
289
|
+
{#each VIEW_PRESETS as { preset, label } (preset)}
|
|
290
|
+
<DropdownMenu.Item
|
|
291
|
+
class="px-2 py-1.5 text-sm flex cursor-pointer items-center rounded-sm transition-colors outline-none select-none hover:bg-muted focus:bg-muted"
|
|
292
|
+
onSelect={() => setView(preset)}
|
|
293
|
+
>
|
|
294
|
+
{label}
|
|
295
|
+
</DropdownMenu.Item>
|
|
296
|
+
{/each}
|
|
297
|
+
</DropdownMenu.SubContent>
|
|
298
|
+
</DropdownMenu.Sub>
|
|
299
|
+
|
|
300
|
+
<DropdownMenu.Item
|
|
301
|
+
closeOnSelect={false}
|
|
302
|
+
class="{itemClass} {measureActive ? 'text-primary' : ''}"
|
|
303
|
+
onSelect={toggleMeasure}
|
|
304
|
+
>
|
|
305
|
+
<Ruler class="h-4 w-4" />
|
|
306
|
+
<span class="flex-1">Measure</span>
|
|
307
|
+
{#if measureActive}
|
|
308
|
+
<Check class="h-4 w-4" />
|
|
309
|
+
{/if}
|
|
310
|
+
</DropdownMenu.Item>
|
|
311
|
+
|
|
312
|
+
<!-- Scene tools -->
|
|
313
|
+
{#if config.showSceneManager || config.showScreenshotButton || config.showFullscreenButton}
|
|
314
|
+
<DropdownMenu.Separator class="my-1 h-px bg-border" />
|
|
315
|
+
{/if}
|
|
316
|
+
|
|
317
|
+
{#if config.showSceneManager}
|
|
318
|
+
<DropdownMenu.Item
|
|
319
|
+
closeOnSelect={false}
|
|
320
|
+
class="{itemClass} {sceneManagerOpen ? 'text-primary' : ''}"
|
|
321
|
+
onSelect={() => (sceneManagerOpen = !sceneManagerOpen)}
|
|
322
|
+
>
|
|
323
|
+
<Layers class="h-4 w-4" />
|
|
324
|
+
<span class="flex-1">Scene manager</span>
|
|
325
|
+
{#if sceneManagerOpen}
|
|
326
|
+
<Check class="h-4 w-4" />
|
|
327
|
+
{/if}
|
|
328
|
+
</DropdownMenu.Item>
|
|
329
|
+
{/if}
|
|
330
|
+
|
|
331
|
+
{#if config.showScreenshotButton}
|
|
332
|
+
<DropdownMenu.Item class={itemClass} onSelect={downloadScreenshot}>
|
|
333
|
+
<Camera class="h-4 w-4" />
|
|
334
|
+
Screenshot
|
|
335
|
+
</DropdownMenu.Item>
|
|
336
|
+
{/if}
|
|
337
|
+
|
|
338
|
+
{#if config.showFullscreenButton}
|
|
339
|
+
<DropdownMenu.Item class={itemClass} onSelect={toggleFullscreen}>
|
|
340
|
+
{#if isFullscreen}
|
|
341
|
+
<Minimize class="h-4 w-4" />
|
|
342
|
+
Exit fullscreen
|
|
343
|
+
{:else}
|
|
344
|
+
<Maximize class="h-4 w-4" />
|
|
345
|
+
Fullscreen
|
|
346
|
+
{/if}
|
|
347
|
+
</DropdownMenu.Item>
|
|
348
|
+
{/if}
|
|
349
|
+
</DropdownMenu.Content>
|
|
350
|
+
</DropdownMenu.Portal>
|
|
351
|
+
</DropdownMenu.Root>
|
|
234
352
|
</div>
|
|
235
353
|
{/if}
|
|
236
354
|
</div>
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// Published public API of @selvajs/ui
|
|
3
|
+
// ============================================================================
|
|
4
|
+
//
|
|
5
|
+
// This is the ONLY surface npm consumers of @selvajs/ui see (package.json maps
|
|
6
|
+
// the "." export's `svelte`/`types` conditions at this file's build output).
|
|
7
|
+
//
|
|
8
|
+
// Scope: the compute-app SDK — everything an external host app needs to embed a
|
|
9
|
+
// Grasshopper-driven app (ComputeApp), drive solves, and wire pre-step
|
|
10
|
+
// producers. Verified against real consumers (parafa, parapet): they import
|
|
11
|
+
// ComputeApp + its types, the solve seam, and external/storage. Nothing else.
|
|
12
|
+
//
|
|
13
|
+
// Deliberately NOT public: design-system primitives (Button, Card, Dialog, …),
|
|
14
|
+
// page-chrome layout (AppShell, SideNav, …), toast/Toaster, ThemeSwitcher,
|
|
15
|
+
// DataTable, contexts/composables, cn/randomId. These remain importable from
|
|
16
|
+
// the full barrel (./index.ts) INSIDE the monorepo via the "@selvajs/source"
|
|
17
|
+
// export condition, but never ship to npm. If an external consumer ever needs a
|
|
18
|
+
// primitive directly, promote it here explicitly rather than re-exporting the
|
|
19
|
+
// whole primitives barrel.
|
|
20
|
+
|
|
21
|
+
// Compute app (schema + viewer + solve controls composed into a runnable app)
|
|
22
|
+
export { default as AppLayout } from './components/compute/AppLayout.svelte';
|
|
23
|
+
export { default as ComputeApp } from './components/compute/ComputeApp.svelte';
|
|
24
|
+
|
|
25
|
+
// Full-screen states a host app renders
|
|
26
|
+
export { default as ErrorScreen } from './components/ErrorScreen.svelte';
|
|
27
|
+
|
|
28
|
+
// Solve Session seam (transport-agnostic value/lifecycle state machine + its
|
|
29
|
+
// driver interface). Exported so transports outside this package can satisfy
|
|
30
|
+
// SolveDriver and drive a session. See CONTEXT.md.
|
|
31
|
+
export {
|
|
32
|
+
createSolveSession,
|
|
33
|
+
createRequestResponseDriver,
|
|
34
|
+
type SolveSession,
|
|
35
|
+
type SolveSessionArgs,
|
|
36
|
+
type SolveDriver,
|
|
37
|
+
type SolveReporter
|
|
38
|
+
} from './compute/createSolveSession.svelte';
|
|
39
|
+
|
|
40
|
+
// Client-slot context type (host apps render their own cell for client-sourced
|
|
41
|
+
// inputs; parafa uses ClientSlotArgs).
|
|
42
|
+
export type { ClientSlotArgs, ClientSlot } from './contexts/clientSlotContext.svelte';
|
|
43
|
+
|
|
44
|
+
// Pre-step producer transit storage (parafa wires producers via these).
|
|
45
|
+
export * from './external/storage';
|
|
46
|
+
|
|
47
|
+
// Schema utilities a ComputeApp host reasonably needs to read/shape values.
|
|
48
|
+
export * from './schema/defaults';
|
|
49
|
+
export * from './schema/traversal';
|
|
50
|
+
export * from './schema/dynamic-value-list';
|
|
51
|
+
|
|
52
|
+
// UI-facing runtime types (not from schema)
|
|
53
|
+
export type { ActionButton } from './types/actionButton';
|
|
54
|
+
export type { SolveFn, SolveResult } from './types/solveFn';
|
|
55
|
+
export { DEFAULT_PRESET_LABELS, type PresetLabels } from './types/presetLabels';
|