@zseven-w/pen-core 0.6.0 → 0.7.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.
Files changed (45) hide show
  1. package/README.md +18 -9
  2. package/package.json +5 -5
  3. package/src/__tests__/arc-path.test.ts +23 -23
  4. package/src/__tests__/codegen-utils.test.ts +50 -0
  5. package/src/__tests__/design-md-parser.test.ts +49 -0
  6. package/src/__tests__/font-utils.test.ts +15 -15
  7. package/src/__tests__/layout-engine.test.ts +169 -83
  8. package/src/__tests__/merge-helpers.test.ts +143 -0
  9. package/src/__tests__/node-diff.test.ts +139 -0
  10. package/src/__tests__/node-helpers.test.ts +19 -19
  11. package/src/__tests__/node-merge.test.ts +425 -0
  12. package/src/__tests__/normalize-stroke-fill-schema.test.ts +416 -0
  13. package/src/__tests__/normalize-tree-layout.test.ts +294 -0
  14. package/src/__tests__/normalize.test.ts +119 -80
  15. package/src/__tests__/path-anchors.test.ts +98 -0
  16. package/src/__tests__/resolve-variables-recursive.test.ts +109 -54
  17. package/src/__tests__/strip-redundant-section-fills.test.ts +278 -0
  18. package/src/__tests__/text-measure.test.ts +84 -79
  19. package/src/__tests__/tree-utils.test.ts +133 -102
  20. package/src/__tests__/unwrap-fake-phone-mockup.test.ts +322 -0
  21. package/src/__tests__/variables.test.ts +68 -65
  22. package/src/arc-path.ts +35 -35
  23. package/src/boolean-ops.ts +131 -142
  24. package/src/constants.ts +36 -36
  25. package/src/design-md-parser.ts +363 -0
  26. package/src/font-utils.ts +30 -15
  27. package/src/id.ts +1 -1
  28. package/src/index.ts +47 -13
  29. package/src/layout/engine.ts +255 -224
  30. package/src/layout/normalize-tree.ts +140 -0
  31. package/src/layout/strip-redundant-section-fills.ts +155 -0
  32. package/src/layout/text-measure.ts +130 -106
  33. package/src/layout/unwrap-fake-phone-mockup.ts +147 -0
  34. package/src/merge/index.ts +16 -0
  35. package/src/merge/merge-helpers.ts +113 -0
  36. package/src/merge/node-diff.ts +123 -0
  37. package/src/merge/node-merge.ts +651 -0
  38. package/src/node-helpers.ts +18 -5
  39. package/src/normalize/normalize-stroke-fill-schema.ts +297 -0
  40. package/src/normalize.ts +75 -81
  41. package/src/path-anchors.ts +331 -0
  42. package/src/sync-lock.ts +3 -3
  43. package/src/tree-utils.ts +180 -158
  44. package/src/variables/replace-refs.ts +63 -60
  45. package/src/variables/resolve.ts +98 -106
@@ -1,76 +1,77 @@
1
- import { nanoid } from 'nanoid'
2
- import type { PenNode, PathNode } from '@zseven-w/pen-types'
1
+ import { nanoid } from 'nanoid';
2
+ import type { PenNode, PathNode } from '@zseven-w/pen-types';
3
3
 
4
- export type BooleanOpType = 'union' | 'subtract' | 'intersect'
4
+ export type BooleanOpType = 'union' | 'subtract' | 'intersect';
5
5
 
6
6
  // ---------------------------------------------------------------------------
7
7
  // Paper.js scope — headless (no canvas needed)
8
8
  // ---------------------------------------------------------------------------
9
9
 
10
10
  interface PaperBoundsLike {
11
- center: unknown
12
- x: number
13
- y: number
14
- width: number
15
- height: number
11
+ center: unknown;
12
+ x: number;
13
+ y: number;
14
+ width: number;
15
+ height: number;
16
16
  }
17
17
 
18
18
  interface PaperPathItem {
19
- bounds: PaperBoundsLike
20
- pathData: string
21
- translate: (point: unknown) => void
22
- rotate: (angle: number, center: unknown) => void
23
- unite: (path: PaperPathItem) => PaperPathItem
24
- subtract: (path: PaperPathItem) => PaperPathItem
25
- intersect: (path: PaperPathItem) => PaperPathItem
26
- remove: () => void
19
+ bounds: PaperBoundsLike;
20
+ pathData: string;
21
+ translate: (point: unknown) => void;
22
+ rotate: (angle: number, center: unknown) => void;
23
+ unite: (path: PaperPathItem) => PaperPathItem;
24
+ subtract: (path: PaperPathItem) => PaperPathItem;
25
+ intersect: (path: PaperPathItem) => PaperPathItem;
26
+ remove: () => void;
27
27
  }
28
28
 
29
29
  interface PaperScope {
30
- setup: (size: unknown) => void
31
- activate: () => void
32
- Size: new (width: number, height: number) => unknown
33
- Point: new (x: number, y: number) => unknown
30
+ setup: (size: unknown) => void;
31
+ activate: () => void;
32
+ Size: new (width: number, height: number) => unknown;
33
+ Point: new (x: number, y: number) => unknown;
34
34
  CompoundPath: {
35
- create: (pathData: string) => PaperPathItem
36
- }
35
+ create: (pathData: string) => PaperPathItem;
36
+ };
37
37
  }
38
38
 
39
39
  interface PaperModule {
40
- PaperScope: new () => PaperScope
41
- Point: new (x: number, y: number) => unknown
40
+ PaperScope: new () => PaperScope;
41
+ Point: new (x: number, y: number) => unknown;
42
42
  }
43
43
 
44
- let paperModule: PaperModule | null | undefined
45
- let scope: PaperScope | null = null
44
+ let paperModule: PaperModule | null | undefined;
45
+ let scope: PaperScope | null = null;
46
46
 
47
47
  function getPaperModule(): PaperModule | null {
48
- if (paperModule !== undefined) return paperModule
48
+ if (paperModule !== undefined) return paperModule;
49
49
  try {
50
50
  // Indirect require via globalThis to load paper.js at runtime without
51
51
  // triggering esbuild's direct-eval warning. The assignment to globalThis
52
52
  // happens once; subsequent calls read from the cached paperModule.
53
- const _r = (globalThis as any)['require'] as NodeRequire | undefined
54
- ?? (typeof require !== 'undefined' ? require : undefined)
55
- if (!_r) throw new Error('require not available')
56
- paperModule = _r('paper') as PaperModule
53
+ const _r =
54
+ ((globalThis as any)['require'] as NodeRequire | undefined) ??
55
+ (typeof require !== 'undefined' ? require : undefined);
56
+ if (!_r) throw new Error('require not available');
57
+ paperModule = _r('paper') as PaperModule;
57
58
  } catch {
58
- paperModule = null
59
+ paperModule = null;
59
60
  }
60
- return paperModule
61
+ return paperModule;
61
62
  }
62
63
 
63
64
  function getScope(): PaperScope {
64
- const paper = getPaperModule()
65
+ const paper = getPaperModule();
65
66
  if (!paper) {
66
- throw new Error('paper runtime is unavailable')
67
+ throw new Error('paper runtime is unavailable');
67
68
  }
68
69
  if (!scope) {
69
- scope = new paper.PaperScope()
70
- scope.setup(new scope.Size(1, 1))
70
+ scope = new paper.PaperScope();
71
+ scope.setup(new scope.Size(1, 1));
71
72
  }
72
- scope.activate()
73
- return scope
73
+ scope.activate();
74
+ return scope;
74
75
  }
75
76
 
76
77
  // ---------------------------------------------------------------------------
@@ -78,31 +79,26 @@ function getScope(): PaperScope {
78
79
  // ---------------------------------------------------------------------------
79
80
 
80
81
  function sizeVal(v: number | string | undefined, fallback: number): number {
81
- if (typeof v === 'number') return v
82
+ if (typeof v === 'number') return v;
82
83
  if (typeof v === 'string') {
83
- const m = v.match(/\((\d+(?:\.\d+)?)\)/)
84
- if (m) return parseFloat(m[1])
85
- const n = parseFloat(v)
86
- if (!isNaN(n)) return n
84
+ const m = v.match(/\((\d+(?:\.\d+)?)\)/);
85
+ if (m) return parseFloat(m[1]);
86
+ const n = parseFloat(v);
87
+ if (!isNaN(n)) return n;
87
88
  }
88
- return fallback
89
+ return fallback;
89
90
  }
90
91
 
91
- function rectToPath(
92
- w: number,
93
- h: number,
94
- cr?: number | [number, number, number, number],
95
- ): string {
92
+ function rectToPath(w: number, h: number, cr?: number | [number, number, number, number]): string {
96
93
  if (!cr || (typeof cr === 'number' && cr === 0)) {
97
- return `M 0 0 L ${w} 0 L ${w} ${h} L 0 ${h} Z`
94
+ return `M 0 0 L ${w} 0 L ${w} ${h} L 0 ${h} Z`;
98
95
  }
99
- let [tl, tr, br, bl] =
100
- typeof cr === 'number' ? [cr, cr, cr, cr] : cr
101
- const maxR = Math.min(w, h) / 2
102
- tl = Math.min(tl, maxR)
103
- tr = Math.min(tr, maxR)
104
- br = Math.min(br, maxR)
105
- bl = Math.min(bl, maxR)
96
+ let [tl, tr, br, bl] = typeof cr === 'number' ? [cr, cr, cr, cr] : cr;
97
+ const maxR = Math.min(w, h) / 2;
98
+ tl = Math.min(tl, maxR);
99
+ tr = Math.min(tr, maxR);
100
+ br = Math.min(br, maxR);
101
+ bl = Math.min(bl, maxR);
106
102
  return [
107
103
  `M ${tl} 0`,
108
104
  `L ${w - tr} 0`,
@@ -116,7 +112,7 @@ function rectToPath(
116
112
  'Z',
117
113
  ]
118
114
  .filter(Boolean)
119
- .join(' ')
115
+ .join(' ');
120
116
  }
121
117
 
122
118
  function ellipseToPath(rx: number, ry: number): string {
@@ -128,32 +124,35 @@ function ellipseToPath(rx: number, ry: number): string {
128
124
  `A ${rx} ${ry} 0 0 1 ${rx} 0`,
129
125
  `A ${rx} ${ry} 0 0 1 ${rx * 2} ${ry}`,
130
126
  'Z',
131
- ].join(' ')
127
+ ].join(' ');
132
128
  }
133
129
 
134
130
  function polygonToPath(count: number, w: number, h: number): string {
135
- const raw: [number, number][] = []
131
+ const raw: [number, number][] = [];
136
132
  for (let i = 0; i < count; i++) {
137
- const angle = (i * 2 * Math.PI) / count - Math.PI / 2
138
- raw.push([Math.cos(angle), Math.sin(angle)])
133
+ const angle = (i * 2 * Math.PI) / count - Math.PI / 2;
134
+ raw.push([Math.cos(angle), Math.sin(angle)]);
139
135
  }
140
- let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity
136
+ let minX = Infinity,
137
+ maxX = -Infinity,
138
+ minY = Infinity,
139
+ maxY = -Infinity;
141
140
  for (const [rx, ry] of raw) {
142
- if (rx < minX) minX = rx
143
- if (rx > maxX) maxX = rx
144
- if (ry < minY) minY = ry
145
- if (ry > maxY) maxY = ry
141
+ if (rx < minX) minX = rx;
142
+ if (rx > maxX) maxX = rx;
143
+ if (ry < minY) minY = ry;
144
+ if (ry > maxY) maxY = ry;
146
145
  }
147
- const rw = maxX - minX
148
- const rh = maxY - minY
149
- const parts: string[] = []
146
+ const rw = maxX - minX;
147
+ const rh = maxY - minY;
148
+ const parts: string[] = [];
150
149
  for (let i = 0; i < count; i++) {
151
- const px = ((raw[i][0] - minX) / rw) * w
152
- const py = ((raw[i][1] - minY) / rh) * h
153
- parts.push(i === 0 ? `M ${px} ${py}` : `L ${px} ${py}`)
150
+ const px = ((raw[i][0] - minX) / rw) * w;
151
+ const py = ((raw[i][1] - minY) / rh) * h;
152
+ parts.push(i === 0 ? `M ${px} ${py}` : `L ${px} ${py}`);
154
153
  }
155
- parts.push('Z')
156
- return parts.join(' ')
154
+ parts.push('Z');
155
+ return parts.join(' ');
157
156
  }
158
157
 
159
158
  /** Convert a shape node to an SVG path `d` string in local coordinates (origin at 0,0). */
@@ -161,26 +160,26 @@ function nodeToLocalPath(node: PenNode): string | null {
161
160
  switch (node.type) {
162
161
  case 'rectangle':
163
162
  case 'frame': {
164
- const w = sizeVal(node.width, 100)
165
- const h = sizeVal(node.height, 100)
166
- return rectToPath(w, h, node.cornerRadius)
163
+ const w = sizeVal(node.width, 100);
164
+ const h = sizeVal(node.height, 100);
165
+ return rectToPath(w, h, node.cornerRadius);
167
166
  }
168
167
  case 'ellipse': {
169
- const w = sizeVal(node.width, 100)
170
- const h = sizeVal(node.height, 100)
171
- return ellipseToPath(w / 2, h / 2)
168
+ const w = sizeVal(node.width, 100);
169
+ const h = sizeVal(node.height, 100);
170
+ return ellipseToPath(w / 2, h / 2);
172
171
  }
173
172
  case 'polygon': {
174
- const w = sizeVal(node.width, 100)
175
- const h = sizeVal(node.height, 100)
176
- return polygonToPath(node.polygonCount || 6, w, h)
173
+ const w = sizeVal(node.width, 100);
174
+ const h = sizeVal(node.height, 100);
175
+ return polygonToPath(node.polygonCount || 6, w, h);
177
176
  }
178
177
  case 'path':
179
- return node.d
178
+ return node.d;
180
179
  case 'line':
181
- return `M 0 0 L ${(node.x2 ?? (node.x ?? 0) + 100) - (node.x ?? 0)} ${(node.y2 ?? (node.y ?? 0)) - (node.y ?? 0)}`
180
+ return `M 0 0 L ${(node.x2 ?? (node.x ?? 0) + 100) - (node.x ?? 0)} ${(node.y2 ?? node.y ?? 0) - (node.y ?? 0)}`;
182
181
  default:
183
- return null
182
+ return null;
184
183
  }
185
184
  }
186
185
 
@@ -189,18 +188,11 @@ function nodeToLocalPath(node: PenNode): string | null {
189
188
  // ---------------------------------------------------------------------------
190
189
 
191
190
  /** Types that can participate in boolean operations. */
192
- const BOOLEAN_TYPES = new Set([
193
- 'rectangle',
194
- 'ellipse',
195
- 'polygon',
196
- 'path',
197
- 'line',
198
- 'frame',
199
- ])
191
+ const BOOLEAN_TYPES = new Set(['rectangle', 'ellipse', 'polygon', 'path', 'line', 'frame']);
200
192
 
201
193
  export function canBooleanOp(nodes: PenNode[]): boolean {
202
- if (nodes.length < 2) return false
203
- return nodes.every((n) => BOOLEAN_TYPES.has(n.type))
194
+ if (nodes.length < 2) return false;
195
+ return nodes.every((n) => BOOLEAN_TYPES.has(n.type));
204
196
  }
205
197
 
206
198
  /**
@@ -208,94 +200,91 @@ export function canBooleanOp(nodes: PenNode[]): boolean {
208
200
  * coordinates (applying x, y, rotation).
209
201
  */
210
202
  function nodeToPaperPath(node: PenNode): PaperPathItem | null {
211
- const d = nodeToLocalPath(node)
212
- if (!d) return null
203
+ const d = nodeToLocalPath(node);
204
+ if (!d) return null;
213
205
 
214
- const s = getScope()
215
- let item: PaperPathItem
206
+ const s = getScope();
207
+ let item: PaperPathItem;
216
208
  try {
217
- item = s.CompoundPath.create(d)
209
+ item = s.CompoundPath.create(d);
218
210
  } catch {
219
- return null
211
+ return null;
220
212
  }
221
213
 
222
214
  // Apply node transform: translate to (x, y), then rotate around center
223
- const x = node.x ?? 0
224
- const y = node.y ?? 0
225
- item.translate(new s.Point(x, y))
215
+ const x = node.x ?? 0;
216
+ const y = node.y ?? 0;
217
+ item.translate(new s.Point(x, y));
226
218
 
227
- const rotation = node.rotation ?? 0
219
+ const rotation = node.rotation ?? 0;
228
220
  if (rotation !== 0) {
229
221
  // Rotate around the bounding-box center of the translated item
230
- item.rotate(rotation, item.bounds.center)
222
+ item.rotate(rotation, item.bounds.center);
231
223
  }
232
224
 
233
- return item
225
+ return item;
234
226
  }
235
227
 
236
228
  /**
237
229
  * Execute a boolean operation on the given PenNodes.
238
230
  * Returns a new PathNode with the result, or null on failure.
239
231
  */
240
- export function executeBooleanOp(
241
- nodes: PenNode[],
242
- operation: BooleanOpType,
243
- ): PathNode | null {
244
- if (nodes.length < 2) return null
232
+ export function executeBooleanOp(nodes: PenNode[], operation: BooleanOpType): PathNode | null {
233
+ if (nodes.length < 2) return null;
245
234
 
246
- if (!getPaperModule()) return null
235
+ if (!getPaperModule()) return null;
247
236
 
248
- const paperPaths = nodes.map(nodeToPaperPath)
249
- if (paperPaths.some((p) => p === null)) return null
237
+ const paperPaths = nodes.map(nodeToPaperPath);
238
+ if (paperPaths.some((p) => p === null)) return null;
250
239
 
251
- const paths = paperPaths as PaperPathItem[]
240
+ const paths = paperPaths as PaperPathItem[];
252
241
 
253
242
  // Accumulate: fold left with the boolean operation
254
- let result = paths[0]
243
+ let result = paths[0];
255
244
  for (let i = 1; i < paths.length; i++) {
256
245
  switch (operation) {
257
246
  case 'union':
258
- result = result.unite(paths[i])
259
- break
247
+ result = result.unite(paths[i]);
248
+ break;
260
249
  case 'subtract':
261
- result = result.subtract(paths[i])
262
- break
250
+ result = result.subtract(paths[i]);
251
+ break;
263
252
  case 'intersect':
264
- result = result.intersect(paths[i])
265
- break
253
+ result = result.intersect(paths[i]);
254
+ break;
266
255
  }
267
256
  }
268
257
 
269
258
  // Extract SVG path data
270
- const pathData = result.pathData
271
- if (!pathData || pathData.trim().length === 0) return null
259
+ const pathData = result.pathData;
260
+ if (!pathData || pathData.trim().length === 0) return null;
272
261
 
273
262
  // Get bounding box for positioning
274
- const bounds = result.bounds
263
+ const bounds = result.bounds;
275
264
 
276
265
  // Translate path so it starts at origin (0,0)
277
- const paper = getPaperModule()
278
- if (!paper) return null
279
- result.translate(new paper.Point(-bounds.x, -bounds.y))
280
- const originPathData = result.pathData
266
+ const paper = getPaperModule();
267
+ if (!paper) return null;
268
+ result.translate(new paper.Point(-bounds.x, -bounds.y));
269
+ const originPathData = result.pathData;
281
270
 
282
271
  // Clean up Paper.js items
283
- for (const p of paths) p.remove()
284
- result.remove()
272
+ for (const p of paths) p.remove();
273
+ result.remove();
285
274
 
286
275
  // Build the label
287
276
  const opLabels: Record<BooleanOpType, string> = {
288
277
  union: 'Union',
289
278
  subtract: 'Subtract',
290
279
  intersect: 'Intersect',
291
- }
280
+ };
292
281
 
293
282
  // Inherit style from first operand
294
- const first = nodes[0]
295
- const fill = 'fill' in first ? first.fill : undefined
296
- const stroke = 'stroke' in first ? first.stroke : undefined
297
- const effects = 'effects' in first ? first.effects : undefined
298
- const opacity = first.opacity
283
+ const first = nodes[0];
284
+ const fill = 'fill' in first ? first.fill : undefined;
285
+ const stroke = 'stroke' in first ? first.stroke : undefined;
286
+ const effects = 'effects' in first ? first.effects : undefined;
287
+ const opacity = first.opacity;
299
288
 
300
289
  return {
301
290
  id: nanoid(),
@@ -310,5 +299,5 @@ export function executeBooleanOp(
310
299
  stroke,
311
300
  effects,
312
301
  opacity,
313
- }
302
+ };
314
303
  }
package/src/constants.ts CHANGED
@@ -1,49 +1,49 @@
1
- export const MIN_ZOOM = 0.02
2
- export const MAX_ZOOM = 256
3
- export const ZOOM_STEP = 0.1
4
- export const SNAP_THRESHOLD = 5
5
- export const DEFAULT_FILL = '#d1d5db'
6
- export const DEFAULT_STROKE = '#374151'
7
- export const DEFAULT_STROKE_WIDTH = 1
8
- export const CANVAS_BACKGROUND_LIGHT = '#e5e5e5'
9
- export const CANVAS_BACKGROUND_DARK = '#1a1a1a'
1
+ export const MIN_ZOOM = 0.02;
2
+ export const MAX_ZOOM = 256;
3
+ export const ZOOM_STEP = 0.1;
4
+ export const SNAP_THRESHOLD = 5;
5
+ export const DEFAULT_FILL = '#d1d5db';
6
+ export const DEFAULT_STROKE = '#374151';
7
+ export const DEFAULT_STROKE_WIDTH = 1;
8
+ export const CANVAS_BACKGROUND_LIGHT = '#e5e5e5';
9
+ export const CANVAS_BACKGROUND_DARK = '#1a1a1a';
10
10
 
11
- export const SELECTION_BLUE = '#0d99ff'
12
- export const COMPONENT_COLOR = '#a855f7'
13
- export const INSTANCE_COLOR = '#9281f7'
11
+ export const SELECTION_BLUE = '#0d99ff';
12
+ export const COMPONENT_COLOR = '#a855f7';
13
+ export const INSTANCE_COLOR = '#9281f7';
14
14
 
15
15
  // Hover / overlay / indicator
16
- export const HOVER_BLUE = '#3b82f6'
17
- export const HOVER_LINE_WIDTH = 1.5
18
- export const HOVER_DASH = [4, 4]
19
- export const INDICATOR_BLUE = '#3B82F6'
20
- export const INDICATOR_LINE_WIDTH = 2
21
- export const INDICATOR_DASH = [6, 4]
22
- export const INDICATOR_ENDPOINT_RADIUS = 3
16
+ export const HOVER_BLUE = '#3b82f6';
17
+ export const HOVER_LINE_WIDTH = 1.5;
18
+ export const HOVER_DASH = [4, 4];
19
+ export const INDICATOR_BLUE = '#3B82F6';
20
+ export const INDICATOR_LINE_WIDTH = 2;
21
+ export const INDICATOR_DASH = [6, 4];
22
+ export const INDICATOR_ENDPOINT_RADIUS = 3;
23
23
 
24
24
  // Frame labels
25
- export const FRAME_LABEL_FONT_SIZE = 12
26
- export const FRAME_LABEL_OFFSET_Y = 6
27
- export const FRAME_LABEL_COLOR = '#999999'
25
+ export const FRAME_LABEL_FONT_SIZE = 12;
26
+ export const FRAME_LABEL_OFFSET_Y = 6;
27
+ export const FRAME_LABEL_COLOR = '#999999';
28
28
 
29
29
  // Pen tool
30
- export const PEN_ANCHOR_FILL = '#ffffff'
31
- export const PEN_ANCHOR_RADIUS = 4
32
- export const PEN_ANCHOR_FIRST_RADIUS = 5
33
- export const PEN_HANDLE_DOT_RADIUS = 3
34
- export const PEN_HANDLE_LINE_STROKE = '#888888'
35
- export const PEN_RUBBER_BAND_STROKE = 'rgba(13, 153, 255, 0.5)'
36
- export const PEN_RUBBER_BAND_DASH = [4, 4]
37
- export const PEN_CLOSE_HIT_THRESHOLD = 8
30
+ export const PEN_ANCHOR_FILL = '#ffffff';
31
+ export const PEN_ANCHOR_RADIUS = 4;
32
+ export const PEN_ANCHOR_FIRST_RADIUS = 5;
33
+ export const PEN_HANDLE_DOT_RADIUS = 3;
34
+ export const PEN_HANDLE_LINE_STROKE = '#888888';
35
+ export const PEN_RUBBER_BAND_STROKE = 'rgba(13, 153, 255, 0.5)';
36
+ export const PEN_RUBBER_BAND_DASH = [4, 4];
37
+ export const PEN_CLOSE_HIT_THRESHOLD = 8;
38
38
 
39
39
  // Dimension label
40
- export const DIMENSION_LABEL_OFFSET_Y = 8
40
+ export const DIMENSION_LABEL_OFFSET_Y = 8;
41
41
 
42
42
  // Default node colors
43
- export const DEFAULT_FRAME_FILL = '#ffffff'
44
- export const DEFAULT_TEXT_FILL = '#000000'
43
+ export const DEFAULT_FRAME_FILL = '#ffffff';
44
+ export const DEFAULT_TEXT_FILL = '#000000';
45
45
 
46
46
  // Smart guides
47
- export const GUIDE_COLOR = '#FF6B35'
48
- export const GUIDE_LINE_WIDTH = 1
49
- export const GUIDE_DASH = [3, 3]
47
+ export const GUIDE_COLOR = '#FF6B35';
48
+ export const GUIDE_LINE_WIDTH = 1;
49
+ export const GUIDE_DASH = [3, 3];