@things-factory/board-ai 10.0.0-beta.108
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/PLAN-styling-tools.md +1253 -0
- package/client/components/board-ai-chat.test.ts +149 -0
- package/client/components/board-ai-chat.ts +2574 -0
- package/client/components/chat-echo-dedup.test.ts +116 -0
- package/client/components/chat-echo-dedup.ts +89 -0
- package/client/components/chat-input-builder.ts +47 -0
- package/client/components/default-catalog-entries.test.ts +59 -0
- package/client/components/default-catalog-entries.ts +100 -0
- package/client/components/default-slash-templates.test.ts +55 -0
- package/client/components/default-slash-templates.ts +129 -0
- package/client/components/markdown.test.ts +518 -0
- package/client/components/markdown.ts +508 -0
- package/client/components/mention-popup-helpers.ts +433 -0
- package/client/components/mention-popup.test.ts +588 -0
- package/client/components/mention-popup.ts +238 -0
- package/client/index.ts +11 -0
- package/client/tsconfig.json +13 -0
- package/client/utils/board-edit-patch.ts +374 -0
- package/config/config.development.js +43 -0
- package/config/config.production.js +15 -0
- package/dist-client/client/components/board-ai-chat.d.ts +277 -0
- package/dist-client/client/components/board-ai-chat.js +2444 -0
- package/dist-client/client/components/board-ai-chat.js.map +1 -0
- package/dist-client/client/components/board-ai-chat.test.d.ts +1 -0
- package/dist-client/client/components/board-ai-chat.test.js +138 -0
- package/dist-client/client/components/board-ai-chat.test.js.map +1 -0
- package/dist-client/client/components/chat-echo-dedup.d.ts +45 -0
- package/dist-client/client/components/chat-echo-dedup.js +58 -0
- package/dist-client/client/components/chat-echo-dedup.js.map +1 -0
- package/dist-client/client/components/chat-echo-dedup.test.d.ts +1 -0
- package/dist-client/client/components/chat-echo-dedup.test.js +101 -0
- package/dist-client/client/components/chat-echo-dedup.test.js.map +1 -0
- package/dist-client/client/components/chat-input-builder.d.ts +39 -0
- package/dist-client/client/components/chat-input-builder.js +26 -0
- package/dist-client/client/components/chat-input-builder.js.map +1 -0
- package/dist-client/client/components/default-catalog-entries.d.ts +15 -0
- package/dist-client/client/components/default-catalog-entries.js +86 -0
- package/dist-client/client/components/default-catalog-entries.js.map +1 -0
- package/dist-client/client/components/default-catalog-entries.test.d.ts +1 -0
- package/dist-client/client/components/default-catalog-entries.test.js +52 -0
- package/dist-client/client/components/default-catalog-entries.test.js.map +1 -0
- package/dist-client/client/components/default-slash-templates.d.ts +17 -0
- package/dist-client/client/components/default-slash-templates.js +113 -0
- package/dist-client/client/components/default-slash-templates.js.map +1 -0
- package/dist-client/client/components/default-slash-templates.test.d.ts +1 -0
- package/dist-client/client/components/default-slash-templates.test.js +51 -0
- package/dist-client/client/components/default-slash-templates.test.js.map +1 -0
- package/dist-client/client/components/markdown.d.ts +60 -0
- package/dist-client/client/components/markdown.js +422 -0
- package/dist-client/client/components/markdown.js.map +1 -0
- package/dist-client/client/components/markdown.test.d.ts +1 -0
- package/dist-client/client/components/markdown.test.js +418 -0
- package/dist-client/client/components/markdown.test.js.map +1 -0
- package/dist-client/client/components/mention-popup-helpers.d.ts +165 -0
- package/dist-client/client/components/mention-popup-helpers.js +330 -0
- package/dist-client/client/components/mention-popup-helpers.js.map +1 -0
- package/dist-client/client/components/mention-popup.d.ts +35 -0
- package/dist-client/client/components/mention-popup.js +223 -0
- package/dist-client/client/components/mention-popup.js.map +1 -0
- package/dist-client/client/components/mention-popup.test.d.ts +1 -0
- package/dist-client/client/components/mention-popup.test.js +474 -0
- package/dist-client/client/components/mention-popup.test.js.map +1 -0
- package/dist-client/client/index.d.ts +11 -0
- package/dist-client/client/index.js +12 -0
- package/dist-client/client/index.js.map +1 -0
- package/dist-client/client/utils/board-edit-patch.d.ts +157 -0
- package/dist-client/client/utils/board-edit-patch.js +284 -0
- package/dist-client/client/utils/board-edit-patch.js.map +1 -0
- package/dist-client/server/service/agentic-loop.d.ts +87 -0
- package/dist-client/server/service/agentic-loop.js +276 -0
- package/dist-client/server/service/agentic-loop.js.map +1 -0
- package/dist-client/server/service/assistant.d.ts +84 -0
- package/dist-client/server/service/assistant.js +2220 -0
- package/dist-client/server/service/assistant.js.map +1 -0
- package/dist-client/server/service/catalog-resolver.d.ts +18 -0
- package/dist-client/server/service/catalog-resolver.js +93 -0
- package/dist-client/server/service/catalog-resolver.js.map +1 -0
- package/dist-client/server/service/component-tree.d.ts +68 -0
- package/dist-client/server/service/component-tree.js +68 -0
- package/dist-client/server/service/component-tree.js.map +1 -0
- package/dist-client/server/service/mention-resolver.d.ts +96 -0
- package/dist-client/server/service/mention-resolver.js +172 -0
- package/dist-client/server/service/mention-resolver.js.map +1 -0
- package/dist-client/server/service/prefab-registry.d.ts +73 -0
- package/dist-client/server/service/prefab-registry.js +252 -0
- package/dist-client/server/service/prefab-registry.js.map +1 -0
- package/dist-client/server/service/styling/clear-tools.d.ts +15 -0
- package/dist-client/server/service/styling/clear-tools.js +50 -0
- package/dist-client/server/service/styling/clear-tools.js.map +1 -0
- package/dist-client/server/service/styling/color-utils.d.ts +43 -0
- package/dist-client/server/service/styling/color-utils.js +205 -0
- package/dist-client/server/service/styling/color-utils.js.map +1 -0
- package/dist-client/server/service/styling/copy-tools.d.ts +27 -0
- package/dist-client/server/service/styling/copy-tools.js +78 -0
- package/dist-client/server/service/styling/copy-tools.js.map +1 -0
- package/dist-client/server/service/styling/effect-tools.d.ts +45 -0
- package/dist-client/server/service/styling/effect-tools.js +85 -0
- package/dist-client/server/service/styling/effect-tools.js.map +1 -0
- package/dist-client/server/service/styling/fill-tools.d.ts +60 -0
- package/dist-client/server/service/styling/fill-tools.js +226 -0
- package/dist-client/server/service/styling/fill-tools.js.map +1 -0
- package/dist-client/server/service/styling/material-tools.d.ts +32 -0
- package/dist-client/server/service/styling/material-tools.js +88 -0
- package/dist-client/server/service/styling/material-tools.js.map +1 -0
- package/dist-client/server/service/styling/read-tools.d.ts +34 -0
- package/dist-client/server/service/styling/read-tools.js +42 -0
- package/dist-client/server/service/styling/read-tools.js.map +1 -0
- package/dist-client/server/service/styling/registry.d.ts +91 -0
- package/dist-client/server/service/styling/registry.js +345 -0
- package/dist-client/server/service/styling/registry.js.map +1 -0
- package/dist-client/server/service/styling/stroke-tools.d.ts +35 -0
- package/dist-client/server/service/styling/stroke-tools.js +94 -0
- package/dist-client/server/service/styling/stroke-tools.js.map +1 -0
- package/dist-client/server/service/styling/text-tools.d.ts +32 -0
- package/dist-client/server/service/styling/text-tools.js +101 -0
- package/dist-client/server/service/styling/text-tools.js.map +1 -0
- package/dist-client/server/service/types.d.ts +265 -0
- package/dist-client/server/service/types.js +2 -0
- package/dist-client/server/service/types.js.map +1 -0
- package/dist-client/server/service/validation/board-model-schema.d.ts +8311 -0
- package/dist-client/server/service/validation/board-model-schema.js +402 -0
- package/dist-client/server/service/validation/board-model-schema.js.map +1 -0
- package/dist-client/server/service/validation/tool-validation.d.ts +31 -0
- package/dist-client/server/service/validation/tool-validation.js +193 -0
- package/dist-client/server/service/validation/tool-validation.js.map +1 -0
- package/dist-client/tsconfig.tsbuildinfo +1 -0
- package/dist-server/index.d.ts +23 -0
- package/dist-server/index.js +27 -0
- package/dist-server/index.js.map +1 -0
- package/dist-server/service/agentic-loop.d.ts +87 -0
- package/dist-server/service/agentic-loop.js +280 -0
- package/dist-server/service/agentic-loop.js.map +1 -0
- package/dist-server/service/apply-patch.d.ts +47 -0
- package/dist-server/service/apply-patch.js +369 -0
- package/dist-server/service/apply-patch.js.map +1 -0
- package/dist-server/service/assistant.d.ts +84 -0
- package/dist-server/service/assistant.js +2237 -0
- package/dist-server/service/assistant.js.map +1 -0
- package/dist-server/service/board-ai-resolver.d.ts +54 -0
- package/dist-server/service/board-ai-resolver.js +409 -0
- package/dist-server/service/board-ai-resolver.js.map +1 -0
- package/dist-server/service/catalog-resolver.d.ts +18 -0
- package/dist-server/service/catalog-resolver.js +98 -0
- package/dist-server/service/catalog-resolver.js.map +1 -0
- package/dist-server/service/chat-message/chat-message-broadcast.d.ts +13 -0
- package/dist-server/service/chat-message/chat-message-broadcast.js +13 -0
- package/dist-server/service/chat-message/chat-message-broadcast.js.map +1 -0
- package/dist-server/service/chat-message/chat-message-publish.d.ts +6 -0
- package/dist-server/service/chat-message/chat-message-publish.js +27 -0
- package/dist-server/service/chat-message/chat-message-publish.js.map +1 -0
- package/dist-server/service/chat-message/chat-message-subscription.d.ts +8 -0
- package/dist-server/service/chat-message/chat-message-subscription.js +53 -0
- package/dist-server/service/chat-message/chat-message-subscription.js.map +1 -0
- package/dist-server/service/chat-message/chat-message.d.ts +56 -0
- package/dist-server/service/chat-message/chat-message.js +149 -0
- package/dist-server/service/chat-message/chat-message.js.map +1 -0
- package/dist-server/service/chat-message/index.d.ts +5 -0
- package/dist-server/service/chat-message/index.js +12 -0
- package/dist-server/service/chat-message/index.js.map +1 -0
- package/dist-server/service/chat-session/chat-session.d.ts +31 -0
- package/dist-server/service/chat-session/chat-session.js +131 -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-participant/chat-session-participant.d.ts +30 -0
- package/dist-server/service/chat-session-participant/chat-session-participant.js +112 -0
- package/dist-server/service/chat-session-participant/chat-session-participant.js.map +1 -0
- package/dist-server/service/chat-session-participant/index.d.ts +4 -0
- package/dist-server/service/chat-session-participant/index.js +10 -0
- package/dist-server/service/chat-session-participant/index.js.map +1 -0
- package/dist-server/service/chat-session-participant/invite-notification.d.ts +27 -0
- package/dist-server/service/chat-session-participant/invite-notification.js +30 -0
- package/dist-server/service/chat-session-participant/invite-notification.js.map +1 -0
- package/dist-server/service/chat-session-participant/presence.d.ts +4 -0
- package/dist-server/service/chat-session-participant/presence.js +22 -0
- package/dist-server/service/chat-session-participant/presence.js.map +1 -0
- package/dist-server/service/chat-session-resolver.d.ts +20 -0
- package/dist-server/service/chat-session-resolver.js +356 -0
- package/dist-server/service/chat-session-resolver.js.map +1 -0
- package/dist-server/service/component-tree.d.ts +68 -0
- package/dist-server/service/component-tree.js +73 -0
- package/dist-server/service/component-tree.js.map +1 -0
- package/dist-server/service/index.d.ts +20 -0
- package/dist-server/service/index.js +34 -0
- package/dist-server/service/index.js.map +1 -0
- package/dist-server/service/mention-marker.d.ts +78 -0
- package/dist-server/service/mention-marker.js +162 -0
- package/dist-server/service/mention-marker.js.map +1 -0
- package/dist-server/service/mention-resolver.d.ts +96 -0
- package/dist-server/service/mention-resolver.js +177 -0
- package/dist-server/service/mention-resolver.js.map +1 -0
- package/dist-server/service/patch-entry/board-patch-broadcast.d.ts +13 -0
- package/dist-server/service/patch-entry/board-patch-broadcast.js +13 -0
- package/dist-server/service/patch-entry/board-patch-broadcast.js.map +1 -0
- package/dist-server/service/patch-entry/board-patch-subscription.d.ts +12 -0
- package/dist-server/service/patch-entry/board-patch-subscription.js +63 -0
- package/dist-server/service/patch-entry/board-patch-subscription.js.map +1 -0
- package/dist-server/service/patch-entry/index.d.ts +4 -0
- package/dist-server/service/patch-entry/index.js +12 -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/prefab-registry.d.ts +73 -0
- package/dist-server/service/prefab-registry.js +258 -0
- package/dist-server/service/prefab-registry.js.map +1 -0
- package/dist-server/service/styling/clear-tools.d.ts +15 -0
- package/dist-server/service/styling/clear-tools.js +53 -0
- package/dist-server/service/styling/clear-tools.js.map +1 -0
- package/dist-server/service/styling/color-utils.d.ts +43 -0
- package/dist-server/service/styling/color-utils.js +210 -0
- package/dist-server/service/styling/color-utils.js.map +1 -0
- package/dist-server/service/styling/copy-tools.d.ts +27 -0
- package/dist-server/service/styling/copy-tools.js +81 -0
- package/dist-server/service/styling/copy-tools.js.map +1 -0
- package/dist-server/service/styling/effect-tools.d.ts +45 -0
- package/dist-server/service/styling/effect-tools.js +90 -0
- package/dist-server/service/styling/effect-tools.js.map +1 -0
- package/dist-server/service/styling/fill-tools.d.ts +60 -0
- package/dist-server/service/styling/fill-tools.js +229 -0
- package/dist-server/service/styling/fill-tools.js.map +1 -0
- package/dist-server/service/styling/material-tools.d.ts +32 -0
- package/dist-server/service/styling/material-tools.js +92 -0
- package/dist-server/service/styling/material-tools.js.map +1 -0
- package/dist-server/service/styling/read-tools.d.ts +34 -0
- package/dist-server/service/styling/read-tools.js +45 -0
- package/dist-server/service/styling/read-tools.js.map +1 -0
- package/dist-server/service/styling/registry.d.ts +91 -0
- package/dist-server/service/styling/registry.js +352 -0
- package/dist-server/service/styling/registry.js.map +1 -0
- package/dist-server/service/styling/stroke-tools.d.ts +35 -0
- package/dist-server/service/styling/stroke-tools.js +97 -0
- package/dist-server/service/styling/stroke-tools.js.map +1 -0
- package/dist-server/service/styling/text-tools.d.ts +32 -0
- package/dist-server/service/styling/text-tools.js +105 -0
- package/dist-server/service/styling/text-tools.js.map +1 -0
- package/dist-server/service/subscribe-auth.d.ts +14 -0
- package/dist-server/service/subscribe-auth.js +33 -0
- package/dist-server/service/subscribe-auth.js.map +1 -0
- package/dist-server/service/types.d.ts +265 -0
- package/dist-server/service/types.js +3 -0
- package/dist-server/service/types.js.map +1 -0
- package/dist-server/service/validation/board-model-schema.d.ts +8311 -0
- package/dist-server/service/validation/board-model-schema.js +409 -0
- package/dist-server/service/validation/board-model-schema.js.map +1 -0
- package/dist-server/service/validation/tool-validation.d.ts +31 -0
- package/dist-server/service/validation/tool-validation.js +196 -0
- package/dist-server/service/validation/tool-validation.js.map +1 -0
- package/dist-server/tsconfig.tsbuildinfo +1 -0
- package/package.json +48 -0
- package/server/index.ts +23 -0
- package/server/service/agentic-loop.test.ts +705 -0
- package/server/service/agentic-loop.ts +431 -0
- package/server/service/apply-patch-drift.test.ts +829 -0
- package/server/service/apply-patch.test.ts +757 -0
- package/server/service/apply-patch.ts +411 -0
- package/server/service/assistant-integration.test.ts +922 -0
- package/server/service/assistant-llm-smoke.test.ts +296 -0
- package/server/service/assistant.test.ts +2034 -0
- package/server/service/assistant.ts +2378 -0
- package/server/service/board-ai-resolver.ts +407 -0
- package/server/service/catalog-resolver.test.ts +137 -0
- package/server/service/catalog-resolver.ts +102 -0
- package/server/service/chat-message/chat-message-broadcast.integration.test.ts +73 -0
- package/server/service/chat-message/chat-message-broadcast.test.ts +40 -0
- package/server/service/chat-message/chat-message-broadcast.ts +27 -0
- package/server/service/chat-message/chat-message-publish.ts +22 -0
- package/server/service/chat-message/chat-message-subscription.ts +41 -0
- package/server/service/chat-message/chat-message.ts +169 -0
- package/server/service/chat-message/index.ts +12 -0
- package/server/service/chat-session/chat-session.ts +130 -0
- package/server/service/chat-session/index.ts +5 -0
- package/server/service/chat-session-participant/chat-session-participant.ts +110 -0
- package/server/service/chat-session-participant/index.ts +6 -0
- package/server/service/chat-session-participant/invite-notification.test.ts +30 -0
- package/server/service/chat-session-participant/invite-notification.ts +48 -0
- package/server/service/chat-session-participant/presence.test.ts +39 -0
- package/server/service/chat-session-participant/presence.ts +21 -0
- package/server/service/chat-session-resolver.ts +312 -0
- package/server/service/component-tree.test.ts +248 -0
- package/server/service/component-tree.ts +135 -0
- package/server/service/index.ts +32 -0
- package/server/service/mention-marker.extract.test.ts +61 -0
- package/server/service/mention-marker.test.ts +228 -0
- package/server/service/mention-marker.ts +184 -0
- package/server/service/mention-resolver.test.ts +273 -0
- package/server/service/mention-resolver.ts +264 -0
- package/server/service/patch-entry/board-patch-broadcast.ts +24 -0
- package/server/service/patch-entry/board-patch-subscription.test.ts +40 -0
- package/server/service/patch-entry/board-patch-subscription.ts +50 -0
- package/server/service/patch-entry/index.ts +12 -0
- package/server/service/patch-entry/patch-entry.ts +89 -0
- package/server/service/prefab-registry.test.ts +172 -0
- package/server/service/prefab-registry.ts +332 -0
- package/server/service/styling/clear-tools.test.ts +164 -0
- package/server/service/styling/clear-tools.ts +69 -0
- package/server/service/styling/color-utils.test.ts +130 -0
- package/server/service/styling/color-utils.ts +226 -0
- package/server/service/styling/copy-tools.test.ts +152 -0
- package/server/service/styling/copy-tools.ts +115 -0
- package/server/service/styling/effect-tools.test.ts +277 -0
- package/server/service/styling/effect-tools.ts +124 -0
- package/server/service/styling/fill-tools.test.ts +331 -0
- package/server/service/styling/fill-tools.ts +293 -0
- package/server/service/styling/material-tools.test.ts +294 -0
- package/server/service/styling/material-tools.ts +114 -0
- package/server/service/styling/read-tools.test.ts +78 -0
- package/server/service/styling/read-tools.ts +67 -0
- package/server/service/styling/registry.test.ts +203 -0
- package/server/service/styling/registry.ts +423 -0
- package/server/service/styling/stroke-tools.test.ts +245 -0
- package/server/service/styling/stroke-tools.ts +145 -0
- package/server/service/styling/text-tools.test.ts +253 -0
- package/server/service/styling/text-tools.ts +131 -0
- package/server/service/styling/tier1-tools.test.ts +300 -0
- package/server/service/subscribe-auth.test.ts +57 -0
- package/server/service/subscribe-auth.ts +34 -0
- package/server/service/types.ts +235 -0
- package/server/service/validation/board-model-schema.test.ts +778 -0
- package/server/service/validation/board-model-schema.ts +446 -0
- package/server/service/validation/tool-validation.ts +228 -0
- package/things-factory.config.js +1 -0
- package/translations/en.json +49 -0
- package/translations/ja.json +48 -0
- package/translations/ko.json +49 -0
- package/translations/ms.json +48 -0
- package/translations/zh.json +48 -0
- package/tsconfig.json +9 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* copy-tools — copyStyle source → targets 복사.
|
|
3
|
+
*/
|
|
4
|
+
import { buildCopyStyleOps } from './copy-tools'
|
|
5
|
+
|
|
6
|
+
const COMPONENTS = [
|
|
7
|
+
{
|
|
8
|
+
refid: 35,
|
|
9
|
+
type: 'rect',
|
|
10
|
+
fillStyle: { type: 'solid', color: '#ff0000' },
|
|
11
|
+
strokeStyle: { color: '#000', width: 2 },
|
|
12
|
+
shadow: { blur: 4 },
|
|
13
|
+
material3d: { preset: 'metal' },
|
|
14
|
+
fontFamily: 'Inter',
|
|
15
|
+
fontSize: 14,
|
|
16
|
+
alpha: 0.9,
|
|
17
|
+
hidden: false
|
|
18
|
+
},
|
|
19
|
+
{ refid: 36, type: 'rect' }, // styling 없음
|
|
20
|
+
{ refid: 37, type: 'rect' },
|
|
21
|
+
{ refid: 38, type: 'circle' }
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
describe('buildCopyStyleOps — 검증', () => {
|
|
25
|
+
test('sourceRefid 비-숫자', () => {
|
|
26
|
+
const r = buildCopyStyleOps({} as any, COMPONENTS)
|
|
27
|
+
expect(r.ops).toEqual([])
|
|
28
|
+
expect(r.errors[0]).toMatch(/sourceRefid/)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
test('targetRefids 빈 배열', () => {
|
|
32
|
+
const r = buildCopyStyleOps(
|
|
33
|
+
{ sourceRefid: 35, targetRefids: [] },
|
|
34
|
+
COMPONENTS
|
|
35
|
+
)
|
|
36
|
+
expect(r.ops).toEqual([])
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
test('source 없음', () => {
|
|
40
|
+
const r = buildCopyStyleOps(
|
|
41
|
+
{ sourceRefid: 999, targetRefids: [36] },
|
|
42
|
+
COMPONENTS
|
|
43
|
+
)
|
|
44
|
+
expect(r.ops).toEqual([])
|
|
45
|
+
expect(r.errors[0]).toMatch(/Source.*not found/)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
test('알 수 없는 aspect', () => {
|
|
49
|
+
const r = buildCopyStyleOps(
|
|
50
|
+
{ sourceRefid: 35, targetRefids: [36], aspects: ['xyz' as any] },
|
|
51
|
+
COMPONENTS
|
|
52
|
+
)
|
|
53
|
+
expect(r.ops).toEqual([])
|
|
54
|
+
expect(r.errors[0]).toMatch(/Unknown aspect/)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
test('source 가 target 에 포함되면 자동 제외', () => {
|
|
58
|
+
const r = buildCopyStyleOps(
|
|
59
|
+
{ sourceRefid: 35, targetRefids: [35, 36, 37] },
|
|
60
|
+
COMPONENTS
|
|
61
|
+
)
|
|
62
|
+
expect(r.ops).toHaveLength(2) // 35 제외
|
|
63
|
+
expect(r.ops.map(o => (o as any).refid).sort()).toEqual([36, 37])
|
|
64
|
+
})
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
describe('buildCopyStyleOps — 정상 동작', () => {
|
|
68
|
+
test('default aspects (visibility 제외) — 모든 styling 복사', () => {
|
|
69
|
+
const r = buildCopyStyleOps(
|
|
70
|
+
{ sourceRefid: 35, targetRefids: [36, 37] },
|
|
71
|
+
COMPONENTS
|
|
72
|
+
)
|
|
73
|
+
expect(r.ops).toHaveLength(2)
|
|
74
|
+
const patch = (r.ops[0] as any).patch
|
|
75
|
+
expect(patch.fillStyle).toEqual({ type: 'solid', color: '#ff0000' })
|
|
76
|
+
expect(patch.strokeStyle).toEqual({ color: '#000', width: 2 })
|
|
77
|
+
expect(patch.shadow).toEqual({ blur: 4 })
|
|
78
|
+
expect(patch.material3d).toEqual({ preset: 'metal' })
|
|
79
|
+
expect(patch.fontFamily).toBe('Inter')
|
|
80
|
+
expect(patch.fontSize).toBe(14)
|
|
81
|
+
expect(patch.alpha).toBe(0.9)
|
|
82
|
+
// visibility default 제외
|
|
83
|
+
expect(patch.hidden).toBeUndefined()
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
test('aspects 부분 — fill 만', () => {
|
|
87
|
+
const r = buildCopyStyleOps(
|
|
88
|
+
{ sourceRefid: 35, targetRefids: [36], aspects: ['fill'] },
|
|
89
|
+
COMPONENTS
|
|
90
|
+
)
|
|
91
|
+
const patch = (r.ops[0] as any).patch
|
|
92
|
+
expect(patch.fillStyle).toBeDefined()
|
|
93
|
+
expect(patch.strokeStyle).toBeUndefined()
|
|
94
|
+
expect(patch.shadow).toBeUndefined()
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
test('aspects 다중 — fill + stroke', () => {
|
|
98
|
+
const r = buildCopyStyleOps(
|
|
99
|
+
{
|
|
100
|
+
sourceRefid: 35,
|
|
101
|
+
targetRefids: [36],
|
|
102
|
+
aspects: ['fill', 'stroke']
|
|
103
|
+
},
|
|
104
|
+
COMPONENTS
|
|
105
|
+
)
|
|
106
|
+
const patch = (r.ops[0] as any).patch
|
|
107
|
+
expect(patch.fillStyle).toBeDefined()
|
|
108
|
+
expect(patch.strokeStyle).toBeDefined()
|
|
109
|
+
expect(patch.shadow).toBeUndefined()
|
|
110
|
+
expect(patch.fontFamily).toBeUndefined()
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
test('source 가 styling 없는 aspect — patch 에 누락', () => {
|
|
114
|
+
const sparse = [
|
|
115
|
+
{ refid: 100, type: 'rect', fillStyle: { color: 'red' } },
|
|
116
|
+
{ refid: 101, type: 'rect' }
|
|
117
|
+
]
|
|
118
|
+
const r = buildCopyStyleOps(
|
|
119
|
+
{ sourceRefid: 100, targetRefids: [101] },
|
|
120
|
+
sparse
|
|
121
|
+
)
|
|
122
|
+
const patch = (r.ops[0] as any).patch
|
|
123
|
+
expect(patch.fillStyle).toBeDefined()
|
|
124
|
+
expect(patch.strokeStyle).toBeUndefined()
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
test('source 가 모든 aspect 비어있으면 — 에러', () => {
|
|
128
|
+
const empty = [
|
|
129
|
+
{ refid: 200, type: 'rect' },
|
|
130
|
+
{ refid: 201, type: 'rect' }
|
|
131
|
+
]
|
|
132
|
+
const r = buildCopyStyleOps(
|
|
133
|
+
{ sourceRefid: 200, targetRefids: [201] },
|
|
134
|
+
empty
|
|
135
|
+
)
|
|
136
|
+
expect(r.ops).toEqual([])
|
|
137
|
+
expect(r.errors[0]).toMatch(/no styling/)
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
test('visibility aspect 명시 → hidden 복사', () => {
|
|
141
|
+
const c = [
|
|
142
|
+
{ refid: 1, type: 'rect', hidden: true },
|
|
143
|
+
{ refid: 2, type: 'rect' }
|
|
144
|
+
]
|
|
145
|
+
const r = buildCopyStyleOps(
|
|
146
|
+
{ sourceRefid: 1, targetRefids: [2], aspects: ['visibility'] },
|
|
147
|
+
c
|
|
148
|
+
)
|
|
149
|
+
const patch = (r.ops[0] as any).patch
|
|
150
|
+
expect(patch.hidden).toBe(true)
|
|
151
|
+
})
|
|
152
|
+
})
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* copyStyle — 한 컴포넌트의 styling 을 다른 컴포넌트들에 복사.
|
|
3
|
+
*
|
|
4
|
+
* 자주 쓰는 패턴: "MOTOR1 처럼 다른 모터들도 같게 만들어줘"
|
|
5
|
+
* AI: getComponentByRefid(MOTOR1) 또는 getComponentStyle(MOTOR1) → copyStyle(...)
|
|
6
|
+
*
|
|
7
|
+
* aspects 인자로 어느 styling 영역만 복사할지 선택 가능 — 부분 통일 (예: 색만 통일).
|
|
8
|
+
*
|
|
9
|
+
* 입력 — sourceRefid 의 실제 styling 값을 미리 알 필요 없음. server 가 components
|
|
10
|
+
* 에서 source 찾아 styling 추출 후 target 들에 patch 생성.
|
|
11
|
+
*/
|
|
12
|
+
import type { BoardEditOp } from '../types'
|
|
13
|
+
|
|
14
|
+
export interface CopyStyleInput {
|
|
15
|
+
sourceRefid: number
|
|
16
|
+
targetRefids: number[]
|
|
17
|
+
aspects?: Array<'fill' | 'stroke' | 'shadow' | 'material' | 'font' | 'opacity' | 'visibility'>
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface OpsResult {
|
|
21
|
+
ops: BoardEditOp[]
|
|
22
|
+
errors: string[]
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const ASPECT_FIELDS: Record<string, string[]> = {
|
|
26
|
+
fill: ['fillStyle'],
|
|
27
|
+
stroke: ['strokeStyle'],
|
|
28
|
+
shadow: ['shadow'],
|
|
29
|
+
material: ['material3d'],
|
|
30
|
+
font: [
|
|
31
|
+
'fontFamily',
|
|
32
|
+
'fontSize',
|
|
33
|
+
'fontWeight',
|
|
34
|
+
'fontStyle',
|
|
35
|
+
'fontColor',
|
|
36
|
+
'textAlign',
|
|
37
|
+
'textBaseline',
|
|
38
|
+
'lineHeight'
|
|
39
|
+
],
|
|
40
|
+
opacity: ['alpha'],
|
|
41
|
+
visibility: ['hidden']
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const DEFAULT_ASPECTS: Array<keyof typeof ASPECT_FIELDS> = [
|
|
45
|
+
'fill',
|
|
46
|
+
'stroke',
|
|
47
|
+
'shadow',
|
|
48
|
+
'material',
|
|
49
|
+
'font',
|
|
50
|
+
'opacity'
|
|
51
|
+
// visibility 는 default 에서 제외 — 의도 없이 hidden 복사 위험
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* input + components 배열 → BoardEditOp[].
|
|
56
|
+
*
|
|
57
|
+
* components 는 board 의 모든 컴포넌트 (caller 가 전달). source 찾아 styling 추출.
|
|
58
|
+
*/
|
|
59
|
+
export function buildCopyStyleOps(
|
|
60
|
+
input: CopyStyleInput,
|
|
61
|
+
components: ReadonlyArray<any>
|
|
62
|
+
): OpsResult {
|
|
63
|
+
if (typeof input.sourceRefid !== 'number') {
|
|
64
|
+
return { ops: [], errors: ['sourceRefid (number) required'] }
|
|
65
|
+
}
|
|
66
|
+
if (!Array.isArray(input.targetRefids) || input.targetRefids.length === 0) {
|
|
67
|
+
return { ops: [], errors: ['targetRefids must be a non-empty array'] }
|
|
68
|
+
}
|
|
69
|
+
const targets = input.targetRefids.filter(
|
|
70
|
+
n => typeof n === 'number' && Number.isFinite(n) && n !== input.sourceRefid
|
|
71
|
+
)
|
|
72
|
+
if (targets.length === 0) {
|
|
73
|
+
return { ops: [], errors: ['No valid target refids (excluding source)'] }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const source = components.find(
|
|
77
|
+
c => c && typeof c.refid === 'number' && c.refid === input.sourceRefid
|
|
78
|
+
)
|
|
79
|
+
if (!source) {
|
|
80
|
+
return {
|
|
81
|
+
ops: [],
|
|
82
|
+
errors: [`Source component refid=${input.sourceRefid} not found`]
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const aspects = input.aspects && input.aspects.length > 0 ? input.aspects : DEFAULT_ASPECTS
|
|
87
|
+
const validAspects = Object.keys(ASPECT_FIELDS)
|
|
88
|
+
const unknown = aspects.filter(a => !validAspects.includes(a))
|
|
89
|
+
if (unknown.length > 0) {
|
|
90
|
+
return {
|
|
91
|
+
ops: [],
|
|
92
|
+
errors: [`Unknown aspect(s): ${unknown.join(', ')}. Valid: ${validAspects.join('/')}`]
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// source 에서 aspect 별 필드 추출 → patch 합치기
|
|
97
|
+
const patch: any = {}
|
|
98
|
+
for (const aspect of aspects) {
|
|
99
|
+
const fields = ASPECT_FIELDS[aspect]
|
|
100
|
+
for (const field of fields) {
|
|
101
|
+
if (source[field] !== undefined) {
|
|
102
|
+
patch[field] = source[field]
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (Object.keys(patch).length === 0) {
|
|
108
|
+
return { ops: [], errors: ['Source has no styling for the requested aspects'] }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
ops: targets.map(refid => ({ op: 'modify', refid, patch })),
|
|
113
|
+
errors: []
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* effect-tools — setShadow / setOpacity / setVisible 테스트.
|
|
3
|
+
*
|
|
4
|
+
* shadow 의 canonical fields (CSS box-shadow / Canvas API 표준):
|
|
5
|
+
* { color, offsetX, offsetY, blur }
|
|
6
|
+
* things-scene drawer 가 옛 이름 (left/top/blurSize) 도 backwards-compat 으로
|
|
7
|
+
* 읽지만, 새 코드는 canonical 만 사용.
|
|
8
|
+
*
|
|
9
|
+
* setOpacity → state.alpha (canonical), setVisible → state.hidden (visible 의 inverse).
|
|
10
|
+
*/
|
|
11
|
+
import { buildSetShadowOps, buildSetOpacityOps, buildSetVisibleOps } from './effect-tools'
|
|
12
|
+
|
|
13
|
+
describe('buildSetShadowOps — 입력 검증', () => {
|
|
14
|
+
test('refids 비어있으면 에러', () => {
|
|
15
|
+
const r = buildSetShadowOps({ refids: [], color: 'black' })
|
|
16
|
+
expect(r.ops).toEqual([])
|
|
17
|
+
expect(r.errors[0]).toMatch(/non-empty array/)
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
test('비-숫자 refids 필터링', () => {
|
|
21
|
+
const r = buildSetShadowOps({
|
|
22
|
+
refids: [1, NaN, 'x' as any, 2],
|
|
23
|
+
color: 'black'
|
|
24
|
+
})
|
|
25
|
+
expect(r.ops).toHaveLength(2)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
test('아무 인자도 없으면 에러', () => {
|
|
29
|
+
const r = buildSetShadowOps({ refids: [1] })
|
|
30
|
+
expect(r.ops).toEqual([])
|
|
31
|
+
expect(r.errors[0]).toMatch(/No shadow changes/)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
test('잘못된 색상 → 에러', () => {
|
|
35
|
+
const r = buildSetShadowOps({ refids: [1], color: 'invalid-color' })
|
|
36
|
+
expect(r.ops).toEqual([])
|
|
37
|
+
expect(r.errors[0]).toMatch(/Invalid/)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
test('blur 음수 → 에러', () => {
|
|
41
|
+
const r = buildSetShadowOps({ refids: [1], blur: -1 })
|
|
42
|
+
expect(r.ops).toEqual([])
|
|
43
|
+
expect(r.errors[0]).toMatch(/Invalid blur/)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
test('blur Infinity → 에러', () => {
|
|
47
|
+
const r = buildSetShadowOps({ refids: [1], blur: Infinity })
|
|
48
|
+
expect(r.ops).toEqual([])
|
|
49
|
+
expect(r.errors[0]).toMatch(/Invalid blur/)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
test('blur 0 허용', () => {
|
|
53
|
+
const r = buildSetShadowOps({ refids: [1], blur: 0, color: 'black' })
|
|
54
|
+
expect(r.ops).toHaveLength(1)
|
|
55
|
+
expect(r.ops[0].patch).toEqual({ shadow: { color: '#000000', blur: 0 } })
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
test('offsetX NaN → 에러', () => {
|
|
59
|
+
const r = buildSetShadowOps({ refids: [1], offsetX: NaN })
|
|
60
|
+
expect(r.ops).toEqual([])
|
|
61
|
+
expect(r.errors[0]).toMatch(/Invalid offsetX/)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
test('offsetY Infinity → 에러', () => {
|
|
65
|
+
const r = buildSetShadowOps({ refids: [1], offsetY: Infinity })
|
|
66
|
+
expect(r.ops).toEqual([])
|
|
67
|
+
expect(r.errors[0]).toMatch(/Invalid offsetY/)
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
test('offsetX 음수 허용 (CSS box-shadow 정상 사용)', () => {
|
|
71
|
+
const r = buildSetShadowOps({ refids: [1], offsetX: -3 })
|
|
72
|
+
expect(r.ops).toHaveLength(1)
|
|
73
|
+
expect(r.ops[0].patch).toEqual({ shadow: { offsetX: -3 } })
|
|
74
|
+
})
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
describe('buildSetShadowOps — canonical fields (CSS / Canvas 표준)', () => {
|
|
78
|
+
test('blur 검증', () => {
|
|
79
|
+
const r = buildSetShadowOps({ refids: [1], blur: 8 })
|
|
80
|
+
expect(r.ops[0].patch).toEqual({ shadow: { blur: 8 } })
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
test('offsetX 만', () => {
|
|
84
|
+
const r = buildSetShadowOps({ refids: [1], offsetX: 5 })
|
|
85
|
+
expect(r.ops[0].patch).toEqual({ shadow: { offsetX: 5 } })
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
test('offsetY 만', () => {
|
|
89
|
+
const r = buildSetShadowOps({ refids: [1], offsetY: 10 })
|
|
90
|
+
expect(r.ops[0].patch).toEqual({ shadow: { offsetY: 10 } })
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
test('color 정규화', () => {
|
|
94
|
+
const r = buildSetShadowOps({ refids: [1], color: 'red' })
|
|
95
|
+
expect(r.ops[0].patch).toEqual({ shadow: { color: '#ff0000' } })
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
test('전체 필드 — color/blur/offsetX/offsetY', () => {
|
|
99
|
+
const r = buildSetShadowOps({
|
|
100
|
+
refids: [1],
|
|
101
|
+
color: 'navy',
|
|
102
|
+
blur: 4,
|
|
103
|
+
offsetX: 3,
|
|
104
|
+
offsetY: 2
|
|
105
|
+
})
|
|
106
|
+
expect(r.ops[0].patch).toEqual({
|
|
107
|
+
shadow: {
|
|
108
|
+
color: '#000080',
|
|
109
|
+
blur: 4,
|
|
110
|
+
offsetX: 3,
|
|
111
|
+
offsetY: 2
|
|
112
|
+
}
|
|
113
|
+
})
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
test('offsetX/Y 0 명시 — 실제 0 으로 저장', () => {
|
|
117
|
+
const r = buildSetShadowOps({ refids: [1], offsetX: 0, offsetY: 0 })
|
|
118
|
+
expect(r.ops[0].patch).toEqual({ shadow: { offsetX: 0, offsetY: 0 } })
|
|
119
|
+
})
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
describe('buildSetShadowOps — enabled=false (그림자 제거)', () => {
|
|
123
|
+
test('enabled=false → blur 0 + transparent (drawer 가 안 그림)', () => {
|
|
124
|
+
const r = buildSetShadowOps({ refids: [1], enabled: false })
|
|
125
|
+
expect(r.ops).toHaveLength(1)
|
|
126
|
+
expect(r.ops[0].patch).toEqual({
|
|
127
|
+
shadow: { blur: 0, color: 'transparent' }
|
|
128
|
+
})
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
test('enabled=false 가 다른 인자보다 우선 — color/blur 무시하고 제거', () => {
|
|
132
|
+
const r = buildSetShadowOps({
|
|
133
|
+
refids: [1],
|
|
134
|
+
enabled: false,
|
|
135
|
+
color: 'red',
|
|
136
|
+
blur: 8
|
|
137
|
+
})
|
|
138
|
+
expect(r.ops[0].patch).toEqual({
|
|
139
|
+
shadow: { blur: 0, color: 'transparent' }
|
|
140
|
+
})
|
|
141
|
+
})
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
describe('buildSetShadowOps — multi-target', () => {
|
|
145
|
+
test('동일 shadow 가 여러 refid 에 분배', () => {
|
|
146
|
+
const r = buildSetShadowOps({
|
|
147
|
+
refids: [10, 20, 30],
|
|
148
|
+
color: 'black',
|
|
149
|
+
blur: 6
|
|
150
|
+
})
|
|
151
|
+
expect(r.ops).toHaveLength(3)
|
|
152
|
+
expect(r.ops[0].patch).toEqual({
|
|
153
|
+
shadow: { color: '#000000', blur: 6 }
|
|
154
|
+
})
|
|
155
|
+
expect(r.ops[2]).toMatchObject({
|
|
156
|
+
refid: 30,
|
|
157
|
+
patch: { shadow: { color: '#000000', blur: 6 } }
|
|
158
|
+
})
|
|
159
|
+
})
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
describe('buildSetOpacityOps — 입력 검증', () => {
|
|
163
|
+
test('refids 비어있으면 에러', () => {
|
|
164
|
+
const r = buildSetOpacityOps({ refids: [], alpha: 0.5 })
|
|
165
|
+
expect(r.ops).toEqual([])
|
|
166
|
+
expect(r.errors[0]).toMatch(/non-empty array/)
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
test('opacity 가 number 가 아니면 에러', () => {
|
|
170
|
+
const r = buildSetOpacityOps({ refids: [1], alpha: 'half' as any })
|
|
171
|
+
expect(r.ops).toEqual([])
|
|
172
|
+
expect(r.errors[0]).toMatch(/alpha must be 0..1/)
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
test('opacity > 1 → 에러', () => {
|
|
176
|
+
const r = buildSetOpacityOps({ refids: [1], alpha: 1.5 })
|
|
177
|
+
expect(r.ops).toEqual([])
|
|
178
|
+
expect(r.errors[0]).toMatch(/alpha must be 0..1/)
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
test('opacity < 0 → 에러', () => {
|
|
182
|
+
const r = buildSetOpacityOps({ refids: [1], alpha: -0.1 })
|
|
183
|
+
expect(r.ops).toEqual([])
|
|
184
|
+
expect(r.errors[0]).toMatch(/alpha must be 0..1/)
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
test('opacity 0 (완전 투명) 허용', () => {
|
|
188
|
+
const r = buildSetOpacityOps({ refids: [1], alpha: 0 })
|
|
189
|
+
expect(r.ops).toHaveLength(1)
|
|
190
|
+
expect(r.ops[0].patch).toEqual({ alpha: 0 })
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
test('opacity 1 (완전 불투명) 허용', () => {
|
|
194
|
+
const r = buildSetOpacityOps({ refids: [1], alpha: 1 })
|
|
195
|
+
expect(r.ops[0].patch).toEqual({ alpha: 1 })
|
|
196
|
+
})
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
describe('buildSetOpacityOps — alpha 매핑', () => {
|
|
200
|
+
test('opacity 0.5 → state.alpha 0.5', () => {
|
|
201
|
+
const r = buildSetOpacityOps({ refids: [1], alpha: 0.5 })
|
|
202
|
+
expect(r.ops[0].patch).toEqual({ alpha: 0.5 })
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
test('multi-target 동일 alpha', () => {
|
|
206
|
+
const r = buildSetOpacityOps({ refids: [1, 2, 3], alpha: 0.7 })
|
|
207
|
+
expect(r.ops).toHaveLength(3)
|
|
208
|
+
expect(r.ops.every(o => (o as any).patch.alpha === 0.7)).toBe(true)
|
|
209
|
+
})
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
describe('buildSetVisibleOps — 입력 검증', () => {
|
|
213
|
+
test('refids 비어있으면 에러', () => {
|
|
214
|
+
const r = buildSetVisibleOps({ refids: [], visible: true })
|
|
215
|
+
expect(r.ops).toEqual([])
|
|
216
|
+
expect(r.errors[0]).toMatch(/non-empty array/)
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
test('visible 이 boolean 이 아니면 에러', () => {
|
|
220
|
+
const r = buildSetVisibleOps({ refids: [1], visible: 'yes' as any })
|
|
221
|
+
expect(r.ops).toEqual([])
|
|
222
|
+
expect(r.errors[0]).toMatch(/visible must be a boolean/)
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
test('visible=undefined → 에러', () => {
|
|
226
|
+
const r = buildSetVisibleOps({ refids: [1], visible: undefined as any })
|
|
227
|
+
expect(r.ops).toEqual([])
|
|
228
|
+
})
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
describe('buildSetVisibleOps — hidden = !visible', () => {
|
|
232
|
+
test('visible=true → hidden=false', () => {
|
|
233
|
+
const r = buildSetVisibleOps({ refids: [1], visible: true })
|
|
234
|
+
expect(r.ops[0].patch).toEqual({ hidden: false })
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
test('visible=false → hidden=true', () => {
|
|
238
|
+
const r = buildSetVisibleOps({ refids: [1], visible: false })
|
|
239
|
+
expect(r.ops[0].patch).toEqual({ hidden: true })
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
test('multi-target 일괄 hide', () => {
|
|
243
|
+
const r = buildSetVisibleOps({ refids: [1, 2, 3, 4, 5], visible: false })
|
|
244
|
+
expect(r.ops).toHaveLength(5)
|
|
245
|
+
expect(r.ops.every(o => (o as any).patch.hidden === true)).toBe(true)
|
|
246
|
+
})
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
describe('effect-tools — 시나리오', () => {
|
|
250
|
+
test('카드에 부드러운 그림자', () => {
|
|
251
|
+
const r = buildSetShadowOps({
|
|
252
|
+
refids: [42],
|
|
253
|
+
color: 'rgba(0,0,0,0.2)',
|
|
254
|
+
blur: 12,
|
|
255
|
+
offsetX: 0,
|
|
256
|
+
offsetY: 4
|
|
257
|
+
})
|
|
258
|
+
// rgba 는 정규화되어 hex 로 변환 (alpha 채널 손실, color-utils 의 정책)
|
|
259
|
+
expect(r.ops[0].patch.shadow).toMatchObject({
|
|
260
|
+
blur: 12,
|
|
261
|
+
offsetX: 0,
|
|
262
|
+
offsetY: 4
|
|
263
|
+
})
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
test('비활성 컴포넌트 — 50% 투명', () => {
|
|
267
|
+
const r = buildSetOpacityOps({ refids: [1, 2], alpha: 0.5 })
|
|
268
|
+
expect(r.ops).toHaveLength(2)
|
|
269
|
+
expect(r.ops[0].patch).toEqual({ alpha: 0.5 })
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
test('숨김 토글 — 다중 컴포넌트', () => {
|
|
273
|
+
const r = buildSetVisibleOps({ refids: [10, 11, 12], visible: false })
|
|
274
|
+
expect(r.ops).toHaveLength(3)
|
|
275
|
+
expect(r.ops[0].patch).toEqual({ hidden: true })
|
|
276
|
+
})
|
|
277
|
+
})
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* setShadow / setOpacity / setVisible — 효과 / 가시성 / 투명도.
|
|
3
|
+
*
|
|
4
|
+
* setShadow: state.shadow (color/blur/offsetX/offsetY)
|
|
5
|
+
* setOpacity: state.alpha (컴포넌트 전체 알파, 0..1)
|
|
6
|
+
* setVisible: state.hidden 토글 (true → 숨김)
|
|
7
|
+
*/
|
|
8
|
+
import type { BoardEditOp } from '../types'
|
|
9
|
+
import { normalizeColor, validateColor } from './color-utils'
|
|
10
|
+
|
|
11
|
+
export interface SetShadowInput {
|
|
12
|
+
refids: number[]
|
|
13
|
+
/** synthetic — false 면 blur=0 + color=transparent 매핑 (drawer 가 안 그림). 모델에 enabled 필드 없음. */
|
|
14
|
+
enabled?: boolean
|
|
15
|
+
/** state.shadow.color */
|
|
16
|
+
color?: string
|
|
17
|
+
/** Blur radius (CSS box-shadow 의 blur-radius, Canvas 의 shadowBlur). state.shadow.blur. */
|
|
18
|
+
blur?: number
|
|
19
|
+
/** Horizontal offset (CSS box-shadow 의 offset-x, Canvas 의 shadowOffsetX). state.shadow.offsetX. */
|
|
20
|
+
offsetX?: number
|
|
21
|
+
/** Vertical offset (CSS box-shadow 의 offset-y, Canvas 의 shadowOffsetY). state.shadow.offsetY. */
|
|
22
|
+
offsetY?: number
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface SetOpacityInput {
|
|
26
|
+
refids: number[]
|
|
27
|
+
/** state.alpha 의 canonical 이름. 0..1. */
|
|
28
|
+
alpha: number
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface SetVisibleInput {
|
|
32
|
+
refids: number[]
|
|
33
|
+
visible: boolean
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface OpsResult {
|
|
37
|
+
ops: BoardEditOp[]
|
|
38
|
+
errors: string[]
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* setShadow — CSS box-shadow / Canvas API 표준 이름 (color/offsetX/offsetY/blur) 그대로
|
|
43
|
+
* state.shadow 에 set. things-scene drawer 는 옛 이름 (left/top/blurSize) 도
|
|
44
|
+
* backwards-compat 으로 읽지만 (effect/shadow.ts:36-47), 새 모델은 canonical 만 사용.
|
|
45
|
+
*
|
|
46
|
+
* synthetic enabled=false → blur 0 + transparent 매핑 (drawer 가 안 그림).
|
|
47
|
+
* 모델에 enabled 필드는 없음.
|
|
48
|
+
*/
|
|
49
|
+
export function buildSetShadowOps(input: SetShadowInput): OpsResult {
|
|
50
|
+
if (!Array.isArray(input.refids) || input.refids.length === 0) {
|
|
51
|
+
return { ops: [], errors: ['refids must be a non-empty array'] }
|
|
52
|
+
}
|
|
53
|
+
const refids = input.refids.filter(n => typeof n === 'number' && Number.isFinite(n))
|
|
54
|
+
if (refids.length === 0) return { ops: [], errors: ['No valid numeric refids'] }
|
|
55
|
+
|
|
56
|
+
const noOp =
|
|
57
|
+
input.enabled === undefined &&
|
|
58
|
+
input.color === undefined &&
|
|
59
|
+
input.blur === undefined &&
|
|
60
|
+
input.offsetX === undefined &&
|
|
61
|
+
input.offsetY === undefined
|
|
62
|
+
if (noOp) return { ops: [], errors: ['No shadow changes specified'] }
|
|
63
|
+
|
|
64
|
+
if (input.enabled === false) {
|
|
65
|
+
// synthetic — false 면 blur 0 + transparent 으로 변환 (drawer 가 안 그림)
|
|
66
|
+
const patch = { shadow: { blur: 0, color: 'transparent' } }
|
|
67
|
+
return { ops: refids.map(refid => ({ op: 'modify', refid, patch })), errors: [] }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (input.color !== undefined) {
|
|
71
|
+
const v = validateColor(input.color)
|
|
72
|
+
if (!v.valid) return { ops: [], errors: [v.error ?? 'Invalid shadow color'] }
|
|
73
|
+
}
|
|
74
|
+
if (input.blur !== undefined && (input.blur < 0 || !Number.isFinite(input.blur))) {
|
|
75
|
+
return { ops: [], errors: [`Invalid blur: ${input.blur}`] }
|
|
76
|
+
}
|
|
77
|
+
if (input.offsetX !== undefined && !Number.isFinite(input.offsetX)) {
|
|
78
|
+
return { ops: [], errors: [`Invalid offsetX: ${input.offsetX}`] }
|
|
79
|
+
}
|
|
80
|
+
if (input.offsetY !== undefined && !Number.isFinite(input.offsetY)) {
|
|
81
|
+
return { ops: [], errors: [`Invalid offsetY: ${input.offsetY}`] }
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// CSS / Canvas 표준 canonical 이름 — rename 없음
|
|
85
|
+
const sh: Record<string, number | string> = {}
|
|
86
|
+
// validateColor 가 통과한 색만 도달 — normalizeColor 가 여기서 null 반환 안 함
|
|
87
|
+
if (input.color !== undefined) sh.color = normalizeColor(input.color)!
|
|
88
|
+
if (input.blur !== undefined) sh.blur = input.blur
|
|
89
|
+
if (input.offsetX !== undefined) sh.offsetX = input.offsetX
|
|
90
|
+
if (input.offsetY !== undefined) sh.offsetY = input.offsetY
|
|
91
|
+
|
|
92
|
+
const patch = { shadow: sh }
|
|
93
|
+
return { ops: refids.map(refid => ({ op: 'modify', refid, patch })), errors: [] }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function buildSetOpacityOps(input: SetOpacityInput): OpsResult {
|
|
97
|
+
if (!Array.isArray(input.refids) || input.refids.length === 0) {
|
|
98
|
+
return { ops: [], errors: ['refids must be a non-empty array'] }
|
|
99
|
+
}
|
|
100
|
+
if (typeof input.alpha !== 'number' || input.alpha < 0 || input.alpha > 1) {
|
|
101
|
+
return { ops: [], errors: [`alpha must be 0..1: ${input.alpha}`] }
|
|
102
|
+
}
|
|
103
|
+
const refids = input.refids.filter(n => typeof n === 'number' && Number.isFinite(n))
|
|
104
|
+
if (refids.length === 0) return { ops: [], errors: ['No valid numeric refids'] }
|
|
105
|
+
|
|
106
|
+
// canonical 이름 그대로
|
|
107
|
+
const patch = { alpha: input.alpha }
|
|
108
|
+
return { ops: refids.map(refid => ({ op: 'modify', refid, patch })), errors: [] }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function buildSetVisibleOps(input: SetVisibleInput): OpsResult {
|
|
112
|
+
if (!Array.isArray(input.refids) || input.refids.length === 0) {
|
|
113
|
+
return { ops: [], errors: ['refids must be a non-empty array'] }
|
|
114
|
+
}
|
|
115
|
+
if (typeof input.visible !== 'boolean') {
|
|
116
|
+
return { ops: [], errors: ['visible must be a boolean'] }
|
|
117
|
+
}
|
|
118
|
+
const refids = input.refids.filter(n => typeof n === 'number' && Number.isFinite(n))
|
|
119
|
+
if (refids.length === 0) return { ops: [], errors: ['No valid numeric refids'] }
|
|
120
|
+
|
|
121
|
+
// hidden 은 visible 의 inverse
|
|
122
|
+
const patch = { hidden: !input.visible }
|
|
123
|
+
return { ops: refids.map(refid => ({ op: 'modify', refid, patch })), errors: [] }
|
|
124
|
+
}
|