@ifc-lite/viewer 1.21.0 → 1.22.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.
Files changed (87) hide show
  1. package/.turbo/turbo-build.log +57 -50
  2. package/.turbo/turbo-typecheck.log +1 -1
  3. package/CHANGELOG.md +10 -0
  4. package/dist/assets/arrow-fie-E7fe.js +20 -0
  5. package/dist/assets/ascii-points-source-bTjLVmUX.js +1 -0
  6. package/dist/assets/{basketViewActivator-Bzw51jhm.js → basketViewActivator-EHAhHlwN.js} +12 -13
  7. package/dist/assets/bcf-Bhx-K17f.js +281 -0
  8. package/dist/assets/{browser-C5TFR7sH.js → browser-CVf8ATeW.js} +6 -6
  9. package/dist/assets/cesium-B4ZIU9jS.js +17742 -0
  10. package/dist/assets/decode-worker-CYqSjk1n.js +172 -0
  11. package/dist/assets/e57-source-CQHxE8n3.js +1 -0
  12. package/dist/assets/emscripten-module.browser-DcFZLAUx.js +1 -0
  13. package/dist/assets/exporters-KTio0Tdm.js +5723 -0
  14. package/dist/assets/geometry-controller.worker-Cm2P_EJr.js +7 -0
  15. package/dist/assets/geometry.worker-DchLBqZ8.js +1 -0
  16. package/dist/assets/{ids-B7AXEv7h.js → ids-CS7VCFin.js} +5 -5
  17. package/dist/assets/ifc-lite-C6wEhXa6.js +7 -0
  18. package/dist/assets/{ifc-lite_bg-DlKs5-yM.wasm → ifc-lite_bg-CSeT3fNI.wasm} +0 -0
  19. package/dist/assets/{ifc-lite_bg-PqmRe3Ph.wasm → ifc-lite_bg-ns4cSnX2.wasm} +0 -0
  20. package/dist/assets/{index-DVNSvEMh.js → index-8k9h-ANq.js} +60997 -59926
  21. package/dist/assets/index-BZC2YaOP.css +1 -0
  22. package/dist/assets/index-HqAIQkr6.js +22 -0
  23. package/dist/assets/inline-worker-BpBzlmd6.js +1 -0
  24. package/dist/assets/las-BW6LIc_j.js +1 -0
  25. package/dist/assets/las-source-C_IGrgRq.js +1 -0
  26. package/dist/assets/laz-source-jj3xI5Y4.js +125 -0
  27. package/dist/assets/maplibre-gl-C4LXKM6c.js +808 -0
  28. package/dist/assets/{native-bridge-BiD01jI9.js → native-bridge-DNrEhx2R.js} +5 -8
  29. package/dist/assets/{parser.worker-Bnbrl6gy.js → parser.worker-BcjkIo89.js} +2 -2
  30. package/dist/assets/pcd-source-Ck0UnVDn.js +3 -0
  31. package/dist/assets/ply-source-C8jjyzxE.js +4 -0
  32. package/dist/assets/{exporters-u0sz2Upj.js → sandbox-BSn5MyEJ.js} +11745 -7412
  33. package/dist/assets/{server-client-DP8fMPY9.js → server-client-D-kU2XAF.js} +4 -4
  34. package/dist/assets/{three-CDRZThFA.js → three-DwNDHx9-.js} +163 -171
  35. package/dist/assets/wasm-bridge-Cha08LdC.js +1 -0
  36. package/dist/assets/{workerHelpers-CBbWSJmd.js → workerHelpers-pUUnk9Wc.js} +1 -1
  37. package/dist/assets/zip-BJqVbRkU.js +2 -0
  38. package/dist/index.html +10 -12
  39. package/package.json +11 -11
  40. package/src/components/mcp/PlaygroundChat.tsx +90 -52
  41. package/src/components/viewer/CesiumOverlay.tsx +150 -91
  42. package/src/components/viewer/CesiumPlacementEditor.tsx +1009 -0
  43. package/src/components/viewer/ChatPanel.tsx +76 -93
  44. package/src/components/viewer/EntityContextMenu.tsx +68 -10
  45. package/src/components/viewer/MainToolbar.tsx +33 -3
  46. package/src/components/viewer/ViewportContainer.tsx +70 -16
  47. package/src/components/viewer/ViewportOverlays.tsx +2 -98
  48. package/src/components/viewer/chat/ByokKeyModal.tsx +338 -0
  49. package/src/components/viewer/chat/ByokStreamingPill.tsx +62 -0
  50. package/src/components/viewer/chat/ByokTrustDiagram.tsx +192 -0
  51. package/src/components/viewer/properties/GeoreferencingPanel.tsx +49 -52
  52. package/src/components/viewer/properties/ModelMetadataPanel.tsx +55 -44
  53. package/src/components/viewer/selectionHandlers.ts +7 -1
  54. package/src/lib/geo/cesium-bridge.ts +86 -50
  55. package/src/lib/geo/cesium-placement.test.ts +244 -0
  56. package/src/lib/geo/cesium-placement.ts +231 -0
  57. package/src/lib/geo/effective-georef.test.ts +74 -1
  58. package/src/lib/geo/effective-georef.ts +40 -93
  59. package/src/lib/geo/geo-scale.ts +104 -0
  60. package/src/lib/geo/reproject.test.ts +130 -0
  61. package/src/lib/geo/reproject.ts +37 -12
  62. package/src/lib/geo/terrain-elevation.ts +198 -89
  63. package/src/lib/lens/adapter.ts +52 -6
  64. package/src/lib/llm/clipboard-detect.test.ts +150 -0
  65. package/src/lib/llm/clipboard-detect.ts +90 -0
  66. package/src/lib/llm/models.ts +28 -0
  67. package/src/lib/llm/stream-direct.ts +16 -4
  68. package/src/lib/llm/types.ts +8 -0
  69. package/src/services/playground-model.ts +55 -0
  70. package/src/store/index.ts +4 -5
  71. package/src/store/slices/cesiumSlice.ts +100 -19
  72. package/src/store.ts +3 -0
  73. package/dist/assets/arrow-CZ5kQ26f.js +0 -20
  74. package/dist/assets/bcf-4K724hw0.js +0 -281
  75. package/dist/assets/cesium-DUOzBlqv.js +0 -17817
  76. package/dist/assets/decode-worker-t2EGKAxO.js +0 -1708
  77. package/dist/assets/emscripten-module.browser-CY5t0Vfq.js +0 -1
  78. package/dist/assets/geometry-controller.worker-NH8pZmrU.js +0 -7
  79. package/dist/assets/geometry.worker-Bp4rW_R1.js +0 -1
  80. package/dist/assets/ifc-lite-DfZHk36-.js +0 -7
  81. package/dist/assets/index-CSWgTe1s.css +0 -1
  82. package/dist/assets/index-XwKzDuw6.js +0 -22
  83. package/dist/assets/maplibre-gl-CGLcoNXc.js +0 -811
  84. package/dist/assets/sandbox-DPD1ROr0.js +0 -9700
  85. package/dist/assets/wasm-bridge-CErti6zX.js +0 -1
  86. package/dist/assets/zip-DBEtpeu6.js +0 -12
  87. package/src/components/viewer/CesiumSettingsDialog.tsx +0 -100
@@ -139,6 +139,20 @@ export const FREE_MODELS: LLMModel[] = rawFreeModels.map(applyCapabilities);
139
139
  // Requests go directly from the browser to the provider (no server proxy).
140
140
 
141
141
  const ANTHROPIC_BYOK_MODELS: LLMModel[] = [
142
+ {
143
+ id: 'claude-opus-4-7',
144
+ name: 'Claude Opus 4.7',
145
+ provider: 'Anthropic',
146
+ tier: 'byok',
147
+ source: 'anthropic',
148
+ contextWindow: 1_000_000,
149
+ supportsImages: true,
150
+ supportsFileAttachments: true,
151
+ cost: '$$$',
152
+ // Opus 4.7 returns 400 if temperature/top_p/top_k are present.
153
+ // See `whats-new-claude-4-7` docs § Sampling parameters removed.
154
+ acceptsSamplingParams: false,
155
+ },
142
156
  {
143
157
  id: 'claude-opus-4-6',
144
158
  name: 'Claude Opus 4.6',
@@ -175,6 +189,20 @@ const ANTHROPIC_BYOK_MODELS: LLMModel[] = [
175
189
  ];
176
190
 
177
191
  const OPENAI_BYOK_MODELS: LLMModel[] = [
192
+ {
193
+ id: 'gpt-5.5',
194
+ name: 'GPT-5.5',
195
+ provider: 'OpenAI',
196
+ tier: 'byok',
197
+ source: 'openai',
198
+ contextWindow: 1_000_000,
199
+ supportsImages: true,
200
+ supportsFileAttachments: true,
201
+ cost: '$$$',
202
+ // GPT-5 reasoning family only accepts the default temperature (1).
203
+ // Sending any other value returns 400 from /v1/chat/completions.
204
+ acceptsSamplingParams: false,
205
+ },
178
206
  {
179
207
  id: 'gpt-5.4',
180
208
  name: 'GPT-5.4',
@@ -15,6 +15,7 @@
15
15
 
16
16
  import Anthropic from '@anthropic-ai/sdk';
17
17
  import { readSseStream, type StreamMessage, type StreamOptions } from './stream-client.js';
18
+ import { getModelById } from './models.js';
18
19
 
19
20
  const STREAM_REQUEST_TIMEOUT_MS = 45_000;
20
21
 
@@ -71,12 +72,19 @@ export async function streamAnthropicChat(
71
72
  dangerouslyAllowBrowser: true,
72
73
  });
73
74
 
75
+ // Opus 4.7+ returns 400 if `temperature` (or `top_p`/`top_k`) is present.
76
+ // We gate on the per-model `acceptsSamplingParams` flag in models.ts so
77
+ // future Claude models that adopt the same policy only need a flag bump,
78
+ // and Opus 4.6 / Sonnet 4.6 / Haiku 4.5 keep their tuned temperature.
79
+ const modelDef = getModelById(model);
80
+ const sendSamplingParams = modelDef?.acceptsSamplingParams !== false;
81
+
74
82
  let fullText = '';
75
83
  try {
76
84
  const stream = client.messages.stream({
77
85
  model,
78
86
  max_tokens: 8192,
79
- temperature: 0.3,
87
+ ...(sendSamplingParams ? { temperature: 0.3 } : {}),
80
88
  system: system || undefined,
81
89
  messages: toAnthropicMessages(messages),
82
90
  });
@@ -118,8 +126,6 @@ export async function streamAnthropicChat(
118
126
 
119
127
  // ── OpenAI ─────────────────────────────────────────────────────────────────
120
128
 
121
- import { getModelById } from './models.js';
122
-
123
129
  /**
124
130
  * Stream an OpenAI model. Automatically picks the right API:
125
131
  * - Chat Completions (`/v1/chat/completions`) for standard chat models
@@ -147,13 +153,19 @@ async function streamOpenAiChatCompletions(
147
153
  ? [{ role: 'system', content: system }, ...messages]
148
154
  : [...messages];
149
155
 
156
+ // GPT-5 reasoning models (gpt-5.5, gpt-5.5-pro) only accept the default
157
+ // temperature; sending any other value returns 400. Mirror the Anthropic
158
+ // path: when the model is flagged `acceptsSamplingParams: false`, omit.
159
+ const modelDef = getModelById(model);
160
+ const sendSamplingParams = modelDef?.acceptsSamplingParams !== false;
161
+
150
162
  const { response, cleanup } = await openAiFetch(
151
163
  'https://api.openai.com/v1/chat/completions',
152
164
  {
153
165
  model,
154
166
  messages: allMessages.map((m) => ({ role: m.role, content: m.content })),
155
167
  stream: true,
156
- temperature: 0.3,
168
+ ...(sendSamplingParams ? { temperature: 0.3 } : {}),
157
169
  max_completion_tokens: 8192,
158
170
  },
159
171
  apiKey,
@@ -163,6 +163,14 @@ export interface LLMModel {
163
163
  cost?: ModelCost;
164
164
  /** OpenAI API variant: 'chat' (default) or 'responses' (Codex-style models) */
165
165
  openaiApi?: 'chat' | 'responses';
166
+ /**
167
+ * Whether the model accepts the classic sampling parameters (`temperature`,
168
+ * `top_p`, `top_k`). Default: true. Set to `false` for models that reject
169
+ * them (Anthropic Claude Opus 4.7 and later — see
170
+ * https://platform.claude.com/docs/en/about-claude/models/whats-new-claude-4-7).
171
+ * When `false`, the stream client omits these params from the request body.
172
+ */
173
+ acceptsSamplingParams?: boolean;
166
174
  }
167
175
 
168
176
  export type ChatStatus = 'idle' | 'sending' | 'streaming' | 'error';
@@ -0,0 +1,55 @@
1
+ /* This Source Code Form is subject to the terms of the Mozilla Public
2
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
+
5
+ /**
6
+ * Per-tab persistence for the model the /mcp/playground chat uses.
7
+ *
8
+ * The playground driver calls Anthropic's `messages.create` with the native
9
+ * tools API, so the selected model must be Anthropic. We keep a separate
10
+ * preference from the viewer's `chatActiveModel` so the two pages can default
11
+ * differently — the viewer often runs on a free proxy model, while playground
12
+ * users have already opted into BYOK by being here.
13
+ */
14
+
15
+ import { getByokModelsForSource } from '@/lib/llm/models';
16
+
17
+ const STORAGE_KEY = 'ifc-lite:playground-model:v1';
18
+ const CHANGED_EVENT = 'ifc-lite:playground-model-changed';
19
+
20
+ /**
21
+ * Default fallback when nothing is in storage. Sonnet hits the sweet spot for
22
+ * tool-calling agentic loops — fast enough for 25 sequential tool calls,
23
+ * smart enough to pick the right tool. Opus 4.7 is better at planning but
24
+ * costs ~3x; Haiku is faster but more likely to mis-pick tools.
25
+ */
26
+ const FALLBACK_MODEL = 'claude-sonnet-4-6';
27
+
28
+ function isValidAnthropicModel(id: string): boolean {
29
+ return getByokModelsForSource('anthropic').some((m) => m.id === id);
30
+ }
31
+
32
+ export function getPlaygroundModel(): string {
33
+ try {
34
+ const stored = localStorage.getItem(STORAGE_KEY);
35
+ if (stored && isValidAnthropicModel(stored)) return stored;
36
+ } catch {
37
+ /* localStorage blocked / quota-exceeded — fall through to default */
38
+ }
39
+ return FALLBACK_MODEL;
40
+ }
41
+
42
+ export function setPlaygroundModel(modelId: string): void {
43
+ if (!isValidAnthropicModel(modelId)) return;
44
+ try {
45
+ localStorage.setItem(STORAGE_KEY, modelId);
46
+ } catch {
47
+ /* storage write failed — selection only lives for this tab session */
48
+ }
49
+ window.dispatchEvent(new Event(CHANGED_EVENT));
50
+ }
51
+
52
+ export function subscribePlaygroundModel(listener: () => void): () => void {
53
+ window.addEventListener(CHANGED_EVENT, listener);
54
+ return () => window.removeEventListener(CHANGED_EVENT, listener);
55
+ }
@@ -90,7 +90,7 @@ export type { ChatSlice } from './slices/chatSlice.js';
90
90
  export type { DesktopEntitlementSlice } from './slices/desktopEntitlementSlice.js';
91
91
 
92
92
  // Re-export Cesium types
93
- export type { CesiumSlice, CesiumDataSource } from './slices/cesiumSlice.js';
93
+ export type { CesiumSlice, CesiumDataSource, CesiumPlacementDraft } from './slices/cesiumSlice.js';
94
94
 
95
95
  // Re-export Schedule (4D) types + selectors
96
96
  export type { ScheduleSlice, ScheduleTimeRange, GanttTimeScale } from './slices/scheduleSlice.js';
@@ -271,13 +271,12 @@ const createViewerStore = () => create<ViewerState>()((...args) => ({
271
271
  cesiumAvailable: false,
272
272
  cesiumEnabled: false,
273
273
  cesiumTerrainHeight: null,
274
- // Default the clamp toggle ON so models authored at sea-level
275
- // OrthogonalHeight don't load buried below the 3D-tiles terrain on
276
- // first activation. Users can still uncheck it manually.
277
- cesiumTerrainClamp: true,
278
274
  cesiumSourceModelId: null,
279
275
  cesiumTerrainClipY: null,
280
276
  cesiumGlbLoaded: false,
277
+ cesiumPlacementEditMode: false,
278
+ cesiumPlacementDraftModelId: null,
279
+ cesiumPlacementDraft: null,
281
280
 
282
281
  // Drawing 2D
283
282
  drawing2D: null,
@@ -15,11 +15,19 @@
15
15
  */
16
16
 
17
17
  import type { StateCreator } from 'zustand';
18
+ import type { MapConversion } from '@ifc-lite/parser';
18
19
 
19
- export type CesiumDataSource =
20
- | 'osm-buildings' // Cesium OSM Buildings (free via Cesium ion)
21
- | 'bing-aerial' // Bing Maps aerial imagery draped on terrain
22
- | 'google-photorealistic'; // Google Photorealistic 3D Tiles (requires API key)
20
+ import { clearTerrainElevationCache } from '@/lib/geo/terrain-elevation';
21
+
22
+ export type CesiumDataSource = 'google-photorealistic';
23
+
24
+ export interface CesiumPlacementDraft {
25
+ eastings: number;
26
+ northings: number;
27
+ orthogonalHeight: number;
28
+ xAxisAbscissa: number;
29
+ xAxisOrdinate: number;
30
+ }
23
31
 
24
32
  export interface CesiumSlice {
25
33
  // State
@@ -31,16 +39,22 @@ export interface CesiumSlice {
31
39
  cesiumIonToken: string;
32
40
  /** Terrain enabled (Cesium World Terrain). */
33
41
  cesiumTerrainEnabled: boolean;
34
- /** Clamp model to terrain height at its geodetic position. */
35
- cesiumTerrainClamp: boolean;
36
42
  /** Terrain height at model position (queried from Cesium, meters). null = not yet queried. */
37
43
  cesiumTerrainHeight: number | null;
44
+ /** Human-readable source label for the sampled terrain height. */
45
+ cesiumTerrainSource: string | null;
38
46
  /** Model ID that the Cesium overlay is currently displaying. */
39
47
  cesiumSourceModelId: string | null;
40
48
  /** Terrain clip Y position in viewer space. When set, fragments below this Y are discarded. */
41
49
  cesiumTerrainClipY: number | null;
42
50
  /** Whether the GLB model has been loaded into Cesium (hides WebGPU overlay). */
43
51
  cesiumGlbLoaded: boolean;
52
+ /** Whether the direct placement editor is active. */
53
+ cesiumPlacementEditMode: boolean;
54
+ /** Source model currently associated with the placement draft. */
55
+ cesiumPlacementDraftModelId: string | null;
56
+ /** Preview placement values shown in Cesium before applying to IFC georeference. */
57
+ cesiumPlacementDraft: CesiumPlacementDraft | null;
44
58
 
45
59
  // Actions
46
60
  setCesiumAvailable: (available: boolean) => void;
@@ -49,11 +63,19 @@ export interface CesiumSlice {
49
63
  setCesiumDataSource: (source: CesiumDataSource) => void;
50
64
  setCesiumIonToken: (token: string) => void;
51
65
  setCesiumTerrainEnabled: (enabled: boolean) => void;
52
- setCesiumTerrainClamp: (clamp: boolean) => void;
53
66
  setCesiumTerrainHeight: (height: number | null) => void;
67
+ setCesiumTerrainSource: (source: string | null) => void;
54
68
  setCesiumSourceModelId: (modelId: string | null) => void;
55
69
  setCesiumTerrainClipY: (y: number | null) => void;
56
70
  setCesiumGlbLoaded: (loaded: boolean) => void;
71
+ setCesiumPlacementEditMode: (enabled: boolean) => void;
72
+ toggleCesiumPlacementEditMode: () => void;
73
+ beginCesiumPlacementDraft: (
74
+ modelId: string,
75
+ conversion: Pick<MapConversion, 'eastings' | 'northings' | 'orthogonalHeight' | 'xAxisAbscissa' | 'xAxisOrdinate'>,
76
+ ) => void;
77
+ updateCesiumPlacementDraft: (values: Partial<CesiumPlacementDraft>) => void;
78
+ resetCesiumPlacementDraft: () => void;
57
79
  }
58
80
 
59
81
  const STORAGE_KEY_ION_TOKEN = 'ifc-lite:cesium-ion-token';
@@ -80,13 +102,9 @@ function saveToStorage(key: string, value: string): void {
80
102
  } catch { /* storage unavailable */ }
81
103
  }
82
104
 
83
- const VALID_DATA_SOURCES = new Set<CesiumDataSource>(['osm-buildings', 'bing-aerial', 'google-photorealistic']);
84
-
85
105
  function loadDataSource(): CesiumDataSource {
86
- const stored = loadFromStorage(STORAGE_KEY_DATA_SOURCE, '');
87
- return VALID_DATA_SOURCES.has(stored as CesiumDataSource)
88
- ? (stored as CesiumDataSource)
89
- : 'google-photorealistic';
106
+ // Only Google Photorealistic is supported; upgrade any stale stored value.
107
+ return 'google-photorealistic';
90
108
  }
91
109
 
92
110
  /** Resolve the Cesium ion token: user override > build-time default */
@@ -101,27 +119,90 @@ export const createCesiumSlice: StateCreator<CesiumSlice, [], [], CesiumSlice> =
101
119
  cesiumDataSource: loadDataSource(),
102
120
  cesiumIonToken: resolveIonToken(),
103
121
  cesiumTerrainEnabled: true,
104
- cesiumTerrainClamp: true,
105
122
  cesiumTerrainHeight: null,
123
+ cesiumTerrainSource: null,
106
124
  cesiumSourceModelId: null,
107
125
  cesiumTerrainClipY: null,
108
126
  cesiumGlbLoaded: false,
127
+ cesiumPlacementEditMode: false,
128
+ cesiumPlacementDraftModelId: null,
129
+ cesiumPlacementDraft: null,
109
130
 
110
131
  setCesiumAvailable: (available) => set({ cesiumAvailable: available }),
111
132
  setCesiumEnabled: (enabled) => set({ cesiumEnabled: enabled }),
112
- toggleCesium: () => set((s) => ({ cesiumEnabled: !s.cesiumEnabled })),
133
+ toggleCesium: () => set((s) => ({
134
+ cesiumEnabled: !s.cesiumEnabled,
135
+ ...(s.cesiumEnabled
136
+ ? {
137
+ cesiumPlacementEditMode: false,
138
+ cesiumPlacementDraftModelId: null,
139
+ cesiumPlacementDraft: null,
140
+ }
141
+ : {}),
142
+ })),
113
143
  setCesiumDataSource: (source) => {
144
+ clearTerrainElevationCache();
114
145
  saveToStorage(STORAGE_KEY_DATA_SOURCE, source);
115
- set({ cesiumDataSource: source });
146
+ set({
147
+ cesiumDataSource: source,
148
+ cesiumTerrainHeight: null,
149
+ cesiumTerrainSource: null,
150
+ cesiumTerrainClipY: null,
151
+ });
116
152
  },
117
153
  setCesiumIonToken: (token) => {
154
+ clearTerrainElevationCache();
118
155
  saveToStorage(STORAGE_KEY_ION_TOKEN, token);
119
- set({ cesiumIonToken: token || DEFAULT_ION_TOKEN });
156
+ set({
157
+ cesiumIonToken: token || DEFAULT_ION_TOKEN,
158
+ cesiumTerrainHeight: null,
159
+ cesiumTerrainSource: null,
160
+ cesiumTerrainClipY: null,
161
+ });
162
+ },
163
+ setCesiumTerrainEnabled: (enabled) => {
164
+ clearTerrainElevationCache();
165
+ set({
166
+ cesiumTerrainEnabled: enabled,
167
+ cesiumTerrainHeight: null,
168
+ cesiumTerrainSource: null,
169
+ cesiumTerrainClipY: null,
170
+ });
120
171
  },
121
- setCesiumTerrainEnabled: (enabled) => set({ cesiumTerrainEnabled: enabled }),
122
- setCesiumTerrainClamp: (clamp) => set({ cesiumTerrainClamp: clamp }),
123
172
  setCesiumTerrainHeight: (height) => set({ cesiumTerrainHeight: height }),
173
+ setCesiumTerrainSource: (source) => set({ cesiumTerrainSource: source }),
124
174
  setCesiumSourceModelId: (modelId) => set({ cesiumSourceModelId: modelId }),
125
175
  setCesiumTerrainClipY: (y) => set({ cesiumTerrainClipY: y }),
126
176
  setCesiumGlbLoaded: (loaded) => set({ cesiumGlbLoaded: loaded }),
177
+ setCesiumPlacementEditMode: (enabled) => set({ cesiumPlacementEditMode: enabled }),
178
+ toggleCesiumPlacementEditMode: () => set((s) => ({
179
+ cesiumPlacementEditMode: !s.cesiumPlacementEditMode,
180
+ ...(!s.cesiumPlacementEditMode
181
+ ? {}
182
+ : {
183
+ cesiumPlacementDraftModelId: null,
184
+ cesiumPlacementDraft: null,
185
+ }),
186
+ })),
187
+ beginCesiumPlacementDraft: (modelId, conversion) => set({
188
+ cesiumPlacementDraftModelId: modelId,
189
+ cesiumPlacementDraft: {
190
+ eastings: conversion.eastings,
191
+ northings: conversion.northings,
192
+ orthogonalHeight: conversion.orthogonalHeight,
193
+ // IFC MapConversion's x-axis cos/sin pair is optional in the schema.
194
+ // When absent, the convention is "no rotation": cos=1, sin=0.
195
+ xAxisAbscissa: conversion.xAxisAbscissa ?? 1,
196
+ xAxisOrdinate: conversion.xAxisOrdinate ?? 0,
197
+ },
198
+ }),
199
+ updateCesiumPlacementDraft: (values) => set((state) => ({
200
+ cesiumPlacementDraft: state.cesiumPlacementDraft
201
+ ? { ...state.cesiumPlacementDraft, ...values }
202
+ : null,
203
+ })),
204
+ resetCesiumPlacementDraft: () => set({
205
+ cesiumPlacementDraftModelId: null,
206
+ cesiumPlacementDraft: null,
207
+ }),
127
208
  });
package/src/store.ts CHANGED
@@ -64,6 +64,9 @@ export { customPlaneCenter } from './store/slices/sectionSlice.js';
64
64
  export { loadLastSectionMode } from './store/slices/sectionSlice.js';
65
65
  export type { LastSectionMode } from './store/slices/sectionSlice.js';
66
66
 
67
+ // Re-export Cesium placement types for backwards-compatible '@/store' imports.
68
+ export type { CesiumPlacementDraft } from './store/slices/cesiumSlice.js';
69
+
67
70
  // Re-export Schedule (4D) types + helpers
68
71
  export type { ScheduleSlice, ScheduleTimeRange, GanttTimeScale } from './store/slices/scheduleSlice.js';
69
72
  export {