@things-factory/board-ui 10.1.20 → 10.1.25

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 (34) 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/utils/board-model-diff.d.ts +3 -3
  16. package/dist-client/utils/board-model-diff.js +0 -0
  17. package/dist-client/utils/board-model-diff.js.map +1 -1
  18. package/dist-client/utils/patch-apply-gate.js +1 -1
  19. package/dist-client/utils/patch-apply-gate.js.map +1 -1
  20. package/package.json +9 -8
  21. package/dist-client/board-placement-palette.d.ts +0 -39
  22. package/dist-client/board-placement-palette.js +0 -8
  23. package/dist-client/board-placement-palette.js.map +0 -1
  24. package/dist-client/board-provider.d.ts +0 -3
  25. package/dist-client/board-provider.js +0 -87
  26. package/dist-client/board-provider.js.map +0 -1
  27. package/dist-client/pages/board-action-dispatch.d.ts +0 -31
  28. package/dist-client/pages/board-action-dispatch.js +0 -80
  29. package/dist-client/pages/board-action-dispatch.js.map +0 -1
  30. package/dist-client/pages/board-edit-dispatch.d.ts +0 -74
  31. package/dist-client/pages/board-edit-dispatch.js +0 -299
  32. package/dist-client/pages/board-edit-dispatch.js.map +0 -1
  33. package/dist-client/tsconfig.tsbuildinfo +0 -1
  34. package/dist-server/tsconfig.tsbuildinfo +0 -1
@@ -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;
@@ -1,299 +0,0 @@
1
- import { mergeComponent } from '@things-factory/board-ai';
2
- import { findSceneComponent } from './board-action-dispatch';
3
- const NOOP_RESULT = { applied: false, inverseOps: [] };
4
- /**
5
- * 단일 BoardEditOp 를 things-scene 에 in-place 적용.
6
- *
7
- * `replace` 는 받지 않음 (호스트의 wholesale 경로). 미지원 op / 누락 컴포넌트 시
8
- * `applied: false` 반환. 호스트가 missed 로 분류해 사용자 경고.
9
- */
10
- export function dispatchBoardEditOp(scene, op, ctx = {}) {
11
- if (!scene || !op)
12
- return NOOP_RESULT;
13
- const normalize = ctx.normalize ?? ((c) => c);
14
- switch (op.op) {
15
- case 'add': {
16
- const normalized = normalize(op.component);
17
- // add 의 inverse 는 새로 발급될 refid 가 필요 → scene.add 직전/직후 차집합으로 캡처.
18
- const prevRefids = new Set(collectAllRefids(scene));
19
- scene.add(normalized, {});
20
- const newRefids = collectAllRefids(scene).filter(r => !prevRefids.has(r));
21
- const inverseOps = newRefids.map(refid => ({ op: 'remove', refid }));
22
- return { applied: true, inverseOps };
23
- }
24
- case 'remove': {
25
- const target = findSceneComponent(scene, { refid: op.refid });
26
- if (!target || !target.parent)
27
- return NOOP_RESULT;
28
- const savedModel = JSON.parse(JSON.stringify(target.model));
29
- const prevSelected = scene.selected ?? [];
30
- scene.selected = [target];
31
- scene.remove();
32
- scene.selected = prevSelected.filter((c) => c !== target);
33
- return { applied: true, inverseOps: [{ op: 'add', component: savedModel }] };
34
- }
35
- case 'modify': {
36
- const target = findSceneComponent(scene, { refid: op.refid });
37
- if (!target)
38
- return NOOP_RESULT;
39
- const oldValues = captureOldKeys(target.model, op.patch);
40
- const merged = mergeComponent(target.model, op.patch);
41
- target.set(merged);
42
- // change 는 자동 snapshot 안 함 → commander.execute 로 명시 push
43
- scene.commander?.execute(null, false);
44
- return {
45
- applied: true,
46
- inverseOps: [{ op: 'modify', refid: op.refid, patch: oldValues }]
47
- };
48
- }
49
- case 'modifyBoard': {
50
- const root = scene.root;
51
- if (!root || typeof root.set !== 'function')
52
- return NOOP_RESULT;
53
- const cleanPatch = { ...(op.patch || {}) };
54
- delete cleanPatch.components; // 자식 변경은 별도 op
55
- const oldValues = captureOldKeys(root.model, cleanPatch);
56
- const merged = mergeComponent(root.model, cleanPatch);
57
- root.set(merged);
58
- scene.commander?.execute(null, false);
59
- return { applied: true, inverseOps: [{ op: 'modifyBoard', patch: oldValues }] };
60
- }
61
- case 'align': {
62
- const targets = op.refids
63
- .map(r => findSceneComponent(scene, { refid: r }))
64
- .filter((c) => c);
65
- if (targets.length < 2)
66
- return NOOP_RESULT;
67
- const beforeBounds = targets.map((c) => ({
68
- refid: c.get('refid'),
69
- left: c.get('left'),
70
- top: c.get('top'),
71
- width: c.get('width'),
72
- height: c.get('height')
73
- }));
74
- const prevSelected = scene.selected ?? [];
75
- scene.selected = targets;
76
- scene.align(op.direction);
77
- scene.selected = prevSelected;
78
- // 정렬 직전 좌표를 modify inverse 시퀀스로 — 정확한 좌표 복원
79
- const inverseOps = beforeBounds.map(b => ({
80
- op: 'modify',
81
- refid: b.refid,
82
- patch: { left: b.left, top: b.top, width: b.width, height: b.height }
83
- }));
84
- return { applied: true, inverseOps };
85
- }
86
- case 'distribute': {
87
- const targets = op.refids
88
- .map(r => findSceneComponent(scene, { refid: r }))
89
- .filter((c) => c);
90
- if (targets.length < 2)
91
- return NOOP_RESULT;
92
- const beforeBounds = targets.map((c) => ({
93
- refid: c.get('refid'),
94
- left: c.get('left'),
95
- top: c.get('top')
96
- }));
97
- const prevSelected = scene.selected ?? [];
98
- scene.selected = targets;
99
- // things-scene API 는 'VERTICAL' / 'HORIZONTAL' 대문자
100
- scene.distribute(op.axis === 'horizontal' ? 'HORIZONTAL' : 'VERTICAL');
101
- scene.selected = prevSelected;
102
- const inverseOps = beforeBounds.map(b => ({
103
- op: 'modify',
104
- refid: b.refid,
105
- patch: { left: b.left, top: b.top }
106
- }));
107
- return { applied: true, inverseOps };
108
- }
109
- case 'group': {
110
- const targets = op.refids
111
- .map(r => findSceneComponent(scene, { refid: r }))
112
- .filter((c) => c);
113
- if (targets.length < 2)
114
- return NOOP_RESULT;
115
- const prevRefids = new Set(collectAllRefids(scene));
116
- const prevSelected = scene.selected ?? [];
117
- scene.selected = targets;
118
- scene.group();
119
- scene.selected = prevSelected;
120
- const newRefids = collectAllRefids(scene).filter(r => !prevRefids.has(r));
121
- const inverseOps = newRefids.map(refid => ({ op: 'ungroup', refid }));
122
- return { applied: true, inverseOps };
123
- }
124
- case 'ungroup': {
125
- const target = findSceneComponent(scene, { refid: op.refid });
126
- if (!target)
127
- return NOOP_RESULT;
128
- const childRefids = [];
129
- const children = target.components ?? [];
130
- for (const child of children) {
131
- const r = child.get?.('refid');
132
- if (typeof r === 'number')
133
- childRefids.push(r);
134
- }
135
- const prevSelected = scene.selected ?? [];
136
- scene.selected = [target];
137
- scene.ungroup();
138
- scene.selected = prevSelected.filter((c) => c !== target);
139
- const inverseOps = childRefids.length >= 2 ? [{ op: 'group', refids: childRefids }] : [];
140
- return { applied: true, inverseOps };
141
- }
142
- case 'zorder': {
143
- const target = findSceneComponent(scene, { refid: op.refid });
144
- if (!target)
145
- return NOOP_RESULT;
146
- const prevSelected = scene.selected ?? [];
147
- scene.selected = [target];
148
- scene.zorder(op.direction);
149
- scene.selected = prevSelected;
150
- // Best-effort: forward↔backward, front↔back. 정확하지 않을 수 있지만
151
- // 단일 step 단순 반전이라 대부분 OK.
152
- const opp = {
153
- forward: 'backward',
154
- backward: 'forward',
155
- front: 'back',
156
- back: 'front'
157
- };
158
- const dir = opp[op.direction];
159
- const inverseOps = dir
160
- ? [{ op: 'zorder', refid: op.refid, direction: dir }]
161
- : [];
162
- return { applied: true, inverseOps };
163
- }
164
- case 'arrange': {
165
- // Sugar layout — host 측에서 grid/row/column 위치 계산.
166
- // align/distribute 와 달리 things-scene 의 native API 가 없으므로 직접 좌표 계산
167
- // 해서 target.set() 으로 적용. left/top 만 변경, width/height 유지.
168
- const targets = op.refids
169
- .map(r => findSceneComponent(scene, { refid: r }))
170
- .filter((c) => c);
171
- if (targets.length < 2)
172
- return NOOP_RESULT;
173
- const beforePositions = targets.map((c) => ({
174
- refid: c.get('refid'),
175
- left: c.get('left'),
176
- top: c.get('top')
177
- }));
178
- const sizes = targets.map((c) => ({
179
- width: typeof c.get('width') === 'number' ? c.get('width') : 0,
180
- height: typeof c.get('height') === 'number' ? c.get('height') : 0
181
- }));
182
- const positions = computeArrangePositions(op.layout, beforePositions, sizes);
183
- for (let i = 0; i < targets.length; i++) {
184
- const t = targets[i];
185
- const pos = positions[i];
186
- const merged = mergeComponent(t.model, { left: pos.left, top: pos.top });
187
- t.set(merged);
188
- }
189
- // 단일 snapshot — N 개 modify 가 한 undo step
190
- scene.commander?.execute(null, false);
191
- const inverseOps = beforePositions.map(b => ({
192
- op: 'modify',
193
- refid: b.refid,
194
- patch: { left: b.left, top: b.top }
195
- }));
196
- return { applied: true, inverseOps };
197
- }
198
- case 'replace':
199
- // 'replace' 는 호스트의 wholesale 경로 — dispatcher 는 받지 않음.
200
- return NOOP_RESULT;
201
- default:
202
- return NOOP_RESULT;
203
- }
204
- }
205
- /**
206
- * Sugar layout 위치 계산 — grid / row / column.
207
- *
208
- * 정책:
209
- * - left/top 만 변경. width/height 는 보존 (사용자 의도).
210
- * - grid: cell size = max(width)/max(height) — 컴포넌트 사이즈 다를 때 겹침 방지.
211
- * Row-major 채움.
212
- * - row/column: 각 컴포넌트의 실 size + gap 으로 누적. align (start/center/end) 으로
213
- * cross-axis 정렬.
214
- * - anchor 미지정 시 첫 컴포넌트의 현재 (left, top) — 예측 가능.
215
- *
216
- * 입력: layout 정의 + 각 target 의 현재 (left, top) (anchor default 용) + sizes.
217
- * 출력: 각 target 의 새 (left, top). targets 와 동일 순서.
218
- *
219
- * Pure function — host dispatcher 외부에서도 단위 테스트 가능.
220
- */
221
- export function computeArrangePositions(layout, current, sizes) {
222
- if (current.length === 0)
223
- return [];
224
- const anchor = layout.anchor ?? { left: current[0].left, top: current[0].top };
225
- const gap = typeof layout.gap === 'number' ? layout.gap : 10;
226
- if (layout.type === 'grid') {
227
- const cols = Math.max(1, Math.floor(layout.cols));
228
- const cellW = sizes.reduce((m, s) => Math.max(m, s.width), 0);
229
- const cellH = sizes.reduce((m, s) => Math.max(m, s.height), 0);
230
- return current.map((_, i) => {
231
- const row = Math.floor(i / cols);
232
- const col = i % cols;
233
- return {
234
- left: anchor.left + col * (cellW + gap),
235
- top: anchor.top + row * (cellH + gap)
236
- };
237
- });
238
- }
239
- if (layout.type === 'row') {
240
- const align = layout.align ?? 'start';
241
- const maxH = sizes.reduce((m, s) => Math.max(m, s.height), 0);
242
- const out = [];
243
- let cursor = anchor.left;
244
- for (let i = 0; i < sizes.length; i++) {
245
- const s = sizes[i];
246
- let top = anchor.top;
247
- if (align === 'center')
248
- top = anchor.top + (maxH - s.height) / 2;
249
- else if (align === 'end')
250
- top = anchor.top + (maxH - s.height);
251
- out.push({ left: cursor, top });
252
- cursor += s.width + gap;
253
- }
254
- return out;
255
- }
256
- // column
257
- const align = layout.align ?? 'start';
258
- const maxW = sizes.reduce((m, s) => Math.max(m, s.width), 0);
259
- const out = [];
260
- let cursor = anchor.top;
261
- for (let i = 0; i < sizes.length; i++) {
262
- const s = sizes[i];
263
- let left = anchor.left;
264
- if (align === 'center')
265
- left = anchor.left + (maxW - s.width) / 2;
266
- else if (align === 'end')
267
- left = anchor.left + (maxW - s.width);
268
- out.push({ left, top: cursor });
269
- cursor += s.height + gap;
270
- }
271
- return out;
272
- }
273
- /**
274
- * scene 의 모든 컴포넌트 refid 수집 — add/group 의 inverse 계산용.
275
- *
276
- * scene.add / scene.group 직전·직후 호출해 차집합으로 새 발급 refid 식별.
277
- */
278
- export function collectAllRefids(scene) {
279
- const refids = [];
280
- const map = scene?.rootContainer?.refidIndexMap;
281
- if (map && typeof map.forEach === 'function') {
282
- map.forEach((_, refid) => refids.push(refid));
283
- }
284
- return refids;
285
- }
286
- /**
287
- * patch 가 변경하려는 키들에 대해 model 의 현재 값을 deep-clone 으로 캡처.
288
- * inverse modify / modifyBoard op 의 patch 로 사용. 원본에 없던 키는 null 로 보관
289
- * (revert 시 명시적 null reset).
290
- */
291
- export function captureOldKeys(model, patch) {
292
- const out = {};
293
- for (const k of Object.keys(patch || {})) {
294
- const v = model?.[k];
295
- out[k] = v === undefined ? null : JSON.parse(JSON.stringify(v));
296
- }
297
- return out;
298
- }
299
- //# sourceMappingURL=board-edit-dispatch.js.map