@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,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'