@things-factory/board-ai 10.0.0-beta.64
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/client/components/board-ai-chat.test.ts +120 -0
- package/client/components/board-ai-chat.ts +1502 -0
- package/client/components/chat-input-builder.ts +40 -0
- package/client/components/markdown.test.ts +220 -0
- package/client/components/markdown.ts +184 -0
- package/client/index.ts +11 -0
- package/client/tsconfig.json +13 -0
- package/client/utils/board-edit-patch.ts +200 -0
- package/config/config.development.js +43 -0
- package/config/config.production.js +15 -0
- package/dist-client/components/board-ai-chat.d.ts +127 -0
- package/dist-client/components/board-ai-chat.js +1455 -0
- package/dist-client/components/board-ai-chat.js.map +1 -0
- package/dist-client/components/board-ai-chat.test.d.ts +1 -0
- package/dist-client/components/board-ai-chat.test.js +112 -0
- package/dist-client/components/board-ai-chat.test.js.map +1 -0
- package/dist-client/components/chat-input-builder.d.ts +30 -0
- package/dist-client/components/chat-input-builder.js +25 -0
- package/dist-client/components/chat-input-builder.js.map +1 -0
- package/dist-client/components/markdown.d.ts +16 -0
- package/dist-client/components/markdown.js +167 -0
- package/dist-client/components/markdown.js.map +1 -0
- package/dist-client/components/markdown.test.d.ts +1 -0
- package/dist-client/components/markdown.test.js +187 -0
- package/dist-client/components/markdown.test.js.map +1 -0
- package/dist-client/index.d.ts +11 -0
- package/dist-client/index.js +12 -0
- package/dist-client/index.js.map +1 -0
- package/dist-client/tsconfig.tsbuildinfo +1 -0
- package/dist-client/utils/board-edit-patch.d.ts +73 -0
- package/dist-client/utils/board-edit-patch.js +159 -0
- package/dist-client/utils/board-edit-patch.js.map +1 -0
- package/dist-server/index.d.ts +21 -0
- package/dist-server/index.js +25 -0
- package/dist-server/index.js.map +1 -0
- package/dist-server/service/apply-patch.d.ts +46 -0
- package/dist-server/service/apply-patch.js +211 -0
- package/dist-server/service/apply-patch.js.map +1 -0
- package/dist-server/service/assistant.d.ts +75 -0
- package/dist-server/service/assistant.js +1298 -0
- package/dist-server/service/assistant.js.map +1 -0
- package/dist-server/service/board-ai-resolver.d.ts +40 -0
- package/dist-server/service/board-ai-resolver.js +260 -0
- package/dist-server/service/board-ai-resolver.js.map +1 -0
- package/dist-server/service/chat-message/chat-message.d.ts +24 -0
- package/dist-server/service/chat-message/chat-message.js +108 -0
- package/dist-server/service/chat-message/chat-message.js.map +1 -0
- package/dist-server/service/chat-message/index.d.ts +3 -0
- package/dist-server/service/chat-message/index.js +7 -0
- package/dist-server/service/chat-message/index.js.map +1 -0
- package/dist-server/service/chat-session/chat-session.d.ts +22 -0
- package/dist-server/service/chat-session/chat-session.js +109 -0
- package/dist-server/service/chat-session/chat-session.js.map +1 -0
- package/dist-server/service/chat-session/index.d.ts +3 -0
- package/dist-server/service/chat-session/index.js +7 -0
- package/dist-server/service/chat-session/index.js.map +1 -0
- package/dist-server/service/chat-session-resolver.d.ts +13 -0
- package/dist-server/service/chat-session-resolver.js +178 -0
- package/dist-server/service/chat-session-resolver.js.map +1 -0
- package/dist-server/service/index.d.ts +14 -0
- package/dist-server/service/index.js +26 -0
- package/dist-server/service/index.js.map +1 -0
- package/dist-server/service/patch-entry/index.d.ts +3 -0
- package/dist-server/service/patch-entry/index.js +7 -0
- package/dist-server/service/patch-entry/index.js.map +1 -0
- package/dist-server/service/patch-entry/patch-entry.d.ts +16 -0
- package/dist-server/service/patch-entry/patch-entry.js +96 -0
- package/dist-server/service/patch-entry/patch-entry.js.map +1 -0
- package/dist-server/service/types.d.ts +137 -0
- package/dist-server/service/types.js +3 -0
- package/dist-server/service/types.js.map +1 -0
- package/dist-server/tsconfig.tsbuildinfo +1 -0
- package/package.json +47 -0
- package/server/index.ts +21 -0
- package/server/service/apply-patch.test.ts +640 -0
- package/server/service/apply-patch.ts +250 -0
- package/server/service/assistant.test.ts +1317 -0
- package/server/service/assistant.ts +1431 -0
- package/server/service/board-ai-resolver.ts +239 -0
- package/server/service/chat-message/chat-message.ts +110 -0
- package/server/service/chat-message/index.ts +5 -0
- package/server/service/chat-session/chat-session.ts +103 -0
- package/server/service/chat-session/index.ts +5 -0
- package/server/service/chat-session-resolver.ts +154 -0
- package/server/service/index.ts +24 -0
- package/server/service/patch-entry/index.ts +5 -0
- package/server/service/patch-entry/patch-entry.ts +89 -0
- package/server/service/types.ts +138 -0
- package/things-factory.config.js +1 -0
- package/translations/en.json +39 -0
- package/translations/ja.json +39 -0
- package/translations/ko.json +40 -0
- package/translations/ms.json +39 -0
- package/translations/zh.json +39 -0
- package/tsconfig.json +9 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-side BoardEditPatch types + applier.
|
|
3
|
+
*
|
|
4
|
+
* NOTE: 동일 정의가 server/service/types.ts + apply-patch.ts 에 있음.
|
|
5
|
+
* client 번들이 server 코드를 import 하지 않도록 분리. 변경 시 양쪽 동기화 필요.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* 컴포넌트 타깃팅은 항상 `refid` (things-scene universal numeric handle).
|
|
9
|
+
* `id` 는 데이터 바인딩 이름이며 unique 가 아니므로 targeting 에 사용하지 않는다.
|
|
10
|
+
*
|
|
11
|
+
* 보드는 최상위 부모 — 자체 속성 (fillStyle / width / height / name 등) 을 갖고,
|
|
12
|
+
* `modifyBoard` 로 변경. 자식 컴포넌트 변경 (`modify`) 와 분리.
|
|
13
|
+
*/
|
|
14
|
+
export type BoardEditOp = {
|
|
15
|
+
op: 'add';
|
|
16
|
+
component: any;
|
|
17
|
+
} | {
|
|
18
|
+
op: 'remove';
|
|
19
|
+
refid: number;
|
|
20
|
+
} | {
|
|
21
|
+
op: 'modify';
|
|
22
|
+
refid: number;
|
|
23
|
+
patch: any;
|
|
24
|
+
} | {
|
|
25
|
+
op: 'modifyBoard';
|
|
26
|
+
patch: any;
|
|
27
|
+
} | {
|
|
28
|
+
op: 'replace';
|
|
29
|
+
board: any;
|
|
30
|
+
};
|
|
31
|
+
export interface BoardEditPatch {
|
|
32
|
+
ops: BoardEditOp[];
|
|
33
|
+
summary: string;
|
|
34
|
+
confidence: number;
|
|
35
|
+
}
|
|
36
|
+
export interface PatchApplyReport {
|
|
37
|
+
/** 패치 적용 후 보드. 모든 op 가 noop 이어도 입력 그대로 반환. */
|
|
38
|
+
board: any;
|
|
39
|
+
/** 실제로 보드를 바꾼 op 들. */
|
|
40
|
+
applied: BoardEditOp[];
|
|
41
|
+
/** id 매칭 실패 등으로 noop 이 된 op 들 — 호출자가 사용자에게 알릴 단서. */
|
|
42
|
+
missed: BoardEditOp[];
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Patch 를 BoardModel 에 적용 (pure, board 를 mutate 하지 않음).
|
|
46
|
+
*/
|
|
47
|
+
export declare function applyBoardEditPatch(board: any | undefined, patch: BoardEditPatch): any;
|
|
48
|
+
/**
|
|
49
|
+
* Verbose 변형 — 각 op 의 적용 여부를 보고.
|
|
50
|
+
*
|
|
51
|
+
* `modify` 와 `remove` 는 id 가 보드에 없으면 silent no-op 이 된다. LLM 이 잘못된
|
|
52
|
+
* id 를 만들어 보내면 사용자에게 "수정했습니다" 라고 답하지만 실제로는 아무 변화도
|
|
53
|
+
* 없는 상황이 발생 — 호스트가 missed 를 보고 사용자에게 경고할 수 있도록 별도
|
|
54
|
+
* entry point 제공.
|
|
55
|
+
*/
|
|
56
|
+
export declare function applyBoardEditPatchVerbose(board: any | undefined, patch: BoardEditPatch): PatchApplyReport;
|
|
57
|
+
/**
|
|
58
|
+
* 주어진 board 상태에서 op 의 inverse 를 계산.
|
|
59
|
+
*
|
|
60
|
+
* Revert 기능 — patch 적용 직전 board + op 로 역연산을 만든다. 호스트가
|
|
61
|
+
* 누적해두면 나중에 역순 실행만으로 복원.
|
|
62
|
+
*
|
|
63
|
+
* add 의 inverse 는 새로 발급될 refid 를 알아야 → 모델 단계에서 계산 불가.
|
|
64
|
+
* 호스트가 scene.add 직후 refid 를 캡처해 직접 만들 것.
|
|
65
|
+
*/
|
|
66
|
+
export declare function computeInverseOp(board: any, op: BoardEditOp): BoardEditOp | null;
|
|
67
|
+
/**
|
|
68
|
+
* 컴포넌트에 부분 patch 를 적용 (deep merge).
|
|
69
|
+
* threeD 등 nested object 는 deep merge — 색만 바꾸려고 했을 때 geometry 까지 사라지지 않도록.
|
|
70
|
+
*
|
|
71
|
+
* host (board-modeller-page) 에서 things-scene 의 component.set(merged) 호출 전에 사용.
|
|
72
|
+
*/
|
|
73
|
+
export declare function mergeComponent(base: any, patch: any): any;
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-side BoardEditPatch types + applier.
|
|
3
|
+
*
|
|
4
|
+
* NOTE: 동일 정의가 server/service/types.ts + apply-patch.ts 에 있음.
|
|
5
|
+
* client 번들이 server 코드를 import 하지 않도록 분리. 변경 시 양쪽 동기화 필요.
|
|
6
|
+
*/
|
|
7
|
+
const EMPTY_BOARD = { width: 1000, height: 600, components: [] };
|
|
8
|
+
/**
|
|
9
|
+
* Patch 를 BoardModel 에 적용 (pure, board 를 mutate 하지 않음).
|
|
10
|
+
*/
|
|
11
|
+
export function applyBoardEditPatch(board, patch) {
|
|
12
|
+
return applyBoardEditPatchVerbose(board, patch).board;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Verbose 변형 — 각 op 의 적용 여부를 보고.
|
|
16
|
+
*
|
|
17
|
+
* `modify` 와 `remove` 는 id 가 보드에 없으면 silent no-op 이 된다. LLM 이 잘못된
|
|
18
|
+
* id 를 만들어 보내면 사용자에게 "수정했습니다" 라고 답하지만 실제로는 아무 변화도
|
|
19
|
+
* 없는 상황이 발생 — 호스트가 missed 를 보고 사용자에게 경고할 수 있도록 별도
|
|
20
|
+
* entry point 제공.
|
|
21
|
+
*/
|
|
22
|
+
export function applyBoardEditPatchVerbose(board, patch) {
|
|
23
|
+
let result = board ?? EMPTY_BOARD;
|
|
24
|
+
const applied = [];
|
|
25
|
+
const missed = [];
|
|
26
|
+
for (const op of patch.ops) {
|
|
27
|
+
const next = applyOp(result, op);
|
|
28
|
+
if (next === result || componentsUnchanged(result, next)) {
|
|
29
|
+
missed.push(op);
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
applied.push(op);
|
|
33
|
+
result = next;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return { board: result, applied, missed };
|
|
37
|
+
}
|
|
38
|
+
function componentsUnchanged(prev, next) {
|
|
39
|
+
// applyOp 는 항상 새 객체를 만든다. 따라서 reference 비교가 안 되고 내용 비교 필요.
|
|
40
|
+
// components 는 map/filter 결과 reference 도 다를 수 있으므로 length + 요소 ref 비교.
|
|
41
|
+
const a = prev.components ?? [];
|
|
42
|
+
const b = next.components ?? [];
|
|
43
|
+
if (a.length !== b.length)
|
|
44
|
+
return false;
|
|
45
|
+
for (let i = 0; i < a.length; i++) {
|
|
46
|
+
if (a[i] !== b[i])
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
return prev.width === next.width && prev.height === next.height && prev.fillStyle === next.fillStyle;
|
|
50
|
+
}
|
|
51
|
+
function applyOp(board, op) {
|
|
52
|
+
switch (op.op) {
|
|
53
|
+
case 'replace':
|
|
54
|
+
return op.board;
|
|
55
|
+
case 'add':
|
|
56
|
+
return { ...board, components: [...(board.components || []), op.component] };
|
|
57
|
+
case 'remove':
|
|
58
|
+
return {
|
|
59
|
+
...board,
|
|
60
|
+
components: (board.components || []).filter((c) => c?.refid !== op.refid)
|
|
61
|
+
};
|
|
62
|
+
case 'modify':
|
|
63
|
+
return {
|
|
64
|
+
...board,
|
|
65
|
+
components: (board.components || []).map((c) => c?.refid === op.refid ? mergeComponent(c, op.patch) : c)
|
|
66
|
+
};
|
|
67
|
+
case 'modifyBoard': {
|
|
68
|
+
const patch = { ...(op.patch || {}) };
|
|
69
|
+
delete patch.components; // 자식 변경은 별도 op
|
|
70
|
+
return mergeComponent(board, patch); // 최상위 board 자체에 deep-merge
|
|
71
|
+
}
|
|
72
|
+
default:
|
|
73
|
+
return board;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* 주어진 board 상태에서 op 의 inverse 를 계산.
|
|
78
|
+
*
|
|
79
|
+
* Revert 기능 — patch 적용 직전 board + op 로 역연산을 만든다. 호스트가
|
|
80
|
+
* 누적해두면 나중에 역순 실행만으로 복원.
|
|
81
|
+
*
|
|
82
|
+
* add 의 inverse 는 새로 발급될 refid 를 알아야 → 모델 단계에서 계산 불가.
|
|
83
|
+
* 호스트가 scene.add 직후 refid 를 캡처해 직접 만들 것.
|
|
84
|
+
*/
|
|
85
|
+
export function computeInverseOp(board, op) {
|
|
86
|
+
if (!board)
|
|
87
|
+
return null;
|
|
88
|
+
const components = board.components ?? [];
|
|
89
|
+
switch (op.op) {
|
|
90
|
+
case 'add':
|
|
91
|
+
return null;
|
|
92
|
+
case 'remove': {
|
|
93
|
+
const target = components.find((c) => c?.refid === op.refid);
|
|
94
|
+
if (!target)
|
|
95
|
+
return null;
|
|
96
|
+
return { op: 'add', component: JSON.parse(JSON.stringify(target)) };
|
|
97
|
+
}
|
|
98
|
+
case 'modify': {
|
|
99
|
+
const target = components.find((c) => c?.refid === op.refid);
|
|
100
|
+
if (!target)
|
|
101
|
+
return null;
|
|
102
|
+
const oldValues = {};
|
|
103
|
+
for (const k of Object.keys(op.patch || {})) {
|
|
104
|
+
const v = target[k];
|
|
105
|
+
oldValues[k] = v === undefined ? null : JSON.parse(JSON.stringify(v));
|
|
106
|
+
}
|
|
107
|
+
return { op: 'modify', refid: op.refid, patch: oldValues };
|
|
108
|
+
}
|
|
109
|
+
case 'modifyBoard': {
|
|
110
|
+
const oldValues = {};
|
|
111
|
+
const patch = op.patch || {};
|
|
112
|
+
for (const k of Object.keys(patch)) {
|
|
113
|
+
if (k === 'components')
|
|
114
|
+
continue;
|
|
115
|
+
const v = board[k];
|
|
116
|
+
oldValues[k] = v === undefined ? null : JSON.parse(JSON.stringify(v));
|
|
117
|
+
}
|
|
118
|
+
return { op: 'modifyBoard', patch: oldValues };
|
|
119
|
+
}
|
|
120
|
+
case 'replace':
|
|
121
|
+
return { op: 'replace', board: JSON.parse(JSON.stringify(board)) };
|
|
122
|
+
default:
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* 컴포넌트에 부분 patch 를 적용 (deep merge).
|
|
128
|
+
* threeD 등 nested object 는 deep merge — 색만 바꾸려고 했을 때 geometry 까지 사라지지 않도록.
|
|
129
|
+
*
|
|
130
|
+
* host (board-modeller-page) 에서 things-scene 의 component.set(merged) 호출 전에 사용.
|
|
131
|
+
*/
|
|
132
|
+
export function mergeComponent(base, patch) {
|
|
133
|
+
const out = { ...base };
|
|
134
|
+
for (const key of Object.keys(patch)) {
|
|
135
|
+
if (isPlainObject(base[key]) && isPlainObject(patch[key])) {
|
|
136
|
+
out[key] = deepMerge(base[key], patch[key]);
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
out[key] = patch[key];
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return out;
|
|
143
|
+
}
|
|
144
|
+
function deepMerge(a, b) {
|
|
145
|
+
const out = { ...a };
|
|
146
|
+
for (const key of Object.keys(b)) {
|
|
147
|
+
if (isPlainObject(a[key]) && isPlainObject(b[key])) {
|
|
148
|
+
out[key] = deepMerge(a[key], b[key]);
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
out[key] = b[key];
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return out;
|
|
155
|
+
}
|
|
156
|
+
function isPlainObject(v) {
|
|
157
|
+
return v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
158
|
+
}
|
|
159
|
+
//# sourceMappingURL=board-edit-patch.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"board-edit-patch.js","sourceRoot":"","sources":["../../client/utils/board-edit-patch.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Client-side BoardEditPatch types + applier.\n *\n * NOTE: 동일 정의가 server/service/types.ts + apply-patch.ts 에 있음.\n * client 번들이 server 코드를 import 하지 않도록 분리. 변경 시 양쪽 동기화 필요.\n */\n\n/**\n * 컴포넌트 타깃팅은 항상 `refid` (things-scene universal numeric handle).\n * `id` 는 데이터 바인딩 이름이며 unique 가 아니므로 targeting 에 사용하지 않는다.\n *\n * 보드는 최상위 부모 — 자체 속성 (fillStyle / width / height / name 등) 을 갖고,\n * `modifyBoard` 로 변경. 자식 컴포넌트 변경 (`modify`) 와 분리.\n */\nexport type BoardEditOp =\n | { op: 'add'; component: any }\n | { op: 'remove'; refid: number }\n | { op: 'modify'; refid: number; patch: any }\n | { op: 'modifyBoard'; patch: any }\n | { op: 'replace'; board: any }\n\nexport interface BoardEditPatch {\n ops: BoardEditOp[]\n summary: string\n confidence: number\n}\n\nexport interface PatchApplyReport {\n /** 패치 적용 후 보드. 모든 op 가 noop 이어도 입력 그대로 반환. */\n board: any\n /** 실제로 보드를 바꾼 op 들. */\n applied: BoardEditOp[]\n /** id 매칭 실패 등으로 noop 이 된 op 들 — 호출자가 사용자에게 알릴 단서. */\n missed: BoardEditOp[]\n}\n\nconst EMPTY_BOARD = { width: 1000, height: 600, components: [] as any[] }\n\n/**\n * Patch 를 BoardModel 에 적용 (pure, board 를 mutate 하지 않음).\n */\nexport function applyBoardEditPatch(board: any | undefined, patch: BoardEditPatch): any {\n return applyBoardEditPatchVerbose(board, patch).board\n}\n\n/**\n * Verbose 변형 — 각 op 의 적용 여부를 보고.\n *\n * `modify` 와 `remove` 는 id 가 보드에 없으면 silent no-op 이 된다. LLM 이 잘못된\n * id 를 만들어 보내면 사용자에게 \"수정했습니다\" 라고 답하지만 실제로는 아무 변화도\n * 없는 상황이 발생 — 호스트가 missed 를 보고 사용자에게 경고할 수 있도록 별도\n * entry point 제공.\n */\nexport function applyBoardEditPatchVerbose(\n board: any | undefined,\n patch: BoardEditPatch\n): PatchApplyReport {\n let result: any = board ?? EMPTY_BOARD\n const applied: BoardEditOp[] = []\n const missed: BoardEditOp[] = []\n\n for (const op of patch.ops) {\n const next = applyOp(result, op)\n if (next === result || componentsUnchanged(result, next)) {\n missed.push(op)\n } else {\n applied.push(op)\n result = next\n }\n }\n\n return { board: result, applied, missed }\n}\n\nfunction componentsUnchanged(prev: any, next: any): boolean {\n // applyOp 는 항상 새 객체를 만든다. 따라서 reference 비교가 안 되고 내용 비교 필요.\n // components 는 map/filter 결과 reference 도 다를 수 있으므로 length + 요소 ref 비교.\n const a = prev.components ?? []\n const b = next.components ?? []\n if (a.length !== b.length) return false\n for (let i = 0; i < a.length; i++) {\n if (a[i] !== b[i]) return false\n }\n return prev.width === next.width && prev.height === next.height && prev.fillStyle === next.fillStyle\n}\n\nfunction applyOp(board: any, op: BoardEditOp): any {\n switch (op.op) {\n case 'replace':\n return op.board\n case 'add':\n return { ...board, components: [...(board.components || []), op.component] }\n case 'remove':\n return {\n ...board,\n components: (board.components || []).filter((c: any) => c?.refid !== op.refid)\n }\n case 'modify':\n return {\n ...board,\n components: (board.components || []).map((c: any) =>\n c?.refid === op.refid ? mergeComponent(c, op.patch) : c\n )\n }\n case 'modifyBoard': {\n const patch = { ...(op.patch || {}) }\n delete patch.components // 자식 변경은 별도 op\n return mergeComponent(board, patch) // 최상위 board 자체에 deep-merge\n }\n default:\n return board\n }\n}\n\n/**\n * 주어진 board 상태에서 op 의 inverse 를 계산.\n *\n * Revert 기능 — patch 적용 직전 board + op 로 역연산을 만든다. 호스트가\n * 누적해두면 나중에 역순 실행만으로 복원.\n *\n * add 의 inverse 는 새로 발급될 refid 를 알아야 → 모델 단계에서 계산 불가.\n * 호스트가 scene.add 직후 refid 를 캡처해 직접 만들 것.\n */\nexport function computeInverseOp(board: any, op: BoardEditOp): BoardEditOp | null {\n if (!board) return null\n const components = board.components ?? []\n\n switch (op.op) {\n case 'add':\n return null\n\n case 'remove': {\n const target = components.find((c: any) => c?.refid === op.refid)\n if (!target) return null\n return { op: 'add', component: JSON.parse(JSON.stringify(target)) }\n }\n\n case 'modify': {\n const target = components.find((c: any) => c?.refid === op.refid)\n if (!target) return null\n const oldValues: any = {}\n for (const k of Object.keys(op.patch || {})) {\n const v = (target as any)[k]\n oldValues[k] = v === undefined ? null : JSON.parse(JSON.stringify(v))\n }\n return { op: 'modify', refid: op.refid, patch: oldValues }\n }\n\n case 'modifyBoard': {\n const oldValues: any = {}\n const patch = op.patch || {}\n for (const k of Object.keys(patch)) {\n if (k === 'components') continue\n const v = (board as any)[k]\n oldValues[k] = v === undefined ? null : JSON.parse(JSON.stringify(v))\n }\n return { op: 'modifyBoard', patch: oldValues }\n }\n\n case 'replace':\n return { op: 'replace', board: JSON.parse(JSON.stringify(board)) }\n\n default:\n return null\n }\n}\n\n/**\n * 컴포넌트에 부분 patch 를 적용 (deep merge).\n * threeD 등 nested object 는 deep merge — 색만 바꾸려고 했을 때 geometry 까지 사라지지 않도록.\n *\n * host (board-modeller-page) 에서 things-scene 의 component.set(merged) 호출 전에 사용.\n */\nexport function mergeComponent(base: any, patch: any): any {\n const out: any = { ...base }\n for (const key of Object.keys(patch)) {\n if (isPlainObject(base[key]) && isPlainObject(patch[key])) {\n out[key] = deepMerge(base[key], patch[key])\n } else {\n out[key] = patch[key]\n }\n }\n return out\n}\n\nfunction deepMerge(a: any, b: any): any {\n const out: any = { ...a }\n for (const key of Object.keys(b)) {\n if (isPlainObject(a[key]) && isPlainObject(b[key])) {\n out[key] = deepMerge(a[key], b[key])\n } else {\n out[key] = b[key]\n }\n }\n return out\n}\n\nfunction isPlainObject(v: any): boolean {\n return v !== null && typeof v === 'object' && !Array.isArray(v)\n}\n"]}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @things-factory/board-ai — AI 주도 보드 모델링.
|
|
3
|
+
*
|
|
4
|
+
* 단일 진입점 `chat()` 으로 보드 생성·편집·스타일링을 자연어로 통합 처리.
|
|
5
|
+
* board-import 의 BoardModel 위에서 동작하며, ai-client-base 가 LLM provider 추상화 담당.
|
|
6
|
+
*
|
|
7
|
+
* 사용:
|
|
8
|
+
* ```ts
|
|
9
|
+
* import { createAIClient } from '@things-factory/ai-client-base'
|
|
10
|
+
* import { DefaultBoardAIAssistant, applyBoardEditPatch } from '@things-factory/board-ai'
|
|
11
|
+
*
|
|
12
|
+
* const base = createAIClient({ provider: 'anthropic', apiKey: '...' })
|
|
13
|
+
* const ai = new DefaultBoardAIAssistant(base, { scopes: ['fmsim'] })
|
|
14
|
+
*
|
|
15
|
+
* let board
|
|
16
|
+
* const r = await ai.chat([{ role: 'user', content: 'AGV 3대를 우측에 추가' }], board)
|
|
17
|
+
* if (r.patch) board = applyBoardEditPatch(board, r.patch)
|
|
18
|
+
* console.log(r.reply)
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export * from './service/index.js';
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const tslib_1 = require("tslib");
|
|
4
|
+
/**
|
|
5
|
+
* @things-factory/board-ai — AI 주도 보드 모델링.
|
|
6
|
+
*
|
|
7
|
+
* 단일 진입점 `chat()` 으로 보드 생성·편집·스타일링을 자연어로 통합 처리.
|
|
8
|
+
* board-import 의 BoardModel 위에서 동작하며, ai-client-base 가 LLM provider 추상화 담당.
|
|
9
|
+
*
|
|
10
|
+
* 사용:
|
|
11
|
+
* ```ts
|
|
12
|
+
* import { createAIClient } from '@things-factory/ai-client-base'
|
|
13
|
+
* import { DefaultBoardAIAssistant, applyBoardEditPatch } from '@things-factory/board-ai'
|
|
14
|
+
*
|
|
15
|
+
* const base = createAIClient({ provider: 'anthropic', apiKey: '...' })
|
|
16
|
+
* const ai = new DefaultBoardAIAssistant(base, { scopes: ['fmsim'] })
|
|
17
|
+
*
|
|
18
|
+
* let board
|
|
19
|
+
* const r = await ai.chat([{ role: 'user', content: 'AGV 3대를 우측에 추가' }], board)
|
|
20
|
+
* if (r.patch) board = applyBoardEditPatch(board, r.patch)
|
|
21
|
+
* console.log(r.reply)
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
tslib_1.__exportStar(require("./service/index.js"), exports);
|
|
25
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../server/index.ts"],"names":[],"mappings":";;;AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,6DAAkC","sourcesContent":["/**\n * @things-factory/board-ai — AI 주도 보드 모델링.\n *\n * 단일 진입점 `chat()` 으로 보드 생성·편집·스타일링을 자연어로 통합 처리.\n * board-import 의 BoardModel 위에서 동작하며, ai-client-base 가 LLM provider 추상화 담당.\n *\n * 사용:\n * ```ts\n * import { createAIClient } from '@things-factory/ai-client-base'\n * import { DefaultBoardAIAssistant, applyBoardEditPatch } from '@things-factory/board-ai'\n *\n * const base = createAIClient({ provider: 'anthropic', apiKey: '...' })\n * const ai = new DefaultBoardAIAssistant(base, { scopes: ['fmsim'] })\n *\n * let board\n * const r = await ai.chat([{ role: 'user', content: 'AGV 3대를 우측에 추가' }], board)\n * if (r.patch) board = applyBoardEditPatch(board, r.patch)\n * console.log(r.reply)\n * ```\n */\nexport * from './service/index.js'\n"]}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BoardEditPatch 를 BoardModel 에 적용하는 표준 helper.
|
|
3
|
+
*
|
|
4
|
+
* 호출자가 patch 응답을 받아 보드에 반영할 때 사용.
|
|
5
|
+
* Pure function — 입력을 mutate 하지 않음.
|
|
6
|
+
*/
|
|
7
|
+
import type { BoardComponent, BoardModel } from '@things-factory/board-import';
|
|
8
|
+
import type { BoardEditOp, BoardEditPatch } from './types.js';
|
|
9
|
+
export interface PatchApplyReport {
|
|
10
|
+
/** 패치 적용 후 보드. 모든 op 가 noop 이어도 입력 그대로 반환. */
|
|
11
|
+
board: BoardModel;
|
|
12
|
+
/** 실제로 보드를 바꾼 op 들. */
|
|
13
|
+
applied: BoardEditOp[];
|
|
14
|
+
/** id 매칭 실패 등으로 noop 이 된 op 들 — 호출자가 사용자에게 알릴 단서. */
|
|
15
|
+
missed: BoardEditOp[];
|
|
16
|
+
}
|
|
17
|
+
export declare function applyBoardEditPatch(board: BoardModel | undefined, patch: BoardEditPatch): BoardModel;
|
|
18
|
+
/**
|
|
19
|
+
* Verbose 변형 — 각 op 의 적용 여부를 보고.
|
|
20
|
+
*
|
|
21
|
+
* `modify` 와 `remove` 는 id 가 보드에 없으면 silent no-op 이 된다 (patch 함수의
|
|
22
|
+
* 의도적 단순화). LLM 이 잘못된 id 를 만들어 보내면 사용자에게 "수정했습니다"
|
|
23
|
+
* 라고 답하지만 실제로는 아무 변화도 없는 상황이 발생 — 호스트가 missed 를
|
|
24
|
+
* 보고 사용자에게 경고할 수 있도록 별도 entry point 제공.
|
|
25
|
+
*/
|
|
26
|
+
export declare function applyBoardEditPatchVerbose(board: BoardModel | undefined, patch: BoardEditPatch): PatchApplyReport;
|
|
27
|
+
/**
|
|
28
|
+
* 주어진 board 상태에서 op 의 inverse 를 계산.
|
|
29
|
+
*
|
|
30
|
+
* Revert 기능의 코어 — patch 적용 직전 board 와 op 만 알면 그 op 의 역연산을
|
|
31
|
+
* 만들 수 있으므로, 호스트가 in-place 적용하면서 함께 누적해 두면 나중에
|
|
32
|
+
* 역순 실행만으로 복원 가능.
|
|
33
|
+
*
|
|
34
|
+
* 반환:
|
|
35
|
+
* - inverse op 또는 null (unsupported / 데이터 부족)
|
|
36
|
+
* - add 의 inverse 는 add 후 발급되는 refid 가 필요해서 모델 단계에서 계산 불가 →
|
|
37
|
+
* 호스트가 scene.add 직후 refid 를 캡처해 직접 만들 것 (이 함수는 pre-applied
|
|
38
|
+
* board 만 보고 만들 수 있는 종류만 처리: remove / modify / modifyBoard / replace)
|
|
39
|
+
*/
|
|
40
|
+
export declare function computeInverseOp(board: BoardModel | undefined, op: BoardEditOp): BoardEditOp | null;
|
|
41
|
+
export declare function applyOp(board: BoardModel, op: BoardEditOp): BoardModel;
|
|
42
|
+
/**
|
|
43
|
+
* 컴포넌트에 부분 patch 를 적용.
|
|
44
|
+
* threeD 등 nested object 는 deep merge — 호출자가 색만 바꾸려고 했을 때 geometry 까지 사라지지 않도록.
|
|
45
|
+
*/
|
|
46
|
+
export declare function mergeComponent(base: BoardComponent, patch: Partial<BoardComponent>): BoardComponent;
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.applyBoardEditPatch = applyBoardEditPatch;
|
|
4
|
+
exports.applyBoardEditPatchVerbose = applyBoardEditPatchVerbose;
|
|
5
|
+
exports.computeInverseOp = computeInverseOp;
|
|
6
|
+
exports.applyOp = applyOp;
|
|
7
|
+
exports.mergeComponent = mergeComponent;
|
|
8
|
+
const EMPTY_BOARD = {
|
|
9
|
+
width: 1000,
|
|
10
|
+
height: 600,
|
|
11
|
+
components: []
|
|
12
|
+
};
|
|
13
|
+
function applyBoardEditPatch(board, patch) {
|
|
14
|
+
return applyBoardEditPatchVerbose(board, patch).board;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Verbose 변형 — 각 op 의 적용 여부를 보고.
|
|
18
|
+
*
|
|
19
|
+
* `modify` 와 `remove` 는 id 가 보드에 없으면 silent no-op 이 된다 (patch 함수의
|
|
20
|
+
* 의도적 단순화). LLM 이 잘못된 id 를 만들어 보내면 사용자에게 "수정했습니다"
|
|
21
|
+
* 라고 답하지만 실제로는 아무 변화도 없는 상황이 발생 — 호스트가 missed 를
|
|
22
|
+
* 보고 사용자에게 경고할 수 있도록 별도 entry point 제공.
|
|
23
|
+
*/
|
|
24
|
+
function applyBoardEditPatchVerbose(board, patch) {
|
|
25
|
+
let result = board ?? EMPTY_BOARD;
|
|
26
|
+
const applied = [];
|
|
27
|
+
const missed = [];
|
|
28
|
+
for (const op of patch.ops) {
|
|
29
|
+
const next = applyOp(result, op);
|
|
30
|
+
if (next === result || componentsUnchanged(result, next)) {
|
|
31
|
+
missed.push(op);
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
applied.push(op);
|
|
35
|
+
result = next;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return { board: result, applied, missed };
|
|
39
|
+
}
|
|
40
|
+
function componentsUnchanged(prev, next) {
|
|
41
|
+
// applyOp 는 항상 새 객체를 만든다 (`{ ...board, components: ... }`). 따라서 reference
|
|
42
|
+
// 비교가 안 되고 내용 비교가 필요. components 는 map/filter 결과 reference 도 다를 수
|
|
43
|
+
// 있으므로 length + JSON 깊이 비교.
|
|
44
|
+
const a = prev.components ?? [];
|
|
45
|
+
const b = next.components ?? [];
|
|
46
|
+
if (a.length !== b.length)
|
|
47
|
+
return false;
|
|
48
|
+
for (let i = 0; i < a.length; i++) {
|
|
49
|
+
if (a[i] !== b[i])
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
// root meta (width/height/...) 비교
|
|
53
|
+
return prev.width === next.width && prev.height === next.height && prev.fillStyle === next.fillStyle;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* 주어진 board 상태에서 op 의 inverse 를 계산.
|
|
57
|
+
*
|
|
58
|
+
* Revert 기능의 코어 — patch 적용 직전 board 와 op 만 알면 그 op 의 역연산을
|
|
59
|
+
* 만들 수 있으므로, 호스트가 in-place 적용하면서 함께 누적해 두면 나중에
|
|
60
|
+
* 역순 실행만으로 복원 가능.
|
|
61
|
+
*
|
|
62
|
+
* 반환:
|
|
63
|
+
* - inverse op 또는 null (unsupported / 데이터 부족)
|
|
64
|
+
* - add 의 inverse 는 add 후 발급되는 refid 가 필요해서 모델 단계에서 계산 불가 →
|
|
65
|
+
* 호스트가 scene.add 직후 refid 를 캡처해 직접 만들 것 (이 함수는 pre-applied
|
|
66
|
+
* board 만 보고 만들 수 있는 종류만 처리: remove / modify / modifyBoard / replace)
|
|
67
|
+
*/
|
|
68
|
+
function computeInverseOp(board, op) {
|
|
69
|
+
if (!board)
|
|
70
|
+
return null;
|
|
71
|
+
const components = board.components ?? [];
|
|
72
|
+
switch (op.op) {
|
|
73
|
+
case 'add':
|
|
74
|
+
// add 의 inverse 는 새로 발급될 refid 를 알아야 → 호스트 측에서 처리
|
|
75
|
+
return null;
|
|
76
|
+
case 'remove': {
|
|
77
|
+
const target = components.find(c => c?.refid === op.refid);
|
|
78
|
+
if (!target)
|
|
79
|
+
return null; // 매칭 실패 — silent no-op 이라 inverse 도 없음
|
|
80
|
+
return { op: 'add', component: JSON.parse(JSON.stringify(target)) };
|
|
81
|
+
}
|
|
82
|
+
case 'modify': {
|
|
83
|
+
const target = components.find(c => c?.refid === op.refid);
|
|
84
|
+
if (!target)
|
|
85
|
+
return null;
|
|
86
|
+
// patch 가 건드린 키만 보관. nested 는 통째로.
|
|
87
|
+
const oldValues = {};
|
|
88
|
+
for (const k of Object.keys(op.patch || {})) {
|
|
89
|
+
const v = target[k];
|
|
90
|
+
oldValues[k] = v === undefined ? null : JSON.parse(JSON.stringify(v));
|
|
91
|
+
}
|
|
92
|
+
return { op: 'modify', refid: op.refid, patch: oldValues };
|
|
93
|
+
}
|
|
94
|
+
case 'modifyBoard': {
|
|
95
|
+
const oldValues = {};
|
|
96
|
+
const patch = op.patch || {};
|
|
97
|
+
for (const k of Object.keys(patch)) {
|
|
98
|
+
if (k === 'components')
|
|
99
|
+
continue;
|
|
100
|
+
const v = board[k];
|
|
101
|
+
oldValues[k] = v === undefined ? null : JSON.parse(JSON.stringify(v));
|
|
102
|
+
}
|
|
103
|
+
return { op: 'modifyBoard', patch: oldValues };
|
|
104
|
+
}
|
|
105
|
+
case 'replace':
|
|
106
|
+
// 이전 보드 통째로 보관 — replace 의 자연스러운 inverse 는 또 다른 replace
|
|
107
|
+
return { op: 'replace', board: JSON.parse(JSON.stringify(board)) };
|
|
108
|
+
default:
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function applyOp(board, op) {
|
|
113
|
+
const components = board.components ?? [];
|
|
114
|
+
switch (op.op) {
|
|
115
|
+
case 'replace':
|
|
116
|
+
return op.board;
|
|
117
|
+
case 'add':
|
|
118
|
+
return { ...board, components: [...components, op.component] };
|
|
119
|
+
case 'remove':
|
|
120
|
+
return { ...board, components: components.filter(c => c?.refid !== op.refid) };
|
|
121
|
+
case 'modify':
|
|
122
|
+
return {
|
|
123
|
+
...board,
|
|
124
|
+
components: components.map(c => c?.refid === op.refid ? mergeComponent(c, op.patch) : c)
|
|
125
|
+
};
|
|
126
|
+
case 'modifyBoard': {
|
|
127
|
+
// 루트 속성 (fillStyle / width / height / name 등) 만 deep merge.
|
|
128
|
+
// components 키는 무시 — 자식 변경은 add/remove/modify 별도 op 로.
|
|
129
|
+
const patch = { ...op.patch };
|
|
130
|
+
delete patch.components;
|
|
131
|
+
return mergeBoardRoot(board, patch);
|
|
132
|
+
}
|
|
133
|
+
default:
|
|
134
|
+
return board;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
function mergeBoardRoot(board, patch) {
|
|
138
|
+
const out = { ...board };
|
|
139
|
+
for (const key of Object.keys(patch)) {
|
|
140
|
+
const bv = board[key];
|
|
141
|
+
const pv = patch[key];
|
|
142
|
+
if (bv !== null &&
|
|
143
|
+
pv !== null &&
|
|
144
|
+
typeof bv === 'object' &&
|
|
145
|
+
typeof pv === 'object' &&
|
|
146
|
+
!Array.isArray(bv) &&
|
|
147
|
+
!Array.isArray(pv)) {
|
|
148
|
+
out[key] = deepMergeRoot(bv, pv);
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
out[key] = pv;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return out;
|
|
155
|
+
}
|
|
156
|
+
function deepMergeRoot(a, b) {
|
|
157
|
+
const out = { ...a };
|
|
158
|
+
for (const key of Object.keys(b)) {
|
|
159
|
+
const av = a[key];
|
|
160
|
+
const bv = b[key];
|
|
161
|
+
if (av !== null &&
|
|
162
|
+
bv !== null &&
|
|
163
|
+
typeof av === 'object' &&
|
|
164
|
+
typeof bv === 'object' &&
|
|
165
|
+
!Array.isArray(av) &&
|
|
166
|
+
!Array.isArray(bv)) {
|
|
167
|
+
out[key] = deepMergeRoot(av, bv);
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
out[key] = bv;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return out;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* 컴포넌트에 부분 patch 를 적용.
|
|
177
|
+
* threeD 등 nested object 는 deep merge — 호출자가 색만 바꾸려고 했을 때 geometry 까지 사라지지 않도록.
|
|
178
|
+
*/
|
|
179
|
+
function mergeComponent(base, patch) {
|
|
180
|
+
const out = { ...base };
|
|
181
|
+
for (const key of Object.keys(patch)) {
|
|
182
|
+
const baseVal = base[key];
|
|
183
|
+
const patchVal = patch[key];
|
|
184
|
+
if (isPlainObject(baseVal) &&
|
|
185
|
+
isPlainObject(patchVal)) {
|
|
186
|
+
out[key] = deepMerge(baseVal, patchVal);
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
out[key] = patchVal;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return out;
|
|
193
|
+
}
|
|
194
|
+
function deepMerge(a, b) {
|
|
195
|
+
const out = { ...a };
|
|
196
|
+
for (const key of Object.keys(b)) {
|
|
197
|
+
const av = a[key];
|
|
198
|
+
const bv = b[key];
|
|
199
|
+
if (isPlainObject(av) && isPlainObject(bv)) {
|
|
200
|
+
out[key] = deepMerge(av, bv);
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
out[key] = bv;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return out;
|
|
207
|
+
}
|
|
208
|
+
function isPlainObject(v) {
|
|
209
|
+
return v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
210
|
+
}
|
|
211
|
+
//# sourceMappingURL=apply-patch.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"apply-patch.js","sourceRoot":"","sources":["../../server/service/apply-patch.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * BoardEditPatch 를 BoardModel 에 적용하는 표준 helper.\n *\n * 호출자가 patch 응답을 받아 보드에 반영할 때 사용.\n * Pure function — 입력을 mutate 하지 않음.\n */\nimport type { BoardComponent, BoardModel } from '@things-factory/board-import'\nimport type { BoardEditOp, BoardEditPatch } from './types.js'\n\nconst EMPTY_BOARD: BoardModel = {\n width: 1000,\n height: 600,\n components: []\n}\n\nexport interface PatchApplyReport {\n /** 패치 적용 후 보드. 모든 op 가 noop 이어도 입력 그대로 반환. */\n board: BoardModel\n /** 실제로 보드를 바꾼 op 들. */\n applied: BoardEditOp[]\n /** id 매칭 실패 등으로 noop 이 된 op 들 — 호출자가 사용자에게 알릴 단서. */\n missed: BoardEditOp[]\n}\n\nexport function applyBoardEditPatch(\n board: BoardModel | undefined,\n patch: BoardEditPatch\n): BoardModel {\n return applyBoardEditPatchVerbose(board, patch).board\n}\n\n/**\n * Verbose 변형 — 각 op 의 적용 여부를 보고.\n *\n * `modify` 와 `remove` 는 id 가 보드에 없으면 silent no-op 이 된다 (patch 함수의\n * 의도적 단순화). LLM 이 잘못된 id 를 만들어 보내면 사용자에게 \"수정했습니다\"\n * 라고 답하지만 실제로는 아무 변화도 없는 상황이 발생 — 호스트가 missed 를\n * 보고 사용자에게 경고할 수 있도록 별도 entry point 제공.\n */\nexport function applyBoardEditPatchVerbose(\n board: BoardModel | undefined,\n patch: BoardEditPatch\n): PatchApplyReport {\n let result: BoardModel = board ?? EMPTY_BOARD\n const applied: BoardEditOp[] = []\n const missed: BoardEditOp[] = []\n\n for (const op of patch.ops) {\n const next = applyOp(result, op)\n if (next === result || componentsUnchanged(result, next)) {\n missed.push(op)\n } else {\n applied.push(op)\n result = next\n }\n }\n\n return { board: result, applied, missed }\n}\n\nfunction componentsUnchanged(prev: BoardModel, next: BoardModel): boolean {\n // applyOp 는 항상 새 객체를 만든다 (`{ ...board, components: ... }`). 따라서 reference\n // 비교가 안 되고 내용 비교가 필요. components 는 map/filter 결과 reference 도 다를 수\n // 있으므로 length + JSON 깊이 비교.\n const a = prev.components ?? []\n const b = next.components ?? []\n if (a.length !== b.length) return false\n for (let i = 0; i < a.length; i++) {\n if (a[i] !== b[i]) return false\n }\n // root meta (width/height/...) 비교\n return prev.width === next.width && prev.height === next.height && prev.fillStyle === next.fillStyle\n}\n\n/**\n * 주어진 board 상태에서 op 의 inverse 를 계산.\n *\n * Revert 기능의 코어 — patch 적용 직전 board 와 op 만 알면 그 op 의 역연산을\n * 만들 수 있으므로, 호스트가 in-place 적용하면서 함께 누적해 두면 나중에\n * 역순 실행만으로 복원 가능.\n *\n * 반환:\n * - inverse op 또는 null (unsupported / 데이터 부족)\n * - add 의 inverse 는 add 후 발급되는 refid 가 필요해서 모델 단계에서 계산 불가 →\n * 호스트가 scene.add 직후 refid 를 캡처해 직접 만들 것 (이 함수는 pre-applied\n * board 만 보고 만들 수 있는 종류만 처리: remove / modify / modifyBoard / replace)\n */\nexport function computeInverseOp(\n board: BoardModel | undefined,\n op: BoardEditOp\n): BoardEditOp | null {\n if (!board) return null\n const components = board.components ?? []\n\n switch (op.op) {\n case 'add':\n // add 의 inverse 는 새로 발급될 refid 를 알아야 → 호스트 측에서 처리\n return null\n\n case 'remove': {\n const target = components.find(c => (c as any)?.refid === op.refid)\n if (!target) return null // 매칭 실패 — silent no-op 이라 inverse 도 없음\n return { op: 'add', component: JSON.parse(JSON.stringify(target)) }\n }\n\n case 'modify': {\n const target = components.find(c => (c as any)?.refid === op.refid)\n if (!target) return null\n // patch 가 건드린 키만 보관. nested 는 통째로.\n const oldValues: any = {}\n for (const k of Object.keys(op.patch || {})) {\n const v = (target as any)[k]\n oldValues[k] = v === undefined ? null : JSON.parse(JSON.stringify(v))\n }\n return { op: 'modify', refid: op.refid, patch: oldValues }\n }\n\n case 'modifyBoard': {\n const oldValues: any = {}\n const patch = op.patch || {}\n for (const k of Object.keys(patch)) {\n if (k === 'components') continue\n const v = (board as any)[k]\n oldValues[k] = v === undefined ? null : JSON.parse(JSON.stringify(v))\n }\n return { op: 'modifyBoard', patch: oldValues }\n }\n\n case 'replace':\n // 이전 보드 통째로 보관 — replace 의 자연스러운 inverse 는 또 다른 replace\n return { op: 'replace', board: JSON.parse(JSON.stringify(board)) }\n\n default:\n return null\n }\n}\n\nexport function applyOp(board: BoardModel, op: BoardEditOp): BoardModel {\n const components = board.components ?? []\n switch (op.op) {\n case 'replace':\n return op.board\n\n case 'add':\n return { ...board, components: [...components, op.component] }\n\n case 'remove':\n return { ...board, components: components.filter(c => (c as any)?.refid !== op.refid) }\n\n case 'modify':\n return {\n ...board,\n components: components.map(c =>\n (c as any)?.refid === op.refid ? mergeComponent(c, op.patch) : c\n )\n }\n\n case 'modifyBoard': {\n // 루트 속성 (fillStyle / width / height / name 등) 만 deep merge.\n // components 키는 무시 — 자식 변경은 add/remove/modify 별도 op 로.\n const patch = { ...op.patch } as any\n delete patch.components\n return mergeBoardRoot(board, patch)\n }\n\n default:\n return board\n }\n}\n\nfunction mergeBoardRoot(board: BoardModel, patch: any): BoardModel {\n const out: any = { ...board }\n for (const key of Object.keys(patch)) {\n const bv = (board as any)[key]\n const pv = patch[key]\n if (\n bv !== null &&\n pv !== null &&\n typeof bv === 'object' &&\n typeof pv === 'object' &&\n !Array.isArray(bv) &&\n !Array.isArray(pv)\n ) {\n out[key] = deepMergeRoot(bv, pv)\n } else {\n out[key] = pv\n }\n }\n return out\n}\n\nfunction deepMergeRoot(a: any, b: any): any {\n const out: any = { ...a }\n for (const key of Object.keys(b)) {\n const av = a[key]\n const bv = b[key]\n if (\n av !== null &&\n bv !== null &&\n typeof av === 'object' &&\n typeof bv === 'object' &&\n !Array.isArray(av) &&\n !Array.isArray(bv)\n ) {\n out[key] = deepMergeRoot(av, bv)\n } else {\n out[key] = bv\n }\n }\n return out\n}\n\n/**\n * 컴포넌트에 부분 patch 를 적용.\n * threeD 등 nested object 는 deep merge — 호출자가 색만 바꾸려고 했을 때 geometry 까지 사라지지 않도록.\n */\nexport function mergeComponent(base: BoardComponent, patch: Partial<BoardComponent>): BoardComponent {\n const out: any = { ...base }\n for (const key of Object.keys(patch)) {\n const baseVal = (base as any)[key]\n const patchVal = (patch as any)[key]\n if (\n isPlainObject(baseVal) &&\n isPlainObject(patchVal)\n ) {\n out[key] = deepMerge(baseVal, patchVal)\n } else {\n out[key] = patchVal\n }\n }\n return out as BoardComponent\n}\n\nfunction deepMerge(a: any, b: any): any {\n const out: any = { ...a }\n for (const key of Object.keys(b)) {\n const av = a[key]\n const bv = b[key]\n if (isPlainObject(av) && isPlainObject(bv)) {\n out[key] = deepMerge(av, bv)\n } else {\n out[key] = bv\n }\n }\n return out\n}\n\nfunction isPlainObject(v: any): boolean {\n return v !== null && typeof v === 'object' && !Array.isArray(v)\n}\n"]}
|