@operato/board 10.0.0-beta.5 → 10.0.0-beta.51

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 (89) hide show
  1. package/CHANGELOG.md +422 -0
  2. package/dist/src/component/container.js +18 -3
  3. package/dist/src/component/container.js.map +1 -1
  4. package/dist/src/component/conveyance.d.ts +2 -0
  5. package/dist/src/component/conveyance.js +38 -0
  6. package/dist/src/component/conveyance.js.map +1 -0
  7. package/dist/src/component/etc.js +2 -10
  8. package/dist/src/component/etc.js.map +1 -1
  9. package/dist/src/component/facility.d.ts +2 -0
  10. package/dist/src/component/facility.js +35 -0
  11. package/dist/src/component/facility.js.map +1 -0
  12. package/dist/src/component/index.d.ts +5 -0
  13. package/dist/src/component/index.js +5 -0
  14. package/dist/src/component/index.js.map +1 -1
  15. package/dist/src/component/line.js +4 -28
  16. package/dist/src/component/line.js.map +1 -1
  17. package/dist/src/component/manufacturing.d.ts +2 -0
  18. package/dist/src/component/manufacturing.js +41 -0
  19. package/dist/src/component/manufacturing.js.map +1 -0
  20. package/dist/src/component/register-default-groups.js +19 -2
  21. package/dist/src/component/register-default-groups.js.map +1 -1
  22. package/dist/src/component/shape.js +5 -29
  23. package/dist/src/component/shape.js.map +1 -1
  24. package/dist/src/component/storage.d.ts +2 -0
  25. package/dist/src/component/storage.js +27 -0
  26. package/dist/src/component/storage.js.map +1 -0
  27. package/dist/src/component/text-and-media.js +2 -25
  28. package/dist/src/component/text-and-media.js.map +1 -1
  29. package/dist/src/component/transport.d.ts +2 -0
  30. package/dist/src/component/transport.js +36 -0
  31. package/dist/src/component/transport.js.map +1 -0
  32. package/dist/src/component/warehouse.d.ts +1 -0
  33. package/dist/src/component/warehouse.js +8 -1
  34. package/dist/src/component/warehouse.js.map +1 -1
  35. package/dist/src/data-storage/board-model-cache.d.ts +30 -0
  36. package/dist/src/data-storage/board-model-cache.js +93 -0
  37. package/dist/src/data-storage/board-model-cache.js.map +1 -0
  38. package/dist/src/graphql/playback-buffer.d.ts +79 -0
  39. package/dist/src/graphql/playback-buffer.js +139 -0
  40. package/dist/src/graphql/playback-buffer.js.map +1 -0
  41. package/dist/src/graphql/playback-buffer.test.d.ts +1 -0
  42. package/dist/src/graphql/playback-buffer.test.js +261 -0
  43. package/dist/src/graphql/playback-buffer.test.js.map +1 -0
  44. package/dist/src/graphql/playback-subscription.d.ts +89 -0
  45. package/dist/src/graphql/playback-subscription.js +258 -0
  46. package/dist/src/graphql/playback-subscription.js.map +1 -0
  47. package/dist/src/index.d.ts +3 -0
  48. package/dist/src/index.js +2 -0
  49. package/dist/src/index.js.map +1 -1
  50. package/dist/src/modeller/bulk-create-dialog.d.ts +51 -0
  51. package/dist/src/modeller/bulk-create-dialog.js +531 -0
  52. package/dist/src/modeller/bulk-create-dialog.js.map +1 -0
  53. package/dist/src/modeller/bulk-create.d.ts +101 -0
  54. package/dist/src/modeller/bulk-create.js +199 -0
  55. package/dist/src/modeller/bulk-create.js.map +1 -0
  56. package/dist/src/modeller/component-toolbar/component-menu.js +8 -1
  57. package/dist/src/modeller/component-toolbar/component-menu.js.map +1 -1
  58. package/dist/src/modeller/edit-toolbar-style.js +38 -1
  59. package/dist/src/modeller/edit-toolbar-style.js.map +1 -1
  60. package/dist/src/modeller/edit-toolbar.d.ts +21 -16
  61. package/dist/src/modeller/edit-toolbar.js +362 -201
  62. package/dist/src/modeller/edit-toolbar.js.map +1 -1
  63. package/dist/src/modeller/scene-viewer/ox-scene-viewer.d.ts +2 -1
  64. package/dist/src/modeller/scene-viewer/ox-scene-viewer.js +7 -11
  65. package/dist/src/modeller/scene-viewer/ox-scene-viewer.js.map +1 -1
  66. package/dist/src/ox-board-modeller.d.ts +8 -1
  67. package/dist/src/ox-board-modeller.js +125 -6
  68. package/dist/src/ox-board-modeller.js.map +1 -1
  69. package/dist/src/ox-board-preview.d.ts +36 -0
  70. package/dist/src/ox-board-preview.js +114 -0
  71. package/dist/src/ox-board-preview.js.map +1 -0
  72. package/dist/src/ox-board-template-list.d.ts +1 -0
  73. package/dist/src/ox-board-template-list.js +19 -1
  74. package/dist/src/ox-board-template-list.js.map +1 -1
  75. package/dist/src/ox-board-viewer.d.ts +50 -1
  76. package/dist/src/ox-board-viewer.js +271 -28
  77. package/dist/src/ox-board-viewer.js.map +1 -1
  78. package/dist/src/ox-playback-controls.d.ts +56 -0
  79. package/dist/src/ox-playback-controls.js +515 -0
  80. package/dist/src/ox-playback-controls.js.map +1 -0
  81. package/dist/src/selector/ox-board-selector.js +11 -1
  82. package/dist/src/selector/ox-board-selector.js.map +1 -1
  83. package/dist/tsconfig.tsbuildinfo +1 -1
  84. package/package.json +17 -12
  85. package/translations/en.json +19 -1
  86. package/translations/ja.json +19 -1
  87. package/translations/ko.json +19 -1
  88. package/translations/ms.json +19 -1
  89. package/translations/zh.json +19 -1
@@ -3,18 +3,72 @@
3
3
  */
4
4
  import { __decorate } from "tslib";
5
5
  import { html, LitElement } from 'lit';
6
- import { property, query, queryAll } from 'lit/decorators.js';
6
+ import { property, state } from 'lit/decorators.js';
7
7
  import { copyToClipboard, isMacOS } from '@operato/utils';
8
+ import { OxPopup } from '@operato/popup';
8
9
  import { style } from './edit-toolbar-style.js';
10
+ import './bulk-create-dialog.js';
9
11
  const MACOS = isMacOS();
12
+ /**
13
+ * 컴포넌트 model JSON 만으로 type-aware bounding box (width, height) 추출.
14
+ *
15
+ * things-scene 의 component.bounds 는 *path 의 bounding box* 인데, model JSON 만
16
+ * 으로는 path 를 그릴 수 없으므로 type-specific 필드를 직접 본다.
17
+ * - width/height 명시 (Rect, Container 등) → 그대로
18
+ * - rx/ry (Ellipse 등) → 2*rx, 2*ry
19
+ * - points 배열 (Line, Polygon 등) → 점들의 bounding box
20
+ * - 그 외 → 50x50 fallback
21
+ *
22
+ * bulk-paste 가 영역 안에 N 개 복제할 때 *원본 크기를 보존* 하기 위해 사용.
23
+ */
24
+ function deriveBoundsFromModel(model) {
25
+ if (typeof model.width === 'number' && typeof model.height === 'number') {
26
+ return { width: model.width, height: model.height };
27
+ }
28
+ if (typeof model.rx === 'number' && typeof model.ry === 'number') {
29
+ return { width: 2 * Math.abs(model.rx), height: 2 * Math.abs(model.ry) };
30
+ }
31
+ if (Array.isArray(model.points) && model.points.length > 0) {
32
+ let minX = Infinity;
33
+ let maxX = -Infinity;
34
+ let minY = Infinity;
35
+ let maxY = -Infinity;
36
+ for (const p of model.points) {
37
+ if (typeof (p === null || p === void 0 ? void 0 : p.x) === 'number') {
38
+ if (p.x < minX)
39
+ minX = p.x;
40
+ if (p.x > maxX)
41
+ maxX = p.x;
42
+ }
43
+ if (typeof (p === null || p === void 0 ? void 0 : p.y) === 'number') {
44
+ if (p.y < minY)
45
+ minY = p.y;
46
+ if (p.y > maxY)
47
+ maxY = p.y;
48
+ }
49
+ }
50
+ if (isFinite(minX) && isFinite(maxX) && isFinite(minY) && isFinite(maxY)) {
51
+ return { width: maxX - minX, height: maxY - minY };
52
+ }
53
+ }
54
+ return { width: 50, height: 50 };
55
+ }
10
56
  export class EditToolbar extends LitElement {
11
57
  constructor() {
12
58
  super(...arguments);
13
59
  this.selected = [];
14
60
  this.hideProperty = false;
61
+ this._dimension = null;
62
+ this._gizmoAttached = false;
15
63
  }
16
64
  firstUpdated() {
17
65
  this.addEventListener('mousewheel', this.onWheelEvent.bind(this), false);
66
+ // 툴바 버튼 클릭 후 씬으로 포커스를 돌려서 키보드 단축키가 계속 작동하도록 한다
67
+ this.addEventListener('click', () => {
68
+ var _a, _b, _c;
69
+ ;
70
+ (_c = (_b = (_a = this.scene) === null || _a === void 0 ? void 0 : _a.root) === null || _b === void 0 ? void 0 : _b.element) === null || _c === void 0 ? void 0 : _c.focus();
71
+ });
18
72
  window.addEventListener('paste', (e) => {
19
73
  var _a;
20
74
  try {
@@ -24,33 +78,7 @@ export class EditToolbar extends LitElement {
24
78
  console.error('model paste failed', e);
25
79
  }
26
80
  });
27
- this.aligners.forEach(aligner => aligner.addEventListener('click', this.onTapAlign.bind(this)));
28
- this.zorders.forEach(zorder => zorder.addEventListener('click', this.onTapZorder.bind(this)));
29
- this.distributes.forEach(distribute => distribute.addEventListener('click', this.onTapDistribute.bind(this)));
30
- this.undo.addEventListener('click', this.onTapUndo.bind(this));
31
- this.redo.addEventListener('click', this.onTapRedo.bind(this));
32
- this.fullscreen.addEventListener('click', this.onTapFullscreen.bind(this));
33
- this.styleCopy.addEventListener('click', this.onStartStylePasteMode.bind(this));
34
- this.databindCopy.addEventListener('click', this.onStartDatabindPasteMode.bind(this));
35
- this.cut.addEventListener('click', this.onTapCut.bind(this));
36
- this.copy.addEventListener('click', this.onTapCopy.bind(this));
37
- this.paste.addEventListener('click', this.onTapPaste.bind(this));
38
- this.delete.addEventListener('click', this.onTapDelete.bind(this));
39
- this.renderRoot
40
- .querySelector('#font-increase')
41
- .addEventListener('click', this.onTapFontIncrease.bind(this));
42
- this.renderRoot
43
- .querySelector('#font-decrease')
44
- .addEventListener('click', this.onTapFontDecrease.bind(this));
45
- this.renderRoot.querySelector('#group').addEventListener('click', this.onTapGroup.bind(this));
46
- this.renderRoot.querySelector('#ungroup').addEventListener('click', this.onTapUngroup.bind(this));
47
- this.renderRoot
48
- .querySelector('#toggle-property')
49
- .addEventListener('click', this.onTapToggle.bind(this));
50
- this.renderRoot
51
- .querySelector('#fit-scene')
52
- .addEventListener('click', this.onTapFitScene.bind(this));
53
- this.renderRoot.querySelector('#preview').addEventListener('click', this.onTapPreview.bind(this));
81
+ // 모든 버튼은 템플릿 @click 디렉티브로 바인딩 — 조건부 렌더링 시에도 정상 작동
54
82
  }
55
83
  updated(changes) {
56
84
  changes.has('scene') && this.onSceneChanged(this.scene, changes.get('scene'));
@@ -61,127 +89,122 @@ export class EditToolbar extends LitElement {
61
89
  <div tools>
62
90
  <span><slot></slot></span>
63
91
 
64
- <span button id="undo" title="undo (${this.getShortcutString('cmd', 'z')})"> </span>
65
- <span button id="redo" title="redo (${this.getShortcutString('cmd', 'shift', 'z')})"> </span>
92
+ <span button id="undo" title="undo (${this.getShortcutString('cmd', 'z')})" @click=${this.onTapUndo}> </span>
93
+ <span button id="redo" title="redo (${this.getShortcutString('cmd', 'shift', 'z')})" @click=${this.onTapRedo}> </span>
66
94
 
67
95
  <span class="vline"></span>
68
96
 
69
- <span button id="style-copy" title="style copy (${this.getShortcutString('cmd', '1')})"> </span>
70
- <span button id="databind-copy" title="databind copy (${this.getShortcutString('cmd', '2')})"> </span>
97
+ <span button id="style-copy" title="style copy (${this.getShortcutString('cmd', '1')})" @click=${this.onStartStylePasteMode}> </span>
98
+ <span button id="databind-copy" title="databind copy (${this.getShortcutString('cmd', '2')})" @click=${this.onStartDatabindPasteMode}> </span>
71
99
 
72
100
  <span class="vline"></span>
73
101
 
74
- <span button id="cut" title="cut (${this.getShortcutString('cmd', 'x')})"> </span>
75
- <span button id="copy" title="copy (${this.getShortcutString('cmd', 'c')})"> </span>
76
- <span button id="paste" title="paste (${this.getShortcutString('cmd', 'v')})"> </span>
77
- <span
78
- button
79
- id="delete"
102
+ <span button id="cut" title="cut (${this.getShortcutString('cmd', 'x')})" @click=${this.onTapCut}> </span>
103
+ <span button id="copy" title="copy (${this.getShortcutString('cmd', 'c')})" @click=${this.onTapCopy}> </span>
104
+ <span button id="paste" title="paste (${this.getShortcutString('cmd', 'v')})" @click=${this.onTapPaste}> </span>
105
+ <span button id="delete"
80
106
  title="delete (${this.getShortcutString('backspace')}, ${this.getShortcutString('delete')})"
81
- >
82
- </span>
107
+ @click=${this.onTapDelete}> </span>
83
108
 
84
109
  <span class="vline"></span>
85
110
 
86
- <span
87
- button
88
- data-align="left"
89
- id="align-left"
90
- title="align left (${this.getShortcutString('alt', 'shift', 'l')})"
91
- >
92
- </span>
93
- <span
94
- button
95
- data-align="center"
96
- id="align-center"
97
- title="align center (${this.getShortcutString('alt', 'shift', 'c')})"
98
- >
99
- </span>
100
- <span
101
- button
102
- data-align="right"
103
- id="align-right"
104
- title="align right (${this.getShortcutString('alt', 'shift', 'r')})"
105
- >
106
- </span>
111
+ <span button data-align="left" id="align-left"
112
+ title="align left (${this.getShortcutString('alt', 'shift', 'l')})" @click=${this.onTapAlign}> </span>
113
+ <span button data-align="center" id="align-center"
114
+ title="align center (${this.getShortcutString('alt', 'shift', 'c')})" @click=${this.onTapAlign}> </span>
115
+ <span button data-align="right" id="align-right"
116
+ title="align right (${this.getShortcutString('alt', 'shift', 'r')})" @click=${this.onTapAlign}> </span>
107
117
 
108
- <span button data-align="top" id="align-top" title="align top (${this.getShortcutString('alt', 'shift', 't')})">
109
- </span>
110
- <span
111
- button
112
- data-align="middle"
113
- id="align-middle"
114
- title="align middle (${this.getShortcutString('alt', 'shift', 'm')})"
115
- >
116
- </span>
117
- <span
118
- button
119
- data-align="bottom"
120
- id="align-bottom"
121
- title="align bottom (${this.getShortcutString('alt', 'shift', 'b')})"
122
- >
123
- </span>
118
+ <span button data-align="top" id="align-top"
119
+ title="align top (${this.getShortcutString('alt', 'shift', 't')})" @click=${this.onTapAlign}> </span>
120
+ <span button data-align="middle" id="align-middle"
121
+ title="align middle (${this.getShortcutString('alt', 'shift', 'm')})" @click=${this.onTapAlign}> </span>
122
+ <span button data-align="bottom" id="align-bottom"
123
+ title="align bottom (${this.getShortcutString('alt', 'shift', 'b')})" @click=${this.onTapAlign}> </span>
124
124
 
125
- <span
126
- button
127
- data-distribute="HORIZONTAL"
128
- id="distribute-horizontal"
129
- title="distribute horizontally (${this.getShortcutString('alt', 'shift', 'h')})"
130
- >
131
- </span>
125
+ <span button data-distribute="HORIZONTAL" id="distribute-horizontal"
126
+ title="distribute horizontally (${this.getShortcutString('alt', 'shift', 'h')})" @click=${this.onTapDistribute}> </span>
127
+ <span button data-distribute="VERTICAL" id="distribute-vertical"
128
+ title="distribute vertically (${this.getShortcutString('alt', 'shift', 'v')})" @click=${this.onTapDistribute}> </span>
132
129
 
133
- <span
134
- button
135
- data-distribute="VERTICAL"
136
- id="distribute-vertical"
137
- title="distribute vertically (${this.getShortcutString('alt', 'shift', 'v')})"
138
- >
139
- </span>
130
+ ${this._dimension === '3d'
131
+ ? html `
132
+ <span class="vline"></span>
140
133
 
141
- <span class="vline"></span>
134
+ <span button data-align="z-front" id="align-z-front" title="align Z front" @click=${this.onTapAlign}> </span>
135
+ <span button data-align="z-middle" id="align-z-middle" title="align Z middle" @click=${this.onTapAlign}> </span>
136
+ <span button data-align="z-back" id="align-z-back" title="align Z back" @click=${this.onTapAlign}> </span>
137
+ <span button data-distribute="Z" id="distribute-z" title="distribute Z" @click=${this.onTapDistribute}> </span>
138
+ `
139
+ : this._dimension === '2d'
140
+ ? html `
141
+ <span class="vline"></span>
142
142
 
143
- <span
144
- button
145
- id="front"
146
- data-zorder="front"
147
- title="bring to front (${this.getShortcutString('cmd', 'shift', 'f')})"
148
- >
149
- </span>
150
- <span button id="back" data-zorder="back" title="send to back (${this.getShortcutString('cmd', 'shift', 'b')})">
151
- </span>
152
- <span button id="forward" data-zorder="forward" title="bring forward (${this.getShortcutString('cmd', 'f')})">
153
- </span>
154
- <span button id="backward" data-zorder="backward" title="send backward (${this.getShortcutString('cmd', 'b')})">
155
- </span>
143
+ <span button id="front" data-zorder="front"
144
+ title="bring to front (${this.getShortcutString('cmd', 'shift', 'f')})" @click=${this.onTapZorder}> </span>
145
+ <span button id="back" data-zorder="back"
146
+ title="send to back (${this.getShortcutString('cmd', 'shift', 'b')})" @click=${this.onTapZorder}> </span>
147
+ <span button id="forward" data-zorder="forward"
148
+ title="bring forward (${this.getShortcutString('cmd', 'f')})" @click=${this.onTapZorder}> </span>
149
+ <span button id="backward" data-zorder="backward"
150
+ title="send backward (${this.getShortcutString('cmd', 'b')})" @click=${this.onTapZorder}> </span>
151
+ `
152
+ : ''}
156
153
 
157
- <span class="vline"></span>
154
+ ${this._dimension === '2d' ? html `
155
+ <span class="vline"></span>
156
+
157
+ <span button id="group" title="group (${this.getShortcutString('cmd', 'g')})" @click=${this.onTapGroup}> </span>
158
+ <span button id="ungroup" title="ungroup (${this.getShortcutString('cmd', 'shift', 'g')})" @click=${this.onTapUngroup}> </span>
159
+ ` : this._dimension === '3d' ? html `
160
+ <span class="vline"></span>
158
161
 
159
- <span button id="group" title="group (${this.getShortcutString('cmd', 'g')})"> </span>
160
- <span button id="ungroup" title="ungroup (${this.getShortcutString('cmd', 'shift', 'g')})"> </span>
162
+ <span button class="gizmo-btn" data-gizmo="translate" title="Move (W)"
163
+ @click=${() => this._setGizmoMode('translate')}>
164
+ <svg viewBox="0 0 20 20" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8">
165
+ <line x1="10" y1="2" x2="10" y2="18"/><line x1="2" y1="10" x2="18" y2="10"/>
166
+ <polyline points="10,2 7.5,5"/><polyline points="10,2 12.5,5"/>
167
+ <polyline points="18,10 15,7.5"/><polyline points="18,10 15,12.5"/>
168
+ <polyline points="10,18 7.5,15"/><polyline points="10,18 12.5,15"/>
169
+ <polyline points="2,10 5,7.5"/><polyline points="2,10 5,12.5"/>
170
+ </svg>
171
+ </span>
172
+ <span button class="gizmo-btn" data-gizmo="rotate" title="Rotate (E)"
173
+ @click=${() => this._setGizmoMode('rotate')}>
174
+ <svg viewBox="0 0 20 20" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8">
175
+ <path d="M14.5 3.5A7 7 0 1 0 17 10"/>
176
+ <polyline points="14,1 15,3.5 12.5,4.5"/>
177
+ </svg>
178
+ </span>
179
+ <span button class="gizmo-btn" data-gizmo="scale" title="Scale (R)"
180
+ @click=${() => this._setGizmoMode('scale')}>
181
+ <svg viewBox="0 0 20 20" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8">
182
+ <line x1="4" y1="16" x2="16" y2="4"/>
183
+ <rect x="2" y="13" width="4" height="4" rx="0.5" fill="currentColor"/>
184
+ <rect x="14" y="2" width="4" height="4" rx="0.5" fill="currentColor"/>
185
+ </svg>
186
+ </span>
187
+ ` : ''}
161
188
 
162
189
  <span class="vline"></span>
163
190
 
164
- <span button id="font-increase" title="increase font size"></span>
165
- <span button id="font-decrease" title="decrease font size" style="scale: 0.7;"></span>
191
+ <span button id="font-increase" title="increase font size" @click=${this.onTapFontIncrease}></span>
192
+ <span button id="font-decrease" title="decrease font size" style="scale: 0.7;" @click=${this.onTapFontDecrease}></span>
166
193
 
167
194
  <span class="vline"></span>
168
195
  <span padding></span>
169
196
 
170
- <span button id="fit-scene" title="fit scene (${this.getShortcutString('cmd', 'd')})"> </span>
197
+ <span button id="fit-scene" title="fit scene (${this.getShortcutString('cmd', 'd')})" @click=${this.onTapFitScene}> </span>
171
198
 
172
199
  <span class="vline"></span>
173
200
 
174
- <span button id="preview" title="preview (${this.getShortcutString('ctrl', 'p')})"> </span>
201
+ <span button id="preview" title="preview (${this.getShortcutString('ctrl', 'p')})" @click=${this.onTapPreview}> </span>
175
202
 
176
- <span button id="fullscreen" title="fullscreen (${this.getShortcutString('f11')})"> </span>
203
+ <span button id="fullscreen" title="fullscreen (${this.getShortcutString('f11')})" @click=${this.onTapFullscreen}> </span>
177
204
 
178
- <span
179
- button
180
- id="toggle-property"
205
+ <span button id="toggle-property"
181
206
  title="toggle property panel (${this.getShortcutString('cmd', 'h')})"
182
- toggles="true"
183
- >
184
- </span>
207
+ toggles="true" @click=${this.onTapToggle}> </span>
185
208
  </div>
186
209
  `;
187
210
  }
@@ -224,7 +247,7 @@ export class EditToolbar extends LitElement {
224
247
  return symbols.join(MACOS ? '' : '+');
225
248
  }
226
249
  onShortcut(e) {
227
- var _a, _b, _c, _d;
250
+ var _a, _b, _c, _d, _e, _f;
228
251
  if (MACOS)
229
252
  var ctrlKey = e.metaKey;
230
253
  else
@@ -262,7 +285,12 @@ export class EditToolbar extends LitElement {
262
285
  this.onTapCut();
263
286
  break;
264
287
  case 'KeyV':
265
- if (ctrlKey && !shiftKey) {
288
+ if (ctrlKey && shiftKey && !altKey) {
289
+ // Ctrl+Shift+V = Bulk Paste — 클립보드의 *단일* 컴포넌트를 N개로 일괄 붙여넣기.
290
+ this.onBulkPaste();
291
+ defaultPrevent = true;
292
+ }
293
+ else if (ctrlKey && !shiftKey) {
266
294
  this.onTapPaste();
267
295
  defaultPrevent = false;
268
296
  }
@@ -275,23 +303,43 @@ export class EditToolbar extends LitElement {
275
303
  defaultPrevent = true;
276
304
  break;
277
305
  case 'KeyG':
278
- if (ctrlKey && !shiftKey)
279
- this.onTapGroup();
280
- else if (ctrlKey && shiftKey)
281
- this.onTapUngroup();
306
+ if (this._dimension === '2d') {
307
+ if (ctrlKey && !shiftKey)
308
+ this.onTapGroup();
309
+ else if (ctrlKey && shiftKey)
310
+ this.onTapUngroup();
311
+ }
282
312
  break;
283
- case 'KeyF':
284
- if (ctrlKey && !shiftKey)
285
- (_a = this.scene) === null || _a === void 0 ? void 0 : _a.zorder('forward');
286
- else if (ctrlKey && shiftKey)
287
- (_b = this.scene) === null || _b === void 0 ? void 0 : _b.zorder('front');
313
+ case 'BracketRight':
314
+ // Ctrl+] = Bring Forward, Ctrl+Shift+] = Bring to Front
315
+ // (Photoshop / Illustrator 표준 컨벤션)
316
+ if (this._dimension === '2d') {
317
+ if (ctrlKey && !shiftKey) {
318
+ (_a = this.scene) === null || _a === void 0 ? void 0 : _a.zorder('forward');
319
+ defaultPrevent = true;
320
+ }
321
+ else if (ctrlKey && shiftKey) {
322
+ (_b = this.scene) === null || _b === void 0 ? void 0 : _b.zorder('front');
323
+ defaultPrevent = true;
324
+ }
325
+ }
326
+ break;
327
+ case 'BracketLeft':
328
+ // Ctrl+[ = Send Backward, Ctrl+Shift+[ = Send to Back
329
+ if (this._dimension === '2d') {
330
+ if (ctrlKey && !shiftKey) {
331
+ (_c = this.scene) === null || _c === void 0 ? void 0 : _c.zorder('backward');
332
+ defaultPrevent = true;
333
+ }
334
+ else if (ctrlKey && shiftKey) {
335
+ (_d = this.scene) === null || _d === void 0 ? void 0 : _d.zorder('back');
336
+ defaultPrevent = true;
337
+ }
338
+ }
288
339
  break;
289
340
  case 'KeyB':
290
- if (ctrlKey && !shiftKey)
291
- (_c = this.scene) === null || _c === void 0 ? void 0 : _c.zorder('backward');
292
- else if (ctrlKey && shiftKey)
293
- (_d = this.scene) === null || _d === void 0 ? void 0 : _d.zorder('back');
294
- else if (altKey && shiftKey)
341
+ // zorder BracketLeft 로 이동. KeyB 의 Alt+Shift+B = align bottom 만 유지.
342
+ if (altKey && shiftKey)
295
343
  this.onTapAlign('bottom');
296
344
  break;
297
345
  case 'KeyH':
@@ -335,23 +383,38 @@ export class EditToolbar extends LitElement {
335
383
  this.onTapFitScene();
336
384
  defaultPrevent = true;
337
385
  }
386
+ else if (!shiftKey && !altKey) {
387
+ const target = e.composedPath()[0];
388
+ const tagName = target.tagName;
389
+ if (!target.isContentEditable && tagName !== 'INPUT' && tagName !== 'SELECT' && tagName !== 'TEXTAREA') {
390
+ this.onTapDataBinding();
391
+ defaultPrevent = true;
392
+ }
393
+ }
338
394
  break;
339
395
  case 'KeyE':
340
396
  if (ctrlKey && shiftKey)
341
397
  this.onTapDownloadModel();
342
398
  break;
399
+ case 'F10':
400
+ this.onTapEditModel();
401
+ defaultPrevent = true;
402
+ break;
343
403
  case 'Digit1':
344
404
  if (ctrlKey) {
345
- console.log('MODEL', this.scene && this.scene.model);
346
405
  defaultPrevent = true;
347
406
  }
348
407
  break;
349
408
  case 'Digit2':
350
409
  if (ctrlKey) {
351
- console.log('SELECTED', this.scene && this.scene.selected);
352
410
  defaultPrevent = true;
353
411
  }
354
412
  break;
413
+ case 'Escape':
414
+ (_e = this.scene) === null || _e === void 0 ? void 0 : _e.stopStylePasteMode();
415
+ (_f = this.scene) === null || _f === void 0 ? void 0 : _f.stopDatabindPasteMode();
416
+ defaultPrevent = true;
417
+ break;
355
418
  default:
356
419
  return false;
357
420
  }
@@ -359,40 +422,75 @@ export class EditToolbar extends LitElement {
359
422
  e.preventDefault();
360
423
  return true;
361
424
  }
425
+ _setDisabled(id, disabled) {
426
+ const el = this.renderRoot.querySelector(`#${id}`);
427
+ disabled ? el === null || el === void 0 ? void 0 : el.setAttribute('disabled', '') : el === null || el === void 0 ? void 0 : el.removeAttribute('disabled');
428
+ }
362
429
  onExecute(command, undoable, redoable) {
363
- !undoable ? this.undo.setAttribute('disabled', '') : this.undo.removeAttribute('disabled');
364
- !redoable ? this.redo.setAttribute('disabled', '') : this.redo.removeAttribute('disabled');
430
+ this._setDisabled('undo', !undoable);
431
+ this._setDisabled('redo', !redoable);
365
432
  }
366
433
  onUndo(undoable, redoable) {
367
- !undoable ? this.undo.setAttribute('disabled', '') : this.undo.removeAttribute('disabled');
368
- !redoable ? this.redo.setAttribute('disabled', '') : this.redo.removeAttribute('disabled');
434
+ this._setDisabled('undo', !undoable);
435
+ this._setDisabled('redo', !redoable);
369
436
  }
370
437
  onRedo(undoable, redoable) {
371
- !undoable ? this.undo.setAttribute('disabled', '') : this.undo.removeAttribute('disabled');
372
- !redoable ? this.redo.setAttribute('disabled', '') : this.redo.removeAttribute('disabled');
438
+ this._setDisabled('undo', !undoable);
439
+ this._setDisabled('redo', !redoable);
373
440
  }
374
441
  onSceneChanged(after, before) {
442
+ var _a, _b;
375
443
  if (before) {
376
444
  before.off('execute', this.onExecute, this);
377
445
  before.off('undo', this.onUndo, this);
378
446
  before.off('redo', this.onRedo, this);
447
+ before.off('dimension', this._onDimensionChanged, this);
448
+ before.off('gizmoattach', this._onGizmoAttachChanged, this);
379
449
  }
380
450
  if (after) {
381
451
  after.on('execute', this.onExecute, this);
382
452
  after.on('undo', this.onUndo, this);
383
453
  after.on('redo', this.onRedo, this);
454
+ after.on('dimension', this._onDimensionChanged, this);
455
+ after.on('gizmoattach', this._onGizmoAttachChanged, this);
456
+ // scene 설정 시 현재 dimension을 즉시 반영 (초기 이벤트를 놓친 경우 대비)
457
+ const threed = (_b = (_a = after.model_layer) === null || _a === void 0 ? void 0 : _a.model) === null || _b === void 0 ? void 0 : _b.threed;
458
+ this._dimension = threed ? '3d' : '2d';
384
459
  }
385
460
  }
461
+ _onDimensionChanged(dimension) {
462
+ this._dimension = dimension;
463
+ }
464
+ _onGizmoAttachChanged(isAttached) {
465
+ this._gizmoAttached = isAttached;
466
+ }
467
+ _setGizmoMode(mode) {
468
+ var _a, _b;
469
+ ;
470
+ (_b = (_a = this.scene) === null || _a === void 0 ? void 0 : _a.root) === null || _b === void 0 ? void 0 : _b.setGizmoMode(mode);
471
+ }
386
472
  onSelectedChanged(after, before) {
473
+ var hasSelection = after.length > 0;
387
474
  var alignable = after.length > 1;
388
- this.aligners.forEach(aligner => alignable ? aligner.removeAttribute('disabled') : aligner.setAttribute('disabled', ''));
389
- var movable = after.length === 1;
390
- /* forward, backward 이동은 컴포넌트만 가능하다. */
391
- !movable ? this.forward.setAttribute('disabled', '') : this.forward.removeAttribute('disabled');
392
- !movable ? this.backward.setAttribute('disabled', '') : this.backward.removeAttribute('disabled');
393
- /* 여러 컴포넌트는 front, back 이동이 가능하다. */
394
- !(alignable || movable) ? this.front.setAttribute('disabled', '') : this.front.removeAttribute('disabled');
395
- !(alignable || movable) ? this.back.setAttribute('disabled', '') : this.back.removeAttribute('disabled');
475
+ ['style-copy', 'databind-copy', 'cut', 'copy', 'delete'].forEach(id => this._setDisabled(id, !hasSelection));
476
+ // 정렬 버튼
477
+ this.renderRoot.querySelectorAll('[data-align]').forEach(el => alignable ? el.removeAttribute('disabled') : el.setAttribute('disabled', ''));
478
+ if (this._dimension === '2d') {
479
+ var movable = after.length === 1;
480
+ /* forward, backward 이동은 컴포넌트만 가능하다. */
481
+ this._setDisabled('forward', !movable);
482
+ this._setDisabled('backward', !movable);
483
+ /* 여러 컴포넌트는 front, back 이동이 가능하다. */
484
+ this._setDisabled('front', !(alignable || movable));
485
+ this._setDisabled('back', !(alignable || movable));
486
+ }
487
+ // 분배 버튼
488
+ this.renderRoot.querySelectorAll('[data-distribute]').forEach(el => alignable ? el.removeAttribute('disabled') : el.setAttribute('disabled', ''));
489
+ if (this._dimension === '3d') {
490
+ this.renderRoot.querySelectorAll('.gizmo-btn').forEach(btn => {
491
+ hasSelection ? btn.removeAttribute('disabled') : btn.setAttribute('disabled', '');
492
+ });
493
+ }
396
494
  }
397
495
  onTapUndo() {
398
496
  var _a;
@@ -403,12 +501,20 @@ export class EditToolbar extends LitElement {
403
501
  (_a = this.scene) === null || _a === void 0 ? void 0 : _a.redo();
404
502
  }
405
503
  onStartStylePasteMode() {
406
- var _a;
407
- (_a = this.scene) === null || _a === void 0 ? void 0 : _a.startStylePasteMode();
504
+ var _a, _b, _c;
505
+ if (((_a = this.selected) === null || _a === void 0 ? void 0 : _a.length) !== 1)
506
+ return;
507
+ if (this.selected[0] === ((_b = this.scene) === null || _b === void 0 ? void 0 : _b.root))
508
+ return;
509
+ (_c = this.scene) === null || _c === void 0 ? void 0 : _c.startStylePasteMode();
408
510
  }
409
511
  onStartDatabindPasteMode() {
410
- var _a;
411
- (_a = this.scene) === null || _a === void 0 ? void 0 : _a.startDatabindPasteMode();
512
+ var _a, _b, _c;
513
+ if (((_a = this.selected) === null || _a === void 0 ? void 0 : _a.length) !== 1)
514
+ return;
515
+ if (this.selected[0] === ((_b = this.scene) === null || _b === void 0 ? void 0 : _b.root))
516
+ return;
517
+ (_c = this.scene) === null || _c === void 0 ? void 0 : _c.startDatabindPasteMode();
412
518
  }
413
519
  onTapCut() {
414
520
  var _a;
@@ -432,6 +538,97 @@ export class EditToolbar extends LitElement {
432
538
  var _a;
433
539
  (_a = this.scene) === null || _a === void 0 ? void 0 : _a.remove();
434
540
  }
541
+ /**
542
+ * 멀티 컴포넌트 일괄 붙여넣기 — Ctrl+Shift+V 진입점.
543
+ *
544
+ * 흐름:
545
+ * 1. 클립보드 (this.cliped) 검증 — *단일* 컴포넌트만 허용
546
+ * 2. scene.startBulkCreateMode → AddLayer 가 캔버스 위에 드래그 박스 그림
547
+ * 3. 사용자가 영역 드래그 → Promise resolve({area, template})
548
+ * ESC/취소 → Promise resolve(null)
549
+ * 4. 영역이 결정되면 dialog 오픈
550
+ */
551
+ async onBulkPaste() {
552
+ var _a;
553
+ if (!this.cliped) {
554
+ console.warn('bulk-paste: 먼저 Ctrl+C 로 컴포넌트를 복사하세요.');
555
+ return;
556
+ }
557
+ let parsed;
558
+ try {
559
+ parsed = JSON.parse(this.cliped);
560
+ }
561
+ catch (e) {
562
+ console.warn('bulk-paste: 클립보드 파싱 실패', e);
563
+ return;
564
+ }
565
+ if (!Array.isArray(parsed) || parsed.length === 0)
566
+ return;
567
+ if (parsed.length > 1) {
568
+ console.warn('bulk-paste: 단일 컴포넌트만 일괄 붙여넣기 가능. 다중은 일반 paste 사용.');
569
+ return;
570
+ }
571
+ const template = parsed[0];
572
+ // bulk 모드 임을 시각 구분하기 위해 파란 점선 (일반 add 의 검정과 색만 차별).
573
+ const result = await ((_a = this.scene) === null || _a === void 0 ? void 0 : _a.startBulkCreateMode(template, { boxStrokeStyle: '#2964c2' }));
574
+ if (result) {
575
+ this._openBulkCreateDialog(template, result.area);
576
+ }
577
+ }
578
+ _openBulkCreateDialog(template, initialArea) {
579
+ var _a;
580
+ const dialogEl = document.createElement('bulk-create-dialog');
581
+ dialogEl.template = template;
582
+ dialogEl.initialArea = initialArea;
583
+ // scene 의 모든 id 수집 — indexMap 기반 scene.ids 대신 트리 walk 로 안정성 확보.
584
+ const existingIds = new Set();
585
+ const collectIds = (comp) => {
586
+ var _a, _b, _c;
587
+ if (!comp)
588
+ return;
589
+ const id = (_b = (_a = comp === null || comp === void 0 ? void 0 : comp.state) === null || _a === void 0 ? void 0 : _a.id) !== null && _b !== void 0 ? _b : (_c = comp === null || comp === void 0 ? void 0 : comp.model) === null || _c === void 0 ? void 0 : _c.id;
590
+ if (typeof id === 'string' && id)
591
+ existingIds.add(id);
592
+ const children = comp.components || [];
593
+ for (const c of children)
594
+ collectIds(c);
595
+ };
596
+ const root = (_a = this.scene) === null || _a === void 0 ? void 0 : _a.root;
597
+ if (root)
598
+ collectIds(root);
599
+ dialogEl.existingIds = existingIds;
600
+ dialogEl.onConfirm = (models) => {
601
+ const scene = this.scene;
602
+ if (!scene || !models.length) {
603
+ popup === null || popup === void 0 ? void 0 : popup.close();
604
+ return;
605
+ }
606
+ // cliped 의 template model 자체에서 type-aware bounds 추출. things-scene 의
607
+ // component.bounds (= path bounding box) 와 동일 결과를 model JSON 의
608
+ // type-specific 필드 (width/height, rx/ry, points) 만으로 계산.
609
+ const bounds = deriveBoundsFromModel(template);
610
+ scene.undoableChange(() => {
611
+ for (const m of models) {
612
+ const cx = m.left + m.width / 2;
613
+ const cy = m.top + m.height / 2;
614
+ scene.add(m, {
615
+ left: cx - bounds.width / 2,
616
+ top: cy - bounds.height / 2,
617
+ width: bounds.width,
618
+ height: bounds.height
619
+ });
620
+ }
621
+ });
622
+ popup === null || popup === void 0 ? void 0 : popup.close();
623
+ };
624
+ dialogEl.onCancel = () => {
625
+ popup === null || popup === void 0 ? void 0 : popup.close();
626
+ };
627
+ const popup = OxPopup.open({
628
+ template: dialogEl,
629
+ backdrop: true
630
+ });
631
+ }
435
632
  onTapSelectAll() {
436
633
  var _a;
437
634
  (_a = this.scene) === null || _a === void 0 ? void 0 : _a.select('(child)');
@@ -523,9 +720,15 @@ export class EditToolbar extends LitElement {
523
720
  onTapPreview() {
524
721
  this.dispatchEvent(new CustomEvent('open-preview'));
525
722
  }
723
+ onTapDataBinding() {
724
+ this.dispatchEvent(new CustomEvent('open-data-binding'));
725
+ }
526
726
  onTapDownloadModel() {
527
727
  this.dispatchEvent(new CustomEvent('download-model'));
528
728
  }
729
+ onTapEditModel() {
730
+ this.dispatchEvent(new CustomEvent('edit-model'));
731
+ }
529
732
  onTapDistribute(e) {
530
733
  if (!this.scene)
531
734
  return;
@@ -548,51 +751,9 @@ __decorate([
548
751
  property({ type: Boolean })
549
752
  ], EditToolbar.prototype, "hideProperty", void 0);
550
753
  __decorate([
551
- query('#redo')
552
- ], EditToolbar.prototype, "redo", void 0);
553
- __decorate([
554
- query('#undo')
555
- ], EditToolbar.prototype, "undo", void 0);
556
- __decorate([
557
- query('#fullscreen')
558
- ], EditToolbar.prototype, "fullscreen", void 0);
559
- __decorate([
560
- query('#style-copy')
561
- ], EditToolbar.prototype, "styleCopy", void 0);
562
- __decorate([
563
- query('#databind-copy')
564
- ], EditToolbar.prototype, "databindCopy", void 0);
565
- __decorate([
566
- query('#cut')
567
- ], EditToolbar.prototype, "cut", void 0);
568
- __decorate([
569
- query('#copy')
570
- ], EditToolbar.prototype, "copy", void 0);
571
- __decorate([
572
- query('#paste')
573
- ], EditToolbar.prototype, "paste", void 0);
574
- __decorate([
575
- query('#delete')
576
- ], EditToolbar.prototype, "delete", void 0);
577
- __decorate([
578
- query('#forward')
579
- ], EditToolbar.prototype, "forward", void 0);
580
- __decorate([
581
- query('#backward')
582
- ], EditToolbar.prototype, "backward", void 0);
583
- __decorate([
584
- query('#front')
585
- ], EditToolbar.prototype, "front", void 0);
586
- __decorate([
587
- query('#back')
588
- ], EditToolbar.prototype, "back", void 0);
589
- __decorate([
590
- queryAll('[data-align]')
591
- ], EditToolbar.prototype, "aligners", void 0);
592
- __decorate([
593
- queryAll('[data-zorder]')
594
- ], EditToolbar.prototype, "zorders", void 0);
754
+ state()
755
+ ], EditToolbar.prototype, "_dimension", void 0);
595
756
  __decorate([
596
- queryAll('[data-distribute]')
597
- ], EditToolbar.prototype, "distributes", void 0);
757
+ state()
758
+ ], EditToolbar.prototype, "_gizmoAttached", void 0);
598
759
  //# sourceMappingURL=edit-toolbar.js.map