@things-factory/figure-ui 10.1.14 → 10.1.16

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 (31) hide show
  1. package/client/modeller/figure-ai-target.ts +12 -0
  2. package/client/modeller/figure-canvas.ts +18 -2
  3. package/client/modeller/figure-inspector.ts +48 -7
  4. package/client/modeller/figure-side.ts +1 -0
  5. package/client/modeller/figure-source.test.ts +2 -0
  6. package/client/modeller/figure-source.ts +25 -3
  7. package/client/pages/figure-modeller-page.ts +364 -49
  8. package/dist-client/modeller/figure-ai-target.d.ts +10 -0
  9. package/dist-client/modeller/figure-ai-target.js +10 -0
  10. package/dist-client/modeller/figure-ai-target.js.map +1 -1
  11. package/dist-client/modeller/figure-canvas.d.ts +1 -0
  12. package/dist-client/modeller/figure-canvas.js +21 -2
  13. package/dist-client/modeller/figure-canvas.js.map +1 -1
  14. package/dist-client/modeller/figure-inspector.d.ts +4 -1
  15. package/dist-client/modeller/figure-inspector.js +47 -6
  16. package/dist-client/modeller/figure-inspector.js.map +1 -1
  17. package/dist-client/modeller/figure-side.js +1 -0
  18. package/dist-client/modeller/figure-side.js.map +1 -1
  19. package/dist-client/modeller/figure-source.d.ts +10 -3
  20. package/dist-client/modeller/figure-source.js +5 -0
  21. package/dist-client/modeller/figure-source.js.map +1 -1
  22. package/dist-client/pages/figure-modeller-page.d.ts +48 -1
  23. package/dist-client/pages/figure-modeller-page.js +376 -52
  24. package/dist-client/pages/figure-modeller-page.js.map +1 -1
  25. package/dist-client/route.d.ts +1 -1
  26. package/dist-client/tsconfig.tsbuildinfo +1 -1
  27. package/dist-server/tsconfig.tsbuildinfo +1 -1
  28. package/package.json +5 -5
  29. package/test/ai-proposal-contract.test.ts +3 -3
  30. package/translations/en.json +10 -1
  31. package/translations/ko.json +11 -1
@@ -9,3 +9,15 @@ export function takeFigureAITarget() {
9
9
  pending = undefined
10
10
  return target
11
11
  }
12
+
13
+ let pendingStage: { figureId?: string; source?: any; proposal?: any } | undefined
14
+ export function stageFigureProposal(data: { figureId?: string; source?: any; proposal?: any }) {
15
+ pendingStage = structuredClone(data)
16
+ window.dispatchEvent(new CustomEvent('figure-ai-stage', { detail: data }))
17
+ }
18
+
19
+ export function consumePendingFigureProposal() {
20
+ const target = pendingStage
21
+ pendingStage = undefined
22
+ return target
23
+ }
@@ -8,6 +8,19 @@ import { gridStep } from './part-edits.js'
8
8
  import { DEFAULT_VIEW, WORKBENCH } from './figure-view.js'
9
9
  import type { ViewSettings } from './figure-view.js'
10
10
  import type { BoardModel, PartModel } from './figure-source.js'
11
+ import type { FigurePlacement } from '@hatiolab/figure-model'
12
+
13
+ function toScenePlacement(placement?: FigurePlacement): 'floor' | 'inverted' | 'space' {
14
+ switch (placement) {
15
+ case 'ceiling':
16
+ return 'inverted'
17
+ case 'center':
18
+ return 'space'
19
+ case 'floor':
20
+ default:
21
+ return 'floor'
22
+ }
23
+ }
11
24
 
12
25
  /**
13
26
  * 판의 종이 색 — 거의 흰색.
@@ -188,6 +201,7 @@ export class FigureCanvas extends localize(i18next)(LitElement) {
188
201
  /** 보기 설정. 자산이 아니라 작업대다 — 저장되지 않는다(`figure-view.ts`). */
189
202
  @property({ type: Object }) view: ViewSettings = DEFAULT_VIEW
190
203
  @property({ type: Array }) parts: PartModel[] = []
204
+ @property({ type: Number }) revision = 0
191
205
  /** 지금 고른 부품 이름. 목록에서 고른 것을 캔버스에 알린다. */
192
206
  @property({ type: String }) picked?: string
193
207
 
@@ -275,7 +289,8 @@ export class FigureCanvas extends localize(i18next)(LitElement) {
275
289
  willUpdate(changed: Map<string, unknown>) {
276
290
  if (!this.hasUpdated) return
277
291
 
278
- if (changed.has('board') || changed.has('parts') || changed.has('figureId')) this.standUp()
292
+ if (changed.has('revision')) this.standing = undefined
293
+ if (changed.has('board') || changed.has('parts') || changed.has('figureId') || changed.has('revision')) this.standUp()
279
294
  if (changed.has('view')) this.applyView()
280
295
  if (changed.has('picked')) this.showPicked()
281
296
  }
@@ -317,7 +332,7 @@ export class FigureCanvas extends localize(i18next)(LitElement) {
317
332
  return
318
333
  }
319
334
 
320
- const signature = `${this.figureId ?? ''}:${board.width}x${board.height}x${board.depth}`
335
+ const signature = `${this.figureId ?? ''}:${board.width}x${board.height}x${board.depth}:${board.placement ?? 'floor'}`
321
336
  if (this.scene && this.standing === signature) {
322
337
  this.syncParts()
323
338
  return
@@ -333,6 +348,7 @@ export class FigureCanvas extends localize(i18next)(LitElement) {
333
348
  width: board.width,
334
349
  height: board.height,
335
350
  depth: board.depth,
351
+ placement: toScenePlacement(board.placement),
336
352
  threed: true,
337
353
  environment: 'studio',
338
354
  /*
@@ -13,10 +13,11 @@ import {
13
13
  SEGMENTED_PRIMITIVES,
14
14
  SEGMENT_PRESETS,
15
15
  SIZING_RULES,
16
+ FIGURE_PLACEMENTS,
16
17
  trianglesOf
17
18
  } from '@hatiolab/figure-model'
18
19
  import { ANCHOR_RULES, PART_ROLES, REPEAT_LIMIT, anchorOfPart, repeatPlanOfPart } from '@hatiolab/figure-model'
19
- import type { AnchorRule, Axis, FigurePart, MaterialPreset, PartRole, PrimitiveKind } from '@hatiolab/figure-model'
20
+ import type { AnchorRule, Axis, FigurePart, FigurePlacement, MaterialPreset, PartRole, PrimitiveKind } from '@hatiolab/figure-model'
20
21
  import { activePalette } from '@hatiolab/things-scene'
21
22
 
22
23
  import { partToFigure, partFromFigure } from './figure-source.js'
@@ -774,7 +775,8 @@ export class FigureInspector extends localize(i18next)(LitElement) {
774
775
  `
775
776
  ]
776
777
 
777
- /** 보드. 위치를 보드 중심 기준으로 바꾸려면 이것이 있어야 한다. */
778
+ /** 도형의 DB 식별자. 비어 있으면 미저장 신규 모델이다. */
779
+ @property({ type: String }) figureId = ''
778
780
  /** 도형의 표시 이름. 정본은 저작면이 들고, 여기서는 보여 주고 바꾼다(`figure-rename` 으로 알린다). */
779
781
  @property({ type: String }) figureName = ''
780
782
  /** 분류 하나 — 팔레트에서 묶는 기준. */
@@ -962,17 +964,49 @@ export class FigureInspector extends localize(i18next)(LitElement) {
962
964
  />
963
965
  </div>
964
966
  <div inline-field>
965
- <label>${i18next.t('figure.label.figure-type')}${this.helpButton('figure.text.type-name-cannot-change')}</label>
966
- <code fixed title=${i18next.t('figure.text.type-name-cannot-change')}>
967
- <md-icon>lock</md-icon>${this.board?.figureType ?? ''}
968
- </code>
969
- ${this.helpHint('figure.text.type-name-cannot-change')}
967
+ <label>${i18next.t('figure.label.figure-type')}${this.figureId ? this.helpButton('figure.text.type-name-cannot-change') : nothing}</label>
968
+ ${!this.figureId
969
+ ? html`<input
970
+ .value=${this.board?.figureType ?? ''}
971
+ placeholder=${i18next.t('figure.text.figure-type-placeholder', { defaultValue: '도형 식별 타입 (예: OHT)' })}
972
+ @change=${(e: Event) =>
973
+ this.dispatchEvent(
974
+ new CustomEvent('figure-type-change', {
975
+ detail: { type: (e.target as HTMLInputElement).value.trim().toUpperCase() },
976
+ bubbles: true,
977
+ composed: true
978
+ })
979
+ )}
980
+ />`
981
+ : html`<code fixed title=${i18next.t('figure.text.type-name-cannot-change')}>
982
+ <md-icon>lock</md-icon>${this.board?.figureType ?? ''}
983
+ </code>
984
+ ${this.helpHint('figure.text.type-name-cannot-change')}`}
970
985
  </div>
971
986
  <!--
972
987
  왜 못 고치는지 **적어 둔다.** 전에는 tooltip 에만 있었고, 그러면 눌러 보고 안 되는 것을
973
988
  알게 된다 — 화면이 안 되는 이유를 말하지 않으면 사용자가 자기 실수라고 생각한다.
974
989
  -->
975
990
 
991
+ <div inline-field>
992
+ <label>${i18next.t('figure.label.figure-placement')}${this.helpButton('figure.text.placement-explained')}</label>
993
+ <select
994
+ .value=${this.board?.placement ?? 'floor'}
995
+ @change=${(e: Event) => this.setPlacement((e.target as HTMLSelectElement).value as FigurePlacement)}
996
+ >
997
+ <option value="floor" ?selected=${(this.board?.placement ?? 'floor') === 'floor'}>
998
+ ${i18next.t('figure.label.placement-floor')}
999
+ </option>
1000
+ <option value="ceiling" ?selected=${this.board?.placement === 'ceiling'}>
1001
+ ${i18next.t('figure.label.placement-ceiling')}
1002
+ </option>
1003
+ <option value="center" ?selected=${this.board?.placement === 'center'}>
1004
+ ${i18next.t('figure.label.placement-center')}
1005
+ </option>
1006
+ </select>
1007
+ ${this.helpHint('figure.text.placement-explained')}
1008
+ </div>
1009
+
976
1010
  <div inline-field>
977
1011
  <label>${i18next.t('figure.label.category')}${this.helpButton('figure.text.category-explained')}</label>
978
1012
  <input
@@ -1041,6 +1075,13 @@ export class FigureInspector extends localize(i18next)(LitElement) {
1041
1075
  this.dispatchEvent(new CustomEvent('figure-catalog', { detail: patch, bubbles: true, composed: true }))
1042
1076
  }
1043
1077
 
1078
+ /** 3D 배치 기준면을 바꾼다 ('floor' | 'ceiling' | 'center'). */
1079
+ private setPlacement(placement: FigurePlacement) {
1080
+ if (!this.board) return
1081
+ const board = { ...this.board, placement }
1082
+ this.dispatchEvent(new CustomEvent('update-board', { detail: { board }, bubbles: true, composed: true }))
1083
+ }
1084
+
1044
1085
  private renderIdentity(part: FigurePart) {
1045
1086
  return html`
1046
1087
  <section>
@@ -192,6 +192,7 @@ export class FigureSide extends localize(i18next)(LitElement) {
192
192
 
193
193
  <figure-inspector
194
194
  ?off=${this.tab !== 'properties'}
195
+ .figureId=${this.figureId}
195
196
  .figureName=${this.figureName}
196
197
  .figureCategory=${this.figureCategory}
197
198
  .figureTags=${this.figureTags}
@@ -145,8 +145,10 @@ describe('펴기 — 형식을 씬 모델로', () => {
145
145
  */
146
146
  /** 형식이 가진 것을 되도록 다 담은 정본. 안 쓰이는 필드도 넣는다. */
147
147
  const FULL: FigureSource = {
148
+ version: 1,
148
149
  type: 'AGV',
149
150
  base: { x: 3000, y: 800, z: 2000 },
151
+ placement: 'floor',
150
152
  detailLevel: 'L',
151
153
  styleKit: 'factory',
152
154
  parts: [
@@ -1,4 +1,4 @@
1
- import type { DetailLevel, FigurePart, FigureSource, Vec3 } from '@hatiolab/figure-model'
1
+ import { FIGURE_SOURCE_VERSION, type DetailLevel, type FigurePart, type FigurePlacement, type FigureSource, type Vec3 } from '@hatiolab/figure-model'
2
2
 
3
3
  /**
4
4
  * Scene model <-> `FigureSource`.
@@ -72,14 +72,27 @@ import type { DetailLevel, FigurePart, FigureSource, Vec3 } from '@hatiolab/figu
72
72
  * under `shape` costs a line here, and forgetting that line was the fourth
73
73
  * of the four.
74
74
  */
75
- /** 보드의 상태. 기준 상자이자 원본 데이터의 뼈대다. */
75
+ /** 컴포넌트의 상태. 씬의 용어 그대로다. */
76
76
  export interface BoardModel {
77
- /** 기준 상자. 씬의 용어 그대로다. */
77
+ /** 형식 버전. */
78
+ version?: number
79
+ /*
80
+ The three sizes keep the scene component's names, and the scene assigns its axes differently
81
+ from the format: here `depth` is the vertical one. Naming the axis alone ("the y axis") does
82
+ not say which of the two conventions is meant, and this interface sits on the boundary between
83
+ them — so each field names the `FigureSource` field it becomes. `toFigureSource` and
84
+ `fromFigureSource` below are the only places that may disagree with these three lines.
85
+ */
86
+ /** 가로 너비 (mm) → `base.x`. */
78
87
  width: number
88
+ /** 앞뒤 깊이 (mm) → `base.z`. 저작자가 위에서 내려다볼 때의 세로다. */
79
89
  height: number
90
+ /** 높이 (mm) → `base.y`. 서 있는 방향이다. */
80
91
  depth: number
81
92
  /** 컴포넌트 타입 이름. **저장되는 식별자다.** */
82
93
  figureType?: string
94
+ /** 3D 배치 기준면 ('floor' | 'ceiling' | 'center'). */
95
+ placement?: FigurePlacement
83
96
  detailLevel?: DetailLevel
84
97
  styleKit?: string
85
98
  /**
@@ -137,7 +150,12 @@ export interface PartModel {
137
150
  */
138
151
  hidden?: boolean
139
152
 
153
+ /** 색 토큰 이름 (`palette.primary` 등). */
140
154
  token?: string
155
+ /*
156
+ `FigurePart['material']` is required in the format, so these index into it directly. The
157
+ `NonNullable` on `shape` above is not the same case — `shape` is optional there.
158
+ */
141
159
  preset?: FigurePart['material']['preset']
142
160
  flatShading?: boolean
143
161
  transparent?: boolean
@@ -228,8 +246,10 @@ export function toFigureSource(board: BoardModel, parts: PartModel[]): FigureSou
228
246
  const half = halfOf(board)
229
247
 
230
248
  return withoutEmpty({
249
+ version: board.version ?? FIGURE_SOURCE_VERSION,
231
250
  type: board.figureType ?? '',
232
251
  base: { x: board.width, y: board.depth, z: board.height },
252
+ placement: board.placement,
233
253
  detailLevel: board.detailLevel,
234
254
  styleKit: board.styleKit,
235
255
  parts: parts.map(part => partTo(part, half)),
@@ -292,10 +312,12 @@ export function fromFigureSource(source: FigureSource): { board: BoardModel; par
292
312
  const half: Vec3 = { x: source.base.x / 2, y: source.base.y / 2, z: source.base.z / 2 }
293
313
 
294
314
  const board = withoutEmpty({
315
+ version: source.version,
295
316
  width: source.base.x,
296
317
  height: source.base.z,
297
318
  depth: source.base.y,
298
319
  figureType: source.type,
320
+ placement: source.placement,
299
321
  detailLevel: source.detailLevel,
300
322
  styleKit: source.styleKit,
301
323
  animations: source.animations,