@things-factory/board-ui 10.1.24 → 10.1.26

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 (33) hide show
  1. package/dist-client/board-placement-tray.js +3 -3
  2. package/dist-client/board-placement-tray.js.map +1 -1
  3. package/dist-client/board-placement-view.d.ts +1 -1
  4. package/dist-client/board-placement-view.js.map +1 -1
  5. package/dist-client/index.d.ts +0 -3
  6. package/dist-client/index.js +0 -3
  7. package/dist-client/index.js.map +1 -1
  8. package/dist-client/pages/board-modeller-page.d.ts +5 -5
  9. package/dist-client/pages/board-modeller-page.js +19 -21
  10. package/dist-client/pages/board-modeller-page.js.map +1 -1
  11. package/dist-client/pages/board-player-page.js +1 -1
  12. package/dist-client/pages/board-player-page.js.map +1 -1
  13. package/dist-client/pages/board-viewer-page.js +1 -1
  14. package/dist-client/pages/board-viewer-page.js.map +1 -1
  15. package/dist-client/tsconfig.tsbuildinfo +1 -1
  16. package/dist-client/utils/board-model-diff.d.ts +3 -3
  17. package/dist-client/utils/board-model-diff.js +0 -0
  18. package/dist-client/utils/board-model-diff.js.map +1 -1
  19. package/dist-client/utils/patch-apply-gate.js +1 -1
  20. package/dist-client/utils/patch-apply-gate.js.map +1 -1
  21. package/package.json +9 -8
  22. package/dist-client/board-placement-palette.d.ts +0 -39
  23. package/dist-client/board-placement-palette.js +0 -8
  24. package/dist-client/board-placement-palette.js.map +0 -1
  25. package/dist-client/board-provider.d.ts +0 -3
  26. package/dist-client/board-provider.js +0 -87
  27. package/dist-client/board-provider.js.map +0 -1
  28. package/dist-client/pages/board-action-dispatch.d.ts +0 -31
  29. package/dist-client/pages/board-action-dispatch.js +0 -80
  30. package/dist-client/pages/board-action-dispatch.js.map +0 -1
  31. package/dist-client/pages/board-edit-dispatch.d.ts +0 -74
  32. package/dist-client/pages/board-edit-dispatch.js +0 -299
  33. package/dist-client/pages/board-edit-dispatch.js.map +0 -1
@@ -1,8 +1,8 @@
1
- import type { BoardEditOp } from '@things-factory/board-ai';
1
+ import type { SceneEditOp } from '@operato/scene-ops';
2
2
  interface AnyModel {
3
3
  components?: any[];
4
4
  [k: string]: any;
5
5
  }
6
- /** prev → next 모델 차이를 BoardEditOp[] 로. 변화 없으면 빈 배열. */
7
- export declare function diffBoardModel(prev: AnyModel | null | undefined, next: AnyModel | null | undefined): BoardEditOp[];
6
+ /** prev → next 모델 차이를 SceneEditOp[] 로. 변화 없으면 빈 배열. */
7
+ export declare function diffBoardModel(prev: AnyModel | null | undefined, next: AnyModel | null | undefined): SceneEditOp[];
8
8
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"board-model-diff.js","sourceRoot":"","sources":["../../client/utils/board-model-diff.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * diffBoardModel (Phase Board-AI C3a) — 사용자 직접편집을 BoardEditOp[] 로 캡처하기 위한\n * *순수* 모델 diff. 모델러의 @model-changed 직전/직후 모델을 비교해 add/remove/modify/modifyBoard\n * 를 산출 → recordDirectPatch 로 보내 다른 참여자/AI 에 방송(C3b).\n *\n * 범위(프로토타입): top-level 컴포넌트의 add/remove/modify + 보드 root 속성(modifyBoard).\n * 중첩(group 내부) 편집은 coarse — 추후 정밀화. 순수·결정적 → 단위테스트.\n */\nimport type { BoardEditOp } from '@things-factory/board-ai'\n\ninterface AnyModel {\n components?: any[]\n [k: string]: any\n}\n\n/** prev → next 모델 차이를 BoardEditOp[] 로. 변화 없으면 빈 배열. */\nexport function diffBoardModel(prev: AnyModel | null | undefined, next: AnyModel | null | undefined): BoardEditOp[] {\n const ops: BoardEditOp[] = []\n const prevComps = indexByRefid(prev?.components)\n const nextComps = indexByRefid(next?.components)\n\n // remove — prev 에 있고 next 에 없음\n for (const refid of prevComps.keys()) {\n if (!nextComps.has(refid)) ops.push({ op: 'remove', refid } as BoardEditOp)\n }\n // add — next 에 있고 prev 에 없음 (전체 컴포넌트)\n for (const [refid, comp] of nextComps) {\n if (!prevComps.has(refid)) ops.push({ op: 'add', component: comp } as BoardEditOp)\n }\n // modify — 양쪽에 있고 속성 변경\n for (const [refid, nc] of nextComps) {\n const pc = prevComps.get(refid)\n if (!pc) continue\n const patch = propDiff(pc, nc, COMPONENT_SKIP)\n if (patch) ops.push({ op: 'modify', refid, patch } as BoardEditOp)\n }\n // modifyBoard — 보드 root 속성(components 제외) 변경\n const boardPatch = propDiff(prev || {}, next || {}, BOARD_SKIP)\n if (boardPatch) ops.unshift({ op: 'modifyBoard', patch: boardPatch } as BoardEditOp)\n\n return ops\n}\n\nconst COMPONENT_SKIP = new Set(['components'])\nconst BOARD_SKIP = new Set(['components'])\n\n/** top-level 컴포넌트를 refid 로 색인 (refid 없는 건 제외 — diff 불가). */\nfunction indexByRefid(components: any[] | undefined): Map<number, any> {\n const map = new Map<number, any>()\n if (!Array.isArray(components)) return map\n for (const c of components) {\n if (c && c.refid != null) map.set(c.refid, c)\n }\n return map\n}\n\n/**\n * a → b 의 속성 차이. b 에서 *변경되거나 추가된* 키만 (deep 비교는 JSON.stringify).\n * skip 키 제외. 차이 없으면 undefined.\n */\nfunction propDiff(a: any, b: any, skip: Set<string>): Record<string, any> | undefined {\n const patch: Record<string, any> = {}\n for (const key of Object.keys(b || {})) {\n if (skip.has(key)) continue\n if (stableStr(a?.[key]) !== stableStr(b[key])) patch[key] = b[key]\n }\n return Object.keys(patch).length ? patch : undefined\n}\n\nfunction stableStr(v: any): string {\n if (v === undefined) return '\u0000undef'\n try {\n return JSON.stringify(v)\n } catch {\n return String(v)\n }\n}\n"]}
1
+ {"version":3,"file":"board-model-diff.js","sourceRoot":"","sources":["../../client/utils/board-model-diff.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * diffBoardModel (Phase Board-AI C3a) — 사용자 직접편집을 SceneEditOp[] 로 캡처하기 위한\n * *순수* 모델 diff. 모델러의 @model-changed 직전/직후 모델을 비교해 add/remove/modify/modifyScene\n * 를 산출 → recordDirectPatch 로 보내 다른 참여자/AI 에 방송(C3b).\n *\n * 범위(프로토타입): top-level 컴포넌트의 add/remove/modify + 보드 root 속성(modifyScene).\n * 중첩(group 내부) 편집은 coarse — 추후 정밀화. 순수·결정적 → 단위테스트.\n */\nimport type { SceneEditOp } from '@operato/scene-ops'\n\ninterface AnyModel {\n components?: any[]\n [k: string]: any\n}\n\n/** prev → next 모델 차이를 SceneEditOp[] 로. 변화 없으면 빈 배열. */\nexport function diffBoardModel(prev: AnyModel | null | undefined, next: AnyModel | null | undefined): SceneEditOp[] {\n const ops: SceneEditOp[] = []\n const prevComps = indexByRefid(prev?.components)\n const nextComps = indexByRefid(next?.components)\n\n // remove — prev 에 있고 next 에 없음\n for (const refid of prevComps.keys()) {\n if (!nextComps.has(refid)) ops.push({ op: 'remove', refid } as SceneEditOp)\n }\n // add — next 에 있고 prev 에 없음 (전체 컴포넌트)\n for (const [refid, comp] of nextComps) {\n if (!prevComps.has(refid)) ops.push({ op: 'add', component: comp } as SceneEditOp)\n }\n // modify — 양쪽에 있고 속성 변경\n for (const [refid, nc] of nextComps) {\n const pc = prevComps.get(refid)\n if (!pc) continue\n const patch = propDiff(pc, nc, COMPONENT_SKIP)\n if (patch) ops.push({ op: 'modify', refid, patch } as SceneEditOp)\n }\n // modifyScene — 보드 root 속성(components 제외) 변경\n const boardPatch = propDiff(prev || {}, next || {}, BOARD_SKIP)\n if (boardPatch) ops.unshift({ op: 'modifyScene', patch: boardPatch } as SceneEditOp)\n\n return ops\n}\n\nconst COMPONENT_SKIP = new Set(['components'])\nconst BOARD_SKIP = new Set(['components'])\n\n/** top-level 컴포넌트를 refid 로 색인 (refid 없는 건 제외 — diff 불가). */\nfunction indexByRefid(components: any[] | undefined): Map<number, any> {\n const map = new Map<number, any>()\n if (!Array.isArray(components)) return map\n for (const c of components) {\n if (c && c.refid != null) map.set(c.refid, c)\n }\n return map\n}\n\n/**\n * a → b 의 속성 차이. b 에서 *변경되거나 추가된* 키만 (deep 비교는 JSON.stringify).\n * skip 키 제외. 차이 없으면 undefined.\n */\nfunction propDiff(a: any, b: any, skip: Set<string>): Record<string, any> | undefined {\n const patch: Record<string, any> = {}\n for (const key of Object.keys(b || {})) {\n if (skip.has(key)) continue\n if (stableStr(a?.[key]) !== stableStr(b[key])) patch[key] = b[key]\n }\n return Object.keys(patch).length ? patch : undefined\n}\n\nfunction stableStr(v: any): string {\n if (v === undefined) return '\u0000undef'\n try {\n return JSON.stringify(v)\n } catch {\n return String(v)\n }\n}\n"]}
@@ -1,7 +1,7 @@
1
1
  /*
2
2
  * PatchApplyGate (Phase Board-AI C2) — 같은 patch 를 모델러에서 *정확히 1회만* 적용 보장.
3
3
  * 실시간 공동편집에서 같은 patch 가 두 경로로 도달할 수 있다:
4
- * ① 로컬 낙관적 적용(onBoardEditPatch) ② 서버 브로드캐스트 구독(boardPatchActivity)
4
+ * ① 로컬 낙관적 적용(onSceneEditPatch) ② 서버 브로드캐스트 구독(boardPatchActivity)
5
5
  * 자기발신 echo + 두 경로 도착 순서 무관 — 먼저 claim 한 쪽이 적용, 나머지는 skip.
6
6
  *
7
7
  * patchId 없으면(영속 안 된 로컬 전용 patch) 게이팅 불가 → 항상 적용(true).
@@ -1 +1 @@
1
- {"version":3,"file":"patch-apply-gate.js","sourceRoot":"","sources":["../../client/utils/patch-apply-gate.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,MAAM,OAAO,cAAc;IAIzB,YAA6B,MAAM,GAAG;QAAT,QAAG,GAAH,GAAG,CAAM;QAHrB,YAAO,GAAG,IAAI,GAAG,EAAU,CAAA;QAC3B,UAAK,GAAa,EAAE,CAAA;IAEI,CAAC;IAE1C,uEAAuE;IACvE,QAAQ,CAAC,OAAkC;QACzC,IAAI,CAAC,OAAO;YAAE,OAAO,IAAI,CAAA;QACzB,IAAI,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC;YAAE,OAAO,KAAK,CAAA;QAC3C,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAA;QACzB,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;QACxB,IAAI,IAAI,CAAC,KAAK,CAAC,MAAM,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;YACjC,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAA;YAClC,IAAI,OAAO,KAAK,SAAS;gBAAE,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;QACzD,CAAC;QACD,OAAO,IAAI,CAAA;IACb,CAAC;IAED,wBAAwB;IACxB,GAAG,CAAC,OAAe;QACjB,OAAO,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAA;IAClC,CAAC;IAED,iCAAiC;IACjC,IAAI,IAAI;QACN,OAAO,IAAI,CAAC,OAAO,CAAC,IAAI,CAAA;IAC1B,CAAC;CACF","sourcesContent":["/*\n * PatchApplyGate (Phase Board-AI C2) — 같은 patch 를 모델러에서 *정확히 1회만* 적용 보장.\n * 실시간 공동편집에서 같은 patch 가 두 경로로 도달할 수 있다:\n * ① 로컬 낙관적 적용(onBoardEditPatch) ② 서버 브로드캐스트 구독(boardPatchActivity)\n * 자기발신 echo + 두 경로 도착 순서 무관 — 먼저 claim 한 쪽이 적용, 나머지는 skip.\n *\n * patchId 없으면(영속 안 된 로컬 전용 patch) 게이팅 불가 → 항상 적용(true).\n * 메모리 bound — 오래된 id 부터 제거(FIFO).\n */\nexport class PatchApplyGate {\n private readonly claimed = new Set<string>()\n private readonly order: string[] = []\n\n constructor(private readonly cap = 500) {}\n\n /** 아직 적용 전이면 claim 하고 true. 이미 claim 됐으면 false (skip). id 없으면 true. */\n tryClaim(patchId: string | undefined | null): boolean {\n if (!patchId) return true\n if (this.claimed.has(patchId)) return false\n this.claimed.add(patchId)\n this.order.push(patchId)\n if (this.order.length > this.cap) {\n const evicted = this.order.shift()\n if (evicted !== undefined) this.claimed.delete(evicted)\n }\n return true\n }\n\n /** 이미 claim(적용) 됐는지. */\n has(patchId: string): boolean {\n return this.claimed.has(patchId)\n }\n\n /** 현재 추적 중인 patch 수 (테스트/진단). */\n get size(): number {\n return this.claimed.size\n }\n}\n"]}
1
+ {"version":3,"file":"patch-apply-gate.js","sourceRoot":"","sources":["../../client/utils/patch-apply-gate.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,MAAM,OAAO,cAAc;IAIzB,YAA6B,MAAM,GAAG;QAAT,QAAG,GAAH,GAAG,CAAM;QAHrB,YAAO,GAAG,IAAI,GAAG,EAAU,CAAA;QAC3B,UAAK,GAAa,EAAE,CAAA;IAEI,CAAC;IAE1C,uEAAuE;IACvE,QAAQ,CAAC,OAAkC;QACzC,IAAI,CAAC,OAAO;YAAE,OAAO,IAAI,CAAA;QACzB,IAAI,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC;YAAE,OAAO,KAAK,CAAA;QAC3C,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAA;QACzB,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;QACxB,IAAI,IAAI,CAAC,KAAK,CAAC,MAAM,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;YACjC,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAA;YAClC,IAAI,OAAO,KAAK,SAAS;gBAAE,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;QACzD,CAAC;QACD,OAAO,IAAI,CAAA;IACb,CAAC;IAED,wBAAwB;IACxB,GAAG,CAAC,OAAe;QACjB,OAAO,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAA;IAClC,CAAC;IAED,iCAAiC;IACjC,IAAI,IAAI;QACN,OAAO,IAAI,CAAC,OAAO,CAAC,IAAI,CAAA;IAC1B,CAAC;CACF","sourcesContent":["/*\n * PatchApplyGate (Phase Board-AI C2) — 같은 patch 를 모델러에서 *정확히 1회만* 적용 보장.\n * 실시간 공동편집에서 같은 patch 가 두 경로로 도달할 수 있다:\n * ① 로컬 낙관적 적용(onSceneEditPatch) ② 서버 브로드캐스트 구독(boardPatchActivity)\n * 자기발신 echo + 두 경로 도착 순서 무관 — 먼저 claim 한 쪽이 적용, 나머지는 skip.\n *\n * patchId 없으면(영속 안 된 로컬 전용 patch) 게이팅 불가 → 항상 적용(true).\n * 메모리 bound — 오래된 id 부터 제거(FIFO).\n */\nexport class PatchApplyGate {\n private readonly claimed = new Set<string>()\n private readonly order: string[] = []\n\n constructor(private readonly cap = 500) {}\n\n /** 아직 적용 전이면 claim 하고 true. 이미 claim 됐으면 false (skip). id 없으면 true. */\n tryClaim(patchId: string | undefined | null): boolean {\n if (!patchId) return true\n if (this.claimed.has(patchId)) return false\n this.claimed.add(patchId)\n this.order.push(patchId)\n if (this.order.length > this.cap) {\n const evicted = this.order.shift()\n if (evicted !== undefined) this.claimed.delete(evicted)\n }\n return true\n }\n\n /** 이미 claim(적용) 됐는지. */\n has(patchId: string): boolean {\n return this.claimed.has(patchId)\n }\n\n /** 현재 추적 중인 patch 수 (테스트/진단). */\n get size(): number {\n return this.claimed.size\n }\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@things-factory/board-ui",
3
- "version": "10.1.24",
3
+ "version": "10.1.26",
4
4
  "main": "dist-server/index.js",
5
5
  "browser": "dist-client/index.js",
6
6
  "things-factory": true,
@@ -19,8 +19,8 @@
19
19
  "scripts": {
20
20
  "build": "npm run build:server && npm run build:client",
21
21
  "copy:files": "copyfiles -e \"./client/**/*.{ts,js,json}\" -u 1 \"./client/**/*\" dist-client",
22
- "build:client": "npm run copy:files && npx tsc --p ./client/tsconfig.json",
23
- "build:server": "npx tsc --p ./server/tsconfig.json",
22
+ "build:client": "npm run clean:client && npm run copy:files && npx tsc --p ./client/tsconfig.json",
23
+ "build:server": "npm run clean:server && npx tsc --p ./server/tsconfig.json",
24
24
  "clean:client": "npx rimraf dist-client",
25
25
  "clean:server": "npx rimraf dist-server",
26
26
  "clean": "npm run clean:server && npm run clean:client"
@@ -30,7 +30,7 @@
30
30
  "@material/web": "^2.0.0",
31
31
  "@operato/app": "^10.0.0",
32
32
  "@operato/attachment": "^10.0.0",
33
- "@operato/board": "^10.0.0",
33
+ "@operato/board": "^10.14.0",
34
34
  "@operato/data-grist": "^10.0.0",
35
35
  "@operato/graphql": "^10.0.0",
36
36
  "@operato/help": "^10.0.0",
@@ -39,13 +39,14 @@
39
39
  "@operato/oops": "^10.0.0",
40
40
  "@operato/popup": "^10.0.0",
41
41
  "@operato/pull-to-refresh": "^10.0.0",
42
+ "@operato/scene-ops": "^10.14.1",
42
43
  "@operato/shell": "^10.0.0",
43
44
  "@operato/styles": "^10.0.0",
44
45
  "@operato/utils": "^10.0.0",
45
- "@things-factory/barcode-base": "^10.1.24",
46
- "@things-factory/board-ai": "^10.1.24",
47
- "@things-factory/fav-base": "^10.1.24",
46
+ "@things-factory/barcode-base": "^10.1.26",
47
+ "@things-factory/board-ai": "^10.1.26",
48
+ "@things-factory/fav-base": "^10.1.26",
48
49
  "@things-factory/utils": "^10.1.20"
49
50
  },
50
- "gitHead": "0d88a504e0ffa63cd91279b88c6998d4bf1b3d86"
51
+ "gitHead": "151bda14d899442c6846532ddded1e2bfe23a2f5"
51
52
  }
@@ -1,39 +0,0 @@
1
- export interface BoardPlacementItem {
2
- id: string;
3
- label: string;
4
- sub?: string;
5
- /**
6
- * 계층에서의 부모 — 있으면 트레이가 **트리로** 보여 줄 수 있다. 없으면 최상위.
7
- *
8
- * 호스트가 안 주면 예전처럼 평평한 목록이다(선택 사항). 부모를 못 찾은 항목은 **버리지 않고**
9
- * 최상위로 올린다 — 목록에서 조용히 사라지면 저작하는 사람은 "그런 게 없네" 로 끝난다.
10
- */
11
- parentId?: string;
12
- /** 종류 묶음 — 있으면 트레이가 **종류별로** 보여 줄 수 있다(같은 종류 여럿에 한 번에 배치). */
13
- group?: string;
14
- }
15
- /** 한 항목의 가능한 표현(N:M). key = 호스트가 정하는 표현 식별자(같은 항목 안에서 유일해야 한다). */
16
- export interface BoardPlacementView {
17
- key: string;
18
- label: string;
19
- sub?: string;
20
- }
21
- export interface BoardPlacementProvider {
22
- label?: string;
23
- /** 이 board 의 배치 후보(전체). 비어있으면 트레이 숨김. boardId 로 컨텍스트 해소. */
24
- items(boardId: string): BoardPlacementItem[] | Promise<BoardPlacementItem[]>;
25
- /** 항목의 가능한 표현들(능력×목적, N:M). 없으면(미지정) 단일 componentFor 로 동작. */
26
- viewsFor?(itemId: string): BoardPlacementView[];
27
- /** 선택한 view(또는 기본)로 추가할 things-scene 컴포넌트 model(반드시 `tag` 포함). null=표현 없음. */
28
- componentFor(item: BoardPlacementItem, viewKey?: string): any;
29
- /**
30
- * 배치된 컴포넌트 → 그것이 어느 표현인지(`viewsFor` 가 준 key). 트레이가 "이미 놓았는지" 를 판단한다.
31
- *
32
- * 구현하지 않으면 트레이는 컴포넌트 `type` 을 key 로 보고 비교한다 — `viewsFor` 의 key 가 type 과 다른
33
- * 호스트는 **반드시 구현해야 한다.** 구현하지 않으면 이미 놓은 표현도 계속 "안 놓음" 으로 보이고,
34
- * "모두 자동 배치" 가 같은 것을 또 놓는다.
35
- */
36
- viewKeyOfComponent?(component: any): string | null;
37
- }
38
- export declare function registerBoardPlacementProvider(p: BoardPlacementProvider): void;
39
- export declare function boardPlacementProvider(): BoardPlacementProvider | null;
@@ -1,8 +0,0 @@
1
- let _provider = null;
2
- export function registerBoardPlacementProvider(p) {
3
- _provider = p;
4
- }
5
- export function boardPlacementProvider() {
6
- return _provider;
7
- }
8
- //# sourceMappingURL=board-placement-palette.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"board-placement-palette.js","sourceRoot":"","sources":["../client/board-placement-palette.ts"],"names":[],"mappings":"AAiDA,IAAI,SAAS,GAAkC,IAAI,CAAA;AACnD,MAAM,UAAU,8BAA8B,CAAC,CAAyB;IACtE,SAAS,GAAG,CAAC,CAAA;AACf,CAAC;AACD,MAAM,UAAU,sBAAsB;IACpC,OAAO,SAAS,CAAA;AAClB,CAAC","sourcesContent":["/*\n * board 배치 팔레트 — 일반(표현-무관) 확장점. 호스트가 \"보드에 배치할 항목\"을 공급하고,\n * board-modeller 가 트레이로 노출 + place 시 컴포넌트를 생성한다. board 제품(@operato/board)은 무변경,\n * 이 통합층(board-ui)이 훅만 제공. 트윈은 이 훅의 호스트 구현자일 뿐(트윈 전용 로직 없음).\n *\n * 대칭: operato-twin map-modeller 의 \"미배치 노드 트레이\"(A2)를 board 표현으로 확장하는 일반 메커니즘.\n * place 는 board-edit 'add' op(기존 채널)로 컴포넌트를 추가하고, 컴포넌트 model 의 `tag` 로 라이브 결합\n * (board-provider 가 data(tag:) 구독). 이미 배치됨 = scene 에 같은 tag 컴포넌트 존재.\n */\nexport interface BoardPlacementItem {\n id: string // 곧 컴포넌트 tag (라이브 결합 키)\n label: string\n sub?: string\n /**\n * 계층에서의 부모 — 있으면 트레이가 **트리로** 보여 줄 수 있다. 없으면 최상위.\n *\n * 호스트가 안 주면 예전처럼 평평한 목록이다(선택 사항). 부모를 못 찾은 항목은 **버리지 않고**\n * 최상위로 올린다 — 목록에서 조용히 사라지면 저작하는 사람은 \"그런 게 없네\" 로 끝난다.\n */\n parentId?: string\n /** 종류 묶음 — 있으면 트레이가 **종류별로** 보여 줄 수 있다(같은 종류 여럿에 한 번에 배치). */\n group?: string\n}\n\n/** 한 항목의 가능한 표현(N:M). key = 호스트가 정하는 표현 식별자(같은 항목 안에서 유일해야 한다). */\nexport interface BoardPlacementView {\n key: string\n label: string\n sub?: string\n}\n\nexport interface BoardPlacementProvider {\n label?: string // 트레이 제목(기본 \"미배치\")\n /** 이 board 의 배치 후보(전체). 비어있으면 트레이 숨김. boardId 로 컨텍스트 해소. */\n items(boardId: string): BoardPlacementItem[] | Promise<BoardPlacementItem[]>\n /** 항목의 가능한 표현들(능력×목적, N:M). 없으면(미지정) 단일 componentFor 로 동작. */\n viewsFor?(itemId: string): BoardPlacementView[]\n /** 선택한 view(또는 기본)로 추가할 things-scene 컴포넌트 model(반드시 `tag` 포함). null=표현 없음. */\n componentFor(item: BoardPlacementItem, viewKey?: string): any\n /**\n * 배치된 컴포넌트 → 그것이 어느 표현인지(`viewsFor` 가 준 key). 트레이가 \"이미 놓았는지\" 를 판단한다.\n *\n * 구현하지 않으면 트레이는 컴포넌트 `type` 을 key 로 보고 비교한다 — `viewsFor` 의 key 가 type 과 다른\n * 호스트는 **반드시 구현해야 한다.** 구현하지 않으면 이미 놓은 표현도 계속 \"안 놓음\" 으로 보이고,\n * \"모두 자동 배치\" 가 같은 것을 또 놓는다.\n */\n viewKeyOfComponent?(component: any): string | null\n}\n\nlet _provider: BoardPlacementProvider | null = null\nexport function registerBoardPlacementProvider(p: BoardPlacementProvider): void {\n _provider = p\n}\nexport function boardPlacementProvider(): BoardPlacementProvider | null {\n return _provider\n}\n"]}
@@ -1,3 +0,0 @@
1
- import { ReferenceMap } from '@hatiolab/things-scene';
2
- export declare function createBoardProvider(): ReferenceMap;
3
- export declare const provider: ReferenceMap;
@@ -1,87 +0,0 @@
1
- import gql from 'graphql-tag';
2
- import { create, error, ReferenceMap } from '@hatiolab/things-scene';
3
- import { BoardDataStorage, BoardModelCache } from '@operato/board';
4
- import { client, gqlContext, subscribe } from '@operato/graphql';
5
- export function createBoardProvider() {
6
- const _provider = new ReferenceMap(async (boardId, resolve, reject) => {
7
- const startTime = performance.now();
8
- try {
9
- const cached = await BoardModelCache.get(boardId);
10
- const response = await client.query({
11
- query: gql `
12
- query FetchBoardById($id: String!, $cachedUpdatedAt: String) {
13
- board(id: $id, cachedUpdatedAt: $cachedUpdatedAt) {
14
- model
15
- updatedAt
16
- }
17
- }
18
- `,
19
- variables: { id: boardId, cachedUpdatedAt: cached?.updatedAt },
20
- context: gqlContext()
21
- });
22
- const board = response.data?.board;
23
- if (!board) {
24
- reject(new Error('Failed to get the requested board.'));
25
- return;
26
- }
27
- let model;
28
- if (board.model === null && cached) {
29
- // 캐시 유효
30
- model = cached.model;
31
- }
32
- else {
33
- // 새 모델
34
- model = JSON.parse(board.model);
35
- await BoardModelCache.put(boardId, '', board.model, board.updatedAt);
36
- }
37
- let scene;
38
- try {
39
- scene = await provider.get(boardId);
40
- }
41
- catch (e) {
42
- scene = create({
43
- model,
44
- mode: 0,
45
- refProvider: provider,
46
- dataStorage: new BoardDataStorage(boardId),
47
- loading: { startTime },
48
- dataSubscriptionProvider: {
49
- subscribe: async (tag, component) => {
50
- return await subscribe({
51
- query: gql `
52
- subscription {
53
- data(tag: "${tag}") {
54
- tag
55
- data
56
- ts
57
- }
58
- }
59
- `
60
- }, {
61
- next: async ({ data }) => {
62
- if (data) {
63
- const payload = data.data;
64
- component.app?.lagTracker?.record?.(payload.tag, payload.ts);
65
- component.data = payload.data;
66
- }
67
- }
68
- });
69
- },
70
- dispose() { }
71
- }
72
- });
73
- // s.app.baseUrl = undefined;
74
- }
75
- resolve(scene);
76
- }
77
- catch (e) {
78
- error(e);
79
- reject(e);
80
- }
81
- }, async (id, ref) => {
82
- ref.dispose();
83
- });
84
- return _provider;
85
- }
86
- export const provider = createBoardProvider();
87
- //# sourceMappingURL=board-provider.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"board-provider.js","sourceRoot":"","sources":["../client/board-provider.ts"],"names":[],"mappings":"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","sourcesContent":["import gql from 'graphql-tag'\n\nimport { create, error, ReferenceMap, Scene } from '@hatiolab/things-scene'\nimport { BoardDataStorage, BoardModelCache } from '@operato/board'\nimport { client, gqlContext, subscribe } from '@operato/graphql'\n\nexport function createBoardProvider() {\n const _provider = new ReferenceMap(\n async (boardId, resolve, reject): Promise<any> => {\n const startTime = performance.now()\n try {\n const cached = await BoardModelCache.get(boardId)\n\n const response = await client.query({\n query: gql`\n query FetchBoardById($id: String!, $cachedUpdatedAt: String) {\n board(id: $id, cachedUpdatedAt: $cachedUpdatedAt) {\n model\n updatedAt\n }\n }\n `,\n variables: { id: boardId, cachedUpdatedAt: cached?.updatedAt },\n context: gqlContext()\n })\n\n const board = response.data?.board\n\n if (!board) {\n reject(new Error('Failed to get the requested board.'))\n return\n }\n\n let model: any\n if (board.model === null && cached) {\n // 캐시 유효\n model = cached.model\n } else {\n // 새 모델\n model = JSON.parse(board.model)\n await BoardModelCache.put(boardId, '', board.model, board.updatedAt)\n }\n\n let scene: Scene\n\n try {\n scene = await provider.get(boardId)\n } catch (e) {\n scene = create({\n model,\n mode: 0,\n refProvider: provider as any,\n dataStorage: new BoardDataStorage(boardId),\n loading: { startTime },\n dataSubscriptionProvider: {\n subscribe: async (tag, component) => {\n return await subscribe(\n {\n query: gql`\n subscription {\n data(tag: \"${tag}\") {\n tag\n data\n ts\n }\n }\n `\n },\n {\n next: async ({ data }) => {\n if (data) {\n const payload = data.data\n // 전달 지연 기록 (scene/app 레벨 트래커) — ts 는 component.data 에 넣지 않음.\n ;(component as any).app?.lagTracker?.record?.(payload.tag, payload.ts)\n component.data = payload.data\n }\n }\n }\n )\n },\n\n dispose() {}\n }\n } as any)\n\n // s.app.baseUrl = undefined;\n }\n\n resolve(scene)\n } catch (e) {\n error(e)\n reject(e)\n }\n },\n async (id, ref) => {\n ref.dispose()\n }\n )\n\n return _provider\n}\n\nexport const provider = createBoardProvider()\n"]}
@@ -1,31 +0,0 @@
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;
@@ -1,80 +0,0 @@
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
@@ -1 +0,0 @@
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"]}
@@ -1,74 +0,0 @@
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;