@ifc-lite/viewer 1.17.4 → 1.18.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 (196) hide show
  1. package/.turbo/turbo-build.log +20 -17
  2. package/.turbo/turbo-typecheck.log +1 -1
  3. package/CHANGELOG.md +630 -0
  4. package/DESKTOP_CONTRACT_VERSION +1 -1
  5. package/dist/assets/{basketViewActivator-BmnNtVfZ.js → basketViewActivator-Cm1QEk_R.js} +1 -1
  6. package/dist/assets/drawing-2d-DoxKMqbO.js +257 -0
  7. package/dist/assets/{exporters-ChAtBmlj.js → exporters-B_OBqIyD.js} +3479 -2845
  8. package/dist/assets/{geometry.worker-BQ0rzNo-.js → geometry.worker-xHHy-9DV.js} +1 -1
  9. package/dist/assets/ids-DQ5jY0E8.js +1 -0
  10. package/dist/assets/ifc-lite_bg-ADjKXSms.wasm +0 -0
  11. package/dist/assets/{index-Co8E2-FE.js → index-BKq-M3Mk.js} +55873 -40593
  12. package/dist/assets/index-COnQRuqY.css +1 -0
  13. package/dist/assets/{native-bridge-BRvbckFQ.js → native-bridge-SHXiQwFW.js} +104 -104
  14. package/dist/assets/sandbox-jez21HtV.js +9627 -0
  15. package/dist/assets/{server-client-BV8zHZ7Y.js → server-client-ncOQVNso.js} +1 -1
  16. package/dist/assets/{wasm-bridge-g01g7T9b.js → wasm-bridge-DyfBSB8z.js} +1 -1
  17. package/dist/index.html +8 -7
  18. package/index.html +1 -0
  19. package/package.json +13 -13
  20. package/src/App.tsx +16 -2
  21. package/src/apache-arrow.d.ts +30 -0
  22. package/src/components/viewer/AddElementPanel.tsx +758 -0
  23. package/src/components/viewer/BulkPropertyEditor.tsx +7 -0
  24. package/src/components/viewer/CesiumOverlay.tsx +62 -19
  25. package/src/components/viewer/ChatPanel.tsx +259 -93
  26. package/src/components/viewer/CommandPalette.tsx +56 -7
  27. package/src/components/viewer/EntityContextMenu.tsx +168 -4
  28. package/src/components/viewer/ExportChangesButton.tsx +25 -5
  29. package/src/components/viewer/ExportDialog.tsx +19 -1
  30. package/src/components/viewer/MainToolbar.tsx +73 -13
  31. package/src/components/viewer/PropertiesPanel.tsx +237 -23
  32. package/src/components/viewer/SearchInline.tsx +669 -0
  33. package/src/components/viewer/SearchModal.filter.builder.tsx +766 -0
  34. package/src/components/viewer/SearchModal.filter.tsx +514 -0
  35. package/src/components/viewer/SearchModal.text.tsx +388 -0
  36. package/src/components/viewer/SearchModal.tsx +235 -0
  37. package/src/components/viewer/SettingsPage.tsx +252 -101
  38. package/src/components/viewer/ThemeSwitch.tsx +63 -7
  39. package/src/components/viewer/ToolOverlays.tsx +5 -0
  40. package/src/components/viewer/ViewerLayout.tsx +25 -4
  41. package/src/components/viewer/Viewport.tsx +25 -3
  42. package/src/components/viewer/ViewportContainer.tsx +51 -64
  43. package/src/components/viewer/ViewportOverlays.tsx +5 -2
  44. package/src/components/viewer/annotations/AnnotationDropInput.tsx +203 -0
  45. package/src/components/viewer/annotations/AnnotationLayer.tsx +287 -0
  46. package/src/components/viewer/annotations/AnnotationPin.tsx +90 -0
  47. package/src/components/viewer/annotations/AnnotationPopover.tsx +296 -0
  48. package/src/components/viewer/bcf/BCFTopicDetail.tsx +4 -4
  49. package/src/components/viewer/chat/ModelSelector.tsx +90 -54
  50. package/src/components/viewer/lists/ListPanel.tsx +14 -21
  51. package/src/components/viewer/properties/GeoreferencingPanel.tsx +113 -51
  52. package/src/components/viewer/properties/LocationMap.tsx +9 -7
  53. package/src/components/viewer/properties/ModelMetadataPanel.tsx +1 -1
  54. package/src/components/viewer/properties/RawStepCard.tsx +332 -0
  55. package/src/components/viewer/properties/RawStepRow.tsx +261 -0
  56. package/src/components/viewer/properties/ScheduleCard.tsx +224 -0
  57. package/src/components/viewer/properties/TaskEditCard.tsx +510 -0
  58. package/src/components/viewer/properties/raw-step-format.ts +193 -0
  59. package/src/components/viewer/schedule/AnimationSettingsPopover.tsx +542 -0
  60. package/src/components/viewer/schedule/GanttDependencyArrows.tsx +89 -0
  61. package/src/components/viewer/schedule/GanttDragTooltip.tsx +48 -0
  62. package/src/components/viewer/schedule/GanttEmptyState.tsx +97 -0
  63. package/src/components/viewer/schedule/GanttPanel.tsx +295 -0
  64. package/src/components/viewer/schedule/GanttTaskBar.tsx +199 -0
  65. package/src/components/viewer/schedule/GanttTaskTree.tsx +250 -0
  66. package/src/components/viewer/schedule/GanttTimeline.tsx +305 -0
  67. package/src/components/viewer/schedule/GanttToolbar.tsx +406 -0
  68. package/src/components/viewer/schedule/GenerateAdvancedPanel.tsx +147 -0
  69. package/src/components/viewer/schedule/GenerateScheduleDialog.tsx +392 -0
  70. package/src/components/viewer/schedule/HeightStrategyPanel.tsx +120 -0
  71. package/src/components/viewer/schedule/generate-schedule.test.ts +439 -0
  72. package/src/components/viewer/schedule/generate-schedule.ts +648 -0
  73. package/src/components/viewer/schedule/schedule-animator.test.ts +452 -0
  74. package/src/components/viewer/schedule/schedule-animator.ts +488 -0
  75. package/src/components/viewer/schedule/schedule-selection.test.ts +148 -0
  76. package/src/components/viewer/schedule/schedule-selection.ts +163 -0
  77. package/src/components/viewer/schedule/schedule-utils.ts +223 -0
  78. package/src/components/viewer/schedule/useConstructionSequence.ts +156 -0
  79. package/src/components/viewer/schedule/useGanttBarDrag.test.ts +90 -0
  80. package/src/components/viewer/schedule/useGanttBarDrag.ts +305 -0
  81. package/src/components/viewer/schedule/useGanttSelection3DHighlight.ts +152 -0
  82. package/src/components/viewer/schedule/useOverlayCompositor.ts +108 -0
  83. package/src/components/viewer/selectionHandlers.ts +446 -0
  84. package/src/components/viewer/tools/AddElementOverlay.tsx +540 -0
  85. package/src/components/viewer/tools/SectionCapControls.tsx +237 -0
  86. package/src/components/viewer/tools/SectionPanel.tsx +39 -18
  87. package/src/components/viewer/useAnimationLoop.ts +9 -1
  88. package/src/components/viewer/useDuplicateShortcut.ts +77 -0
  89. package/src/components/viewer/useMouseControls.ts +9 -1
  90. package/src/components/viewer/useRenderUpdates.ts +1 -1
  91. package/src/hooks/ids/idsDataAccessor.ts +60 -24
  92. package/src/hooks/ingest/viewerModelIngest.ts +7 -2
  93. package/src/hooks/useIfcFederation.ts +326 -71
  94. package/src/hooks/useIfcLoader.ts +23 -10
  95. package/src/hooks/useKeyboardShortcuts.ts +25 -0
  96. package/src/hooks/useSandbox.ts +1 -1
  97. package/src/hooks/useSearchIndex.ts +125 -0
  98. package/src/hooks/useViewControls.ts +13 -5
  99. package/src/index.css +550 -10
  100. package/src/lib/desktop-entitlement.ts +2 -4
  101. package/src/lib/geo/cesium-bridge.ts +15 -7
  102. package/src/lib/geo/effective-georef.test.ts +73 -0
  103. package/src/lib/geo/effective-georef.ts +111 -0
  104. package/src/lib/geo/reproject.ts +105 -19
  105. package/src/lib/llm/byok-guard.test.ts +77 -0
  106. package/src/lib/llm/byok-guard.ts +39 -0
  107. package/src/lib/llm/free-models.test.ts +0 -6
  108. package/src/lib/llm/models.ts +104 -42
  109. package/src/lib/llm/stream-client.ts +74 -110
  110. package/src/lib/llm/stream-direct.test.ts +130 -0
  111. package/src/lib/llm/stream-direct.ts +316 -0
  112. package/src/lib/llm/system-prompt.test.ts +14 -0
  113. package/src/lib/llm/system-prompt.ts +102 -1
  114. package/src/lib/llm/types.ts +20 -2
  115. package/src/lib/recent-files.ts +38 -4
  116. package/src/lib/scripts/templates/bim-globals.d.ts +136 -114
  117. package/src/lib/scripts/templates/construction-schedule.ts +223 -0
  118. package/src/lib/scripts/templates.ts +7 -0
  119. package/src/lib/search/common-ifc-types.ts +36 -0
  120. package/src/lib/search/filter-evaluate.test.ts +537 -0
  121. package/src/lib/search/filter-evaluate.ts +610 -0
  122. package/src/lib/search/filter-rules.test.ts +119 -0
  123. package/src/lib/search/filter-rules.ts +198 -0
  124. package/src/lib/search/filter-schema.test.ts +233 -0
  125. package/src/lib/search/filter-schema.ts +146 -0
  126. package/src/lib/search/recent-searches.test.ts +116 -0
  127. package/src/lib/search/recent-searches.ts +93 -0
  128. package/src/lib/search/result-export.test.ts +101 -0
  129. package/src/lib/search/result-export.ts +104 -0
  130. package/src/lib/search/saved-filters.test.ts +118 -0
  131. package/src/lib/search/saved-filters.ts +154 -0
  132. package/src/lib/search/tier0-scan.test.ts +196 -0
  133. package/src/lib/search/tier0-scan.ts +237 -0
  134. package/src/lib/search/tier1-index.test.ts +242 -0
  135. package/src/lib/search/tier1-index.ts +448 -0
  136. package/src/main.tsx +1 -10
  137. package/src/sdk/adapters/export-adapter.test.ts +434 -1
  138. package/src/sdk/adapters/export-adapter.ts +404 -1
  139. package/src/sdk/adapters/export-schedule-splice.test.ts +127 -0
  140. package/src/sdk/adapters/export-schedule-splice.ts +87 -0
  141. package/src/sdk/adapters/model-compat.ts +8 -2
  142. package/src/sdk/adapters/schedule-adapter.ts +73 -0
  143. package/src/sdk/adapters/store-adapter.ts +201 -0
  144. package/src/sdk/adapters/visibility-adapter.ts +3 -0
  145. package/src/sdk/local-backend.ts +16 -8
  146. package/src/services/api-keys.ts +73 -0
  147. package/src/services/desktop-export.ts +3 -1
  148. package/src/services/desktop-native-metadata.ts +41 -18
  149. package/src/services/file-dialog.ts +4 -1
  150. package/src/services/tauri-modules.d.ts +25 -0
  151. package/src/store/basketVisibleSet.ts +3 -0
  152. package/src/store/constants.ts +20 -2
  153. package/src/store/globalId.ts +4 -1
  154. package/src/store/index.ts +82 -6
  155. package/src/store/slices/addElementMeshes.ts +365 -0
  156. package/src/store/slices/addElementSlice.ts +275 -0
  157. package/src/store/slices/annotationsSlice.test.ts +133 -0
  158. package/src/store/slices/annotationsSlice.ts +251 -0
  159. package/src/store/slices/cesiumSlice.ts +5 -0
  160. package/src/store/slices/chatSlice.test.ts +6 -76
  161. package/src/store/slices/chatSlice.ts +17 -58
  162. package/src/store/slices/dataSlice.test.ts +23 -4
  163. package/src/store/slices/dataSlice.ts +1 -1
  164. package/src/store/slices/modelSlice.test.ts +67 -9
  165. package/src/store/slices/modelSlice.ts +39 -7
  166. package/src/store/slices/mutationSlice.ts +964 -3
  167. package/src/store/slices/overlayCompositor.test.ts +164 -0
  168. package/src/store/slices/overlaySlice.test.ts +93 -0
  169. package/src/store/slices/overlaySlice.ts +151 -0
  170. package/src/store/slices/pinboardSlice.test.ts +6 -1
  171. package/src/store/slices/playbackSlice.ts +128 -0
  172. package/src/store/slices/schedule-edit-helpers.test.ts +97 -0
  173. package/src/store/slices/schedule-edit-helpers.ts +179 -0
  174. package/src/store/slices/scheduleSlice.test.ts +694 -0
  175. package/src/store/slices/scheduleSlice.ts +1330 -0
  176. package/src/store/slices/searchSlice.test.ts +342 -0
  177. package/src/store/slices/searchSlice.ts +341 -0
  178. package/src/store/slices/sectionSlice.test.ts +87 -7
  179. package/src/store/slices/sectionSlice.ts +151 -5
  180. package/src/store/slices/selectionSlice.test.ts +46 -0
  181. package/src/store/slices/selectionSlice.ts +20 -0
  182. package/src/store/slices/uiSlice.ts +28 -5
  183. package/src/store/types.ts +26 -0
  184. package/src/store.ts +14 -0
  185. package/src/utils/nativeSpatialDataStore.ts +4 -1
  186. package/src/utils/viewportUtils.ts +7 -2
  187. package/src/vite-env.d.ts +0 -4
  188. package/dist/assets/drawing-2d-gWfpdfYe.js +0 -257
  189. package/dist/assets/ids-B4jTqB1O.js +0 -1
  190. package/dist/assets/ifc-lite_bg-BX4E7TX8.wasm +0 -0
  191. package/dist/assets/index-DckuDqlv.css +0 -1
  192. package/dist/assets/sandbox-DZiNLNMk.js +0 -5933
  193. package/src/components/viewer/UpgradePage.tsx +0 -71
  194. package/src/lib/desktop/ClerkDesktopEntitlementSync.tsx +0 -175
  195. package/src/lib/llm/ClerkChatSync.tsx +0 -74
  196. package/src/lib/llm/clerk-auth.ts +0 -62
@@ -0,0 +1,332 @@
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
+ * Raw STEP tab content — lists every positional argument on the
7
+ * selected entity with an inline editor for each scalar value, and
8
+ * lets the user drill into `#N` references to chase the graph
9
+ * (auto-skipping trivial single-ref wrappers along the way). The
10
+ * entry point for `bim.store.setPositionalAttribute` from the UI.
11
+ *
12
+ * This is intentionally close-to-the-metal: STEP literals are shown
13
+ * verbatim, no friendly transforms, and the help line at the bottom
14
+ * documents the convention so a power user with `IfcRectangleProfileDef`
15
+ * open can edit `XDim` without consulting the script panel.
16
+ */
17
+
18
+ import { useCallback, useEffect, useMemo, useState } from 'react';
19
+ import { ArrowLeft, ChevronRight, FileBox, Info, Sparkles } from 'lucide-react';
20
+ import { getAttributeNames } from '@ifc-lite/parser';
21
+ import type { EntityRef } from '@ifc-lite/parser';
22
+ import type { IfcDataStore } from '@ifc-lite/parser';
23
+ import type { IfcAttributeValue } from '@ifc-lite/mutations';
24
+ import { useViewerStore } from '@/store';
25
+ import { RawStepRow } from './RawStepRow';
26
+ import { extractRawStepTokens, serializeStepToken } from './raw-step-format';
27
+
28
+ /** Max wrappers to skip when auto-following a `#N` click. Caps the
29
+ * loop in case of cyclic STEP graphs (shouldn't happen in valid
30
+ * IFC, but stay defensive). */
31
+ const AUTO_FOLLOW_DEPTH = 16;
32
+
33
+ /**
34
+ * Apply per-index overlay overrides on top of the base STEP tokens.
35
+ * Returns a fresh array so React detects the change. Out-of-range
36
+ * indices are ignored — the StoreEditor refuses them on write, but
37
+ * stay defensive in case the override map outlives the entity.
38
+ */
39
+ function applyOverlayTokens(
40
+ base: string[],
41
+ overlay: Map<number, IfcAttributeValue> | null,
42
+ ): string[] {
43
+ if (!overlay || overlay.size === 0) return base;
44
+ const merged = base.slice();
45
+ for (const [index, value] of overlay) {
46
+ if (index >= 0 && index < merged.length) {
47
+ merged[index] = serializeStepToken(value);
48
+ }
49
+ }
50
+ return merged;
51
+ }
52
+
53
+ /**
54
+ * Read raw STEP tokens for an entity by id. Returns null if the entity
55
+ * is overlay-only or the source bytes can't be parsed.
56
+ */
57
+ function readSourceTokens(dataStore: IfcDataStore | null, expressId: number): string[] | null {
58
+ if (!dataStore?.source) return null;
59
+ const ref: EntityRef | undefined = dataStore.entityIndex.byId.get(expressId);
60
+ if (!ref || ref.byteLength <= 0) return null;
61
+ return extractRawStepTokens(dataStore.source, ref.byteOffset, ref.byteLength);
62
+ }
63
+
64
+ /**
65
+ * If the target entity is a single-positional-arg wrapper whose only
66
+ * arg is itself a `#N` reference, follow that chain. Returns the
67
+ * deepest "meaningful" expressId — the first one whose body has more
68
+ * than one arg, or whose single arg isn't a reference. Caps recursion
69
+ * at AUTO_FOLLOW_DEPTH and bails on tombstoned entities.
70
+ */
71
+ function autoFollowWrappers(
72
+ startId: number,
73
+ dataStore: IfcDataStore | null,
74
+ isDeleted: (id: number) => boolean,
75
+ ): number {
76
+ let current = startId;
77
+ for (let i = 0; i < AUTO_FOLLOW_DEPTH; i++) {
78
+ if (isDeleted(current)) return current;
79
+ const tokens = readSourceTokens(dataStore, current);
80
+ if (!tokens || tokens.length !== 1) return current;
81
+ const m = tokens[0].match(/^#(\d+)$/);
82
+ if (!m) return current;
83
+ const next = Number.parseInt(m[1], 10);
84
+ if (!Number.isFinite(next) || next === current) return current;
85
+ current = next;
86
+ }
87
+ return current;
88
+ }
89
+
90
+ interface RawStepCardProps {
91
+ modelId: string;
92
+ entityId: number;
93
+ entityType: string;
94
+ /** The active model's data store — needed to read the source bytes. */
95
+ dataStore: IfcDataStore | null;
96
+ /** Edit affordances are gated on edit mode (matches Properties tab). */
97
+ enableEditing: boolean;
98
+ }
99
+
100
+ export function RawStepCard({
101
+ modelId,
102
+ entityId,
103
+ entityType,
104
+ dataStore,
105
+ enableEditing,
106
+ }: RawStepCardProps) {
107
+ // Subscribe to the mutation version so overlay overrides re-render
108
+ // here exactly when they would in the Properties tab.
109
+ const mutationVersion = useViewerStore((s) => s.mutationVersion);
110
+ const getMutationView = useViewerStore((s) => s.getMutationView);
111
+
112
+ // Drill-through navigation. The stack holds expressIds the user has
113
+ // clicked into; an empty stack means "show the 3D-selected entity".
114
+ // Reset whenever the root selection changes — drilling stays scoped
115
+ // to a single 3D click, otherwise the UI feels haunted.
116
+ const [navStack, setNavStack] = useState<number[]>([]);
117
+ useEffect(() => {
118
+ setNavStack([]);
119
+ }, [modelId, entityId]);
120
+
121
+ const currentId = navStack.length > 0 ? navStack[navStack.length - 1] : entityId;
122
+ const isAtRoot = navStack.length === 0;
123
+
124
+ // Resolve the current entity's type for the header. Lookup order:
125
+ // 1. The 3D-selected root carries its type via `entityType`.
126
+ // 2. The parsed data store knows source-buffer entities by id.
127
+ // 3. Overlay-only entities (drill-create / duplicate) live in the
128
+ // mutation view's `newEntities` map — without this fallback
129
+ // drilled overlay entities would lose schema-aware attribute
130
+ // labels and render as `#<id>`.
131
+ const currentType = useMemo(() => {
132
+ if (currentId === entityId) return entityType;
133
+ const t = dataStore?.entities.getTypeName(currentId);
134
+ if (t) return t;
135
+ const view = getMutationView(modelId);
136
+ const overlay = view?.getNewEntity(currentId);
137
+ if (overlay) return overlay.type;
138
+ return `#${currentId}`;
139
+ // eslint-disable-next-line react-hooks/exhaustive-deps
140
+ }, [currentId, entityId, entityType, dataStore, modelId, getMutationView, mutationVersion]);
141
+
142
+ // Resolve display tokens for the *current* entity. Tokenize the raw
143
+ // STEP body when source bytes exist, fall back to overlay-only
144
+ // NewEntity records otherwise. Per-index overrides land on top.
145
+ const { tokens, isOverlayOnly, overlayMap } = useMemo(() => {
146
+ const view = getMutationView(modelId);
147
+ const overlay = view?.getPositionalMutationsForEntity(currentId) ?? null;
148
+
149
+ const sourceTokens = readSourceTokens(dataStore, currentId);
150
+ if (sourceTokens) {
151
+ return {
152
+ tokens: applyOverlayTokens(sourceTokens, overlay),
153
+ isOverlayOnly: false,
154
+ overlayMap: overlay,
155
+ };
156
+ }
157
+
158
+ if (view) {
159
+ const overlayEntity = view.getNewEntity(currentId);
160
+ if (overlayEntity) {
161
+ const baseTokens = (overlayEntity.attributes as IfcAttributeValue[]).map(serializeStepToken);
162
+ return {
163
+ tokens: applyOverlayTokens(baseTokens, overlay),
164
+ isOverlayOnly: true,
165
+ overlayMap: overlay,
166
+ };
167
+ }
168
+ }
169
+
170
+ return { tokens: null as string[] | null, isOverlayOnly: false, overlayMap: overlay };
171
+ // mutationVersion forces this hook to re-run when any overlay
172
+ // (positional or overlay-entity) changes — overlay maps are
173
+ // mutated in place, so identity-based memoization isn't enough.
174
+ // eslint-disable-next-line react-hooks/exhaustive-deps
175
+ }, [dataStore, currentId, modelId, getMutationView, mutationVersion]);
176
+
177
+ // Schema attribute names for the current type. Falls back to
178
+ // "Arg N" for entities the generated registry doesn't know.
179
+ const attributeNames = useMemo(() => getAttributeNames(currentType) ?? [], [currentType]);
180
+
181
+ // Per-row mutation indicator — drives the purple dot.
182
+ const mutatedIndices = useMemo(() => {
183
+ if (!overlayMap) return new Set<number>();
184
+ return new Set(overlayMap.keys());
185
+ }, [overlayMap]);
186
+
187
+ // Drill into a `#N` reference, auto-skipping trivial wrappers so a
188
+ // single click takes the user from `OwnerHistory → IfcOwnerHistory`
189
+ // (one hop) without three intermediate stops on identity-only
190
+ // wrapper entities.
191
+ const handleNavigate = useCallback(
192
+ (refId: number) => {
193
+ const view = getMutationView(modelId);
194
+ const isDeleted = (id: number) => view?.isDeleted?.(id) ?? false;
195
+ const target = autoFollowWrappers(refId, dataStore, isDeleted);
196
+ setNavStack((prev) => {
197
+ // No-op if the user is already viewing the target — refs that
198
+ // self-loop or land on the current node would otherwise grow
199
+ // the breadcrumb forever.
200
+ const tail = prev.length > 0 ? prev[prev.length - 1] : entityId;
201
+ if (target === tail) return prev;
202
+ return [...prev, target];
203
+ });
204
+ },
205
+ [dataStore, modelId, entityId, getMutationView],
206
+ );
207
+
208
+ const handleBack = useCallback(() => {
209
+ setNavStack((prev) => prev.slice(0, -1));
210
+ }, []);
211
+
212
+ const handleResetToRoot = useCallback(() => {
213
+ setNavStack([]);
214
+ }, []);
215
+
216
+ if (!tokens || tokens.length === 0) {
217
+ return (
218
+ <div className="rounded-md border border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-950 p-6 text-center">
219
+ <FileBox className="h-5 w-5 mx-auto mb-2 text-zinc-400" />
220
+ <p className="text-xs font-mono text-zinc-500 dark:text-zinc-500">
221
+ {dataStore
222
+ ? `Entity #${currentId} has no positional STEP arguments`
223
+ : 'Raw STEP is unavailable for this model'}
224
+ </p>
225
+ {!isAtRoot && (
226
+ <button
227
+ type="button"
228
+ onClick={handleResetToRoot}
229
+ className="mt-3 text-[10px] font-mono text-emerald-600 dark:text-emerald-400 hover:underline"
230
+ >
231
+ ← Back to {entityType} #{entityId}
232
+ </button>
233
+ )}
234
+ </div>
235
+ );
236
+ }
237
+
238
+ return (
239
+ <div className="rounded-md border border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-950 overflow-hidden">
240
+ {/* Breadcrumb (only when drilled in) */}
241
+ {!isAtRoot && (
242
+ <div className="flex items-center gap-1 px-2 py-1.5 border-b border-zinc-200 dark:border-zinc-800 bg-emerald-50/40 dark:bg-emerald-950/15 text-[10px] font-mono">
243
+ <button
244
+ type="button"
245
+ onClick={handleBack}
246
+ className="inline-flex items-center gap-0.5 px-1.5 py-0.5 rounded hover:bg-emerald-100 dark:hover:bg-emerald-900/40 text-emerald-700 dark:text-emerald-400"
247
+ title="Back one step"
248
+ >
249
+ <ArrowLeft className="h-3 w-3" />
250
+ <span>back</span>
251
+ </button>
252
+ <button
253
+ type="button"
254
+ onClick={handleResetToRoot}
255
+ className="px-1 py-0.5 rounded text-zinc-600 dark:text-zinc-400 hover:bg-zinc-100 dark:hover:bg-zinc-800 truncate"
256
+ title={`Back to selected entity ${entityType} #${entityId}`}
257
+ >
258
+ {entityType} #{entityId}
259
+ </button>
260
+ {navStack.map((id, i) => (
261
+ <span key={`${id}-${i}`} className="flex items-center gap-1 min-w-0">
262
+ <ChevronRight className="h-3 w-3 text-zinc-400 shrink-0" />
263
+ <button
264
+ type="button"
265
+ onClick={() => setNavStack(navStack.slice(0, i + 1))}
266
+ disabled={i === navStack.length - 1}
267
+ className="px-1 py-0.5 rounded text-zinc-600 dark:text-zinc-400 hover:bg-zinc-100 dark:hover:bg-zinc-800 disabled:hover:bg-transparent disabled:text-emerald-700 dark:disabled:text-emerald-400 disabled:font-semibold truncate"
268
+ >
269
+ #{id}
270
+ </button>
271
+ </span>
272
+ ))}
273
+ </div>
274
+ )}
275
+
276
+ {/* Header */}
277
+ <div className="flex items-center justify-between gap-2 px-3 py-2 border-b border-zinc-200 dark:border-zinc-800 bg-zinc-50/80 dark:bg-zinc-900/40">
278
+ <div className="flex items-center gap-2 min-w-0">
279
+ <FileBox className="h-3.5 w-3.5 text-purple-600 dark:text-purple-400 shrink-0" />
280
+ <span
281
+ className="font-mono text-[11px] font-semibold tracking-wide text-zinc-700 dark:text-zinc-200 truncate"
282
+ title={`${currentType} #${currentId}`}
283
+ >
284
+ {currentType} #{currentId}
285
+ </span>
286
+ </div>
287
+ {isOverlayOnly && (
288
+ <span
289
+ className="inline-flex items-center gap-1 rounded-sm border border-emerald-300 dark:border-emerald-700 bg-emerald-50 dark:bg-emerald-950/40 px-1.5 py-0.5 text-[9px] font-mono uppercase tracking-wider text-emerald-700 dark:text-emerald-300"
290
+ title="This entity was added through the overlay (bim.store.addEntity / addColumn)."
291
+ >
292
+ <Sparkles className="h-2.5 w-2.5" />
293
+ New
294
+ </span>
295
+ )}
296
+ </div>
297
+
298
+ {/* Rows */}
299
+ <div className="divide-y-0">
300
+ {tokens.map((token, idx) => {
301
+ const name = attributeNames[idx] || `Arg ${idx}`;
302
+ return (
303
+ <RawStepRow
304
+ key={idx}
305
+ modelId={modelId}
306
+ entityId={currentId}
307
+ index={idx}
308
+ name={name}
309
+ displayToken={token}
310
+ isMutated={mutatedIndices.has(idx)}
311
+ enableEditing={enableEditing}
312
+ onNavigate={handleNavigate}
313
+ />
314
+ );
315
+ })}
316
+ </div>
317
+
318
+ {/* Help footer */}
319
+ <div className="flex items-start gap-2 px-3 py-2 border-t border-zinc-200 dark:border-zinc-800 bg-zinc-50/60 dark:bg-zinc-900/30">
320
+ <Info className="h-3 w-3 mt-0.5 text-zinc-400 dark:text-zinc-500 shrink-0" />
321
+ <p className="text-[10.5px] font-mono leading-relaxed text-zinc-500 dark:text-zinc-500">
322
+ STEP literals: numbers, <code className="px-0.5 rounded bg-zinc-200/60 dark:bg-zinc-800/60">$</code> for null,{' '}
323
+ <code className="px-0.5 rounded bg-zinc-200/60 dark:bg-zinc-800/60">.T.</code>/
324
+ <code className="px-0.5 rounded bg-zinc-200/60 dark:bg-zinc-800/60">.F.</code> for booleans,{' '}
325
+ <code className="px-0.5 rounded bg-zinc-200/60 dark:bg-zinc-800/60">#42</code> for refs (click to drill),{' '}
326
+ <code className="px-0.5 rounded bg-zinc-200/60 dark:bg-zinc-800/60">.AREA.</code> for enums. Edits
327
+ land on the export overlay — undo/redo via the toolbar.
328
+ </p>
329
+ </div>
330
+ </div>
331
+ );
332
+ }
@@ -0,0 +1,261 @@
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
+ * One row in the Raw STEP editor — a positional STEP argument with an
7
+ * inline pen-icon editor. Mirrors the visual rhythm of the existing
8
+ * AttributeEditorField (PropertiesPanel.tsx) but operates against
9
+ * `bim.store.setPositionalAttribute` instead of the named-attribute path.
10
+ */
11
+
12
+ import { useCallback, useState } from 'react';
13
+ import { PenLine, X, Check, AlertCircle } from 'lucide-react';
14
+ import { Button } from '@/components/ui/button';
15
+ import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
16
+ import { useViewerStore } from '@/store';
17
+ import { isInlineEditableToken, parseRawStepInput } from './raw-step-format';
18
+
19
+ /** Match a bare `#N` STEP entity reference. */
20
+ const REF_TOKEN_RE = /^#(\d+)$/;
21
+
22
+ interface RawStepRowProps {
23
+ modelId: string;
24
+ entityId: number;
25
+ /** Zero-based positional index. */
26
+ index: number;
27
+ /** Schema attribute name (or `Arg N` fallback). */
28
+ name: string;
29
+ /** Current value as a STEP token (verbatim from source, or
30
+ * serialized from an overlay override). */
31
+ displayToken: string;
32
+ /** Whether this index has an active overlay override. */
33
+ isMutated: boolean;
34
+ /** Set false to lock the row (e.g. native-metadata model). */
35
+ enableEditing: boolean;
36
+ /** Drill into a `#N` reference. RawStepCard auto-skips trivial
37
+ * single-ref wrappers and pushes the meaningful target onto the
38
+ * navigation stack. */
39
+ onNavigate?: (refId: number) => void;
40
+ }
41
+
42
+ export function RawStepRow({
43
+ modelId,
44
+ entityId,
45
+ index,
46
+ name,
47
+ displayToken,
48
+ isMutated,
49
+ enableEditing,
50
+ onNavigate,
51
+ }: RawStepRowProps) {
52
+ const setPositionalAttribute = useViewerStore((s) => s.setPositionalAttribute);
53
+ const bumpMutationVersion = useViewerStore((s) => s.bumpMutationVersion);
54
+
55
+ const [editing, setEditing] = useState(false);
56
+ const [draft, setDraft] = useState('');
57
+ const [error, setError] = useState<string | null>(null);
58
+
59
+ const editable = enableEditing && isInlineEditableToken(displayToken);
60
+ const display = displayToken;
61
+ const refMatch = display.match(REF_TOKEN_RE);
62
+ const refTargetId = refMatch ? Number.parseInt(refMatch[1], 10) : null;
63
+
64
+ const inputRef = useCallback((node: HTMLInputElement | null) => {
65
+ if (node) {
66
+ node.focus();
67
+ node.select();
68
+ }
69
+ }, []);
70
+
71
+ const startEdit = useCallback(() => {
72
+ if (!editable) return;
73
+ setDraft(display);
74
+ setError(null);
75
+ setEditing(true);
76
+ }, [editable, display]);
77
+
78
+ const cancelEdit = useCallback(() => {
79
+ setEditing(false);
80
+ setError(null);
81
+ }, []);
82
+
83
+ const saveEdit = useCallback(() => {
84
+ const parsed = parseRawStepInput(draft);
85
+ if ('error' in parsed) {
86
+ setError(parsed.error);
87
+ return;
88
+ }
89
+ setPositionalAttribute(modelId, entityId, index, parsed.value);
90
+ bumpMutationVersion();
91
+ setEditing(false);
92
+ setError(null);
93
+ }, [draft, modelId, entityId, index, setPositionalAttribute, bumpMutationVersion]);
94
+
95
+ const handleKeyDown = useCallback(
96
+ (e: React.KeyboardEvent<HTMLInputElement>) => {
97
+ if (e.key === 'Enter') {
98
+ e.preventDefault();
99
+ saveEdit();
100
+ } else if (e.key === 'Escape') {
101
+ e.preventDefault();
102
+ cancelEdit();
103
+ }
104
+ },
105
+ [saveEdit, cancelEdit],
106
+ );
107
+
108
+ return (
109
+ <div
110
+ className={`group grid grid-cols-[28px_minmax(80px,140px)_minmax(0,1fr)_auto] items-center gap-2 px-3 py-1.5 text-sm border-b border-zinc-200/60 dark:border-zinc-800/60 ${
111
+ isMutated ? 'bg-purple-50/40 dark:bg-purple-950/15' : ''
112
+ }`}
113
+ >
114
+ {/* Positional index */}
115
+ <span
116
+ className="text-[10px] font-mono text-zinc-400 dark:text-zinc-500 tabular-nums tracking-wide"
117
+ aria-label={`positional index ${index}`}
118
+ >
119
+ [{index}]
120
+ </span>
121
+
122
+ {/* Schema attribute name */}
123
+ <span
124
+ className="text-zinc-600 dark:text-zinc-400 truncate font-mono text-xs"
125
+ title={name}
126
+ >
127
+ {name}
128
+ </span>
129
+
130
+ {/* Value cell — display or input */}
131
+ {editing ? (
132
+ <div className="flex items-center gap-1 min-w-0">
133
+ <input
134
+ ref={inputRef}
135
+ value={draft}
136
+ onChange={(e) => {
137
+ setDraft(e.target.value);
138
+ if (error) setError(null);
139
+ }}
140
+ onKeyDown={handleKeyDown}
141
+ onBlur={(e) => {
142
+ // Don't save when the blur is caused by clicking the
143
+ // confirm/cancel buttons — those handle their own action.
144
+ const next = e.relatedTarget as HTMLElement | null;
145
+ if (next?.dataset.rawStepAction) return;
146
+ saveEdit();
147
+ }}
148
+ className={`flex-1 min-w-0 h-7 px-2 text-xs font-mono bg-white dark:bg-zinc-900 border outline-none focus:ring-1 ${
149
+ error
150
+ ? 'border-red-400 dark:border-red-500 focus:ring-red-400'
151
+ : 'border-purple-300 dark:border-purple-700 focus:ring-purple-400'
152
+ }`}
153
+ spellCheck={false}
154
+ autoCapitalize="off"
155
+ autoCorrect="off"
156
+ aria-invalid={error ? true : undefined}
157
+ aria-describedby={error ? `raw-step-err-${entityId}-${index}` : undefined}
158
+ />
159
+ </div>
160
+ ) : refTargetId !== null && onNavigate ? (
161
+ // Reference token — render as a navigable chip. Drilling into
162
+ // a ref shouldn't share the same hover affordance as editing
163
+ // a scalar; the emerald accent matches the Raw tab indicator
164
+ // and visually separates "follow" from "edit". Editing a ref
165
+ // (changing it to point at a different `#N`) goes through
166
+ // the pen icon on the right.
167
+ <button
168
+ type="button"
169
+ onClick={() => onNavigate(refTargetId)}
170
+ className="min-w-0 text-left font-mono text-xs truncate px-1.5 py-0.5 rounded text-emerald-700 dark:text-emerald-400 bg-emerald-50/50 dark:bg-emerald-950/25 hover:bg-emerald-100 dark:hover:bg-emerald-900/40 hover:underline decoration-dotted underline-offset-2"
171
+ title={`Drill into ${display} (auto-skips trivial wrappers)`}
172
+ >
173
+ {display}
174
+ </button>
175
+ ) : (
176
+ <button
177
+ type="button"
178
+ disabled={!editable}
179
+ onClick={startEdit}
180
+ className={`min-w-0 text-left font-mono text-xs truncate px-1.5 py-0.5 rounded ${
181
+ editable
182
+ ? 'cursor-text hover:bg-zinc-100 dark:hover:bg-zinc-800'
183
+ : 'cursor-default text-zinc-500 dark:text-zinc-500'
184
+ } ${
185
+ display === '$'
186
+ ? 'text-zinc-400 dark:text-zinc-600'
187
+ : 'text-zinc-800 dark:text-zinc-200'
188
+ }`}
189
+ title={editable ? 'Click to edit' : 'This value type is not inline-editable'}
190
+ >
191
+ {display}
192
+ </button>
193
+ )}
194
+
195
+ {/* Action cluster */}
196
+ <div className="flex items-center gap-1 shrink-0">
197
+ {isMutated && !editing && (
198
+ <Tooltip>
199
+ <TooltipTrigger asChild>
200
+ <span
201
+ aria-label="overlay override active"
202
+ className="inline-block h-1.5 w-1.5 rounded-full bg-purple-500 dark:bg-purple-400"
203
+ />
204
+ </TooltipTrigger>
205
+ <TooltipContent side="left">Overlay override</TooltipContent>
206
+ </Tooltip>
207
+ )}
208
+
209
+ {editing ? (
210
+ <>
211
+ <Button
212
+ variant="ghost"
213
+ size="icon"
214
+ data-raw-step-action="save"
215
+ onMouseDown={(e) => e.preventDefault()}
216
+ onClick={saveEdit}
217
+ className="h-6 w-6 p-0 hover:bg-emerald-100 dark:hover:bg-emerald-950/30"
218
+ title="Save (Enter)"
219
+ >
220
+ <Check className="h-3.5 w-3.5 text-emerald-600 dark:text-emerald-400" />
221
+ </Button>
222
+ <Button
223
+ variant="ghost"
224
+ size="icon"
225
+ data-raw-step-action="cancel"
226
+ onMouseDown={(e) => e.preventDefault()}
227
+ onClick={cancelEdit}
228
+ className="h-6 w-6 p-0 hover:bg-red-100 dark:hover:bg-red-950/30"
229
+ title="Cancel (Esc)"
230
+ >
231
+ <X className="h-3.5 w-3.5 text-red-500 dark:text-red-400" />
232
+ </Button>
233
+ </>
234
+ ) : editable ? (
235
+ <Button
236
+ variant="ghost"
237
+ size="icon"
238
+ onClick={startEdit}
239
+ className="h-6 w-6 p-0 opacity-0 group-hover:opacity-100 focus-visible:opacity-100 transition-opacity"
240
+ title="Edit"
241
+ >
242
+ <PenLine className="h-3.5 w-3.5 text-purple-600 dark:text-purple-400" />
243
+ </Button>
244
+ ) : (
245
+ <span className="h-6 w-6" aria-hidden />
246
+ )}
247
+ </div>
248
+
249
+ {/* Validation error — full-width row beneath the value */}
250
+ {error && (
251
+ <p
252
+ id={`raw-step-err-${entityId}-${index}`}
253
+ className="col-span-4 -mt-1 mb-1 ml-[36px] flex items-center gap-1 text-[11px] text-red-600 dark:text-red-400"
254
+ >
255
+ <AlertCircle className="h-3 w-3 shrink-0" />
256
+ <span>{error}</span>
257
+ </p>
258
+ )}
259
+ </div>
260
+ );
261
+ }