@things-factory/board-ui 10.0.0-beta.64 → 10.0.0-beta.65
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.
- package/dist-client/pages/board-action-dispatch.d.ts +31 -0
- package/dist-client/pages/board-action-dispatch.js +80 -0
- package/dist-client/pages/board-action-dispatch.js.map +1 -0
- package/dist-client/pages/board-action-dispatch.test.d.ts +1 -0
- package/dist-client/pages/board-action-dispatch.test.js +235 -0
- package/dist-client/pages/board-action-dispatch.test.js.map +1 -0
- package/dist-client/pages/board-edit-dispatch.d.ts +74 -0
- package/dist-client/pages/board-edit-dispatch.js +299 -0
- package/dist-client/pages/board-edit-dispatch.js.map +1 -0
- package/dist-client/pages/board-edit-dispatch.test.d.ts +1 -0
- package/dist-client/pages/board-edit-dispatch.test.js +858 -0
- package/dist-client/pages/board-edit-dispatch.test.js.map +1 -0
- package/dist-client/pages/board-modeller-page.d.ts +40 -28
- package/dist-client/pages/board-modeller-page.js +206 -186
- package/dist-client/pages/board-modeller-page.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host 측 BoardActionOp dispatcher — AI 가 지시한 ephemeral scene action 을
|
|
3
|
+
* things-scene API 로 실행.
|
|
4
|
+
*
|
|
5
|
+
* 별개 채널 — board-edit-patch 와 달리 model 변경 X, undo 영향 X, dirty flag 무관.
|
|
6
|
+
* pure function 형태 (스크롤되는 customElement 와 분리) — Lit 외부에서 단위 테스트 가능.
|
|
7
|
+
*/
|
|
8
|
+
import type { BoardActionOp } from '@things-factory/board-ai';
|
|
9
|
+
/**
|
|
10
|
+
* AI 의 BoardEditOp / BoardActionOp 가 지정한 대상 (id 또는 refid) 으로
|
|
11
|
+
* things-scene component 검색.
|
|
12
|
+
*
|
|
13
|
+
* id (사용자/AI 가 명시적으로 설정한 string identifier) 와
|
|
14
|
+
* refid (things-scene 이 모든 컴포넌트에 자동 발급하는 numeric identifier) 는
|
|
15
|
+
* 서로 별개 개념. 이 함수는 호스트 측에서 둘 중 어느 쪽으로든 lookup 가능하게 통합.
|
|
16
|
+
* refid 가 universal 이므로 우선.
|
|
17
|
+
*/
|
|
18
|
+
export declare function findSceneComponent(scene: any, target: {
|
|
19
|
+
id?: string;
|
|
20
|
+
refid?: number;
|
|
21
|
+
}): any;
|
|
22
|
+
/**
|
|
23
|
+
* 단일 BoardActionOp 를 things-scene 에 적용.
|
|
24
|
+
*
|
|
25
|
+
* 호스트는 actions 배열을 순회하며 매 op 마다 호출. 실패 (잘못된 action /
|
|
26
|
+
* 누락된 컴포넌트 / API 예외) 시 `false` 반환 — 호스트가 warn 처리.
|
|
27
|
+
*
|
|
28
|
+
* setSceneMode 의 경우 호스트 측 reactive `mode` property 동기는 별도 (return
|
|
29
|
+
* 후 호스트가 scene.mode 재읽기). things-scene 의 mode: edit=1, view=0.
|
|
30
|
+
*/
|
|
31
|
+
export declare function dispatchBoardAction(scene: any, action: BoardActionOp): boolean;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI 의 BoardEditOp / BoardActionOp 가 지정한 대상 (id 또는 refid) 으로
|
|
3
|
+
* things-scene component 검색.
|
|
4
|
+
*
|
|
5
|
+
* id (사용자/AI 가 명시적으로 설정한 string identifier) 와
|
|
6
|
+
* refid (things-scene 이 모든 컴포넌트에 자동 발급하는 numeric identifier) 는
|
|
7
|
+
* 서로 별개 개념. 이 함수는 호스트 측에서 둘 중 어느 쪽으로든 lookup 가능하게 통합.
|
|
8
|
+
* refid 가 universal 이므로 우선.
|
|
9
|
+
*/
|
|
10
|
+
export function findSceneComponent(scene, target) {
|
|
11
|
+
if (!scene)
|
|
12
|
+
return null;
|
|
13
|
+
if (typeof target.refid === 'number') {
|
|
14
|
+
const byRefid = scene.rootContainer?.refidIndexMap?.get(target.refid);
|
|
15
|
+
if (byRefid)
|
|
16
|
+
return byRefid;
|
|
17
|
+
}
|
|
18
|
+
if (typeof target.id === 'string' && target.id.length > 0) {
|
|
19
|
+
return scene.findById?.(target.id) ?? null;
|
|
20
|
+
}
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* 단일 BoardActionOp 를 things-scene 에 적용.
|
|
25
|
+
*
|
|
26
|
+
* 호스트는 actions 배열을 순회하며 매 op 마다 호출. 실패 (잘못된 action /
|
|
27
|
+
* 누락된 컴포넌트 / API 예외) 시 `false` 반환 — 호스트가 warn 처리.
|
|
28
|
+
*
|
|
29
|
+
* setSceneMode 의 경우 호스트 측 reactive `mode` property 동기는 별도 (return
|
|
30
|
+
* 후 호스트가 scene.mode 재읽기). things-scene 의 mode: edit=1, view=0.
|
|
31
|
+
*/
|
|
32
|
+
export function dispatchBoardAction(scene, action) {
|
|
33
|
+
if (!scene || !action)
|
|
34
|
+
return false;
|
|
35
|
+
switch (action.action) {
|
|
36
|
+
case 'selectComponents': {
|
|
37
|
+
const refids = Array.isArray(action.refids) ? action.refids : [];
|
|
38
|
+
const targets = refids
|
|
39
|
+
.map(r => findSceneComponent(scene, { refid: r }))
|
|
40
|
+
.filter((c) => c);
|
|
41
|
+
scene.selected = targets;
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
case 'centerToComponent': {
|
|
45
|
+
const target = findSceneComponent(scene, { refid: action.refid });
|
|
46
|
+
if (!target)
|
|
47
|
+
return false;
|
|
48
|
+
const animated = action.animated !== false;
|
|
49
|
+
scene.centerTo(target, animated);
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
case 'fitToView': {
|
|
53
|
+
const mode = action.mode ?? 'fit';
|
|
54
|
+
scene.fit(mode);
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
case 'setSceneMode': {
|
|
58
|
+
scene.mode = action.mode === 'edit' ? 1 : 0;
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
case 'highlightComponents': {
|
|
62
|
+
// things-scene 의 highlightSearchResults API 위임 — 2D outline + 3D outline
|
|
63
|
+
// 모두 자동 처리 (operato-board 의 3D first-class UX).
|
|
64
|
+
const refids = Array.isArray(action.refids) ? action.refids : [];
|
|
65
|
+
const targets = refids
|
|
66
|
+
.map(r => findSceneComponent(scene, { refid: r }))
|
|
67
|
+
.filter((c) => c);
|
|
68
|
+
if (typeof scene.highlightSearchResults === 'function') {
|
|
69
|
+
scene.highlightSearchResults(targets);
|
|
70
|
+
}
|
|
71
|
+
// 변경된 시각 반영
|
|
72
|
+
if (typeof scene.invalidate === 'function')
|
|
73
|
+
scene.invalidate();
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
default:
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=board-action-dispatch.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"board-action-dispatch.js","sourceRoot":"","sources":["../../client/pages/board-action-dispatch.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Host 측 BoardActionOp dispatcher — AI 가 지시한 ephemeral scene action 을\n * things-scene API 로 실행.\n *\n * 별개 채널 — board-edit-patch 와 달리 model 변경 X, undo 영향 X, dirty flag 무관.\n * pure function 형태 (스크롤되는 customElement 와 분리) — Lit 외부에서 단위 테스트 가능.\n */\nimport type { BoardActionOp } from '@things-factory/board-ai'\n\n/**\n * AI 의 BoardEditOp / BoardActionOp 가 지정한 대상 (id 또는 refid) 으로\n * things-scene component 검색.\n *\n * id (사용자/AI 가 명시적으로 설정한 string identifier) 와\n * refid (things-scene 이 모든 컴포넌트에 자동 발급하는 numeric identifier) 는\n * 서로 별개 개념. 이 함수는 호스트 측에서 둘 중 어느 쪽으로든 lookup 가능하게 통합.\n * refid 가 universal 이므로 우선.\n */\nexport function findSceneComponent(\n scene: any,\n target: { id?: string; refid?: number }\n): any {\n if (!scene) return null\n if (typeof target.refid === 'number') {\n const byRefid = scene.rootContainer?.refidIndexMap?.get(target.refid)\n if (byRefid) return byRefid\n }\n if (typeof target.id === 'string' && target.id.length > 0) {\n return scene.findById?.(target.id) ?? null\n }\n return null\n}\n\n/**\n * 단일 BoardActionOp 를 things-scene 에 적용.\n *\n * 호스트는 actions 배열을 순회하며 매 op 마다 호출. 실패 (잘못된 action /\n * 누락된 컴포넌트 / API 예외) 시 `false` 반환 — 호스트가 warn 처리.\n *\n * setSceneMode 의 경우 호스트 측 reactive `mode` property 동기는 별도 (return\n * 후 호스트가 scene.mode 재읽기). things-scene 의 mode: edit=1, view=0.\n */\nexport function dispatchBoardAction(scene: any, action: BoardActionOp): boolean {\n if (!scene || !action) return false\n switch (action.action) {\n case 'selectComponents': {\n const refids = Array.isArray(action.refids) ? action.refids : []\n const targets = refids\n .map(r => findSceneComponent(scene, { refid: r }))\n .filter((c: any) => c)\n scene.selected = targets\n return true\n }\n case 'centerToComponent': {\n const target = findSceneComponent(scene, { refid: action.refid })\n if (!target) return false\n const animated = action.animated !== false\n scene.centerTo(target, animated)\n return true\n }\n case 'fitToView': {\n const mode = action.mode ?? 'fit'\n scene.fit(mode)\n return true\n }\n case 'setSceneMode': {\n scene.mode = action.mode === 'edit' ? 1 : 0\n return true\n }\n case 'highlightComponents': {\n // things-scene 의 highlightSearchResults API 위임 — 2D outline + 3D outline\n // 모두 자동 처리 (operato-board 의 3D first-class UX).\n const refids = Array.isArray(action.refids) ? action.refids : []\n const targets = refids\n .map(r => findSceneComponent(scene, { refid: r }))\n .filter((c: any) => c)\n if (typeof scene.highlightSearchResults === 'function') {\n scene.highlightSearchResults(targets)\n }\n // 변경된 시각 반영\n if (typeof scene.invalidate === 'function') scene.invalidate()\n return true\n }\n default:\n return false\n }\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* board-action-dispatch.ts 의 pure helper 회귀 방지.
|
|
3
|
+
*
|
|
4
|
+
* 핵심 보호:
|
|
5
|
+
* - 가짜 scene 객체에 BoardActionOp 던져 실제 things-scene API 호출 인자 검증
|
|
6
|
+
* - findSceneComponent 의 refid 우선 / id fallback 동작
|
|
7
|
+
* - 잘못된 action / 누락 컴포넌트 시 false 반환 (silent fail 차단)
|
|
8
|
+
*
|
|
9
|
+
* AI 가 액션을 만들고 호스트가 things-scene 으로 정확히 매핑하지 않으면 사용자에게는
|
|
10
|
+
* "AI 응답은 왔는데 화면 그대로" 회귀로 보임 — 이 layer 가 그 사각지대.
|
|
11
|
+
*/
|
|
12
|
+
import { dispatchBoardAction, findSceneComponent } from './board-action-dispatch';
|
|
13
|
+
// ── Mock scene factory ──────────────────────────────────────────────
|
|
14
|
+
function makeMockScene(components = []) {
|
|
15
|
+
const refidIndexMap = new Map();
|
|
16
|
+
for (const c of components)
|
|
17
|
+
refidIndexMap.set(c.refid, { ...c, model: { ...c } });
|
|
18
|
+
const calls = [];
|
|
19
|
+
return {
|
|
20
|
+
scene: {
|
|
21
|
+
mode: 1, // edit
|
|
22
|
+
selected: [],
|
|
23
|
+
rootContainer: { refidIndexMap },
|
|
24
|
+
findById: (id) => {
|
|
25
|
+
for (const c of refidIndexMap.values()) {
|
|
26
|
+
if (c.id === id)
|
|
27
|
+
return c;
|
|
28
|
+
}
|
|
29
|
+
return null;
|
|
30
|
+
},
|
|
31
|
+
centerTo(target, animated) {
|
|
32
|
+
calls.push({ method: 'centerTo', target, animated });
|
|
33
|
+
},
|
|
34
|
+
fit(mode) {
|
|
35
|
+
calls.push({ method: 'fit', mode });
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
calls,
|
|
39
|
+
refidIndexMap
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
describe('findSceneComponent', () => {
|
|
43
|
+
test('refid 매칭 — refidIndexMap 에서 lookup', () => {
|
|
44
|
+
const { scene } = makeMockScene([{ refid: 35, id: 'motor-1' }]);
|
|
45
|
+
const r = findSceneComponent(scene, { refid: 35 });
|
|
46
|
+
expect(r?.refid).toBe(35);
|
|
47
|
+
expect(r?.id).toBe('motor-1');
|
|
48
|
+
});
|
|
49
|
+
test('refid 가 없는데 매칭 시도 → null', () => {
|
|
50
|
+
const { scene } = makeMockScene([{ refid: 35 }]);
|
|
51
|
+
expect(findSceneComponent(scene, { refid: 999 })).toBeNull();
|
|
52
|
+
});
|
|
53
|
+
test('id 매칭 — refid 미지정, findById fallback', () => {
|
|
54
|
+
const { scene } = makeMockScene([{ refid: 1, id: 'motor-1' }]);
|
|
55
|
+
const r = findSceneComponent(scene, { id: 'motor-1' });
|
|
56
|
+
expect(r?.id).toBe('motor-1');
|
|
57
|
+
});
|
|
58
|
+
test('refid 우선 — refid + id 둘 다 주어져도 refid 가 매칭되면 id 무시', () => {
|
|
59
|
+
const { scene } = makeMockScene([
|
|
60
|
+
{ refid: 35, id: 'A' },
|
|
61
|
+
{ refid: 99, id: 'B' }
|
|
62
|
+
]);
|
|
63
|
+
const r = findSceneComponent(scene, { refid: 35, id: 'B' });
|
|
64
|
+
expect(r?.refid).toBe(35); // refid 우선
|
|
65
|
+
});
|
|
66
|
+
test('scene 이 null → null', () => {
|
|
67
|
+
expect(findSceneComponent(null, { refid: 1 })).toBeNull();
|
|
68
|
+
});
|
|
69
|
+
test('rootContainer / refidIndexMap 없을 때도 안전', () => {
|
|
70
|
+
expect(findSceneComponent({}, { refid: 1 })).toBeNull();
|
|
71
|
+
expect(findSceneComponent({ rootContainer: {} }, { refid: 1 })).toBeNull();
|
|
72
|
+
});
|
|
73
|
+
test('findById 없을 때 id lookup → null (no throw)', () => {
|
|
74
|
+
expect(findSceneComponent({ rootContainer: {} }, { id: 'X' })).toBeNull();
|
|
75
|
+
});
|
|
76
|
+
test('빈 target → null', () => {
|
|
77
|
+
const { scene } = makeMockScene([{ refid: 1 }]);
|
|
78
|
+
expect(findSceneComponent(scene, {})).toBeNull();
|
|
79
|
+
});
|
|
80
|
+
test('id 가 빈 문자열이면 fallback 도 안 함', () => {
|
|
81
|
+
const { scene } = makeMockScene([{ refid: 1, id: '' }]);
|
|
82
|
+
expect(findSceneComponent(scene, { id: '' })).toBeNull();
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
describe('dispatchBoardAction — selectComponents', () => {
|
|
86
|
+
test('refid 들 → scene.selected 에 매칭된 컴포넌트 set', () => {
|
|
87
|
+
const { scene } = makeMockScene([
|
|
88
|
+
{ refid: 1 },
|
|
89
|
+
{ refid: 2 },
|
|
90
|
+
{ refid: 3 }
|
|
91
|
+
]);
|
|
92
|
+
const ok = dispatchBoardAction(scene, { action: 'selectComponents', refids: [1, 3] });
|
|
93
|
+
expect(ok).toBe(true);
|
|
94
|
+
expect(scene.selected).toHaveLength(2);
|
|
95
|
+
expect(scene.selected.map((c) => c.refid).sort()).toEqual([1, 3]);
|
|
96
|
+
});
|
|
97
|
+
test('빈 배열 → scene.selected = [] (deselect)', () => {
|
|
98
|
+
const { scene } = makeMockScene([{ refid: 1 }]);
|
|
99
|
+
scene.selected = [{ refid: 1 }];
|
|
100
|
+
const ok = dispatchBoardAction(scene, { action: 'selectComponents', refids: [] });
|
|
101
|
+
expect(ok).toBe(true);
|
|
102
|
+
expect(scene.selected).toEqual([]);
|
|
103
|
+
});
|
|
104
|
+
test('존재하지 않는 refid 는 결과에서 빠지고 나머지는 적용', () => {
|
|
105
|
+
const { scene } = makeMockScene([{ refid: 1 }, { refid: 2 }]);
|
|
106
|
+
dispatchBoardAction(scene, { action: 'selectComponents', refids: [1, 999, 2] });
|
|
107
|
+
expect(scene.selected).toHaveLength(2);
|
|
108
|
+
expect(scene.selected.map((c) => c.refid).sort()).toEqual([1, 2]);
|
|
109
|
+
});
|
|
110
|
+
test('전부 존재하지 않으면 빈 배열로 set (silent — selected 가 비어짐)', () => {
|
|
111
|
+
const { scene } = makeMockScene([{ refid: 1 }]);
|
|
112
|
+
scene.selected = [{ refid: 1 }]; // 사전 selection
|
|
113
|
+
dispatchBoardAction(scene, { action: 'selectComponents', refids: [999] });
|
|
114
|
+
expect(scene.selected).toEqual([]); // 매칭 없음 → deselect
|
|
115
|
+
});
|
|
116
|
+
test('refids 가 배열 아니면 빈 배열로 처리', () => {
|
|
117
|
+
const { scene } = makeMockScene([{ refid: 1 }]);
|
|
118
|
+
dispatchBoardAction(scene, { action: 'selectComponents', refids: 'not-array' });
|
|
119
|
+
expect(scene.selected).toEqual([]);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
describe('dispatchBoardAction — centerToComponent', () => {
|
|
123
|
+
test('정상 호출 — scene.centerTo(target, animated=true) default', () => {
|
|
124
|
+
const { scene, calls } = makeMockScene([{ refid: 7 }]);
|
|
125
|
+
const ok = dispatchBoardAction(scene, { action: 'centerToComponent', refid: 7 });
|
|
126
|
+
expect(ok).toBe(true);
|
|
127
|
+
expect(calls).toHaveLength(1);
|
|
128
|
+
expect(calls[0].method).toBe('centerTo');
|
|
129
|
+
expect(calls[0].target.refid).toBe(7);
|
|
130
|
+
expect(calls[0].animated).toBe(true); // default
|
|
131
|
+
});
|
|
132
|
+
test('animated: false 명시', () => {
|
|
133
|
+
const { scene, calls } = makeMockScene([{ refid: 7 }]);
|
|
134
|
+
dispatchBoardAction(scene, { action: 'centerToComponent', refid: 7, animated: false });
|
|
135
|
+
expect(calls[0].animated).toBe(false);
|
|
136
|
+
});
|
|
137
|
+
test('animated: true 명시', () => {
|
|
138
|
+
const { scene, calls } = makeMockScene([{ refid: 7 }]);
|
|
139
|
+
dispatchBoardAction(scene, { action: 'centerToComponent', refid: 7, animated: true });
|
|
140
|
+
expect(calls[0].animated).toBe(true);
|
|
141
|
+
});
|
|
142
|
+
test('존재하지 않는 refid → false 반환, scene.centerTo 미호출', () => {
|
|
143
|
+
const { scene, calls } = makeMockScene([{ refid: 7 }]);
|
|
144
|
+
const ok = dispatchBoardAction(scene, { action: 'centerToComponent', refid: 999 });
|
|
145
|
+
expect(ok).toBe(false);
|
|
146
|
+
expect(calls).toHaveLength(0);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
describe('dispatchBoardAction — fitToView', () => {
|
|
150
|
+
test('mode 미지정 → scene.fit("fit") default', () => {
|
|
151
|
+
const { scene, calls } = makeMockScene();
|
|
152
|
+
const ok = dispatchBoardAction(scene, { action: 'fitToView' });
|
|
153
|
+
expect(ok).toBe(true);
|
|
154
|
+
expect(calls).toEqual([{ method: 'fit', mode: 'fit' }]);
|
|
155
|
+
});
|
|
156
|
+
test('mode 명시 — fit/ratio/width/height 전달', () => {
|
|
157
|
+
for (const mode of ['fit', 'ratio', 'width', 'height']) {
|
|
158
|
+
const { scene, calls } = makeMockScene();
|
|
159
|
+
dispatchBoardAction(scene, { action: 'fitToView', mode });
|
|
160
|
+
expect(calls[0].mode).toBe(mode);
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
describe('dispatchBoardAction — setSceneMode', () => {
|
|
165
|
+
test('view → scene.mode = 0', () => {
|
|
166
|
+
const { scene } = makeMockScene();
|
|
167
|
+
scene.mode = 1; // edit
|
|
168
|
+
const ok = dispatchBoardAction(scene, { action: 'setSceneMode', mode: 'view' });
|
|
169
|
+
expect(ok).toBe(true);
|
|
170
|
+
expect(scene.mode).toBe(0);
|
|
171
|
+
});
|
|
172
|
+
test('edit → scene.mode = 1', () => {
|
|
173
|
+
const { scene } = makeMockScene();
|
|
174
|
+
scene.mode = 0; // view
|
|
175
|
+
dispatchBoardAction(scene, { action: 'setSceneMode', mode: 'edit' });
|
|
176
|
+
expect(scene.mode).toBe(1);
|
|
177
|
+
});
|
|
178
|
+
test('잘못된 mode 값 → view 로 폴백 (edit 이 아니면 0)', () => {
|
|
179
|
+
// 정책: "edit" 외 모든 입력은 view (0). server-side schema 가 enum 으로 1차 차단하지만
|
|
180
|
+
// host 도 방어 — 잘못된 입력에 대해 throw 하지 않고 안전한 default 로.
|
|
181
|
+
const { scene } = makeMockScene();
|
|
182
|
+
scene.mode = 1;
|
|
183
|
+
dispatchBoardAction(scene, { action: 'setSceneMode', mode: 'something' });
|
|
184
|
+
expect(scene.mode).toBe(0);
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
describe('dispatchBoardAction — 방어', () => {
|
|
188
|
+
test('scene 이 null → false (no throw)', () => {
|
|
189
|
+
expect(dispatchBoardAction(null, { action: 'fitToView' })).toBe(false);
|
|
190
|
+
});
|
|
191
|
+
test('action 이 null → false', () => {
|
|
192
|
+
const { scene } = makeMockScene();
|
|
193
|
+
expect(dispatchBoardAction(scene, null)).toBe(false);
|
|
194
|
+
});
|
|
195
|
+
test('알 수 없는 action 종류 → false', () => {
|
|
196
|
+
const { scene } = makeMockScene();
|
|
197
|
+
expect(dispatchBoardAction(scene, { action: 'unknownThing' })).toBe(false);
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
describe('통합 — 다중 action 시퀀스 (호스트 onBoardActionExecute 패턴)', () => {
|
|
201
|
+
test('centerTo + select + fit 순서대로 적용, 누적 호출 검증', () => {
|
|
202
|
+
const { scene, calls } = makeMockScene([{ refid: 7 }]);
|
|
203
|
+
const actions = [
|
|
204
|
+
{ action: 'centerToComponent', refid: 7 },
|
|
205
|
+
{ action: 'selectComponents', refids: [7] },
|
|
206
|
+
{ action: 'fitToView', mode: 'width' }
|
|
207
|
+
];
|
|
208
|
+
for (const a of actions)
|
|
209
|
+
dispatchBoardAction(scene, a);
|
|
210
|
+
// centerTo 와 fit 만 calls 에 누적 (selected 는 직접 할당)
|
|
211
|
+
expect(calls).toHaveLength(2);
|
|
212
|
+
expect(calls[0].method).toBe('centerTo');
|
|
213
|
+
expect(calls[1].method).toBe('fit');
|
|
214
|
+
expect(calls[1].mode).toBe('width');
|
|
215
|
+
expect(scene.selected).toHaveLength(1);
|
|
216
|
+
expect(scene.selected[0].refid).toBe(7);
|
|
217
|
+
});
|
|
218
|
+
test('select 후 setSceneMode — 두 효과 모두 반영', () => {
|
|
219
|
+
const { scene } = makeMockScene([{ refid: 1 }]);
|
|
220
|
+
dispatchBoardAction(scene, { action: 'selectComponents', refids: [1] });
|
|
221
|
+
dispatchBoardAction(scene, { action: 'setSceneMode', mode: 'view' });
|
|
222
|
+
expect(scene.selected).toHaveLength(1);
|
|
223
|
+
expect(scene.mode).toBe(0);
|
|
224
|
+
});
|
|
225
|
+
test('일부 action 실패 (없는 refid) 해도 후속 action 은 정상 진행', () => {
|
|
226
|
+
const { scene, calls } = makeMockScene([{ refid: 1 }]);
|
|
227
|
+
const ok1 = dispatchBoardAction(scene, { action: 'centerToComponent', refid: 999 });
|
|
228
|
+
const ok2 = dispatchBoardAction(scene, { action: 'fitToView' });
|
|
229
|
+
expect(ok1).toBe(false);
|
|
230
|
+
expect(ok2).toBe(true);
|
|
231
|
+
expect(calls).toHaveLength(1); // fit 만 호출됨
|
|
232
|
+
expect(calls[0].method).toBe('fit');
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
//# sourceMappingURL=board-action-dispatch.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"board-action-dispatch.test.js","sourceRoot":"","sources":["../../client/pages/board-action-dispatch.test.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * board-action-dispatch.ts 의 pure helper 회귀 방지.\n *\n * 핵심 보호:\n * - 가짜 scene 객체에 BoardActionOp 던져 실제 things-scene API 호출 인자 검증\n * - findSceneComponent 의 refid 우선 / id fallback 동작\n * - 잘못된 action / 누락 컴포넌트 시 false 반환 (silent fail 차단)\n *\n * AI 가 액션을 만들고 호스트가 things-scene 으로 정확히 매핑하지 않으면 사용자에게는\n * \"AI 응답은 왔는데 화면 그대로\" 회귀로 보임 — 이 layer 가 그 사각지대.\n */\nimport { dispatchBoardAction, findSceneComponent } from './board-action-dispatch'\nimport type { BoardActionOp } from '@things-factory/board-ai'\n\n// ── Mock scene factory ──────────────────────────────────────────────\nfunction makeMockScene(components: Array<{ refid: number; id?: string }> = []) {\n const refidIndexMap = new Map<number, any>()\n for (const c of components) refidIndexMap.set(c.refid, { ...c, model: { ...c } })\n\n const calls: any[] = []\n return {\n scene: {\n mode: 1, // edit\n selected: [] as any[],\n rootContainer: { refidIndexMap },\n findById: (id: string) => {\n for (const c of refidIndexMap.values()) {\n if (c.id === id) return c\n }\n return null\n },\n centerTo(target: any, animated: boolean) {\n calls.push({ method: 'centerTo', target, animated })\n },\n fit(mode: string) {\n calls.push({ method: 'fit', mode })\n }\n } as any,\n calls,\n refidIndexMap\n }\n}\n\ndescribe('findSceneComponent', () => {\n test('refid 매칭 — refidIndexMap 에서 lookup', () => {\n const { scene } = makeMockScene([{ refid: 35, id: 'motor-1' }])\n const r = findSceneComponent(scene, { refid: 35 })\n expect(r?.refid).toBe(35)\n expect(r?.id).toBe('motor-1')\n })\n\n test('refid 가 없는데 매칭 시도 → null', () => {\n const { scene } = makeMockScene([{ refid: 35 }])\n expect(findSceneComponent(scene, { refid: 999 })).toBeNull()\n })\n\n test('id 매칭 — refid 미지정, findById fallback', () => {\n const { scene } = makeMockScene([{ refid: 1, id: 'motor-1' }])\n const r = findSceneComponent(scene, { id: 'motor-1' })\n expect(r?.id).toBe('motor-1')\n })\n\n test('refid 우선 — refid + id 둘 다 주어져도 refid 가 매칭되면 id 무시', () => {\n const { scene } = makeMockScene([\n { refid: 35, id: 'A' },\n { refid: 99, id: 'B' }\n ])\n const r = findSceneComponent(scene, { refid: 35, id: 'B' })\n expect(r?.refid).toBe(35) // refid 우선\n })\n\n test('scene 이 null → null', () => {\n expect(findSceneComponent(null, { refid: 1 })).toBeNull()\n })\n\n test('rootContainer / refidIndexMap 없을 때도 안전', () => {\n expect(findSceneComponent({}, { refid: 1 })).toBeNull()\n expect(findSceneComponent({ rootContainer: {} }, { refid: 1 })).toBeNull()\n })\n\n test('findById 없을 때 id lookup → null (no throw)', () => {\n expect(findSceneComponent({ rootContainer: {} }, { id: 'X' })).toBeNull()\n })\n\n test('빈 target → null', () => {\n const { scene } = makeMockScene([{ refid: 1 }])\n expect(findSceneComponent(scene, {})).toBeNull()\n })\n\n test('id 가 빈 문자열이면 fallback 도 안 함', () => {\n const { scene } = makeMockScene([{ refid: 1, id: '' }])\n expect(findSceneComponent(scene, { id: '' })).toBeNull()\n })\n})\n\ndescribe('dispatchBoardAction — selectComponents', () => {\n test('refid 들 → scene.selected 에 매칭된 컴포넌트 set', () => {\n const { scene } = makeMockScene([\n { refid: 1 },\n { refid: 2 },\n { refid: 3 }\n ])\n const ok = dispatchBoardAction(scene, { action: 'selectComponents', refids: [1, 3] })\n expect(ok).toBe(true)\n expect(scene.selected).toHaveLength(2)\n expect(scene.selected.map((c: any) => c.refid).sort()).toEqual([1, 3])\n })\n\n test('빈 배열 → scene.selected = [] (deselect)', () => {\n const { scene } = makeMockScene([{ refid: 1 }])\n scene.selected = [{ refid: 1 }]\n const ok = dispatchBoardAction(scene, { action: 'selectComponents', refids: [] })\n expect(ok).toBe(true)\n expect(scene.selected).toEqual([])\n })\n\n test('존재하지 않는 refid 는 결과에서 빠지고 나머지는 적용', () => {\n const { scene } = makeMockScene([{ refid: 1 }, { refid: 2 }])\n dispatchBoardAction(scene, { action: 'selectComponents', refids: [1, 999, 2] })\n expect(scene.selected).toHaveLength(2)\n expect(scene.selected.map((c: any) => c.refid).sort()).toEqual([1, 2])\n })\n\n test('전부 존재하지 않으면 빈 배열로 set (silent — selected 가 비어짐)', () => {\n const { scene } = makeMockScene([{ refid: 1 }])\n scene.selected = [{ refid: 1 }] // 사전 selection\n dispatchBoardAction(scene, { action: 'selectComponents', refids: [999] })\n expect(scene.selected).toEqual([]) // 매칭 없음 → deselect\n })\n\n test('refids 가 배열 아니면 빈 배열로 처리', () => {\n const { scene } = makeMockScene([{ refid: 1 }])\n dispatchBoardAction(scene, { action: 'selectComponents', refids: 'not-array' as any })\n expect(scene.selected).toEqual([])\n })\n})\n\ndescribe('dispatchBoardAction — centerToComponent', () => {\n test('정상 호출 — scene.centerTo(target, animated=true) default', () => {\n const { scene, calls } = makeMockScene([{ refid: 7 }])\n const ok = dispatchBoardAction(scene, { action: 'centerToComponent', refid: 7 })\n expect(ok).toBe(true)\n expect(calls).toHaveLength(1)\n expect(calls[0].method).toBe('centerTo')\n expect(calls[0].target.refid).toBe(7)\n expect(calls[0].animated).toBe(true) // default\n })\n\n test('animated: false 명시', () => {\n const { scene, calls } = makeMockScene([{ refid: 7 }])\n dispatchBoardAction(scene, { action: 'centerToComponent', refid: 7, animated: false })\n expect(calls[0].animated).toBe(false)\n })\n\n test('animated: true 명시', () => {\n const { scene, calls } = makeMockScene([{ refid: 7 }])\n dispatchBoardAction(scene, { action: 'centerToComponent', refid: 7, animated: true })\n expect(calls[0].animated).toBe(true)\n })\n\n test('존재하지 않는 refid → false 반환, scene.centerTo 미호출', () => {\n const { scene, calls } = makeMockScene([{ refid: 7 }])\n const ok = dispatchBoardAction(scene, { action: 'centerToComponent', refid: 999 })\n expect(ok).toBe(false)\n expect(calls).toHaveLength(0)\n })\n})\n\ndescribe('dispatchBoardAction — fitToView', () => {\n test('mode 미지정 → scene.fit(\"fit\") default', () => {\n const { scene, calls } = makeMockScene()\n const ok = dispatchBoardAction(scene, { action: 'fitToView' })\n expect(ok).toBe(true)\n expect(calls).toEqual([{ method: 'fit', mode: 'fit' }])\n })\n\n test('mode 명시 — fit/ratio/width/height 전달', () => {\n for (const mode of ['fit', 'ratio', 'width', 'height'] as const) {\n const { scene, calls } = makeMockScene()\n dispatchBoardAction(scene, { action: 'fitToView', mode })\n expect(calls[0].mode).toBe(mode)\n }\n })\n})\n\ndescribe('dispatchBoardAction — setSceneMode', () => {\n test('view → scene.mode = 0', () => {\n const { scene } = makeMockScene()\n scene.mode = 1 // edit\n const ok = dispatchBoardAction(scene, { action: 'setSceneMode', mode: 'view' })\n expect(ok).toBe(true)\n expect(scene.mode).toBe(0)\n })\n\n test('edit → scene.mode = 1', () => {\n const { scene } = makeMockScene()\n scene.mode = 0 // view\n dispatchBoardAction(scene, { action: 'setSceneMode', mode: 'edit' })\n expect(scene.mode).toBe(1)\n })\n\n test('잘못된 mode 값 → view 로 폴백 (edit 이 아니면 0)', () => {\n // 정책: \"edit\" 외 모든 입력은 view (0). server-side schema 가 enum 으로 1차 차단하지만\n // host 도 방어 — 잘못된 입력에 대해 throw 하지 않고 안전한 default 로.\n const { scene } = makeMockScene()\n scene.mode = 1\n dispatchBoardAction(scene, { action: 'setSceneMode', mode: 'something' as any })\n expect(scene.mode).toBe(0)\n })\n})\n\ndescribe('dispatchBoardAction — 방어', () => {\n test('scene 이 null → false (no throw)', () => {\n expect(dispatchBoardAction(null, { action: 'fitToView' })).toBe(false)\n })\n\n test('action 이 null → false', () => {\n const { scene } = makeMockScene()\n expect(dispatchBoardAction(scene, null as any)).toBe(false)\n })\n\n test('알 수 없는 action 종류 → false', () => {\n const { scene } = makeMockScene()\n expect(\n dispatchBoardAction(scene, { action: 'unknownThing' } as any)\n ).toBe(false)\n })\n})\n\ndescribe('통합 — 다중 action 시퀀스 (호스트 onBoardActionExecute 패턴)', () => {\n test('centerTo + select + fit 순서대로 적용, 누적 호출 검증', () => {\n const { scene, calls } = makeMockScene([{ refid: 7 }])\n const actions: BoardActionOp[] = [\n { action: 'centerToComponent', refid: 7 },\n { action: 'selectComponents', refids: [7] },\n { action: 'fitToView', mode: 'width' }\n ]\n for (const a of actions) dispatchBoardAction(scene, a)\n\n // centerTo 와 fit 만 calls 에 누적 (selected 는 직접 할당)\n expect(calls).toHaveLength(2)\n expect(calls[0].method).toBe('centerTo')\n expect(calls[1].method).toBe('fit')\n expect(calls[1].mode).toBe('width')\n expect(scene.selected).toHaveLength(1)\n expect(scene.selected[0].refid).toBe(7)\n })\n\n test('select 후 setSceneMode — 두 효과 모두 반영', () => {\n const { scene } = makeMockScene([{ refid: 1 }])\n dispatchBoardAction(scene, { action: 'selectComponents', refids: [1] })\n dispatchBoardAction(scene, { action: 'setSceneMode', mode: 'view' })\n expect(scene.selected).toHaveLength(1)\n expect(scene.mode).toBe(0)\n })\n\n test('일부 action 실패 (없는 refid) 해도 후속 action 은 정상 진행', () => {\n const { scene, calls } = makeMockScene([{ refid: 1 }])\n const ok1 = dispatchBoardAction(scene, { action: 'centerToComponent', refid: 999 })\n const ok2 = dispatchBoardAction(scene, { action: 'fitToView' })\n expect(ok1).toBe(false)\n expect(ok2).toBe(true)\n expect(calls).toHaveLength(1) // fit 만 호출됨\n expect(calls[0].method).toBe('fit')\n })\n})\n"]}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host 측 BoardEditOp dispatcher — AI 의 model 변경 op 를 things-scene 에 in-place
|
|
3
|
+
* 적용 + revert 용 inverse op 캡처.
|
|
4
|
+
*
|
|
5
|
+
* 핵심:
|
|
6
|
+
* - scene 의 in-place mutation API (scene.add / target.set / scene.remove 등)
|
|
7
|
+
* 로 적용 → commander 가 자동 snapshot push → CMD+Z / dirty / selection 보존.
|
|
8
|
+
* - 'replace' 는 부득이 wholesale 경로 (호스트가 별도 처리). dispatcher 는 받지 않음.
|
|
9
|
+
* - 각 op 의 inverse 를 적용 직전/직후 scene 상태로 계산해 함께 반환 — 호스트가
|
|
10
|
+
* 누적해 두면 나중에 같은 dispatcher 로 역순 적용해 revert 구현.
|
|
11
|
+
*
|
|
12
|
+
* Pure function — Lit element 외부에서 mock scene 으로 단위 테스트 가능.
|
|
13
|
+
*/
|
|
14
|
+
import type { BoardEditOp, ArrangeLayout } from '@things-factory/board-ai';
|
|
15
|
+
export interface DispatchContext {
|
|
16
|
+
/**
|
|
17
|
+
* `add` op 의 component 를 things-scene 의 default 와 deep-merge.
|
|
18
|
+
* 호스트가 BoardModeller 의 template registry 를 보고 채운다 — dispatcher 는 호출만.
|
|
19
|
+
* 미지정 시 component 를 그대로 scene.add 에 전달.
|
|
20
|
+
*/
|
|
21
|
+
normalize?: (c: any) => any;
|
|
22
|
+
}
|
|
23
|
+
export interface DispatchResult {
|
|
24
|
+
/** op 가 실제로 scene 에 반영됐는지. silent no-op (없는 refid 등) → false */
|
|
25
|
+
applied: boolean;
|
|
26
|
+
/** 적용 직전/직후 상태로 계산한 revert 용 inverse — 단일 op 가 다중 inverse 를
|
|
27
|
+
* 생성할 수 있음 (예: align 1개 → modify N개, group 1개 → ungroup 1개). */
|
|
28
|
+
inverseOps: BoardEditOp[];
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* 단일 BoardEditOp 를 things-scene 에 in-place 적용.
|
|
32
|
+
*
|
|
33
|
+
* `replace` 는 받지 않음 (호스트의 wholesale 경로). 미지원 op / 누락 컴포넌트 시
|
|
34
|
+
* `applied: false` 반환. 호스트가 missed 로 분류해 사용자 경고.
|
|
35
|
+
*/
|
|
36
|
+
export declare function dispatchBoardEditOp(scene: any, op: BoardEditOp, ctx?: DispatchContext): DispatchResult;
|
|
37
|
+
/**
|
|
38
|
+
* Sugar layout 위치 계산 — grid / row / column.
|
|
39
|
+
*
|
|
40
|
+
* 정책:
|
|
41
|
+
* - left/top 만 변경. width/height 는 보존 (사용자 의도).
|
|
42
|
+
* - grid: cell size = max(width)/max(height) — 컴포넌트 사이즈 다를 때 겹침 방지.
|
|
43
|
+
* Row-major 채움.
|
|
44
|
+
* - row/column: 각 컴포넌트의 실 size + gap 으로 누적. align (start/center/end) 으로
|
|
45
|
+
* cross-axis 정렬.
|
|
46
|
+
* - anchor 미지정 시 첫 컴포넌트의 현재 (left, top) — 예측 가능.
|
|
47
|
+
*
|
|
48
|
+
* 입력: layout 정의 + 각 target 의 현재 (left, top) (anchor default 용) + sizes.
|
|
49
|
+
* 출력: 각 target 의 새 (left, top). targets 와 동일 순서.
|
|
50
|
+
*
|
|
51
|
+
* Pure function — host dispatcher 외부에서도 단위 테스트 가능.
|
|
52
|
+
*/
|
|
53
|
+
export declare function computeArrangePositions(layout: ArrangeLayout, current: Array<{
|
|
54
|
+
left: number;
|
|
55
|
+
top: number;
|
|
56
|
+
}>, sizes: Array<{
|
|
57
|
+
width: number;
|
|
58
|
+
height: number;
|
|
59
|
+
}>): Array<{
|
|
60
|
+
left: number;
|
|
61
|
+
top: number;
|
|
62
|
+
}>;
|
|
63
|
+
/**
|
|
64
|
+
* scene 의 모든 컴포넌트 refid 수집 — add/group 의 inverse 계산용.
|
|
65
|
+
*
|
|
66
|
+
* scene.add / scene.group 직전·직후 호출해 차집합으로 새 발급 refid 식별.
|
|
67
|
+
*/
|
|
68
|
+
export declare function collectAllRefids(scene: any): number[];
|
|
69
|
+
/**
|
|
70
|
+
* patch 가 변경하려는 키들에 대해 model 의 현재 값을 deep-clone 으로 캡처.
|
|
71
|
+
* inverse modify / modifyBoard op 의 patch 로 사용. 원본에 없던 키는 null 로 보관
|
|
72
|
+
* (revert 시 명시적 null reset).
|
|
73
|
+
*/
|
|
74
|
+
export declare function captureOldKeys(model: any, patch: any): any;
|