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

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 +17 -8
  14. package/dist-server/service/converters/generic-to.js +44 -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 +46 -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 +91 -0
  63. package/server/service/converters/generic-to.ts +66 -49
  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 +35 -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
@@ -1,7 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.applyDataBinding = exports.assembleBoard = exports.applyMapping = void 0;
3
+ exports.applyDataBinding = exports.assembleBoard = exports.applyMapping = exports.DEFAULT_ADAPTERS = void 0;
4
4
  exports.importBoard = importBoard;
5
+ exports.importBoardVariants = importBoardVariants;
5
6
  /**
6
7
  * High-level Pipeline — 도면 한 장 → 운영 가능한 Board Model.
7
8
  *
@@ -13,46 +14,114 @@ exports.importBoard = importBoard;
13
14
  * const result = await importBoard({ file: buf, filename: 'plant.dxf', scopes: ['fmsim'] })
14
15
  * ```
15
16
  */
16
- const dxf_adapter_js_1 = require("../adapters/dxf-adapter.js");
17
- const stage2_mapping_js_1 = require("./stage2-mapping.js");
18
- const stage3_board_js_1 = require("./stage3-board.js");
19
- const stage4_binding_js_1 = require("./stage4-binding.js");
20
- const DEFAULT_ADAPTERS = [new dxf_adapter_js_1.DxfAdapter()];
17
+ const dxf_adapter_1 = require("../adapters/dxf-adapter");
18
+ const image_adapter_1 = require("../adapters/image-adapter");
19
+ const stage2_mapping_1 = require("./stage2-mapping");
20
+ const stage3_board_1 = require("./stage3-board");
21
+ const stage4_binding_1 = require("./stage4-binding");
22
+ // Adapter detection 순서가 중요: 정밀 형식 (DXF) 먼저, 폴백 / 비정형 (Image) 나중.
23
+ // detect() 가 filename 확장자 우선이라 충돌은 거의 없지만, 시그니처 모호한 경우 (예:
24
+ // 확장자 없는 binary stream) 도 정밀 형식이 먼저 잡히는 게 안전.
25
+ //
26
+ // import-tools.ts 의 listImportAdapters builder 가 본 배열을 inspect — 외부 export.
27
+ exports.DEFAULT_ADAPTERS = [new dxf_adapter_1.DxfAdapter(), new image_adapter_1.ImageAdapter()];
21
28
  async function importBoard(opts) {
29
+ // 단일 변형 — multi-variant adapter 라도 첫 시안만 반환. back-compat.
30
+ const result = await importBoardVariants(opts);
31
+ const first = result.variants[0];
32
+ if (!first) {
33
+ throw new Error('Import produced no variants.');
34
+ }
35
+ return {
36
+ boardModel: first.boardModel,
37
+ mapping: result.mappings[0],
38
+ parse: result.parse
39
+ };
40
+ }
41
+ /**
42
+ * 다중 시안 import — parse 1번 (비싼 VLM/DXF), 시안별 mapping + assemble.
43
+ *
44
+ * AI multi-variant adapter (ImageAdapter) 는 parse.variants 에 N 시안 반환 → 각각 mapping +
45
+ * assemble. Single-variant adapter (DxfAdapter) 는 parse.variants 미존재 → 단일 시안 1 boardModel.
46
+ */
47
+ async function importBoardVariants(opts) {
22
48
  const adapter = opts.adapter ?? (await pickAdapter(opts.file, opts.filename));
23
49
  if (!adapter)
24
50
  throw new Error('지원되지 않는 도면 형식이거나 어댑터를 식별할 수 없습니다.');
25
51
  const parsed = await adapter.parse(opts.file, opts.parseOptions);
26
- const mapping = (0, stage2_mapping_js_1.applyMapping)(parsed, { scopes: opts.scopes });
52
+ const importMeta = {
53
+ format: adapter.format,
54
+ sourceBbox: parsed.bbox,
55
+ units: parsed.units,
56
+ importedAt: new Date().toISOString()
57
+ };
27
58
  const assembleOptions = {
28
59
  scopes: opts.scopes,
29
60
  normalizeOrigin: opts.normalizeOrigin ?? true,
30
61
  flipY: opts.flipY ?? true,
31
62
  scale: opts.scale ?? 1,
32
- importMeta: {
33
- format: adapter.format,
34
- sourceBbox: parsed.bbox,
35
- units: parsed.units,
36
- importedAt: new Date().toISOString()
37
- }
63
+ importMeta
38
64
  };
39
- let boardModel = (0, stage3_board_js_1.assembleBoard)(mapping.components, parsed.bbox, assembleOptions);
65
+ // multi-variant adapter — parse.variants 가 존재
66
+ if (Array.isArray(parsed.variants) && parsed.variants.length > 0) {
67
+ const variants = [];
68
+ const mappings = [];
69
+ for (const v of parsed.variants) {
70
+ // 각 시안의 entities 로 mapping (시안별로 다름)
71
+ const variantParsed = {
72
+ ...parsed,
73
+ entities: v.entities,
74
+ bbox: v.bbox ?? parsed.bbox
75
+ };
76
+ const mapping = (0, stage2_mapping_1.applyMapping)(variantParsed, { scopes: opts.scopes });
77
+ mappings.push(mapping);
78
+ let boardModel = (0, stage3_board_1.assembleBoard)(mapping.components, variantParsed.bbox, assembleOptions);
79
+ if (opts.applyBinding !== false) {
80
+ boardModel = (0, stage4_binding_1.applyDataBinding)(boardModel, { scopes: opts.scopes });
81
+ }
82
+ variants.push({
83
+ id: v.id,
84
+ label: v.label,
85
+ description: v.description,
86
+ subjectName: v.subjectName,
87
+ subjectDescription: v.subjectDescription,
88
+ importStrategy: v.importStrategy,
89
+ boardModel,
90
+ stats: mapping.stats
91
+ });
92
+ }
93
+ return { variants, mappings, parse: parsed };
94
+ }
95
+ // single-variant adapter (DXF 등) — 단일 boardModel
96
+ const mapping = (0, stage2_mapping_1.applyMapping)(parsed, { scopes: opts.scopes });
97
+ let boardModel = (0, stage3_board_1.assembleBoard)(mapping.components, parsed.bbox, assembleOptions);
40
98
  if (opts.applyBinding !== false) {
41
- boardModel = (0, stage4_binding_js_1.applyDataBinding)(boardModel, { scopes: opts.scopes });
99
+ boardModel = (0, stage4_binding_1.applyDataBinding)(boardModel, { scopes: opts.scopes });
42
100
  }
43
- return { boardModel, mapping, parse: parsed };
101
+ return {
102
+ variants: [
103
+ {
104
+ id: 'default',
105
+ label: 'default',
106
+ boardModel,
107
+ stats: mapping.stats
108
+ }
109
+ ],
110
+ mappings: [mapping],
111
+ parse: parsed
112
+ };
44
113
  }
45
114
  async function pickAdapter(file, filename) {
46
- for (const a of DEFAULT_ADAPTERS) {
115
+ for (const a of exports.DEFAULT_ADAPTERS) {
47
116
  if (await a.detect(file, filename))
48
117
  return a;
49
118
  }
50
119
  return undefined;
51
120
  }
52
- var stage2_mapping_js_2 = require("./stage2-mapping.js");
53
- Object.defineProperty(exports, "applyMapping", { enumerable: true, get: function () { return stage2_mapping_js_2.applyMapping; } });
54
- var stage3_board_js_2 = require("./stage3-board.js");
55
- Object.defineProperty(exports, "assembleBoard", { enumerable: true, get: function () { return stage3_board_js_2.assembleBoard; } });
56
- var stage4_binding_js_2 = require("./stage4-binding.js");
57
- Object.defineProperty(exports, "applyDataBinding", { enumerable: true, get: function () { return stage4_binding_js_2.applyDataBinding; } });
121
+ var stage2_mapping_2 = require("./stage2-mapping");
122
+ Object.defineProperty(exports, "applyMapping", { enumerable: true, get: function () { return stage2_mapping_2.applyMapping; } });
123
+ var stage3_board_2 = require("./stage3-board");
124
+ Object.defineProperty(exports, "assembleBoard", { enumerable: true, get: function () { return stage3_board_2.assembleBoard; } });
125
+ var stage4_binding_2 = require("./stage4-binding");
126
+ Object.defineProperty(exports, "applyDataBinding", { enumerable: true, get: function () { return stage4_binding_2.applyDataBinding; } });
58
127
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../server/service/pipeline/index.ts"],"names":[],"mappings":";;;AA6CA,kCA0BC;AAvED;;;;;;;;;;GAUG;AACH,+DAAuD;AAGvD,2DAAkD;AAClD,uDAAwF;AACxF,2DAAsD;AA2BtD,MAAM,gBAAgB,GAAoB,CAAC,IAAI,2BAAU,EAAE,CAAC,CAAA;AAErD,KAAK,UAAU,WAAW,CAAC,IAAmB;IACnD,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,IAAI,CAAC,MAAM,WAAW,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAA;IAC7E,IAAI,CAAC,OAAO;QAAE,MAAM,IAAI,KAAK,CAAC,mCAAmC,CAAC,CAAA;IAElE,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,YAAY,CAAC,CAAA;IAChE,MAAM,OAAO,GAAG,IAAA,gCAAY,EAAC,MAAM,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC,CAAA;IAE7D,MAAM,eAAe,GAAoB;QACvC,MAAM,EAAE,IAAI,CAAC,MAAM;QACnB,eAAe,EAAE,IAAI,CAAC,eAAe,IAAI,IAAI;QAC7C,KAAK,EAAE,IAAI,CAAC,KAAK,IAAI,IAAI;QACzB,KAAK,EAAE,IAAI,CAAC,KAAK,IAAI,CAAC;QACtB,UAAU,EAAE;YACV,MAAM,EAAE,OAAO,CAAC,MAAM;YACtB,UAAU,EAAE,MAAM,CAAC,IAAI;YACvB,KAAK,EAAE,MAAM,CAAC,KAAK;YACnB,UAAU,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;SACrC;KACF,CAAA;IACD,IAAI,UAAU,GAAG,IAAA,+BAAa,EAAC,OAAO,CAAC,UAAU,EAAE,MAAM,CAAC,IAAI,EAAE,eAAe,CAAC,CAAA;IAEhF,IAAI,IAAI,CAAC,YAAY,KAAK,KAAK,EAAE,CAAC;QAChC,UAAU,GAAG,IAAA,oCAAgB,EAAC,UAAU,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC,CAAA;IACpE,CAAC;IAED,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,CAAA;AAC/C,CAAC;AAED,KAAK,UAAU,WAAW,CACxB,IAAkB,EAClB,QAAiB;IAEjB,KAAK,MAAM,CAAC,IAAI,gBAAgB,EAAE,CAAC;QACjC,IAAI,MAAM,CAAC,CAAC,MAAM,CAAC,IAAI,EAAE,QAAQ,CAAC;YAAE,OAAO,CAAC,CAAA;IAC9C,CAAC;IACD,OAAO,SAAS,CAAA;AAClB,CAAC;AAED,yDAAkD;AAAzC,iHAAA,YAAY,OAAA;AACrB,qDAAiD;AAAxC,gHAAA,aAAa,OAAA;AACtB,yDAAsD;AAA7C,qHAAA,gBAAgB,OAAA","sourcesContent":["/**\n * High-level Pipeline — 도면 한 장 → 운영 가능한 Board Model.\n *\n * 백엔드에서 호출:\n * ```ts\n * import { importBoard } from '@things-factory/board-import'\n *\n * const buf = await fs.promises.readFile(path)\n * const result = await importBoard({ file: buf, filename: 'plant.dxf', scopes: ['fmsim'] })\n * ```\n */\nimport { DxfAdapter } from '../adapters/dxf-adapter.js'\nimport type { AdapterInput, FormatAdapter, ParseOptions, ParseResult } from '../types/index.js'\nimport type { MappingResult } from '../types/registry.js'\nimport { applyMapping } from './stage2-mapping.js'\nimport { assembleBoard, type AssembleOptions, type BoardModel } from './stage3-board.js'\nimport { applyDataBinding } from './stage4-binding.js'\n\nexport interface ImportOptions {\n file: AdapterInput\n filename?: string\n /** 등록된 매핑 규칙의 어느 scope 를 사용할지 */\n scopes?: string[]\n /** 어댑터 옵션 (excludeLayers 등) */\n parseOptions?: ParseOptions\n /** 좌표 normalize (좌상단 0,0) */\n normalizeOrigin?: boolean\n /** Y 축 뒤집기 (CAD Y-up → scene Y-down) */\n flipY?: boolean\n /** scene unit 변환 배율 */\n scale?: number\n /** 데이터 바인딩 stage 적용 여부 */\n applyBinding?: boolean\n /** 사용자 정의 어댑터 (있으면 디폴트 검색 대신 사용) */\n adapter?: FormatAdapter\n}\n\nexport interface ImportResult {\n boardModel: BoardModel\n mapping: MappingResult\n parse: ParseResult\n}\n\nconst DEFAULT_ADAPTERS: FormatAdapter[] = [new DxfAdapter()]\n\nexport async function importBoard(opts: ImportOptions): Promise<ImportResult> {\n const adapter = opts.adapter ?? (await pickAdapter(opts.file, opts.filename))\n if (!adapter) throw new Error('지원되지 않는 도면 형식이거나 어댑터를 식별할 수 없습니다.')\n\n const parsed = await adapter.parse(opts.file, opts.parseOptions)\n const mapping = applyMapping(parsed, { scopes: opts.scopes })\n\n const assembleOptions: AssembleOptions = {\n scopes: opts.scopes,\n normalizeOrigin: opts.normalizeOrigin ?? true,\n flipY: opts.flipY ?? true,\n scale: opts.scale ?? 1,\n importMeta: {\n format: adapter.format,\n sourceBbox: parsed.bbox,\n units: parsed.units,\n importedAt: new Date().toISOString()\n }\n }\n let boardModel = assembleBoard(mapping.components, parsed.bbox, assembleOptions)\n\n if (opts.applyBinding !== false) {\n boardModel = applyDataBinding(boardModel, { scopes: opts.scopes })\n }\n\n return { boardModel, mapping, parse: parsed }\n}\n\nasync function pickAdapter(\n file: AdapterInput,\n filename?: string\n): Promise<FormatAdapter | undefined> {\n for (const a of DEFAULT_ADAPTERS) {\n if (await a.detect(file, filename)) return a\n }\n return undefined\n}\n\nexport { applyMapping } from './stage2-mapping.js'\nexport { assembleBoard } from './stage3-board.js'\nexport { applyDataBinding } from './stage4-binding.js'\nexport type { BoardModel, BoardComponent, AssembleOptions } from './stage3-board.js'\nexport type { MappingOptions } from './stage2-mapping.js'\nexport type { BindingOptions } from './stage4-binding.js'\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../server/service/pipeline/index.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * High-level Pipeline — 도면 한 장 → 운영 가능한 Board Model.\n *\n * 백엔드에서 호출:\n * ```ts\n * import { importBoard } from '@things-factory/board-import'\n *\n * const buf = await fs.promises.readFile(path)\n * const result = await importBoard({ file: buf, filename: 'plant.dxf', scopes: ['fmsim'] })\n * ```\n */\nimport { DxfAdapter } from '../adapters/dxf-adapter'\nimport { ImageAdapter } from '../adapters/image-adapter'\nimport type { AdapterInput, BoundingBox, FormatAdapter, ParseOptions, ParseResult } from '../types/index'\nimport type { MappingResult } from '../types/registry'\nimport { applyMapping } from './stage2-mapping'\nimport { assembleBoard, type AssembleOptions, type BoardModel } from './stage3-board'\nimport { applyDataBinding } from './stage4-binding'\n\nexport interface ImportOptions {\n file: AdapterInput\n filename?: string\n /** 등록된 매핑 규칙의 어느 scope 를 사용할지 */\n scopes?: string[]\n /** 어댑터 옵션 (excludeLayers 등) */\n parseOptions?: ParseOptions\n /** 좌표 normalize (좌상단 0,0) */\n normalizeOrigin?: boolean\n /** Y 축 뒤집기 (CAD Y-up → scene Y-down) */\n flipY?: boolean\n /** scene unit 변환 배율 */\n scale?: number\n /** 데이터 바인딩 stage 적용 여부 */\n applyBinding?: boolean\n /** 사용자 정의 어댑터 (있으면 디폴트 검색 대신 사용) */\n adapter?: FormatAdapter\n}\n\nexport interface ImportResult {\n boardModel: BoardModel\n mapping: MappingResult\n parse: ParseResult\n}\n\n/**\n * 변환 시안 (variant) — AI 가 같은 도면을 다른 관점으로 해석한 결과. 사용자가 review 단계에서\n * 시각 비교 후 선택. parse 가 multi-variant 응답하면 (현재 ImageAdapter), 각 variant 마다\n * 독립 mapping + assemble. DXF 같은 single-variant adapter 는 1 boardModel 만.\n */\nexport interface VariantResult {\n id: string\n label: string\n description?: string\n /** 보드 정체성 — name 자동 추천 기반. */\n subjectName?: string\n /** 보드 정체성 — description 자동 추천 기반. */\n subjectDescription?: string\n /** AI 의 의도 narrative — review 카드 표시 (transient). */\n importStrategy?: string\n boardModel: BoardModel\n /** mapping 통계 (시안별 — entities 가 다르므로 stats 도 다름). */\n stats: MappingResult['stats']\n}\n\nexport interface ImportVariantsResult {\n variants: VariantResult[]\n /** 각 시안의 mapping 결과 묶음. variants[i] 와 같은 인덱스. */\n mappings: MappingResult[]\n parse: ParseResult\n}\n\n// Adapter detection 순서가 중요: 정밀 형식 (DXF) 먼저, 폴백 / 비정형 (Image) 나중.\n// detect() 가 filename 확장자 우선이라 충돌은 거의 없지만, 시그니처 모호한 경우 (예:\n// 확장자 없는 binary stream) 도 정밀 형식이 먼저 잡히는 게 안전.\n//\n// import-tools.ts 의 listImportAdapters builder 가 본 배열을 inspect — 외부 export.\nexport const DEFAULT_ADAPTERS: FormatAdapter[] = [new DxfAdapter(), new ImageAdapter()]\n\nexport async function importBoard(opts: ImportOptions): Promise<ImportResult> {\n // 단일 변형 — multi-variant adapter 라도 첫 시안만 반환. back-compat.\n const result = await importBoardVariants(opts)\n const first = result.variants[0]\n if (!first) {\n throw new Error('Import produced no variants.')\n }\n return {\n boardModel: first.boardModel,\n mapping: result.mappings[0],\n parse: result.parse\n }\n}\n\n/**\n * 다중 시안 import — parse 1번 (비싼 VLM/DXF), 시안별 mapping + assemble.\n *\n * AI multi-variant adapter (ImageAdapter) 는 parse.variants 에 N 시안 반환 → 각각 mapping +\n * assemble. Single-variant adapter (DxfAdapter) 는 parse.variants 미존재 → 단일 시안 1 boardModel.\n */\nexport async function importBoardVariants(opts: ImportOptions): Promise<ImportVariantsResult> {\n const adapter = opts.adapter ?? (await pickAdapter(opts.file, opts.filename))\n if (!adapter) throw new Error('지원되지 않는 도면 형식이거나 어댑터를 식별할 수 없습니다.')\n\n const parsed = await adapter.parse(opts.file, opts.parseOptions)\n const importMeta = {\n format: adapter.format,\n sourceBbox: parsed.bbox,\n units: parsed.units,\n importedAt: new Date().toISOString()\n }\n const assembleOptions: AssembleOptions = {\n scopes: opts.scopes,\n normalizeOrigin: opts.normalizeOrigin ?? true,\n flipY: opts.flipY ?? true,\n scale: opts.scale ?? 1,\n importMeta\n }\n\n // multi-variant adapter — parse.variants 가 존재\n if (Array.isArray(parsed.variants) && parsed.variants.length > 0) {\n const variants: VariantResult[] = []\n const mappings: MappingResult[] = []\n for (const v of parsed.variants) {\n // 각 시안의 entities 로 mapping (시안별로 다름)\n const variantParsed: ParseResult = {\n ...parsed,\n entities: v.entities,\n bbox: v.bbox ?? parsed.bbox\n }\n const mapping = applyMapping(variantParsed, { scopes: opts.scopes })\n mappings.push(mapping)\n let boardModel = assembleBoard(mapping.components, variantParsed.bbox, assembleOptions)\n if (opts.applyBinding !== false) {\n boardModel = applyDataBinding(boardModel, { scopes: opts.scopes })\n }\n variants.push({\n id: v.id,\n label: v.label,\n description: v.description,\n subjectName: v.subjectName,\n subjectDescription: v.subjectDescription,\n importStrategy: v.importStrategy,\n boardModel,\n stats: mapping.stats\n })\n }\n return { variants, mappings, parse: parsed }\n }\n\n // single-variant adapter (DXF 등) — 단일 boardModel\n const mapping = applyMapping(parsed, { scopes: opts.scopes })\n let boardModel = assembleBoard(mapping.components, parsed.bbox, assembleOptions)\n if (opts.applyBinding !== false) {\n boardModel = applyDataBinding(boardModel, { scopes: opts.scopes })\n }\n return {\n variants: [\n {\n id: 'default',\n label: 'default',\n boardModel,\n stats: mapping.stats\n }\n ],\n mappings: [mapping],\n parse: parsed\n }\n}\n\nasync function pickAdapter(\n file: AdapterInput,\n filename?: string\n): Promise<FormatAdapter | undefined> {\n for (const a of DEFAULT_ADAPTERS) {\n if (await a.detect(file, filename)) return a\n }\n return undefined\n}\n\nexport { applyMapping } from './stage2-mapping'\nexport { assembleBoard } from './stage3-board'\nexport { applyDataBinding } from './stage4-binding'\nexport type { BoardModel, BoardComponent, AssembleOptions } from './stage3-board'\nexport type { MappingOptions } from './stage2-mapping'\nexport type { BindingOptions } from './stage4-binding'\n"]}
@@ -1,14 +1,29 @@
1
- import type { ParseResult } from '../types/index.js';
2
- import type { MappingResult } from '../types/registry.js';
3
- /** 매핑되지 않은 entity 가 변환되는 placeholder 도메인 type. */
4
- export declare const GENERIC_OBJECT_TYPE = "GenericObject";
1
+ import type { ParseResult } from '../types/index';
2
+ import type { ComponentCategory, MappingResult } from '../types/registry';
3
+ /**
4
+ * 매핑되지 않은 entity 가 변환되는 placeholder 도메인 type.
5
+ *
6
+ * 반드시 things-scene 의 `Component.register('placeholder', Placeholder)` 에서 쓰는
7
+ * **kebab-case** 와 일치해야 한다.
8
+ */
9
+ export declare const GENERIC_OBJECT_TYPE = "placeholder";
10
+ /**
11
+ * 카테고리 힌트 → fallback placeholder type 의 코어 기본 매핑.
12
+ */
13
+ export declare const DEFAULT_CATEGORY_FALLBACK: Record<ComponentCategory, string>;
5
14
  export interface MappingOptions {
6
15
  /** 어떤 scope 의 규칙을 사용할지 (생략 시 전부) */
7
16
  scopes?: string[];
8
17
  /**
9
- * 매칭 안 된 entity 를 GenericObject 로 변환할지 여부 (default: true).
18
+ * 매칭 안 된 entity 를 placeholder 로 변환할지 여부 (default: true).
10
19
  * false 면 buffer 에서 제외되고 stats.skipped 로 카운트.
11
20
  */
12
21
  emitGeneric?: boolean;
22
+ /**
23
+ * 카테고리 힌트(`entity.attribs.categoryHint`) → fallback placeholder type override.
24
+ * `DEFAULT_CATEGORY_FALLBACK` 와 머지되어 사용. 도메인 패키지가 자기 추상 type 을
25
+ * 사용하고 싶을 때 명시 (예: `{ vehicle: 'GenericTransport' }`).
26
+ */
27
+ categoryFallback?: Partial<Record<ComponentCategory, string>>;
13
28
  }
14
29
  export declare function applyMapping(parsed: ParseResult, options?: MappingOptions): MappingResult;
@@ -1,16 +1,35 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.GENERIC_OBJECT_TYPE = void 0;
3
+ exports.DEFAULT_CATEGORY_FALLBACK = exports.GENERIC_OBJECT_TYPE = void 0;
4
4
  exports.applyMapping = applyMapping;
5
5
  /**
6
6
  * Stage 2 — Universal Entities → Component Drafts.
7
7
  * 등록된 ImportRule 들을 entity 마다 평가하여 컴포넌트 type 결정.
8
8
  */
9
- const index_js_1 = require("../registry/index.js");
10
- /** 매핑되지 않은 entity 가 변환되는 placeholder 도메인 type. */
11
- exports.GENERIC_OBJECT_TYPE = 'GenericObject';
9
+ const index_1 = require("../registry/index");
10
+ /**
11
+ * 매핑되지 않은 entity 가 변환되는 placeholder 도메인 type.
12
+ *
13
+ * 반드시 things-scene 의 `Component.register('placeholder', Placeholder)` 에서 쓰는
14
+ * **kebab-case** 와 일치해야 한다.
15
+ */
16
+ exports.GENERIC_OBJECT_TYPE = 'placeholder';
17
+ /**
18
+ * 카테고리 힌트 → fallback placeholder type 의 코어 기본 매핑.
19
+ */
20
+ exports.DEFAULT_CATEGORY_FALLBACK = {
21
+ vehicle: 'placeholder',
22
+ equipment: 'placeholder',
23
+ container: 'placeholder',
24
+ io: 'placeholder',
25
+ structure: 'placeholder',
26
+ path: 'placeholder',
27
+ region: 'placeholder',
28
+ marker: 'placeholder',
29
+ unknown: exports.GENERIC_OBJECT_TYPE
30
+ };
12
31
  function applyMapping(parsed, options = {}) {
13
- const rules = (0, index_js_1.getImportRules)(options.scopes)
32
+ const rules = (0, index_1.getImportRules)(options.scopes)
14
33
  .slice()
15
34
  .sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
16
35
  const components = [];
@@ -18,6 +37,7 @@ function applyMapping(parsed, options = {}) {
18
37
  const stats = { total: parsed.entities.length, matched: 0, generic: 0, skipped: 0 };
19
38
  const usedIds = new Map();
20
39
  const emitGeneric = options.emitGeneric !== false;
40
+ const fallbackTable = { ...exports.DEFAULT_CATEGORY_FALLBACK, ...(options.categoryFallback || {}) };
21
41
  for (const entity of parsed.entities) {
22
42
  const rule = rules.find(r => matchRule(r.match, entity));
23
43
  if (!rule) {
@@ -25,7 +45,9 @@ function applyMapping(parsed, options = {}) {
25
45
  stats.skipped++;
26
46
  continue;
27
47
  }
28
- const draft = makeGenericDraft(entity, usedIds);
48
+ const hint = entity.attribs.categoryHint || 'unknown';
49
+ const fallbackType = fallbackTable[hint] || fallbackTable.unknown;
50
+ const draft = makeGenericDraft(entity, usedIds, fallbackType, hint);
29
51
  components.push(draft);
30
52
  stats.generic++;
31
53
  continue;
@@ -57,21 +79,29 @@ function applyMapping(parsed, options = {}) {
57
79
  return { components, stats, warnings };
58
80
  }
59
81
  /**
60
- * 매칭되지 않은 entity 를 GenericObject draft 로 변환.
82
+ * 매칭되지 않은 entity 를 placeholder draft 로 변환.
61
83
  * 좌표·크기·원본 attribs 모두 보존하여 사후 변환 가능하게 한다.
84
+ *
85
+ * - `type`: 카테고리 힌트 fallback 매핑으로 결정 (기본 GenericObject).
86
+ * - `category`: 힌트 그대로 (없으면 'unknown') — 3D Preset 폴백에 사용됨.
87
+ * - state: placeholderLabel / placeholderHint 만 — 시각 표현은 컴포넌트 자체 render 가 담당.
88
+ * things-scene 의 GenericObject 의 renderPlaceholder 가 theme.fill / theme.stroke
89
+ * 를 자체 적용하므로 state.fillStyle / strokeStyle / strokeDashArray 를 두면 Shape.postrender
90
+ * 가 그것을 추가로 적용해 시각 leak (보드 전체에 stale path 가 칠해지는 회귀) 가 발생.
62
91
  */
63
- function makeGenericDraft(entity, usedIds) {
64
- const id = resolveId('auto', entity, exports.GENERIC_OBJECT_TYPE, usedIds);
92
+ function makeGenericDraft(entity, usedIds, type = exports.GENERIC_OBJECT_TYPE, category = 'unknown') {
93
+ const id = resolveId('auto', entity, type, usedIds);
94
+ const placeholderLabel = entity.attribs.name || entity.attribs.layer || entity.attribs.text || '';
95
+ const placeholderHint = entity.attribs.layer || entity.attribs.name;
96
+ const baseState = {
97
+ placeholderLabel,
98
+ ...(placeholderHint ? { placeholderHint } : {})
99
+ };
65
100
  return {
66
- category: 'unknown',
67
- type: exports.GENERIC_OBJECT_TYPE,
101
+ category,
102
+ type,
68
103
  id,
69
- state: {
70
- strokeStyle: '#888888',
71
- strokeDashArray: '4 4',
72
- fillStyle: 'rgba(128,128,128,0.15)',
73
- label: entity.attribs.name || entity.attribs.layer || entity.attribs.text || ''
74
- },
104
+ state: baseState,
75
105
  source: entity
76
106
  };
77
107
  }
@@ -139,7 +169,22 @@ function matchString(pattern, value) {
139
169
  return pattern.test(value);
140
170
  }
141
171
  // ── ID 부여 전략 ──────────────────────────────────────────────────
142
- function resolveId(strategy, entity, type, usedIds) {
172
+ /**
173
+ * 컴포넌트 ID 부여.
174
+ *
175
+ * **정책: 의미 있는 ID 만 부여한다.** 의미 없는 시퀀스 (`generic-object-1`, `STK_002`) 는
176
+ * 사용자/시스템 혼동만 야기하고 운영 가치가 없다 — 차라리 미부여 (undefined) 가 낫다.
177
+ *
178
+ * - **fromAttribute / fromAdjacentText / fromIfcGlobalId**: 명시 strategy — 해당 출처
179
+ * 사용. 없으면 undefined.
180
+ * - **auto / default**: VLM 이 도면 텍스트 라벨을 추출해 attribs.name 에 채웠으면 그것 사용.
181
+ * 없으면 undefined.
182
+ * - **custom**: 호출자 정의. 빈 결과면 undefined.
183
+ *
184
+ * 중복 ID 는 두 번째부터 undefined (이미 운영 ID 로 정확히 일치하는 게 있으니 새로 부여하면
185
+ * 잘못된 동일성 매칭 야기).
186
+ */
187
+ function resolveId(strategy, entity, _type, usedIds) {
143
188
  let id;
144
189
  if (typeof strategy === 'object' && 'custom' in strategy) {
145
190
  id = strategy.custom(entity);
@@ -157,17 +202,30 @@ function resolveId(strategy, entity, type, usedIds) {
157
202
  break;
158
203
  case 'auto':
159
204
  default:
160
- id = undefined;
205
+ // VLM 이 도면 텍스트 라벨 추출했으면 그것. 없으면 ID 미부여.
206
+ id = entity.attribs.name;
161
207
  }
162
208
  }
163
- if (!id) {
164
- const seq = (usedIds.get(type) ?? 0) + 1;
165
- usedIds.set(type, seq);
166
- id = `${type.toUpperCase()}_${String(seq).padStart(3, '0')}`;
167
- }
168
- else {
169
- usedIds.set(id, (usedIds.get(id) ?? 0) + 1);
209
+ if (id)
210
+ id = sanitizeId(id);
211
+ // 중복 회피 — 이미 사용된 ID 면 undefined (의미 없는 자동 변형 부여 X)
212
+ if (id && usedIds.has(id)) {
213
+ return undefined;
170
214
  }
215
+ if (id)
216
+ usedIds.set(id, 1);
171
217
  return id;
172
218
  }
219
+ /**
220
+ * ID 정규화 — 알파넘 + 하이픈/언더스코어 만 허용. 공백 → 하이픈, 다른 특수문자 제거.
221
+ * 빈 결과면 undefined.
222
+ */
223
+ function sanitizeId(raw) {
224
+ const cleaned = raw
225
+ .trim()
226
+ .replace(/\s+/g, '-')
227
+ .replace(/[^a-zA-Z0-9_\-]/g, '')
228
+ .slice(0, 64);
229
+ return cleaned || undefined;
230
+ }
173
231
  //# sourceMappingURL=stage2-mapping.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"stage2-mapping.js","sourceRoot":"","sources":["../../../server/service/pipeline/stage2-mapping.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Stage 2 — Universal Entities → Component Drafts.\n * 등록된 ImportRule 들을 entity 마다 평가하여 컴포넌트 type 결정.\n */\nimport { getImportRules } from '../registry/index.js'\nimport type { ParseResult, UniversalEntity } from '../types/index.js'\nimport type {\n ComponentDraft,\n IdStrategy,\n ImportMatcher,\n MappingResult,\n MappingWarning\n} from '../types/registry.js'\n\n/** 매핑되지 않은 entity 가 변환되는 placeholder 도메인 type. */\nexport const GENERIC_OBJECT_TYPE = 'GenericObject'\n\nexport interface MappingOptions {\n /** 어떤 scope 의 규칙을 사용할지 (생략 시 전부) */\n scopes?: string[]\n /**\n * 매칭 안 된 entity 를 GenericObject 로 변환할지 여부 (default: true).\n * false 면 buffer 에서 제외되고 stats.skipped 로 카운트.\n */\n emitGeneric?: boolean\n}\n\nexport function applyMapping(parsed: ParseResult, options: MappingOptions = {}): MappingResult {\n const rules = getImportRules(options.scopes)\n .slice()\n .sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0))\n\n const components: ComponentDraft[] = []\n const warnings: MappingWarning[] = []\n const stats = { total: parsed.entities.length, matched: 0, generic: 0, skipped: 0 }\n\n const usedIds = new Map<string, number>()\n const emitGeneric = options.emitGeneric !== false\n\n for (const entity of parsed.entities) {\n const rule = rules.find(r => matchRule(r.match, entity))\n\n if (!rule) {\n if (!emitGeneric) {\n stats.skipped++\n continue\n }\n const draft = makeGenericDraft(entity, usedIds)\n components.push(draft)\n stats.generic++\n continue\n }\n\n const id = resolveId(rule.build.idStrategy ?? 'auto', entity, rule.componentType, usedIds)\n let draft: ComponentDraft = {\n category: rule.category ?? 'unknown',\n type: rule.componentType,\n id,\n state: { ...(rule.build.defaultState || {}) },\n matchedBy: rule,\n source: entity\n }\n\n if (rule.build.transform) {\n try {\n draft = rule.build.transform(entity, draft)\n } catch (e) {\n warnings.push({\n level: 'warn',\n entity,\n message: `transform 실패 (${rule.componentType}): ${(e as Error).message}`\n })\n }\n }\n\n components.push(draft)\n stats.matched++\n }\n\n return { components, stats, warnings }\n}\n\n/**\n * 매칭되지 않은 entity 를 GenericObject draft 로 변환.\n * 좌표·크기·원본 attribs 모두 보존하여 사후 변환 가능하게 한다.\n */\nfunction makeGenericDraft(entity: UniversalEntity, usedIds: Map<string, number>): ComponentDraft {\n const id = resolveId('auto', entity, GENERIC_OBJECT_TYPE, usedIds)\n return {\n category: 'unknown',\n type: GENERIC_OBJECT_TYPE,\n id,\n state: {\n strokeStyle: '#888888',\n strokeDashArray: '4 4',\n fillStyle: 'rgba(128,128,128,0.15)',\n label: entity.attribs.name || entity.attribs.layer || entity.attribs.text || ''\n },\n source: entity\n }\n}\n\n// ── 매칭 로직 ──────────────────────────────────────────────────────\n\nfunction matchRule(matcher: ImportMatcher, entity: UniversalEntity): boolean {\n if (matcher.cad) {\n const m = matcher.cad\n let ok = true\n if (m.blockNamePattern && !m.blockNamePattern.test(entity.attribs.name || '')) ok = false\n if (m.layer && !matchString(m.layer, entity.attribs.layer || '')) ok = false\n if (m.entityKinds && !m.entityKinds.includes(entity.kind)) ok = false\n if (m.textAdjacent) {\n const adjacentText = (entity.neighbors || [])\n .filter(r => r.relation === 'labeled-by')\n .map(r => r.uid)\n if (!adjacentText.some(t => m.textAdjacent!.test(t))) ok = false\n }\n if (ok) return true\n }\n\n if (matcher.ifc) {\n const m = matcher.ifc\n let ok = true\n if (m.ifcClass && !matchString(m.ifcClass, entity.attribs.semanticType || '')) ok = false\n if (ok) return true\n }\n\n if (matcher.geometry) {\n const g = matcher.geometry\n let ok = true\n if (g.shape && entity.attribs.shape !== g.shape) ok = false\n if (g.sizeRange && entity.size) {\n const area = entity.size.width * entity.size.height\n if (g.sizeRange.minArea !== undefined && area < g.sizeRange.minArea) ok = false\n if (g.sizeRange.maxArea !== undefined && area > g.sizeRange.maxArea) ok = false\n }\n if (g.aspectRatio && entity.size) {\n const ratio = entity.size.height === 0 ? 1 : entity.size.width / entity.size.height\n if (g.aspectRatio.min !== undefined && ratio < g.aspectRatio.min) ok = false\n if (g.aspectRatio.max !== undefined && ratio > g.aspectRatio.max) ok = false\n }\n if (ok) return true\n }\n\n if (matcher.image && matcher.image.labels.length > 0) {\n const label = entity.attribs.vlmLabel\n if (label && matcher.image.labels.includes(label)) return true\n }\n\n return false\n}\n\nfunction matchString(pattern: string | RegExp, value: string): boolean {\n if (typeof pattern === 'string') return pattern === value\n return pattern.test(value)\n}\n\n// ── ID 부여 전략 ──────────────────────────────────────────────────\n\nfunction resolveId(\n strategy: IdStrategy,\n entity: UniversalEntity,\n type: string,\n usedIds: Map<string, number>\n): string {\n let id: string | undefined\n\n if (typeof strategy === 'object' && 'custom' in strategy) {\n id = strategy.custom(entity)\n } else {\n switch (strategy) {\n case 'fromAttribute':\n id = entity.attribs.name\n break\n case 'fromAdjacentText':\n id = (entity.neighbors || []).find(r => r.relation === 'labeled-by')?.uid\n break\n case 'fromIfcGlobalId':\n id = entity.attribs.ifcGlobalId\n break\n case 'auto':\n default:\n id = undefined\n }\n }\n\n if (!id) {\n const seq = (usedIds.get(type) ?? 0) + 1\n usedIds.set(type, seq)\n id = `${type.toUpperCase()}_${String(seq).padStart(3, '0')}`\n } else {\n usedIds.set(id, (usedIds.get(id) ?? 0) + 1)\n }\n return id\n}\n"]}
1
+ {"version":3,"file":"stage2-mapping.js","sourceRoot":"","sources":["../../../server/service/pipeline/stage2-mapping.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Stage 2 — Universal Entities → Component Drafts.\n * 등록된 ImportRule 들을 entity 마다 평가하여 컴포넌트 type 결정.\n */\nimport { getImportRules } from '../registry/index'\nimport type { ParseResult, UniversalEntity } from '../types/index'\nimport type {\n ComponentCategory,\n ComponentDraft,\n IdStrategy,\n ImportMatcher,\n MappingResult,\n MappingWarning\n} from '../types/registry'\n\n/**\n * 매핑되지 않은 entity 가 변환되는 placeholder 도메인 type.\n *\n * 반드시 things-scene 의 `Component.register('placeholder', Placeholder)` 에서 쓰는\n * **kebab-case** 와 일치해야 한다.\n */\nexport const GENERIC_OBJECT_TYPE = 'placeholder'\n\n/**\n * 카테고리 힌트 → fallback placeholder type 의 코어 기본 매핑.\n */\nexport const DEFAULT_CATEGORY_FALLBACK: Record<ComponentCategory, string> = {\n vehicle: 'placeholder',\n equipment: 'placeholder',\n container: 'placeholder',\n io: 'placeholder',\n structure: 'placeholder',\n path: 'placeholder',\n region: 'placeholder',\n marker: 'placeholder',\n unknown: GENERIC_OBJECT_TYPE\n}\n\nexport interface MappingOptions {\n /** 어떤 scope 의 규칙을 사용할지 (생략 시 전부) */\n scopes?: string[]\n /**\n * 매칭 안 된 entity 를 placeholder 로 변환할지 여부 (default: true).\n * false 면 buffer 에서 제외되고 stats.skipped 로 카운트.\n */\n emitGeneric?: boolean\n /**\n * 카테고리 힌트(`entity.attribs.categoryHint`) → fallback placeholder type override.\n * `DEFAULT_CATEGORY_FALLBACK` 와 머지되어 사용. 도메인 패키지가 자기 추상 type 을\n * 사용하고 싶을 때 명시 (예: `{ vehicle: 'GenericTransport' }`).\n */\n categoryFallback?: Partial<Record<ComponentCategory, string>>\n}\n\nexport function applyMapping(parsed: ParseResult, options: MappingOptions = {}): MappingResult {\n const rules = getImportRules(options.scopes)\n .slice()\n .sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0))\n\n const components: ComponentDraft[] = []\n const warnings: MappingWarning[] = []\n const stats = { total: parsed.entities.length, matched: 0, generic: 0, skipped: 0 }\n\n const usedIds = new Map<string, number>()\n const emitGeneric = options.emitGeneric !== false\n const fallbackTable = { ...DEFAULT_CATEGORY_FALLBACK, ...(options.categoryFallback || {}) }\n\n for (const entity of parsed.entities) {\n const rule = rules.find(r => matchRule(r.match, entity))\n\n if (!rule) {\n if (!emitGeneric) {\n stats.skipped++\n continue\n }\n const hint = (entity.attribs.categoryHint as ComponentCategory | undefined) || 'unknown'\n const fallbackType = fallbackTable[hint] || fallbackTable.unknown\n const draft = makeGenericDraft(entity, usedIds, fallbackType, hint)\n components.push(draft)\n stats.generic++\n continue\n }\n\n const id = resolveId(rule.build.idStrategy ?? 'auto', entity, rule.componentType, usedIds)\n let draft: ComponentDraft = {\n category: rule.category ?? 'unknown',\n type: rule.componentType,\n id,\n state: { ...(rule.build.defaultState || {}) },\n matchedBy: rule,\n source: entity\n }\n\n if (rule.build.transform) {\n try {\n draft = rule.build.transform(entity, draft)\n } catch (e) {\n warnings.push({\n level: 'warn',\n entity,\n message: `transform 실패 (${rule.componentType}): ${(e as Error).message}`\n })\n }\n }\n\n components.push(draft)\n stats.matched++\n }\n\n return { components, stats, warnings }\n}\n\n/**\n * 매칭되지 않은 entity 를 placeholder draft 로 변환.\n * 좌표·크기·원본 attribs 모두 보존하여 사후 변환 가능하게 한다.\n *\n * - `type`: 카테고리 힌트 fallback 매핑으로 결정 (기본 GenericObject).\n * - `category`: 힌트 그대로 (없으면 'unknown') — 3D Preset 폴백에 사용됨.\n * - state: placeholderLabel / placeholderHint 만 — 시각 표현은 컴포넌트 자체 render 가 담당.\n * things-scene 의 GenericObject 의 renderPlaceholder 가 theme.fill / theme.stroke\n * 를 자체 적용하므로 state.fillStyle / strokeStyle / strokeDashArray 를 두면 Shape.postrender\n * 가 그것을 추가로 적용해 시각 leak (보드 전체에 stale path 가 칠해지는 회귀) 가 발생.\n */\nfunction makeGenericDraft(\n entity: UniversalEntity,\n usedIds: Map<string, number>,\n type: string = GENERIC_OBJECT_TYPE,\n category: ComponentCategory = 'unknown'\n): ComponentDraft {\n const id = resolveId('auto', entity, type, usedIds)\n const placeholderLabel =\n entity.attribs.name || entity.attribs.layer || entity.attribs.text || ''\n const placeholderHint = entity.attribs.layer || entity.attribs.name\n\n const baseState: Record<string, any> = {\n placeholderLabel,\n ...(placeholderHint ? { placeholderHint } : {})\n }\n\n return {\n category,\n type,\n id,\n state: baseState,\n source: entity\n }\n}\n\n// ── 매칭 로직 ──────────────────────────────────────────────────────\n\nfunction matchRule(matcher: ImportMatcher, entity: UniversalEntity): boolean {\n if (matcher.cad) {\n const m = matcher.cad\n let ok = true\n if (m.blockNamePattern && !m.blockNamePattern.test(entity.attribs.name || '')) ok = false\n if (m.layer && !matchString(m.layer, entity.attribs.layer || '')) ok = false\n if (m.entityKinds && !m.entityKinds.includes(entity.kind)) ok = false\n if (m.textAdjacent) {\n const adjacentText = (entity.neighbors || [])\n .filter(r => r.relation === 'labeled-by')\n .map(r => r.uid)\n if (!adjacentText.some(t => m.textAdjacent!.test(t))) ok = false\n }\n if (ok) return true\n }\n\n if (matcher.ifc) {\n const m = matcher.ifc\n let ok = true\n if (m.ifcClass && !matchString(m.ifcClass, entity.attribs.semanticType || '')) ok = false\n if (ok) return true\n }\n\n if (matcher.geometry) {\n const g = matcher.geometry\n let ok = true\n if (g.shape && entity.attribs.shape !== g.shape) ok = false\n if (g.sizeRange && entity.size) {\n const area = entity.size.width * entity.size.height\n if (g.sizeRange.minArea !== undefined && area < g.sizeRange.minArea) ok = false\n if (g.sizeRange.maxArea !== undefined && area > g.sizeRange.maxArea) ok = false\n }\n if (g.aspectRatio && entity.size) {\n const ratio = entity.size.height === 0 ? 1 : entity.size.width / entity.size.height\n if (g.aspectRatio.min !== undefined && ratio < g.aspectRatio.min) ok = false\n if (g.aspectRatio.max !== undefined && ratio > g.aspectRatio.max) ok = false\n }\n if (ok) return true\n }\n\n if (matcher.image && matcher.image.labels.length > 0) {\n const label = entity.attribs.vlmLabel\n if (label && matcher.image.labels.includes(label)) return true\n }\n\n return false\n}\n\nfunction matchString(pattern: string | RegExp, value: string): boolean {\n if (typeof pattern === 'string') return pattern === value\n return pattern.test(value)\n}\n\n// ── ID 부여 전략 ──────────────────────────────────────────────────\n\n/**\n * 컴포넌트 ID 부여.\n *\n * **정책: 의미 있는 ID 만 부여한다.** 의미 없는 시퀀스 (`generic-object-1`, `STK_002`) 는\n * 사용자/시스템 혼동만 야기하고 운영 가치가 없다 — 차라리 미부여 (undefined) 가 낫다.\n *\n * - **fromAttribute / fromAdjacentText / fromIfcGlobalId**: 명시 strategy — 해당 출처\n * 사용. 없으면 undefined.\n * - **auto / default**: VLM 이 도면 텍스트 라벨을 추출해 attribs.name 에 채웠으면 그것 사용.\n * 없으면 undefined.\n * - **custom**: 호출자 정의. 빈 결과면 undefined.\n *\n * 중복 ID 는 두 번째부터 undefined (이미 운영 ID 로 정확히 일치하는 게 있으니 새로 부여하면\n * 잘못된 동일성 매칭 야기).\n */\nfunction resolveId(\n strategy: IdStrategy,\n entity: UniversalEntity,\n _type: string,\n usedIds: Map<string, number>\n): string | undefined {\n let id: string | undefined\n\n if (typeof strategy === 'object' && 'custom' in strategy) {\n id = strategy.custom(entity)\n } else {\n switch (strategy) {\n case 'fromAttribute':\n id = entity.attribs.name\n break\n case 'fromAdjacentText':\n id = (entity.neighbors || []).find(r => r.relation === 'labeled-by')?.uid\n break\n case 'fromIfcGlobalId':\n id = entity.attribs.ifcGlobalId\n break\n case 'auto':\n default:\n // VLM 이 도면 텍스트 라벨 추출했으면 그것. 없으면 ID 미부여.\n id = entity.attribs.name\n }\n }\n\n if (id) id = sanitizeId(id)\n\n // 중복 회피 — 이미 사용된 ID 면 undefined (의미 없는 자동 변형 부여 X)\n if (id && usedIds.has(id)) {\n return undefined\n }\n if (id) usedIds.set(id, 1)\n return id\n}\n\n/**\n * ID 정규화 — 알파넘 + 하이픈/언더스코어 만 허용. 공백 → 하이픈, 다른 특수문자 제거.\n * 빈 결과면 undefined.\n */\nfunction sanitizeId(raw: string): string | undefined {\n const cleaned = raw\n .trim()\n .replace(/\\s+/g, '-')\n .replace(/[^a-zA-Z0-9_\\-]/g, '')\n .slice(0, 64)\n return cleaned || undefined\n}\n"]}
@@ -1,5 +1,5 @@
1
- import type { BoundingBox } from '../types/index.js';
2
- import type { ComponentCategory, ComponentDraft } from '../types/registry.js';
1
+ import type { BoundingBox } from '../types/index';
2
+ import type { ComponentCategory, ComponentDraft } from '../types/registry';
3
3
  export interface BoardModel {
4
4
  width?: number;
5
5
  height?: number;
@@ -5,29 +5,84 @@ exports.assembleBoard = assembleBoard;
5
5
  * Stage 3 — ComponentDraft → Board Model JSON.
6
6
  * 좌표/단위 변환 + 3D Preset 적용.
7
7
  */
8
- const index_js_1 = require("../registry/index.js");
8
+ const index_1 = require("../registry/index");
9
+ /**
10
+ * 보드 외곽 padding (양쪽). 컴포넌트가 root 가장자리에 정확히 맞닿지 않도록 살짝 여백.
11
+ * 너무 크면 빈 공간 낭비, 너무 작으면 시각적으로 갑갑함 → 40 unit.
12
+ */
13
+ const BOARD_PADDING = 40;
14
+ /**
15
+ * 빈 보드 / 너무 작은 컨텐츠일 때 fallback 크기.
16
+ * 최소 1000x1000 — things-scene 의 RootContainer 기본 크기에 맞춤.
17
+ */
18
+ const MIN_BOARD_SIZE = 1000;
9
19
  function assembleBoard(drafts, bbox, options = {}) {
10
20
  const scale = options.scale ?? 1;
11
21
  const offsetX = options.normalizeOrigin ? -bbox.min.x : 0;
12
22
  const offsetY = options.normalizeOrigin ? -bbox.min.y : 0;
13
- const boardWidth = (bbox.max.x - bbox.min.x) * scale;
14
- const boardHeight = (bbox.max.y - bbox.min.y) * scale;
15
- const components = drafts.map(d => {
23
+ // 1단계: bbox 기반 가설 board 크기. 그러나 entity bbox 와 position+size 가 어긋나는 경우
24
+ // 컴포넌트가 root 외곽으로 spill 할 수 있어 2단계에서 컴포넌트 실제 edge 로 보정한다.
25
+ let boardWidth = (bbox.max.x - bbox.min.x) * scale;
26
+ let boardHeight = (bbox.max.y - bbox.min.y) * scale;
27
+ const layouts = drafts.map(d => {
16
28
  const src = d.source;
17
29
  const left = src ? (src.position.x + offsetX) * scale : 0;
18
30
  const rawTop = src ? (src.position.y + offsetY) * scale : 0;
19
- const top = options.flipY ? boardHeight - rawTop - (src?.size?.height ?? 0) * scale : rawTop;
20
31
  const width = (src?.size?.width ?? 1) * scale;
21
32
  const height = (src?.size?.height ?? 1) * scale;
33
+ return { d, src, left, rawTop, width, height };
34
+ });
35
+ // 2단계: 컴포넌트 실제 edge 로 board 크기 보정. bbox 보다 컴포넌트 right/bottom 이
36
+ // 크면 그것 기준. 모든 컴포넌트 left/top 도 음수 안 되도록 0 이상 보장 (있으면 offset 보정).
37
+ let computedRight = 0;
38
+ let computedBottom = 0;
39
+ let computedMinLeft = 0;
40
+ let computedMinTop = 0;
41
+ for (const l of layouts) {
42
+ if (l.left < computedMinLeft)
43
+ computedMinLeft = l.left;
44
+ if (l.rawTop < computedMinTop)
45
+ computedMinTop = l.rawTop;
46
+ if (l.left + l.width > computedRight)
47
+ computedRight = l.left + l.width;
48
+ if (l.rawTop + l.height > computedBottom)
49
+ computedBottom = l.rawTop + l.height;
50
+ }
51
+ // 음수 left/top 이 있으면 추가 offset 으로 0 으로 끌어올림.
52
+ const extraOffsetX = computedMinLeft < 0 ? -computedMinLeft : 0;
53
+ const extraOffsetY = computedMinTop < 0 ? -computedMinTop : 0;
54
+ if (extraOffsetX || extraOffsetY) {
55
+ for (const l of layouts) {
56
+ l.left += extraOffsetX;
57
+ l.rawTop += extraOffsetY;
58
+ }
59
+ computedRight += extraOffsetX;
60
+ computedBottom += extraOffsetY;
61
+ }
62
+ // 최종 board 크기 — bbox 와 컴포넌트 실제 max edge 중 큰 값 + padding.
63
+ // 컴포넌트 없으면 padding 의미 없고, fallback 으로 MIN_BOARD_SIZE.
64
+ boardWidth = Math.max(boardWidth, computedRight);
65
+ boardHeight = Math.max(boardHeight, computedBottom);
66
+ if (layouts.length > 0) {
67
+ boardWidth += BOARD_PADDING * 2;
68
+ boardHeight += BOARD_PADDING * 2;
69
+ }
70
+ boardWidth = Math.max(boardWidth, MIN_BOARD_SIZE);
71
+ boardHeight = Math.max(boardHeight, MIN_BOARD_SIZE);
72
+ // 3단계: padding 만큼 모든 컴포넌트 안쪽으로 평행이동 + flipY 적용.
73
+ const components = layouts.map(({ d, src, left, rawTop, width, height }) => {
74
+ const finalLeft = left + BOARD_PADDING;
75
+ const finalRawTop = rawTop + BOARD_PADDING;
76
+ const finalTop = options.flipY ? boardHeight - finalRawTop - height : finalRawTop;
22
77
  const rotation = src?.rotation;
23
- const preset = (0, index_js_1.find3DPreset)(d.type, d.category, options.scopes);
78
+ const preset = (0, index_1.find3DPreset)(d.type, d.category, options.scopes);
24
79
  const threeD = presetToState(preset);
25
80
  return {
26
81
  category: d.category,
27
82
  type: d.type,
28
83
  id: d.id,
29
- left,
30
- top,
84
+ left: finalLeft,
85
+ top: finalTop,
31
86
  width,
32
87
  height,
33
88
  ...(rotation !== undefined && { rotation: options.flipY ? -rotation : rotation }),
@@ -36,8 +91,8 @@ function assembleBoard(drafts, bbox, options = {}) {
36
91
  };
37
92
  });
38
93
  return {
39
- width: boardWidth || 1000,
40
- height: boardHeight || 1000,
94
+ width: boardWidth,
95
+ height: boardHeight,
41
96
  fillStyle: options.backgroundColor ?? '#ffffff',
42
97
  components,
43
98
  importMeta: options.importMeta
@@ -1 +1 @@
1
- {"version":3,"file":"stage3-board.js","sourceRoot":"","sources":["../../../server/service/pipeline/stage3-board.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Stage 3 — ComponentDraft → Board Model JSON.\n * 좌표/단위 변환 + 3D Preset 적용.\n */\nimport { find3DPreset } from '../registry/index.js'\nimport type { BoundingBox } from '../types/index.js'\nimport type { ComponentCategory, ComponentDraft } from '../types/registry.js'\n\nexport interface BoardModel {\n width?: number\n height?: number\n fillStyle?: string\n /**\n * 컴포넌트 목록. things-scene 보드 모델 호환을 위해 optional.\n * 빈 보드 / legacy 형식 / root container 가 직접 model 인 경우 누락 가능.\n * 사용처는 항상 `(model.components ?? [])` 패턴으로 접근.\n */\n components?: BoardComponent[]\n /** 메타: import 출처 */\n importMeta?: {\n format: string\n sourceBbox: BoundingBox\n units?: string\n importedAt: string\n }\n /** things-scene / 사용자 도메인이 임의 추가하는 필드 */\n [k: string]: any\n}\n\nexport interface BoardComponent {\n /** Universal category — 모든 컴포넌트가 보유. 변환·렌더 정책의 기반. */\n category: ComponentCategory\n /** 도메인 type. 'GenericObject' 면 변환 가능한 placeholder. */\n type: string\n id?: string\n left: number\n top: number\n width: number\n height: number\n rotation?: number\n /** 3D Preset 해석 결과 (있으면) */\n threeD?: Record<string, any>\n [k: string]: any\n}\n\nexport interface AssembleOptions {\n /** scope 들 — 3D preset 조회 시 사용 */\n scopes?: string[]\n /** 좌표계 정규화: true 면 entities 의 minX,minY 가 0,0 이 되도록 이동 */\n normalizeOrigin?: boolean\n /** Y 축 뒤집기: CAD (Y-up) → scene (Y-down) */\n flipY?: boolean\n /** 단위 → scene unit 변환 비율. 1 이면 그대로. */\n scale?: number\n /** 보드 배경색 */\n backgroundColor?: string\n /** importMeta 를 모델에 첨부 */\n importMeta?: BoardModel['importMeta']\n}\n\nexport function assembleBoard(\n drafts: ComponentDraft[],\n bbox: BoundingBox,\n options: AssembleOptions = {}\n): BoardModel {\n const scale = options.scale ?? 1\n const offsetX = options.normalizeOrigin ? -bbox.min.x : 0\n const offsetY = options.normalizeOrigin ? -bbox.min.y : 0\n\n const boardWidth = (bbox.max.x - bbox.min.x) * scale\n const boardHeight = (bbox.max.y - bbox.min.y) * scale\n\n const components: BoardComponent[] = drafts.map(d => {\n const src = d.source\n const left = src ? (src.position.x + offsetX) * scale : 0\n const rawTop = src ? (src.position.y + offsetY) * scale : 0\n const top = options.flipY ? boardHeight - rawTop - (src?.size?.height ?? 0) * scale : rawTop\n const width = (src?.size?.width ?? 1) * scale\n const height = (src?.size?.height ?? 1) * scale\n const rotation = src?.rotation\n\n const preset = find3DPreset(d.type, d.category, options.scopes)\n const threeD = presetToState(preset)\n\n return {\n category: d.category,\n type: d.type,\n id: d.id,\n left,\n top,\n width,\n height,\n ...(rotation !== undefined && { rotation: options.flipY ? -rotation : rotation }),\n threeD,\n ...d.state\n }\n })\n\n return {\n width: boardWidth || 1000,\n height: boardHeight || 1000,\n fillStyle: options.backgroundColor ?? '#ffffff',\n components,\n importMeta: options.importMeta\n }\n}\n\nfunction presetToState(preset: import('../types/registry.js').ThreeDPreset): Record<string, any> {\n return {\n geometry: preset.geometry,\n ...(preset.material && { material: preset.material }),\n origin: preset.origin ?? 'bottom'\n }\n}\n"]}
1
+ {"version":3,"file":"stage3-board.js","sourceRoot":"","sources":["../../../server/service/pipeline/stage3-board.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Stage 3 — ComponentDraft → Board Model JSON.\n * 좌표/단위 변환 + 3D Preset 적용.\n */\nimport { find3DPreset } from '../registry/index'\nimport type { BoundingBox } from '../types/index'\nimport type { ComponentCategory, ComponentDraft } from '../types/registry'\n\nexport interface BoardModel {\n width?: number\n height?: number\n fillStyle?: string\n /**\n * 컴포넌트 목록. things-scene 보드 모델 호환을 위해 optional.\n * 빈 보드 / legacy 형식 / root container 가 직접 model 인 경우 누락 가능.\n * 사용처는 항상 `(model.components ?? [])` 패턴으로 접근.\n */\n components?: BoardComponent[]\n /** 메타: import 출처 */\n importMeta?: {\n format: string\n sourceBbox: BoundingBox\n units?: string\n importedAt: string\n }\n /** things-scene / 사용자 도메인이 임의 추가하는 필드 */\n [k: string]: any\n}\n\nexport interface BoardComponent {\n /** Universal category — 모든 컴포넌트가 보유. 변환·렌더 정책의 기반. */\n category: ComponentCategory\n /** 도메인 type. 'GenericObject' 면 변환 가능한 placeholder. */\n type: string\n id?: string\n left: number\n top: number\n width: number\n height: number\n rotation?: number\n /** 3D Preset 해석 결과 (있으면) */\n threeD?: Record<string, any>\n [k: string]: any\n}\n\nexport interface AssembleOptions {\n /** scope 들 — 3D preset 조회 시 사용 */\n scopes?: string[]\n /** 좌표계 정규화: true 면 entities 의 minX,minY 가 0,0 이 되도록 이동 */\n normalizeOrigin?: boolean\n /** Y 축 뒤집기: CAD (Y-up) → scene (Y-down) */\n flipY?: boolean\n /** 단위 → scene unit 변환 비율. 1 이면 그대로. */\n scale?: number\n /** 보드 배경색 */\n backgroundColor?: string\n /** importMeta 를 모델에 첨부 */\n importMeta?: BoardModel['importMeta']\n}\n\n/**\n * 보드 외곽 padding (양쪽). 컴포넌트가 root 가장자리에 정확히 맞닿지 않도록 살짝 여백.\n * 너무 크면 빈 공간 낭비, 너무 작으면 시각적으로 갑갑함 → 40 unit.\n */\nconst BOARD_PADDING = 40\n\n/**\n * 빈 보드 / 너무 작은 컨텐츠일 때 fallback 크기.\n * 최소 1000x1000 — things-scene 의 RootContainer 기본 크기에 맞춤.\n */\nconst MIN_BOARD_SIZE = 1000\n\nexport function assembleBoard(\n drafts: ComponentDraft[],\n bbox: BoundingBox,\n options: AssembleOptions = {}\n): BoardModel {\n const scale = options.scale ?? 1\n const offsetX = options.normalizeOrigin ? -bbox.min.x : 0\n const offsetY = options.normalizeOrigin ? -bbox.min.y : 0\n\n // 1단계: bbox 기반 가설 board 크기. 그러나 entity bbox 와 position+size 가 어긋나는 경우\n // 컴포넌트가 root 외곽으로 spill 할 수 있어 2단계에서 컴포넌트 실제 edge 로 보정한다.\n let boardWidth = (bbox.max.x - bbox.min.x) * scale\n let boardHeight = (bbox.max.y - bbox.min.y) * scale\n\n // 1.5단계: 컴포넌트 layout 1차 (flipY 미적용 — boardHeight 확정 전이라 미루기).\n type Layout = {\n d: ComponentDraft\n src: ComponentDraft['source']\n left: number\n rawTop: number\n width: number\n height: number\n }\n const layouts: Layout[] = drafts.map(d => {\n const src = d.source\n const left = src ? (src.position.x + offsetX) * scale : 0\n const rawTop = src ? (src.position.y + offsetY) * scale : 0\n const width = (src?.size?.width ?? 1) * scale\n const height = (src?.size?.height ?? 1) * scale\n return { d, src, left, rawTop, width, height }\n })\n\n // 2단계: 컴포넌트 실제 edge 로 board 크기 보정. bbox 보다 컴포넌트 right/bottom 이\n // 크면 그것 기준. 모든 컴포넌트 left/top 도 음수 안 되도록 0 이상 보장 (있으면 offset 보정).\n let computedRight = 0\n let computedBottom = 0\n let computedMinLeft = 0\n let computedMinTop = 0\n for (const l of layouts) {\n if (l.left < computedMinLeft) computedMinLeft = l.left\n if (l.rawTop < computedMinTop) computedMinTop = l.rawTop\n if (l.left + l.width > computedRight) computedRight = l.left + l.width\n if (l.rawTop + l.height > computedBottom) computedBottom = l.rawTop + l.height\n }\n // 음수 left/top 이 있으면 추가 offset 으로 0 으로 끌어올림.\n const extraOffsetX = computedMinLeft < 0 ? -computedMinLeft : 0\n const extraOffsetY = computedMinTop < 0 ? -computedMinTop : 0\n if (extraOffsetX || extraOffsetY) {\n for (const l of layouts) {\n l.left += extraOffsetX\n l.rawTop += extraOffsetY\n }\n computedRight += extraOffsetX\n computedBottom += extraOffsetY\n }\n\n // 최종 board 크기 — bbox 와 컴포넌트 실제 max edge 중 큰 값 + padding.\n // 컴포넌트 없으면 padding 의미 없고, fallback 으로 MIN_BOARD_SIZE.\n boardWidth = Math.max(boardWidth, computedRight)\n boardHeight = Math.max(boardHeight, computedBottom)\n if (layouts.length > 0) {\n boardWidth += BOARD_PADDING * 2\n boardHeight += BOARD_PADDING * 2\n }\n boardWidth = Math.max(boardWidth, MIN_BOARD_SIZE)\n boardHeight = Math.max(boardHeight, MIN_BOARD_SIZE)\n\n // 3단계: padding 만큼 모든 컴포넌트 안쪽으로 평행이동 + flipY 적용.\n const components: BoardComponent[] = layouts.map(({ d, src, left, rawTop, width, height }) => {\n const finalLeft = left + BOARD_PADDING\n const finalRawTop = rawTop + BOARD_PADDING\n const finalTop = options.flipY ? boardHeight - finalRawTop - height : finalRawTop\n const rotation = src?.rotation\n\n const preset = find3DPreset(d.type, d.category, options.scopes)\n const threeD = presetToState(preset)\n\n return {\n category: d.category,\n type: d.type,\n id: d.id,\n left: finalLeft,\n top: finalTop,\n width,\n height,\n ...(rotation !== undefined && { rotation: options.flipY ? -rotation : rotation }),\n threeD,\n ...d.state\n }\n })\n\n return {\n width: boardWidth,\n height: boardHeight,\n fillStyle: options.backgroundColor ?? '#ffffff',\n components,\n importMeta: options.importMeta\n }\n}\n\nfunction presetToState(preset: import('../types/registry.js').ThreeDPreset): Record<string, any> {\n return {\n geometry: preset.geometry,\n ...(preset.material && { material: preset.material }),\n origin: preset.origin ?? 'bottom'\n }\n}\n"]}
@@ -1,4 +1,4 @@
1
- import type { BoardModel } from './stage3-board.js';
1
+ import type { BoardModel } from './stage3-board';
2
2
  export interface BindingOptions {
3
3
  scopes?: string[];
4
4
  }