@pooder/kit 4.1.0 → 4.3.0
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/CHANGELOG.md +12 -0
- package/dist/index.d.mts +62 -10
- package/dist/index.d.ts +62 -10
- package/dist/index.js +756 -348
- package/dist/index.mjs +753 -347
- package/package.json +3 -2
- package/src/CanvasService.ts +96 -89
- package/src/ViewportSystem.ts +92 -0
- package/src/background.ts +230 -230
- package/src/constraints.ts +191 -27
- package/src/coordinate.ts +106 -106
- package/src/dieline.ts +955 -871
- package/src/feature.ts +282 -195
- package/src/featureComplete.ts +46 -0
- package/src/film.ts +194 -194
- package/src/geometry.ts +161 -30
- package/src/image.ts +512 -512
- package/src/index.ts +10 -9
- package/src/mirror.ts +128 -128
- package/src/ruler.ts +508 -500
- package/src/tracer.ts +570 -570
- package/src/units.ts +27 -0
- package/src/white-ink.ts +373 -373
- package/tsconfig.test.json +15 -0
package/src/constraints.ts
CHANGED
|
@@ -1,17 +1,35 @@
|
|
|
1
|
-
import { DielineFeature } from "./geometry";
|
|
1
|
+
import { DielineFeature, getNearestPointOnDieline, getLowestPointOnDieline } from "./geometry";
|
|
2
2
|
|
|
3
3
|
export interface ConstraintContext {
|
|
4
4
|
dielineWidth: number;
|
|
5
5
|
dielineHeight: number;
|
|
6
|
+
// Context may need access to geometry functions or the geometry itself
|
|
7
|
+
// For now, getNearestPointOnDieline creates its own paper scope, but ideally we pass a simplified geometry representation
|
|
8
|
+
geometry?: any;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface ConstraintFeature extends DielineFeature {
|
|
12
|
+
constraints?: Array<{
|
|
13
|
+
type: string;
|
|
14
|
+
params?: any;
|
|
15
|
+
validateOnly?: boolean;
|
|
16
|
+
}>;
|
|
6
17
|
}
|
|
7
18
|
|
|
8
19
|
export type ConstraintHandler = (
|
|
9
20
|
x: number,
|
|
10
21
|
y: number,
|
|
11
|
-
feature:
|
|
12
|
-
context: ConstraintContext
|
|
22
|
+
feature: ConstraintFeature,
|
|
23
|
+
context: ConstraintContext,
|
|
24
|
+
params?: any
|
|
13
25
|
) => { x: number; y: number };
|
|
14
26
|
|
|
27
|
+
export interface ConstraintConfig {
|
|
28
|
+
type: string;
|
|
29
|
+
params?: any;
|
|
30
|
+
validateOnly?: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
15
33
|
export class ConstraintRegistry {
|
|
16
34
|
private static handlers = new Map<string, ConstraintHandler>();
|
|
17
35
|
|
|
@@ -22,40 +40,118 @@ export class ConstraintRegistry {
|
|
|
22
40
|
static apply(
|
|
23
41
|
x: number,
|
|
24
42
|
y: number,
|
|
25
|
-
feature:
|
|
26
|
-
context: ConstraintContext
|
|
43
|
+
feature: ConstraintFeature,
|
|
44
|
+
context: ConstraintContext,
|
|
45
|
+
constraints?: ConstraintConfig[] // Optional override, defaults to feature.constraints
|
|
27
46
|
): { x: number; y: number } {
|
|
28
|
-
|
|
47
|
+
const list = constraints || feature.constraints;
|
|
48
|
+
if (!list || list.length === 0) {
|
|
29
49
|
return { x, y };
|
|
30
50
|
}
|
|
31
51
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
52
|
+
let currentX = x;
|
|
53
|
+
let currentY = y;
|
|
54
|
+
|
|
55
|
+
for (const constraint of list) {
|
|
56
|
+
const handler = this.handlers.get(constraint.type);
|
|
57
|
+
if (handler) {
|
|
58
|
+
const result = handler(currentX, currentY, feature, context, constraint.params || {});
|
|
59
|
+
currentX = result.x;
|
|
60
|
+
currentY = result.y;
|
|
61
|
+
}
|
|
35
62
|
}
|
|
36
63
|
|
|
37
|
-
return { x, y };
|
|
64
|
+
return { x: currentX, y: currentY };
|
|
38
65
|
}
|
|
39
66
|
}
|
|
40
67
|
|
|
41
68
|
// --- Built-in Strategies ---
|
|
42
69
|
|
|
43
70
|
/**
|
|
44
|
-
*
|
|
45
|
-
* Snaps the feature to the nearest
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
71
|
+
* Path Constraint Strategy (formerly placement='edge')
|
|
72
|
+
* Snaps the feature to the nearest point on the Dieline Path.
|
|
73
|
+
*/
|
|
74
|
+
const pathConstraint: ConstraintHandler = (x, y, feature, context, params) => {
|
|
75
|
+
// We need to denormalize, find nearest, then normalize back
|
|
76
|
+
// This is expensive but accurate.
|
|
77
|
+
const { dielineWidth, dielineHeight, geometry } = context;
|
|
78
|
+
if (!geometry) return { x, y }; // Cannot snap without geometry
|
|
79
|
+
|
|
80
|
+
// Geometry is centered at (cx, cy)
|
|
81
|
+
// x, y are normalized (0-1) relative to bounding box
|
|
82
|
+
const minX = geometry.x - geometry.width / 2;
|
|
83
|
+
const minY = geometry.y - geometry.height / 2;
|
|
84
|
+
|
|
85
|
+
const absX = minX + x * geometry.width;
|
|
86
|
+
const absY = minY + y * geometry.height;
|
|
87
|
+
|
|
88
|
+
// Use geometry helper
|
|
89
|
+
// Note: getNearestPointOnDieline creates a fresh paper scope each time.
|
|
90
|
+
// Optimization: geometry object passed in context could be a reusable paper path?
|
|
91
|
+
// For now, keep it simple as per existing logic.
|
|
92
|
+
const nearest = getNearestPointOnDieline(
|
|
93
|
+
{ x: absX, y: absY },
|
|
94
|
+
geometry
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
let finalX = nearest.x;
|
|
98
|
+
let finalY = nearest.y;
|
|
99
|
+
|
|
100
|
+
// Only allow vertical offset if explicit offset limits are provided
|
|
101
|
+
// Otherwise, we snap strictly to the path (offset = 0)
|
|
102
|
+
const hasOffsetParams = params.minOffset !== undefined || params.maxOffset !== undefined;
|
|
103
|
+
|
|
104
|
+
if (hasOffsetParams && nearest.normal) {
|
|
105
|
+
// Project the cursor vector onto the normal vector
|
|
106
|
+
// This ensures the feature stays on the "normal line" of the nearest path point
|
|
107
|
+
const dx = absX - nearest.x;
|
|
108
|
+
const dy = absY - nearest.y;
|
|
109
|
+
|
|
110
|
+
const nx = nearest.normal.x;
|
|
111
|
+
const ny = nearest.normal.y;
|
|
112
|
+
|
|
113
|
+
// Dot product to get scalar projection
|
|
114
|
+
const dist = dx * nx + dy * ny;
|
|
115
|
+
|
|
116
|
+
// Limit the offset
|
|
117
|
+
// geometry.width is in pixels, dielineWidth is in physical units (e.g. mm)
|
|
118
|
+
// We assume dielineWidth corresponds to geometry.width
|
|
119
|
+
const scale = dielineWidth > 0 ? geometry.width / dielineWidth : 1;
|
|
120
|
+
|
|
121
|
+
// If one is provided but the other is not, default the other to 0.
|
|
122
|
+
// If neither is provided (shouldn't happen due to hasOffsetParams check), default to 0.
|
|
123
|
+
const rawMin = params.minOffset !== undefined ? params.minOffset : 0;
|
|
124
|
+
const rawMax = params.maxOffset !== undefined ? params.maxOffset : 0;
|
|
125
|
+
|
|
126
|
+
// However, if we want to allow one-sided infinity, user must explicitly provide Infinity?
|
|
127
|
+
// Wait, user requirement: "If only one is passed, the other defaults to 0."
|
|
128
|
+
// This implies:
|
|
129
|
+
// { minOffset: -5 } -> maxOffset = 0 (range: -5 to 0)
|
|
130
|
+
// { maxOffset: 5 } -> minOffset = 0 (range: 0 to 5)
|
|
131
|
+
// { minOffset: -5, maxOffset: 5 } -> (range: -5 to 5)
|
|
132
|
+
|
|
133
|
+
const minOffset = rawMin * scale;
|
|
134
|
+
const maxOffset = rawMax * scale;
|
|
135
|
+
|
|
136
|
+
const clampedDist = Math.max(minOffset, Math.min(dist, maxOffset));
|
|
137
|
+
|
|
138
|
+
finalX = nearest.x + nx * clampedDist;
|
|
139
|
+
finalY = nearest.y + ny * clampedDist;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Re-normalize
|
|
143
|
+
const nx = geometry.width > 0 ? (finalX - minX) / geometry.width : 0.5;
|
|
144
|
+
const ny = geometry.height > 0 ? (finalY - minY) / geometry.height : 0.5;
|
|
145
|
+
|
|
146
|
+
return { x: nx, y: ny };
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Edge Constraint Strategy (Box Edge)
|
|
151
|
+
* Snaps the feature to the nearest allowed edge of the BOUNDING BOX.
|
|
55
152
|
*/
|
|
56
|
-
const edgeConstraint: ConstraintHandler = (x, y, feature, context) => {
|
|
153
|
+
const edgeConstraint: ConstraintHandler = (x, y, feature, context, params) => {
|
|
57
154
|
const { dielineWidth, dielineHeight } = context;
|
|
58
|
-
const params = feature.constraints?.params || {};
|
|
59
155
|
const allowedEdges = params.allowedEdges || [
|
|
60
156
|
"top",
|
|
61
157
|
"bottom",
|
|
@@ -130,12 +226,9 @@ const edgeConstraint: ConstraintHandler = (x, y, feature, context) => {
|
|
|
130
226
|
/**
|
|
131
227
|
* Internal Constraint Strategy
|
|
132
228
|
* Keeps the feature strictly inside the dieline bounds with optional margin.
|
|
133
|
-
* Params:
|
|
134
|
-
* - margin: number (default: 0) - physical margin
|
|
135
229
|
*/
|
|
136
|
-
const internalConstraint: ConstraintHandler = (x, y, feature, context) => {
|
|
230
|
+
const internalConstraint: ConstraintHandler = (x, y, feature, context, params) => {
|
|
137
231
|
const { dielineWidth, dielineHeight } = context;
|
|
138
|
-
const params = feature.constraints?.params || {};
|
|
139
232
|
const margin = params.margin || 0;
|
|
140
233
|
const fw = feature.width || 0;
|
|
141
234
|
const fh = feature.height || 0;
|
|
@@ -153,6 +246,77 @@ const internalConstraint: ConstraintHandler = (x, y, feature, context) => {
|
|
|
153
246
|
return { x: clampedX, y: clampedY };
|
|
154
247
|
};
|
|
155
248
|
|
|
249
|
+
/**
|
|
250
|
+
* Bottom Tangent Strategy (stand protrusion)
|
|
251
|
+
* Forces a feature to be tangent to the dieline bottom edge from outside (below).
|
|
252
|
+
*/
|
|
253
|
+
const tangentBottomConstraint: ConstraintHandler = (x, y, feature, context, params) => {
|
|
254
|
+
const { dielineWidth, dielineHeight } = context;
|
|
255
|
+
const gap = params.gap || 0;
|
|
256
|
+
const confineX = params.confineX !== false;
|
|
257
|
+
|
|
258
|
+
const extentY =
|
|
259
|
+
feature.shape === "circle"
|
|
260
|
+
? feature.radius || 0
|
|
261
|
+
: (feature.height || 0) / 2;
|
|
262
|
+
const newY = 1 + (extentY + gap) / dielineHeight;
|
|
263
|
+
|
|
264
|
+
let newX = x;
|
|
265
|
+
if (confineX) {
|
|
266
|
+
const extentX =
|
|
267
|
+
feature.shape === "circle"
|
|
268
|
+
? feature.radius || 0
|
|
269
|
+
: (feature.width || 0) / 2;
|
|
270
|
+
const minX = extentX / dielineWidth;
|
|
271
|
+
const maxX = 1 - extentX / dielineWidth;
|
|
272
|
+
newX = minX > maxX ? 0.5 : Math.max(minX, Math.min(newX, maxX));
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return { x: newX, y: newY };
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Lowest Tangent Strategy (Lowest Point Lock)
|
|
280
|
+
* Finds the lowest point of the Dieline geometry and locks the feature's Y to that level.
|
|
281
|
+
* Allows horizontal movement (X).
|
|
282
|
+
*/
|
|
283
|
+
const lowestTangentConstraint: ConstraintHandler = (x, y, feature, context, params) => {
|
|
284
|
+
const { dielineWidth, dielineHeight, geometry } = context;
|
|
285
|
+
if (!geometry) return { x, y };
|
|
286
|
+
|
|
287
|
+
const lowest = getLowestPointOnDieline(geometry);
|
|
288
|
+
|
|
289
|
+
// Calculate normalized Y of the lowest point
|
|
290
|
+
const minY = geometry.y - geometry.height / 2;
|
|
291
|
+
const normY = (lowest.y - minY) / geometry.height;
|
|
292
|
+
|
|
293
|
+
const gap = params.gap || 0;
|
|
294
|
+
const confineX = params.confineX !== false;
|
|
295
|
+
|
|
296
|
+
const extentY =
|
|
297
|
+
feature.shape === "circle"
|
|
298
|
+
? feature.radius || 0
|
|
299
|
+
: (feature.height || 0) / 2;
|
|
300
|
+
|
|
301
|
+
const newY = normY + (extentY + gap) / dielineHeight;
|
|
302
|
+
|
|
303
|
+
let newX = x;
|
|
304
|
+
if (confineX) {
|
|
305
|
+
const extentX =
|
|
306
|
+
feature.shape === "circle"
|
|
307
|
+
? feature.radius || 0
|
|
308
|
+
: (feature.width || 0) / 2;
|
|
309
|
+
const minX = extentX / dielineWidth;
|
|
310
|
+
const maxX = 1 - extentX / dielineWidth;
|
|
311
|
+
newX = minX > maxX ? 0.5 : Math.max(minX, Math.min(newX, maxX));
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return { x: newX, y: newY };
|
|
315
|
+
};
|
|
316
|
+
|
|
156
317
|
// Register built-ins
|
|
318
|
+
ConstraintRegistry.register("path", pathConstraint);
|
|
157
319
|
ConstraintRegistry.register("edge", edgeConstraint);
|
|
158
320
|
ConstraintRegistry.register("internal", internalConstraint);
|
|
321
|
+
ConstraintRegistry.register("tangent-bottom", tangentBottomConstraint);
|
|
322
|
+
ConstraintRegistry.register("lowest-tangent", lowestTangentConstraint);
|
package/src/coordinate.ts
CHANGED
|
@@ -1,106 +1,106 @@
|
|
|
1
|
-
export interface Point {
|
|
2
|
-
x: number;
|
|
3
|
-
y: number;
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
export interface Size {
|
|
7
|
-
width: number;
|
|
8
|
-
height: number;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export type Unit = "px" | "mm" | "cm" | "in";
|
|
12
|
-
|
|
13
|
-
export interface Layout {
|
|
14
|
-
scale: number;
|
|
15
|
-
offsetX: number;
|
|
16
|
-
offsetY: number;
|
|
17
|
-
width: number;
|
|
18
|
-
height: number;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export class Coordinate {
|
|
22
|
-
/**
|
|
23
|
-
* Calculate layout to fit content within container while preserving aspect ratio.
|
|
24
|
-
*/
|
|
25
|
-
static calculateLayout(
|
|
26
|
-
container: Size,
|
|
27
|
-
content: Size,
|
|
28
|
-
padding: number = 0,
|
|
29
|
-
): Layout {
|
|
30
|
-
const availableWidth = Math.max(0, container.width - padding * 2);
|
|
31
|
-
const availableHeight = Math.max(0, container.height - padding * 2);
|
|
32
|
-
|
|
33
|
-
if (content.width === 0 || content.height === 0) {
|
|
34
|
-
return { scale: 1, offsetX: 0, offsetY: 0, width: 0, height: 0 };
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const scaleX = availableWidth / content.width;
|
|
38
|
-
const scaleY = availableHeight / content.height;
|
|
39
|
-
const scale = Math.min(scaleX, scaleY);
|
|
40
|
-
|
|
41
|
-
const width = content.width * scale;
|
|
42
|
-
const height = content.height * scale;
|
|
43
|
-
|
|
44
|
-
const offsetX = (container.width - width) / 2;
|
|
45
|
-
const offsetY = (container.height - height) / 2;
|
|
46
|
-
|
|
47
|
-
return { scale, offsetX, offsetY, width, height };
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Convert an absolute value to a normalized value (0-1).
|
|
52
|
-
* @param value Absolute value (e.g., pixels)
|
|
53
|
-
* @param total Total dimension size (e.g., canvas width)
|
|
54
|
-
*/
|
|
55
|
-
static toNormalized(value: number, total: number): number {
|
|
56
|
-
return total === 0 ? 0 : value / total;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Convert a normalized value (0-1) to an absolute value.
|
|
61
|
-
* @param normalized Normalized value (0-1)
|
|
62
|
-
* @param total Total dimension size (e.g., canvas width)
|
|
63
|
-
*/
|
|
64
|
-
static toAbsolute(normalized: number, total: number): number {
|
|
65
|
-
return normalized * total;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Normalize a point's coordinates.
|
|
70
|
-
*/
|
|
71
|
-
static normalizePoint(point: Point, size: Size): Point {
|
|
72
|
-
return {
|
|
73
|
-
x: this.toNormalized(point.x, size.width),
|
|
74
|
-
y: this.toNormalized(point.y, size.height),
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Denormalize a point's coordinates to absolute pixels.
|
|
80
|
-
*/
|
|
81
|
-
static denormalizePoint(point: Point, size: Size): Point {
|
|
82
|
-
return {
|
|
83
|
-
x: this.toAbsolute(point.x, size.width),
|
|
84
|
-
y: this.toAbsolute(point.y, size.height),
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
static convertUnit(value: number, from: Unit, to: Unit): number {
|
|
89
|
-
if (from === to) return value;
|
|
90
|
-
|
|
91
|
-
// Base unit: mm
|
|
92
|
-
const toMM: Record<Unit, number> = {
|
|
93
|
-
px: 0.264583, // 1px = 0.264583mm (96 DPI)
|
|
94
|
-
mm: 1,
|
|
95
|
-
cm: 10,
|
|
96
|
-
in: 25.4
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
const mmValue = value * (from === 'px' ? toMM.px : toMM[from] || 1);
|
|
100
|
-
|
|
101
|
-
if (to === 'px') {
|
|
102
|
-
return mmValue / toMM.px;
|
|
103
|
-
}
|
|
104
|
-
return mmValue / (toMM[to] || 1);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
1
|
+
export interface Point {
|
|
2
|
+
x: number;
|
|
3
|
+
y: number;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface Size {
|
|
7
|
+
width: number;
|
|
8
|
+
height: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type Unit = "px" | "mm" | "cm" | "in";
|
|
12
|
+
|
|
13
|
+
export interface Layout {
|
|
14
|
+
scale: number;
|
|
15
|
+
offsetX: number;
|
|
16
|
+
offsetY: number;
|
|
17
|
+
width: number;
|
|
18
|
+
height: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class Coordinate {
|
|
22
|
+
/**
|
|
23
|
+
* Calculate layout to fit content within container while preserving aspect ratio.
|
|
24
|
+
*/
|
|
25
|
+
static calculateLayout(
|
|
26
|
+
container: Size,
|
|
27
|
+
content: Size,
|
|
28
|
+
padding: number = 0,
|
|
29
|
+
): Layout {
|
|
30
|
+
const availableWidth = Math.max(0, container.width - padding * 2);
|
|
31
|
+
const availableHeight = Math.max(0, container.height - padding * 2);
|
|
32
|
+
|
|
33
|
+
if (content.width === 0 || content.height === 0) {
|
|
34
|
+
return { scale: 1, offsetX: 0, offsetY: 0, width: 0, height: 0 };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const scaleX = availableWidth / content.width;
|
|
38
|
+
const scaleY = availableHeight / content.height;
|
|
39
|
+
const scale = Math.min(scaleX, scaleY);
|
|
40
|
+
|
|
41
|
+
const width = content.width * scale;
|
|
42
|
+
const height = content.height * scale;
|
|
43
|
+
|
|
44
|
+
const offsetX = (container.width - width) / 2;
|
|
45
|
+
const offsetY = (container.height - height) / 2;
|
|
46
|
+
|
|
47
|
+
return { scale, offsetX, offsetY, width, height };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Convert an absolute value to a normalized value (0-1).
|
|
52
|
+
* @param value Absolute value (e.g., pixels)
|
|
53
|
+
* @param total Total dimension size (e.g., canvas width)
|
|
54
|
+
*/
|
|
55
|
+
static toNormalized(value: number, total: number): number {
|
|
56
|
+
return total === 0 ? 0 : value / total;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Convert a normalized value (0-1) to an absolute value.
|
|
61
|
+
* @param normalized Normalized value (0-1)
|
|
62
|
+
* @param total Total dimension size (e.g., canvas width)
|
|
63
|
+
*/
|
|
64
|
+
static toAbsolute(normalized: number, total: number): number {
|
|
65
|
+
return normalized * total;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Normalize a point's coordinates.
|
|
70
|
+
*/
|
|
71
|
+
static normalizePoint(point: Point, size: Size): Point {
|
|
72
|
+
return {
|
|
73
|
+
x: this.toNormalized(point.x, size.width),
|
|
74
|
+
y: this.toNormalized(point.y, size.height),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Denormalize a point's coordinates to absolute pixels.
|
|
80
|
+
*/
|
|
81
|
+
static denormalizePoint(point: Point, size: Size): Point {
|
|
82
|
+
return {
|
|
83
|
+
x: this.toAbsolute(point.x, size.width),
|
|
84
|
+
y: this.toAbsolute(point.y, size.height),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
static convertUnit(value: number, from: Unit, to: Unit): number {
|
|
89
|
+
if (from === to) return value;
|
|
90
|
+
|
|
91
|
+
// Base unit: mm
|
|
92
|
+
const toMM: Record<Unit, number> = {
|
|
93
|
+
px: 0.264583, // 1px = 0.264583mm (96 DPI)
|
|
94
|
+
mm: 1,
|
|
95
|
+
cm: 10,
|
|
96
|
+
in: 25.4
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const mmValue = value * (from === 'px' ? toMM.px : toMM[from] || 1);
|
|
100
|
+
|
|
101
|
+
if (to === 'px') {
|
|
102
|
+
return mmValue / toMM.px;
|
|
103
|
+
}
|
|
104
|
+
return mmValue / (toMM[to] || 1);
|
|
105
|
+
}
|
|
106
|
+
}
|