Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c85fcdddd9 | |||
| b3ecc582d3 | |||
| 5e47af7641 | |||
| 008e6fde55 | |||
| c575203580 | |||
| 3b0c6dbcec | |||
| 314dd69357 | |||
| e88d0b75d5 | |||
| 16abccbb2e | |||
| 7ca5fcb625 | |||
| d33d32ad30 | |||
| 47ba084767 | |||
| 311e3f70ec | |||
| 1c05477c72 | |||
| 7b29add6c7 | |||
| 1ebbed1441 | |||
| ae83bca72b | |||
| d1d76f8b9a | |||
| 65062b8f89 | |||
| ece7211243 | |||
| 022687d28c | |||
| e4f991ebce | |||
| 2548062453 |
@@ -46,7 +46,7 @@
|
||||
]"
|
||||
@click="$router.push('/revit-mapper')"
|
||||
>
|
||||
Revit integration
|
||||
Assign Revit Categories
|
||||
</button>
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
<template>
|
||||
<div class="px-2">
|
||||
<p class="h5">Layer Selection</p>
|
||||
<div class="space-y-2 my-2">
|
||||
<!-- Multi-select layer dropdown -->
|
||||
<FormSelectMulti
|
||||
:model-value="selectedLayers"
|
||||
name="layerSelection"
|
||||
label="Select layers"
|
||||
class="w-full"
|
||||
fixed-height
|
||||
size="sm"
|
||||
:items="layerOptions"
|
||||
:allow-unset="false"
|
||||
by="id"
|
||||
search
|
||||
:search-placeholder="'Search layers...'"
|
||||
:filter-predicate="layerSearchFilterPredicate"
|
||||
mount-menu-on-body
|
||||
@update:model-value="(value) => $emit('update:selectedLayers', value as LayerOption[])"
|
||||
>
|
||||
<template #something-selected="{ value }">
|
||||
<span class="text-primary text-xs">
|
||||
{{ `${value.length} layer${value.length !== 1 ? 's' : ''} selected` }}
|
||||
</span>
|
||||
</template>
|
||||
<template #option="{ item }">
|
||||
<span class="text-xs">{{ item.name }}</span>
|
||||
</template>
|
||||
</FormSelectMulti>
|
||||
|
||||
<!-- Layer selection summary -->
|
||||
<div
|
||||
v-if="selectedLayers.length === 0"
|
||||
class="space-y-2 p-2 bg-highlight-1 rounded-md text-body-xs"
|
||||
>
|
||||
<div class="text-foreground-2">
|
||||
No layers selected, choose layers from the dropdown above!
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="space-y-2 p-2 bg-highlight-1 rounded-md text-body-xs">
|
||||
<div>
|
||||
Selected {{ selectedLayers.length }} layer{{
|
||||
selectedLayers.length !== 1 ? 's' : ''
|
||||
}}:
|
||||
{{ selectedLayers.map((l) => l.name).join(', ') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface LayerOption {
|
||||
id: string
|
||||
name: string
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
defineProps<{
|
||||
selectedLayers: LayerOption[]
|
||||
layerOptions: LayerOption[]
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
'update:selectedLayers': [layers: LayerOption[]]
|
||||
}>()
|
||||
|
||||
// Search predicate for layer dropdown
|
||||
const layerSearchFilterPredicate = (
|
||||
item: LayerOption | string | number | Record<string, unknown>,
|
||||
query: string
|
||||
): boolean => {
|
||||
if (typeof item === 'object' && item !== null && 'name' in item) {
|
||||
const layerItem = item as LayerOption
|
||||
return layerItem.name.toLowerCase().includes(query.toLowerCase())
|
||||
}
|
||||
return false
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,48 @@
|
||||
<template>
|
||||
<div class="py-1 px-2 bg-foundation border rounded-lg">
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="text-xs font-medium grow">{{ categoryLabel }}</div>
|
||||
|
||||
<div class="flex space-x-1">
|
||||
<div class="flex justify-center items-center text-xs text-foreground-2 mr-1">
|
||||
{{ countText }}
|
||||
</div>
|
||||
<FormButton
|
||||
v-if="tooltipText"
|
||||
v-tippy="tooltipText"
|
||||
size="sm"
|
||||
color="outline"
|
||||
:icon-left="CursorArrowRaysIcon"
|
||||
hide-text
|
||||
@click="$emit('select')"
|
||||
/>
|
||||
<FormButton
|
||||
v-else
|
||||
size="sm"
|
||||
color="outline"
|
||||
:icon-left="CursorArrowRaysIcon"
|
||||
hide-text
|
||||
@click="$emit('select')"
|
||||
/>
|
||||
<FormButton class="!px-1.5" size="sm" color="outline" @click="$emit('clear')">
|
||||
<TrashIcon class="w-3 h-3" />
|
||||
</FormButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { CursorArrowRaysIcon, TrashIcon } from '@heroicons/vue/24/outline'
|
||||
|
||||
defineProps<{
|
||||
categoryLabel: string
|
||||
countText: string
|
||||
tooltipText?: string
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
select: []
|
||||
clear: []
|
||||
}>()
|
||||
</script>
|
||||
@@ -0,0 +1,15 @@
|
||||
<template>
|
||||
<div class="space-y-2 p-2 bg-highlight-1 rounded-md text-body-xs">
|
||||
<div v-if="!hasSelection">
|
||||
No objects selected, go ahead and select some from your model!
|
||||
</div>
|
||||
<div v-else>{{ selectionSummary }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
hasSelection: boolean
|
||||
selectionSummary: string
|
||||
}>()
|
||||
</script>
|
||||
@@ -6,25 +6,29 @@ import type {
|
||||
export const IRevitMapperBindingKey = 'revitMapperBinding'
|
||||
|
||||
export interface IRevitMapperBinding extends IBinding<IMapperBindingEvents> {
|
||||
// Gets list of available Revit categories for the UI dropdown
|
||||
getAvailableCategories: () => Promise<Category[]>
|
||||
// Gets list of defined layers in doc
|
||||
getAvailableLayers: () => Promise<LayerOption[]>
|
||||
|
||||
// Assigns selected objects to a specific Revit category
|
||||
assignToCategory: (objectIds: string[], categoryValue: string) => Promise<void>
|
||||
// Object methods
|
||||
assignObjectsToCategory: (objectIds: string[], categoryValue: string) => Promise<void>
|
||||
clearObjectsCategoryAssignment: (objectIds: string[]) => Promise<void>
|
||||
clearAllObjectsCategoryAssignments: () => Promise<void>
|
||||
getCurrentObjectsMappings: () => Promise<CategoryMapping[]>
|
||||
|
||||
// Removes category assignments from specific objects
|
||||
clearCategoryAssignment: (objectIds: string[]) => Promise<void>
|
||||
|
||||
// Removes all category assignments in the doc
|
||||
clearAllCategoryAssignments: () => Promise<void>
|
||||
|
||||
// Gets all current mappings to show in the UI table
|
||||
getCurrentMappings: () => Promise<CategoryMapping[]>
|
||||
// Layer methods
|
||||
assignLayerToCategory: (layerIds: string[], categoryValue: string) => Promise<void>
|
||||
clearLayerCategoryAssignment: (layerIds: string[]) => Promise<void>
|
||||
clearAllLayerCategoryAssignments: () => Promise<void>
|
||||
getCurrentLayerMappings: () => Promise<LayerCategoryMapping[]>
|
||||
getEffectiveObjectsForLayerMapping: (
|
||||
layerIds: string[],
|
||||
categoryValue: string
|
||||
) => Promise<string[]>
|
||||
}
|
||||
|
||||
export interface IMapperBindingEvents extends IBindingSharedEvents {
|
||||
// Notify when mappings change
|
||||
mappingsChanged: (mappings: CategoryMapping[]) => void
|
||||
layersChanged: (layers: LayerOption[]) => void
|
||||
}
|
||||
|
||||
export interface Category {
|
||||
@@ -39,39 +43,88 @@ export interface CategoryMapping {
|
||||
objectCount: number
|
||||
}
|
||||
|
||||
export interface LayerCategoryMapping {
|
||||
categoryValue: string
|
||||
categoryLabel: string
|
||||
layerIds: string[]
|
||||
layerNames: string[]
|
||||
layerCount: number
|
||||
}
|
||||
|
||||
export interface LayerOption {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
|
||||
// Mock implementation for dev/testing
|
||||
export class MockedMapperBinding implements IRevitMapperBinding {
|
||||
private mockMappings: CategoryMapping[] = []
|
||||
|
||||
public assignToCategory(objectIds: string[], categoryValue: string): Promise<void> {
|
||||
public assignObjectsToCategory(
|
||||
objectIds: string[],
|
||||
categoryValue: string
|
||||
): Promise<void> {
|
||||
console.log('Mock: Assigning objects to category', { objectIds, categoryValue })
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
public getAvailableCategories(): Promise<Category[]> {
|
||||
public getAvailableLayers(): Promise<LayerOption[]> {
|
||||
return Promise.resolve([
|
||||
{ value: 'OST_Walls', label: 'Walls' },
|
||||
{ value: 'OST_Floors', label: 'Floors' },
|
||||
{ value: 'OST_Ceilings', label: 'Ceilings' },
|
||||
{ value: 'OST_Columns', label: 'Columns' }
|
||||
{ id: 'layer1', name: 'Ground Floor' },
|
||||
{ id: 'layer2', name: 'Ground Floor/Walls' },
|
||||
{ id: 'layer3', name: 'Ground Floor/Walls/Interior' },
|
||||
{ id: 'layer4', name: 'Second Floor' }
|
||||
])
|
||||
}
|
||||
|
||||
public clearCategoryAssignment(objectIds: string[]): Promise<void> {
|
||||
public clearObjectsCategoryAssignment(objectIds: string[]): Promise<void> {
|
||||
console.log('Mock: Clearing category assignment', { objectIds })
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
public clearAllCategoryAssignments(): Promise<void> {
|
||||
public clearAllObjectsCategoryAssignments(): Promise<void> {
|
||||
console.log('Mock: Clearing all assignments')
|
||||
this.mockMappings = []
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
public getCurrentMappings(): Promise<CategoryMapping[]> {
|
||||
public getCurrentObjectsMappings(): Promise<CategoryMapping[]> {
|
||||
return Promise.resolve(this.mockMappings)
|
||||
}
|
||||
|
||||
public assignLayerToCategory(
|
||||
layerIds: string[],
|
||||
categoryValue: string
|
||||
): Promise<void> {
|
||||
console.log('Mock: Assigning layers to category', { layerIds, categoryValue })
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
public clearLayerCategoryAssignment(layerIds: string[]): Promise<void> {
|
||||
console.log('Mock: Clearing layer category assignment', { layerIds })
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
public clearAllLayerCategoryAssignments(): Promise<void> {
|
||||
console.log('Mock: Clearing all layer assignments')
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
public getCurrentLayerMappings(): Promise<LayerCategoryMapping[]> {
|
||||
return Promise.resolve([])
|
||||
}
|
||||
|
||||
public getEffectiveObjectsForLayerMapping(
|
||||
layerIds: string[],
|
||||
categoryValue: string
|
||||
): Promise<string[]> {
|
||||
console.log('Mock: Getting effective objects for layer mapping', {
|
||||
layerIds,
|
||||
categoryValue
|
||||
})
|
||||
return Promise.resolve(['obj1', 'obj2', 'obj3'])
|
||||
}
|
||||
|
||||
public showDevTools(): Promise<void> {
|
||||
console.log('Braaaaa, no way!')
|
||||
return Promise.resolve()
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
import type { CategoryOption } from './types'
|
||||
|
||||
/**
|
||||
* Hardcoded Revit BuiltInCategories for the Interop Lite mapper.
|
||||
*/
|
||||
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_BridgeCables', label: 'Bridge Cables' },
|
||||
{ value: 'OST_BridgeDecks', label: 'Bridge Decks' },
|
||||
{ value: 'OST_ExpansionJoints', label: 'Expansion Joints' },
|
||||
{ value: 'OST_BridgePiers', label: 'Bridge Piers' },
|
||||
{ value: 'OST_VibrationManagement', label: 'Vibration Management' },
|
||||
|
||||
// ELECTRICAL
|
||||
{ value: 'OST_AudioVisualDevices', label: 'Audio Visual Devices' },
|
||||
{ value: 'OST_CableTray', label: 'Cable Tray' },
|
||||
{ value: 'OST_CableTrayFitting', label: 'Cable Tray Fittings' },
|
||||
{ value: 'OST_CommunicationDevices', label: 'Communication Devices' },
|
||||
{ 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_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' },
|
||||
{ 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' },
|
||||
|
||||
// MECHANICAL
|
||||
{ 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' },
|
||||
|
||||
// PIPING
|
||||
{ value: 'OST_PipeAccessory', label: 'Pipe Accessories' },
|
||||
{ value: 'OST_PipeCurves', label: 'Pipes' },
|
||||
{ value: 'OST_PipeFitting', label: 'Pipe Fittings' },
|
||||
{ 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' }
|
||||
] as const
|
||||
|
||||
/**
|
||||
* Get available categories sorted alphabetically by label.
|
||||
*/
|
||||
export function getAvailableCategories(): CategoryOption[] {
|
||||
return [...REVIT_CATEGORIES].sort((a, b) => a.label.localeCompare(b.label))
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the human-readable label for a category value.
|
||||
*/
|
||||
export function getCategoryLabel(categoryValue: string): string {
|
||||
const category = REVIT_CATEGORIES.find((c) => c.value === categoryValue)
|
||||
return category?.label ?? categoryValue
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface CategoryOption {
|
||||
value: string // e.g. "OST_Walls"
|
||||
label: string // e.g. "Walls"
|
||||
}
|
||||
+2
-2
@@ -115,7 +115,7 @@
|
||||
</span>
|
||||
</FormButton> -->
|
||||
</div>
|
||||
<!--Revit Integration button (only if mapper binding exists)-->
|
||||
<!--Assign Revit Categories button (only if mapper binding exists)-->
|
||||
<div v-if="app.$revitMapperBinding" class="mt-2">
|
||||
<hr class="border-outline-2 mb-2" />
|
||||
<FormButton
|
||||
@@ -125,7 +125,7 @@
|
||||
full-width
|
||||
@click="$router.push('/revit-mapper')"
|
||||
>
|
||||
Revit Integration
|
||||
Assign Revit Categories
|
||||
</FormButton>
|
||||
</div>
|
||||
</LayoutPanel>
|
||||
|
||||
+454
-127
@@ -1,31 +1,57 @@
|
||||
<template>
|
||||
<div class="flex flex-col space-y-2">
|
||||
<div class="px-2 mt-2">
|
||||
<FormButton to="/" size="sm" :icon-left="ArrowLeftIcon" class="my-2">
|
||||
<div class="px-2 space-y-1">
|
||||
<FormButton to="/" size="sm" :icon-left="ArrowLeftIcon" class="my-1">
|
||||
Home
|
||||
</FormButton>
|
||||
<hr />
|
||||
</div>
|
||||
|
||||
<!-- Step 1: Selection Mode (currently only support by selection) -->
|
||||
<!-- Step 1: Mapping Mode Selection -->
|
||||
<div class="px-2">
|
||||
<p class="h5">Selection</p>
|
||||
<p class="h5">Mapping Mode</p>
|
||||
<div class="space-y-2 my-2">
|
||||
<div class="space-y-2 p-2 bg-highlight-1 rounded-md text-body-xs">
|
||||
<div v-if="(selectionInfo?.selectedObjectIds?.length || 0) === 0">
|
||||
No objects selected, go ahead and select some from your model!
|
||||
</div>
|
||||
<div v-else>{{ selectionInfo?.summary }}</div>
|
||||
<FormSelectBase
|
||||
:model-value="selectedMappingMode"
|
||||
name="mappingMode"
|
||||
label="Mapping mode"
|
||||
class="w-full"
|
||||
fixed-height
|
||||
size="sm"
|
||||
:items="mappingModeOptions"
|
||||
:allow-unset="false"
|
||||
mount-menu-on-body
|
||||
@update:model-value="(value) => handleModeChange(value as string)"
|
||||
>
|
||||
<template #something-selected="{ value }">
|
||||
<span class="text-primary text-xs">{{ value }}</span>
|
||||
</template>
|
||||
<template #option="{ item }">
|
||||
<span class="text-xs">{{ item }}</span>
|
||||
</template>
|
||||
</FormSelectBase>
|
||||
|
||||
<!-- Mode-specific content -->
|
||||
<div v-if="selectedMappingMode === 'Selection'">
|
||||
<MapperSelectionMapper
|
||||
:has-selection="(selectionInfo?.selectedObjectIds?.length || 0) > 0"
|
||||
:selection-summary="selectionInfo?.summary || ''"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<MapperLayerMapper
|
||||
v-if="selectedMappingMode === 'Layer'"
|
||||
v-model:selected-layers="selectedLayers"
|
||||
:layer-options="layerOptions"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 2: Category Selection (only shown when objects are selected) -->
|
||||
<div v-if="hasObjectsSelected" class="px-2">
|
||||
<!-- Step 2: Category Selection -->
|
||||
<div v-if="hasTargetsSelected" class="px-2">
|
||||
<p class="h5">Target Category</p>
|
||||
<div class="space-y-2 my-2">
|
||||
<!-- Flex layout with dropdown and apply button side by side -->
|
||||
<div class="flex space-x-2">
|
||||
<div class="flex space-x-2 items-center">
|
||||
<div class="flex-1">
|
||||
<FormSelectBase
|
||||
key="label"
|
||||
@@ -44,7 +70,7 @@
|
||||
>
|
||||
<template #something-selected="{ value }">
|
||||
<span class="text-primary text-xs">
|
||||
{{ Array.isArray(value) ? value[0]?.label : value.label }}
|
||||
{{ Array.isArray(value) ? value[0]?.label : value?.label }}
|
||||
</span>
|
||||
</template>
|
||||
<template #option="{ item }">
|
||||
@@ -53,70 +79,111 @@
|
||||
</FormSelectBase>
|
||||
</div>
|
||||
|
||||
<!-- Apply button - same height as dropdown -->
|
||||
<!-- Apply button -->
|
||||
<FormButton
|
||||
color="primary"
|
||||
size="sm"
|
||||
class="h-8"
|
||||
:disabled="!selectedCategory"
|
||||
@click="assignToCategory()"
|
||||
>
|
||||
Apply Mapping
|
||||
Apply
|
||||
</FormButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr v-if="hasObjectsSelected" />
|
||||
<hr />
|
||||
|
||||
<!-- Step 3: Mappings Summary Table -->
|
||||
<div v-if="mappings.length > 0" class="px-2">
|
||||
<p class="h5">Current Mappings</p>
|
||||
<!-- Step 3: Mappings Summary Tables -->
|
||||
<div
|
||||
v-if="currentMappings.length > 0 || currentLayerMappings.length > 0"
|
||||
class="px-2"
|
||||
>
|
||||
<p class="h5">
|
||||
{{ `Current Mappings (${currentMappings.length > 0 ? 'Object' : 'Layer'})` }}
|
||||
</p>
|
||||
|
||||
<!-- Only mapping items get space-y -->
|
||||
<div class="space-y-1 my-2">
|
||||
<div
|
||||
v-for="mapping in mappings"
|
||||
:key="mapping.categoryValue"
|
||||
class="py-1 px-2 bg-foundation border rounded-lg"
|
||||
>
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="text-sm font-medium grow">{{ mapping.categoryLabel }}</div>
|
||||
|
||||
<div class="flex space-x-1">
|
||||
<div
|
||||
class="flex justify-center items-center text-xs text-foreground-2 mr-1"
|
||||
>
|
||||
{{ mapping.objectCount }} object{{
|
||||
mapping.objectCount !== 1 ? 's' : ''
|
||||
}}
|
||||
</div>
|
||||
<FormButton
|
||||
size="sm"
|
||||
color="outline"
|
||||
:icon-left="CursorArrowRaysIcon"
|
||||
hide-text
|
||||
@click="selectMappedObjects(mapping)"
|
||||
/>
|
||||
<FormButton
|
||||
class="!px-1.5"
|
||||
size="sm"
|
||||
color="outline"
|
||||
@click="clearMapping(mapping)"
|
||||
>
|
||||
<TrashIcon class="w-3 h-3" />
|
||||
</FormButton>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Object Mappings Section -->
|
||||
<div v-if="currentMappings.length > 0" class="my-2">
|
||||
<div class="space-y-1">
|
||||
<MapperMappedElementItem
|
||||
v-for="mapping in currentMappings"
|
||||
:key="mapping.categoryValue"
|
||||
:category-label="mapping.categoryLabel"
|
||||
:count-text="`${mapping.objectCount} object${
|
||||
mapping.objectCount !== 1 ? 's' : ''
|
||||
}`"
|
||||
@select="selectMappedObjects(mapping)"
|
||||
@clear="clearMapping(mapping)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Clear all button separated, with custom margin -->
|
||||
<div class="flex justify-end">
|
||||
<FormButton size="sm" color="danger" @click="clearAllMappings()">
|
||||
Clear All
|
||||
</FormButton>
|
||||
<!-- Layer Mappings Section -->
|
||||
<div v-if="currentLayerMappings.length > 0" class="my-2">
|
||||
<div class="space-y-1">
|
||||
<MapperMappedElementItem
|
||||
v-for="layerMapping in currentLayerMappings"
|
||||
:key="layerMapping.categoryValue"
|
||||
:category-label="layerMapping.categoryLabel"
|
||||
:count-text="`${layerMapping.layerCount} layer${
|
||||
layerMapping.layerCount !== 1 ? 's' : ''
|
||||
}`"
|
||||
:tooltip-text="`Layers: ${layerMapping.layerNames.join(', ')}`"
|
||||
@select="selectMappedLayers(layerMapping)"
|
||||
@clear="clearLayerMapping(layerMapping)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Clear All and Select All buttons -->
|
||||
<div class="flex justify-end space-x-2">
|
||||
<!-- Selection mode buttons -->
|
||||
<div
|
||||
v-if="selectedMappingMode === 'Selection' && currentMappings.length > 0"
|
||||
class="flex space-x-2"
|
||||
>
|
||||
<FormButton size="sm" color="outline" @click="selectAllMappedObjects()">
|
||||
Select All
|
||||
</FormButton>
|
||||
<FormButton size="sm" color="danger" @click="clearAllMappings()">
|
||||
Clear All Objects
|
||||
</FormButton>
|
||||
</div>
|
||||
|
||||
<!-- Layer mode buttons -->
|
||||
<div
|
||||
v-else-if="selectedMappingMode === 'Layer' && currentLayerMappings.length > 0"
|
||||
class="flex space-x-2"
|
||||
>
|
||||
<FormButton size="sm" color="outline" @click="selectAllMappedLayers()">
|
||||
Select All
|
||||
</FormButton>
|
||||
<FormButton size="sm" color="danger" @click="clearAllLayerMappings()">
|
||||
Clear All Layers
|
||||
</FormButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mode Confirmation Dialog -->
|
||||
<CommonDialog
|
||||
v-model:open="showModeConfirmDialog"
|
||||
title="Switch Mapping Mode"
|
||||
fullscreen="none"
|
||||
>
|
||||
<div class="text-sm text-foreground">
|
||||
{{ conflictMessage }}
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex justify-end space-x-2">
|
||||
<FormButton size="sm" color="outline" @click="cancelModeChange()">
|
||||
Cancel
|
||||
</FormButton>
|
||||
<FormButton size="sm" color="danger" @click="confirmModeChange()">
|
||||
Clear & Switch
|
||||
</FormButton>
|
||||
</div>
|
||||
</CommonDialog>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -124,89 +191,349 @@
|
||||
<script setup lang="ts">
|
||||
// === IMPORTS ===
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { ArrowLeftIcon, CursorArrowRaysIcon } from '@heroicons/vue/20/solid'
|
||||
import { TrashIcon } from '@heroicons/vue/24/outline'
|
||||
import { ArrowLeftIcon } from '@heroicons/vue/20/solid'
|
||||
import { useSelectionStore } from '~/store/selection'
|
||||
import type {
|
||||
Category,
|
||||
CategoryMapping
|
||||
CategoryMapping,
|
||||
LayerCategoryMapping
|
||||
} from '~/lib/bindings/definitions/IRevitMapperBinding'
|
||||
|
||||
// === STORE INTEGRATION ===
|
||||
const selectionStore = useSelectionStore()
|
||||
const { selectionInfo, hasBinding: hasSelectionBinding } = storeToRefs(selectionStore)
|
||||
const { $revitMapperBinding, $baseBinding } = useNuxtApp()
|
||||
// Import categories
|
||||
import { getAvailableCategories, getCategoryLabel } from '~/lib/mapper/revit-categories'
|
||||
|
||||
// === REACTIVE STATE ===
|
||||
const categoryOptions = ref<Category[]>([])
|
||||
// === STORES ===
|
||||
const selectionStore = useSelectionStore()
|
||||
const { selectionInfo } = storeToRefs(selectionStore)
|
||||
|
||||
// === STATE ===
|
||||
const selectedMappingMode = ref<string>('Selection')
|
||||
const mappingModeOptions = ['Selection', 'Layer']
|
||||
const selectedCategory = ref<Category | undefined>(undefined)
|
||||
const categoryOptions = ref<Category[]>([])
|
||||
const mappings = ref<CategoryMapping[]>([])
|
||||
|
||||
// Layer-specific state
|
||||
const selectedLayers = ref<LayerOption[]>([])
|
||||
const layerOptions = ref<LayerOption[]>([])
|
||||
const layerMappings = ref<LayerCategoryMapping[]>([])
|
||||
|
||||
// Mode switching state
|
||||
const showModeConfirmDialog = ref(false)
|
||||
const pendingMode = ref<string>('')
|
||||
const conflictMessage = ref('')
|
||||
|
||||
// === TYPES ===
|
||||
interface LayerOption {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
|
||||
// === COMPUTED ===
|
||||
const hasObjectsSelected = computed(
|
||||
() => (selectionInfo.value?.selectedObjectIds?.length || 0) > 0
|
||||
)
|
||||
|
||||
const searchFilterPredicate = (
|
||||
item: { value: string; label: string },
|
||||
search: string
|
||||
) => item.label.toLocaleLowerCase().includes(search.toLocaleLowerCase())
|
||||
|
||||
// === WATCHERS ===
|
||||
watch(
|
||||
hasSelectionBinding,
|
||||
(newValue) => {
|
||||
if (newValue) {
|
||||
selectionStore.refreshSelectionFromHostApp()
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
// === INITIALIZATION ===
|
||||
const loadCategories = async () => {
|
||||
const categories = (await $revitMapperBinding?.getAvailableCategories()) || []
|
||||
categoryOptions.value = categories
|
||||
}
|
||||
const refreshMappings = async () => {
|
||||
const currentMappings = (await $revitMapperBinding?.getCurrentMappings()) || []
|
||||
mappings.value = currentMappings
|
||||
}
|
||||
|
||||
// === CATEGORY ASSIGNMENT ===
|
||||
const assignToCategory = async () => {
|
||||
if (!selectedCategory.value || !selectionInfo.value?.selectedObjectIds) {
|
||||
return
|
||||
const hasTargetsSelected = computed(() => {
|
||||
if (selectedMappingMode.value === 'Selection') {
|
||||
return (selectionInfo.value?.selectedObjectIds?.length || 0) > 0
|
||||
} else if (selectedMappingMode.value === 'Layer') {
|
||||
return selectedLayers.value.length > 0
|
||||
}
|
||||
const objectIds = selectionInfo.value.selectedObjectIds
|
||||
await $revitMapperBinding?.assignToCategory(objectIds, selectedCategory.value.value)
|
||||
await refreshMappings()
|
||||
selectedCategory.value = undefined
|
||||
}
|
||||
// === CATEGORY CLEARING ===
|
||||
const clearMapping = async (mapping: CategoryMapping) => {
|
||||
await $revitMapperBinding?.clearCategoryAssignment(mapping.objectIds)
|
||||
await refreshMappings()
|
||||
}
|
||||
const clearAllMappings = async () => {
|
||||
await $revitMapperBinding?.clearAllCategoryAssignments()
|
||||
await refreshMappings()
|
||||
return false
|
||||
})
|
||||
|
||||
// Show appropriate mappings based on current mode
|
||||
const currentMappings = computed(() => {
|
||||
return selectedMappingMode.value === 'Selection' ? mappings.value : []
|
||||
})
|
||||
|
||||
const currentLayerMappings = computed(() => {
|
||||
return selectedMappingMode.value === 'Layer' ? layerMappings.value : []
|
||||
})
|
||||
|
||||
// === METHODS ===
|
||||
const app = useNuxtApp()
|
||||
const { $revitMapperBinding, $baseBinding } = app
|
||||
|
||||
// Search predicate for category dropdown
|
||||
const searchFilterPredicate = (item: Category, query: string) => {
|
||||
return item.label.toLowerCase().includes(query.toLowerCase())
|
||||
}
|
||||
|
||||
// === OBJECT SELECTION ===
|
||||
// Handle mode changes with conflict checking
|
||||
const handleModeChange = (newMode: string) => {
|
||||
// If switching to same mode, do nothing
|
||||
if (newMode === selectedMappingMode.value) return
|
||||
|
||||
// Check for conflicts - ONLY show dialog if there are existing mappings
|
||||
if (newMode === 'Layer' && mappings.value.length > 0) {
|
||||
// Switching to Layer mode with existing object mappings
|
||||
pendingMode.value = newMode
|
||||
conflictMessage.value = `Switching to Layer mode will clear all current object mappings. Continue?`
|
||||
showModeConfirmDialog.value = true
|
||||
} else if (newMode === 'Selection' && layerMappings.value.length > 0) {
|
||||
// Switching to Selection mode with existing layer mappings
|
||||
pendingMode.value = newMode
|
||||
conflictMessage.value = `Switching to Selection mode will clear all current layer mappings. Continue?`
|
||||
showModeConfirmDialog.value = true
|
||||
} else {
|
||||
// No conflicts, switch directly (no existing mappings or switching to same mode)
|
||||
selectedMappingMode.value = newMode
|
||||
}
|
||||
}
|
||||
|
||||
// Cancel mode change
|
||||
const cancelModeChange = () => {
|
||||
showModeConfirmDialog.value = false
|
||||
pendingMode.value = ''
|
||||
conflictMessage.value = ''
|
||||
}
|
||||
|
||||
// Confirm mode change and clear conflicting mappings
|
||||
const confirmModeChange = async () => {
|
||||
try {
|
||||
if (pendingMode.value === 'Layer') {
|
||||
// Clear all object mappings before switching to Layer mode
|
||||
await $revitMapperBinding?.clearAllObjectsCategoryAssignments()
|
||||
} else if (pendingMode.value === 'Selection') {
|
||||
// Clear all layer mappings before switching to Selection mode
|
||||
await $revitMapperBinding?.clearAllLayerCategoryAssignments()
|
||||
}
|
||||
|
||||
// Switch mode
|
||||
selectedMappingMode.value = pendingMode.value
|
||||
await refreshMappings()
|
||||
|
||||
// Close dialog
|
||||
showModeConfirmDialog.value = false
|
||||
pendingMode.value = ''
|
||||
conflictMessage.value = ''
|
||||
} catch (error) {
|
||||
console.error('Failed to clear mappings during mode switch:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// Assign selected objects/layers to the chosen category
|
||||
const assignToCategory = async () => {
|
||||
if (!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
|
||||
)
|
||||
selectedLayers.value = []
|
||||
}
|
||||
|
||||
selectedCategory.value = undefined
|
||||
await refreshMappings()
|
||||
} catch (error) {
|
||||
console.error('Failed to assign to category:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// Clear a specific object mapping
|
||||
const clearMapping = async (mapping: CategoryMapping) => {
|
||||
try {
|
||||
await $revitMapperBinding?.clearObjectsCategoryAssignment(mapping.objectIds)
|
||||
await refreshMappings()
|
||||
} catch (error) {
|
||||
console.error('Failed to clear mapping:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// Clear all object mappings
|
||||
const clearAllMappings = async () => {
|
||||
try {
|
||||
await $revitMapperBinding?.clearAllObjectsCategoryAssignments()
|
||||
await refreshMappings()
|
||||
} catch (error) {
|
||||
console.error('Failed to clear all mappings:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// Clear a specific layer mapping
|
||||
const clearLayerMapping = async (layerMapping: LayerCategoryMapping) => {
|
||||
try {
|
||||
await $revitMapperBinding?.clearLayerCategoryAssignment(layerMapping.layerIds)
|
||||
await refreshMappings()
|
||||
} catch (error) {
|
||||
console.error('Failed to clear layer mapping:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// Clear all layer mappings
|
||||
const clearAllLayerMappings = async () => {
|
||||
try {
|
||||
await $revitMapperBinding?.clearAllLayerCategoryAssignments()
|
||||
await refreshMappings()
|
||||
} catch (error) {
|
||||
console.error('Failed to clear all layer mappings:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// Select mapped objects in Rhino
|
||||
const selectMappedObjects = async (mapping: CategoryMapping) => {
|
||||
await $baseBinding?.highlightObjects(mapping.objectIds)
|
||||
try {
|
||||
await $baseBinding?.highlightObjects(mapping.objectIds)
|
||||
} catch (error) {
|
||||
console.error('Failed to highlight objects:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// Select mapped layers (highlight objects on those layers)
|
||||
const selectMappedLayers = async (layerMapping: LayerCategoryMapping) => {
|
||||
try {
|
||||
const effectiveObjectIds =
|
||||
(await $revitMapperBinding?.getEffectiveObjectsForLayerMapping(
|
||||
layerMapping.layerIds,
|
||||
layerMapping.categoryValue
|
||||
)) || []
|
||||
|
||||
if (effectiveObjectIds.length > 0) {
|
||||
await $baseBinding?.highlightObjects(effectiveObjectIds)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to highlight effective objects:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// Select all mapped objects (Selection mode)
|
||||
const selectAllMappedObjects = async () => {
|
||||
try {
|
||||
const allObjectIds = currentMappings.value.flatMap((mapping) => mapping.objectIds)
|
||||
if (allObjectIds.length > 0) {
|
||||
await $baseBinding?.highlightObjects(allObjectIds)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to select all mapped objects:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// Select all objects affected by layer mappings (Layer mode)
|
||||
const selectAllMappedLayers = async () => {
|
||||
try {
|
||||
const allEffectiveObjectIds: string[] = []
|
||||
|
||||
// Get effective objects for each layer mapping
|
||||
for (const layerMapping of currentLayerMappings.value) {
|
||||
const effectiveObjectIds =
|
||||
(await $revitMapperBinding?.getEffectiveObjectsForLayerMapping(
|
||||
layerMapping.layerIds,
|
||||
layerMapping.categoryValue
|
||||
)) || []
|
||||
allEffectiveObjectIds.push(...effectiveObjectIds)
|
||||
}
|
||||
|
||||
// Remove duplicates and highlight
|
||||
const uniqueObjectIds = [...new Set(allEffectiveObjectIds)]
|
||||
if (uniqueObjectIds.length > 0) {
|
||||
await $baseBinding?.highlightObjects(uniqueObjectIds)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to select all layer-mapped objects:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// Load available categories, layers, and current mappings
|
||||
const loadData = async () => {
|
||||
try {
|
||||
const [categories, rawMappings, rawLayerMappings, layers] = await Promise.all([
|
||||
getAvailableCategories() || [],
|
||||
$revitMapperBinding?.getCurrentObjectsMappings() || [],
|
||||
$revitMapperBinding?.getCurrentLayerMappings() || [],
|
||||
loadAvailableLayers()
|
||||
])
|
||||
|
||||
categoryOptions.value = categories
|
||||
|
||||
// Mappings need to be changed human-readable labels
|
||||
mappings.value = rawMappings.map((mapping) => ({
|
||||
...mapping,
|
||||
categoryLabel: getCategoryLabel(mapping.categoryValue)
|
||||
}))
|
||||
|
||||
layerMappings.value = rawLayerMappings.map((mapping) => ({
|
||||
...mapping,
|
||||
categoryLabel: getCategoryLabel(mapping.categoryValue)
|
||||
}))
|
||||
|
||||
layerOptions.value = layers
|
||||
} catch (error) {
|
||||
console.error('Failed to load mapper data:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh both object and layer mappings
|
||||
const refreshMappings = async () => {
|
||||
try {
|
||||
const [rawMappings, rawLayerMappings] = await Promise.all([
|
||||
$revitMapperBinding?.getCurrentObjectsMappings() || [],
|
||||
$revitMapperBinding?.getCurrentLayerMappings() || []
|
||||
])
|
||||
|
||||
// Transform to resolve labels
|
||||
mappings.value = rawMappings.map((mapping) => ({
|
||||
...mapping,
|
||||
categoryLabel: getCategoryLabel(mapping.categoryValue)
|
||||
}))
|
||||
|
||||
layerMappings.value = rawLayerMappings.map((mapping) => ({
|
||||
...mapping,
|
||||
categoryLabel: getCategoryLabel(mapping.categoryValue)
|
||||
}))
|
||||
} catch (error) {
|
||||
console.error('Failed to refresh mappings:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// Load available layers from Rhino document
|
||||
const loadAvailableLayers = async (): Promise<LayerOption[]> => {
|
||||
try {
|
||||
// Call the backend method to get available layers
|
||||
const layers = (await $revitMapperBinding?.getAvailableLayers()) || []
|
||||
return layers
|
||||
} catch (error) {
|
||||
console.error('Failed to load layers:', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh just the 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)
|
||||
}
|
||||
}
|
||||
|
||||
// === LIFECYCLE ===
|
||||
onMounted(async () => {
|
||||
$revitMapperBinding?.on('mappingsChanged', (updatedMappings: CategoryMapping[]) => {
|
||||
mappings.value = updatedMappings
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
|
||||
// Listen for mappings changes
|
||||
$revitMapperBinding?.on('mappingsChanged', (newMappings: CategoryMapping[]) => {
|
||||
mappings.value = newMappings.map((mapping) => ({
|
||||
...mapping,
|
||||
categoryLabel: getCategoryLabel(mapping.categoryValue)
|
||||
}))
|
||||
refreshLayerMappings()
|
||||
})
|
||||
|
||||
// Listen for layer list changes
|
||||
$revitMapperBinding?.on('layersChanged', (newLayers: LayerOption[]) => {
|
||||
layerOptions.value = newLayers
|
||||
selectedLayers.value = []
|
||||
})
|
||||
await loadCategories()
|
||||
await refreshMappings()
|
||||
if (hasSelectionBinding.value) {
|
||||
selectionStore.refreshSelectionFromHostApp()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user