返回 oh-my-ppt
shapeRegistry.ts
1 import type { InsertShapeType } from './buildInsertElementHtml'
2
3 export interface ShapeDefinition {
4 type: InsertShapeType
5 defaultWidth: number
6 defaultHeight: number
7 defaultFill: string
8 defaultStroke: string
9 strokeWidth: number
10 /**
11 * Produce the inner SVG markup for a viewBox of the given width/height.
12 * Filled shapes use preserveAspectRatio="none" so users can stretch them
13 * freely; the stroke-only line keeps a non-scaling stroke.
14 */
15 renderInner: (viewBoxW: number, viewBoxH: number, ctx: ShapeRenderContext) => string
16 }
17
18 export interface ShapeRenderContext {
19 fill: string
20 stroke: string
21 strokeWidth: number
22 }
23
24 const DEFAULT_FILL = '#d4e4c1'
25 const DEFAULT_STROKE = '#7a875f'
26
27 function round(n: number): number {
28 return Math.round(n * 100) / 100
29 }
30
31 /** Points string for a regular polygon inscribed in radius r around (cx, cy). */
32 function regularPolygon(
33 cx: number,
34 cy: number,
35 r: number,
36 sides: number,
37 startAngleDeg: number
38 ): string {
39 const pts: string[] = []
40 for (let i = 0; i < sides; i++) {
41 const a = ((startAngleDeg + (360 / sides) * i) * Math.PI) / 180
42 pts.push(`${round(cx + r * Math.cos(a))},${round(cy + r * Math.sin(a))}`)
43 }
44 return pts.join(' ')
45 }
46
47 /** Points string for a star with `points` spikes, alternating outer/inner radius. */
48 function starPolygon(
49 cx: number,
50 cy: number,
51 outerR: number,
52 innerR: number,
53 points: number,
54 startAngleDeg: number
55 ): string {
56 const pts: string[] = []
57 for (let i = 0; i < points * 2; i++) {
58 const r = i % 2 === 0 ? outerR : innerR
59 const a = ((startAngleDeg + (180 / points) * i) * Math.PI) / 180
60 pts.push(`${round(cx + r * Math.cos(a))},${round(cy + r * Math.sin(a))}`)
61 }
62 return pts.join(' ')
63 }
64
65 function filledPolygon(
66 points: string,
67 ctx: ShapeRenderContext,
68 extraAttrs = ''
69 ): string {
70 return `<polygon points="${points}" fill="${ctx.fill}" stroke="${ctx.stroke}" stroke-width="${ctx.strokeWidth}" stroke-linejoin="round" ${extraAttrs} />`
71 }
72
73 export const SHAPE_REGISTRY: Record<InsertShapeType, ShapeDefinition> = {
74 rect: {
75 type: 'rect',
76 defaultWidth: 220,
77 defaultHeight: 120,
78 defaultFill: DEFAULT_FILL,
79 defaultStroke: DEFAULT_STROKE,
80 strokeWidth: 2,
81 renderInner: (w, h, ctx) => {
82 const inset = ctx.strokeWidth / 2 + 0.5
83 return `<rect x="${inset}" y="${inset}" width="${round(w - inset * 2)}" height="${round(h - inset * 2)}" fill="${ctx.fill}" stroke="${ctx.stroke}" stroke-width="${ctx.strokeWidth}" />`
84 }
85 },
86 'rounded-rect': {
87 type: 'rounded-rect',
88 defaultWidth: 240,
89 defaultHeight: 120,
90 defaultFill: DEFAULT_FILL,
91 defaultStroke: DEFAULT_STROKE,
92 strokeWidth: 2,
93 renderInner: (w, h, ctx) => {
94 const inset = ctx.strokeWidth / 2 + 0.5
95 const rx = Math.min(w, h) * 0.15
96 return `<rect x="${inset}" y="${inset}" width="${round(w - inset * 2)}" height="${round(h - inset * 2)}" rx="${rx}" ry="${rx}" fill="${ctx.fill}" stroke="${ctx.stroke}" stroke-width="${ctx.strokeWidth}" />`
97 }
98 },
99 ellipse: {
100 type: 'ellipse',
101 defaultWidth: 140,
102 defaultHeight: 140,
103 defaultFill: DEFAULT_FILL,
104 defaultStroke: DEFAULT_STROKE,
105 strokeWidth: 2,
106 renderInner: (w, h, ctx) => {
107 const cx = w / 2
108 const cy = h / 2
109 const rx = w / 2 - ctx.strokeWidth / 2 - 0.5
110 const ry = h / 2 - ctx.strokeWidth / 2 - 0.5
111 return `<ellipse cx="${cx}" cy="${cy}" rx="${rx}" ry="${ry}" fill="${ctx.fill}" stroke="${ctx.stroke}" stroke-width="${ctx.strokeWidth}" />`
112 }
113 },
114 triangle: {
115 type: 'triangle',
116 defaultWidth: 140,
117 defaultHeight: 120,
118 defaultFill: DEFAULT_FILL,
119 defaultStroke: DEFAULT_STROKE,
120 strokeWidth: 2,
121 renderInner: (w, h, ctx) => {
122 const pad = ctx.strokeWidth / 2 + 0.5
123 const pts = `${w / 2},${pad} ${w - pad},${h - pad} ${pad},${h - pad}`
124 return filledPolygon(pts, ctx)
125 }
126 },
127 diamond: {
128 type: 'diamond',
129 defaultWidth: 160,
130 defaultHeight: 120,
131 defaultFill: DEFAULT_FILL,
132 defaultStroke: DEFAULT_STROKE,
133 strokeWidth: 2,
134 renderInner: (w, h, ctx) => {
135 const pad = ctx.strokeWidth / 2 + 0.5
136 const cx = w / 2
137 const cy = h / 2
138 const pts = `${cx},${pad} ${w - pad},${cy} ${cx},${h - pad} ${pad},${cy}`
139 return filledPolygon(pts, ctx)
140 }
141 },
142 pentagon: {
143 type: 'pentagon',
144 defaultWidth: 140,
145 defaultHeight: 140,
146 defaultFill: DEFAULT_FILL,
147 defaultStroke: DEFAULT_STROKE,
148 strokeWidth: 2,
149 renderInner: (w, h, ctx) => {
150 const pad = ctx.strokeWidth / 2 + 0.5
151 const r = Math.min(w, h) / 2 - pad
152 const pts = regularPolygon(w / 2, h / 2, r, 5, -90)
153 return filledPolygon(pts, ctx)
154 }
155 },
156 hexagon: {
157 type: 'hexagon',
158 defaultWidth: 160,
159 defaultHeight: 140,
160 defaultFill: DEFAULT_FILL,
161 defaultStroke: DEFAULT_STROKE,
162 strokeWidth: 2,
163 renderInner: (w, h, ctx) => {
164 const pad = ctx.strokeWidth / 2 + 0.5
165 const r = Math.min(w, h) / 2 - pad
166 // pointy-top hexagon (vertex at top)
167 const pts = regularPolygon(w / 2, h / 2, r, 6, -90)
168 return filledPolygon(pts, ctx)
169 }
170 },
171 parallelogram: {
172 type: 'parallelogram',
173 defaultWidth: 220,
174 defaultHeight: 120,
175 defaultFill: DEFAULT_FILL,
176 defaultStroke: DEFAULT_STROKE,
177 strokeWidth: 2,
178 renderInner: (w, h, ctx) => {
179 const pad = ctx.strokeWidth / 2 + 0.5
180 const skew = h * 0.4
181 const pts = `${pad + skew},${pad} ${w - pad},${pad} ${w - pad - skew},${h - pad} ${pad},${h - pad}`
182 return filledPolygon(pts, ctx)
183 }
184 },
185 trapezoid: {
186 type: 'trapezoid',
187 defaultWidth: 200,
188 defaultHeight: 120,
189 defaultFill: DEFAULT_FILL,
190 defaultStroke: DEFAULT_STROKE,
191 strokeWidth: 2,
192 renderInner: (w, h, ctx) => {
193 const pad = ctx.strokeWidth / 2 + 0.5
194 const topInset = w * 0.2
195 const pts = `${pad + topInset},${pad} ${w - pad - topInset},${pad} ${w - pad},${h - pad} ${pad},${h - pad}`
196 return filledPolygon(pts, ctx)
197 }
198 },
199 'star-5': {
200 type: 'star-5',
201 defaultWidth: 140,
202 defaultHeight: 140,
203 defaultFill: DEFAULT_FILL,
204 defaultStroke: DEFAULT_STROKE,
205 strokeWidth: 2,
206 renderInner: (w, h, ctx) => {
207 const pad = ctx.strokeWidth / 2 + 0.5
208 const outerR = Math.min(w, h) / 2 - pad
209 const innerR = outerR * 0.4
210 const pts = starPolygon(w / 2, h / 2, outerR, innerR, 5, -90)
211 return filledPolygon(pts, ctx)
212 }
213 },
214 line: {
215 type: 'line',
216 defaultWidth: 240,
217 defaultHeight: 8,
218 defaultFill: 'none',
219 defaultStroke: DEFAULT_STROKE,
220 strokeWidth: 4,
221 renderInner: (w, h, { stroke, strokeWidth }) => {
222 const y = h / 2
223 return `<line x1="0" y1="${y}" x2="${w}" y2="${y}" fill="none" stroke="${stroke}" stroke-width="${strokeWidth}" stroke-linecap="round" vector-effect="non-scaling-stroke" />`
224 }
225 },
226 'arrow-right': {
227 type: 'arrow-right',
228 defaultWidth: 240,
229 defaultHeight: 80,
230 defaultFill: DEFAULT_STROKE,
231 defaultStroke: DEFAULT_STROKE,
232 strokeWidth: 2,
233 renderInner: (w, h, ctx) => {
234 // Solid block arrow: rectangular shaft + triangular head, filled.
235 const pad = ctx.strokeWidth / 2 + 0.5
236 const shaftH = h * 0.5
237 const shaftTop = (h - shaftH) / 2
238 const shaftBottom = shaftTop + shaftH
239 const headW = h * 0.6
240 const bodyEnd = w - headW
241 const pts = [
242 `${pad},${round(shaftTop)}`,
243 `${round(bodyEnd)},${round(shaftTop)}`,
244 `${round(bodyEnd)},${pad}`,
245 `${w - pad},${h / 2}`,
246 `${round(bodyEnd)},${h - pad}`,
247 `${round(bodyEnd)},${round(shaftBottom)}`,
248 `${pad},${round(shaftBottom)}`
249 ].join(' ')
250 return filledPolygon(pts, ctx)
251 }
252 },
253 'chevron-right': {
254 type: 'chevron-right',
255 defaultWidth: 200,
256 defaultHeight: 120,
257 defaultFill: DEFAULT_FILL,
258 defaultStroke: DEFAULT_STROKE,
259 strokeWidth: 2,
260 renderInner: (w, h, ctx) => {
261 const pad = ctx.strokeWidth / 2 + 0.5
262 const notch = w * 0.35
263 const pts = [
264 `${pad},${pad}`,
265 `${round(w - notch)},${pad}`,
266 `${w - pad},${h / 2}`,
267 `${round(w - notch)},${h - pad}`,
268 `${pad},${h - pad}`,
269 `${round(pad + notch)},${h / 2}`
270 ].join(' ')
271 return filledPolygon(pts, ctx)
272 }
273 }
274 }
275
276 export const SHAPE_LIST: ShapeDefinition[] = Object.values(SHAPE_REGISTRY)
277
278 export function getShapeDefinition(type: string): ShapeDefinition | undefined {
279 return SHAPE_REGISTRY[type as InsertShapeType]
280 }
281
282 export function isRegisteredShapeType(type: string): boolean {
283 return Boolean(SHAPE_REGISTRY[type as InsertShapeType])
284 }
285
285 lines TYPESCRIPT