@pascal-app/editor 1.0.0-beta.5 → 1.0.0
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/LICENSE +21 -0
- package/bunfig.toml +4 -0
- package/package.json +13 -7
- package/scripts/generate-print-golden-house.ts +89 -0
- package/src/components/editor/bake-exporter.tsx +1 -0
- package/src/components/editor/camera-dragging-lifecycle.test.ts +41 -0
- package/src/components/editor/camera-dragging-lifecycle.ts +10 -2
- package/src/components/editor/capture-camera-rig.tsx +59 -0
- package/src/components/editor/custom-camera-controls.tsx +20 -11
- package/src/components/editor/editor-layout-mobile.tsx +6 -6
- package/src/components/editor/editor-layout-v2.tsx +9 -2
- package/src/components/editor/export-manager.tsx +175 -33
- package/src/components/editor/first-person/build-collider-world.test.ts +30 -0
- package/src/components/editor/first-person-controls.tsx +180 -12
- package/src/components/editor/floating-action-menu.tsx +42 -51
- package/src/components/editor/floorplan-panel.tsx +76 -12
- package/src/components/editor/grid.tsx +12 -7
- package/src/components/editor/group-actions.ts +34 -50
- package/src/components/editor/group-move-3d.ts +51 -21
- package/src/components/editor/group-transform-shared.test.ts +78 -0
- package/src/components/editor/group-transform-shared.ts +74 -7
- package/src/components/editor/handles/handle-arrow-raycast.test.ts +86 -0
- package/src/components/editor/handles/handle-arrow.tsx +109 -48
- package/src/components/editor/handles/linear-resize-drag.ts +67 -0
- package/src/components/editor/handles/resize-snap.test.ts +46 -0
- package/src/components/editor/handles/resize-snap.ts +10 -1
- package/src/components/editor/handles/use-handle-drag.ts +18 -1
- package/src/components/editor/index.tsx +140 -21
- package/src/components/editor/node-action-menu.tsx +14 -1
- package/src/components/editor/node-arrow-handles.tsx +47 -58
- package/src/components/editor/selection-manager.tsx +104 -42
- package/src/components/editor/snapshot-capture-overlay.tsx +298 -39
- package/src/components/editor/snapshot-capture.test.ts +422 -0
- package/src/components/editor/snapshot-capture.ts +145 -0
- package/src/components/editor/thumbnail-generator.tsx +284 -237
- package/src/components/editor/use-floorplan-background-placement.ts +53 -5
- package/src/components/editor/wall-move-side-handles.tsx +299 -7
- package/src/components/editor-2d/floorplan-group-move.tsx +67 -33
- package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +3 -1
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +129 -38
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +14 -1
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.test.tsx +16 -0
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +2 -0
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +52 -3
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +196 -1
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +180 -32
- package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +402 -0
- package/src/components/systems/ceiling/ceiling-bracket-batch.ts +371 -0
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +347 -0
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +281 -305
- package/src/components/systems/roof/roof-edit-system.test.tsx +120 -0
- package/src/components/systems/roof/roof-edit-system.tsx +10 -19
- package/src/components/systems/selection-affordance-manager.tsx +59 -9
- package/src/components/systems/selection-affordance-services.ts +19 -0
- package/src/components/systems/stair/stair-edit-system.tsx +5 -2
- package/src/components/tools/fence/fence-drafting.test.ts +100 -0
- package/src/components/tools/fence/fence-drafting.ts +6 -25
- package/src/components/tools/item/draft-creation.test.ts +12 -0
- package/src/components/tools/item/draft-creation.ts +10 -0
- package/src/components/tools/item/face-host-commit.test.ts +65 -0
- package/src/components/tools/item/face-host-commit.ts +56 -0
- package/src/components/tools/item/face-host-preview.test.ts +77 -0
- package/src/components/tools/item/face-host-preview.ts +82 -0
- package/src/components/tools/item/move-tool.tsx +4 -3
- package/src/components/tools/item/placement-math.test.ts +11 -1
- package/src/components/tools/item/placement-math.ts +0 -20
- package/src/components/tools/item/placement-strategies.test.ts +331 -4
- package/src/components/tools/item/placement-strategies.ts +135 -2
- package/src/components/tools/item/placement-surface.test.ts +61 -0
- package/src/components/tools/item/placement-surface.ts +24 -0
- package/src/components/tools/item/placement-types.ts +6 -0
- package/src/components/tools/item/test-face-host.ts +121 -0
- package/src/components/tools/item/use-draft-node.test.tsx +336 -0
- package/src/components/tools/item/use-draft-node.ts +26 -4
- package/src/components/tools/item/use-placement-coordinator.tsx +428 -153
- package/src/components/tools/registered-tool-install-lifecycle.test.tsx +256 -0
- package/src/components/tools/registry/move-registry-node-tool.test.ts +10 -0
- package/src/components/tools/registry/move-registry-node-tool.tsx +306 -110
- package/src/components/tools/registry-tool-context.tsx +30 -0
- package/src/components/tools/select/box-select-tool.tsx +3 -1
- package/src/components/tools/select/marquee-footprint.test.ts +53 -0
- package/src/components/tools/select/marquee-footprint.ts +29 -0
- package/src/components/tools/shared/placement-box.tsx +57 -7
- package/src/components/tools/shared/placement-dimension-guides.tsx +121 -0
- package/src/components/tools/shared/pointer-support-cap.test.ts +319 -17
- package/src/components/tools/shared/pointer-support-cap.ts +45 -8
- package/src/components/tools/site/terrain-sculpt-grid.tsx +9 -5
- package/src/components/tools/site/terrain-sculpt-tool.tsx +1 -1
- package/src/components/tools/stair/stair-tool.tsx +127 -74
- package/src/components/tools/tool-manager.tsx +25 -8
- package/src/components/tools/wall/wall-drafting.test.ts +320 -2
- package/src/components/tools/wall/wall-drafting.ts +23 -114
- package/src/components/ui/action-menu/control-modes.tsx +9 -6
- package/src/components/ui/action-menu/index.tsx +11 -1
- package/src/components/ui/command-palette/editor-commands.tsx +9 -12
- package/src/components/ui/command-palette/index.tsx +7 -2
- package/src/components/ui/controls/material-paint-panel.tsx +6 -4
- package/src/components/ui/controls/material-picker.tsx +4 -4
- package/src/components/ui/controls/scene-material-list.tsx +6 -5
- package/src/components/ui/controls/segmented-control.tsx +8 -1
- package/src/components/ui/controls/slider-control.tsx +19 -3
- package/src/components/ui/controls/toggle-control.tsx +18 -5
- package/src/components/ui/controls/tool-options-panel.tsx +126 -0
- package/src/components/ui/floating-level-selector.tsx +22 -5
- package/src/components/ui/helpers/contextual-helper-panel.tsx +9 -0
- package/src/components/ui/helpers/helper-manager.tsx +47 -11
- package/src/components/ui/helpers/item-helper.tsx +1 -1
- package/src/components/ui/helpers/registered-tool-helper.tsx +21 -0
- package/src/components/ui/panels/homogeneous-selection.test.ts +80 -0
- package/src/components/ui/panels/homogeneous-selection.ts +47 -0
- package/src/components/ui/panels/mobile-selection-bar.tsx +11 -5
- package/src/components/ui/panels/multi-field-value.test.ts +209 -0
- package/src/components/ui/panels/multi-field-value.ts +163 -0
- package/src/components/ui/panels/multi-height-mode.tsx +185 -0
- package/src/components/ui/panels/multi-parametric-inspector.tsx +263 -0
- package/src/components/ui/panels/multi-selection-panel.tsx +48 -37
- package/src/components/ui/panels/node-display.ts +4 -0
- package/src/components/ui/panels/panel-manager.tsx +77 -3
- package/src/components/ui/panels/parametric-field-control.tsx +173 -0
- package/src/components/ui/panels/parametric-field-utils.ts +18 -0
- package/src/components/ui/panels/parametric-inspector.tsx +29 -162
- package/src/components/ui/panels/reference-panel.tsx +31 -1
- package/src/components/ui/primitives/shortcut-token.tsx +23 -1
- package/src/components/ui/scene-loader.tsx +35 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +390 -36
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +20 -23
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.test.ts +140 -0
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +109 -0
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +19 -5
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +116 -46
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +5 -2
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +28 -11
- package/src/components/ui/sidebar/panels/site-panel/tree-node.test.ts +8 -0
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +14 -6
- package/src/components/ui/sidebar/tab-bar.tsx +2 -0
- package/src/components/ui/sidebar/use-plugin-panels.tsx +17 -2
- package/src/components/viewer/viewer-scene-header.tsx +2 -1
- package/src/components/viewer/viewer-stage.tsx +4 -1
- package/src/components/viewer-overlay.tsx +1 -22
- package/src/components/viewer-zone-system.tsx +6 -2
- package/src/components/walkthrough-hud.tsx +19 -10
- package/src/hooks/use-auto-save.test.ts +73 -1
- package/src/hooks/use-auto-save.ts +76 -5
- package/src/hooks/use-ceiling-events.test.ts +122 -0
- package/src/hooks/use-drag-action.ts +17 -3
- package/src/hooks/use-grid-events.test.ts +240 -0
- package/src/hooks/use-grid-events.ts +190 -11
- package/src/hooks/use-keyboard.test.ts +244 -0
- package/src/hooks/use-keyboard.ts +129 -62
- package/src/hooks/use-registered-tool-enabled.ts +20 -0
- package/src/hooks/use-save-shortcut.ts +33 -0
- package/src/index.tsx +82 -4
- package/src/lib/active-placement-surface.test.ts +16 -0
- package/src/lib/active-placement-surface.ts +14 -0
- package/src/lib/ceiling-surface-raycast.test.ts +64 -0
- package/src/lib/ceiling-surface-raycast.ts +12 -0
- package/src/lib/contextual-help-extension.ts +17 -0
- package/src/lib/continuation.test.ts +11 -0
- package/src/lib/continuation.ts +8 -1
- package/src/lib/direct-manipulation.test.ts +132 -0
- package/src/lib/direct-manipulation.ts +38 -1
- package/src/lib/editor-api.ts +6 -3
- package/src/lib/elevation-guides.test.ts +28 -1
- package/src/lib/elevation-guides.ts +22 -7
- package/src/lib/floating-menu-scale.test.ts +29 -0
- package/src/lib/floating-menu-scale.ts +14 -0
- package/src/lib/floorplan/apply-alignment.test.ts +47 -2
- package/src/lib/floorplan/apply-alignment.ts +10 -1
- package/src/lib/floorplan/floorplan-export.test.ts +397 -1
- package/src/lib/floorplan/floorplan-export.tsx +111 -16
- package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +26 -0
- package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +32 -24
- package/src/lib/floorplan/floorplan-readonly.ts +1 -0
- package/src/lib/floorplan-grid-event-point.test.ts +29 -0
- package/src/lib/floorplan-grid-event-point.ts +13 -0
- package/src/lib/glb-export.test.ts +1169 -21
- package/src/lib/glb-export.ts +629 -101
- package/src/lib/grid-event-presentation.ts +16 -0
- package/src/lib/history.test.ts +528 -147
- package/src/lib/history.ts +81 -7
- package/src/lib/host-tree-children.test.ts +27 -0
- package/src/lib/host-tree-children.ts +79 -0
- package/src/lib/interaction/hot-set.test.ts +10 -1
- package/src/lib/interaction/hot-set.ts +3 -1
- package/src/lib/interaction/overlay-policy.test.ts +33 -1
- package/src/lib/interaction/overlay-policy.ts +16 -0
- package/src/lib/interaction/registered-drafting.ts +36 -0
- package/src/lib/interaction/scope.ts +31 -0
- package/src/lib/level-print-export.test.ts +484 -0
- package/src/lib/level-print-export.ts +526 -0
- package/src/lib/local-project-presentation-persistence.test.ts +302 -0
- package/src/lib/local-project-presentation-persistence.ts +335 -0
- package/src/lib/material-paint.ts +2 -0
- package/src/lib/model-export.ts +29 -0
- package/src/lib/paint-preview-owner.ts +71 -0
- package/src/lib/planar-cursor-placement.test.ts +178 -1
- package/src/lib/planar-cursor-placement.ts +61 -5
- package/src/lib/plugin-panels.test.ts +92 -1
- package/src/lib/plugin-panels.ts +48 -0
- package/src/lib/portable-export.test.ts +366 -0
- package/src/lib/portable-export.ts +969 -0
- package/src/lib/print-3mf.test.ts +86 -0
- package/src/lib/print-3mf.ts +161 -0
- package/src/lib/print-content-scope.test.ts +107 -0
- package/src/lib/print-content-scope.ts +90 -0
- package/src/lib/print-export.test.ts +294 -0
- package/src/lib/print-export.ts +735 -0
- package/src/lib/print-feature-thickness.test.ts +147 -0
- package/src/lib/print-feature-thickness.ts +180 -0
- package/src/lib/print-golden-house.test-fixture.ts +352 -0
- package/src/lib/print-golden-house.test.ts +249 -0
- package/src/lib/print-roof-solids.test.ts +133 -0
- package/src/lib/print-roof-solids.ts +630 -0
- package/src/lib/print-shell-compiler-baseline.test.ts +419 -0
- package/src/lib/print-shell-compiler-baseline.ts +225 -0
- package/src/lib/print-shell-compiler-manifold-core.ts +330 -0
- package/src/lib/print-shell-compiler-manifold-worker.ts +187 -0
- package/src/lib/print-shell-compiler-manifold.worker.ts +20 -0
- package/src/lib/print-shell-compiler-mesh-data.ts +32 -0
- package/src/lib/print-shell-compiler-protocol.ts +30 -0
- package/src/lib/print-shell-compiler.ts +273 -0
- package/src/lib/print-wall-solids.test.ts +194 -0
- package/src/lib/print-wall-solids.ts +328 -0
- package/src/lib/rigid-plan-svg-transform.test.ts +35 -0
- package/src/lib/rigid-plan-svg-transform.ts +24 -0
- package/src/lib/scene.ts +22 -8
- package/src/lib/selection-routing.test.ts +105 -1
- package/src/lib/selection-routing.ts +19 -0
- package/src/lib/sfx-bus.ts +4 -0
- package/src/lib/sfx-player.ts +8 -0
- package/src/lib/snapping-mode.test.ts +32 -4
- package/src/lib/snapping-mode.ts +11 -7
- package/src/lib/usdz-export.ts +38 -0
- package/src/lib/walkthrough-pointer-lock.ts +47 -0
- package/src/store/live-draft-preview-stores.test.ts +3 -0
- package/src/store/terrain-sculpt-mode.test.ts +3 -6
- package/src/store/tool-mode.test.ts +87 -0
- package/src/store/use-camera-hint-focus.ts +48 -0
- package/src/store/use-editor.tsx +299 -188
- package/src/store/use-floorplan-draft-preview.ts +9 -0
- package/src/store/use-interaction-scope.test.ts +23 -1
- package/src/store/use-interaction-scope.ts +63 -2
- package/src/store/use-placement-preview.ts +56 -3
- package/src/components/tools/roof/roof-tool.tsx +0 -656
- package/src/components/ui/helpers/roof-helper.tsx +0 -14
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import type {
|
|
3
|
+
ViewerPresentationConfiguration,
|
|
4
|
+
ViewerPresentationContribution,
|
|
5
|
+
} from '@pascal-app/viewer'
|
|
6
|
+
import {
|
|
7
|
+
createLocalProjectPresentationPersistence,
|
|
8
|
+
getLocalProjectPresentationStorageKey,
|
|
9
|
+
} from './local-project-presentation-persistence'
|
|
10
|
+
|
|
11
|
+
const CONTRIBUTION_ID = 'test:environment:presentation'
|
|
12
|
+
|
|
13
|
+
type TestSnapshot = {
|
|
14
|
+
version: 1
|
|
15
|
+
value: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
class MemoryStorage {
|
|
19
|
+
readonly values = new Map<string, string>()
|
|
20
|
+
|
|
21
|
+
getItem(key: string): string | null {
|
|
22
|
+
return this.values.get(key) ?? null
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
setItem(key: string, value: string): void {
|
|
26
|
+
this.values.set(key, value)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
class TestConfiguration implements ViewerPresentationConfiguration {
|
|
31
|
+
private readonly listeners = new Set<() => void>()
|
|
32
|
+
value = 'default'
|
|
33
|
+
|
|
34
|
+
getSnapshot = (): TestSnapshot => ({ version: 1, value: this.value })
|
|
35
|
+
|
|
36
|
+
restore = (snapshot: unknown): void => {
|
|
37
|
+
if (
|
|
38
|
+
typeof snapshot !== 'object' ||
|
|
39
|
+
snapshot === null ||
|
|
40
|
+
!('version' in snapshot) ||
|
|
41
|
+
snapshot.version !== 1 ||
|
|
42
|
+
!('value' in snapshot) ||
|
|
43
|
+
typeof snapshot.value !== 'string'
|
|
44
|
+
) {
|
|
45
|
+
throw new Error('Invalid test presentation snapshot')
|
|
46
|
+
}
|
|
47
|
+
this.value = snapshot.value
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
reset = (): void => {
|
|
51
|
+
this.value = 'default'
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
subscribe = (onChange: () => void): (() => void) => {
|
|
55
|
+
this.listeners.add(onChange)
|
|
56
|
+
return () => this.listeners.delete(onChange)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
setValue(value: string): void {
|
|
60
|
+
this.value = value
|
|
61
|
+
for (const listener of this.listeners) listener()
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
class TestRegistry {
|
|
66
|
+
private readonly listeners = new Set<() => void>()
|
|
67
|
+
private contributions: ViewerPresentationContribution[] = []
|
|
68
|
+
|
|
69
|
+
getSnapshot = (): readonly ViewerPresentationContribution[] => this.contributions
|
|
70
|
+
|
|
71
|
+
subscribe = (onChange: () => void): (() => void) => {
|
|
72
|
+
this.listeners.add(onChange)
|
|
73
|
+
return () => this.listeners.delete(onChange)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
register(configuration: ViewerPresentationConfiguration): void {
|
|
77
|
+
this.contributions = [
|
|
78
|
+
{
|
|
79
|
+
id: CONTRIBUTION_ID,
|
|
80
|
+
component: async () => ({ default: () => null }),
|
|
81
|
+
configuration,
|
|
82
|
+
},
|
|
83
|
+
]
|
|
84
|
+
for (const listener of this.listeners) listener()
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
uninstall(): void {
|
|
88
|
+
this.contributions = []
|
|
89
|
+
for (const listener of this.listeners) listener()
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function readStoredValue(storage: MemoryStorage, projectId: string): string {
|
|
94
|
+
const raw = storage.getItem(getLocalProjectPresentationStorageKey(projectId))
|
|
95
|
+
if (!raw) throw new Error(`Missing presentation sidecar for ${projectId}`)
|
|
96
|
+
const sidecar = JSON.parse(raw) as {
|
|
97
|
+
contributions: Record<string, TestSnapshot>
|
|
98
|
+
}
|
|
99
|
+
const snapshot = sidecar.contributions[CONTRIBUTION_ID]
|
|
100
|
+
if (!snapshot) throw new Error(`Missing ${CONTRIBUTION_ID} snapshot`)
|
|
101
|
+
return snapshot.value
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
describe('local project presentation persistence', () => {
|
|
105
|
+
test('persists anonymous settings when assigning a new first project id', () => {
|
|
106
|
+
const storage = new MemoryStorage()
|
|
107
|
+
const registry = new TestRegistry()
|
|
108
|
+
const configuration = new TestConfiguration()
|
|
109
|
+
registry.register(configuration)
|
|
110
|
+
const persistence = createLocalProjectPresentationPersistence({
|
|
111
|
+
registry,
|
|
112
|
+
storage,
|
|
113
|
+
pageHideTarget: null,
|
|
114
|
+
flushDelayMs: 60_000,
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
persistence.switchProject(null)
|
|
118
|
+
configuration.setValue('anonymous atmosphere')
|
|
119
|
+
persistence.switchProject('first-project')
|
|
120
|
+
|
|
121
|
+
expect(configuration.value).toBe('anonymous atmosphere')
|
|
122
|
+
persistence.flush()
|
|
123
|
+
expect(readStoredValue(storage, 'first-project')).toBe('anonymous atmosphere')
|
|
124
|
+
persistence.dispose()
|
|
125
|
+
|
|
126
|
+
const reloadedRegistry = new TestRegistry()
|
|
127
|
+
const reloadedConfiguration = new TestConfiguration()
|
|
128
|
+
reloadedRegistry.register(reloadedConfiguration)
|
|
129
|
+
const reloadedPersistence = createLocalProjectPresentationPersistence({
|
|
130
|
+
registry: reloadedRegistry,
|
|
131
|
+
storage,
|
|
132
|
+
pageHideTarget: null,
|
|
133
|
+
})
|
|
134
|
+
reloadedPersistence.switchProject('first-project')
|
|
135
|
+
|
|
136
|
+
expect(reloadedConfiguration.value).toBe('anonymous atmosphere')
|
|
137
|
+
reloadedPersistence.dispose()
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
test('restores a stored project instead of overwriting it with anonymous settings', () => {
|
|
141
|
+
const storage = new MemoryStorage()
|
|
142
|
+
const storedRegistry = new TestRegistry()
|
|
143
|
+
const storedConfiguration = new TestConfiguration()
|
|
144
|
+
storedRegistry.register(storedConfiguration)
|
|
145
|
+
const storedPersistence = createLocalProjectPresentationPersistence({
|
|
146
|
+
registry: storedRegistry,
|
|
147
|
+
storage,
|
|
148
|
+
pageHideTarget: null,
|
|
149
|
+
flushDelayMs: 60_000,
|
|
150
|
+
})
|
|
151
|
+
storedPersistence.switchProject('existing-project')
|
|
152
|
+
storedConfiguration.setValue('stored atmosphere')
|
|
153
|
+
storedPersistence.flush()
|
|
154
|
+
storedPersistence.dispose()
|
|
155
|
+
|
|
156
|
+
const registry = new TestRegistry()
|
|
157
|
+
const configuration = new TestConfiguration()
|
|
158
|
+
registry.register(configuration)
|
|
159
|
+
const persistence = createLocalProjectPresentationPersistence({
|
|
160
|
+
registry,
|
|
161
|
+
storage,
|
|
162
|
+
pageHideTarget: null,
|
|
163
|
+
flushDelayMs: 60_000,
|
|
164
|
+
})
|
|
165
|
+
persistence.switchProject(null)
|
|
166
|
+
configuration.setValue('anonymous atmosphere')
|
|
167
|
+
persistence.switchProject('existing-project')
|
|
168
|
+
|
|
169
|
+
expect(configuration.value).toBe('stored atmosphere')
|
|
170
|
+
persistence.flush()
|
|
171
|
+
expect(readStoredValue(storage, 'existing-project')).toBe('stored atmosphere')
|
|
172
|
+
persistence.dispose()
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
test('flushes the old project before restoring defaults or the next project', () => {
|
|
176
|
+
const storage = new MemoryStorage()
|
|
177
|
+
const registry = new TestRegistry()
|
|
178
|
+
const configuration = new TestConfiguration()
|
|
179
|
+
registry.register(configuration)
|
|
180
|
+
const persistence = createLocalProjectPresentationPersistence({
|
|
181
|
+
registry,
|
|
182
|
+
storage,
|
|
183
|
+
pageHideTarget: null,
|
|
184
|
+
flushDelayMs: 60_000,
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
persistence.switchProject('project-a')
|
|
188
|
+
configuration.setValue('project A atmosphere')
|
|
189
|
+
persistence.switchProject('project-b')
|
|
190
|
+
|
|
191
|
+
expect(readStoredValue(storage, 'project-a')).toBe('project A atmosphere')
|
|
192
|
+
expect(configuration.value).toBe('default')
|
|
193
|
+
|
|
194
|
+
configuration.setValue('project B atmosphere')
|
|
195
|
+
persistence.switchProject('project-a')
|
|
196
|
+
|
|
197
|
+
expect(readStoredValue(storage, 'project-b')).toBe('project B atmosphere')
|
|
198
|
+
expect(configuration.value).toBe('project A atmosphere')
|
|
199
|
+
persistence.dispose()
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
test('restores a project after reload and flushes pending changes on dispose', () => {
|
|
203
|
+
const storage = new MemoryStorage()
|
|
204
|
+
const firstRegistry = new TestRegistry()
|
|
205
|
+
const firstConfiguration = new TestConfiguration()
|
|
206
|
+
firstRegistry.register(firstConfiguration)
|
|
207
|
+
const firstPersistence = createLocalProjectPresentationPersistence({
|
|
208
|
+
registry: firstRegistry,
|
|
209
|
+
storage,
|
|
210
|
+
pageHideTarget: null,
|
|
211
|
+
flushDelayMs: 60_000,
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
firstPersistence.switchProject('reloadable')
|
|
215
|
+
firstConfiguration.setValue('saved sky')
|
|
216
|
+
firstPersistence.dispose()
|
|
217
|
+
|
|
218
|
+
const reloadedRegistry = new TestRegistry()
|
|
219
|
+
const reloadedConfiguration = new TestConfiguration()
|
|
220
|
+
reloadedRegistry.register(reloadedConfiguration)
|
|
221
|
+
const reloadedPersistence = createLocalProjectPresentationPersistence({
|
|
222
|
+
registry: reloadedRegistry,
|
|
223
|
+
storage,
|
|
224
|
+
pageHideTarget: null,
|
|
225
|
+
})
|
|
226
|
+
reloadedPersistence.switchProject('reloadable')
|
|
227
|
+
|
|
228
|
+
expect(reloadedConfiguration.value).toBe('saved sky')
|
|
229
|
+
reloadedPersistence.dispose()
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
test('recovers corrupted presentation data without touching scene storage', () => {
|
|
233
|
+
const storage = new MemoryStorage()
|
|
234
|
+
const sceneStorageKey = 'pascal-editor-scene'
|
|
235
|
+
const sceneData = '{"nodes":{"site":{"type":"site"}}}'
|
|
236
|
+
storage.setItem(sceneStorageKey, sceneData)
|
|
237
|
+
storage.setItem(getLocalProjectPresentationStorageKey('corrupted'), '{not-json')
|
|
238
|
+
|
|
239
|
+
const registry = new TestRegistry()
|
|
240
|
+
const configuration = new TestConfiguration()
|
|
241
|
+
configuration.value = 'leaked from another project'
|
|
242
|
+
registry.register(configuration)
|
|
243
|
+
const persistence = createLocalProjectPresentationPersistence({
|
|
244
|
+
registry,
|
|
245
|
+
storage,
|
|
246
|
+
pageHideTarget: null,
|
|
247
|
+
flushDelayMs: 60_000,
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
persistence.switchProject('corrupted')
|
|
251
|
+
expect(configuration.value).toBe('default')
|
|
252
|
+
persistence.flush()
|
|
253
|
+
|
|
254
|
+
expect(readStoredValue(storage, 'corrupted')).toBe('default')
|
|
255
|
+
expect(storage.getItem(sceneStorageKey)).toBe(sceneData)
|
|
256
|
+
persistence.dispose()
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
test('retains configuration while a contribution is unregistered and registered again', () => {
|
|
260
|
+
const storage = new MemoryStorage()
|
|
261
|
+
const registry = new TestRegistry()
|
|
262
|
+
const firstConfiguration = new TestConfiguration()
|
|
263
|
+
registry.register(firstConfiguration)
|
|
264
|
+
const persistence = createLocalProjectPresentationPersistence({
|
|
265
|
+
registry,
|
|
266
|
+
storage,
|
|
267
|
+
pageHideTarget: null,
|
|
268
|
+
flushDelayMs: 60_000,
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
persistence.switchProject('plugin-cycle')
|
|
272
|
+
firstConfiguration.setValue('retained atmosphere')
|
|
273
|
+
registry.uninstall()
|
|
274
|
+
|
|
275
|
+
const reinstalledConfiguration = new TestConfiguration()
|
|
276
|
+
registry.register(reinstalledConfiguration)
|
|
277
|
+
|
|
278
|
+
expect(reinstalledConfiguration.value).toBe('retained atmosphere')
|
|
279
|
+
persistence.dispose()
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
test('flushes pending changes on pagehide', () => {
|
|
283
|
+
const storage = new MemoryStorage()
|
|
284
|
+
const registry = new TestRegistry()
|
|
285
|
+
const configuration = new TestConfiguration()
|
|
286
|
+
const pageHideTarget = new EventTarget()
|
|
287
|
+
registry.register(configuration)
|
|
288
|
+
const persistence = createLocalProjectPresentationPersistence({
|
|
289
|
+
registry,
|
|
290
|
+
storage,
|
|
291
|
+
pageHideTarget,
|
|
292
|
+
flushDelayMs: 60_000,
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
persistence.switchProject('pagehide')
|
|
296
|
+
configuration.setValue('last slider value')
|
|
297
|
+
pageHideTarget.dispatchEvent(new Event('pagehide'))
|
|
298
|
+
|
|
299
|
+
expect(readStoredValue(storage, 'pagehide')).toBe('last slider value')
|
|
300
|
+
persistence.dispose()
|
|
301
|
+
})
|
|
302
|
+
})
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { type ViewerPresentationContribution, viewerPresentationRegistry } from '@pascal-app/viewer'
|
|
4
|
+
import { z } from 'zod'
|
|
5
|
+
|
|
6
|
+
export const LOCAL_PROJECT_PRESENTATION_STORAGE_KEY_PREFIX = 'pascal:project-presentation:v1:'
|
|
7
|
+
const LOCAL_PROJECT_PRESENTATION_VERSION = 1 as const
|
|
8
|
+
const DEFAULT_FLUSH_DELAY_MS = 250
|
|
9
|
+
|
|
10
|
+
type PresentationConfiguration = NonNullable<ViewerPresentationContribution['configuration']>
|
|
11
|
+
|
|
12
|
+
type PresentationRegistry = {
|
|
13
|
+
getSnapshot: () => readonly ViewerPresentationContribution[]
|
|
14
|
+
subscribe: (onChange: () => void) => () => void
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
type PresentationStorage = {
|
|
18
|
+
getItem: (key: string) => string | null
|
|
19
|
+
setItem: (key: string, value: string) => void
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
type PageHideTarget = Pick<EventTarget, 'addEventListener' | 'removeEventListener'>
|
|
23
|
+
|
|
24
|
+
const LocalProjectPresentationSidecarSchema = z
|
|
25
|
+
.object({
|
|
26
|
+
version: z.literal(LOCAL_PROJECT_PRESENTATION_VERSION),
|
|
27
|
+
projectId: z.string(),
|
|
28
|
+
contributions: z.record(z.string(), z.unknown()),
|
|
29
|
+
})
|
|
30
|
+
.strict()
|
|
31
|
+
type LocalProjectPresentationSidecar = z.infer<typeof LocalProjectPresentationSidecarSchema>
|
|
32
|
+
|
|
33
|
+
type StoredSidecar = {
|
|
34
|
+
contributions: Record<string, unknown>
|
|
35
|
+
malformed: boolean
|
|
36
|
+
source: 'missing' | 'stored' | 'unavailable'
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type LocalProjectPresentationPersistence = {
|
|
40
|
+
switchProject: (projectId: string | null) => void
|
|
41
|
+
flush: () => void
|
|
42
|
+
dispose: () => void
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export type LocalProjectPresentationPersistenceOptions = {
|
|
46
|
+
registry?: PresentationRegistry
|
|
47
|
+
storage?: PresentationStorage | null
|
|
48
|
+
pageHideTarget?: PageHideTarget | null
|
|
49
|
+
flushDelayMs?: number
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function getLocalProjectPresentationStorageKey(projectId: string): string {
|
|
53
|
+
return `${LOCAL_PROJECT_PRESENTATION_STORAGE_KEY_PREFIX}${encodeURIComponent(projectId)}`
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function emptyContributions(): Record<string, unknown> {
|
|
57
|
+
return Object.create(null) as Record<string, unknown>
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function getBrowserStorage(): PresentationStorage | null {
|
|
61
|
+
if (typeof window === 'undefined') return null
|
|
62
|
+
try {
|
|
63
|
+
return window.localStorage
|
|
64
|
+
} catch {
|
|
65
|
+
return null
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function readSidecar(storage: PresentationStorage | null, projectId: string): StoredSidecar {
|
|
70
|
+
if (!storage) {
|
|
71
|
+
return {
|
|
72
|
+
contributions: emptyContributions(),
|
|
73
|
+
malformed: false,
|
|
74
|
+
source: 'unavailable',
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
let raw: string | null
|
|
79
|
+
try {
|
|
80
|
+
raw = storage.getItem(getLocalProjectPresentationStorageKey(projectId))
|
|
81
|
+
} catch {
|
|
82
|
+
return {
|
|
83
|
+
contributions: emptyContributions(),
|
|
84
|
+
malformed: false,
|
|
85
|
+
source: 'unavailable',
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (raw === null) {
|
|
89
|
+
return {
|
|
90
|
+
contributions: emptyContributions(),
|
|
91
|
+
malformed: false,
|
|
92
|
+
source: 'missing',
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
const parsed = LocalProjectPresentationSidecarSchema.safeParse(JSON.parse(raw))
|
|
98
|
+
if (!parsed.success || parsed.data.projectId !== projectId) {
|
|
99
|
+
return {
|
|
100
|
+
contributions: emptyContributions(),
|
|
101
|
+
malformed: true,
|
|
102
|
+
source: 'stored',
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
contributions: Object.assign(emptyContributions(), parsed.data.contributions),
|
|
107
|
+
malformed: false,
|
|
108
|
+
source: 'stored',
|
|
109
|
+
}
|
|
110
|
+
} catch {
|
|
111
|
+
return {
|
|
112
|
+
contributions: emptyContributions(),
|
|
113
|
+
malformed: true,
|
|
114
|
+
source: 'stored',
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
class LocalProjectPresentationPersistenceImpl implements LocalProjectPresentationPersistence {
|
|
120
|
+
private projectId: string | null = null
|
|
121
|
+
private projectInitialized = false
|
|
122
|
+
private contributions = emptyContributions()
|
|
123
|
+
private readonly configurations = new Map<
|
|
124
|
+
string,
|
|
125
|
+
{ configuration: PresentationConfiguration; unsubscribe: () => void }
|
|
126
|
+
>()
|
|
127
|
+
private readonly initializedConfigurationIds = new Set<string>()
|
|
128
|
+
private readonly registry: PresentationRegistry
|
|
129
|
+
private readonly storage: PresentationStorage | null
|
|
130
|
+
private readonly pageHideTarget: PageHideTarget | null
|
|
131
|
+
private readonly flushDelayMs: number
|
|
132
|
+
private readonly unsubscribeRegistry: () => void
|
|
133
|
+
private flushTimer: ReturnType<typeof setTimeout> | undefined
|
|
134
|
+
private dirty = false
|
|
135
|
+
private suppressWrites = false
|
|
136
|
+
private disposed = false
|
|
137
|
+
|
|
138
|
+
constructor(options: LocalProjectPresentationPersistenceOptions) {
|
|
139
|
+
this.registry = options.registry ?? viewerPresentationRegistry
|
|
140
|
+
this.storage = options.storage === undefined ? getBrowserStorage() : options.storage
|
|
141
|
+
this.pageHideTarget =
|
|
142
|
+
options.pageHideTarget === undefined
|
|
143
|
+
? typeof window === 'undefined'
|
|
144
|
+
? null
|
|
145
|
+
: window
|
|
146
|
+
: options.pageHideTarget
|
|
147
|
+
this.flushDelayMs = options.flushDelayMs ?? DEFAULT_FLUSH_DELAY_MS
|
|
148
|
+
this.unsubscribeRegistry = this.registry.subscribe(this.reconcileRegistry)
|
|
149
|
+
this.pageHideTarget?.addEventListener('pagehide', this.flushOnPageHide)
|
|
150
|
+
this.reconcileRegistry()
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
switchProject(projectId: string | null): void {
|
|
154
|
+
const nextProjectId = projectId && projectId.length > 0 ? projectId : null
|
|
155
|
+
if (this.projectInitialized && nextProjectId === this.projectId) return
|
|
156
|
+
|
|
157
|
+
const assigningAnonymousPresentation =
|
|
158
|
+
this.projectInitialized && this.projectId === null && nextProjectId !== null
|
|
159
|
+
|
|
160
|
+
this.flush()
|
|
161
|
+
this.projectId = nextProjectId
|
|
162
|
+
this.projectInitialized = true
|
|
163
|
+
this.initializedConfigurationIds.clear()
|
|
164
|
+
|
|
165
|
+
const stored = nextProjectId
|
|
166
|
+
? readSidecar(this.storage, nextProjectId)
|
|
167
|
+
: {
|
|
168
|
+
contributions: emptyContributions(),
|
|
169
|
+
malformed: false,
|
|
170
|
+
source: 'missing' as const,
|
|
171
|
+
}
|
|
172
|
+
this.contributions = stored.contributions
|
|
173
|
+
|
|
174
|
+
if (assigningAnonymousPresentation && stored.source === 'missing') {
|
|
175
|
+
let capturedConfiguration = false
|
|
176
|
+
for (const [id, entry] of this.configurations) {
|
|
177
|
+
this.initializedConfigurationIds.add(id)
|
|
178
|
+
try {
|
|
179
|
+
this.contributions[id] = entry.configuration.getSnapshot()
|
|
180
|
+
capturedConfiguration = true
|
|
181
|
+
} catch {
|
|
182
|
+
// Leave the live configuration intact when an optional adapter cannot export.
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (capturedConfiguration) this.markDirty()
|
|
186
|
+
return
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
let recoveredInvalidConfiguration = stored.malformed
|
|
190
|
+
for (const [id, entry] of this.configurations) {
|
|
191
|
+
if (!this.restoreConfiguration(id, entry.configuration)) {
|
|
192
|
+
recoveredInvalidConfiguration = true
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (recoveredInvalidConfiguration) this.markDirty()
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
flush = (): void => {
|
|
200
|
+
if (this.flushTimer !== undefined) {
|
|
201
|
+
clearTimeout(this.flushTimer)
|
|
202
|
+
this.flushTimer = undefined
|
|
203
|
+
}
|
|
204
|
+
const projectId = this.projectId
|
|
205
|
+
if (!this.dirty || !projectId || !this.storage) return
|
|
206
|
+
|
|
207
|
+
const contributions = Object.assign(emptyContributions(), this.contributions)
|
|
208
|
+
for (const [id, entry] of this.configurations) {
|
|
209
|
+
if (!this.initializedConfigurationIds.has(id)) continue
|
|
210
|
+
try {
|
|
211
|
+
contributions[id] = entry.configuration.getSnapshot()
|
|
212
|
+
} catch {
|
|
213
|
+
// Retain the last valid snapshot when one contribution cannot export.
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const sidecar: LocalProjectPresentationSidecar = {
|
|
218
|
+
version: LOCAL_PROJECT_PRESENTATION_VERSION,
|
|
219
|
+
projectId,
|
|
220
|
+
contributions,
|
|
221
|
+
}
|
|
222
|
+
try {
|
|
223
|
+
this.storage.setItem(
|
|
224
|
+
getLocalProjectPresentationStorageKey(projectId),
|
|
225
|
+
JSON.stringify(sidecar),
|
|
226
|
+
)
|
|
227
|
+
this.contributions = contributions
|
|
228
|
+
this.dirty = false
|
|
229
|
+
} catch {
|
|
230
|
+
// Storage denial, quota exhaustion, and non-serializable plugin data are non-fatal.
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
dispose(): void {
|
|
235
|
+
if (this.disposed) return
|
|
236
|
+
this.disposed = true
|
|
237
|
+
this.flush()
|
|
238
|
+
this.unsubscribeRegistry()
|
|
239
|
+
this.pageHideTarget?.removeEventListener('pagehide', this.flushOnPageHide)
|
|
240
|
+
for (const entry of this.configurations.values()) entry.unsubscribe()
|
|
241
|
+
this.configurations.clear()
|
|
242
|
+
this.initializedConfigurationIds.clear()
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
private readonly flushOnPageHide = (): void => {
|
|
246
|
+
this.flush()
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
private readonly reconcileRegistry = (): void => {
|
|
250
|
+
if (this.disposed) return
|
|
251
|
+
|
|
252
|
+
const current = new Map<string, PresentationConfiguration>()
|
|
253
|
+
for (const contribution of this.registry.getSnapshot()) {
|
|
254
|
+
if (contribution.configuration) {
|
|
255
|
+
current.set(contribution.id, contribution.configuration)
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
for (const [id, entry] of this.configurations) {
|
|
260
|
+
if (current.get(id) === entry.configuration) continue
|
|
261
|
+
if (this.initializedConfigurationIds.has(id)) {
|
|
262
|
+
this.captureConfiguration(id, entry.configuration)
|
|
263
|
+
}
|
|
264
|
+
entry.unsubscribe()
|
|
265
|
+
this.configurations.delete(id)
|
|
266
|
+
this.initializedConfigurationIds.delete(id)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
for (const [id, configuration] of current) {
|
|
270
|
+
if (this.configurations.has(id)) continue
|
|
271
|
+
|
|
272
|
+
let unsubscribe: () => void = () => undefined
|
|
273
|
+
try {
|
|
274
|
+
unsubscribe = configuration.subscribe(() => {
|
|
275
|
+
if (!this.suppressWrites) this.markDirty()
|
|
276
|
+
})
|
|
277
|
+
} catch {
|
|
278
|
+
// A broken optional adapter must not affect the editor or other contributions.
|
|
279
|
+
}
|
|
280
|
+
this.configurations.set(id, { configuration, unsubscribe })
|
|
281
|
+
|
|
282
|
+
if (this.projectInitialized && !this.restoreConfiguration(id, configuration)) {
|
|
283
|
+
this.markDirty()
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
private restoreConfiguration(id: string, configuration: PresentationConfiguration): boolean {
|
|
289
|
+
let valid = true
|
|
290
|
+
this.suppressWrites = true
|
|
291
|
+
try {
|
|
292
|
+
if (Object.hasOwn(this.contributions, id)) {
|
|
293
|
+
try {
|
|
294
|
+
configuration.restore(this.contributions[id])
|
|
295
|
+
} catch {
|
|
296
|
+
valid = false
|
|
297
|
+
delete this.contributions[id]
|
|
298
|
+
try {
|
|
299
|
+
configuration.reset()
|
|
300
|
+
} catch {}
|
|
301
|
+
}
|
|
302
|
+
} else {
|
|
303
|
+
try {
|
|
304
|
+
configuration.reset()
|
|
305
|
+
} catch {}
|
|
306
|
+
}
|
|
307
|
+
} finally {
|
|
308
|
+
this.suppressWrites = false
|
|
309
|
+
this.initializedConfigurationIds.add(id)
|
|
310
|
+
}
|
|
311
|
+
return valid
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
private captureConfiguration(id: string, configuration: PresentationConfiguration): void {
|
|
315
|
+
try {
|
|
316
|
+
this.contributions[id] = configuration.getSnapshot()
|
|
317
|
+
this.markDirty()
|
|
318
|
+
} catch {
|
|
319
|
+
// Preserve the prior snapshot if the retiring adapter cannot export.
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
private markDirty(): void {
|
|
324
|
+
if (!this.projectId || this.suppressWrites || this.disposed) return
|
|
325
|
+
this.dirty = true
|
|
326
|
+
if (this.flushTimer !== undefined) clearTimeout(this.flushTimer)
|
|
327
|
+
this.flushTimer = setTimeout(this.flush, this.flushDelayMs)
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
export function createLocalProjectPresentationPersistence(
|
|
332
|
+
options: LocalProjectPresentationPersistenceOptions = {},
|
|
333
|
+
): LocalProjectPresentationPersistence {
|
|
334
|
+
return new LocalProjectPresentationPersistenceImpl(options)
|
|
335
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { GlbExportOptions } from './glb-export'
|
|
2
|
+
|
|
3
|
+
export type ModelExportFormat = 'glb' | 'usdz' | 'stl' | 'obj' | 'print-stl' | 'print-3mf'
|
|
4
|
+
|
|
5
|
+
export type ModelExportOptions = Pick<
|
|
6
|
+
GlbExportOptions,
|
|
7
|
+
'onlyVisible' | 'excludedNodeTypes' | 'includedPresentationIds'
|
|
8
|
+
> & {
|
|
9
|
+
download?: boolean
|
|
10
|
+
printScale?: number
|
|
11
|
+
printScope?: 'whole' | 'levels'
|
|
12
|
+
printContent?: 'structure' | 'everything'
|
|
13
|
+
printBase?: 'none' | 'plinth'
|
|
14
|
+
printMinimumFeatureMm?: number
|
|
15
|
+
printPlinthMarginMm?: number
|
|
16
|
+
printPlinthThicknessMm?: number
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type ModelExportArtifact = {
|
|
20
|
+
blob: Blob
|
|
21
|
+
filename: string
|
|
22
|
+
metadata?: unknown
|
|
23
|
+
warnings?: readonly string[]
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type ModelExport = (
|
|
27
|
+
format?: ModelExportFormat,
|
|
28
|
+
options?: ModelExportOptions,
|
|
29
|
+
) => Promise<ModelExportArtifact | null>
|