@things-factory/board-ai 10.0.0-beta.82 → 10.0.0-beta.83

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.
@@ -15,6 +15,11 @@ const VALID_DASH = new Set([
15
15
  'long-dash-dot',
16
16
  'long-dash-dot-dot'
17
17
  ]);
18
+ // Canvas CanvasRenderingContext2D 가 허용하는 lineCap / lineJoin 만.
19
+ // 잘못된 값은 Canvas 가 silent ignore → AI 가 'flat' / 'cap' 등을 보내도
20
+ // 효과 없음. 명시 검증으로 ops 미실행 + 명확한 에러.
21
+ const VALID_LINE_CAP = new Set(['butt', 'round', 'square']);
22
+ const VALID_LINE_JOIN = new Set(['miter', 'round', 'bevel']);
18
23
  export function buildSetStrokeOps(input) {
19
24
  if (!Array.isArray(input.refids) || input.refids.length === 0) {
20
25
  return { ops: [], errors: ['refids must be a non-empty array of numbers'] };
@@ -40,6 +45,18 @@ export function buildSetStrokeOps(input) {
40
45
  if (input.lineWidth !== undefined && (input.lineWidth < 0 || !Number.isFinite(input.lineWidth))) {
41
46
  return { ops: [], errors: [`Invalid lineWidth: ${input.lineWidth}`] };
42
47
  }
48
+ if (input.lineCap !== undefined && !VALID_LINE_CAP.has(input.lineCap)) {
49
+ return {
50
+ ops: [],
51
+ errors: [`Unknown lineCap: '${input.lineCap}'. Valid: ${[...VALID_LINE_CAP].join(' / ')}.`]
52
+ };
53
+ }
54
+ if (input.lineJoin !== undefined && !VALID_LINE_JOIN.has(input.lineJoin)) {
55
+ return {
56
+ ops: [],
57
+ errors: [`Unknown lineJoin: '${input.lineJoin}'. Valid: ${[...VALID_LINE_JOIN].join(' / ')}.`]
58
+ };
59
+ }
43
60
  // dash 정규화 — alias → 실제 preset → 검증
44
61
  let dashValue;
45
62
  if (input.dash !== undefined) {
@@ -1 +1 @@
1
- {"version":3,"file":"stroke-tools.js","sourceRoot":"","sources":["../../../../server/service/styling/stroke-tools.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * setStroke — 컴포넌트 테두리 / 라인 스타일.\n *\n * **things-scene 의 실제 데이터 형식 (drawer/stroke.ts 검증):**\n * - `state.strokeStyle: string` (색상 — Canvas API 의 strokeStyle 에 직접 대입되는 CSS color)\n * - `state.lineWidth: number` (top-level)\n * - `state.lineDash: string` (top-level — preset 이름. drawer 가 lineWidth 기반으로 배열 변환)\n * valid: 'solid' | 'round-dot' | 'square-dot' | 'dash' | 'dash-dot' | 'long-dash' | 'long-dash-dot' | 'long-dash-dot-dot'\n * - `state.lineCap: 'butt' | 'round' | 'square'` (top-level)\n * - `state.lineJoin: 'miter' | 'round' | 'bevel'` (top-level)\n *\n * 인터페이스의 `StrokeStyle` 객체 구조는 stale. 사용처는 모두 top-level 직접 접근.\n *\n * 입력 단순화 — AI 친화적 alias 도 받아 things-scene preset 으로 매핑:\n * AI 'dashed' → 'dash', 'dotted' → 'square-dot', 'dashdot' → 'dash-dot'.\n */\nimport type { BoardEditOp } from '../types'\nimport { normalizeColor, validateColor } from './color-utils'\n\nexport interface SetStrokeInput {\n refids: number[]\n /** synthetic — false 면 lineWidth=0 으로 매핑 (drawer 가 안 그림). 모델에 enabled 필드는 안 박힘. */\n enabled?: boolean\n /** state.strokeStyle 의 canonical 이름. CSS color string. */\n strokeStyle?: string\n /** state.lineWidth 의 canonical 이름. pixels. */\n lineWidth?: number\n /** preset 이름. AI 친화 alias 자동 매핑 — 'dashed' / 'dotted' / 'dashdot' 도 가능. */\n dash?:\n | 'solid'\n | 'dashed'\n | 'dotted'\n | 'dashdot'\n | 'round-dot'\n | 'square-dot'\n | 'dash'\n | 'dash-dot'\n | 'long-dash'\n | 'long-dash-dot'\n | 'long-dash-dot-dot'\n | string\n lineCap?: 'butt' | 'round' | 'square'\n lineJoin?: 'miter' | 'round' | 'bevel'\n}\n\nexport interface SetStrokeResult {\n ops: BoardEditOp[]\n errors: string[]\n}\n\n/** AI 친화 alias → things-scene 의 실제 preset 이름. */\nconst DASH_ALIASES: Record<string, string> = {\n dashed: 'dash',\n dotted: 'square-dot',\n dashdot: 'dash-dot'\n}\n\nconst VALID_DASH = new Set([\n 'solid',\n 'round-dot',\n 'square-dot',\n 'dash',\n 'dash-dot',\n 'long-dash',\n 'long-dash-dot',\n 'long-dash-dot-dot'\n])\n\nexport function buildSetStrokeOps(input: SetStrokeInput): SetStrokeResult {\n if (!Array.isArray(input.refids) || input.refids.length === 0) {\n return { ops: [], errors: ['refids must be a non-empty array of numbers'] }\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.strokeStyle === undefined &&\n input.lineWidth === undefined &&\n input.dash === undefined &&\n input.lineCap === undefined &&\n input.lineJoin === undefined\n if (noOp) {\n return { ops: [], errors: ['No stroke changes specified'] }\n }\n\n // 검증\n if (input.strokeStyle !== undefined) {\n const v = validateColor(input.strokeStyle)\n if (!v.valid) return { ops: [], errors: [v.error ?? 'Invalid strokeStyle'] }\n }\n if (input.lineWidth !== undefined && (input.lineWidth < 0 || !Number.isFinite(input.lineWidth))) {\n return { ops: [], errors: [`Invalid lineWidth: ${input.lineWidth}`] }\n }\n\n // dash 정규화 — alias → 실제 preset → 검증\n let dashValue: string | undefined\n if (input.dash !== undefined) {\n const raw = String(input.dash)\n const resolved = DASH_ALIASES[raw] ?? raw\n if (!VALID_DASH.has(resolved)) {\n return {\n ops: [],\n errors: [\n `Unknown dash: '${input.dash}'. Valid: ${[...VALID_DASH].join(' / ')} (or alias: dashed/dotted/dashdot).`\n ]\n }\n }\n dashValue = resolved\n }\n\n // patch 구성 — canonical 이름 그대로. 매핑 X. (color 만 normalizeColor 통과 — 입력 정규화는 mapping 이 아닌 처리)\n const patch: any = {}\n if (input.strokeStyle !== undefined) patch.strokeStyle = normalizeColor(input.strokeStyle)\n if (input.lineWidth !== undefined) patch.lineWidth = input.lineWidth\n if (dashValue !== undefined) patch.lineDash = dashValue\n if (input.lineCap !== undefined) patch.lineCap = input.lineCap\n if (input.lineJoin !== undefined) patch.lineJoin = input.lineJoin\n\n // synthetic enabled — false 면 lineWidth 0 으로 변환 (drawer 가 안 그림 — drawer/stroke.ts:9)\n if (input.enabled === false) {\n patch.lineWidth = 0\n }\n\n const ops: BoardEditOp[] = refids.map(refid => ({ op: 'modify', refid, patch }))\n return { ops, errors: [] }\n}\n"]}
1
+ {"version":3,"file":"stroke-tools.js","sourceRoot":"","sources":["../../../../server/service/styling/stroke-tools.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * setStroke — 컴포넌트 테두리 / 라인 스타일.\n *\n * **things-scene 의 실제 데이터 형식 (drawer/stroke.ts 검증):**\n * - `state.strokeStyle: string` (색상 — Canvas API 의 strokeStyle 에 직접 대입되는 CSS color)\n * - `state.lineWidth: number` (top-level)\n * - `state.lineDash: string` (top-level — preset 이름. drawer 가 lineWidth 기반으로 배열 변환)\n * valid: 'solid' | 'round-dot' | 'square-dot' | 'dash' | 'dash-dot' | 'long-dash' | 'long-dash-dot' | 'long-dash-dot-dot'\n * - `state.lineCap: 'butt' | 'round' | 'square'` (top-level)\n * - `state.lineJoin: 'miter' | 'round' | 'bevel'` (top-level)\n *\n * 인터페이스의 `StrokeStyle` 객체 구조는 stale. 사용처는 모두 top-level 직접 접근.\n *\n * 입력 단순화 — AI 친화적 alias 도 받아 things-scene preset 으로 매핑:\n * AI 'dashed' → 'dash', 'dotted' → 'square-dot', 'dashdot' → 'dash-dot'.\n */\nimport type { BoardEditOp } from '../types'\nimport { normalizeColor, validateColor } from './color-utils'\n\nexport interface SetStrokeInput {\n refids: number[]\n /** synthetic — false 면 lineWidth=0 으로 매핑 (drawer 가 안 그림). 모델에 enabled 필드는 안 박힘. */\n enabled?: boolean\n /** state.strokeStyle 의 canonical 이름. CSS color string. */\n strokeStyle?: string\n /** state.lineWidth 의 canonical 이름. pixels. */\n lineWidth?: number\n /** preset 이름. AI 친화 alias 자동 매핑 — 'dashed' / 'dotted' / 'dashdot' 도 가능. */\n dash?:\n | 'solid'\n | 'dashed'\n | 'dotted'\n | 'dashdot'\n | 'round-dot'\n | 'square-dot'\n | 'dash'\n | 'dash-dot'\n | 'long-dash'\n | 'long-dash-dot'\n | 'long-dash-dot-dot'\n | string\n lineCap?: 'butt' | 'round' | 'square'\n lineJoin?: 'miter' | 'round' | 'bevel'\n}\n\nexport interface SetStrokeResult {\n ops: BoardEditOp[]\n errors: string[]\n}\n\n/** AI 친화 alias → things-scene 의 실제 preset 이름. */\nconst DASH_ALIASES: Record<string, string> = {\n dashed: 'dash',\n dotted: 'square-dot',\n dashdot: 'dash-dot'\n}\n\nconst VALID_DASH = new Set([\n 'solid',\n 'round-dot',\n 'square-dot',\n 'dash',\n 'dash-dot',\n 'long-dash',\n 'long-dash-dot',\n 'long-dash-dot-dot'\n])\n\n// Canvas CanvasRenderingContext2D 가 허용하는 lineCap / lineJoin 만.\n// 잘못된 값은 Canvas 가 silent ignore → AI 가 'flat' / 'cap' 등을 보내도\n// 효과 없음. 명시 검증으로 ops 미실행 + 명확한 에러.\nconst VALID_LINE_CAP = new Set(['butt', 'round', 'square'])\nconst VALID_LINE_JOIN = new Set(['miter', 'round', 'bevel'])\n\nexport function buildSetStrokeOps(input: SetStrokeInput): SetStrokeResult {\n if (!Array.isArray(input.refids) || input.refids.length === 0) {\n return { ops: [], errors: ['refids must be a non-empty array of numbers'] }\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.strokeStyle === undefined &&\n input.lineWidth === undefined &&\n input.dash === undefined &&\n input.lineCap === undefined &&\n input.lineJoin === undefined\n if (noOp) {\n return { ops: [], errors: ['No stroke changes specified'] }\n }\n\n // 검증\n if (input.strokeStyle !== undefined) {\n const v = validateColor(input.strokeStyle)\n if (!v.valid) return { ops: [], errors: [v.error ?? 'Invalid strokeStyle'] }\n }\n if (input.lineWidth !== undefined && (input.lineWidth < 0 || !Number.isFinite(input.lineWidth))) {\n return { ops: [], errors: [`Invalid lineWidth: ${input.lineWidth}`] }\n }\n if (input.lineCap !== undefined && !VALID_LINE_CAP.has(input.lineCap)) {\n return {\n ops: [],\n errors: [`Unknown lineCap: '${input.lineCap}'. Valid: ${[...VALID_LINE_CAP].join(' / ')}.`]\n }\n }\n if (input.lineJoin !== undefined && !VALID_LINE_JOIN.has(input.lineJoin)) {\n return {\n ops: [],\n errors: [`Unknown lineJoin: '${input.lineJoin}'. Valid: ${[...VALID_LINE_JOIN].join(' / ')}.`]\n }\n }\n\n // dash 정규화 — alias → 실제 preset → 검증\n let dashValue: string | undefined\n if (input.dash !== undefined) {\n const raw = String(input.dash)\n const resolved = DASH_ALIASES[raw] ?? raw\n if (!VALID_DASH.has(resolved)) {\n return {\n ops: [],\n errors: [\n `Unknown dash: '${input.dash}'. Valid: ${[...VALID_DASH].join(' / ')} (or alias: dashed/dotted/dashdot).`\n ]\n }\n }\n dashValue = resolved\n }\n\n // patch 구성 — canonical 이름 그대로. 매핑 X. (color 만 normalizeColor 통과 — 입력 정규화는 mapping 이 아닌 처리)\n const patch: any = {}\n if (input.strokeStyle !== undefined) patch.strokeStyle = normalizeColor(input.strokeStyle)\n if (input.lineWidth !== undefined) patch.lineWidth = input.lineWidth\n if (dashValue !== undefined) patch.lineDash = dashValue\n if (input.lineCap !== undefined) patch.lineCap = input.lineCap\n if (input.lineJoin !== undefined) patch.lineJoin = input.lineJoin\n\n // synthetic enabled — false 면 lineWidth 0 으로 변환 (drawer 가 안 그림 — drawer/stroke.ts:9)\n if (input.enabled === false) {\n patch.lineWidth = 0\n }\n\n const ops: BoardEditOp[] = refids.map(refid => ({ op: 'modify', refid, patch }))\n return { ops, errors: [] }\n}\n"]}