@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.
- package/config/config.development.js +44 -0
- package/config/config.production.js +18 -0
- package/dist-server/index.d.ts +7 -0
- package/dist-server/index.js +11 -0
- package/dist-server/index.js.map +1 -0
- package/dist-server/service/adapters/dxf-adapter.d.ts +8 -0
- package/dist-server/service/adapters/dxf-adapter.js +206 -0
- package/dist-server/service/adapters/dxf-adapter.js.map +1 -0
- package/dist-server/service/ai/types.d.ts +93 -0
- package/dist-server/service/ai/types.js +169 -0
- package/dist-server/service/ai/types.js.map +1 -0
- package/dist-server/service/converters/generic-to.d.ts +16 -0
- package/dist-server/service/converters/generic-to.js +78 -0
- package/dist-server/service/converters/generic-to.js.map +1 -0
- package/dist-server/service/import-session/import-session-resolver.d.ts +18 -0
- package/dist-server/service/import-session/import-session-resolver.js +135 -0
- package/dist-server/service/import-session/import-session-resolver.js.map +1 -0
- package/dist-server/service/import-session/import-session.d.ts +28 -0
- package/dist-server/service/import-session/import-session.js +152 -0
- package/dist-server/service/import-session/import-session.js.map +1 -0
- package/dist-server/service/import-session/import-worker.d.ts +1 -0
- package/dist-server/service/import-session/import-worker.js +79 -0
- package/dist-server/service/import-session/import-worker.js.map +1 -0
- package/dist-server/service/import-session/index.d.ts +6 -0
- package/dist-server/service/import-session/index.js +12 -0
- package/dist-server/service/import-session/index.js.map +1 -0
- package/dist-server/service/index.d.ts +22 -0
- package/dist-server/service/index.js +47 -0
- package/dist-server/service/index.js.map +1 -0
- package/dist-server/service/pipeline/index.d.ts +33 -0
- package/dist-server/service/pipeline/index.js +58 -0
- package/dist-server/service/pipeline/index.js.map +1 -0
- package/dist-server/service/pipeline/stage2-mapping.d.ts +14 -0
- package/dist-server/service/pipeline/stage2-mapping.js +173 -0
- package/dist-server/service/pipeline/stage2-mapping.js.map +1 -0
- package/dist-server/service/pipeline/stage3-board.d.ts +52 -0
- package/dist-server/service/pipeline/stage3-board.js +53 -0
- package/dist-server/service/pipeline/stage3-board.js.map +1 -0
- package/dist-server/service/pipeline/stage4-binding.d.ts +9 -0
- package/dist-server/service/pipeline/stage4-binding.js +28 -0
- package/dist-server/service/pipeline/stage4-binding.js.map +1 -0
- package/dist-server/service/registry/index.d.ts +27 -0
- package/dist-server/service/registry/index.js +108 -0
- package/dist-server/service/registry/index.js.map +1 -0
- package/dist-server/service/types/index.d.ts +117 -0
- package/dist-server/service/types/index.js +3 -0
- package/dist-server/service/types/index.js.map +1 -0
- package/dist-server/service/types/registry.d.ts +175 -0
- package/dist-server/service/types/registry.js +3 -0
- package/dist-server/service/types/registry.js.map +1 -0
- package/dist-server/tsconfig.tsbuildinfo +1 -0
- package/package.json +34 -0
- package/server/index.ts +7 -0
- package/server/service/adapters/dxf-adapter.ts +217 -0
- package/server/service/ai/types.ts +284 -0
- package/server/service/converters/generic-to.ts +98 -0
- package/server/service/import-session/import-session-resolver.ts +113 -0
- package/server/service/import-session/import-session.ts +141 -0
- package/server/service/import-session/import-worker.ts +88 -0
- package/server/service/import-session/index.ts +8 -0
- package/server/service/index.ts +96 -0
- package/server/service/pipeline/index.ts +89 -0
- package/server/service/pipeline/stage2-mapping.ts +195 -0
- package/server/service/pipeline/stage3-board.ts +114 -0
- package/server/service/pipeline/stage4-binding.ts +31 -0
- package/server/service/registry/index.ts +127 -0
- package/server/service/types/index.ts +133 -0
- package/server/service/types/registry.ts +186 -0
- package/things-factory.config.js +1 -0
- 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
|
+
}
|