@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,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ImportSession resolver + 비동기 worker.
|
|
3
|
+
*
|
|
4
|
+
* Mutation:
|
|
5
|
+
* - importBoardAsync(attachmentId, options): ImportSession (즉시 반환, 백그라운드에서 처리)
|
|
6
|
+
*
|
|
7
|
+
* Query:
|
|
8
|
+
* - importSession(id): ImportSession (진행 상황 polling)
|
|
9
|
+
*
|
|
10
|
+
* 비동기 worker 는 단일 프로세스 내 setImmediate 기반. 추후 큐(Redis/BullMQ) 로 이전 가능.
|
|
11
|
+
*/
|
|
12
|
+
import { Resolver, Query, Mutation, Arg, Ctx, Directive, Field, InputType } from 'type-graphql'
|
|
13
|
+
import { GraphQLJSON } from 'graphql-scalars'
|
|
14
|
+
import '@things-factory/auth-base'
|
|
15
|
+
|
|
16
|
+
import { getRepository } from '@things-factory/shell'
|
|
17
|
+
|
|
18
|
+
import { ImportSession } from './import-session.js'
|
|
19
|
+
import { runImportJob } from './import-worker.js'
|
|
20
|
+
|
|
21
|
+
@InputType()
|
|
22
|
+
class ImportBoardAsyncInput {
|
|
23
|
+
@Field({ description: 'Attachment id of the source drawing file (uploaded via attachment-base).' })
|
|
24
|
+
attachmentId!: string
|
|
25
|
+
|
|
26
|
+
@Field({ nullable: true, description: 'ChatSession id when triggered from board-ai chat.' })
|
|
27
|
+
chatSessionId?: string
|
|
28
|
+
|
|
29
|
+
@Field(() => [String], { nullable: true, description: 'Registry scopes to use (board-import).' })
|
|
30
|
+
scopes?: string[]
|
|
31
|
+
|
|
32
|
+
@Field({ nullable: true, description: 'Normalize coordinates so minX,minY=0' })
|
|
33
|
+
normalizeOrigin?: boolean
|
|
34
|
+
|
|
35
|
+
@Field({ nullable: true, description: 'Flip Y axis (CAD Y-up → scene Y-down).' })
|
|
36
|
+
flipY?: boolean
|
|
37
|
+
|
|
38
|
+
@Field({ nullable: true, description: 'Scale factor (e.g. 1 for mm:1unit).' })
|
|
39
|
+
scale?: number
|
|
40
|
+
|
|
41
|
+
@Field({ nullable: true, description: 'Apply Stage 4 (data binding).' })
|
|
42
|
+
applyBinding?: boolean
|
|
43
|
+
|
|
44
|
+
@Field(() => GraphQLJSON, { nullable: true, description: 'Adapter parse options (excludeLayers, maxEntities, ...).' })
|
|
45
|
+
parseOptions?: any
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
@Resolver()
|
|
49
|
+
export class ImportSessionResolver {
|
|
50
|
+
@Query(() => ImportSession, { nullable: true, description: 'Get import session progress.' })
|
|
51
|
+
@Directive('@privilege(category: "board-import", privilege: "query")')
|
|
52
|
+
async importSession(
|
|
53
|
+
@Arg('id') id: string,
|
|
54
|
+
@Ctx() context: ResolverContext
|
|
55
|
+
): Promise<ImportSession | null> {
|
|
56
|
+
const { domain } = context.state
|
|
57
|
+
return (await getRepository(ImportSession).findOneBy({ id, domain: { id: domain.id } })) ?? null
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
@Query(() => [ImportSession], { description: 'List recent import sessions for a chat session.' })
|
|
61
|
+
@Directive('@privilege(category: "board-import", privilege: "query")')
|
|
62
|
+
async importSessionsByChatSession(
|
|
63
|
+
@Arg('chatSessionId') chatSessionId: string,
|
|
64
|
+
@Ctx() context: ResolverContext
|
|
65
|
+
): Promise<ImportSession[]> {
|
|
66
|
+
const { domain } = context.state
|
|
67
|
+
return await getRepository(ImportSession).find({
|
|
68
|
+
where: { chatSessionId, domain: { id: domain.id } },
|
|
69
|
+
order: { createdAt: 'DESC' },
|
|
70
|
+
take: 50
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
@Mutation(() => ImportSession, {
|
|
75
|
+
description: '도면 → 보드 변환을 비동기로 시작. 즉시 ImportSession 반환, 백그라운드에서 처리.'
|
|
76
|
+
})
|
|
77
|
+
@Directive('@privilege(category: "board-import", privilege: "mutation")')
|
|
78
|
+
async importBoardAsync(
|
|
79
|
+
@Arg('input') input: ImportBoardAsyncInput,
|
|
80
|
+
@Ctx() context: ResolverContext
|
|
81
|
+
): Promise<ImportSession> {
|
|
82
|
+
const { domain, user } = context.state
|
|
83
|
+
const repo = getRepository(ImportSession)
|
|
84
|
+
|
|
85
|
+
const session = await repo.save({
|
|
86
|
+
domain,
|
|
87
|
+
attachmentId: input.attachmentId,
|
|
88
|
+
chatSessionId: input.chatSessionId,
|
|
89
|
+
status: 'queued',
|
|
90
|
+
progress: 0,
|
|
91
|
+
message: 'Queued',
|
|
92
|
+
optionsJson: JSON.stringify({
|
|
93
|
+
scopes: input.scopes,
|
|
94
|
+
normalizeOrigin: input.normalizeOrigin,
|
|
95
|
+
flipY: input.flipY,
|
|
96
|
+
scale: input.scale,
|
|
97
|
+
applyBinding: input.applyBinding,
|
|
98
|
+
parseOptions: input.parseOptions
|
|
99
|
+
}),
|
|
100
|
+
creator: user
|
|
101
|
+
} as any)
|
|
102
|
+
|
|
103
|
+
// 비동기 worker 시작 — 응답은 이미 즉시 반환됨
|
|
104
|
+
setImmediate(() => {
|
|
105
|
+
runImportJob(session.id!).catch(err => {
|
|
106
|
+
// worker 내부에서 처리되지만 안전망
|
|
107
|
+
console.error('[board-import] runImportJob unhandled:', err)
|
|
108
|
+
})
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
return session
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ImportSession — 도면(Attachment) → BoardModel 변환 작업 단위.
|
|
3
|
+
*
|
|
4
|
+
* 비동기 워크플로우:
|
|
5
|
+
* 1) 클라이언트가 importBoardAsync(attachmentId) 호출 → ImportSession 즉시 반환 (status:'queued')
|
|
6
|
+
* 2) 백그라운드 worker 가 attachment 본문 → importBoard() 실행 → status 갱신
|
|
7
|
+
* 3) 클라이언트는 polling 또는 subscription 으로 진행 상황 추적
|
|
8
|
+
* 4) status='completed' 시 result 에 BoardModel + stats + warnings 포함
|
|
9
|
+
*
|
|
10
|
+
* board-ai 의 ChatSession 과 연결 가능 (chatSessionId) — chat 안에서 자동 발동되면 진행 메시지가
|
|
11
|
+
* 자동으로 ChatMessage(role:'system') 으로 기록되어 AI 컨텍스트에 흐름.
|
|
12
|
+
*/
|
|
13
|
+
import {
|
|
14
|
+
Column,
|
|
15
|
+
CreateDateColumn,
|
|
16
|
+
Entity,
|
|
17
|
+
Index,
|
|
18
|
+
ManyToOne,
|
|
19
|
+
PrimaryGeneratedColumn,
|
|
20
|
+
RelationId,
|
|
21
|
+
UpdateDateColumn
|
|
22
|
+
} from 'typeorm'
|
|
23
|
+
import { Field, Float, ID, Int, ObjectType } from 'type-graphql'
|
|
24
|
+
import { GraphQLJSON } from 'graphql-scalars'
|
|
25
|
+
|
|
26
|
+
import { Domain } from '@things-factory/shell'
|
|
27
|
+
import { User } from '@things-factory/auth-base'
|
|
28
|
+
import { config } from '@things-factory/env'
|
|
29
|
+
|
|
30
|
+
const ORMCONFIG = config.get('ormconfig', {})
|
|
31
|
+
const DATABASE_TYPE = ORMCONFIG.type
|
|
32
|
+
|
|
33
|
+
@Entity()
|
|
34
|
+
@Index('ix_import_session_1', (session: ImportSession) => [session.domain, session.createdAt])
|
|
35
|
+
@Index('ix_import_session_2', (session: ImportSession) => [session.chatSessionId])
|
|
36
|
+
@ObjectType({ description: '도면 → 보드 변환 작업의 영속 단위 (비동기 진행상태 추적).' })
|
|
37
|
+
export class ImportSession {
|
|
38
|
+
@PrimaryGeneratedColumn('uuid')
|
|
39
|
+
@Field(type => ID, { nullable: true })
|
|
40
|
+
readonly id?: string
|
|
41
|
+
|
|
42
|
+
@ManyToOne(type => Domain)
|
|
43
|
+
@Field(type => Domain, { nullable: true })
|
|
44
|
+
domain?: Domain
|
|
45
|
+
|
|
46
|
+
@RelationId((session: ImportSession) => session.domain)
|
|
47
|
+
domainId?: string
|
|
48
|
+
|
|
49
|
+
/** Attachment.id (도면 파일). attachment-base 의존이지만 직접 ManyToOne 대신 ID 만 보유. */
|
|
50
|
+
@Column()
|
|
51
|
+
@Field({ description: 'Attachment id (the source drawing file)' })
|
|
52
|
+
attachmentId!: string
|
|
53
|
+
|
|
54
|
+
/** 어느 ChatSession 에서 발동됐는지 (board-ai 와 연동). 없으면 직접 호출. */
|
|
55
|
+
@Column({ nullable: true })
|
|
56
|
+
@Field({ nullable: true, description: 'ChatSession id when triggered from chat (board-ai).' })
|
|
57
|
+
chatSessionId?: string
|
|
58
|
+
|
|
59
|
+
/** 'queued' | 'parsing' | 'mapping' | 'assembling' | 'binding' | 'completed' | 'failed' */
|
|
60
|
+
@Column({ type: 'varchar', length: 32, default: 'queued' })
|
|
61
|
+
@Field({ description: 'queued | parsing | mapping | assembling | binding | completed | failed' })
|
|
62
|
+
status!: string
|
|
63
|
+
|
|
64
|
+
/** 0..100 */
|
|
65
|
+
@Column({ type: 'float', default: 0 })
|
|
66
|
+
@Field(type => Float, { description: 'Progress percentage 0..100' })
|
|
67
|
+
progress!: number
|
|
68
|
+
|
|
69
|
+
/** 현재 단계 설명 또는 에러 메시지 */
|
|
70
|
+
@Column({ nullable: true, type: 'text' })
|
|
71
|
+
@Field({ nullable: true })
|
|
72
|
+
message?: string
|
|
73
|
+
|
|
74
|
+
/** import 옵션 (scopes, parseOptions, normalizeOrigin 등) */
|
|
75
|
+
@Column({
|
|
76
|
+
nullable: true,
|
|
77
|
+
type:
|
|
78
|
+
DATABASE_TYPE == 'mysql' || DATABASE_TYPE == 'mariadb'
|
|
79
|
+
? 'longtext'
|
|
80
|
+
: DATABASE_TYPE == 'oracle'
|
|
81
|
+
? 'clob'
|
|
82
|
+
: DATABASE_TYPE == 'mssql'
|
|
83
|
+
? 'nvarchar'
|
|
84
|
+
: 'text',
|
|
85
|
+
length: DATABASE_TYPE == 'mssql' ? 'MAX' : undefined
|
|
86
|
+
})
|
|
87
|
+
optionsJson?: string
|
|
88
|
+
|
|
89
|
+
@Field(type => GraphQLJSON, { nullable: true, description: 'Options used (parsed JSON).' })
|
|
90
|
+
get options(): any {
|
|
91
|
+
try {
|
|
92
|
+
return this.optionsJson ? JSON.parse(this.optionsJson) : null
|
|
93
|
+
} catch {
|
|
94
|
+
return null
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** 결과: { boardModel, mapping: { stats, warnings } } */
|
|
99
|
+
@Column({
|
|
100
|
+
nullable: true,
|
|
101
|
+
type:
|
|
102
|
+
DATABASE_TYPE == 'mysql' || DATABASE_TYPE == 'mariadb'
|
|
103
|
+
? 'longtext'
|
|
104
|
+
: DATABASE_TYPE == 'oracle'
|
|
105
|
+
? 'clob'
|
|
106
|
+
: DATABASE_TYPE == 'mssql'
|
|
107
|
+
? 'nvarchar'
|
|
108
|
+
: 'text',
|
|
109
|
+
length: DATABASE_TYPE == 'mssql' ? 'MAX' : undefined
|
|
110
|
+
})
|
|
111
|
+
resultJson?: string
|
|
112
|
+
|
|
113
|
+
@Field(type => GraphQLJSON, { nullable: true, description: 'Result (parsed JSON): { boardModel, stats, warnings }.' })
|
|
114
|
+
get result(): any {
|
|
115
|
+
try {
|
|
116
|
+
return this.resultJson ? JSON.parse(this.resultJson) : null
|
|
117
|
+
} catch {
|
|
118
|
+
return null
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
@Column({ nullable: true })
|
|
123
|
+
@Field(type => Int, { nullable: true, description: 'Total entities counted (when known).' })
|
|
124
|
+
totalEntities?: number
|
|
125
|
+
|
|
126
|
+
@ManyToOne(type => User)
|
|
127
|
+
@Field(type => User, { nullable: true })
|
|
128
|
+
creator?: User
|
|
129
|
+
|
|
130
|
+
@CreateDateColumn()
|
|
131
|
+
@Field({ nullable: true })
|
|
132
|
+
createdAt?: Date
|
|
133
|
+
|
|
134
|
+
@UpdateDateColumn()
|
|
135
|
+
@Field({ nullable: true })
|
|
136
|
+
updatedAt?: Date
|
|
137
|
+
|
|
138
|
+
@Column({ nullable: true })
|
|
139
|
+
@Field({ nullable: true })
|
|
140
|
+
completedAt?: Date
|
|
141
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Background worker — ImportSession 처리.
|
|
3
|
+
*
|
|
4
|
+
* 흐름:
|
|
5
|
+
* 1) ImportSession 로드 (status: queued)
|
|
6
|
+
* 2) Attachment 본문 읽기 (attachment-base STORAGE.readFile)
|
|
7
|
+
* 3) importBoard() 실행 (단계별로 status/progress 갱신)
|
|
8
|
+
* 4) result 저장 + status='completed'
|
|
9
|
+
* 5) 실패 시 status='failed' + message
|
|
10
|
+
*
|
|
11
|
+
* 단일 프로세스 내 setImmediate 기반. 추후 외부 큐(Redis/BullMQ) 로 이전 가능.
|
|
12
|
+
*/
|
|
13
|
+
import { getRepository } from '@things-factory/shell'
|
|
14
|
+
|
|
15
|
+
import { Attachment } from '@things-factory/attachment-base'
|
|
16
|
+
import { STORAGE } from '@things-factory/attachment-base'
|
|
17
|
+
|
|
18
|
+
import { importBoard } from '../pipeline/index.js'
|
|
19
|
+
import { ImportSession } from './import-session.js'
|
|
20
|
+
|
|
21
|
+
export async function runImportJob(sessionId: string): Promise<void> {
|
|
22
|
+
const repo = getRepository(ImportSession)
|
|
23
|
+
const session = await repo.findOneBy({ id: sessionId })
|
|
24
|
+
if (!session) {
|
|
25
|
+
console.error(`[board-import] ImportSession ${sessionId} not found`)
|
|
26
|
+
return
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
await update(repo, sessionId, { status: 'parsing', progress: 5, message: 'Reading attachment' })
|
|
31
|
+
|
|
32
|
+
const attachment = await getRepository(Attachment).findOneBy({ id: session.attachmentId })
|
|
33
|
+
if (!attachment) {
|
|
34
|
+
throw new Error(`Attachment ${session.attachmentId} not found`)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// attachment-base STORAGE.readFile 으로 본문 로드 (Buffer)
|
|
38
|
+
const fileBuffer = STORAGE.readFile(attachment.path!, null) as Buffer
|
|
39
|
+
|
|
40
|
+
await update(repo, sessionId, { status: 'parsing', progress: 20, message: 'Parsing drawing' })
|
|
41
|
+
|
|
42
|
+
const opts = session.options || {}
|
|
43
|
+
const filename = attachment.name || attachment.path
|
|
44
|
+
|
|
45
|
+
// importBoard 는 내부에서 stage 1~4 모두 수행. 단계별 progress 는 우선 거시적 업데이트.
|
|
46
|
+
await update(repo, sessionId, { status: 'mapping', progress: 50, message: 'Mapping entities' })
|
|
47
|
+
|
|
48
|
+
const result = await importBoard({
|
|
49
|
+
file: fileBuffer,
|
|
50
|
+
filename,
|
|
51
|
+
scopes: opts.scopes,
|
|
52
|
+
normalizeOrigin: opts.normalizeOrigin,
|
|
53
|
+
flipY: opts.flipY,
|
|
54
|
+
scale: opts.scale,
|
|
55
|
+
applyBinding: opts.applyBinding,
|
|
56
|
+
parseOptions: opts.parseOptions
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
await update(repo, sessionId, { status: 'assembling', progress: 80, message: 'Assembling board' })
|
|
60
|
+
|
|
61
|
+
const resultJson = JSON.stringify({
|
|
62
|
+
boardModel: result.boardModel,
|
|
63
|
+
stats: result.mapping.stats,
|
|
64
|
+
warnings: result.mapping.warnings
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
await update(repo, sessionId, {
|
|
68
|
+
status: 'completed',
|
|
69
|
+
progress: 100,
|
|
70
|
+
message: `Completed: ${result.mapping.stats.matched} matched, ${result.mapping.stats.generic} generic`,
|
|
71
|
+
totalEntities: result.mapping.stats.total,
|
|
72
|
+
resultJson,
|
|
73
|
+
completedAt: new Date()
|
|
74
|
+
})
|
|
75
|
+
} catch (err) {
|
|
76
|
+
const message = (err as Error)?.message ?? String(err)
|
|
77
|
+
console.error(`[board-import] ImportSession ${sessionId} failed:`, err)
|
|
78
|
+
await update(repo, sessionId, {
|
|
79
|
+
status: 'failed',
|
|
80
|
+
message: `Failed: ${message}`,
|
|
81
|
+
completedAt: new Date()
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function update(repo: any, id: string, patch: Partial<ImportSession>): Promise<void> {
|
|
87
|
+
await repo.update(id, patch as any)
|
|
88
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { ImportSession } from './import-session.js'
|
|
2
|
+
import { ImportSessionResolver } from './import-session-resolver.js'
|
|
3
|
+
|
|
4
|
+
export { ImportSession, ImportSessionResolver }
|
|
5
|
+
export { runImportJob } from './import-worker.js'
|
|
6
|
+
|
|
7
|
+
export const entities = [ImportSession]
|
|
8
|
+
export const resolverClasses = [ImportSessionResolver]
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// ImportSession (비동기 import) — things-factory 모듈 통합용
|
|
2
|
+
import {
|
|
3
|
+
entities as ImportSessionEntities,
|
|
4
|
+
resolverClasses as ImportSessionResolverClasses
|
|
5
|
+
} from './import-session/index.js'
|
|
6
|
+
export * from './import-session/index.js'
|
|
7
|
+
|
|
8
|
+
export const entities = [...ImportSessionEntities]
|
|
9
|
+
export const schema = { resolverClasses: [...ImportSessionResolverClasses] }
|
|
10
|
+
|
|
11
|
+
// Pipeline (high-level)
|
|
12
|
+
export { importBoard } from './pipeline/index.js'
|
|
13
|
+
export type { ImportOptions, ImportResult } from './pipeline/index.js'
|
|
14
|
+
|
|
15
|
+
// Stage 별 (low-level)
|
|
16
|
+
export { applyMapping, GENERIC_OBJECT_TYPE } from './pipeline/stage2-mapping.js'
|
|
17
|
+
export { assembleBoard } from './pipeline/stage3-board.js'
|
|
18
|
+
export { applyDataBinding } from './pipeline/stage4-binding.js'
|
|
19
|
+
export type { BoardModel, BoardComponent, AssembleOptions } from './pipeline/stage3-board.js'
|
|
20
|
+
export type { MappingOptions } from './pipeline/stage2-mapping.js'
|
|
21
|
+
export type { BindingOptions } from './pipeline/stage4-binding.js'
|
|
22
|
+
|
|
23
|
+
// Converters
|
|
24
|
+
export { convertGenericTo, revertToGeneric } from './converters/generic-to.js'
|
|
25
|
+
export type { ConvertOptions } from './converters/generic-to.js'
|
|
26
|
+
|
|
27
|
+
// Adapters
|
|
28
|
+
export { DxfAdapter } from './adapters/dxf-adapter.js'
|
|
29
|
+
|
|
30
|
+
// Registry
|
|
31
|
+
export {
|
|
32
|
+
registerImportRules,
|
|
33
|
+
registerSubscriptionRules,
|
|
34
|
+
register3DPresets,
|
|
35
|
+
getImportRules,
|
|
36
|
+
getSubscriptionRules,
|
|
37
|
+
get3DPresets,
|
|
38
|
+
find3DPreset,
|
|
39
|
+
findSubscriptionRule,
|
|
40
|
+
clearImportRules,
|
|
41
|
+
clearSubscriptionRules,
|
|
42
|
+
clear3DPresets,
|
|
43
|
+
clearAllRegistries
|
|
44
|
+
} from './registry/index.js'
|
|
45
|
+
|
|
46
|
+
// Types — Universal Entity
|
|
47
|
+
export type {
|
|
48
|
+
EntityKind,
|
|
49
|
+
EntityAttribs,
|
|
50
|
+
EntityRelation,
|
|
51
|
+
Vec2,
|
|
52
|
+
Vec3,
|
|
53
|
+
Size2,
|
|
54
|
+
Size3,
|
|
55
|
+
BoundingBox,
|
|
56
|
+
UniversalEntity,
|
|
57
|
+
ParseResult,
|
|
58
|
+
AdapterMetadata,
|
|
59
|
+
ParseOptions,
|
|
60
|
+
AdapterInput,
|
|
61
|
+
FormatAdapter,
|
|
62
|
+
AdapterCapabilities
|
|
63
|
+
} from './types/index.js'
|
|
64
|
+
|
|
65
|
+
// Types — Registry
|
|
66
|
+
export type {
|
|
67
|
+
ComponentCategory,
|
|
68
|
+
ImportRule,
|
|
69
|
+
ImportMatcher,
|
|
70
|
+
ImportBuildSpec,
|
|
71
|
+
IdStrategy,
|
|
72
|
+
ComponentDraft,
|
|
73
|
+
ThreeDPreset,
|
|
74
|
+
ThreeDGeometry,
|
|
75
|
+
ThreeDMaterial,
|
|
76
|
+
SubscriptionRule,
|
|
77
|
+
MappingResult,
|
|
78
|
+
MappingWarning
|
|
79
|
+
} from './types/registry.js'
|
|
80
|
+
|
|
81
|
+
// AI domain interface (Phase 3+) — provider 추상화는 @things-factory/ai-client-base
|
|
82
|
+
export {
|
|
83
|
+
setAIClient,
|
|
84
|
+
getAIClient,
|
|
85
|
+
DefaultBoardImportAI
|
|
86
|
+
} from './ai/types.js'
|
|
87
|
+
export type {
|
|
88
|
+
BoardImportAI,
|
|
89
|
+
AIClient, // deprecated alias for BoardImportAI
|
|
90
|
+
ConventionInferenceOptions,
|
|
91
|
+
RuleSuggestion,
|
|
92
|
+
ClassificationOptions,
|
|
93
|
+
LabeledEntity,
|
|
94
|
+
EntityClassification,
|
|
95
|
+
ImageParseOptions
|
|
96
|
+
} from './ai/types.js'
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* High-level Pipeline — 도면 한 장 → 운영 가능한 Board Model.
|
|
3
|
+
*
|
|
4
|
+
* 백엔드에서 호출:
|
|
5
|
+
* ```ts
|
|
6
|
+
* import { importBoard } from '@things-factory/board-import'
|
|
7
|
+
*
|
|
8
|
+
* const buf = await fs.promises.readFile(path)
|
|
9
|
+
* const result = await importBoard({ file: buf, filename: 'plant.dxf', scopes: ['fmsim'] })
|
|
10
|
+
* ```
|
|
11
|
+
*/
|
|
12
|
+
import { DxfAdapter } from '../adapters/dxf-adapter.js'
|
|
13
|
+
import type { AdapterInput, FormatAdapter, ParseOptions, ParseResult } from '../types/index.js'
|
|
14
|
+
import type { MappingResult } from '../types/registry.js'
|
|
15
|
+
import { applyMapping } from './stage2-mapping.js'
|
|
16
|
+
import { assembleBoard, type AssembleOptions, type BoardModel } from './stage3-board.js'
|
|
17
|
+
import { applyDataBinding } from './stage4-binding.js'
|
|
18
|
+
|
|
19
|
+
export interface ImportOptions {
|
|
20
|
+
file: AdapterInput
|
|
21
|
+
filename?: string
|
|
22
|
+
/** 등록된 매핑 규칙의 어느 scope 를 사용할지 */
|
|
23
|
+
scopes?: string[]
|
|
24
|
+
/** 어댑터 옵션 (excludeLayers 등) */
|
|
25
|
+
parseOptions?: ParseOptions
|
|
26
|
+
/** 좌표 normalize (좌상단 0,0) */
|
|
27
|
+
normalizeOrigin?: boolean
|
|
28
|
+
/** Y 축 뒤집기 (CAD Y-up → scene Y-down) */
|
|
29
|
+
flipY?: boolean
|
|
30
|
+
/** scene unit 변환 배율 */
|
|
31
|
+
scale?: number
|
|
32
|
+
/** 데이터 바인딩 stage 적용 여부 */
|
|
33
|
+
applyBinding?: boolean
|
|
34
|
+
/** 사용자 정의 어댑터 (있으면 디폴트 검색 대신 사용) */
|
|
35
|
+
adapter?: FormatAdapter
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface ImportResult {
|
|
39
|
+
boardModel: BoardModel
|
|
40
|
+
mapping: MappingResult
|
|
41
|
+
parse: ParseResult
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const DEFAULT_ADAPTERS: FormatAdapter[] = [new DxfAdapter()]
|
|
45
|
+
|
|
46
|
+
export async function importBoard(opts: ImportOptions): Promise<ImportResult> {
|
|
47
|
+
const adapter = opts.adapter ?? (await pickAdapter(opts.file, opts.filename))
|
|
48
|
+
if (!adapter) throw new Error('지원되지 않는 도면 형식이거나 어댑터를 식별할 수 없습니다.')
|
|
49
|
+
|
|
50
|
+
const parsed = await adapter.parse(opts.file, opts.parseOptions)
|
|
51
|
+
const mapping = applyMapping(parsed, { scopes: opts.scopes })
|
|
52
|
+
|
|
53
|
+
const assembleOptions: AssembleOptions = {
|
|
54
|
+
scopes: opts.scopes,
|
|
55
|
+
normalizeOrigin: opts.normalizeOrigin ?? true,
|
|
56
|
+
flipY: opts.flipY ?? true,
|
|
57
|
+
scale: opts.scale ?? 1,
|
|
58
|
+
importMeta: {
|
|
59
|
+
format: adapter.format,
|
|
60
|
+
sourceBbox: parsed.bbox,
|
|
61
|
+
units: parsed.units,
|
|
62
|
+
importedAt: new Date().toISOString()
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
let boardModel = assembleBoard(mapping.components, parsed.bbox, assembleOptions)
|
|
66
|
+
|
|
67
|
+
if (opts.applyBinding !== false) {
|
|
68
|
+
boardModel = applyDataBinding(boardModel, { scopes: opts.scopes })
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return { boardModel, mapping, parse: parsed }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function pickAdapter(
|
|
75
|
+
file: AdapterInput,
|
|
76
|
+
filename?: string
|
|
77
|
+
): Promise<FormatAdapter | undefined> {
|
|
78
|
+
for (const a of DEFAULT_ADAPTERS) {
|
|
79
|
+
if (await a.detect(file, filename)) return a
|
|
80
|
+
}
|
|
81
|
+
return undefined
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export { applyMapping } from './stage2-mapping.js'
|
|
85
|
+
export { assembleBoard } from './stage3-board.js'
|
|
86
|
+
export { applyDataBinding } from './stage4-binding.js'
|
|
87
|
+
export type { BoardModel, BoardComponent, AssembleOptions } from './stage3-board.js'
|
|
88
|
+
export type { MappingOptions } from './stage2-mapping.js'
|
|
89
|
+
export type { BindingOptions } from './stage4-binding.js'
|