@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.
Files changed (329) hide show
  1. package/PLAN-styling-tools.md +1253 -0
  2. package/client/components/board-ai-chat.test.ts +149 -0
  3. package/client/components/board-ai-chat.ts +2574 -0
  4. package/client/components/chat-echo-dedup.test.ts +116 -0
  5. package/client/components/chat-echo-dedup.ts +89 -0
  6. package/client/components/chat-input-builder.ts +47 -0
  7. package/client/components/default-catalog-entries.test.ts +59 -0
  8. package/client/components/default-catalog-entries.ts +100 -0
  9. package/client/components/default-slash-templates.test.ts +55 -0
  10. package/client/components/default-slash-templates.ts +129 -0
  11. package/client/components/markdown.test.ts +518 -0
  12. package/client/components/markdown.ts +508 -0
  13. package/client/components/mention-popup-helpers.ts +433 -0
  14. package/client/components/mention-popup.test.ts +588 -0
  15. package/client/components/mention-popup.ts +238 -0
  16. package/client/index.ts +11 -0
  17. package/client/tsconfig.json +13 -0
  18. package/client/utils/board-edit-patch.ts +374 -0
  19. package/config/config.development.js +43 -0
  20. package/config/config.production.js +15 -0
  21. package/dist-client/client/components/board-ai-chat.d.ts +277 -0
  22. package/dist-client/client/components/board-ai-chat.js +2444 -0
  23. package/dist-client/client/components/board-ai-chat.js.map +1 -0
  24. package/dist-client/client/components/board-ai-chat.test.d.ts +1 -0
  25. package/dist-client/client/components/board-ai-chat.test.js +138 -0
  26. package/dist-client/client/components/board-ai-chat.test.js.map +1 -0
  27. package/dist-client/client/components/chat-echo-dedup.d.ts +45 -0
  28. package/dist-client/client/components/chat-echo-dedup.js +58 -0
  29. package/dist-client/client/components/chat-echo-dedup.js.map +1 -0
  30. package/dist-client/client/components/chat-echo-dedup.test.d.ts +1 -0
  31. package/dist-client/client/components/chat-echo-dedup.test.js +101 -0
  32. package/dist-client/client/components/chat-echo-dedup.test.js.map +1 -0
  33. package/dist-client/client/components/chat-input-builder.d.ts +39 -0
  34. package/dist-client/client/components/chat-input-builder.js +26 -0
  35. package/dist-client/client/components/chat-input-builder.js.map +1 -0
  36. package/dist-client/client/components/default-catalog-entries.d.ts +15 -0
  37. package/dist-client/client/components/default-catalog-entries.js +86 -0
  38. package/dist-client/client/components/default-catalog-entries.js.map +1 -0
  39. package/dist-client/client/components/default-catalog-entries.test.d.ts +1 -0
  40. package/dist-client/client/components/default-catalog-entries.test.js +52 -0
  41. package/dist-client/client/components/default-catalog-entries.test.js.map +1 -0
  42. package/dist-client/client/components/default-slash-templates.d.ts +17 -0
  43. package/dist-client/client/components/default-slash-templates.js +113 -0
  44. package/dist-client/client/components/default-slash-templates.js.map +1 -0
  45. package/dist-client/client/components/default-slash-templates.test.d.ts +1 -0
  46. package/dist-client/client/components/default-slash-templates.test.js +51 -0
  47. package/dist-client/client/components/default-slash-templates.test.js.map +1 -0
  48. package/dist-client/client/components/markdown.d.ts +60 -0
  49. package/dist-client/client/components/markdown.js +422 -0
  50. package/dist-client/client/components/markdown.js.map +1 -0
  51. package/dist-client/client/components/markdown.test.d.ts +1 -0
  52. package/dist-client/client/components/markdown.test.js +418 -0
  53. package/dist-client/client/components/markdown.test.js.map +1 -0
  54. package/dist-client/client/components/mention-popup-helpers.d.ts +165 -0
  55. package/dist-client/client/components/mention-popup-helpers.js +330 -0
  56. package/dist-client/client/components/mention-popup-helpers.js.map +1 -0
  57. package/dist-client/client/components/mention-popup.d.ts +35 -0
  58. package/dist-client/client/components/mention-popup.js +223 -0
  59. package/dist-client/client/components/mention-popup.js.map +1 -0
  60. package/dist-client/client/components/mention-popup.test.d.ts +1 -0
  61. package/dist-client/client/components/mention-popup.test.js +474 -0
  62. package/dist-client/client/components/mention-popup.test.js.map +1 -0
  63. package/dist-client/client/index.d.ts +11 -0
  64. package/dist-client/client/index.js +12 -0
  65. package/dist-client/client/index.js.map +1 -0
  66. package/dist-client/client/utils/board-edit-patch.d.ts +157 -0
  67. package/dist-client/client/utils/board-edit-patch.js +284 -0
  68. package/dist-client/client/utils/board-edit-patch.js.map +1 -0
  69. package/dist-client/server/service/agentic-loop.d.ts +87 -0
  70. package/dist-client/server/service/agentic-loop.js +276 -0
  71. package/dist-client/server/service/agentic-loop.js.map +1 -0
  72. package/dist-client/server/service/assistant.d.ts +84 -0
  73. package/dist-client/server/service/assistant.js +2220 -0
  74. package/dist-client/server/service/assistant.js.map +1 -0
  75. package/dist-client/server/service/catalog-resolver.d.ts +18 -0
  76. package/dist-client/server/service/catalog-resolver.js +93 -0
  77. package/dist-client/server/service/catalog-resolver.js.map +1 -0
  78. package/dist-client/server/service/component-tree.d.ts +68 -0
  79. package/dist-client/server/service/component-tree.js +68 -0
  80. package/dist-client/server/service/component-tree.js.map +1 -0
  81. package/dist-client/server/service/mention-resolver.d.ts +96 -0
  82. package/dist-client/server/service/mention-resolver.js +172 -0
  83. package/dist-client/server/service/mention-resolver.js.map +1 -0
  84. package/dist-client/server/service/prefab-registry.d.ts +73 -0
  85. package/dist-client/server/service/prefab-registry.js +252 -0
  86. package/dist-client/server/service/prefab-registry.js.map +1 -0
  87. package/dist-client/server/service/styling/clear-tools.d.ts +15 -0
  88. package/dist-client/server/service/styling/clear-tools.js +50 -0
  89. package/dist-client/server/service/styling/clear-tools.js.map +1 -0
  90. package/dist-client/server/service/styling/color-utils.d.ts +43 -0
  91. package/dist-client/server/service/styling/color-utils.js +205 -0
  92. package/dist-client/server/service/styling/color-utils.js.map +1 -0
  93. package/dist-client/server/service/styling/copy-tools.d.ts +27 -0
  94. package/dist-client/server/service/styling/copy-tools.js +78 -0
  95. package/dist-client/server/service/styling/copy-tools.js.map +1 -0
  96. package/dist-client/server/service/styling/effect-tools.d.ts +45 -0
  97. package/dist-client/server/service/styling/effect-tools.js +85 -0
  98. package/dist-client/server/service/styling/effect-tools.js.map +1 -0
  99. package/dist-client/server/service/styling/fill-tools.d.ts +60 -0
  100. package/dist-client/server/service/styling/fill-tools.js +226 -0
  101. package/dist-client/server/service/styling/fill-tools.js.map +1 -0
  102. package/dist-client/server/service/styling/material-tools.d.ts +32 -0
  103. package/dist-client/server/service/styling/material-tools.js +88 -0
  104. package/dist-client/server/service/styling/material-tools.js.map +1 -0
  105. package/dist-client/server/service/styling/read-tools.d.ts +34 -0
  106. package/dist-client/server/service/styling/read-tools.js +42 -0
  107. package/dist-client/server/service/styling/read-tools.js.map +1 -0
  108. package/dist-client/server/service/styling/registry.d.ts +91 -0
  109. package/dist-client/server/service/styling/registry.js +345 -0
  110. package/dist-client/server/service/styling/registry.js.map +1 -0
  111. package/dist-client/server/service/styling/stroke-tools.d.ts +35 -0
  112. package/dist-client/server/service/styling/stroke-tools.js +94 -0
  113. package/dist-client/server/service/styling/stroke-tools.js.map +1 -0
  114. package/dist-client/server/service/styling/text-tools.d.ts +32 -0
  115. package/dist-client/server/service/styling/text-tools.js +101 -0
  116. package/dist-client/server/service/styling/text-tools.js.map +1 -0
  117. package/dist-client/server/service/types.d.ts +265 -0
  118. package/dist-client/server/service/types.js +2 -0
  119. package/dist-client/server/service/types.js.map +1 -0
  120. package/dist-client/server/service/validation/board-model-schema.d.ts +8311 -0
  121. package/dist-client/server/service/validation/board-model-schema.js +402 -0
  122. package/dist-client/server/service/validation/board-model-schema.js.map +1 -0
  123. package/dist-client/server/service/validation/tool-validation.d.ts +31 -0
  124. package/dist-client/server/service/validation/tool-validation.js +193 -0
  125. package/dist-client/server/service/validation/tool-validation.js.map +1 -0
  126. package/dist-client/tsconfig.tsbuildinfo +1 -0
  127. package/dist-server/index.d.ts +23 -0
  128. package/dist-server/index.js +27 -0
  129. package/dist-server/index.js.map +1 -0
  130. package/dist-server/service/agentic-loop.d.ts +87 -0
  131. package/dist-server/service/agentic-loop.js +280 -0
  132. package/dist-server/service/agentic-loop.js.map +1 -0
  133. package/dist-server/service/apply-patch.d.ts +47 -0
  134. package/dist-server/service/apply-patch.js +369 -0
  135. package/dist-server/service/apply-patch.js.map +1 -0
  136. package/dist-server/service/assistant.d.ts +84 -0
  137. package/dist-server/service/assistant.js +2237 -0
  138. package/dist-server/service/assistant.js.map +1 -0
  139. package/dist-server/service/board-ai-resolver.d.ts +54 -0
  140. package/dist-server/service/board-ai-resolver.js +409 -0
  141. package/dist-server/service/board-ai-resolver.js.map +1 -0
  142. package/dist-server/service/catalog-resolver.d.ts +18 -0
  143. package/dist-server/service/catalog-resolver.js +98 -0
  144. package/dist-server/service/catalog-resolver.js.map +1 -0
  145. package/dist-server/service/chat-message/chat-message-broadcast.d.ts +13 -0
  146. package/dist-server/service/chat-message/chat-message-broadcast.js +13 -0
  147. package/dist-server/service/chat-message/chat-message-broadcast.js.map +1 -0
  148. package/dist-server/service/chat-message/chat-message-publish.d.ts +6 -0
  149. package/dist-server/service/chat-message/chat-message-publish.js +27 -0
  150. package/dist-server/service/chat-message/chat-message-publish.js.map +1 -0
  151. package/dist-server/service/chat-message/chat-message-subscription.d.ts +8 -0
  152. package/dist-server/service/chat-message/chat-message-subscription.js +53 -0
  153. package/dist-server/service/chat-message/chat-message-subscription.js.map +1 -0
  154. package/dist-server/service/chat-message/chat-message.d.ts +56 -0
  155. package/dist-server/service/chat-message/chat-message.js +149 -0
  156. package/dist-server/service/chat-message/chat-message.js.map +1 -0
  157. package/dist-server/service/chat-message/index.d.ts +5 -0
  158. package/dist-server/service/chat-message/index.js +12 -0
  159. package/dist-server/service/chat-message/index.js.map +1 -0
  160. package/dist-server/service/chat-session/chat-session.d.ts +31 -0
  161. package/dist-server/service/chat-session/chat-session.js +131 -0
  162. package/dist-server/service/chat-session/chat-session.js.map +1 -0
  163. package/dist-server/service/chat-session/index.d.ts +3 -0
  164. package/dist-server/service/chat-session/index.js +7 -0
  165. package/dist-server/service/chat-session/index.js.map +1 -0
  166. package/dist-server/service/chat-session-participant/chat-session-participant.d.ts +30 -0
  167. package/dist-server/service/chat-session-participant/chat-session-participant.js +112 -0
  168. package/dist-server/service/chat-session-participant/chat-session-participant.js.map +1 -0
  169. package/dist-server/service/chat-session-participant/index.d.ts +4 -0
  170. package/dist-server/service/chat-session-participant/index.js +10 -0
  171. package/dist-server/service/chat-session-participant/index.js.map +1 -0
  172. package/dist-server/service/chat-session-participant/invite-notification.d.ts +27 -0
  173. package/dist-server/service/chat-session-participant/invite-notification.js +30 -0
  174. package/dist-server/service/chat-session-participant/invite-notification.js.map +1 -0
  175. package/dist-server/service/chat-session-participant/presence.d.ts +4 -0
  176. package/dist-server/service/chat-session-participant/presence.js +22 -0
  177. package/dist-server/service/chat-session-participant/presence.js.map +1 -0
  178. package/dist-server/service/chat-session-resolver.d.ts +20 -0
  179. package/dist-server/service/chat-session-resolver.js +356 -0
  180. package/dist-server/service/chat-session-resolver.js.map +1 -0
  181. package/dist-server/service/component-tree.d.ts +68 -0
  182. package/dist-server/service/component-tree.js +73 -0
  183. package/dist-server/service/component-tree.js.map +1 -0
  184. package/dist-server/service/index.d.ts +20 -0
  185. package/dist-server/service/index.js +34 -0
  186. package/dist-server/service/index.js.map +1 -0
  187. package/dist-server/service/mention-marker.d.ts +78 -0
  188. package/dist-server/service/mention-marker.js +162 -0
  189. package/dist-server/service/mention-marker.js.map +1 -0
  190. package/dist-server/service/mention-resolver.d.ts +96 -0
  191. package/dist-server/service/mention-resolver.js +177 -0
  192. package/dist-server/service/mention-resolver.js.map +1 -0
  193. package/dist-server/service/patch-entry/board-patch-broadcast.d.ts +13 -0
  194. package/dist-server/service/patch-entry/board-patch-broadcast.js +13 -0
  195. package/dist-server/service/patch-entry/board-patch-broadcast.js.map +1 -0
  196. package/dist-server/service/patch-entry/board-patch-subscription.d.ts +12 -0
  197. package/dist-server/service/patch-entry/board-patch-subscription.js +63 -0
  198. package/dist-server/service/patch-entry/board-patch-subscription.js.map +1 -0
  199. package/dist-server/service/patch-entry/index.d.ts +4 -0
  200. package/dist-server/service/patch-entry/index.js +12 -0
  201. package/dist-server/service/patch-entry/index.js.map +1 -0
  202. package/dist-server/service/patch-entry/patch-entry.d.ts +16 -0
  203. package/dist-server/service/patch-entry/patch-entry.js +96 -0
  204. package/dist-server/service/patch-entry/patch-entry.js.map +1 -0
  205. package/dist-server/service/prefab-registry.d.ts +73 -0
  206. package/dist-server/service/prefab-registry.js +258 -0
  207. package/dist-server/service/prefab-registry.js.map +1 -0
  208. package/dist-server/service/styling/clear-tools.d.ts +15 -0
  209. package/dist-server/service/styling/clear-tools.js +53 -0
  210. package/dist-server/service/styling/clear-tools.js.map +1 -0
  211. package/dist-server/service/styling/color-utils.d.ts +43 -0
  212. package/dist-server/service/styling/color-utils.js +210 -0
  213. package/dist-server/service/styling/color-utils.js.map +1 -0
  214. package/dist-server/service/styling/copy-tools.d.ts +27 -0
  215. package/dist-server/service/styling/copy-tools.js +81 -0
  216. package/dist-server/service/styling/copy-tools.js.map +1 -0
  217. package/dist-server/service/styling/effect-tools.d.ts +45 -0
  218. package/dist-server/service/styling/effect-tools.js +90 -0
  219. package/dist-server/service/styling/effect-tools.js.map +1 -0
  220. package/dist-server/service/styling/fill-tools.d.ts +60 -0
  221. package/dist-server/service/styling/fill-tools.js +229 -0
  222. package/dist-server/service/styling/fill-tools.js.map +1 -0
  223. package/dist-server/service/styling/material-tools.d.ts +32 -0
  224. package/dist-server/service/styling/material-tools.js +92 -0
  225. package/dist-server/service/styling/material-tools.js.map +1 -0
  226. package/dist-server/service/styling/read-tools.d.ts +34 -0
  227. package/dist-server/service/styling/read-tools.js +45 -0
  228. package/dist-server/service/styling/read-tools.js.map +1 -0
  229. package/dist-server/service/styling/registry.d.ts +91 -0
  230. package/dist-server/service/styling/registry.js +352 -0
  231. package/dist-server/service/styling/registry.js.map +1 -0
  232. package/dist-server/service/styling/stroke-tools.d.ts +35 -0
  233. package/dist-server/service/styling/stroke-tools.js +97 -0
  234. package/dist-server/service/styling/stroke-tools.js.map +1 -0
  235. package/dist-server/service/styling/text-tools.d.ts +32 -0
  236. package/dist-server/service/styling/text-tools.js +105 -0
  237. package/dist-server/service/styling/text-tools.js.map +1 -0
  238. package/dist-server/service/subscribe-auth.d.ts +14 -0
  239. package/dist-server/service/subscribe-auth.js +33 -0
  240. package/dist-server/service/subscribe-auth.js.map +1 -0
  241. package/dist-server/service/types.d.ts +265 -0
  242. package/dist-server/service/types.js +3 -0
  243. package/dist-server/service/types.js.map +1 -0
  244. package/dist-server/service/validation/board-model-schema.d.ts +8311 -0
  245. package/dist-server/service/validation/board-model-schema.js +409 -0
  246. package/dist-server/service/validation/board-model-schema.js.map +1 -0
  247. package/dist-server/service/validation/tool-validation.d.ts +31 -0
  248. package/dist-server/service/validation/tool-validation.js +196 -0
  249. package/dist-server/service/validation/tool-validation.js.map +1 -0
  250. package/dist-server/tsconfig.tsbuildinfo +1 -0
  251. package/package.json +48 -0
  252. package/server/index.ts +23 -0
  253. package/server/service/agentic-loop.test.ts +705 -0
  254. package/server/service/agentic-loop.ts +431 -0
  255. package/server/service/apply-patch-drift.test.ts +829 -0
  256. package/server/service/apply-patch.test.ts +757 -0
  257. package/server/service/apply-patch.ts +411 -0
  258. package/server/service/assistant-integration.test.ts +922 -0
  259. package/server/service/assistant-llm-smoke.test.ts +296 -0
  260. package/server/service/assistant.test.ts +2034 -0
  261. package/server/service/assistant.ts +2378 -0
  262. package/server/service/board-ai-resolver.ts +407 -0
  263. package/server/service/catalog-resolver.test.ts +137 -0
  264. package/server/service/catalog-resolver.ts +102 -0
  265. package/server/service/chat-message/chat-message-broadcast.integration.test.ts +73 -0
  266. package/server/service/chat-message/chat-message-broadcast.test.ts +40 -0
  267. package/server/service/chat-message/chat-message-broadcast.ts +27 -0
  268. package/server/service/chat-message/chat-message-publish.ts +22 -0
  269. package/server/service/chat-message/chat-message-subscription.ts +41 -0
  270. package/server/service/chat-message/chat-message.ts +169 -0
  271. package/server/service/chat-message/index.ts +12 -0
  272. package/server/service/chat-session/chat-session.ts +130 -0
  273. package/server/service/chat-session/index.ts +5 -0
  274. package/server/service/chat-session-participant/chat-session-participant.ts +110 -0
  275. package/server/service/chat-session-participant/index.ts +6 -0
  276. package/server/service/chat-session-participant/invite-notification.test.ts +30 -0
  277. package/server/service/chat-session-participant/invite-notification.ts +48 -0
  278. package/server/service/chat-session-participant/presence.test.ts +39 -0
  279. package/server/service/chat-session-participant/presence.ts +21 -0
  280. package/server/service/chat-session-resolver.ts +312 -0
  281. package/server/service/component-tree.test.ts +248 -0
  282. package/server/service/component-tree.ts +135 -0
  283. package/server/service/index.ts +32 -0
  284. package/server/service/mention-marker.extract.test.ts +61 -0
  285. package/server/service/mention-marker.test.ts +228 -0
  286. package/server/service/mention-marker.ts +184 -0
  287. package/server/service/mention-resolver.test.ts +273 -0
  288. package/server/service/mention-resolver.ts +264 -0
  289. package/server/service/patch-entry/board-patch-broadcast.ts +24 -0
  290. package/server/service/patch-entry/board-patch-subscription.test.ts +40 -0
  291. package/server/service/patch-entry/board-patch-subscription.ts +50 -0
  292. package/server/service/patch-entry/index.ts +12 -0
  293. package/server/service/patch-entry/patch-entry.ts +89 -0
  294. package/server/service/prefab-registry.test.ts +172 -0
  295. package/server/service/prefab-registry.ts +332 -0
  296. package/server/service/styling/clear-tools.test.ts +164 -0
  297. package/server/service/styling/clear-tools.ts +69 -0
  298. package/server/service/styling/color-utils.test.ts +130 -0
  299. package/server/service/styling/color-utils.ts +226 -0
  300. package/server/service/styling/copy-tools.test.ts +152 -0
  301. package/server/service/styling/copy-tools.ts +115 -0
  302. package/server/service/styling/effect-tools.test.ts +277 -0
  303. package/server/service/styling/effect-tools.ts +124 -0
  304. package/server/service/styling/fill-tools.test.ts +331 -0
  305. package/server/service/styling/fill-tools.ts +293 -0
  306. package/server/service/styling/material-tools.test.ts +294 -0
  307. package/server/service/styling/material-tools.ts +114 -0
  308. package/server/service/styling/read-tools.test.ts +78 -0
  309. package/server/service/styling/read-tools.ts +67 -0
  310. package/server/service/styling/registry.test.ts +203 -0
  311. package/server/service/styling/registry.ts +423 -0
  312. package/server/service/styling/stroke-tools.test.ts +245 -0
  313. package/server/service/styling/stroke-tools.ts +145 -0
  314. package/server/service/styling/text-tools.test.ts +253 -0
  315. package/server/service/styling/text-tools.ts +131 -0
  316. package/server/service/styling/tier1-tools.test.ts +300 -0
  317. package/server/service/subscribe-auth.test.ts +57 -0
  318. package/server/service/subscribe-auth.ts +34 -0
  319. package/server/service/types.ts +235 -0
  320. package/server/service/validation/board-model-schema.test.ts +778 -0
  321. package/server/service/validation/board-model-schema.ts +446 -0
  322. package/server/service/validation/tool-validation.ts +228 -0
  323. package/things-factory.config.js +1 -0
  324. package/translations/en.json +49 -0
  325. package/translations/ja.json +48 -0
  326. package/translations/ko.json +49 -0
  327. package/translations/ms.json +48 -0
  328. package/translations/zh.json +48 -0
  329. package/tsconfig.json +9 -0
@@ -0,0 +1,210 @@
1
+ "use strict";
2
+ /**
3
+ * Color 파싱 / 정규화 유틸 — styling tool 들이 공통으로 사용.
4
+ *
5
+ * AI 가 자연어로 색상 표현하는 다양한 형태를 받아 표준 hex 또는 rgba 로 변환.
6
+ *
7
+ * 지원 입력:
8
+ * - hex 6자리: '#ff0000', '#FF0000'
9
+ * - hex 3자리: '#f00' (→ '#ff0000')
10
+ * - hex 8자리: '#ff0000ff' (alpha 포함)
11
+ * - rgb / rgba: 'rgb(255,0,0)', 'rgba(255,0,0,0.5)'
12
+ * - CSS named: 'red', 'transparent', ...
13
+ * - 시맨틱 alias: 'warm-red', 'forest-green', ...
14
+ *
15
+ * 출력 형태:
16
+ * - normalizeColor(): 표준 hex (#rrggbb), opacity 는 별개로 분리 반환
17
+ * - validateColor(): 유효성만 체크 (boolean + 에러 메시지)
18
+ */
19
+ Object.defineProperty(exports, "__esModule", { value: true });
20
+ exports.parseColor = parseColor;
21
+ exports.validateColor = validateColor;
22
+ exports.normalizeColor = normalizeColor;
23
+ /** CSS named colors → hex (자주 쓰는 것만, 필요 시 확장) */
24
+ const NAMED_COLORS = {
25
+ // 무채
26
+ white: '#ffffff',
27
+ black: '#000000',
28
+ gray: '#808080',
29
+ grey: '#808080',
30
+ silver: '#c0c0c0',
31
+ // 기본 컬러
32
+ red: '#ff0000',
33
+ green: '#008000',
34
+ blue: '#0000ff',
35
+ yellow: '#ffff00',
36
+ cyan: '#00ffff',
37
+ magenta: '#ff00ff',
38
+ orange: '#ffa500',
39
+ purple: '#800080',
40
+ pink: '#ffc0cb',
41
+ brown: '#a52a2a',
42
+ // 자주 쓰는 변형
43
+ navy: '#000080',
44
+ teal: '#008080',
45
+ lime: '#00ff00',
46
+ olive: '#808000',
47
+ maroon: '#800000',
48
+ // 투명
49
+ transparent: 'transparent'
50
+ };
51
+ /**
52
+ * 시맨틱 컬러 alias — AI 가 자연어 표현 사용 가능.
53
+ * Material 3 기반 톤 (brightness 적당, saturation 풍부).
54
+ */
55
+ const SEMANTIC_COLORS = {
56
+ // warm reds
57
+ 'warm-red': '#e74c3c',
58
+ 'soft-red': '#ef4444',
59
+ 'deep-red': '#b91c1c',
60
+ 'dark-red': '#7f1d1d',
61
+ // greens
62
+ 'forest-green': '#228b22',
63
+ 'mint-green': '#10b981',
64
+ 'olive-green': '#65a30d',
65
+ 'lime-green': '#84cc16',
66
+ // blues
67
+ 'sky-blue': '#0ea5e9',
68
+ 'navy-blue': '#1e40af',
69
+ 'royal-blue': '#3b82f6',
70
+ 'powder-blue': '#bfdbfe',
71
+ // yellows / oranges
72
+ 'amber': '#f59e0b',
73
+ 'sunset-orange': '#f97316',
74
+ 'gold': '#eab308',
75
+ 'mustard': '#ca8a04',
76
+ // purples
77
+ 'violet': '#7c3aed',
78
+ 'lavender': '#c4b5fd',
79
+ // grays
80
+ 'slate': '#64748b',
81
+ 'charcoal': '#374151',
82
+ 'cool-gray': '#6b7280',
83
+ 'warm-gray': '#78716c',
84
+ // 시맨틱 의도
85
+ success: '#22c55e',
86
+ warning: '#f59e0b',
87
+ error: '#ef4444',
88
+ info: '#3b82f6'
89
+ };
90
+ /**
91
+ * 색상 입력을 정규 hex + alpha 로 변환.
92
+ * 잘못된 입력은 null — 호출자가 에러 처리.
93
+ */
94
+ function parseColor(input) {
95
+ if (typeof input !== 'string')
96
+ return null;
97
+ const s = input.trim().toLowerCase();
98
+ if (s.length === 0)
99
+ return null;
100
+ // transparent
101
+ if (s === 'transparent' || s === 'none') {
102
+ return { hex: 'transparent', alpha: 0 };
103
+ }
104
+ // hex
105
+ if (s.startsWith('#')) {
106
+ return parseHex(s);
107
+ }
108
+ // rgb / rgba
109
+ const rgbMatch = s.match(/^rgba?\s*\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*(?:,\s*([\d.]+)\s*)?\)$/);
110
+ if (rgbMatch) {
111
+ const r = clampByte(rgbMatch[1]);
112
+ const g = clampByte(rgbMatch[2]);
113
+ const b = clampByte(rgbMatch[3]);
114
+ const a = rgbMatch[4] !== undefined ? clampUnit(rgbMatch[4]) : 1;
115
+ if (r === null || g === null || b === null || a === null)
116
+ return null;
117
+ return { hex: rgbToHex(r, g, b), alpha: a };
118
+ }
119
+ // CSS named
120
+ if (NAMED_COLORS[s]) {
121
+ return parseHex(NAMED_COLORS[s]);
122
+ }
123
+ // 시맨틱 alias
124
+ if (SEMANTIC_COLORS[s]) {
125
+ return parseHex(SEMANTIC_COLORS[s]);
126
+ }
127
+ return null;
128
+ }
129
+ function parseHex(s) {
130
+ const hex = s.startsWith('#') ? s.slice(1) : s;
131
+ if (hex === 'transparent')
132
+ return { hex: 'transparent', alpha: 0 };
133
+ if (!/^[0-9a-f]+$/i.test(hex))
134
+ return null;
135
+ if (hex.length === 3) {
136
+ // #abc → #aabbcc
137
+ const r = hex[0] + hex[0];
138
+ const g = hex[1] + hex[1];
139
+ const b = hex[2] + hex[2];
140
+ return { hex: `#${r}${g}${b}`.toLowerCase(), alpha: 1 };
141
+ }
142
+ if (hex.length === 4) {
143
+ // #abcd → #aabbccdd
144
+ const r = hex[0] + hex[0];
145
+ const g = hex[1] + hex[1];
146
+ const b = hex[2] + hex[2];
147
+ const a = parseInt(hex[3] + hex[3], 16) / 255;
148
+ return { hex: `#${r}${g}${b}`.toLowerCase(), alpha: round3(a) };
149
+ }
150
+ if (hex.length === 6) {
151
+ return { hex: `#${hex}`.toLowerCase(), alpha: 1 };
152
+ }
153
+ if (hex.length === 8) {
154
+ const rgb = hex.slice(0, 6);
155
+ const a = parseInt(hex.slice(6, 8), 16) / 255;
156
+ return { hex: `#${rgb}`.toLowerCase(), alpha: round3(a) };
157
+ }
158
+ return null;
159
+ }
160
+ function clampByte(s) {
161
+ const n = Number(s);
162
+ if (!Number.isFinite(n) || n < 0 || n > 255)
163
+ return null;
164
+ return Math.round(n);
165
+ }
166
+ function clampUnit(s) {
167
+ const n = Number(s);
168
+ if (!Number.isFinite(n) || n < 0 || n > 1)
169
+ return null;
170
+ return n;
171
+ }
172
+ function rgbToHex(r, g, b) {
173
+ const h = (n) => n.toString(16).padStart(2, '0');
174
+ return `#${h(r)}${h(g)}${h(b)}`;
175
+ }
176
+ function round3(n) {
177
+ return Math.round(n * 1000) / 1000;
178
+ }
179
+ /**
180
+ * 단순 검증 — 정규화 가능 여부만.
181
+ * 에러 메시지에 정정 제안 포함 가능.
182
+ */
183
+ function validateColor(input) {
184
+ if (typeof input !== 'string') {
185
+ return { valid: false, error: 'Color must be a string.' };
186
+ }
187
+ const parsed = parseColor(input);
188
+ if (parsed)
189
+ return { valid: true };
190
+ // 비슷한 named color 추천
191
+ const lower = input.trim().toLowerCase();
192
+ const allKnown = [...Object.keys(NAMED_COLORS), ...Object.keys(SEMANTIC_COLORS)];
193
+ const close = allKnown.find(k => k.includes(lower) || lower.includes(k));
194
+ return {
195
+ valid: false,
196
+ error: `Invalid color: '${input}'. Use hex (#ff0000), rgb(255,0,0), or named (red, warm-red, ...).`,
197
+ suggestion: close
198
+ };
199
+ }
200
+ /**
201
+ * 색상을 hex 문자열로 정규화 (저장용).
202
+ * 실패 시 null.
203
+ */
204
+ function normalizeColor(input) {
205
+ const parsed = parseColor(input);
206
+ if (!parsed)
207
+ return null;
208
+ return parsed.hex;
209
+ }
210
+ //# sourceMappingURL=color-utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"color-utils.js","sourceRoot":"","sources":["../../../server/service/styling/color-utils.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Color 파싱 / 정규화 유틸 — styling tool 들이 공통으로 사용.\n *\n * AI 가 자연어로 색상 표현하는 다양한 형태를 받아 표준 hex 또는 rgba 로 변환.\n *\n * 지원 입력:\n * - hex 6자리: '#ff0000', '#FF0000'\n * - hex 3자리: '#f00' (→ '#ff0000')\n * - hex 8자리: '#ff0000ff' (alpha 포함)\n * - rgb / rgba: 'rgb(255,0,0)', 'rgba(255,0,0,0.5)'\n * - CSS named: 'red', 'transparent', ...\n * - 시맨틱 alias: 'warm-red', 'forest-green', ...\n *\n * 출력 형태:\n * - normalizeColor(): 표준 hex (#rrggbb), opacity 는 별개로 분리 반환\n * - validateColor(): 유효성만 체크 (boolean + 에러 메시지)\n */\n\n/** CSS named colors → hex (자주 쓰는 것만, 필요 시 확장) */\nconst NAMED_COLORS: Record<string, string> = {\n // 무채\n white: '#ffffff',\n black: '#000000',\n gray: '#808080',\n grey: '#808080',\n silver: '#c0c0c0',\n // 기본 컬러\n red: '#ff0000',\n green: '#008000',\n blue: '#0000ff',\n yellow: '#ffff00',\n cyan: '#00ffff',\n magenta: '#ff00ff',\n orange: '#ffa500',\n purple: '#800080',\n pink: '#ffc0cb',\n brown: '#a52a2a',\n // 자주 쓰는 변형\n navy: '#000080',\n teal: '#008080',\n lime: '#00ff00',\n olive: '#808000',\n maroon: '#800000',\n // 투명\n transparent: 'transparent'\n}\n\n/**\n * 시맨틱 컬러 alias — AI 가 자연어 표현 사용 가능.\n * Material 3 기반 톤 (brightness 적당, saturation 풍부).\n */\nconst SEMANTIC_COLORS: Record<string, string> = {\n // warm reds\n 'warm-red': '#e74c3c',\n 'soft-red': '#ef4444',\n 'deep-red': '#b91c1c',\n 'dark-red': '#7f1d1d',\n // greens\n 'forest-green': '#228b22',\n 'mint-green': '#10b981',\n 'olive-green': '#65a30d',\n 'lime-green': '#84cc16',\n // blues\n 'sky-blue': '#0ea5e9',\n 'navy-blue': '#1e40af',\n 'royal-blue': '#3b82f6',\n 'powder-blue': '#bfdbfe',\n // yellows / oranges\n 'amber': '#f59e0b',\n 'sunset-orange': '#f97316',\n 'gold': '#eab308',\n 'mustard': '#ca8a04',\n // purples\n 'violet': '#7c3aed',\n 'lavender': '#c4b5fd',\n // grays\n 'slate': '#64748b',\n 'charcoal': '#374151',\n 'cool-gray': '#6b7280',\n 'warm-gray': '#78716c',\n // 시맨틱 의도\n success: '#22c55e',\n warning: '#f59e0b',\n error: '#ef4444',\n info: '#3b82f6'\n}\n\n/** Parse 결과 — color 와 alpha 분리. alpha 는 0..1. */\nexport interface ParsedColor {\n /** 표준 hex (#rrggbb, 소문자) 또는 'transparent' */\n hex: string\n /** Alpha 채널 0..1. transparent 면 0, 명시 alpha 없으면 1. */\n alpha: number\n}\n\n/**\n * 색상 입력을 정규 hex + alpha 로 변환.\n * 잘못된 입력은 null — 호출자가 에러 처리.\n */\nexport function parseColor(input: string | null | undefined): ParsedColor | null {\n if (typeof input !== 'string') return null\n const s = input.trim().toLowerCase()\n if (s.length === 0) return null\n\n // transparent\n if (s === 'transparent' || s === 'none') {\n return { hex: 'transparent', alpha: 0 }\n }\n\n // hex\n if (s.startsWith('#')) {\n return parseHex(s)\n }\n\n // rgb / rgba\n const rgbMatch = s.match(/^rgba?\\s*\\(\\s*([\\d.]+)\\s*,\\s*([\\d.]+)\\s*,\\s*([\\d.]+)\\s*(?:,\\s*([\\d.]+)\\s*)?\\)$/)\n if (rgbMatch) {\n const r = clampByte(rgbMatch[1])\n const g = clampByte(rgbMatch[2])\n const b = clampByte(rgbMatch[3])\n const a = rgbMatch[4] !== undefined ? clampUnit(rgbMatch[4]) : 1\n if (r === null || g === null || b === null || a === null) return null\n return { hex: rgbToHex(r, g, b), alpha: a }\n }\n\n // CSS named\n if (NAMED_COLORS[s]) {\n return parseHex(NAMED_COLORS[s])\n }\n\n // 시맨틱 alias\n if (SEMANTIC_COLORS[s]) {\n return parseHex(SEMANTIC_COLORS[s])\n }\n\n return null\n}\n\nfunction parseHex(s: string): ParsedColor | null {\n const hex = s.startsWith('#') ? s.slice(1) : s\n if (hex === 'transparent') return { hex: 'transparent', alpha: 0 }\n\n if (!/^[0-9a-f]+$/i.test(hex)) return null\n\n if (hex.length === 3) {\n // #abc → #aabbcc\n const r = hex[0] + hex[0]\n const g = hex[1] + hex[1]\n const b = hex[2] + hex[2]\n return { hex: `#${r}${g}${b}`.toLowerCase(), alpha: 1 }\n }\n if (hex.length === 4) {\n // #abcd → #aabbccdd\n const r = hex[0] + hex[0]\n const g = hex[1] + hex[1]\n const b = hex[2] + hex[2]\n const a = parseInt(hex[3] + hex[3], 16) / 255\n return { hex: `#${r}${g}${b}`.toLowerCase(), alpha: round3(a) }\n }\n if (hex.length === 6) {\n return { hex: `#${hex}`.toLowerCase(), alpha: 1 }\n }\n if (hex.length === 8) {\n const rgb = hex.slice(0, 6)\n const a = parseInt(hex.slice(6, 8), 16) / 255\n return { hex: `#${rgb}`.toLowerCase(), alpha: round3(a) }\n }\n return null\n}\n\nfunction clampByte(s: string): number | null {\n const n = Number(s)\n if (!Number.isFinite(n) || n < 0 || n > 255) return null\n return Math.round(n)\n}\n\nfunction clampUnit(s: string): number | null {\n const n = Number(s)\n if (!Number.isFinite(n) || n < 0 || n > 1) return null\n return n\n}\n\nfunction rgbToHex(r: number, g: number, b: number): string {\n const h = (n: number) => n.toString(16).padStart(2, '0')\n return `#${h(r)}${h(g)}${h(b)}`\n}\n\nfunction round3(n: number): number {\n return Math.round(n * 1000) / 1000\n}\n\n/**\n * 단순 검증 — 정규화 가능 여부만.\n * 에러 메시지에 정정 제안 포함 가능.\n */\nexport function validateColor(input: string | null | undefined): {\n valid: boolean\n error?: string\n suggestion?: string\n} {\n if (typeof input !== 'string') {\n return { valid: false, error: 'Color must be a string.' }\n }\n const parsed = parseColor(input)\n if (parsed) return { valid: true }\n\n // 비슷한 named color 추천\n const lower = input.trim().toLowerCase()\n const allKnown = [...Object.keys(NAMED_COLORS), ...Object.keys(SEMANTIC_COLORS)]\n const close = allKnown.find(k => k.includes(lower) || lower.includes(k))\n return {\n valid: false,\n error: `Invalid color: '${input}'. Use hex (#ff0000), rgb(255,0,0), or named (red, warm-red, ...).`,\n suggestion: close\n }\n}\n\n/**\n * 색상을 hex 문자열로 정규화 (저장용).\n * 실패 시 null.\n */\nexport function normalizeColor(input: string | null | undefined): string | null {\n const parsed = parseColor(input)\n if (!parsed) return null\n return parsed.hex\n}\n"]}
@@ -0,0 +1,27 @@
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
+ export interface CopyStyleInput {
14
+ sourceRefid: number;
15
+ targetRefids: number[];
16
+ aspects?: Array<'fill' | 'stroke' | 'shadow' | 'material' | 'font' | 'opacity' | 'visibility'>;
17
+ }
18
+ export interface OpsResult {
19
+ ops: BoardEditOp[];
20
+ errors: string[];
21
+ }
22
+ /**
23
+ * input + components 배열 → BoardEditOp[].
24
+ *
25
+ * components 는 board 의 모든 컴포넌트 (caller 가 전달). source 찾아 styling 추출.
26
+ */
27
+ export declare function buildCopyStyleOps(input: CopyStyleInput, components: ReadonlyArray<any>): OpsResult;
@@ -0,0 +1,81 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildCopyStyleOps = buildCopyStyleOps;
4
+ const ASPECT_FIELDS = {
5
+ fill: ['fillStyle'],
6
+ stroke: ['strokeStyle'],
7
+ shadow: ['shadow'],
8
+ material: ['material3d'],
9
+ font: [
10
+ 'fontFamily',
11
+ 'fontSize',
12
+ 'fontWeight',
13
+ 'fontStyle',
14
+ 'fontColor',
15
+ 'textAlign',
16
+ 'textBaseline',
17
+ 'lineHeight'
18
+ ],
19
+ opacity: ['alpha'],
20
+ visibility: ['hidden']
21
+ };
22
+ const DEFAULT_ASPECTS = [
23
+ 'fill',
24
+ 'stroke',
25
+ 'shadow',
26
+ 'material',
27
+ 'font',
28
+ 'opacity'
29
+ // visibility 는 default 에서 제외 — 의도 없이 hidden 복사 위험
30
+ ];
31
+ /**
32
+ * input + components 배열 → BoardEditOp[].
33
+ *
34
+ * components 는 board 의 모든 컴포넌트 (caller 가 전달). source 찾아 styling 추출.
35
+ */
36
+ function buildCopyStyleOps(input, components) {
37
+ if (typeof input.sourceRefid !== 'number') {
38
+ return { ops: [], errors: ['sourceRefid (number) required'] };
39
+ }
40
+ if (!Array.isArray(input.targetRefids) || input.targetRefids.length === 0) {
41
+ return { ops: [], errors: ['targetRefids must be a non-empty array'] };
42
+ }
43
+ const targets = input.targetRefids.filter(n => typeof n === 'number' && Number.isFinite(n) && n !== input.sourceRefid);
44
+ if (targets.length === 0) {
45
+ return { ops: [], errors: ['No valid target refids (excluding source)'] };
46
+ }
47
+ const source = components.find(c => c && typeof c.refid === 'number' && c.refid === input.sourceRefid);
48
+ if (!source) {
49
+ return {
50
+ ops: [],
51
+ errors: [`Source component refid=${input.sourceRefid} not found`]
52
+ };
53
+ }
54
+ const aspects = input.aspects && input.aspects.length > 0 ? input.aspects : DEFAULT_ASPECTS;
55
+ const validAspects = Object.keys(ASPECT_FIELDS);
56
+ const unknown = aspects.filter(a => !validAspects.includes(a));
57
+ if (unknown.length > 0) {
58
+ return {
59
+ ops: [],
60
+ errors: [`Unknown aspect(s): ${unknown.join(', ')}. Valid: ${validAspects.join('/')}`]
61
+ };
62
+ }
63
+ // source 에서 aspect 별 필드 추출 → patch 합치기
64
+ const patch = {};
65
+ for (const aspect of aspects) {
66
+ const fields = ASPECT_FIELDS[aspect];
67
+ for (const field of fields) {
68
+ if (source[field] !== undefined) {
69
+ patch[field] = source[field];
70
+ }
71
+ }
72
+ }
73
+ if (Object.keys(patch).length === 0) {
74
+ return { ops: [], errors: ['Source has no styling for the requested aspects'] };
75
+ }
76
+ return {
77
+ ops: targets.map(refid => ({ op: 'modify', refid, patch })),
78
+ errors: []
79
+ };
80
+ }
81
+ //# sourceMappingURL=copy-tools.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"copy-tools.js","sourceRoot":"","sources":["../../../server/service/styling/copy-tools.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * copyStyle — 한 컴포넌트의 styling 을 다른 컴포넌트들에 복사.\n *\n * 자주 쓰는 패턴: \"MOTOR1 처럼 다른 모터들도 같게 만들어줘\"\n * AI: getComponentByRefid(MOTOR1) 또는 getComponentStyle(MOTOR1) → copyStyle(...)\n *\n * aspects 인자로 어느 styling 영역만 복사할지 선택 가능 — 부분 통일 (예: 색만 통일).\n *\n * 입력 — sourceRefid 의 실제 styling 값을 미리 알 필요 없음. server 가 components\n * 에서 source 찾아 styling 추출 후 target 들에 patch 생성.\n */\nimport type { BoardEditOp } from '../types'\n\nexport interface CopyStyleInput {\n sourceRefid: number\n targetRefids: number[]\n aspects?: Array<'fill' | 'stroke' | 'shadow' | 'material' | 'font' | 'opacity' | 'visibility'>\n}\n\nexport interface OpsResult {\n ops: BoardEditOp[]\n errors: string[]\n}\n\nconst ASPECT_FIELDS: Record<string, string[]> = {\n fill: ['fillStyle'],\n stroke: ['strokeStyle'],\n shadow: ['shadow'],\n material: ['material3d'],\n font: [\n 'fontFamily',\n 'fontSize',\n 'fontWeight',\n 'fontStyle',\n 'fontColor',\n 'textAlign',\n 'textBaseline',\n 'lineHeight'\n ],\n opacity: ['alpha'],\n visibility: ['hidden']\n}\n\nconst DEFAULT_ASPECTS: Array<keyof typeof ASPECT_FIELDS> = [\n 'fill',\n 'stroke',\n 'shadow',\n 'material',\n 'font',\n 'opacity'\n // visibility 는 default 에서 제외 — 의도 없이 hidden 복사 위험\n]\n\n/**\n * input + components 배열 → BoardEditOp[].\n *\n * components 는 board 의 모든 컴포넌트 (caller 가 전달). source 찾아 styling 추출.\n */\nexport function buildCopyStyleOps(\n input: CopyStyleInput,\n components: ReadonlyArray<any>\n): OpsResult {\n if (typeof input.sourceRefid !== 'number') {\n return { ops: [], errors: ['sourceRefid (number) required'] }\n }\n if (!Array.isArray(input.targetRefids) || input.targetRefids.length === 0) {\n return { ops: [], errors: ['targetRefids must be a non-empty array'] }\n }\n const targets = input.targetRefids.filter(\n n => typeof n === 'number' && Number.isFinite(n) && n !== input.sourceRefid\n )\n if (targets.length === 0) {\n return { ops: [], errors: ['No valid target refids (excluding source)'] }\n }\n\n const source = components.find(\n c => c && typeof c.refid === 'number' && c.refid === input.sourceRefid\n )\n if (!source) {\n return {\n ops: [],\n errors: [`Source component refid=${input.sourceRefid} not found`]\n }\n }\n\n const aspects = input.aspects && input.aspects.length > 0 ? input.aspects : DEFAULT_ASPECTS\n const validAspects = Object.keys(ASPECT_FIELDS)\n const unknown = aspects.filter(a => !validAspects.includes(a))\n if (unknown.length > 0) {\n return {\n ops: [],\n errors: [`Unknown aspect(s): ${unknown.join(', ')}. Valid: ${validAspects.join('/')}`]\n }\n }\n\n // source 에서 aspect 별 필드 추출 → patch 합치기\n const patch: any = {}\n for (const aspect of aspects) {\n const fields = ASPECT_FIELDS[aspect]\n for (const field of fields) {\n if (source[field] !== undefined) {\n patch[field] = source[field]\n }\n }\n }\n\n if (Object.keys(patch).length === 0) {\n return { ops: [], errors: ['Source has no styling for the requested aspects'] }\n }\n\n return {\n ops: targets.map(refid => ({ op: 'modify', refid, patch })),\n errors: []\n }\n}\n"]}
@@ -0,0 +1,45 @@
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
+ export interface SetShadowInput {
10
+ refids: number[];
11
+ /** synthetic — false 면 blur=0 + color=transparent 매핑 (drawer 가 안 그림). 모델에 enabled 필드 없음. */
12
+ enabled?: boolean;
13
+ /** state.shadow.color */
14
+ color?: string;
15
+ /** Blur radius (CSS box-shadow 의 blur-radius, Canvas 의 shadowBlur). state.shadow.blur. */
16
+ blur?: number;
17
+ /** Horizontal offset (CSS box-shadow 의 offset-x, Canvas 의 shadowOffsetX). state.shadow.offsetX. */
18
+ offsetX?: number;
19
+ /** Vertical offset (CSS box-shadow 의 offset-y, Canvas 의 shadowOffsetY). state.shadow.offsetY. */
20
+ offsetY?: number;
21
+ }
22
+ export interface SetOpacityInput {
23
+ refids: number[];
24
+ /** state.alpha 의 canonical 이름. 0..1. */
25
+ alpha: number;
26
+ }
27
+ export interface SetVisibleInput {
28
+ refids: number[];
29
+ visible: boolean;
30
+ }
31
+ export interface OpsResult {
32
+ ops: BoardEditOp[];
33
+ errors: string[];
34
+ }
35
+ /**
36
+ * setShadow — CSS box-shadow / Canvas API 표준 이름 (color/offsetX/offsetY/blur) 그대로
37
+ * state.shadow 에 set. things-scene drawer 는 옛 이름 (left/top/blurSize) 도
38
+ * backwards-compat 으로 읽지만 (effect/shadow.ts:36-47), 새 모델은 canonical 만 사용.
39
+ *
40
+ * synthetic enabled=false → blur 0 + transparent 매핑 (drawer 가 안 그림).
41
+ * 모델에 enabled 필드는 없음.
42
+ */
43
+ export declare function buildSetShadowOps(input: SetShadowInput): OpsResult;
44
+ export declare function buildSetOpacityOps(input: SetOpacityInput): OpsResult;
45
+ export declare function buildSetVisibleOps(input: SetVisibleInput): OpsResult;
@@ -0,0 +1,90 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildSetShadowOps = buildSetShadowOps;
4
+ exports.buildSetOpacityOps = buildSetOpacityOps;
5
+ exports.buildSetVisibleOps = buildSetVisibleOps;
6
+ const color_utils_1 = require("./color-utils");
7
+ /**
8
+ * setShadow — CSS box-shadow / Canvas API 표준 이름 (color/offsetX/offsetY/blur) 그대로
9
+ * state.shadow 에 set. things-scene drawer 는 옛 이름 (left/top/blurSize) 도
10
+ * backwards-compat 으로 읽지만 (effect/shadow.ts:36-47), 새 모델은 canonical 만 사용.
11
+ *
12
+ * synthetic enabled=false → blur 0 + transparent 매핑 (drawer 가 안 그림).
13
+ * 모델에 enabled 필드는 없음.
14
+ */
15
+ function buildSetShadowOps(input) {
16
+ if (!Array.isArray(input.refids) || input.refids.length === 0) {
17
+ return { ops: [], errors: ['refids must be a non-empty array'] };
18
+ }
19
+ const refids = input.refids.filter(n => typeof n === 'number' && Number.isFinite(n));
20
+ if (refids.length === 0)
21
+ return { ops: [], errors: ['No valid numeric refids'] };
22
+ const noOp = input.enabled === undefined &&
23
+ input.color === undefined &&
24
+ input.blur === undefined &&
25
+ input.offsetX === undefined &&
26
+ input.offsetY === undefined;
27
+ if (noOp)
28
+ return { ops: [], errors: ['No shadow changes specified'] };
29
+ if (input.enabled === false) {
30
+ // synthetic — false 면 blur 0 + transparent 으로 변환 (drawer 가 안 그림)
31
+ const patch = { shadow: { blur: 0, color: 'transparent' } };
32
+ return { ops: refids.map(refid => ({ op: 'modify', refid, patch })), errors: [] };
33
+ }
34
+ if (input.color !== undefined) {
35
+ const v = (0, color_utils_1.validateColor)(input.color);
36
+ if (!v.valid)
37
+ return { ops: [], errors: [v.error ?? 'Invalid shadow color'] };
38
+ }
39
+ if (input.blur !== undefined && (input.blur < 0 || !Number.isFinite(input.blur))) {
40
+ return { ops: [], errors: [`Invalid blur: ${input.blur}`] };
41
+ }
42
+ if (input.offsetX !== undefined && !Number.isFinite(input.offsetX)) {
43
+ return { ops: [], errors: [`Invalid offsetX: ${input.offsetX}`] };
44
+ }
45
+ if (input.offsetY !== undefined && !Number.isFinite(input.offsetY)) {
46
+ return { ops: [], errors: [`Invalid offsetY: ${input.offsetY}`] };
47
+ }
48
+ // CSS / Canvas 표준 canonical 이름 — rename 없음
49
+ const sh = {};
50
+ // validateColor 가 통과한 색만 도달 — normalizeColor 가 여기서 null 반환 안 함
51
+ if (input.color !== undefined)
52
+ sh.color = (0, color_utils_1.normalizeColor)(input.color);
53
+ if (input.blur !== undefined)
54
+ sh.blur = input.blur;
55
+ if (input.offsetX !== undefined)
56
+ sh.offsetX = input.offsetX;
57
+ if (input.offsetY !== undefined)
58
+ sh.offsetY = input.offsetY;
59
+ const patch = { shadow: sh };
60
+ return { ops: refids.map(refid => ({ op: 'modify', refid, patch })), errors: [] };
61
+ }
62
+ function buildSetOpacityOps(input) {
63
+ if (!Array.isArray(input.refids) || input.refids.length === 0) {
64
+ return { ops: [], errors: ['refids must be a non-empty array'] };
65
+ }
66
+ if (typeof input.alpha !== 'number' || input.alpha < 0 || input.alpha > 1) {
67
+ return { ops: [], errors: [`alpha must be 0..1: ${input.alpha}`] };
68
+ }
69
+ const refids = input.refids.filter(n => typeof n === 'number' && Number.isFinite(n));
70
+ if (refids.length === 0)
71
+ return { ops: [], errors: ['No valid numeric refids'] };
72
+ // canonical 이름 그대로
73
+ const patch = { alpha: input.alpha };
74
+ return { ops: refids.map(refid => ({ op: 'modify', refid, patch })), errors: [] };
75
+ }
76
+ function buildSetVisibleOps(input) {
77
+ if (!Array.isArray(input.refids) || input.refids.length === 0) {
78
+ return { ops: [], errors: ['refids must be a non-empty array'] };
79
+ }
80
+ if (typeof input.visible !== 'boolean') {
81
+ return { ops: [], errors: ['visible must be a boolean'] };
82
+ }
83
+ const refids = input.refids.filter(n => typeof n === 'number' && Number.isFinite(n));
84
+ if (refids.length === 0)
85
+ return { ops: [], errors: ['No valid numeric refids'] };
86
+ // hidden 은 visible 의 inverse
87
+ const patch = { hidden: !input.visible };
88
+ return { ops: refids.map(refid => ({ op: 'modify', refid, patch })), errors: [] };
89
+ }
90
+ //# sourceMappingURL=effect-tools.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"effect-tools.js","sourceRoot":"","sources":["../../../server/service/styling/effect-tools.ts"],"names":[],"mappings":";;AAgDA,8CA6CC;AAED,gDAaC;AAED,gDAaC;AAnHD,+CAA6D;AAgC7D;;;;;;;GAOG;AACH,SAAgB,iBAAiB,CAAC,KAAqB;IACrD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC9D,OAAO,EAAE,GAAG,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC,kCAAkC,CAAC,EAAE,CAAA;IAClE,CAAC;IACD,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAA;IACpF,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,GAAG,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC,yBAAyB,CAAC,EAAE,CAAA;IAEhF,MAAM,IAAI,GACR,KAAK,CAAC,OAAO,KAAK,SAAS;QAC3B,KAAK,CAAC,KAAK,KAAK,SAAS;QACzB,KAAK,CAAC,IAAI,KAAK,SAAS;QACxB,KAAK,CAAC,OAAO,KAAK,SAAS;QAC3B,KAAK,CAAC,OAAO,KAAK,SAAS,CAAA;IAC7B,IAAI,IAAI;QAAE,OAAO,EAAE,GAAG,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC,6BAA6B,CAAC,EAAE,CAAA;IAErE,IAAI,KAAK,CAAC,OAAO,KAAK,KAAK,EAAE,CAAC;QAC5B,iEAAiE;QACjE,MAAM,KAAK,GAAG,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,aAAa,EAAE,EAAE,CAAA;QAC3D,OAAO,EAAE,GAAG,EAAE,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,EAAE,EAAE,CAAA;IACnF,CAAC;IAED,IAAI,KAAK,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;QAC9B,MAAM,CAAC,GAAG,IAAA,2BAAa,EAAC,KAAK,CAAC,KAAK,CAAC,CAAA;QACpC,IAAI,CAAC,CAAC,CAAC,KAAK;YAAE,OAAO,EAAE,GAAG,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,KAAK,IAAI,sBAAsB,CAAC,EAAE,CAAA;IAC/E,CAAC;IACD,IAAI,KAAK,CAAC,IAAI,KAAK,SAAS,IAAI,CAAC,KAAK,CAAC,IAAI,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC;QACjF,OAAO,EAAE,GAAG,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC,iBAAiB,KAAK,CAAC,IAAI,EAAE,CAAC,EAAE,CAAA;IAC7D,CAAC;IACD,IAAI,KAAK,CAAC,OAAO,KAAK,SAAS,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC;QACnE,OAAO,EAAE,GAAG,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC,oBAAoB,KAAK,CAAC,OAAO,EAAE,CAAC,EAAE,CAAA;IACnE,CAAC;IACD,IAAI,KAAK,CAAC,OAAO,KAAK,SAAS,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC;QACnE,OAAO,EAAE,GAAG,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC,oBAAoB,KAAK,CAAC,OAAO,EAAE,CAAC,EAAE,CAAA;IACnE,CAAC;IAED,2CAA2C;IAC3C,MAAM,EAAE,GAAoC,EAAE,CAAA;IAC9C,+DAA+D;IAC/D,IAAI,KAAK,CAAC,KAAK,KAAK,SAAS;QAAE,EAAE,CAAC,KAAK,GAAG,IAAA,4BAAc,EAAC,KAAK,CAAC,KAAK,CAAE,CAAA;IACtE,IAAI,KAAK,CAAC,IAAI,KAAK,SAAS;QAAE,EAAE,CAAC,IAAI,GAAG,KAAK,CAAC,IAAI,CAAA;IAClD,IAAI,KAAK,CAAC,OAAO,KAAK,SAAS;QAAE,EAAE,CAAC,OAAO,GAAG,KAAK,CAAC,OAAO,CAAA;IAC3D,IAAI,KAAK,CAAC,OAAO,KAAK,SAAS;QAAE,EAAE,CAAC,OAAO,GAAG,KAAK,CAAC,OAAO,CAAA;IAE3D,MAAM,KAAK,GAAG,EAAE,MAAM,EAAE,EAAE,EAAE,CAAA;IAC5B,OAAO,EAAE,GAAG,EAAE,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,EAAE,EAAE,CAAA;AACnF,CAAC;AAED,SAAgB,kBAAkB,CAAC,KAAsB;IACvD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC9D,OAAO,EAAE,GAAG,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC,kCAAkC,CAAC,EAAE,CAAA;IAClE,CAAC;IACD,IAAI,OAAO,KAAK,CAAC,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,KAAK,GAAG,CAAC,IAAI,KAAK,CAAC,KAAK,GAAG,CAAC,EAAE,CAAC;QAC1E,OAAO,EAAE,GAAG,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC,uBAAuB,KAAK,CAAC,KAAK,EAAE,CAAC,EAAE,CAAA;IACpE,CAAC;IACD,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAA;IACpF,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,GAAG,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC,yBAAyB,CAAC,EAAE,CAAA;IAEhF,mBAAmB;IACnB,MAAM,KAAK,GAAG,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,CAAA;IACpC,OAAO,EAAE,GAAG,EAAE,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,EAAE,EAAE,CAAA;AACnF,CAAC;AAED,SAAgB,kBAAkB,CAAC,KAAsB;IACvD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC9D,OAAO,EAAE,GAAG,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC,kCAAkC,CAAC,EAAE,CAAA;IAClE,CAAC;IACD,IAAI,OAAO,KAAK,CAAC,OAAO,KAAK,SAAS,EAAE,CAAC;QACvC,OAAO,EAAE,GAAG,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC,2BAA2B,CAAC,EAAE,CAAA;IAC3D,CAAC;IACD,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAA;IACpF,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,GAAG,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC,yBAAyB,CAAC,EAAE,CAAA;IAEhF,6BAA6B;IAC7B,MAAM,KAAK,GAAG,EAAE,MAAM,EAAE,CAAC,KAAK,CAAC,OAAO,EAAE,CAAA;IACxC,OAAO,EAAE,GAAG,EAAE,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,EAAE,EAAE,CAAA;AACnF,CAAC","sourcesContent":["/**\n * setShadow / setOpacity / setVisible — 효과 / 가시성 / 투명도.\n *\n * setShadow: state.shadow (color/blur/offsetX/offsetY)\n * setOpacity: state.alpha (컴포넌트 전체 알파, 0..1)\n * setVisible: state.hidden 토글 (true → 숨김)\n */\nimport type { BoardEditOp } from '../types'\nimport { normalizeColor, validateColor } from './color-utils'\n\nexport interface SetShadowInput {\n refids: number[]\n /** synthetic — false 면 blur=0 + color=transparent 매핑 (drawer 가 안 그림). 모델에 enabled 필드 없음. */\n enabled?: boolean\n /** state.shadow.color */\n color?: string\n /** Blur radius (CSS box-shadow 의 blur-radius, Canvas 의 shadowBlur). state.shadow.blur. */\n blur?: number\n /** Horizontal offset (CSS box-shadow 의 offset-x, Canvas 의 shadowOffsetX). state.shadow.offsetX. */\n offsetX?: number\n /** Vertical offset (CSS box-shadow 의 offset-y, Canvas 의 shadowOffsetY). state.shadow.offsetY. */\n offsetY?: number\n}\n\nexport interface SetOpacityInput {\n refids: number[]\n /** state.alpha 의 canonical 이름. 0..1. */\n alpha: number\n}\n\nexport interface SetVisibleInput {\n refids: number[]\n visible: boolean\n}\n\nexport interface OpsResult {\n ops: BoardEditOp[]\n errors: string[]\n}\n\n/**\n * setShadow — CSS box-shadow / Canvas API 표준 이름 (color/offsetX/offsetY/blur) 그대로\n * state.shadow 에 set. things-scene drawer 는 옛 이름 (left/top/blurSize) 도\n * backwards-compat 으로 읽지만 (effect/shadow.ts:36-47), 새 모델은 canonical 만 사용.\n *\n * synthetic enabled=false → blur 0 + transparent 매핑 (drawer 가 안 그림).\n * 모델에 enabled 필드는 없음.\n */\nexport function buildSetShadowOps(input: SetShadowInput): OpsResult {\n if (!Array.isArray(input.refids) || input.refids.length === 0) {\n return { ops: [], errors: ['refids must be a non-empty array'] }\n }\n const refids = input.refids.filter(n => typeof n === 'number' && Number.isFinite(n))\n if (refids.length === 0) return { ops: [], errors: ['No valid numeric refids'] }\n\n const noOp =\n input.enabled === undefined &&\n input.color === undefined &&\n input.blur === undefined &&\n input.offsetX === undefined &&\n input.offsetY === undefined\n if (noOp) return { ops: [], errors: ['No shadow changes specified'] }\n\n if (input.enabled === false) {\n // synthetic — false 면 blur 0 + transparent 으로 변환 (drawer 가 안 그림)\n const patch = { shadow: { blur: 0, color: 'transparent' } }\n return { ops: refids.map(refid => ({ op: 'modify', refid, patch })), errors: [] }\n }\n\n if (input.color !== undefined) {\n const v = validateColor(input.color)\n if (!v.valid) return { ops: [], errors: [v.error ?? 'Invalid shadow color'] }\n }\n if (input.blur !== undefined && (input.blur < 0 || !Number.isFinite(input.blur))) {\n return { ops: [], errors: [`Invalid blur: ${input.blur}`] }\n }\n if (input.offsetX !== undefined && !Number.isFinite(input.offsetX)) {\n return { ops: [], errors: [`Invalid offsetX: ${input.offsetX}`] }\n }\n if (input.offsetY !== undefined && !Number.isFinite(input.offsetY)) {\n return { ops: [], errors: [`Invalid offsetY: ${input.offsetY}`] }\n }\n\n // CSS / Canvas 표준 canonical 이름 — rename 없음\n const sh: Record<string, number | string> = {}\n // validateColor 가 통과한 색만 도달 — normalizeColor 가 여기서 null 반환 안 함\n if (input.color !== undefined) sh.color = normalizeColor(input.color)!\n if (input.blur !== undefined) sh.blur = input.blur\n if (input.offsetX !== undefined) sh.offsetX = input.offsetX\n if (input.offsetY !== undefined) sh.offsetY = input.offsetY\n\n const patch = { shadow: sh }\n return { ops: refids.map(refid => ({ op: 'modify', refid, patch })), errors: [] }\n}\n\nexport function buildSetOpacityOps(input: SetOpacityInput): OpsResult {\n if (!Array.isArray(input.refids) || input.refids.length === 0) {\n return { ops: [], errors: ['refids must be a non-empty array'] }\n }\n if (typeof input.alpha !== 'number' || input.alpha < 0 || input.alpha > 1) {\n return { ops: [], errors: [`alpha must be 0..1: ${input.alpha}`] }\n }\n const refids = input.refids.filter(n => typeof n === 'number' && Number.isFinite(n))\n if (refids.length === 0) return { ops: [], errors: ['No valid numeric refids'] }\n\n // canonical 이름 그대로\n const patch = { alpha: input.alpha }\n return { ops: refids.map(refid => ({ op: 'modify', refid, patch })), errors: [] }\n}\n\nexport function buildSetVisibleOps(input: SetVisibleInput): OpsResult {\n if (!Array.isArray(input.refids) || input.refids.length === 0) {\n return { ops: [], errors: ['refids must be a non-empty array'] }\n }\n if (typeof input.visible !== 'boolean') {\n return { ops: [], errors: ['visible must be a boolean'] }\n }\n const refids = input.refids.filter(n => typeof n === 'number' && Number.isFinite(n))\n if (refids.length === 0) return { ops: [], errors: ['No valid numeric refids'] }\n\n // hidden 은 visible 의 inverse\n const patch = { hidden: !input.visible }\n return { ops: refids.map(refid => ({ op: 'modify', refid, patch })), errors: [] }\n}\n"]}
@@ -0,0 +1,60 @@
1
+ /**
2
+ * setFill — 컴포넌트 배경 (state.fillStyle) 변경.
3
+ *
4
+ * 4 modes 지원: none / solid / gradient / pattern.
5
+ *
6
+ * 출력 정책 — refid 당 1 op:
7
+ * - solid: fillStyle = '#rrggbb' or 'rgba(...)' (string)
8
+ * - gradient: fillStyle = { type:'gradient', gradientType, colorStops, rotation, ... }
9
+ * - pattern: fillStyle = { type:'pattern', image, ... }
10
+ * - none: fillStyle = { type:'none' }
11
+ * - mode 미지정 + color 만: 부분 update (string 또는 object 병합)
12
+ *
13
+ * things-scene 실제 데이터 형식 (drawer/fill.ts 검증):
14
+ * - gradient: { type, gradientType, colorStops: [{position, color}], rotation (라디안) }
15
+ * - rotation 은 라디안 — `Math.cos(fRotation)` 직접 사용
16
+ *
17
+ * 이전 2-op (null + new) 방식은 중간 null 상태가 onchange / 3D texture
18
+ * 크래시 유발 → 제거. 정확한 colorStops 출력으로 stale 필드 문제는 자체 해결.
19
+ */
20
+ import type { BoardEditOp } from '../types';
21
+ export interface SetFillInput {
22
+ refids: number[];
23
+ mode?: 'none' | 'solid' | 'gradient' | 'pattern';
24
+ color?: string;
25
+ gradientType?: 'linear' | 'radial';
26
+ /** colorStops — things-scene 의 canonical 필드명 그대로. position 0..1, color hex/rgb/named. */
27
+ colorStops?: Array<{
28
+ color: string;
29
+ position: number;
30
+ }>;
31
+ /** 회전 — 입력은 degrees (사용자/AI 친화), 내부 변환 후 things-scene 의 라디안으로 저장. */
32
+ rotation?: number;
33
+ image?: string;
34
+ /** pattern: 영역에 맞춰 비율 조정 (true) vs 원본 크기 사용 (false). */
35
+ fitPattern?: boolean;
36
+ /** pattern: 반복 안 함 (true) — 1회만 그림. */
37
+ noRepeat?: boolean;
38
+ /** pattern: 정렬. fitPattern=false 일 때만 적용. */
39
+ align?: 'center' | 'left-top' | 'left-bottom' | 'right-top' | 'right-bottom' | 'left' | 'right' | 'top' | 'bottom';
40
+ /** pattern: 영역 내 오프셋 (fitPattern=false 일 때). */
41
+ offsetX?: number;
42
+ offsetY?: number;
43
+ /** pattern: 패턴 크기 (기본은 이미지 원본 크기). */
44
+ patternWidth?: number;
45
+ patternHeight?: number;
46
+ opacity?: number;
47
+ }
48
+ export interface SetFillResult {
49
+ ops: BoardEditOp[];
50
+ errors: string[];
51
+ }
52
+ /**
53
+ * setFill 입력 검증 + BoardEditOp[] 생성.
54
+ *
55
+ * 기본 정책:
56
+ * - refids 비어있거나 mode 도 색도 없으면 에러
57
+ * - 색상 입력은 normalizeColor 통과해야 적용
58
+ * - mode='gradient' 인데 stops 없으면 자동 생성 (color → 어두운 변형)
59
+ */
60
+ export declare function buildSetFillOps(input: SetFillInput): SetFillResult;