@pooder/kit 6.2.0 → 6.2.2

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.
@@ -0,0 +1,206 @@
1
+ import type { Pattern } from "fabric";
2
+ import type { RenderObjectSpec } from "../../services";
3
+ import type {
4
+ SceneGeometrySnapshot,
5
+ SceneLayoutSnapshot,
6
+ SceneRect,
7
+ } from "../../shared/scene/sceneLayoutModel";
8
+ import { generateDielinePath } from "../geometry";
9
+
10
+ export interface ImageSessionOverlayVisualConfig {
11
+ strokeColor: string;
12
+ strokeWidth: number;
13
+ strokeStyle: "solid" | "dashed" | "hidden";
14
+ dashLength: number;
15
+ innerBackground: string;
16
+ outerBackground: string;
17
+ }
18
+
19
+ export interface ImageSessionOverlayViewport {
20
+ left: number;
21
+ top: number;
22
+ width: number;
23
+ height: number;
24
+ }
25
+
26
+ interface BuiltinShapeOverlayPaths {
27
+ hatchPathData: string;
28
+ shapePathData: string;
29
+ }
30
+
31
+ const EPSILON = 0.0001;
32
+ const SHAPE_OUTLINE_COLOR = "rgba(255, 0, 0, 0.9)";
33
+ const DEFAULT_HATCH_FILL = "rgba(255, 0, 0, 0.22)";
34
+
35
+ function buildRectPath(width: number, height: number): string {
36
+ return `M 0 0 L ${width} 0 L ${width} ${height} L 0 ${height} Z`;
37
+ }
38
+
39
+ function buildViewportMaskPath(
40
+ viewport: ImageSessionOverlayViewport,
41
+ cutRect: SceneRect,
42
+ ): string {
43
+ const cutLeft = cutRect.left - viewport.left;
44
+ const cutTop = cutRect.top - viewport.top;
45
+ return [
46
+ buildRectPath(viewport.width, viewport.height),
47
+ `M ${cutLeft} ${cutTop} L ${cutLeft + cutRect.width} ${cutTop} L ${
48
+ cutLeft + cutRect.width
49
+ } ${cutTop + cutRect.height} L ${cutLeft} ${cutTop + cutRect.height} Z`,
50
+ ].join(" ");
51
+ }
52
+
53
+ function resolveCutShapeRadiusPx(
54
+ geometry: SceneGeometrySnapshot,
55
+ cutRect: SceneRect,
56
+ ): number {
57
+ const visualRadius = Number.isFinite(geometry.radius)
58
+ ? Math.max(0, geometry.radius)
59
+ : 0;
60
+ const visualOffset = Number.isFinite(geometry.offset) ? geometry.offset : 0;
61
+ const rawCutRadius =
62
+ visualRadius === 0 ? 0 : Math.max(0, visualRadius + visualOffset);
63
+ const maxRadius = Math.max(0, Math.min(cutRect.width, cutRect.height) / 2);
64
+ return Math.max(0, Math.min(maxRadius, rawCutRadius));
65
+ }
66
+
67
+ function buildBuiltinShapeOverlayPaths(
68
+ cutRect: SceneRect,
69
+ geometry: SceneGeometrySnapshot | null,
70
+ ): BuiltinShapeOverlayPaths | null {
71
+ if (!geometry || geometry.shape === "custom") {
72
+ return null;
73
+ }
74
+
75
+ const radius = resolveCutShapeRadiusPx(geometry, cutRect);
76
+ if (geometry.shape === "rect" && radius <= EPSILON) {
77
+ return null;
78
+ }
79
+
80
+ const shapePathData = generateDielinePath({
81
+ shape: geometry.shape,
82
+ shapeStyle: geometry.shapeStyle,
83
+ width: Math.max(1, cutRect.width),
84
+ height: Math.max(1, cutRect.height),
85
+ radius,
86
+ x: cutRect.width / 2,
87
+ y: cutRect.height / 2,
88
+ features: [],
89
+ canvasWidth: Math.max(1, cutRect.width),
90
+ canvasHeight: Math.max(1, cutRect.height),
91
+ });
92
+ if (!shapePathData) {
93
+ return null;
94
+ }
95
+
96
+ return {
97
+ shapePathData,
98
+ hatchPathData: `${buildRectPath(cutRect.width, cutRect.height)} ${shapePathData}`,
99
+ };
100
+ }
101
+
102
+ export function buildImageSessionOverlaySpecs(args: {
103
+ viewport: ImageSessionOverlayViewport;
104
+ layout: SceneLayoutSnapshot;
105
+ geometry: SceneGeometrySnapshot | null;
106
+ visual: ImageSessionOverlayVisualConfig;
107
+ hatchPattern?: Pattern;
108
+ }): RenderObjectSpec[] {
109
+ const { viewport, layout, geometry, visual, hatchPattern } = args;
110
+ const cutRect = layout.cutRect;
111
+ const specs: RenderObjectSpec[] = [];
112
+
113
+ specs.push({
114
+ id: "image.cropMask.rect",
115
+ type: "path",
116
+ space: "screen",
117
+ data: { id: "image.cropMask.rect", zIndex: 1 },
118
+ props: {
119
+ pathData: buildViewportMaskPath(viewport, cutRect),
120
+ left: viewport.left,
121
+ top: viewport.top,
122
+ originX: "left",
123
+ originY: "top",
124
+ fill: visual.outerBackground,
125
+ stroke: null,
126
+ fillRule: "evenodd",
127
+ selectable: false,
128
+ evented: false,
129
+ excludeFromExport: true,
130
+ objectCaching: false,
131
+ },
132
+ });
133
+
134
+ const shapeOverlay = buildBuiltinShapeOverlayPaths(cutRect, geometry);
135
+ if (shapeOverlay) {
136
+ specs.push({
137
+ id: "image.cropShapeHatch",
138
+ type: "path",
139
+ space: "screen",
140
+ data: { id: "image.cropShapeHatch", zIndex: 5 },
141
+ props: {
142
+ pathData: shapeOverlay.hatchPathData,
143
+ left: cutRect.left,
144
+ top: cutRect.top,
145
+ originX: "left",
146
+ originY: "top",
147
+ fill: hatchPattern || DEFAULT_HATCH_FILL,
148
+ opacity: hatchPattern ? 1 : 0.8,
149
+ stroke: null,
150
+ fillRule: "evenodd",
151
+ selectable: false,
152
+ evented: false,
153
+ excludeFromExport: true,
154
+ objectCaching: false,
155
+ },
156
+ });
157
+ specs.push({
158
+ id: "image.cropShapeOutline",
159
+ type: "path",
160
+ space: "screen",
161
+ data: { id: "image.cropShapeOutline", zIndex: 6 },
162
+ props: {
163
+ pathData: shapeOverlay.shapePathData,
164
+ left: cutRect.left,
165
+ top: cutRect.top,
166
+ originX: "left",
167
+ originY: "top",
168
+ fill: "transparent",
169
+ stroke: SHAPE_OUTLINE_COLOR,
170
+ strokeWidth: 1,
171
+ selectable: false,
172
+ evented: false,
173
+ excludeFromExport: true,
174
+ objectCaching: false,
175
+ },
176
+ });
177
+ }
178
+
179
+ specs.push({
180
+ id: "image.cropFrame",
181
+ type: "rect",
182
+ space: "screen",
183
+ data: { id: "image.cropFrame", zIndex: 7 },
184
+ props: {
185
+ left: cutRect.left,
186
+ top: cutRect.top,
187
+ width: cutRect.width,
188
+ height: cutRect.height,
189
+ originX: "left",
190
+ originY: "top",
191
+ fill: visual.innerBackground,
192
+ stroke:
193
+ visual.strokeStyle === "hidden" ? "rgba(0,0,0,0)" : visual.strokeColor,
194
+ strokeWidth: visual.strokeStyle === "hidden" ? 0 : visual.strokeWidth,
195
+ strokeDashArray:
196
+ visual.strokeStyle === "dashed"
197
+ ? [visual.dashLength, visual.dashLength]
198
+ : undefined,
199
+ selectable: false,
200
+ evented: false,
201
+ excludeFromExport: true,
202
+ },
203
+ });
204
+
205
+ return specs;
206
+ }
@@ -20,11 +20,12 @@ const MIN_ARROW_SIZE = 4;
20
20
  const THICKNESS_TO_STROKE_WIDTH_RATIO = 20;
21
21
 
22
22
  const DEFAULT_THICKNESS = 20;
23
- const DEFAULT_GAP = 45;
23
+ const DEFAULT_GAP = 65;
24
24
  const DEFAULT_FONT_SIZE = 10;
25
25
  const DEFAULT_BACKGROUND_COLOR = "#f0f0f0";
26
26
  const DEFAULT_TEXT_COLOR = "#333333";
27
27
  const DEFAULT_LINE_COLOR = "#999999";
28
+ const RULER_DEBUG_KEY = "ruler.debug";
28
29
 
29
30
  const RULER_THICKNESS_MIN = 10;
30
31
  const RULER_THICKNESS_MAX = 100;
@@ -48,6 +49,7 @@ export class RulerTool implements Extension {
48
49
  private textColor = DEFAULT_TEXT_COLOR;
49
50
  private lineColor = DEFAULT_LINE_COLOR;
50
51
  private fontSize = DEFAULT_FONT_SIZE;
52
+ private debugEnabled = false;
51
53
  private renderSeq = 0;
52
54
  private readonly numericProps = new Set(["thickness", "gap", "fontSize"]);
53
55
  private specs: RenderObjectSpec[] = [];
@@ -112,7 +114,14 @@ export class RulerTool implements Extension {
112
114
  this.syncConfig(configService);
113
115
  configService.onAnyChange((e: { key: string; value: any }) => {
114
116
  let shouldUpdate = false;
115
- if (e.key.startsWith("ruler.")) {
117
+ if (e.key === RULER_DEBUG_KEY) {
118
+ this.debugEnabled = e.value === true;
119
+ this.log("config:update", {
120
+ key: e.key,
121
+ raw: e.value,
122
+ normalized: this.debugEnabled,
123
+ });
124
+ } else if (e.key.startsWith("ruler.")) {
116
125
  const prop = e.key.split(".")[1];
117
126
  if (prop && prop in this) {
118
127
  if (this.numericProps.has(prop)) {
@@ -203,6 +212,12 @@ export class RulerTool implements Extension {
203
212
  max: RULER_FONT_SIZE_MAX,
204
213
  default: DEFAULT_FONT_SIZE,
205
214
  },
215
+ {
216
+ id: RULER_DEBUG_KEY,
217
+ type: "boolean",
218
+ label: "Ruler Debug Log",
219
+ default: false,
220
+ },
206
221
  ] as ConfigurationContribution[],
207
222
  [ContributionPointIds.COMMANDS]: [
208
223
  {
@@ -249,7 +264,12 @@ export class RulerTool implements Extension {
249
264
  };
250
265
  }
251
266
 
267
+ private isDebugEnabled(): boolean {
268
+ return this.debugEnabled;
269
+ }
270
+
252
271
  private log(step: string, payload?: Record<string, unknown>) {
272
+ if (!this.isDebugEnabled()) return;
253
273
  if (payload) {
254
274
  console.debug(`[RulerTool] ${step}`, payload);
255
275
  return;
@@ -279,6 +299,8 @@ export class RulerTool implements Extension {
279
299
  configService.get("ruler.fontSize", this.fontSize),
280
300
  DEFAULT_FONT_SIZE,
281
301
  );
302
+ this.debugEnabled =
303
+ configService.get(RULER_DEBUG_KEY, this.debugEnabled) === true;
282
304
 
283
305
  this.log("config:loaded", {
284
306
  thickness: this.thickness,
package/tests/run.ts CHANGED
@@ -21,6 +21,10 @@ import { createWhiteInkCommands } from "../src/extensions/white-ink/commands";
21
21
  import { createWhiteInkConfigurations } from "../src/extensions/white-ink/config";
22
22
  import { createDielineCommands } from "../src/extensions/dieline/commands";
23
23
  import { createDielineConfigurations } from "../src/extensions/dieline/config";
24
+ import {
25
+ normalizePointInGeometry,
26
+ resolveFeaturePosition,
27
+ } from "../src/extensions/featureCoordinates";
24
28
 
25
29
  function assert(condition: unknown, message: string) {
26
30
  if (!condition) throw new Error(message);
@@ -120,6 +124,38 @@ function testEdgeScale() {
120
124
  assert(height === 80, `expected height 80, got ${height}`);
121
125
  }
122
126
 
127
+ function testFeaturePlacementProjection() {
128
+ const trimGeometry = {
129
+ x: 100,
130
+ y: 120,
131
+ width: 120,
132
+ height: 180,
133
+ };
134
+ const cutGeometry = {
135
+ x: 100,
136
+ y: 120,
137
+ width: 150,
138
+ height: 210,
139
+ };
140
+ const trimFeature = {
141
+ x: 0.82,
142
+ y: 0.68,
143
+ };
144
+
145
+ const trimCenter = resolveFeaturePosition(trimFeature, trimGeometry);
146
+ const cutFeature = normalizePointInGeometry(trimCenter, cutGeometry);
147
+ const cutCenter = resolveFeaturePosition(cutFeature, cutGeometry);
148
+
149
+ assert(
150
+ Math.abs(trimCenter.x - cutCenter.x) < 1e-6,
151
+ `expected projected feature x to stay fixed, got ${trimCenter.x} vs ${cutCenter.x}`,
152
+ );
153
+ assert(
154
+ Math.abs(trimCenter.y - cutCenter.y) < 1e-6,
155
+ `expected projected feature y to stay fixed, got ${trimCenter.y} vs ${cutCenter.y}`,
156
+ );
157
+ }
158
+
123
159
  function testVisibilityDsl() {
124
160
  const layers = new Map([
125
161
  ["ruler-overlay", { exists: true, objectCount: 2 }],
@@ -396,6 +432,7 @@ function main() {
396
432
  testBridgeSelection();
397
433
  testMaskOps();
398
434
  testEdgeScale();
435
+ testFeaturePlacementProjection();
399
436
  testVisibilityDsl();
400
437
  testContributionCompatibility();
401
438
  console.log("ok");