@things-factory/board-import 10.0.0-beta.64

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 (70) hide show
  1. package/config/config.development.js +44 -0
  2. package/config/config.production.js +18 -0
  3. package/dist-server/index.d.ts +7 -0
  4. package/dist-server/index.js +11 -0
  5. package/dist-server/index.js.map +1 -0
  6. package/dist-server/service/adapters/dxf-adapter.d.ts +8 -0
  7. package/dist-server/service/adapters/dxf-adapter.js +206 -0
  8. package/dist-server/service/adapters/dxf-adapter.js.map +1 -0
  9. package/dist-server/service/ai/types.d.ts +93 -0
  10. package/dist-server/service/ai/types.js +169 -0
  11. package/dist-server/service/ai/types.js.map +1 -0
  12. package/dist-server/service/converters/generic-to.d.ts +16 -0
  13. package/dist-server/service/converters/generic-to.js +78 -0
  14. package/dist-server/service/converters/generic-to.js.map +1 -0
  15. package/dist-server/service/import-session/import-session-resolver.d.ts +18 -0
  16. package/dist-server/service/import-session/import-session-resolver.js +135 -0
  17. package/dist-server/service/import-session/import-session-resolver.js.map +1 -0
  18. package/dist-server/service/import-session/import-session.d.ts +28 -0
  19. package/dist-server/service/import-session/import-session.js +152 -0
  20. package/dist-server/service/import-session/import-session.js.map +1 -0
  21. package/dist-server/service/import-session/import-worker.d.ts +1 -0
  22. package/dist-server/service/import-session/import-worker.js +79 -0
  23. package/dist-server/service/import-session/import-worker.js.map +1 -0
  24. package/dist-server/service/import-session/index.d.ts +6 -0
  25. package/dist-server/service/import-session/index.js +12 -0
  26. package/dist-server/service/import-session/index.js.map +1 -0
  27. package/dist-server/service/index.d.ts +22 -0
  28. package/dist-server/service/index.js +47 -0
  29. package/dist-server/service/index.js.map +1 -0
  30. package/dist-server/service/pipeline/index.d.ts +33 -0
  31. package/dist-server/service/pipeline/index.js +58 -0
  32. package/dist-server/service/pipeline/index.js.map +1 -0
  33. package/dist-server/service/pipeline/stage2-mapping.d.ts +14 -0
  34. package/dist-server/service/pipeline/stage2-mapping.js +173 -0
  35. package/dist-server/service/pipeline/stage2-mapping.js.map +1 -0
  36. package/dist-server/service/pipeline/stage3-board.d.ts +52 -0
  37. package/dist-server/service/pipeline/stage3-board.js +53 -0
  38. package/dist-server/service/pipeline/stage3-board.js.map +1 -0
  39. package/dist-server/service/pipeline/stage4-binding.d.ts +9 -0
  40. package/dist-server/service/pipeline/stage4-binding.js +28 -0
  41. package/dist-server/service/pipeline/stage4-binding.js.map +1 -0
  42. package/dist-server/service/registry/index.d.ts +27 -0
  43. package/dist-server/service/registry/index.js +108 -0
  44. package/dist-server/service/registry/index.js.map +1 -0
  45. package/dist-server/service/types/index.d.ts +117 -0
  46. package/dist-server/service/types/index.js +3 -0
  47. package/dist-server/service/types/index.js.map +1 -0
  48. package/dist-server/service/types/registry.d.ts +175 -0
  49. package/dist-server/service/types/registry.js +3 -0
  50. package/dist-server/service/types/registry.js.map +1 -0
  51. package/dist-server/tsconfig.tsbuildinfo +1 -0
  52. package/package.json +34 -0
  53. package/server/index.ts +7 -0
  54. package/server/service/adapters/dxf-adapter.ts +217 -0
  55. package/server/service/ai/types.ts +284 -0
  56. package/server/service/converters/generic-to.ts +98 -0
  57. package/server/service/import-session/import-session-resolver.ts +113 -0
  58. package/server/service/import-session/import-session.ts +141 -0
  59. package/server/service/import-session/import-worker.ts +88 -0
  60. package/server/service/import-session/index.ts +8 -0
  61. package/server/service/index.ts +96 -0
  62. package/server/service/pipeline/index.ts +89 -0
  63. package/server/service/pipeline/stage2-mapping.ts +195 -0
  64. package/server/service/pipeline/stage3-board.ts +114 -0
  65. package/server/service/pipeline/stage4-binding.ts +31 -0
  66. package/server/service/registry/index.ts +127 -0
  67. package/server/service/types/index.ts +133 -0
  68. package/server/service/types/registry.ts +186 -0
  69. package/things-factory.config.js +1 -0
  70. package/tsconfig.json +9 -0
@@ -0,0 +1,195 @@
1
+ /**
2
+ * Stage 2 — Universal Entities → Component Drafts.
3
+ * 등록된 ImportRule 들을 entity 마다 평가하여 컴포넌트 type 결정.
4
+ */
5
+ import { getImportRules } from '../registry/index.js'
6
+ import type { ParseResult, UniversalEntity } from '../types/index.js'
7
+ import type {
8
+ ComponentDraft,
9
+ IdStrategy,
10
+ ImportMatcher,
11
+ MappingResult,
12
+ MappingWarning
13
+ } from '../types/registry.js'
14
+
15
+ /** 매핑되지 않은 entity 가 변환되는 placeholder 도메인 type. */
16
+ export const GENERIC_OBJECT_TYPE = 'GenericObject'
17
+
18
+ export interface MappingOptions {
19
+ /** 어떤 scope 의 규칙을 사용할지 (생략 시 전부) */
20
+ scopes?: string[]
21
+ /**
22
+ * 매칭 안 된 entity 를 GenericObject 로 변환할지 여부 (default: true).
23
+ * false 면 buffer 에서 제외되고 stats.skipped 로 카운트.
24
+ */
25
+ emitGeneric?: boolean
26
+ }
27
+
28
+ export function applyMapping(parsed: ParseResult, options: MappingOptions = {}): MappingResult {
29
+ const rules = getImportRules(options.scopes)
30
+ .slice()
31
+ .sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0))
32
+
33
+ const components: ComponentDraft[] = []
34
+ const warnings: MappingWarning[] = []
35
+ const stats = { total: parsed.entities.length, matched: 0, generic: 0, skipped: 0 }
36
+
37
+ const usedIds = new Map<string, number>()
38
+ const emitGeneric = options.emitGeneric !== false
39
+
40
+ for (const entity of parsed.entities) {
41
+ const rule = rules.find(r => matchRule(r.match, entity))
42
+
43
+ if (!rule) {
44
+ if (!emitGeneric) {
45
+ stats.skipped++
46
+ continue
47
+ }
48
+ const draft = makeGenericDraft(entity, usedIds)
49
+ components.push(draft)
50
+ stats.generic++
51
+ continue
52
+ }
53
+
54
+ const id = resolveId(rule.build.idStrategy ?? 'auto', entity, rule.componentType, usedIds)
55
+ let draft: ComponentDraft = {
56
+ category: rule.category ?? 'unknown',
57
+ type: rule.componentType,
58
+ id,
59
+ state: { ...(rule.build.defaultState || {}) },
60
+ matchedBy: rule,
61
+ source: entity
62
+ }
63
+
64
+ if (rule.build.transform) {
65
+ try {
66
+ draft = rule.build.transform(entity, draft)
67
+ } catch (e) {
68
+ warnings.push({
69
+ level: 'warn',
70
+ entity,
71
+ message: `transform 실패 (${rule.componentType}): ${(e as Error).message}`
72
+ })
73
+ }
74
+ }
75
+
76
+ components.push(draft)
77
+ stats.matched++
78
+ }
79
+
80
+ return { components, stats, warnings }
81
+ }
82
+
83
+ /**
84
+ * 매칭되지 않은 entity 를 GenericObject draft 로 변환.
85
+ * 좌표·크기·원본 attribs 모두 보존하여 사후 변환 가능하게 한다.
86
+ */
87
+ function makeGenericDraft(entity: UniversalEntity, usedIds: Map<string, number>): ComponentDraft {
88
+ const id = resolveId('auto', entity, GENERIC_OBJECT_TYPE, usedIds)
89
+ return {
90
+ category: 'unknown',
91
+ type: GENERIC_OBJECT_TYPE,
92
+ id,
93
+ state: {
94
+ strokeStyle: '#888888',
95
+ strokeDashArray: '4 4',
96
+ fillStyle: 'rgba(128,128,128,0.15)',
97
+ label: entity.attribs.name || entity.attribs.layer || entity.attribs.text || ''
98
+ },
99
+ source: entity
100
+ }
101
+ }
102
+
103
+ // ── 매칭 로직 ──────────────────────────────────────────────────────
104
+
105
+ function matchRule(matcher: ImportMatcher, entity: UniversalEntity): boolean {
106
+ if (matcher.cad) {
107
+ const m = matcher.cad
108
+ let ok = true
109
+ if (m.blockNamePattern && !m.blockNamePattern.test(entity.attribs.name || '')) ok = false
110
+ if (m.layer && !matchString(m.layer, entity.attribs.layer || '')) ok = false
111
+ if (m.entityKinds && !m.entityKinds.includes(entity.kind)) ok = false
112
+ if (m.textAdjacent) {
113
+ const adjacentText = (entity.neighbors || [])
114
+ .filter(r => r.relation === 'labeled-by')
115
+ .map(r => r.uid)
116
+ if (!adjacentText.some(t => m.textAdjacent!.test(t))) ok = false
117
+ }
118
+ if (ok) return true
119
+ }
120
+
121
+ if (matcher.ifc) {
122
+ const m = matcher.ifc
123
+ let ok = true
124
+ if (m.ifcClass && !matchString(m.ifcClass, entity.attribs.semanticType || '')) ok = false
125
+ if (ok) return true
126
+ }
127
+
128
+ if (matcher.geometry) {
129
+ const g = matcher.geometry
130
+ let ok = true
131
+ if (g.shape && entity.attribs.shape !== g.shape) ok = false
132
+ if (g.sizeRange && entity.size) {
133
+ const area = entity.size.width * entity.size.height
134
+ if (g.sizeRange.minArea !== undefined && area < g.sizeRange.minArea) ok = false
135
+ if (g.sizeRange.maxArea !== undefined && area > g.sizeRange.maxArea) ok = false
136
+ }
137
+ if (g.aspectRatio && entity.size) {
138
+ const ratio = entity.size.height === 0 ? 1 : entity.size.width / entity.size.height
139
+ if (g.aspectRatio.min !== undefined && ratio < g.aspectRatio.min) ok = false
140
+ if (g.aspectRatio.max !== undefined && ratio > g.aspectRatio.max) ok = false
141
+ }
142
+ if (ok) return true
143
+ }
144
+
145
+ if (matcher.image && matcher.image.labels.length > 0) {
146
+ const label = entity.attribs.vlmLabel
147
+ if (label && matcher.image.labels.includes(label)) return true
148
+ }
149
+
150
+ return false
151
+ }
152
+
153
+ function matchString(pattern: string | RegExp, value: string): boolean {
154
+ if (typeof pattern === 'string') return pattern === value
155
+ return pattern.test(value)
156
+ }
157
+
158
+ // ── ID 부여 전략 ──────────────────────────────────────────────────
159
+
160
+ function resolveId(
161
+ strategy: IdStrategy,
162
+ entity: UniversalEntity,
163
+ type: string,
164
+ usedIds: Map<string, number>
165
+ ): string {
166
+ let id: string | undefined
167
+
168
+ if (typeof strategy === 'object' && 'custom' in strategy) {
169
+ id = strategy.custom(entity)
170
+ } else {
171
+ switch (strategy) {
172
+ case 'fromAttribute':
173
+ id = entity.attribs.name
174
+ break
175
+ case 'fromAdjacentText':
176
+ id = (entity.neighbors || []).find(r => r.relation === 'labeled-by')?.uid
177
+ break
178
+ case 'fromIfcGlobalId':
179
+ id = entity.attribs.ifcGlobalId
180
+ break
181
+ case 'auto':
182
+ default:
183
+ id = undefined
184
+ }
185
+ }
186
+
187
+ if (!id) {
188
+ const seq = (usedIds.get(type) ?? 0) + 1
189
+ usedIds.set(type, seq)
190
+ id = `${type.toUpperCase()}_${String(seq).padStart(3, '0')}`
191
+ } else {
192
+ usedIds.set(id, (usedIds.get(id) ?? 0) + 1)
193
+ }
194
+ return id
195
+ }
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Stage 3 — ComponentDraft → Board Model JSON.
3
+ * 좌표/단위 변환 + 3D Preset 적용.
4
+ */
5
+ import { find3DPreset } from '../registry/index.js'
6
+ import type { BoundingBox } from '../types/index.js'
7
+ import type { ComponentCategory, ComponentDraft } from '../types/registry.js'
8
+
9
+ export interface BoardModel {
10
+ width?: number
11
+ height?: number
12
+ fillStyle?: string
13
+ /**
14
+ * 컴포넌트 목록. things-scene 보드 모델 호환을 위해 optional.
15
+ * 빈 보드 / legacy 형식 / root container 가 직접 model 인 경우 누락 가능.
16
+ * 사용처는 항상 `(model.components ?? [])` 패턴으로 접근.
17
+ */
18
+ components?: BoardComponent[]
19
+ /** 메타: import 출처 */
20
+ importMeta?: {
21
+ format: string
22
+ sourceBbox: BoundingBox
23
+ units?: string
24
+ importedAt: string
25
+ }
26
+ /** things-scene / 사용자 도메인이 임의 추가하는 필드 */
27
+ [k: string]: any
28
+ }
29
+
30
+ export interface BoardComponent {
31
+ /** Universal category — 모든 컴포넌트가 보유. 변환·렌더 정책의 기반. */
32
+ category: ComponentCategory
33
+ /** 도메인 type. 'GenericObject' 면 변환 가능한 placeholder. */
34
+ type: string
35
+ id?: string
36
+ left: number
37
+ top: number
38
+ width: number
39
+ height: number
40
+ rotation?: number
41
+ /** 3D Preset 해석 결과 (있으면) */
42
+ threeD?: Record<string, any>
43
+ [k: string]: any
44
+ }
45
+
46
+ export interface AssembleOptions {
47
+ /** scope 들 — 3D preset 조회 시 사용 */
48
+ scopes?: string[]
49
+ /** 좌표계 정규화: true 면 entities 의 minX,minY 가 0,0 이 되도록 이동 */
50
+ normalizeOrigin?: boolean
51
+ /** Y 축 뒤집기: CAD (Y-up) → scene (Y-down) */
52
+ flipY?: boolean
53
+ /** 단위 → scene unit 변환 비율. 1 이면 그대로. */
54
+ scale?: number
55
+ /** 보드 배경색 */
56
+ backgroundColor?: string
57
+ /** importMeta 를 모델에 첨부 */
58
+ importMeta?: BoardModel['importMeta']
59
+ }
60
+
61
+ export function assembleBoard(
62
+ drafts: ComponentDraft[],
63
+ bbox: BoundingBox,
64
+ options: AssembleOptions = {}
65
+ ): BoardModel {
66
+ const scale = options.scale ?? 1
67
+ const offsetX = options.normalizeOrigin ? -bbox.min.x : 0
68
+ const offsetY = options.normalizeOrigin ? -bbox.min.y : 0
69
+
70
+ const boardWidth = (bbox.max.x - bbox.min.x) * scale
71
+ const boardHeight = (bbox.max.y - bbox.min.y) * scale
72
+
73
+ const components: BoardComponent[] = drafts.map(d => {
74
+ const src = d.source
75
+ const left = src ? (src.position.x + offsetX) * scale : 0
76
+ const rawTop = src ? (src.position.y + offsetY) * scale : 0
77
+ const top = options.flipY ? boardHeight - rawTop - (src?.size?.height ?? 0) * scale : rawTop
78
+ const width = (src?.size?.width ?? 1) * scale
79
+ const height = (src?.size?.height ?? 1) * scale
80
+ const rotation = src?.rotation
81
+
82
+ const preset = find3DPreset(d.type, d.category, options.scopes)
83
+ const threeD = presetToState(preset)
84
+
85
+ return {
86
+ category: d.category,
87
+ type: d.type,
88
+ id: d.id,
89
+ left,
90
+ top,
91
+ width,
92
+ height,
93
+ ...(rotation !== undefined && { rotation: options.flipY ? -rotation : rotation }),
94
+ threeD,
95
+ ...d.state
96
+ }
97
+ })
98
+
99
+ return {
100
+ width: boardWidth || 1000,
101
+ height: boardHeight || 1000,
102
+ fillStyle: options.backgroundColor ?? '#ffffff',
103
+ components,
104
+ importMeta: options.importMeta
105
+ }
106
+ }
107
+
108
+ function presetToState(preset: import('../types/registry.js').ThreeDPreset): Record<string, any> {
109
+ return {
110
+ geometry: preset.geometry,
111
+ ...(preset.material && { material: preset.material }),
112
+ origin: preset.origin ?? 'bottom'
113
+ }
114
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Stage 4 — 보드 모델에 데이터 바인딩 자동 설정.
3
+ * 컴포넌트 type 에 등록된 SubscriptionRule 을 찾아 subscriptionTag 등 부여.
4
+ */
5
+ import { findSubscriptionRule } from '../registry/index.js'
6
+ import type { BoardComponent, BoardModel } from './stage3-board.js'
7
+
8
+ export interface BindingOptions {
9
+ scopes?: string[]
10
+ }
11
+
12
+ /**
13
+ * 보드 모델의 모든 컴포넌트에 subscription 정보를 주입.
14
+ * things-factory 의 dataSubscriptionProvider 가 인식하는 형태로.
15
+ */
16
+ export function applyDataBinding(model: BoardModel, options: BindingOptions = {}): BoardModel {
17
+ const components = model.components.map(c => bindComponent(c, options))
18
+ return { ...model, components }
19
+ }
20
+
21
+ function bindComponent(c: BoardComponent, options: BindingOptions): BoardComponent {
22
+ const rule = findSubscriptionRule(c.type, options.scopes)
23
+ if (!rule) return c
24
+
25
+ return {
26
+ ...c,
27
+ subscriptionTag: rule.tag,
28
+ subscriptionIdField: rule.idField ?? 'NAME',
29
+ ...(rule.fieldMappings && { subscriptionFieldMappings: rule.fieldMappings })
30
+ }
31
+ }
@@ -0,0 +1,127 @@
1
+ /**
2
+ * 앱이 자기 도메인 매핑을 등록하는 Registry.
3
+ * scope 별로 분리 보관 (예: 'fmsim', 'mms', 'wms').
4
+ *
5
+ * 백엔드 단일 프로세스 안에서 in-memory 로 유지된다. 모듈 부트스트랩 시 등록.
6
+ */
7
+ import type { ComponentCategory, ImportRule, SubscriptionRule, ThreeDPreset } from '../types/registry.js'
8
+
9
+ /**
10
+ * 코어가 제공하는 카테고리별 기본 3D Preset.
11
+ * 앱이 자기 도메인 preset 을 등록 안 해도, 카테고리만 정의되면 합리적 3D 표현.
12
+ *
13
+ * 매칭 우선순위: app preset(componentType) → app preset(category) → 이 디폴트(category) → unknown 디폴트
14
+ */
15
+ const CORE_DEFAULT_PRESETS_BY_CATEGORY: Record<ComponentCategory, ThreeDPreset> = {
16
+ container: { category: 'container', geometry: { kind: 'box', height: 3000 }, material: { color: '#888888', roughness: 0.7 }, origin: 'bottom' },
17
+ vehicle: { category: 'vehicle', geometry: { kind: 'box', height: 800 }, material: { color: '#3a8c5e' }, origin: 'bottom' },
18
+ path: { category: 'path', geometry: { kind: 'extrude', depth: 100 }, material: { color: '#bccfe0' } },
19
+ region: { category: 'region', geometry: { kind: 'extrude', depth: 10 }, material: { color: '#a3d9ff', opacity: 0.4 } },
20
+ equipment: { category: 'equipment', geometry: { kind: 'box', height: 2000 }, material: { color: '#777777' }, origin: 'bottom' },
21
+ io: { category: 'io', geometry: { kind: 'box', height: 200 }, material: { color: '#ffcc66' }, origin: 'bottom' },
22
+ marker: { category: 'marker', geometry: { kind: 'box', height: 100 }, material: { color: '#ffffff' } },
23
+ structure: { category: 'structure', geometry: { kind: 'extrude', depth: 4000 }, material: { color: '#dddddd', opacity: 0.3 }, origin: 'bottom' },
24
+ unknown: { category: 'unknown', geometry: { kind: 'box', height: 500 }, material: { color: '#aaaaaa', opacity: 0.7 }, origin: 'bottom' }
25
+ }
26
+
27
+ interface ScopedRegistry<T> {
28
+ [scope: string]: T[]
29
+ }
30
+
31
+ const importRulesByScope: ScopedRegistry<ImportRule> = {}
32
+ const threeDPresetsByScope: ScopedRegistry<ThreeDPreset> = {}
33
+ const subscriptionRulesByScope: ScopedRegistry<SubscriptionRule> = {}
34
+
35
+ // ── ImportRule ─────────────────────────────────────────────────────
36
+
37
+ export function registerImportRules(scope: string, rules: ImportRule[]): void {
38
+ importRulesByScope[scope] = (importRulesByScope[scope] || []).concat(rules)
39
+ }
40
+
41
+ export function getImportRules(scopes?: string[]): ImportRule[] {
42
+ if (!scopes || scopes.length === 0) {
43
+ return Object.values(importRulesByScope).flat()
44
+ }
45
+ return scopes.flatMap(s => importRulesByScope[s] || [])
46
+ }
47
+
48
+ export function clearImportRules(scope: string): void {
49
+ delete importRulesByScope[scope]
50
+ }
51
+
52
+ // ── 3D Preset ──────────────────────────────────────────────────────
53
+
54
+ export function register3DPresets(scope: string, presets: ThreeDPreset[]): void {
55
+ threeDPresetsByScope[scope] = (threeDPresetsByScope[scope] || []).concat(presets)
56
+ }
57
+
58
+ export function get3DPresets(scopes?: string[]): ThreeDPreset[] {
59
+ if (!scopes || scopes.length === 0) {
60
+ return Object.values(threeDPresetsByScope).flat()
61
+ }
62
+ return scopes.flatMap(s => threeDPresetsByScope[s] || [])
63
+ }
64
+
65
+ /**
66
+ * 컴포넌트 type + category 로 3D preset 을 찾는다.
67
+ * 우선순위:
68
+ * 1) 앱 등록 preset 중 componentType 정확 일치
69
+ * 2) 앱 등록 preset 중 category 일치
70
+ * 3) 코어 디폴트 카테고리 preset
71
+ * 4) 코어 디폴트 'unknown' preset
72
+ */
73
+ export function find3DPreset(
74
+ componentType: string,
75
+ category?: ComponentCategory,
76
+ scopes?: string[]
77
+ ): ThreeDPreset {
78
+ const appPresets = get3DPresets(scopes)
79
+
80
+ const exact = appPresets.find(p => p.componentType === componentType)
81
+ if (exact) return exact
82
+
83
+ if (category) {
84
+ const byCategory = appPresets.find(p => p.category === category && !p.componentType)
85
+ if (byCategory) return byCategory
86
+
87
+ if (CORE_DEFAULT_PRESETS_BY_CATEGORY[category]) {
88
+ return CORE_DEFAULT_PRESETS_BY_CATEGORY[category]
89
+ }
90
+ }
91
+
92
+ return CORE_DEFAULT_PRESETS_BY_CATEGORY.unknown
93
+ }
94
+
95
+ export function clear3DPresets(scope: string): void {
96
+ delete threeDPresetsByScope[scope]
97
+ }
98
+
99
+ // ── Subscription Rule ──────────────────────────────────────────────
100
+
101
+ export function registerSubscriptionRules(scope: string, rules: SubscriptionRule[]): void {
102
+ subscriptionRulesByScope[scope] = (subscriptionRulesByScope[scope] || []).concat(rules)
103
+ }
104
+
105
+ export function getSubscriptionRules(scopes?: string[]): SubscriptionRule[] {
106
+ if (!scopes || scopes.length === 0) {
107
+ return Object.values(subscriptionRulesByScope).flat()
108
+ }
109
+ return scopes.flatMap(s => subscriptionRulesByScope[s] || [])
110
+ }
111
+
112
+ export function findSubscriptionRule(
113
+ componentType: string,
114
+ scopes?: string[]
115
+ ): SubscriptionRule | undefined {
116
+ return getSubscriptionRules(scopes).find(r => r.componentType === componentType)
117
+ }
118
+
119
+ export function clearSubscriptionRules(scope: string): void {
120
+ delete subscriptionRulesByScope[scope]
121
+ }
122
+
123
+ export function clearAllRegistries(): void {
124
+ for (const k of Object.keys(importRulesByScope)) delete importRulesByScope[k]
125
+ for (const k of Object.keys(threeDPresetsByScope)) delete threeDPresetsByScope[k]
126
+ for (const k of Object.keys(subscriptionRulesByScope)) delete subscriptionRulesByScope[k]
127
+ }
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Universal Entity — 도면 형식(DXF/IFC/이미지/SVG/glTF)에 무관한 통일 모델.
3
+ * 모든 Format Adapter 가 이 타입으로 정규화한다.
4
+ */
5
+ export type EntityKind = 'group' | 'shape' | 'path' | 'text' | 'mesh3d' | 'image-region'
6
+
7
+ export interface Vec2 {
8
+ x: number
9
+ y: number
10
+ }
11
+
12
+ export interface Vec3 {
13
+ x: number
14
+ y: number
15
+ z: number
16
+ }
17
+
18
+ export interface Size2 {
19
+ width: number
20
+ height: number
21
+ }
22
+
23
+ export interface Size3 {
24
+ width: number
25
+ height: number
26
+ depth: number
27
+ }
28
+
29
+ export interface BoundingBox {
30
+ min: Vec2
31
+ max: Vec2
32
+ }
33
+
34
+ export interface UniversalEntity {
35
+ /** 도면 내 고유 ID */
36
+ uid: string
37
+ /** 엔티티 종류 */
38
+ kind: EntityKind
39
+ /** 위치 (좌상단 또는 중심 — 어댑터별로 정규화 정책 명시) */
40
+ position: Vec2
41
+ /** 크기 (없을 수도 있음, e.g. text) */
42
+ size?: Size2
43
+ /** 회전 (도수, 시계방향) */
44
+ rotation?: number
45
+ /** 이 엔티티의 bounding box */
46
+ bbox: BoundingBox
47
+ /** 의미적 단서 — 가능한 만큼 채움 */
48
+ attribs: EntityAttribs
49
+ /** 공간 관계 (선택) */
50
+ neighbors?: EntityRelation[]
51
+ /** 3D 자산 참조 (3D 형식만) */
52
+ meshRef?: string
53
+ }
54
+
55
+ export interface EntityAttribs {
56
+ /** 블록 이름, IFC GlobalId, 식별자 */
57
+ name?: string
58
+ /** CAD 레이어 이름 */
59
+ layer?: string
60
+ /** IFC 의미 클래스 (예: 'IfcStockerStorage') */
61
+ semanticType?: string
62
+ /** 텍스트 내용 (text 종류일 때) */
63
+ text?: string
64
+ /** 도면 모양 종류 (rect/circle/polyline 등) */
65
+ shape?: 'rect' | 'circle' | 'polyline' | 'polygon'
66
+ /** 임의 추가 속성 */
67
+ [k: string]: any
68
+ }
69
+
70
+ export interface EntityRelation {
71
+ uid: string
72
+ relation: 'adjacent' | 'inside' | 'connected' | 'labeled-by'
73
+ distance?: number
74
+ }
75
+
76
+ /**
77
+ * Adapter 결과 — 정규화된 entity 목록 + 메타데이터.
78
+ */
79
+ export interface ParseResult {
80
+ entities: UniversalEntity[]
81
+ metadata: AdapterMetadata
82
+ bbox: BoundingBox
83
+ /** 도면 단위 (mm/m/inch 등). 명시되지 않으면 'unknown' */
84
+ units?: 'mm' | 'cm' | 'm' | 'inch' | 'unknown'
85
+ }
86
+
87
+ export interface AdapterMetadata {
88
+ format: string
89
+ formatVersion?: string
90
+ source?: string
91
+ /** 어댑터가 알아낸 추가 메타 (예: DXF $INSUNITS) */
92
+ [k: string]: any
93
+ }
94
+
95
+ export interface ParseOptions {
96
+ /** 최대 entity 수 (성능 가드) */
97
+ maxEntities?: number
98
+ /** 무시할 레이어 이름들 */
99
+ excludeLayers?: string[]
100
+ /** 추가 옵션 */
101
+ [k: string]: any
102
+ }
103
+
104
+ /**
105
+ * Adapter 입력 — 백엔드/프론트엔드 양쪽에서 사용 가능하도록 최대한 폭넓게 수용.
106
+ * - string: DXF 등 텍스트 포맷의 본문
107
+ * - Buffer: Node 환경의 파일 바이너리
108
+ * - ArrayBuffer / Uint8Array: 바이너리 페이로드
109
+ */
110
+ export type AdapterInput = string | ArrayBuffer | Uint8Array | Buffer
111
+
112
+ /**
113
+ * Format Adapter — 형식별 파서. 모든 어댑터는 동일 인터페이스 구현.
114
+ */
115
+ export interface FormatAdapter {
116
+ readonly format: string
117
+ readonly supportedExtensions: string[]
118
+ readonly capabilities: AdapterCapabilities
119
+
120
+ /** 파일이 이 어댑터로 처리 가능한지 빠른 검사 (확장자 + 시그니처) */
121
+ detect(file: AdapterInput, filename?: string): Promise<boolean>
122
+
123
+ /** 실제 파싱 → UniversalEntity 목록 */
124
+ parse(file: AdapterInput, options?: ParseOptions): Promise<ParseResult>
125
+ }
126
+
127
+ export interface AdapterCapabilities {
128
+ has2D: boolean
129
+ has3D: boolean
130
+ hasSemanticLabels: boolean
131
+ hasTextLabels: boolean
132
+ hasAttributes: boolean
133
+ }