bsign-customization-full 0.0.1
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/.env +2 -0
- package/components.json +21 -0
- package/dist/colors/anthracite-gray.webp +0 -0
- package/dist/colors/anthracite-gray_50x50.png +0 -0
- package/dist/colors/dark-wenge.webp +0 -0
- package/dist/colors/dark-wenge_50x50.png +0 -0
- package/dist/colors/indian-rosewood.webp +0 -0
- package/dist/colors/indian-rosewood_50x50.png +0 -0
- package/dist/colors/natural-wood.webp +0 -0
- package/dist/colors/natural-wood_50x50.png +0 -0
- package/dist/colors/redwood.webp +0 -0
- package/dist/colors/redwood_50x50.png +0 -0
- package/dist/colors/walnut.webp +0 -0
- package/dist/colors/walnut_50x50.png +0 -0
- package/dist/html2canvas.esm-BJ_egzt0.js +4802 -0
- package/dist/index-Dw5Zc1iD.js +33162 -0
- package/dist/index.es-Co1KNpGS.js +6681 -0
- package/dist/logo.png +0 -0
- package/dist/purify.es-CKpD2xIC.js +552 -0
- package/dist/sign-constructor.es.js +4 -0
- package/dist/sign-constructor.iife.js +171 -0
- package/dist/size-guide.webp +0 -0
- package/dist/size.webp +0 -0
- package/dist/templates/assets/modern/rectangle-black.webp +0 -0
- package/dist/templates/assets/modern/rectangle-white.webp +0 -0
- package/dist/templates/assets/modern/square-black.webp +0 -0
- package/dist/templates/assets/modern/square-white.webp +0 -0
- package/dist/templates/assets/wave.webp +0 -0
- package/dist/templates/jure.webp +0 -0
- package/dist/templates/modern.webp +0 -0
- package/dist/templates/sherwood.webp +0 -0
- package/dist/templates/wave.webp +0 -0
- package/eslint.config.js +23 -0
- package/index.html +13 -0
- package/modern-debug.svg +39 -0
- package/package.json +62 -0
- package/public/colors/anthracite-gray.webp +0 -0
- package/public/colors/anthracite-gray_50x50.png +0 -0
- package/public/colors/dark-wenge.webp +0 -0
- package/public/colors/dark-wenge_50x50.png +0 -0
- package/public/colors/indian-rosewood.webp +0 -0
- package/public/colors/indian-rosewood_50x50.png +0 -0
- package/public/colors/natural-wood.webp +0 -0
- package/public/colors/natural-wood_50x50.png +0 -0
- package/public/colors/redwood.webp +0 -0
- package/public/colors/redwood_50x50.png +0 -0
- package/public/colors/walnut.webp +0 -0
- package/public/colors/walnut_50x50.png +0 -0
- package/public/logo.png +0 -0
- package/public/size-guide.webp +0 -0
- package/public/size.webp +0 -0
- package/public/templates/assets/modern/rectangle-black.webp +0 -0
- package/public/templates/assets/modern/rectangle-white.webp +0 -0
- package/public/templates/assets/modern/square-black.webp +0 -0
- package/public/templates/assets/modern/square-white.webp +0 -0
- package/public/templates/assets/wave.webp +0 -0
- package/public/templates/jure.webp +0 -0
- package/public/templates/modern.webp +0 -0
- package/public/templates/sherwood.webp +0 -0
- package/public/templates/wave.webp +0 -0
- package/src/App.css +43 -0
- package/src/AppDemo2.tsx +257 -0
- package/src/components/cart-panel.tsx +170 -0
- package/src/components/cart-preview.tsx +356 -0
- package/src/components/cart-view.tsx +113 -0
- package/src/components/constructure-menu.tsx +37 -0
- package/src/components/header.tsx +214 -0
- package/src/components/heading.tsx +28 -0
- package/src/components/icons.tsx +54 -0
- package/src/components/import-file-modal.tsx +252 -0
- package/src/components/layers/grid-view.tsx +29 -0
- package/src/components/layers/image-layer.tsx +128 -0
- package/src/components/layers/layer-forms/material-form.tsx +53 -0
- package/src/components/layers/layer-forms/size-form.tsx +69 -0
- package/src/components/layers/layer-forms/template-form.tsx +39 -0
- package/src/components/layers/layer-forms/text-form.tsx +477 -0
- package/src/components/layers/layers-container.tsx +259 -0
- package/src/components/layers/text-layer.tsx +128 -0
- package/src/components/movable-item.tsx +228 -0
- package/src/components/preview.tsx +258 -0
- package/src/components/resize-button.tsx +83 -0
- package/src/components/size-guide-modal.tsx +47 -0
- package/src/components/size-guide.tsx +98 -0
- package/src/components/ui/button-group.tsx +83 -0
- package/src/components/ui/button.tsx +60 -0
- package/src/components/ui/checkbox.tsx +30 -0
- package/src/components/ui/dialog.tsx +151 -0
- package/src/components/ui/input-group.tsx +168 -0
- package/src/components/ui/input.tsx +21 -0
- package/src/components/ui/label.tsx +22 -0
- package/src/components/ui/popover.tsx +54 -0
- package/src/components/ui/progress.tsx +28 -0
- package/src/components/ui/radio-group.tsx +43 -0
- package/src/components/ui/scroll-area.tsx +56 -0
- package/src/components/ui/select.tsx +191 -0
- package/src/components/ui/separator.tsx +25 -0
- package/src/components/ui/sheet.tsx +141 -0
- package/src/components/ui/slider.tsx +61 -0
- package/src/components/ui/spinner.tsx +15 -0
- package/src/components/ui/textarea.tsx +18 -0
- package/src/components/ui/toggle-group.tsx +73 -0
- package/src/components/ui/toggle.tsx +45 -0
- package/src/components/ui/tooltip.tsx +67 -0
- package/src/fonts/BEBASNEUE-REGULAR.TTF +0 -0
- package/src/fonts/Braille-Regular.ttf +0 -0
- package/src/fonts/GOTHICB.TTF +0 -0
- package/src/hooks/use-mobile.ts +23 -0
- package/src/hooks/use-resize-constraints.ts +62 -0
- package/src/index.css +238 -0
- package/src/index.tsx +141 -0
- package/src/lib/cart-proposal-pdf.ts +350 -0
- package/src/lib/config-font.tsx +109 -0
- package/src/lib/config.ts +730 -0
- package/src/lib/pricing.ts +61 -0
- package/src/lib/type-checks.ts +47 -0
- package/src/lib/utils.ts +146 -0
- package/src/lib/widget-context.tsx +9 -0
- package/src/main.tsx +11 -0
- package/src/store/cart-store.ts +78 -0
- package/src/store/layers-store.ts +337 -0
- package/src/vite-env.d.ts +1 -0
- package/test/preview.html +37 -0
- package/tsconfig.app.json +33 -0
- package/tsconfig.json +13 -0
- package/tsconfig.node.json +25 -0
- package/vite.config.ts +38 -0
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
import { jsPDF } from "jspdf"
|
|
2
|
+
|
|
3
|
+
import { getTemplateById, getTemplateMaterial } from "./config"
|
|
4
|
+
import { CURRENCY_SYMBOL, formatPrice, getCartSummary, getUnitPrice } from "./pricing"
|
|
5
|
+
import type { TCartItem } from "../store/cart-store"
|
|
6
|
+
import type { LayerTextProps } from "../components/layers/text-layer"
|
|
7
|
+
import type { TLayer } from "../components/preview"
|
|
8
|
+
|
|
9
|
+
const PROPOSAL_LOGO_SRC = "https://bsign-store.com/cdn/shop/files/Logo_black_140x.png"
|
|
10
|
+
const PAGE_MARGIN_MM = 14
|
|
11
|
+
const HEADER_HEIGHT_MM = 22
|
|
12
|
+
const IMAGE_BOX_SIZE_MM = 52
|
|
13
|
+
const CARD_PADDING_MM = 4
|
|
14
|
+
const CARD_GAP_MM = 6
|
|
15
|
+
const CARD_TEXT_LINE_HEIGHT_MM = 4.8
|
|
16
|
+
const PAGE_FOOTER_SAFE_SPACE_MM = 12
|
|
17
|
+
|
|
18
|
+
const normalizeQty = (qty: number): number => {
|
|
19
|
+
return Number.isFinite(qty) && qty > 0 ? Math.floor(qty) : 1
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const formatSizeNumber = (value: number): string => {
|
|
23
|
+
return Number.isInteger(value) ? String(value) : value.toFixed(1).replace(/\.0$/, "")
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const formatLinePrice = (value: number): string => {
|
|
27
|
+
return `${CURRENCY_SYMBOL}${Number.isInteger(value) ? value.toFixed(0) : value.toFixed(2)}`
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const getPersonalizationLines = (item: TCartItem): string[] => {
|
|
31
|
+
const textLayers = item.sign.filter((layer) => layer.type === "text" && layer.subtype !== "braille") as TLayer<LayerTextProps>[]
|
|
32
|
+
const lines = textLayers
|
|
33
|
+
.map((layer) => layer.props.text.trim())
|
|
34
|
+
.filter(Boolean)
|
|
35
|
+
return lines.length > 0 ? lines : ["-"]
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const hasAdaFont = (item: TCartItem): boolean => {
|
|
39
|
+
const textLayers = item.sign.filter((layer) => layer.type === "text" && layer.subtype !== "braille") as TLayer<LayerTextProps>[]
|
|
40
|
+
return textLayers.some((layer) => Boolean(layer.props.braille))
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const getColorLabel = (item: TCartItem): string => {
|
|
44
|
+
const template = getTemplateById(item.options.selectedTemplateId)
|
|
45
|
+
const material = getTemplateMaterial({
|
|
46
|
+
template,
|
|
47
|
+
materialId: item.options.selectedMaterialId,
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
return material.label
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const blobToDataUrl = (blob: Blob): Promise<string> => {
|
|
54
|
+
return new Promise((resolve, reject) => {
|
|
55
|
+
const reader = new FileReader()
|
|
56
|
+
reader.onerror = () => reject(reader.error ?? new Error("Unable to read image blob"))
|
|
57
|
+
reader.onload = () => {
|
|
58
|
+
if (typeof reader.result === "string") {
|
|
59
|
+
resolve(reader.result)
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
reject(new Error("Unable to convert image blob to data URL"))
|
|
63
|
+
}
|
|
64
|
+
reader.readAsDataURL(blob)
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const resolveImageDataUrl = async (src: string): Promise<string | null> => {
|
|
69
|
+
if (!src) return null
|
|
70
|
+
if (src.startsWith("data:image/")) return src
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
const response = await fetch(src)
|
|
74
|
+
if (!response.ok) return null
|
|
75
|
+
const blob = await response.blob()
|
|
76
|
+
return blobToDataUrl(blob)
|
|
77
|
+
} catch (error) {
|
|
78
|
+
console.error(error)
|
|
79
|
+
return null
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const getImageFormat = (dataUrl: string): "PNG" | "JPEG" => {
|
|
84
|
+
const mimeType = dataUrl.match(/^data:image\/([a-zA-Z0-9.+-]+);/)?.[1]?.toLowerCase()
|
|
85
|
+
if (mimeType === "jpg" || mimeType === "jpeg") {
|
|
86
|
+
return "JPEG"
|
|
87
|
+
}
|
|
88
|
+
return "PNG"
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const drawImageContained = ({
|
|
92
|
+
pdf,
|
|
93
|
+
dataUrl,
|
|
94
|
+
x,
|
|
95
|
+
y,
|
|
96
|
+
width,
|
|
97
|
+
height,
|
|
98
|
+
}: {
|
|
99
|
+
pdf: jsPDF
|
|
100
|
+
dataUrl: string
|
|
101
|
+
x: number
|
|
102
|
+
y: number
|
|
103
|
+
width: number
|
|
104
|
+
height: number
|
|
105
|
+
}): boolean => {
|
|
106
|
+
try {
|
|
107
|
+
const imageProps = pdf.getImageProperties(dataUrl)
|
|
108
|
+
if (!Number.isFinite(imageProps.width) || !Number.isFinite(imageProps.height) || imageProps.width <= 0 || imageProps.height <= 0) {
|
|
109
|
+
return false
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const scale = Math.min(width / imageProps.width, height / imageProps.height)
|
|
113
|
+
const renderWidth = imageProps.width * scale
|
|
114
|
+
const renderHeight = imageProps.height * scale
|
|
115
|
+
const renderX = x + (width - renderWidth) / 2
|
|
116
|
+
const renderY = y + (height - renderHeight) / 2
|
|
117
|
+
|
|
118
|
+
pdf.addImage(dataUrl, getImageFormat(dataUrl), renderX, renderY, renderWidth, renderHeight, undefined, "FAST")
|
|
119
|
+
return true
|
|
120
|
+
} catch (error) {
|
|
121
|
+
console.error(error)
|
|
122
|
+
return false
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const drawPageHeader = ({
|
|
127
|
+
pdf,
|
|
128
|
+
logoDataUrl,
|
|
129
|
+
proposalDate,
|
|
130
|
+
proposalId,
|
|
131
|
+
}: {
|
|
132
|
+
pdf: jsPDF
|
|
133
|
+
logoDataUrl: string | null
|
|
134
|
+
proposalDate: string
|
|
135
|
+
proposalId: string
|
|
136
|
+
}): number => {
|
|
137
|
+
const pageWidth = pdf.internal.pageSize.getWidth()
|
|
138
|
+
const topY = PAGE_MARGIN_MM
|
|
139
|
+
|
|
140
|
+
if (logoDataUrl) {
|
|
141
|
+
drawImageContained({
|
|
142
|
+
pdf,
|
|
143
|
+
dataUrl: logoDataUrl,
|
|
144
|
+
x: PAGE_MARGIN_MM,
|
|
145
|
+
y: topY - 1,
|
|
146
|
+
width: 30,
|
|
147
|
+
height: 12,
|
|
148
|
+
})
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const titleX = logoDataUrl ? PAGE_MARGIN_MM + 35 : PAGE_MARGIN_MM
|
|
152
|
+
|
|
153
|
+
pdf.setFont("helvetica", "bold")
|
|
154
|
+
pdf.setFontSize(18)
|
|
155
|
+
pdf.setTextColor(28, 29, 29)
|
|
156
|
+
pdf.text("Bsign", titleX, topY + 7)
|
|
157
|
+
|
|
158
|
+
pdf.setFont("helvetica", "normal")
|
|
159
|
+
pdf.setFontSize(9.5)
|
|
160
|
+
pdf.setTextColor(102, 102, 102)
|
|
161
|
+
pdf.text(`Date: ${proposalDate}`, pageWidth - PAGE_MARGIN_MM, topY + 4, { align: "right" })
|
|
162
|
+
pdf.text(`#${proposalId}`, pageWidth - PAGE_MARGIN_MM, topY + 9, { align: "right" })
|
|
163
|
+
|
|
164
|
+
const separatorY = topY + HEADER_HEIGHT_MM - 2
|
|
165
|
+
pdf.setDrawColor(218, 218, 218)
|
|
166
|
+
pdf.line(PAGE_MARGIN_MM, separatorY, pageWidth - PAGE_MARGIN_MM, separatorY)
|
|
167
|
+
|
|
168
|
+
return topY + HEADER_HEIGHT_MM + 2
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const drawSummaryBlock = ({
|
|
172
|
+
pdf,
|
|
173
|
+
y,
|
|
174
|
+
signsCount,
|
|
175
|
+
totalQty,
|
|
176
|
+
totalAmount,
|
|
177
|
+
}: {
|
|
178
|
+
pdf: jsPDF
|
|
179
|
+
y: number
|
|
180
|
+
signsCount: number
|
|
181
|
+
totalQty: number
|
|
182
|
+
totalAmount: number
|
|
183
|
+
}): number => {
|
|
184
|
+
const pageWidth = pdf.internal.pageSize.getWidth()
|
|
185
|
+
const contentWidth = pageWidth - PAGE_MARGIN_MM * 2
|
|
186
|
+
|
|
187
|
+
pdf.setFillColor(247, 247, 247)
|
|
188
|
+
pdf.setDrawColor(222, 222, 222)
|
|
189
|
+
pdf.roundedRect(PAGE_MARGIN_MM, y, contentWidth, 22, 3, 3, "FD")
|
|
190
|
+
|
|
191
|
+
pdf.setFont("helvetica", "bold")
|
|
192
|
+
pdf.setFontSize(10)
|
|
193
|
+
pdf.setTextColor(28, 29, 29)
|
|
194
|
+
pdf.text("Summary", PAGE_MARGIN_MM + 4, y + 6)
|
|
195
|
+
|
|
196
|
+
pdf.setFont("helvetica", "normal")
|
|
197
|
+
pdf.setFontSize(10)
|
|
198
|
+
pdf.setTextColor(56, 56, 56)
|
|
199
|
+
pdf.text(`Signs in cart: ${signsCount}`, PAGE_MARGIN_MM + 4, y + 12)
|
|
200
|
+
pdf.text(`Total quantity: ${totalQty}`, PAGE_MARGIN_MM + 4, y + 17)
|
|
201
|
+
|
|
202
|
+
pdf.setFont("helvetica", "bold")
|
|
203
|
+
pdf.setFontSize(11)
|
|
204
|
+
pdf.setTextColor(28, 29, 29)
|
|
205
|
+
pdf.text(`Total amount: ${formatPrice(totalAmount)}`, pageWidth - PAGE_MARGIN_MM - 4, y + 17, { align: "right" })
|
|
206
|
+
|
|
207
|
+
return y + 28
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export const downloadCartProposalPdf = async (cart: TCartItem[]): Promise<void> => {
|
|
211
|
+
if (!cart.length) return
|
|
212
|
+
|
|
213
|
+
const [logoDataUrl, ...cartImageDataUrls] = await Promise.all([
|
|
214
|
+
resolveImageDataUrl(PROPOSAL_LOGO_SRC),
|
|
215
|
+
...cart.map((item) => resolveImageDataUrl(item.img)),
|
|
216
|
+
])
|
|
217
|
+
|
|
218
|
+
const pdf = new jsPDF({
|
|
219
|
+
orientation: "portrait",
|
|
220
|
+
unit: "mm",
|
|
221
|
+
format: "a4",
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
const pageHeight = pdf.internal.pageSize.getHeight()
|
|
225
|
+
const contentWidth = pdf.internal.pageSize.getWidth() - PAGE_MARGIN_MM * 2
|
|
226
|
+
|
|
227
|
+
const now = new Date()
|
|
228
|
+
const proposalDate = new Intl.DateTimeFormat("en-US", {
|
|
229
|
+
dateStyle: "long",
|
|
230
|
+
}).format(now)
|
|
231
|
+
const proposalId = now.toISOString().replace(/[-:TZ.]/g, "").slice(0, 14)
|
|
232
|
+
|
|
233
|
+
let y = drawPageHeader({
|
|
234
|
+
pdf,
|
|
235
|
+
logoDataUrl,
|
|
236
|
+
proposalDate,
|
|
237
|
+
proposalId,
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
const { totalQty, totalAmount } = getCartSummary(cart)
|
|
241
|
+
y = drawSummaryBlock({
|
|
242
|
+
pdf,
|
|
243
|
+
y,
|
|
244
|
+
signsCount: cart.length,
|
|
245
|
+
totalQty,
|
|
246
|
+
totalAmount,
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
for (const [index, item] of cart.entries()) {
|
|
250
|
+
const qty = normalizeQty(item.qty)
|
|
251
|
+
const lineTotal = getUnitPrice(item.options.selectedSize, qty) * qty
|
|
252
|
+
const colorLabel = getColorLabel(item)
|
|
253
|
+
const templateLabel = getTemplateById(item.options.selectedTemplateId).name
|
|
254
|
+
const personalizationText = getPersonalizationLines(item).join(" | ")
|
|
255
|
+
const adaText = hasAdaFont(item) ? "Yes" : "No"
|
|
256
|
+
const width = formatSizeNumber(item.options.selectedSize.inchs.w)
|
|
257
|
+
const height = formatSizeNumber(item.options.selectedSize.inchs.h)
|
|
258
|
+
|
|
259
|
+
const textX = PAGE_MARGIN_MM + CARD_PADDING_MM + IMAGE_BOX_SIZE_MM + CARD_GAP_MM
|
|
260
|
+
const textWidth = contentWidth - IMAGE_BOX_SIZE_MM - CARD_PADDING_MM * 2 - CARD_GAP_MM
|
|
261
|
+
const detailLines = [
|
|
262
|
+
`Size & Color: ${width}" x ${height}" - ${colorLabel} - ${templateLabel}`,
|
|
263
|
+
`Personalization: ${personalizationText}`,
|
|
264
|
+
`Braille Font: ${adaText}`,
|
|
265
|
+
`Quantity: ${qty}`,
|
|
266
|
+
`Line total: ${formatLinePrice(lineTotal)}`,
|
|
267
|
+
].flatMap((line) => pdf.splitTextToSize(line, textWidth) as string[])
|
|
268
|
+
|
|
269
|
+
const textBlockHeight = 8 + detailLines.length * CARD_TEXT_LINE_HEIGHT_MM
|
|
270
|
+
const cardHeight = Math.max(IMAGE_BOX_SIZE_MM + CARD_PADDING_MM * 2, textBlockHeight + CARD_PADDING_MM * 2)
|
|
271
|
+
|
|
272
|
+
if (y + cardHeight > pageHeight - PAGE_FOOTER_SAFE_SPACE_MM) {
|
|
273
|
+
pdf.addPage()
|
|
274
|
+
y = drawPageHeader({
|
|
275
|
+
pdf,
|
|
276
|
+
logoDataUrl,
|
|
277
|
+
proposalDate,
|
|
278
|
+
proposalId,
|
|
279
|
+
})
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
pdf.setDrawColor(214, 214, 214)
|
|
283
|
+
pdf.setFillColor(255, 255, 255)
|
|
284
|
+
pdf.roundedRect(PAGE_MARGIN_MM, y, contentWidth, cardHeight, 2.5, 2.5, "FD")
|
|
285
|
+
|
|
286
|
+
const imageX = PAGE_MARGIN_MM + CARD_PADDING_MM
|
|
287
|
+
const imageY = y + (cardHeight - IMAGE_BOX_SIZE_MM) / 2
|
|
288
|
+
pdf.setFillColor(244, 244, 244)
|
|
289
|
+
pdf.rect(imageX, imageY, IMAGE_BOX_SIZE_MM, IMAGE_BOX_SIZE_MM, "F")
|
|
290
|
+
|
|
291
|
+
const imageDataUrl = cartImageDataUrls[index]
|
|
292
|
+
const imageRendered = imageDataUrl
|
|
293
|
+
? drawImageContained({
|
|
294
|
+
pdf,
|
|
295
|
+
dataUrl: imageDataUrl,
|
|
296
|
+
x: imageX,
|
|
297
|
+
y: imageY,
|
|
298
|
+
width: IMAGE_BOX_SIZE_MM,
|
|
299
|
+
height: IMAGE_BOX_SIZE_MM,
|
|
300
|
+
})
|
|
301
|
+
: false
|
|
302
|
+
|
|
303
|
+
if (!imageRendered) {
|
|
304
|
+
pdf.setFont("helvetica", "normal")
|
|
305
|
+
pdf.setFontSize(8.5)
|
|
306
|
+
pdf.setTextColor(140, 140, 140)
|
|
307
|
+
pdf.text("Preview unavailable", imageX + IMAGE_BOX_SIZE_MM / 2, imageY + IMAGE_BOX_SIZE_MM / 2, { align: "center" })
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
let lineY = y + CARD_PADDING_MM + 5
|
|
311
|
+
pdf.setFont("helvetica", "bold")
|
|
312
|
+
pdf.setFontSize(12)
|
|
313
|
+
pdf.setTextColor(28, 29, 29)
|
|
314
|
+
pdf.text(`Sign ${index + 1}`, textX, lineY)
|
|
315
|
+
|
|
316
|
+
lineY += 5.5
|
|
317
|
+
pdf.setFont("helvetica", "normal")
|
|
318
|
+
pdf.setFontSize(9.8)
|
|
319
|
+
pdf.setTextColor(52, 52, 52)
|
|
320
|
+
for (const line of detailLines) {
|
|
321
|
+
pdf.text(line, textX, lineY)
|
|
322
|
+
lineY += CARD_TEXT_LINE_HEIGHT_MM
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
y += cardHeight + 6
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (y + 16 < pageHeight - PAGE_FOOTER_SAFE_SPACE_MM) {
|
|
329
|
+
pdf.setDrawColor(224, 224, 224)
|
|
330
|
+
pdf.line(PAGE_MARGIN_MM, y + 2, PAGE_MARGIN_MM + contentWidth, y + 2)
|
|
331
|
+
pdf.setFont("helvetica", "italic")
|
|
332
|
+
pdf.setFontSize(9)
|
|
333
|
+
pdf.setTextColor(120, 120, 120)
|
|
334
|
+
pdf.text("Thank you for your request. We are ready to proceed after your confirmation.", PAGE_MARGIN_MM, y + 8)
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const totalPages = pdf.getNumberOfPages()
|
|
338
|
+
for (let pageNumber = 1; pageNumber <= totalPages; pageNumber += 1) {
|
|
339
|
+
pdf.setPage(pageNumber)
|
|
340
|
+
pdf.setFont("helvetica", "normal")
|
|
341
|
+
pdf.setFontSize(8.5)
|
|
342
|
+
pdf.setTextColor(130, 130, 130)
|
|
343
|
+
pdf.text(`Page ${pageNumber} of ${totalPages}`, pdf.internal.pageSize.getWidth() - PAGE_MARGIN_MM, pageHeight - 5, {
|
|
344
|
+
align: "right",
|
|
345
|
+
})
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const filenameDate = now.getTime();
|
|
349
|
+
pdf.save(`bsign-cart-${filenameDate}.pdf`);
|
|
350
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
export const BASE_FONT_SIZE = 100 as const;
|
|
2
|
+
|
|
3
|
+
export const BASE_TEXT_SCALE_CHANGE_VALUE = 0.05 as const;
|
|
4
|
+
export const MAX_SCALE = 1.8 as const;
|
|
5
|
+
export const MIN_SCALE = 0.5 as const;
|
|
6
|
+
export const MODERN_MIN_FONT_PT = 64 as const;
|
|
7
|
+
|
|
8
|
+
export const AUTO_ADD_TEXT_SCALE_STEP = 0.30 as const;
|
|
9
|
+
export const AUTO_ADD_TEXT_LINE_DENSITY_STEP = 0.15 as const;
|
|
10
|
+
export const AUTO_ADD_TEXT_MIN_SCALE_RATIO = 0.55 as const;
|
|
11
|
+
|
|
12
|
+
export const MAX_LINE_LENGTH = 12 as const;
|
|
13
|
+
export const MIN_LINE_LENGTH = 1 as const;
|
|
14
|
+
export const MAX_LINE_LENGTH_HARD_CAP = 32 as const;
|
|
15
|
+
export const BASE_FONT_SCALE = 1 as const;
|
|
16
|
+
const MAX_LINE_LENGTH_CURVE_POWER = 1 as const;
|
|
17
|
+
const BASE_SIGN_AREA_INCH = 3.5 * 3.5;
|
|
18
|
+
const BASE_RECOMMENDED_PT = 90 as const;
|
|
19
|
+
const BASE_RECOMMENDED_LINE_LENGTH = 8 as const;
|
|
20
|
+
const TEMPLATE_BASE_RECOMMENDED_LINE_LENGTH = {
|
|
21
|
+
modern: 4,
|
|
22
|
+
wave: BASE_RECOMMENDED_LINE_LENGTH,
|
|
23
|
+
} as const;
|
|
24
|
+
|
|
25
|
+
type TSignInchs = {
|
|
26
|
+
w: number;
|
|
27
|
+
h: number;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const getSignSizeFactor = (signInchs?: TSignInchs) => {
|
|
31
|
+
if (!signInchs) return 1;
|
|
32
|
+
|
|
33
|
+
const safeWidth = Math.max(0.1, signInchs.w);
|
|
34
|
+
const safeHeight = Math.max(0.1, signInchs.h);
|
|
35
|
+
const areaRatio = (safeWidth * safeHeight) / BASE_SIGN_AREA_INCH;
|
|
36
|
+
|
|
37
|
+
// Square root keeps growth smoother for bigger signs.
|
|
38
|
+
return Math.sqrt(areaRatio);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const getTemplateBaseRecommendedLineLength = (templateId?: string) => {
|
|
42
|
+
if (!templateId) return BASE_RECOMMENDED_LINE_LENGTH;
|
|
43
|
+
|
|
44
|
+
if (templateId in TEMPLATE_BASE_RECOMMENDED_LINE_LENGTH) {
|
|
45
|
+
return TEMPLATE_BASE_RECOMMENDED_LINE_LENGTH[
|
|
46
|
+
templateId as keyof typeof TEMPLATE_BASE_RECOMMENDED_LINE_LENGTH
|
|
47
|
+
];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return BASE_RECOMMENDED_LINE_LENGTH;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export const getMaxLineLengthByPt = (
|
|
54
|
+
pt: number,
|
|
55
|
+
signInchs?: TSignInchs,
|
|
56
|
+
templateId?: string
|
|
57
|
+
) => {
|
|
58
|
+
const safePt = Math.max(1, pt);
|
|
59
|
+
const sizeFactor = getSignSizeFactor(signInchs);
|
|
60
|
+
const baseRecommendedLineLength = getTemplateBaseRecommendedLineLength(templateId);
|
|
61
|
+
const estimatedLength = baseRecommendedLineLength
|
|
62
|
+
* sizeFactor
|
|
63
|
+
* Math.pow(BASE_RECOMMENDED_PT / safePt, MAX_LINE_LENGTH_CURVE_POWER);
|
|
64
|
+
|
|
65
|
+
return Math.max(
|
|
66
|
+
MIN_LINE_LENGTH,
|
|
67
|
+
Math.min(MAX_LINE_LENGTH_HARD_CAP, Math.round(estimatedLength))
|
|
68
|
+
);
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export const getMinScaleByTemplate = ({
|
|
72
|
+
templateId,
|
|
73
|
+
sizeLimitScale = 1,
|
|
74
|
+
}: {
|
|
75
|
+
templateId?: string;
|
|
76
|
+
sizeLimitScale?: number;
|
|
77
|
+
}) => {
|
|
78
|
+
if (templateId !== "modern") return MIN_SCALE;
|
|
79
|
+
|
|
80
|
+
const normalizedSizeLimitScale =
|
|
81
|
+
Number.isFinite(sizeLimitScale) && sizeLimitScale > 0
|
|
82
|
+
? sizeLimitScale
|
|
83
|
+
: 1;
|
|
84
|
+
|
|
85
|
+
const modernMinScale = MODERN_MIN_FONT_PT / (BASE_FONT_SIZE * normalizedSizeLimitScale);
|
|
86
|
+
|
|
87
|
+
return Math.min(MAX_SCALE, Math.max(MIN_SCALE, modernMinScale));
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export const FONT_RANGE = (() => {
|
|
91
|
+
let range: Record<number, number> = {};
|
|
92
|
+
|
|
93
|
+
let newFontScale = BASE_FONT_SCALE;
|
|
94
|
+
|
|
95
|
+
for (let length = 5; length <= MAX_LINE_LENGTH; length += 1) {
|
|
96
|
+
range[length] = newFontScale;
|
|
97
|
+
newFontScale -= 0.1;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return range;
|
|
101
|
+
})();
|
|
102
|
+
|
|
103
|
+
// export const LINES_SCALE = [
|
|
104
|
+
// { lines: 2, length: 5, scale: 0.75 },
|
|
105
|
+
// { lines: 2, length: 6, scale: 0.8 },
|
|
106
|
+
// { lines: 2, length: 8, scale: 0.8 },
|
|
107
|
+
// { lines: 3, length: 8, scale: 0.6 },
|
|
108
|
+
// { lines: 4, length: 8, scale: 0.7 },
|
|
109
|
+
// ] as const;
|