@things-factory/board-ai 10.0.0-beta.85 → 10.0.0-beta.87

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.
@@ -19,7 +19,8 @@ export interface SetFontInput {
19
19
  fontWeight?: 'normal' | 'bold' | 'lighter' | 'bolder' | string | number;
20
20
  fontStyle?: 'normal' | 'italic' | 'oblique';
21
21
  fontColor?: string;
22
- textAlign?: 'left' | 'center' | 'right' | 'start' | 'end' | 'justify';
22
+ /** Canvas API 표준 'justify' Canvas 미지원이라 제외. */
23
+ textAlign?: 'left' | 'center' | 'right' | 'start' | 'end';
23
24
  textBaseline?: 'top' | 'middle' | 'bottom' | 'hanging' | 'alphabetic' | 'ideographic';
24
25
  lineHeight?: number;
25
26
  }
@@ -1,4 +1,18 @@
1
1
  import { normalizeColor, validateColor } from './color-utils';
2
+ // Canvas CanvasRenderingContext2D 가 허용하는 값만.
3
+ // dash / lineCap / lineJoin 과 동일 패턴 — AI 가 비표준 값 ('serif-italic',
4
+ // 'middle-align' 등) 을 보내도 Canvas 가 silent ignore → 효과 없는 op 가 그대로
5
+ // 실행되는 것 방지.
6
+ const VALID_FONT_STYLE = new Set(['normal', 'italic', 'oblique']);
7
+ const VALID_TEXT_ALIGN = new Set(['start', 'end', 'left', 'right', 'center']);
8
+ const VALID_TEXT_BASELINE = new Set([
9
+ 'top',
10
+ 'middle',
11
+ 'bottom',
12
+ 'hanging',
13
+ 'alphabetic',
14
+ 'ideographic'
15
+ ]);
2
16
  export function buildSetTextOps(input) {
3
17
  if (!Array.isArray(input.refids) || input.refids.length === 0) {
4
18
  return { ops: [], errors: ['refids must be a non-empty array'] };
@@ -44,6 +58,24 @@ export function buildSetFontOps(input) {
44
58
  if (!v.valid)
45
59
  return { ops: [], errors: [v.error ?? 'Invalid fontColor'] };
46
60
  }
61
+ if (input.fontStyle !== undefined && !VALID_FONT_STYLE.has(input.fontStyle)) {
62
+ return {
63
+ ops: [],
64
+ errors: [`Unknown fontStyle: '${input.fontStyle}'. Valid: ${[...VALID_FONT_STYLE].join(' / ')}.`]
65
+ };
66
+ }
67
+ if (input.textAlign !== undefined && !VALID_TEXT_ALIGN.has(input.textAlign)) {
68
+ return {
69
+ ops: [],
70
+ errors: [`Unknown textAlign: '${input.textAlign}'. Valid: ${[...VALID_TEXT_ALIGN].join(' / ')}.`]
71
+ };
72
+ }
73
+ if (input.textBaseline !== undefined && !VALID_TEXT_BASELINE.has(input.textBaseline)) {
74
+ return {
75
+ ops: [],
76
+ errors: [`Unknown textBaseline: '${input.textBaseline}'. Valid: ${[...VALID_TEXT_BASELINE].join(' / ')}.`]
77
+ };
78
+ }
47
79
  const patch = {};
48
80
  if (input.fontFamily)
49
81
  patch.fontFamily = input.fontFamily;
@@ -1 +1 @@
1
- {"version":3,"file":"text-tools.js","sourceRoot":"","sources":["../../../../server/service/styling/text-tools.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * setText / setFont — 컴포넌트의 텍스트 내용 / 폰트 스타일.\n *\n * setText: state.text 변경 (내용)\n * setFont: state.fontFamily / fontSize / fontWeight / fontStyle / fontColor /\n * textAlign / textBaseline / lineHeight 변경 (폰트 스타일)\n *\n * 두 tool 분리 — 의도가 다름. 내용 변경 vs 시각 스타일.\n */\nimport type { BoardEditOp } from '../types'\nimport { normalizeColor, validateColor } from './color-utils'\n\nexport interface SetTextInput {\n refids: number[]\n text: string\n}\n\nexport interface SetFontInput {\n refids: number[]\n fontFamily?: string\n fontSize?: number\n fontWeight?: 'normal' | 'bold' | 'lighter' | 'bolder' | string | number\n fontStyle?: 'normal' | 'italic' | 'oblique'\n fontColor?: string\n textAlign?: 'left' | 'center' | 'right' | 'start' | 'end' | 'justify'\n textBaseline?: 'top' | 'middle' | 'bottom' | 'hanging' | 'alphabetic' | 'ideographic'\n lineHeight?: number\n}\n\nexport interface OpsResult {\n ops: BoardEditOp[]\n errors: string[]\n}\n\nexport function buildSetTextOps(input: SetTextInput): 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.text !== 'string') {\n return { ops: [], errors: ['text must be a string'] }\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 patch = { text: input.text }\n return {\n ops: refids.map(refid => ({ op: 'modify', refid, patch })),\n errors: []\n }\n}\n\nexport function buildSetFontOps(input: SetFontInput): 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 // 어느 인자도 없으면 무의미\n const noOp = !(\n input.fontFamily ||\n input.fontSize !== undefined ||\n input.fontWeight !== undefined ||\n input.fontStyle ||\n input.fontColor ||\n input.textAlign ||\n input.textBaseline ||\n input.lineHeight !== undefined\n )\n if (noOp) return { ops: [], errors: ['No font changes specified'] }\n\n if (input.fontSize !== undefined && (input.fontSize <= 0 || !Number.isFinite(input.fontSize))) {\n return { ops: [], errors: [`Invalid fontSize: ${input.fontSize}`] }\n }\n if (input.lineHeight !== undefined && input.lineHeight <= 0) {\n return { ops: [], errors: [`Invalid lineHeight: ${input.lineHeight}`] }\n }\n if (input.fontColor !== undefined) {\n const v = validateColor(input.fontColor)\n if (!v.valid) return { ops: [], errors: [v.error ?? 'Invalid fontColor'] }\n }\n\n const patch: any = {}\n if (input.fontFamily) patch.fontFamily = input.fontFamily\n if (input.fontSize !== undefined) patch.fontSize = input.fontSize\n if (input.fontWeight !== undefined) patch.fontWeight = String(input.fontWeight)\n if (input.fontStyle) patch.fontStyle = input.fontStyle\n if (input.fontColor !== undefined) patch.fontColor = normalizeColor(input.fontColor)\n if (input.textAlign) patch.textAlign = input.textAlign\n if (input.textBaseline) patch.textBaseline = input.textBaseline\n if (input.lineHeight !== undefined) patch.lineHeight = input.lineHeight\n\n return {\n ops: refids.map(refid => ({ op: 'modify', refid, patch })),\n errors: []\n }\n}\n"]}
1
+ {"version":3,"file":"text-tools.js","sourceRoot":"","sources":["../../../../server/service/styling/text-tools.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * setText / setFont — 컴포넌트의 텍스트 내용 / 폰트 스타일.\n *\n * setText: state.text 변경 (내용)\n * setFont: state.fontFamily / fontSize / fontWeight / fontStyle / fontColor /\n * textAlign / textBaseline / lineHeight 변경 (폰트 스타일)\n *\n * 두 tool 분리 — 의도가 다름. 내용 변경 vs 시각 스타일.\n */\nimport type { BoardEditOp } from '../types'\nimport { normalizeColor, validateColor } from './color-utils'\n\nexport interface SetTextInput {\n refids: number[]\n text: string\n}\n\nexport interface SetFontInput {\n refids: number[]\n fontFamily?: string\n fontSize?: number\n fontWeight?: 'normal' | 'bold' | 'lighter' | 'bolder' | string | number\n fontStyle?: 'normal' | 'italic' | 'oblique'\n fontColor?: string\n /** Canvas API 표준 — 'justify' 는 Canvas 미지원이라 제외. */\n textAlign?: 'left' | 'center' | 'right' | 'start' | 'end'\n textBaseline?: 'top' | 'middle' | 'bottom' | 'hanging' | 'alphabetic' | 'ideographic'\n lineHeight?: number\n}\n\n// Canvas CanvasRenderingContext2D 가 허용하는 값만.\n// dash / lineCap / lineJoin 과 동일 패턴 — AI 가 비표준 값 ('serif-italic',\n// 'middle-align' 등) 을 보내도 Canvas 가 silent ignore → 효과 없는 op 가 그대로\n// 실행되는 것 방지.\nconst VALID_FONT_STYLE = new Set(['normal', 'italic', 'oblique'])\nconst VALID_TEXT_ALIGN = new Set(['start', 'end', 'left', 'right', 'center'])\nconst VALID_TEXT_BASELINE = new Set([\n 'top',\n 'middle',\n 'bottom',\n 'hanging',\n 'alphabetic',\n 'ideographic'\n])\n\nexport interface OpsResult {\n ops: BoardEditOp[]\n errors: string[]\n}\n\nexport function buildSetTextOps(input: SetTextInput): 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.text !== 'string') {\n return { ops: [], errors: ['text must be a string'] }\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 patch = { text: input.text }\n return {\n ops: refids.map(refid => ({ op: 'modify', refid, patch })),\n errors: []\n }\n}\n\nexport function buildSetFontOps(input: SetFontInput): 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 // 어느 인자도 없으면 무의미\n const noOp = !(\n input.fontFamily ||\n input.fontSize !== undefined ||\n input.fontWeight !== undefined ||\n input.fontStyle ||\n input.fontColor ||\n input.textAlign ||\n input.textBaseline ||\n input.lineHeight !== undefined\n )\n if (noOp) return { ops: [], errors: ['No font changes specified'] }\n\n if (input.fontSize !== undefined && (input.fontSize <= 0 || !Number.isFinite(input.fontSize))) {\n return { ops: [], errors: [`Invalid fontSize: ${input.fontSize}`] }\n }\n if (input.lineHeight !== undefined && input.lineHeight <= 0) {\n return { ops: [], errors: [`Invalid lineHeight: ${input.lineHeight}`] }\n }\n if (input.fontColor !== undefined) {\n const v = validateColor(input.fontColor)\n if (!v.valid) return { ops: [], errors: [v.error ?? 'Invalid fontColor'] }\n }\n if (input.fontStyle !== undefined && !VALID_FONT_STYLE.has(input.fontStyle)) {\n return {\n ops: [],\n errors: [`Unknown fontStyle: '${input.fontStyle}'. Valid: ${[...VALID_FONT_STYLE].join(' / ')}.`]\n }\n }\n if (input.textAlign !== undefined && !VALID_TEXT_ALIGN.has(input.textAlign)) {\n return {\n ops: [],\n errors: [`Unknown textAlign: '${input.textAlign}'. Valid: ${[...VALID_TEXT_ALIGN].join(' / ')}.`]\n }\n }\n if (input.textBaseline !== undefined && !VALID_TEXT_BASELINE.has(input.textBaseline)) {\n return {\n ops: [],\n errors: [`Unknown textBaseline: '${input.textBaseline}'. Valid: ${[...VALID_TEXT_BASELINE].join(' / ')}.`]\n }\n }\n\n const patch: any = {}\n if (input.fontFamily) patch.fontFamily = input.fontFamily\n if (input.fontSize !== undefined) patch.fontSize = input.fontSize\n if (input.fontWeight !== undefined) patch.fontWeight = String(input.fontWeight)\n if (input.fontStyle) patch.fontStyle = input.fontStyle\n if (input.fontColor !== undefined) patch.fontColor = normalizeColor(input.fontColor)\n if (input.textAlign) patch.textAlign = input.textAlign\n if (input.textBaseline) patch.textBaseline = input.textBaseline\n if (input.lineHeight !== undefined) patch.lineHeight = input.lineHeight\n\n return {\n ops: refids.map(refid => ({ op: 'modify', refid, patch })),\n errors: []\n }\n}\n"]}