Compare commits

...

7 Commits

Author SHA1 Message Date
oguzhankoral e2ebb28ba5 Fix form select base placeholder on select 2025-08-15 15:54:56 +03:00
oguzhankoral 723922e5d2 WIP 2025-08-15 15:49:32 +03:00
oguzhankoral fc44f7db43 revit mapper store 2025-08-15 13:54:58 +03:00
Björn Steinhagen bc69a34431 feat: add Mixpanel tracking to revit mapper interactions (#50)
* feat: add Mixpanel tracking to revit mapper interactions

* fix: pr comments

* fix: just mode

* chore(interop-lite): rename event name prop

---------

Co-authored-by: oguzhankoral <oguzhankoral@gmail.com>
2025-08-15 13:15:33 +03:00
Björn 7cf55a2fb6 chore: update available categories 2025-08-14 17:56:53 +02:00
Björn 38e07fb62d refactor: cleaning 2025-08-14 16:29:52 +02:00
Björn 093e03ce98 feat: poc
- needs cleaning
- just me, hacking
2025-08-14 15:39:11 +02:00
5 changed files with 413 additions and 92 deletions
@@ -14,6 +14,7 @@ export interface IRevitMapperBinding extends IBinding<IMapperBindingEvents> {
clearObjectsCategoryAssignment: (objectIds: string[]) => Promise<void>
clearAllObjectsCategoryAssignments: () => Promise<void>
getCurrentObjectsMappings: () => Promise<CategoryMapping[]>
getCategoryMappingsForObjects: (objectIds: string[]) => Promise<string[]>
// Layer methods
assignLayerToCategory: (layerIds: string[], categoryValue: string) => Promise<void>
@@ -24,6 +25,7 @@ export interface IRevitMapperBinding extends IBinding<IMapperBindingEvents> {
layerIds: string[],
categoryValue: string
) => Promise<string[]>
getCategoryMappingsForLayers: (layerIds: string[]) => Promise<string[]>
}
export interface IMapperBindingEvents extends IBindingSharedEvents {
@@ -125,6 +127,21 @@ export class MockedMapperBinding implements IRevitMapperBinding {
return Promise.resolve(['obj1', 'obj2', 'obj3'])
}
public getCategoryMappingsForObjects(objectIds: string[]): Promise<string[]> {
console.log('Mock: Getting category mappings for objects', { objectIds })
// Mock returning some categories for testing
return Promise.resolve(
objectIds.length > 1 ? ['OST_Walls', 'OST_Doors'] : ['OST_Walls']
)
}
public getCategoryMappingsForLayers(layerIds: string[]): Promise<string[]> {
console.log('Mock: Getting category mappings for layers', { layerIds })
return Promise.resolve(
layerIds.length > 1 ? ['OST_Floors', 'OST_Ceilings'] : ['OST_Floors']
)
}
public showDevTools(): Promise<void> {
console.log('Braaaaa, no way!')
return Promise.resolve()
+98 -59
View File
@@ -5,97 +5,136 @@ import type { CategoryOption } from './types'
*/
export const REVIT_CATEGORIES: readonly CategoryOption[] = [
// INFRASTRUCTURE
{ value: 'OST_BridgeAbutments', label: 'Bridge Abutments' },
{ value: 'OST_BridgeFraming', label: 'Bridge Framing' },
{ value: 'OST_BridgeBearings', label: 'Bridge Bearings' },
{ value: 'OST_BridgeAbutments', label: 'Abutments' },
{ value: 'OST_AbutmentFoundations', label: 'Abutment Foundations' },
{ value: 'OST_AbutmentPiles', label: 'Abutment Piles' },
{ value: 'OST_AbutmentWalls', label: 'Abutment Walls' },
{ value: 'OST_ApproachSlabs', label: 'Approach Slabs' },
{ value: 'OST_BridgeBearings', label: 'Bearings' },
{ value: 'OST_BridgeCables', label: 'Bridge Cables' },
{ value: 'OST_BridgeDecks', label: 'Bridge Decks' },
{ value: 'OST_BridgeFraming', label: 'Bridge Framing' },
{ value: 'OST_BridgeArches', label: 'Bridge Arches' },
{ value: 'OST_BridgeFramingCrossBracing', label: 'Bridge Framing - Cross Bracing' },
{ value: 'OST_BridgeFramingDiaphragms', label: 'Bridge Framing - Diaphragms' },
{ value: 'OST_BridgeGirders', label: 'Bridge Framing - Girders' },
{ value: 'OST_BridgeFramingTrusses', label: 'Bridge Framing - Trusses' },
{ value: 'OST_ExpansionJoints', label: 'Expansion Joints' },
{ value: 'OST_BridgePiers', label: 'Bridge Piers' },
{ value: 'OST_BridgePiers', label: 'Piers' },
{ value: 'OST_PierCaps', label: 'Pier Caps' },
{ value: 'OST_PierColumns', label: 'Pier Columns' },
{ value: 'OST_BridgeFoundations', label: 'Pier Foundations' },
{ value: 'OST_PierPiles', label: 'Pier Piles' },
{ value: 'OST_BridgeTowers', label: 'Pier Towers' },
{ value: 'OST_PierWalls', label: 'Pier Walls' },
{ value: 'OST_StructuralTendons', label: 'Structural Tendons' },
{ value: 'OST_VibrationManagement', label: 'Vibration Management' },
{ value: 'OST_VibrationDampers', label: 'Vibration Dampers' },
{ value: 'OST_VibrationIsolators', label: 'Vibration Isolators' },
// ARCHITECTURE
{ value: 'OST_AudioVisualDevices', label: 'Audio Visual Devices' },
{ value: 'OST_Casework', label: 'Casework' },
{ value: 'OST_Ceilings', label: 'Ceilings' },
{ value: 'OST_Columns', label: 'Columns' },
{ value: 'OST_CurtainWallPanels', label: 'Curtain Panels' },
// { value: 'OST_CurtaSystem', label: 'Curtain Systems' }, excluded as part of CNX-2299
{ value: 'OST_CurtainWallMullions', label: 'Curtain Wall Mullions' },
{ value: 'OST_Doors', label: 'Doors' },
{ value: 'OST_Entourage', label: 'Entourage' },
{ value: 'OST_FireProtection', label: 'Fire Protection' },
{ value: 'OST_Floors', label: 'Floors' },
{ value: 'OST_FoodServiceEquipment', label: 'Food Service Equipment' },
{ value: 'OST_Furniture', label: 'Furniture' },
{ value: 'OST_FurnitureSystems', label: 'Furniture Systems' },
{ value: 'OST_GenericModel', label: 'Generic Models' },
{ value: 'OST_Hardscape', label: 'Hardscape' },
{ value: 'OST_Lines', label: 'Lines' },
{ value: 'OST_Mass', label: 'Mass' },
{ value: 'OST_MechanicalControlDevices', label: 'Mechanical Control Devices' },
{ value: 'OST_MechanicalEquipment', label: 'Mechanical Equipment' },
{ value: 'OST_MedicalEquipment', label: 'Medical Equipment' },
{ value: 'OST_Parking', label: 'Parking' },
{ value: 'OST_Parts', label: 'Parts' },
{ value: 'OST_Planting', label: 'Planting' },
{ value: 'OST_PlumbingEquipment', label: 'Plumbing Equipment' },
{ value: 'OST_PlumbingFixtures', label: 'Plumbing Fixtures' },
{ value: 'OST_StairsRailing', label: 'Railings' },
{ value: 'OST_StairsRailingBaluster', label: 'Railings - Balusters' },
{ value: 'OST_RailingSupport', label: 'Railings - Supports' },
{ value: 'OST_RailingTermination', label: 'Railings - Terminations' },
{ value: 'OST_Ramps', label: 'Ramps' },
{ value: 'OST_Roads', label: 'Roads' },
{ value: 'OST_Roofs', label: 'Roofs' },
{ value: 'OST_Signage', label: 'Signage' },
{ value: 'OST_Site', label: 'Site' },
{ value: 'OST_SpecialtyEquipment', label: 'Specialty Equipment' },
{ value: 'OST_Stairs', label: 'Stairs' },
{ value: 'OST_TemporaryStructure', label: 'Temporary Structures' },
{ value: 'OST_Topography', label: 'Topography' },
{ value: 'OST_Toposolid', label: 'Toposolid' },
{ value: 'OST_VerticalCirculation', label: 'Vertical Circulation' },
{ value: 'OST_Walls', label: 'Walls' },
{ value: 'OST_Windows', label: 'Windows' },
// ELECTRICAL
{ value: 'OST_AudioVisualDevices', label: 'Audio Visual Devices' },
{ value: 'OST_CableTray', label: 'Cable Tray' },
{ value: 'OST_CableTrayFitting', label: 'Cable Tray Fittings' },
{ value: 'OST_CableTray', label: 'Cable Trays' },
{ value: 'OST_CommunicationDevices', label: 'Communication Devices' },
{ value: 'OST_ConduitFittings', label: 'Conduit Fittings' },
{ value: 'OST_Conduit', label: 'Conduits' },
{ value: 'OST_ConduitFitting', label: 'Conduit Fittings' },
{ value: 'OST_DataDevices', label: 'Data Devices' },
{ value: 'OST_ElectricalEquipment', label: 'Electrical Equipment' },
{ value: 'OST_ElectricalFixtures', label: 'Electrical Fixtures' },
{ value: 'OST_FireAlarmDevices', label: 'Fire Alarm Devices' },
{ value: 'OST_LightingDevices', label: 'Lighting Devices' },
{ value: 'OST_LighintgDevices', label: 'Lighting Devices' },
{ value: 'OST_LightingFixtures', label: 'Lighting Fixtures' },
{ value: 'OST_NurseCallDevices', label: 'Nurse Call Devices' },
{ value: 'OST_SecurityDevices', label: 'Security Devices' },
{ value: 'OST_TelephoneDevices', label: 'Telephone Devices' },
// ARCHITECTURAL
{ value: 'OST_Casework', label: 'Casework' },
{ value: 'OST_Ceilings', label: 'Ceilings' },
{ value: 'OST_Columns', label: 'Columns' },
{ value: 'OST_CurtainWallMullions', label: 'Curtain Wall Mullions' },
{ value: 'OST_CurtainWallPanels', label: 'Curtain Panels' },
// OST_Curtain_Systems excluded as part of CNX-2299
{ value: 'OST_Doors', label: 'Doors' },
{ value: 'OST_Entourage', label: 'Entourage' },
{ value: 'OST_Floors', label: 'Floors' },
{ value: 'OST_FoodServiceEquipment', label: 'Food Service Equipment' },
{ value: 'OST_Furniture', label: 'Furniture' },
{ value: 'OST_FurnitureSystems', label: 'Furniture Systems' },
{ value: 'OST_Hardscape', label: 'Hardscape' },
{ value: 'OST_Parking', label: 'Parking' },
{ value: 'OST_Planting', label: 'Planting' },
{ value: 'OST_Railings', label: 'Railings' },
{ value: 'OST_Ramps', label: 'Ramps' },
{ value: 'OST_Roads', label: 'Roads' },
{ value: 'OST_Roofs', label: 'Roofs' },
{ value: 'OST_Site', label: 'Site' },
{ value: 'OST_SpecialityEquipment', label: 'Speciality Equipment' },
{ value: 'OST_Stairs', label: 'Stairs' },
{ value: 'OST_Topography', label: 'Topography' },
{ value: 'OST_Toposolid', label: 'Toposolid' },
{ value: 'OST_Walls', label: 'Walls' },
{ value: 'OST_Windows', label: 'Windows' },
// STRUCTURAL
// OST_StructuralColumns excluded as part of CNX-2299
{ value: 'OST_StructuralConnections', label: 'Structural Connections' },
// STRUCTURE
{ value: 'OST_Coupler', label: 'Structural Rebar Couplers' },
{ value: 'OST_FabricAreas', label: 'Structural Fabric Areas' },
{ value: 'OST_StructConnections', label: 'Structural Connections' },
{ value: 'OST_StructConnectionAnchors', label: 'Structural Connections - Anchors' },
{ value: 'OST_StructConnectionBolts', label: 'Structural Connections - Bolts' },
{ value: 'OST_StructConnectionPlates', label: 'Structural Connections - Plates' },
{ value: 'OST_StructConnectionProfiles', label: 'Structural Connections - Profiles' },
{
value: 'OST_StructConnectionShearStuds',
label: 'Structural Connections - Shear Studs'
},
{ value: 'OST_StructConnectionWelds', label: 'Structural Connections - Welds' },
// { value: 'OST_StructuralColumns', label: 'Structural Columns' }, excluded as part of CNX-2299
{ value: 'OST_StructuralFoundation', label: 'Structural Foundations' },
{ value: 'OST_StructuralFraming', label: 'Structural Framing' },
{ value: 'OST_StructuralFramingSystem', label: 'Structural Beam Systems' },
{ value: 'OST_StructuralFabricAreas', label: 'Structural Fabric Areas' },
{ value: 'OST_Rebar', label: 'Rebar' },
{ value: 'OST_StructuralStiffener', label: 'Structural Stiffeners' },
{ value: 'OST_StructuralTendons', label: 'Structural Tendons' },
{ value: 'OST_StructuralTruss', label: 'Structural Trusses' },
{ value: 'OST_Rebar', label: 'Structural Rebar' },
// MECHANICAL
{ value: 'OST_DuctTerminal', label: 'Air Terminals' },
{ value: 'OST_DuctAccessory', label: 'Duct Accessories' },
{ value: 'OST_DuctCurves', label: 'Ducts' },
{ value: 'OST_DuctFitting', label: 'Duct Fittings' },
{ value: 'OST_DuctSystem', label: 'Duct Systems' },
{ value: 'OST_MechanicalEquipment', label: 'Mechanical Equipment' },
{ value: 'OST_PlumbingEquipment', label: 'Plumbing Equipment' },
{ value: 'OST_PlumbingFixtures', label: 'Plumbing Fixtures' },
{ value: 'OST_PlaceHolderDucts', label: 'Duct Placeholders' },
{ value: 'OST_DuctCurves', label: 'Ducts' },
{ value: 'OST_MEPAncillaryFraming', label: 'MEP Ancillary Framing' },
// PIPING
{ value: 'OST_PipeAccessory', label: 'Pipe Accessories' },
{ value: 'OST_PipeCurves', label: 'Pipes' },
{ value: 'OST_PipeFitting', label: 'Pipe Fittings' },
{ value: 'OST_PlaceHolderPipes', label: 'Pipe Placeholders' },
{ value: 'OST_PipeSegments', label: 'Pipe Segments' },
{ value: 'OST_PipeCurves', label: 'Pipes' },
{ value: 'OST_Sprinklers', label: 'Sprinklers' },
// GENERAL/MULTI-DISCIPLINE
{ value: 'OST_FireProtection', label: 'Fire Protection' },
{ value: 'OST_GenericModel', label: 'Generic Models' },
{ value: 'OST_Lines', label: 'Lines' },
{ value: 'OST_Mass', label: 'Mass' },
{ value: 'OST_MedicalEquipment', label: 'Medical Equipment' },
{ value: 'OST_Parts', label: 'Parts' },
{ value: 'OST_Signage', label: 'Signage' },
{ value: 'OST_TemporaryStructure', label: 'Temporary Structures' },
{ value: 'OST_VerticalCirculation', label: 'Vertical Circulation' }
{ value: 'OST_CableTrayRun', label: 'Cable Tray Runs' },
{ value: 'OST_Coordination_Model', label: 'Coordination Model' },
{ value: 'OST_DuctSystem', label: 'Duct Systems' },
{ value: 'OST_PipingSystem', label: 'Piping Systems' },
{ value: 'OST_StructuralFramingSystem', label: 'Structural Beam Systems' },
{ value: 'OST_StructuralStiffener', label: 'Structural Stiffeners' }
] as const
/**
+99
View File
@@ -0,0 +1,99 @@
import type { Ref } from 'vue'
import type {
Category,
IRevitMapperBinding
} from '~/lib/bindings/definitions/IRevitMapperBinding'
export interface RevitCategoryState {
// Current categories found for selected targets
currentCategories: Ref<string[]>
// Category selected in the dropdown
selectedCategory: Ref<Category | undefined>
// Computed status for display
categoryStatus: ComputedRef<{
label: string
value: string
isMultiple?: boolean
} | null>
// Update state based on current targets
updateFromTargets: (targetIds: string[], isLayerMode: boolean) => Promise<void>
// Clear all state
clear: () => void
}
export function useRevitCategoryState(
categoryOptions: Ref<Category[]>,
revitMapperBinding: IRevitMapperBinding | undefined
): RevitCategoryState {
const currentCategories = ref<string[]>([])
const selectedCategory = ref<Category | undefined>(undefined)
const categoryStatus = computed(() => {
if (currentCategories.value.length === 0) {
return null
}
if (currentCategories.value.length === 1) {
const category = categoryOptions.value.find(
(cat) => cat.value === currentCategories.value[0]
)
return category ? { label: category.label, value: category.value } : null
}
return {
label: 'Multiple categories',
value: 'multiple',
isMultiple: true
}
})
const updateFromTargets = async (
targetIds: string[],
isLayerMode: boolean
): Promise<void> => {
// Clear state if no targets
if (!targetIds.length || !revitMapperBinding) {
clear()
return
}
try {
// Call connector method based on mode
const categories: string[] = isLayerMode
? await revitMapperBinding.getCategoryMappingsForLayers(targetIds)
: await revitMapperBinding.getCategoryMappingsForObjects(targetIds)
currentCategories.value = categories
// Update dropdown selection based on categories found
if (categories.length === 1) {
selectedCategory.value = categoryOptions.value.find(
(cat) => cat.value === categories[0]
)
} else {
// Multiple or no categories - clear dropdown selection
selectedCategory.value = undefined
}
} catch (error) {
console.error('Failed to get category mappings:', error)
clear()
}
}
const clear = () => {
currentCategories.value = []
selectedCategory.value = undefined
}
return {
currentCategories,
selectedCategory,
categoryStatus,
updateFromTargets,
clear
}
}
+124 -33
View File
@@ -55,9 +55,9 @@
<div class="flex-1">
<FormSelectBase
key="label"
v-model="selectedCategory"
v-model="revitMapperStore.selectedCategory"
name="categoryMapping"
placeholder="Select a category"
:placeholder="dropdownPlaceholder"
label="Target Category"
fixed-height
size="sm"
@@ -68,9 +68,9 @@
:allow-unset="false"
mount-menu-on-body
>
<template #something-selected="{ value }">
<template #something-selected>
<span class="text-primary text-xs">
{{ Array.isArray(value) ? value[0]?.label : value?.label }}
{{ displayLabel }}
</span>
</template>
<template #option="{ item }">
@@ -83,7 +83,7 @@
<FormButton
color="primary"
size="sm"
:disabled="!selectedCategory"
:disabled="!revitMapperStore.selectedCategory?.value"
@click="assignToCategory()"
>
Apply
@@ -193,6 +193,7 @@
import { storeToRefs } from 'pinia'
import { ArrowLeftIcon } from '@heroicons/vue/20/solid'
import { useSelectionStore } from '~/store/selection'
import { useRevitMapper } from '~/store/revitMapper'
import type {
Category,
CategoryMapping,
@@ -201,15 +202,17 @@ import type {
// Import categories
import { getAvailableCategories, getCategoryLabel } from '~/lib/mapper/revit-categories'
import { useMixpanel } from '~/lib/core/composables/mixpanel'
// === STORES ===
const selectionStore = useSelectionStore()
const revitMapperStore = useRevitMapper()
const { selectionInfo } = storeToRefs(selectionStore)
const { trackEvent } = useMixpanel()
// === STATE ===
const selectedMappingMode = ref<string>('Selection')
const mappingModeOptions = ['Selection', 'Layer']
const selectedCategory = ref<Category | undefined>(undefined)
const categoryOptions = ref<Category[]>([])
const mappings = ref<CategoryMapping[]>([])
@@ -229,6 +232,11 @@ interface LayerOption {
name: string
}
// === MAPPING CATEGORY STATE MGMT ===
const app = useNuxtApp()
const { $revitMapperBinding, $baseBinding } = app
// const categoryState = useRevitCategoryState(categoryOptions, $revitMapperBinding)
// === COMPUTED ===
const hasTargetsSelected = computed(() => {
if (selectedMappingMode.value === 'Selection') {
@@ -248,9 +256,23 @@ const currentLayerMappings = computed(() => {
return selectedMappingMode.value === 'Layer' ? layerMappings.value : []
})
const dropdownPlaceholder = computed(() => {
if (revitMapperStore.categoryStatus) {
return revitMapperStore.categoryStatus?.isMultiple
? 'Multiple categories'
: 'Select a category'
}
return undefined
})
const displayLabel = computed(() => {
const multiple = revitMapperStore.categoryStatus?.isMultiple
return multiple
? 'Multiple categories'
: revitMapperStore.selectedCategory?.label || ''
})
// === METHODS ===
const app = useNuxtApp()
const { $revitMapperBinding, $baseBinding } = app
// Search predicate for category dropdown
const searchFilterPredicate = (item: Category, query: string) => {
@@ -297,6 +319,12 @@ const confirmModeChange = async () => {
await $revitMapperBinding?.clearAllLayerCategoryAssignments()
}
// Track the manual mode switch
trackEvent('DUI3 Action', {
name: 'Mapper Mode Changed',
mode: selectedMappingMode.value
})
// Switch mode
selectedMappingMode.value = pendingMode.value
await refreshMappings()
@@ -312,19 +340,38 @@ const confirmModeChange = async () => {
// Assign selected objects/layers to the chosen category
const assignToCategory = async () => {
if (!selectedCategory.value || !hasTargetsSelected.value) return
if (!revitMapperStore.selectedCategory?.value || !hasTargetsSelected.value) return
try {
if (selectedMappingMode.value === 'Selection') {
await $revitMapperBinding?.assignObjectsToCategory(
selectionInfo.value?.selectedObjectIds || [],
selectedCategory.value.value
)
} else if (selectedMappingMode.value === 'Layer') {
await $revitMapperBinding?.assignLayerToCategory(
selectedLayers.value.map((layer) => layer.id),
selectedCategory.value.value
)
let assignedCount = 0
const { selectedCategory } = storeToRefs(revitMapperStore)
const categoryValue = selectedCategory?.value?.value
if (selectedMappingMode.value === 'Selection' && categoryValue) {
const objectIds = selectionInfo.value?.selectedObjectIds || []
await $revitMapperBinding?.assignObjectsToCategory(objectIds, categoryValue)
assignedCount = objectIds.length
// Track the assignment
trackEvent('DUI3 Action', {
name: 'Mapper Assign Category',
category: categoryValue,
count: assignedCount,
mappingType: 'object'
})
} else if (selectedMappingMode.value === 'Layer' && categoryValue) {
const layerIds = selectedLayers.value.map((layer) => layer.id)
await $revitMapperBinding?.assignLayerToCategory(layerIds, categoryValue)
assignedCount = selectedLayers.value.length
// Track the assignment
trackEvent('DUI3 Action', {
name: 'Mapper Assign Category',
category: categoryValue,
count: assignedCount,
mappingType: 'layer'
})
selectedLayers.value = []
}
@@ -384,9 +431,10 @@ const selectMappedObjects = async (mapping: CategoryMapping) => {
}
}
// Select mapped layers (highlight objects on those layers)
// Select mapped layers (highlight objects AND restore UI state)
const selectMappedLayers = async (layerMapping: LayerCategoryMapping) => {
try {
// 1. Highlight objects in Rhino
const effectiveObjectIds =
(await $revitMapperBinding?.getEffectiveObjectsForLayerMapping(
layerMapping.layerIds,
@@ -396,6 +444,24 @@ const selectMappedLayers = async (layerMapping: LayerCategoryMapping) => {
if (effectiveObjectIds.length > 0) {
await $baseBinding?.highlightObjects(effectiveObjectIds)
}
// 2. Restore UI state - populate layer selection
const layersToSelect = layerOptions.value.filter((layer) =>
layerMapping.layerIds.includes(layer.id)
)
selectedLayers.value = layersToSelect
// 3. Pre-select category in dropdown
const categoryToSelect = categoryOptions.value.find(
(cat) => cat.value === layerMapping.categoryValue
)
const { selectedCategory, currentCategories } = storeToRefs(revitMapperStore)
selectedCategory.value = categoryToSelect
// 4. Update reactive state
currentCategories.value = [layerMapping.categoryValue]
} catch (error) {
console.error('Failed to highlight effective objects:', error)
}
@@ -502,20 +568,24 @@ const loadAvailableLayers = async (): Promise<LayerOption[]> => {
}
}
// Refresh just the layer mappings
const refreshLayerMappings = async () => {
try {
const rawLayerMappings =
(await $revitMapperBinding?.getCurrentLayerMappings()) || []
// === WATCHER ===
watch(
() => ({
mode: selectedMappingMode.value,
objectIds: selectionInfo.value?.selectedObjectIds || [],
layerIds: selectedLayers.value.map((l) => l.id)
}),
async ({ mode, objectIds, layerIds }) => {
console.log('watcher', { mode, objectIds, layerIds })
layerMappings.value = rawLayerMappings.map((mapping) => ({
...mapping,
categoryLabel: getCategoryLabel(mapping.categoryValue)
}))
} catch (error) {
console.error('Failed to refresh layer mappings:', error)
}
}
if (mode === 'Selection') {
await revitMapperStore.updateFromTargets(objectIds, false)
} else if (mode === 'Layer') {
await revitMapperStore.updateFromTargets(layerIds, true)
}
},
{ immediate: true, deep: true }
)
// === LIFECYCLE ===
onMounted(async () => {
@@ -537,5 +607,26 @@ onMounted(async () => {
layerOptions.value = newLayers
selectedLayers.value = []
})
// Track mapper opened with the initial mode (after loadData determines it)
trackEvent('DUI3 Action', {
name: 'Mapper Opened',
mode: selectedMappingMode.value
})
})
// Refresh just layer mappings
const refreshLayerMappings = async () => {
try {
const rawLayerMappings =
(await $revitMapperBinding?.getCurrentLayerMappings()) || []
layerMappings.value = rawLayerMappings.map((mapping) => ({
...mapping,
categoryLabel: getCategoryLabel(mapping.categoryValue)
}))
} catch (error) {
console.error('Failed to refresh layer mappings:', error)
}
}
</script>
+75
View File
@@ -0,0 +1,75 @@
import { defineStore } from 'pinia'
import type { Category } from '~/lib/bindings/definitions/IRevitMapperBinding'
import { REVIT_CATEGORIES } from '~/lib/mapper/revit-categories'
export const useRevitMapper = defineStore('revitMapper', () => {
const app = useNuxtApp()
const { $revitMapperBinding } = app
const currentCategories = ref<string[]>([])
const selectedCategory = ref<Category | undefined>()
const categoryOptions = REVIT_CATEGORIES
const categoryStatus = computed(() => {
if (currentCategories.value.length === 0) {
return undefined
}
if (currentCategories.value.length === 1) {
const category = categoryOptions.find(
(cat) => cat.value === currentCategories.value[0]
)
return category ? { label: category.label, value: category.value } : undefined
}
return {
label: 'Multiple categories',
value: 'multiple',
isMultiple: true
}
})
const updateFromTargets = async (
targetIds: string[],
isLayerMode: boolean
): Promise<void> => {
if (!targetIds.length || !$revitMapperBinding) {
clear()
return
}
try {
// Call connector method based on mode
const categories = isLayerMode
? await $revitMapperBinding.getCategoryMappingsForLayers(targetIds)
: await $revitMapperBinding.getCategoryMappingsForObjects(targetIds)
currentCategories.value = categories
// Update dropdown selection based on categories found
if (categories.length === 1) {
selectedCategory.value = categoryOptions.find(
(cat) => cat.value === categories[0]
)
} else {
// Multiple or no categories - clear dropdown selection
selectedCategory.value = undefined
}
} catch (error) {
console.error('Failed to get category mappings:', error)
clear()
}
}
const clear = () => {
currentCategories.value = []
selectedCategory.value = undefined
}
return {
currentCategories,
selectedCategory,
categoryStatus,
updateFromTargets,
clear
}
})