@things-factory/board-import 10.0.0-beta.71 → 10.0.0-beta.72

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 (83) hide show
  1. package/dist-server/index.d.ts +1 -1
  2. package/dist-server/index.js +1 -1
  3. package/dist-server/index.js.map +1 -1
  4. package/dist-server/service/adapters/dxf-adapter.d.ts +1 -1
  5. package/dist-server/service/adapters/dxf-adapter.js +18 -4
  6. package/dist-server/service/adapters/dxf-adapter.js.map +1 -1
  7. package/dist-server/service/adapters/image-adapter.d.ts +32 -0
  8. package/dist-server/service/adapters/image-adapter.js +420 -0
  9. package/dist-server/service/adapters/image-adapter.js.map +1 -0
  10. package/dist-server/service/ai/types.d.ts +96 -4
  11. package/dist-server/service/ai/types.js +221 -12
  12. package/dist-server/service/ai/types.js.map +1 -1
  13. package/dist-server/service/converters/generic-to.d.ts +37 -8
  14. package/dist-server/service/converters/generic-to.js +60 -44
  15. package/dist-server/service/converters/generic-to.js.map +1 -1
  16. package/dist-server/service/import-session/import-actions.d.ts +79 -0
  17. package/dist-server/service/import-session/import-actions.js +98 -0
  18. package/dist-server/service/import-session/import-actions.js.map +1 -0
  19. package/dist-server/service/import-session/import-session-resolver.d.ts +22 -1
  20. package/dist-server/service/import-session/import-session-resolver.js +166 -38
  21. package/dist-server/service/import-session/import-session-resolver.js.map +1 -1
  22. package/dist-server/service/import-session/import-worker.js +55 -13
  23. package/dist-server/service/import-session/import-worker.js.map +1 -1
  24. package/dist-server/service/import-session/index.d.ts +5 -3
  25. package/dist-server/service/import-session/index.js +16 -9
  26. package/dist-server/service/import-session/index.js.map +1 -1
  27. package/dist-server/service/import-session/materialize-from-session.d.ts +66 -0
  28. package/dist-server/service/import-session/materialize-from-session.js +104 -0
  29. package/dist-server/service/import-session/materialize-from-session.js.map +1 -0
  30. package/dist-server/service/import-session/suggest-board-name.d.ts +80 -0
  31. package/dist-server/service/import-session/suggest-board-name.js +195 -0
  32. package/dist-server/service/import-session/suggest-board-name.js.map +1 -0
  33. package/dist-server/service/import-tools.d.ts +10 -0
  34. package/dist-server/service/import-tools.js +227 -0
  35. package/dist-server/service/import-tools.js.map +1 -0
  36. package/dist-server/service/index.d.ts +22 -19
  37. package/dist-server/service/index.js +47 -36
  38. package/dist-server/service/index.js.map +1 -1
  39. package/dist-server/service/pipeline/index.d.ts +42 -9
  40. package/dist-server/service/pipeline/index.js +92 -23
  41. package/dist-server/service/pipeline/index.js.map +1 -1
  42. package/dist-server/service/pipeline/stage2-mapping.d.ts +20 -5
  43. package/dist-server/service/pipeline/stage2-mapping.js +84 -26
  44. package/dist-server/service/pipeline/stage2-mapping.js.map +1 -1
  45. package/dist-server/service/pipeline/stage3-board.d.ts +2 -2
  46. package/dist-server/service/pipeline/stage3-board.js +65 -10
  47. package/dist-server/service/pipeline/stage3-board.js.map +1 -1
  48. package/dist-server/service/pipeline/stage4-binding.d.ts +1 -1
  49. package/dist-server/service/pipeline/stage4-binding.js +2 -2
  50. package/dist-server/service/pipeline/stage4-binding.js.map +1 -1
  51. package/dist-server/service/registry/index.d.ts +1 -1
  52. package/dist-server/service/registry/index.js.map +1 -1
  53. package/dist-server/service/types/index.d.ts +35 -0
  54. package/dist-server/service/types/index.js.map +1 -1
  55. package/dist-server/tsconfig.tsbuildinfo +1 -1
  56. package/package.json +7 -6
  57. package/server/index.ts +1 -1
  58. package/server/service/adapters/dxf-adapter.ts +17 -4
  59. package/server/service/adapters/image-adapter.test.ts +545 -0
  60. package/server/service/adapters/image-adapter.ts +464 -0
  61. package/server/service/ai/types.ts +354 -19
  62. package/server/service/converters/generic-to.test.ts +145 -0
  63. package/server/service/converters/generic-to.ts +114 -45
  64. package/server/service/import-session/import-actions.test.ts +185 -0
  65. package/server/service/import-session/import-actions.ts +164 -0
  66. package/server/service/import-session/import-session-resolver.ts +171 -39
  67. package/server/service/import-session/import-worker.ts +56 -12
  68. package/server/service/import-session/index.ts +19 -3
  69. package/server/service/import-session/materialize-from-session.test.ts +274 -0
  70. package/server/service/import-session/materialize-from-session.ts +158 -0
  71. package/server/service/import-session/suggest-board-name.test.ts +271 -0
  72. package/server/service/import-session/suggest-board-name.ts +279 -0
  73. package/server/service/import-tools.test.ts +137 -0
  74. package/server/service/import-tools.ts +255 -0
  75. package/server/service/index.ts +36 -18
  76. package/server/service/pipeline/index.ts +118 -23
  77. package/server/service/pipeline/stage2-mapping.test.ts +204 -0
  78. package/server/service/pipeline/stage2-mapping.ts +102 -27
  79. package/server/service/pipeline/stage3-board.test.ts +133 -0
  80. package/server/service/pipeline/stage3-board.ts +76 -11
  81. package/server/service/pipeline/stage4-binding.ts +2 -2
  82. package/server/service/registry/index.ts +1 -1
  83. package/server/service/types/index.ts +36 -0
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Stage 3 — assembleBoard 의 컴포넌트 root 외곽 spill 방지 회귀 테스트.
3
+ *
4
+ * 검증 포인트:
5
+ * - bbox 가 컴포넌트 position+size 보다 작아도 board 가 그것 cover
6
+ * - normalizeOrigin + flipY 조합에서 컴포넌트 음수 좌표 회피 (root 위쪽 spill 방지)
7
+ * - padding 으로 컴포넌트가 root 가장자리에 붙지 않음
8
+ * - 빈 board 는 1000x1000 fallback
9
+ */
10
+ import { assembleBoard } from './stage3-board'
11
+ import type { ComponentDraft } from '../types/registry'
12
+ import type { BoundingBox, UniversalEntity } from '../types/index'
13
+
14
+ function entity(uid: string, x: number, y: number, w: number, h: number): UniversalEntity {
15
+ return {
16
+ uid,
17
+ kind: 'shape',
18
+ position: { x, y },
19
+ size: { width: w, height: h },
20
+ bbox: { min: { x, y }, max: { x: x + w, y: y + h } },
21
+ attribs: {}
22
+ }
23
+ }
24
+
25
+ function draft(src: UniversalEntity): ComponentDraft {
26
+ return {
27
+ category: 'unknown',
28
+ type: 'placeholder',
29
+ id: src.uid,
30
+ state: {},
31
+ source: src
32
+ }
33
+ }
34
+
35
+ describe('assembleBoard — board 크기 산정', () => {
36
+ test('컴포넌트 모두 root 안 — left/top 모두 0 이상, right/bottom 모두 board 크기 이하', () => {
37
+ const drafts = [draft(entity('a', 0, 0, 100, 80)), draft(entity('b', 200, 100, 60, 60))]
38
+ const bbox: BoundingBox = { min: { x: 0, y: 0 }, max: { x: 260, y: 160 } }
39
+ const board = assembleBoard(drafts, bbox, { normalizeOrigin: true, flipY: false, scale: 1 })
40
+
41
+ for (const c of board.components ?? []) {
42
+ expect(c.left).toBeGreaterThanOrEqual(0)
43
+ expect(c.top).toBeGreaterThanOrEqual(0)
44
+ expect(c.left + c.width).toBeLessThanOrEqual(board.width!)
45
+ expect(c.top + c.height).toBeLessThanOrEqual(board.height!)
46
+ }
47
+ })
48
+
49
+ test('bbox 가 컴포넌트 size 보다 작아도 board 가 컴포넌트 fully cover', () => {
50
+ // 의도적으로 bbox 를 entity 보다 작게 — VLM 이 lossy bbox 줬을 때 시뮬레이션
51
+ const drafts = [draft(entity('a', 0, 0, 500, 400))]
52
+ const tightBbox: BoundingBox = { min: { x: 0, y: 0 }, max: { x: 100, y: 100 } }
53
+ const board = assembleBoard(drafts, tightBbox, {
54
+ normalizeOrigin: true,
55
+ flipY: false,
56
+ scale: 1
57
+ })
58
+
59
+ const c = board.components![0]
60
+ expect(c.left + c.width).toBeLessThanOrEqual(board.width!)
61
+ expect(c.top + c.height).toBeLessThanOrEqual(board.height!)
62
+ // board 는 컴포넌트 (500x400) + padding 2개 = 580 / 480 이상이어야 함
63
+ expect(board.width!).toBeGreaterThanOrEqual(500)
64
+ expect(board.height!).toBeGreaterThanOrEqual(400)
65
+ })
66
+
67
+ test('flipY=true 일 때 모든 컴포넌트 top 이 0 이상 (root 위쪽 spill 방지)', () => {
68
+ const drafts = [
69
+ draft(entity('a', 0, 0, 100, 100)),
70
+ draft(entity('b', 50, 50, 100, 100)),
71
+ draft(entity('c', 100, 200, 100, 100))
72
+ ]
73
+ const bbox: BoundingBox = { min: { x: 0, y: 0 }, max: { x: 200, y: 300 } }
74
+ const board = assembleBoard(drafts, bbox, { normalizeOrigin: true, flipY: true, scale: 1 })
75
+
76
+ for (const c of board.components ?? []) {
77
+ expect(c.top).toBeGreaterThanOrEqual(0)
78
+ expect(c.top + c.height).toBeLessThanOrEqual(board.height!)
79
+ }
80
+ })
81
+
82
+ test('빈 drafts → board 1000x1000 fallback', () => {
83
+ const bbox: BoundingBox = { min: { x: 0, y: 0 }, max: { x: 0, y: 0 } }
84
+ const board = assembleBoard([], bbox, { normalizeOrigin: true, flipY: true, scale: 1 })
85
+ // 0 + padding 80 < 1000 fallback
86
+ expect(board.width).toBe(1000)
87
+ expect(board.height).toBe(1000)
88
+ expect(board.components).toEqual([])
89
+ })
90
+
91
+ test('padding 적용 — 컴포넌트 가장자리에 정확히 맞닿지 않고 약간의 여백', () => {
92
+ const drafts = [draft(entity('a', 0, 0, 100, 80))]
93
+ const bbox: BoundingBox = { min: { x: 0, y: 0 }, max: { x: 100, y: 80 } }
94
+ const board = assembleBoard(drafts, bbox, { normalizeOrigin: true, flipY: false, scale: 1 })
95
+
96
+ const c = board.components![0]
97
+ // 가장자리에 정확히 0 이 아니라 padding 만큼 안쪽
98
+ expect(c.left).toBeGreaterThan(0)
99
+ expect(c.top).toBeGreaterThan(0)
100
+ // 우측/하단도 padding 만큼 여유
101
+ expect(c.left + c.width).toBeLessThan(board.width!)
102
+ expect(c.top + c.height).toBeLessThan(board.height!)
103
+ })
104
+
105
+ test('normalizeOrigin=true 일 때 음수 entity 좌표도 root 안으로 끌어올림', () => {
106
+ // entity 가 음수 좌표 (e.g. CAD 의 임의 원점) — bbox 가 정확하면 normalizeOrigin 으로
107
+ // 0 이상으로 올려야 함.
108
+ const drafts = [draft(entity('a', -100, -50, 100, 100))]
109
+ const bbox: BoundingBox = { min: { x: -100, y: -50 }, max: { x: 0, y: 50 } }
110
+ const board = assembleBoard(drafts, bbox, { normalizeOrigin: true, flipY: false, scale: 1 })
111
+
112
+ const c = board.components![0]
113
+ expect(c.left).toBeGreaterThanOrEqual(0)
114
+ expect(c.top).toBeGreaterThanOrEqual(0)
115
+ })
116
+
117
+ test('scale 적용 — 모든 좌표/크기가 비율대로', () => {
118
+ const drafts = [draft(entity('a', 10, 20, 100, 80))]
119
+ const bbox: BoundingBox = { min: { x: 0, y: 0 }, max: { x: 200, y: 200 } }
120
+ const board = assembleBoard(drafts, bbox, {
121
+ normalizeOrigin: true,
122
+ flipY: false,
123
+ scale: 0.5
124
+ })
125
+
126
+ const c = board.components![0]
127
+ // scale=0.5 적용
128
+ expect(c.width).toBe(50)
129
+ expect(c.height).toBe(40)
130
+ // padding 은 scale 무관 (40 unit)
131
+ expect(c.left).toBe(10 * 0.5 + 40)
132
+ })
133
+ })
@@ -2,9 +2,9 @@
2
2
  * Stage 3 — ComponentDraft → Board Model JSON.
3
3
  * 좌표/단위 변환 + 3D Preset 적용.
4
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'
5
+ import { find3DPreset } from '../registry/index'
6
+ import type { BoundingBox } from '../types/index'
7
+ import type { ComponentCategory, ComponentDraft } from '../types/registry'
8
8
 
9
9
  export interface BoardModel {
10
10
  width?: number
@@ -58,6 +58,18 @@ export interface AssembleOptions {
58
58
  importMeta?: BoardModel['importMeta']
59
59
  }
60
60
 
61
+ /**
62
+ * 보드 외곽 padding (양쪽). 컴포넌트가 root 가장자리에 정확히 맞닿지 않도록 살짝 여백.
63
+ * 너무 크면 빈 공간 낭비, 너무 작으면 시각적으로 갑갑함 → 40 unit.
64
+ */
65
+ const BOARD_PADDING = 40
66
+
67
+ /**
68
+ * 빈 보드 / 너무 작은 컨텐츠일 때 fallback 크기.
69
+ * 최소 1000x1000 — things-scene 의 RootContainer 기본 크기에 맞춤.
70
+ */
71
+ const MIN_BOARD_SIZE = 1000
72
+
61
73
  export function assembleBoard(
62
74
  drafts: ComponentDraft[],
63
75
  bbox: BoundingBox,
@@ -67,16 +79,69 @@ export function assembleBoard(
67
79
  const offsetX = options.normalizeOrigin ? -bbox.min.x : 0
68
80
  const offsetY = options.normalizeOrigin ? -bbox.min.y : 0
69
81
 
70
- const boardWidth = (bbox.max.x - bbox.min.x) * scale
71
- const boardHeight = (bbox.max.y - bbox.min.y) * scale
82
+ // 1단계: bbox 기반 가설 board 크기. 그러나 entity bbox 와 position+size 가 어긋나는 경우
83
+ // 컴포넌트가 root 외곽으로 spill 할 수 있어 2단계에서 컴포넌트 실제 edge 로 보정한다.
84
+ let boardWidth = (bbox.max.x - bbox.min.x) * scale
85
+ let boardHeight = (bbox.max.y - bbox.min.y) * scale
72
86
 
73
- const components: BoardComponent[] = drafts.map(d => {
87
+ // 1.5단계: 컴포넌트 layout 1차 (flipY 미적용 — boardHeight 확정 전이라 미루기).
88
+ type Layout = {
89
+ d: ComponentDraft
90
+ src: ComponentDraft['source']
91
+ left: number
92
+ rawTop: number
93
+ width: number
94
+ height: number
95
+ }
96
+ const layouts: Layout[] = drafts.map(d => {
74
97
  const src = d.source
75
98
  const left = src ? (src.position.x + offsetX) * scale : 0
76
99
  const rawTop = src ? (src.position.y + offsetY) * scale : 0
77
- const top = options.flipY ? boardHeight - rawTop - (src?.size?.height ?? 0) * scale : rawTop
78
100
  const width = (src?.size?.width ?? 1) * scale
79
101
  const height = (src?.size?.height ?? 1) * scale
102
+ return { d, src, left, rawTop, width, height }
103
+ })
104
+
105
+ // 2단계: 컴포넌트 실제 edge 로 board 크기 보정. bbox 보다 컴포넌트 right/bottom 이
106
+ // 크면 그것 기준. 모든 컴포넌트 left/top 도 음수 안 되도록 0 이상 보장 (있으면 offset 보정).
107
+ let computedRight = 0
108
+ let computedBottom = 0
109
+ let computedMinLeft = 0
110
+ let computedMinTop = 0
111
+ for (const l of layouts) {
112
+ if (l.left < computedMinLeft) computedMinLeft = l.left
113
+ if (l.rawTop < computedMinTop) computedMinTop = l.rawTop
114
+ if (l.left + l.width > computedRight) computedRight = l.left + l.width
115
+ if (l.rawTop + l.height > computedBottom) computedBottom = l.rawTop + l.height
116
+ }
117
+ // 음수 left/top 이 있으면 추가 offset 으로 0 으로 끌어올림.
118
+ const extraOffsetX = computedMinLeft < 0 ? -computedMinLeft : 0
119
+ const extraOffsetY = computedMinTop < 0 ? -computedMinTop : 0
120
+ if (extraOffsetX || extraOffsetY) {
121
+ for (const l of layouts) {
122
+ l.left += extraOffsetX
123
+ l.rawTop += extraOffsetY
124
+ }
125
+ computedRight += extraOffsetX
126
+ computedBottom += extraOffsetY
127
+ }
128
+
129
+ // 최종 board 크기 — bbox 와 컴포넌트 실제 max edge 중 큰 값 + padding.
130
+ // 컴포넌트 없으면 padding 의미 없고, fallback 으로 MIN_BOARD_SIZE.
131
+ boardWidth = Math.max(boardWidth, computedRight)
132
+ boardHeight = Math.max(boardHeight, computedBottom)
133
+ if (layouts.length > 0) {
134
+ boardWidth += BOARD_PADDING * 2
135
+ boardHeight += BOARD_PADDING * 2
136
+ }
137
+ boardWidth = Math.max(boardWidth, MIN_BOARD_SIZE)
138
+ boardHeight = Math.max(boardHeight, MIN_BOARD_SIZE)
139
+
140
+ // 3단계: padding 만큼 모든 컴포넌트 안쪽으로 평행이동 + flipY 적용.
141
+ const components: BoardComponent[] = layouts.map(({ d, src, left, rawTop, width, height }) => {
142
+ const finalLeft = left + BOARD_PADDING
143
+ const finalRawTop = rawTop + BOARD_PADDING
144
+ const finalTop = options.flipY ? boardHeight - finalRawTop - height : finalRawTop
80
145
  const rotation = src?.rotation
81
146
 
82
147
  const preset = find3DPreset(d.type, d.category, options.scopes)
@@ -86,8 +151,8 @@ export function assembleBoard(
86
151
  category: d.category,
87
152
  type: d.type,
88
153
  id: d.id,
89
- left,
90
- top,
154
+ left: finalLeft,
155
+ top: finalTop,
91
156
  width,
92
157
  height,
93
158
  ...(rotation !== undefined && { rotation: options.flipY ? -rotation : rotation }),
@@ -97,8 +162,8 @@ export function assembleBoard(
97
162
  })
98
163
 
99
164
  return {
100
- width: boardWidth || 1000,
101
- height: boardHeight || 1000,
165
+ width: boardWidth,
166
+ height: boardHeight,
102
167
  fillStyle: options.backgroundColor ?? '#ffffff',
103
168
  components,
104
169
  importMeta: options.importMeta
@@ -2,8 +2,8 @@
2
2
  * Stage 4 — 보드 모델에 데이터 바인딩 자동 설정.
3
3
  * 컴포넌트 type 에 등록된 SubscriptionRule 을 찾아 subscriptionTag 등 부여.
4
4
  */
5
- import { findSubscriptionRule } from '../registry/index.js'
6
- import type { BoardComponent, BoardModel } from './stage3-board.js'
5
+ import { findSubscriptionRule } from '../registry/index'
6
+ import type { BoardComponent, BoardModel } from './stage3-board'
7
7
 
8
8
  export interface BindingOptions {
9
9
  scopes?: string[]
@@ -4,7 +4,7 @@
4
4
  *
5
5
  * 백엔드 단일 프로세스 안에서 in-memory 로 유지된다. 모듈 부트스트랩 시 등록.
6
6
  */
7
- import type { ComponentCategory, ImportRule, SubscriptionRule, ThreeDPreset } from '../types/registry.js'
7
+ import type { ComponentCategory, ImportRule, SubscriptionRule, ThreeDPreset } from '../types/registry'
8
8
 
9
9
  /**
10
10
  * 코어가 제공하는 카테고리별 기본 3D Preset.
@@ -63,6 +63,12 @@ export interface EntityAttribs {
63
63
  text?: string
64
64
  /** 도면 모양 종류 (rect/circle/polyline 등) */
65
65
  shape?: 'rect' | 'circle' | 'polyline' | 'polygon'
66
+ /**
67
+ * 카테고리 힌트 — AI 분류기 / 매처가 구체 type 까지는 못 정해도 카테고리만 줄 때 사용.
68
+ * Stage2 매핑이 ImportRule 매치에 실패하면 이 값을 보고 카테고리별 generic-* placeholder
69
+ * 로 폴백한다 (`MappingOptions.categoryFallback`).
70
+ */
71
+ categoryHint?: import('./registry').ComponentCategory
66
72
  /** 임의 추가 속성 */
67
73
  [k: string]: any
68
74
  }
@@ -75,6 +81,10 @@ export interface EntityRelation {
75
81
 
76
82
  /**
77
83
  * Adapter 결과 — 정규화된 entity 목록 + 메타데이터.
84
+ *
85
+ * 단일 변형이 default. AI/VLM 어댑터는 추가로 `variants` (다중 시안) 를 줄 수 있음 —
86
+ * 같은 도면을 다르게 해석한 후보들. 사용자가 review 단계에서 선택. variants[0] 와
87
+ * 최상위 entities 는 동일 (default 시안).
78
88
  */
79
89
  export interface ParseResult {
80
90
  entities: UniversalEntity[]
@@ -82,6 +92,32 @@ export interface ParseResult {
82
92
  bbox: BoundingBox
83
93
  /** 도면 단위 (mm/m/inch 등). 명시되지 않으면 'unknown' */
84
94
  units?: 'mm' | 'cm' | 'm' | 'inch' | 'unknown'
95
+ /**
96
+ * 다중 시안 (AI 가 같은 도면을 다른 관점으로 해석). 미지정 시 단일 변형 (default 만).
97
+ * 각 시안은 자기 entities + 자기 subjectName/Description + 자기 importStrategy.
98
+ */
99
+ variants?: ParseVariant[]
100
+ }
101
+
102
+ /**
103
+ * AI 가 제공하는 다중 해석 시안 — 같은 도면, 다른 관점/분류 전략.
104
+ * 예: "장비 중심" / "운송 중심" / "영역 중심" — 또는 "보수적" / "균형" / "공격적".
105
+ */
106
+ export interface ParseVariant {
107
+ /** URL 친화적 short id — materialize 시 사용. */
108
+ id: string
109
+ /** 사용자에게 보여줄 한글 라벨. 예: "장비 중심", "운송 중심". */
110
+ label: string
111
+ /** 짧은 한 줄 설명 (선택) — 이 시안이 뭘 강조하는가. */
112
+ description?: string
113
+ entities: UniversalEntity[]
114
+ bbox?: BoundingBox
115
+ /** 도면 정체성 — 짧은 제목 (보드 name 추천 기반). */
116
+ subjectName?: string
117
+ /** 도면 정체성 — 1-2 문장 설명 (보드 description 추천 기반). */
118
+ subjectDescription?: string
119
+ /** AI 의 의도 narrative — 왜 이 시안을 이렇게 해석했나. review 카드 표시. */
120
+ importStrategy?: string
85
121
  }
86
122
 
87
123
  export interface AdapterMetadata {