@fieldnotes/core 0.37.0 → 0.38.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/dist/index.js CHANGED
@@ -6039,6 +6039,32 @@ var Viewport = class {
6039
6039
  this.requestRender();
6040
6040
  return el.id;
6041
6041
  }
6042
+ addShape(opts = {}) {
6043
+ const size = opts.size ?? { w: 100, h: 100 };
6044
+ const position = opts.position ?? this.centeredPosition(size);
6045
+ const shape = createShape({
6046
+ position,
6047
+ size,
6048
+ shape: opts.shape,
6049
+ strokeColor: opts.strokeColor,
6050
+ strokeWidth: opts.strokeWidth,
6051
+ fillColor: opts.fillColor,
6052
+ layerId: this.layerManager.activeLayerId
6053
+ });
6054
+ this.historyRecorder.begin();
6055
+ this.store.add(shape);
6056
+ this.historyRecorder.commit();
6057
+ this.getSelectTool()?.setSelection([shape.id]);
6058
+ this.requestRender();
6059
+ return shape.id;
6060
+ }
6061
+ centeredPosition(size) {
6062
+ const c = this.camera.screenToWorld({
6063
+ x: this.wrapper.clientWidth / 2,
6064
+ y: this.wrapper.clientHeight / 2
6065
+ });
6066
+ return { x: c.x - size.w / 2, y: c.y - size.h / 2 };
6067
+ }
6042
6068
  removeLayer(id) {
6043
6069
  this.historyRecorder.begin();
6044
6070
  this.layerManager.removeLayer(id);
@@ -7088,14 +7114,11 @@ function computeSnapGuides(moving, targets, threshold) {
7088
7114
  return { dx: xSnap?.delta ?? 0, dy: ySnap?.delta ?? 0, guides };
7089
7115
  }
7090
7116
 
7091
- // src/tools/select-tool.ts
7117
+ // src/tools/select-overlay.ts
7092
7118
  var HANDLE_SIZE = 8;
7093
- var SNAP_PX = 6;
7094
7119
  var HANDLE_HIT_PADDING2 = 4;
7095
7120
  var SELECTION_PAD = 4;
7096
- var MIN_ELEMENT_SIZE = 20;
7097
7121
  var ROTATE_HANDLE_OFFSET = 24;
7098
- var ROTATE_SNAP = Math.PI / 12;
7099
7122
  var ROTATABLE_TYPES = /* @__PURE__ */ new Set(["note", "text", "image", "html", "shape", "stroke"]);
7100
7123
  var HANDLE_CURSORS = {
7101
7124
  nw: "nwse-resize",
@@ -7103,6 +7126,486 @@ var HANDLE_CURSORS = {
7103
7126
  ne: "nesw-resize",
7104
7127
  sw: "nesw-resize"
7105
7128
  };
7129
+ function getOverlayLayout(el, zoom) {
7130
+ const bounds = getElementBounds(el);
7131
+ if (!bounds) return null;
7132
+ const angle = el.rotation ?? 0;
7133
+ const pad = SELECTION_PAD / zoom;
7134
+ const center2 = { x: bounds.x + bounds.w / 2, y: bounds.y + bounds.h / 2 };
7135
+ const raw = [
7136
+ ["nw", { x: bounds.x - pad, y: bounds.y - pad }],
7137
+ ["ne", { x: bounds.x + bounds.w + pad, y: bounds.y - pad }],
7138
+ ["sw", { x: bounds.x - pad, y: bounds.y + bounds.h + pad }],
7139
+ ["se", { x: bounds.x + bounds.w + pad, y: bounds.y + bounds.h + pad }]
7140
+ ];
7141
+ const corners = raw.map(
7142
+ ([h, p]) => [h, rotatePoint(p, center2, angle)]
7143
+ );
7144
+ const topMid = { x: center2.x, y: bounds.y - pad - ROTATE_HANDLE_OFFSET / zoom };
7145
+ const rotateHandle = rotatePoint(topMid, center2, angle);
7146
+ return { center: center2, corners, rotateHandle, angle };
7147
+ }
7148
+ function getHandlePositions(bounds) {
7149
+ return [
7150
+ ["nw", { x: bounds.x, y: bounds.y }],
7151
+ ["ne", { x: bounds.x + bounds.w, y: bounds.y }],
7152
+ ["sw", { x: bounds.x, y: bounds.y + bounds.h }],
7153
+ ["se", { x: bounds.x + bounds.w, y: bounds.y + bounds.h }]
7154
+ ];
7155
+ }
7156
+ function topMidpoint(layout) {
7157
+ const nw = layout.corners.find(([h]) => h === "nw")?.[1] ?? { x: 0, y: 0 };
7158
+ const ne = layout.corners.find(([h]) => h === "ne")?.[1] ?? { x: 0, y: 0 };
7159
+ return { x: (nw.x + ne.x) / 2, y: (nw.y + ne.y) / 2 };
7160
+ }
7161
+ function drawLockBadge(ctx, at, zoom) {
7162
+ const r = 9 / zoom;
7163
+ ctx.save();
7164
+ ctx.setLineDash([]);
7165
+ ctx.beginPath();
7166
+ ctx.arc(at.x, at.y, r, 0, Math.PI * 2);
7167
+ ctx.fillStyle = "#ffffff";
7168
+ ctx.fill();
7169
+ ctx.strokeStyle = "#2196F3";
7170
+ ctx.lineWidth = 1.5 / zoom;
7171
+ ctx.stroke();
7172
+ const bw = 8 / zoom;
7173
+ const bh = 6 / zoom;
7174
+ ctx.fillStyle = "#2196F3";
7175
+ ctx.fillRect(at.x - bw / 2, at.y - bh / 2 + 1 / zoom, bw, bh);
7176
+ ctx.beginPath();
7177
+ ctx.arc(at.x, at.y - bh / 2 + 1 / zoom, 2.5 / zoom, Math.PI, 0);
7178
+ ctx.lineWidth = 1.4 / zoom;
7179
+ ctx.stroke();
7180
+ ctx.restore();
7181
+ }
7182
+ function renderMarquee(ctx, rect) {
7183
+ ctx.save();
7184
+ ctx.strokeStyle = "#2196F3";
7185
+ ctx.fillStyle = "rgba(33, 150, 243, 0.08)";
7186
+ ctx.lineWidth = 1;
7187
+ ctx.setLineDash([4, 4]);
7188
+ ctx.strokeRect(rect.x, rect.y, rect.w, rect.h);
7189
+ ctx.fillRect(rect.x, rect.y, rect.w, rect.h);
7190
+ ctx.restore();
7191
+ }
7192
+ function renderBindingHighlights(ctx, arrow, zoom, store) {
7193
+ if (!arrow.fromBinding && !arrow.toBinding) return;
7194
+ const pad = SELECTION_PAD / zoom;
7195
+ ctx.save();
7196
+ ctx.strokeStyle = "#2196F3";
7197
+ ctx.lineWidth = 2 / zoom;
7198
+ ctx.setLineDash([]);
7199
+ const drawn = /* @__PURE__ */ new Set();
7200
+ for (const binding of [arrow.fromBinding, arrow.toBinding]) {
7201
+ if (!binding || drawn.has(binding.elementId)) continue;
7202
+ drawn.add(binding.elementId);
7203
+ const target = store.getById(binding.elementId);
7204
+ if (!target) continue;
7205
+ const bounds = getElementBounds(target);
7206
+ if (!bounds) continue;
7207
+ ctx.strokeRect(bounds.x - pad, bounds.y - pad, bounds.w + pad * 2, bounds.h + pad * 2);
7208
+ }
7209
+ ctx.restore();
7210
+ }
7211
+ function renderSelectionBoxes(ctx, p) {
7212
+ if (p.selectedIds.length === 0) return;
7213
+ const zoom = p.zoom;
7214
+ const handleWorldSize = HANDLE_SIZE / zoom;
7215
+ ctx.save();
7216
+ ctx.strokeStyle = "#2196F3";
7217
+ ctx.lineWidth = 1.5 / zoom;
7218
+ ctx.setLineDash([4 / zoom, 4 / zoom]);
7219
+ for (const id of p.selectedIds) {
7220
+ const el = p.store.getById(id);
7221
+ if (!el) continue;
7222
+ if (el.type === "arrow") {
7223
+ renderArrowHandles(ctx, el, zoom);
7224
+ renderBindingHighlights(ctx, el, zoom, p.store);
7225
+ continue;
7226
+ }
7227
+ if (el.type === "shape" && el.shape === "line") {
7228
+ ctx.setLineDash([]);
7229
+ ctx.fillStyle = "#ffffff";
7230
+ const r = handleWorldSize / 2;
7231
+ for (const pt of lineEndpoints(el)) {
7232
+ ctx.beginPath();
7233
+ ctx.arc(pt.x, pt.y, r, 0, Math.PI * 2);
7234
+ ctx.fill();
7235
+ ctx.stroke();
7236
+ }
7237
+ ctx.setLineDash([4 / zoom, 4 / zoom]);
7238
+ continue;
7239
+ }
7240
+ const bounds = getElementBounds(el);
7241
+ if (!bounds) continue;
7242
+ const layout = getOverlayLayout(el, zoom);
7243
+ if (!layout) continue;
7244
+ const pad = SELECTION_PAD / zoom;
7245
+ if (layout.angle === 0) {
7246
+ ctx.strokeRect(bounds.x - pad, bounds.y - pad, bounds.w + pad * 2, bounds.h + pad * 2);
7247
+ } else {
7248
+ const ordered = ["nw", "ne", "se", "sw"].map((h) => layout.corners.find(([c]) => c === h)?.[1]).filter((pp) => !!pp);
7249
+ const [p0, ...others] = ordered;
7250
+ if (p0) {
7251
+ ctx.beginPath();
7252
+ ctx.moveTo(p0.x, p0.y);
7253
+ for (const pp of others) ctx.lineTo(pp.x, pp.y);
7254
+ ctx.closePath();
7255
+ ctx.stroke();
7256
+ }
7257
+ }
7258
+ if (!el.locked) {
7259
+ if ("size" in el) {
7260
+ ctx.setLineDash([]);
7261
+ ctx.fillStyle = "#ffffff";
7262
+ const corners = layout.angle === 0 ? getHandlePositions(bounds) : layout.corners;
7263
+ for (const [, pos] of corners) {
7264
+ ctx.fillRect(
7265
+ pos.x - handleWorldSize / 2,
7266
+ pos.y - handleWorldSize / 2,
7267
+ handleWorldSize,
7268
+ handleWorldSize
7269
+ );
7270
+ ctx.strokeRect(
7271
+ pos.x - handleWorldSize / 2,
7272
+ pos.y - handleWorldSize / 2,
7273
+ handleWorldSize,
7274
+ handleWorldSize
7275
+ );
7276
+ }
7277
+ ctx.setLineDash([4 / zoom, 4 / zoom]);
7278
+ } else if (el.type === "template") {
7279
+ ctx.setLineDash([]);
7280
+ ctx.fillStyle = "#ffffff";
7281
+ const hx = bounds.x + bounds.w;
7282
+ const hy = bounds.y + bounds.h;
7283
+ ctx.fillRect(
7284
+ hx - handleWorldSize / 2,
7285
+ hy - handleWorldSize / 2,
7286
+ handleWorldSize,
7287
+ handleWorldSize
7288
+ );
7289
+ ctx.strokeRect(
7290
+ hx - handleWorldSize / 2,
7291
+ hy - handleWorldSize / 2,
7292
+ handleWorldSize,
7293
+ handleWorldSize
7294
+ );
7295
+ ctx.setLineDash([4 / zoom, 4 / zoom]);
7296
+ }
7297
+ if (p.selectedIds.length === 1 && ROTATABLE_TYPES.has(el.type)) {
7298
+ const stemStart = topMidpoint(layout);
7299
+ const stemEnd = layout.rotateHandle;
7300
+ ctx.beginPath();
7301
+ ctx.moveTo(stemStart.x, stemStart.y);
7302
+ ctx.lineTo(stemEnd.x, stemEnd.y);
7303
+ ctx.stroke();
7304
+ ctx.setLineDash([]);
7305
+ ctx.fillStyle = "#ffffff";
7306
+ ctx.beginPath();
7307
+ ctx.arc(stemEnd.x, stemEnd.y, handleWorldSize / 2, 0, Math.PI * 2);
7308
+ ctx.fill();
7309
+ ctx.stroke();
7310
+ ctx.setLineDash([4 / zoom, 4 / zoom]);
7311
+ }
7312
+ }
7313
+ if (el.locked) {
7314
+ const ne = layout.corners.find(([h]) => h === "ne")?.[1];
7315
+ if (ne) drawLockBadge(ctx, ne, zoom);
7316
+ }
7317
+ }
7318
+ ctx.restore();
7319
+ }
7320
+ function renderGuideLines(ctx, p) {
7321
+ const zoom = p.zoom;
7322
+ const rect = p.rect;
7323
+ ctx.save();
7324
+ ctx.strokeStyle = "#FF4081";
7325
+ ctx.lineWidth = 1 / zoom;
7326
+ ctx.setLineDash([]);
7327
+ for (const g of p.guides) {
7328
+ ctx.beginPath();
7329
+ if (g.axis === "x") {
7330
+ const y0 = rect ? rect.y : p.currentWorld.y - 1e5;
7331
+ const y1 = rect ? rect.y + rect.h : p.currentWorld.y + 1e5;
7332
+ ctx.moveTo(g.position, y0);
7333
+ ctx.lineTo(g.position, y1);
7334
+ } else {
7335
+ const x0 = rect ? rect.x : p.currentWorld.x - 1e5;
7336
+ const x1 = rect ? rect.x + rect.w : p.currentWorld.x + 1e5;
7337
+ ctx.moveTo(x0, g.position);
7338
+ ctx.lineTo(x1, g.position);
7339
+ }
7340
+ ctx.stroke();
7341
+ }
7342
+ ctx.restore();
7343
+ }
7344
+
7345
+ // src/tools/select-hit.ts
7346
+ function hitTest(world, ctx) {
7347
+ const r = 10;
7348
+ const candidates = ctx.store.queryRect({ x: world.x - r, y: world.y - r, w: r * 2, h: r * 2 }).reverse();
7349
+ for (const el of candidates) {
7350
+ if (ctx.isLayerVisible && !ctx.isLayerVisible(el.layerId)) continue;
7351
+ if (ctx.isLayerLocked && ctx.isLayerLocked(el.layerId)) continue;
7352
+ if (el.type === "grid") continue;
7353
+ if (isInsideBounds(world, el)) return el;
7354
+ }
7355
+ return null;
7356
+ }
7357
+ function isInsideBounds(point, el) {
7358
+ if (el.type === "grid") return false;
7359
+ const angle = el.rotation ?? 0;
7360
+ if (angle !== 0) {
7361
+ const b = getElementBounds(el);
7362
+ if (b) {
7363
+ point = rotatePoint(point, { x: b.x + b.w / 2, y: b.y + b.h / 2 }, -angle);
7364
+ }
7365
+ }
7366
+ if (el.type === "shape" && el.shape === "line") {
7367
+ const [a, b] = lineEndpoints(el);
7368
+ const threshold = Math.max(el.strokeWidth / 2, 6);
7369
+ return distSqToSegment(point, a, b) <= threshold * threshold;
7370
+ }
7371
+ if ("size" in el) {
7372
+ const s = el.size;
7373
+ return point.x >= el.position.x && point.x <= el.position.x + s.w && point.y >= el.position.y && point.y <= el.position.y + s.h;
7374
+ }
7375
+ if (el.type === "stroke") {
7376
+ return hitTestStroke(el, point, 10);
7377
+ }
7378
+ if (el.type === "arrow") {
7379
+ return isNearBezier(point, el.from, el.to, el.bend, 10);
7380
+ }
7381
+ if (el.type === "template") {
7382
+ const bounds = getElementBounds(el);
7383
+ if (!bounds) return false;
7384
+ return point.x >= bounds.x && point.x <= bounds.x + bounds.w && point.y >= bounds.y && point.y <= bounds.y + bounds.h;
7385
+ }
7386
+ return false;
7387
+ }
7388
+ function hitTestResizeHandle(world, ctx, selectedIds) {
7389
+ if (selectedIds.length === 0) return null;
7390
+ const zoom = ctx.camera.zoom;
7391
+ const handleHalf = (HANDLE_SIZE / 2 + HANDLE_HIT_PADDING2) / zoom;
7392
+ for (const id of selectedIds) {
7393
+ const el = ctx.store.getById(id);
7394
+ if (!el || !("size" in el)) continue;
7395
+ if (el.locked) continue;
7396
+ if (el.type === "shape" && el.shape === "line") continue;
7397
+ const layout = getOverlayLayout(el, zoom);
7398
+ if (!layout) continue;
7399
+ for (const [handle, pos] of layout.corners) {
7400
+ if (Math.abs(world.x - pos.x) <= handleHalf && Math.abs(world.y - pos.y) <= handleHalf) {
7401
+ return { elementId: id, handle };
7402
+ }
7403
+ }
7404
+ }
7405
+ return null;
7406
+ }
7407
+ function hitTestRotateHandle(world, ctx, selectedIds) {
7408
+ if (selectedIds.length !== 1) return null;
7409
+ const id = selectedIds[0];
7410
+ if (!id) return null;
7411
+ const el = ctx.store.getById(id);
7412
+ if (!el || el.locked || !ROTATABLE_TYPES.has(el.type)) return null;
7413
+ const layout = getOverlayLayout(el, ctx.camera.zoom);
7414
+ if (!layout) return null;
7415
+ const r = (HANDLE_SIZE / 2 + HANDLE_HIT_PADDING2) / ctx.camera.zoom;
7416
+ const dx = world.x - layout.rotateHandle.x;
7417
+ const dy = world.y - layout.rotateHandle.y;
7418
+ return dx * dx + dy * dy <= r * r ? { elementId: id } : null;
7419
+ }
7420
+ function hitTestLineHandles(world, ctx, selectedIds) {
7421
+ if (selectedIds.length === 0) return null;
7422
+ const zoom = ctx.camera.zoom;
7423
+ const r = (HANDLE_SIZE / 2 + HANDLE_HIT_PADDING2) / zoom;
7424
+ const r2 = r * r;
7425
+ for (const id of selectedIds) {
7426
+ const el = ctx.store.getById(id);
7427
+ if (!el || el.type !== "shape" || el.shape !== "line") continue;
7428
+ const [a, b] = lineEndpoints(el);
7429
+ if ((world.x - a.x) ** 2 + (world.y - a.y) ** 2 <= r2) return { elementId: id, fixed: b };
7430
+ if ((world.x - b.x) ** 2 + (world.y - b.y) ** 2 <= r2) return { elementId: id, fixed: a };
7431
+ }
7432
+ return null;
7433
+ }
7434
+ function hitTestTemplateResizeHandle(world, ctx, selectedIds) {
7435
+ if (selectedIds.length === 0) return null;
7436
+ const zoom = ctx.camera.zoom;
7437
+ const handleHalf = (HANDLE_SIZE / 2 + HANDLE_HIT_PADDING2) / zoom;
7438
+ for (const id of selectedIds) {
7439
+ const el = ctx.store.getById(id);
7440
+ if (!el || el.type !== "template") continue;
7441
+ const bounds = getElementBounds(el);
7442
+ if (!bounds) continue;
7443
+ const hx = bounds.x + bounds.w;
7444
+ const hy = bounds.y + bounds.h;
7445
+ if (Math.abs(world.x - hx) <= handleHalf && Math.abs(world.y - hy) <= handleHalf) {
7446
+ return id;
7447
+ }
7448
+ }
7449
+ return null;
7450
+ }
7451
+ function findElementsInRect(marquee, ctx) {
7452
+ const candidates = ctx.store.queryRect(marquee);
7453
+ const ids = [];
7454
+ for (const el of candidates) {
7455
+ if (ctx.isLayerVisible && !ctx.isLayerVisible(el.layerId)) continue;
7456
+ if (ctx.isLayerLocked && ctx.isLayerLocked(el.layerId)) continue;
7457
+ if (el.type === "grid") continue;
7458
+ const bounds = getElementBounds(el);
7459
+ if (bounds && rectsOverlap(marquee, rotatedAABB(bounds, el.rotation ?? 0))) {
7460
+ ids.push(el.id);
7461
+ }
7462
+ }
7463
+ return ids;
7464
+ }
7465
+ function rectsOverlap(a, b) {
7466
+ return a.x <= b.x + b.w && a.x + a.w >= b.x && a.y <= b.y + b.h && a.y + a.h >= b.y;
7467
+ }
7468
+
7469
+ // src/tools/select-resize.ts
7470
+ var MIN_ELEMENT_SIZE = 20;
7471
+ function anchorOffset(handle, w, h) {
7472
+ switch (handle) {
7473
+ case "se":
7474
+ return { x: -w / 2, y: -h / 2 };
7475
+ case "sw":
7476
+ return { x: w / 2, y: -h / 2 };
7477
+ case "ne":
7478
+ return { x: -w / 2, y: h / 2 };
7479
+ case "nw":
7480
+ return { x: w / 2, y: h / 2 };
7481
+ default:
7482
+ return { x: 0, y: 0 };
7483
+ }
7484
+ }
7485
+ function computeResize(el, handle, world, lastWorld, aspectRatio, shiftKey) {
7486
+ const dx = world.x - lastWorld.x;
7487
+ const dy = world.y - lastWorld.y;
7488
+ let { x, y, w, h } = { x: el.position.x, y: el.position.y, w: el.size.w, h: el.size.h };
7489
+ switch (handle) {
7490
+ case "se":
7491
+ w += dx;
7492
+ h += dy;
7493
+ break;
7494
+ case "sw":
7495
+ x += dx;
7496
+ w -= dx;
7497
+ h += dy;
7498
+ break;
7499
+ case "ne":
7500
+ y += dy;
7501
+ w += dx;
7502
+ h -= dy;
7503
+ break;
7504
+ case "nw":
7505
+ x += dx;
7506
+ y += dy;
7507
+ w -= dx;
7508
+ h -= dy;
7509
+ break;
7510
+ }
7511
+ if (shiftKey && aspectRatio > 0) {
7512
+ const absDw = Math.abs(w - el.size.w);
7513
+ const absDh = Math.abs(h - el.size.h);
7514
+ if (absDw >= absDh) {
7515
+ h = w / aspectRatio;
7516
+ } else {
7517
+ w = h * aspectRatio;
7518
+ }
7519
+ if (handle === "nw" || handle === "sw") {
7520
+ x = el.position.x + el.size.w - w;
7521
+ }
7522
+ if (handle === "nw" || handle === "ne") {
7523
+ y = el.position.y + el.size.h - h;
7524
+ }
7525
+ }
7526
+ if (w < MIN_ELEMENT_SIZE) {
7527
+ if (handle === "nw" || handle === "sw") x = el.position.x + el.size.w - MIN_ELEMENT_SIZE;
7528
+ w = MIN_ELEMENT_SIZE;
7529
+ }
7530
+ if (h < MIN_ELEMENT_SIZE) {
7531
+ if (handle === "nw" || handle === "ne") y = el.position.y + el.size.h - MIN_ELEMENT_SIZE;
7532
+ h = MIN_ELEMENT_SIZE;
7533
+ }
7534
+ return { position: { x, y }, size: { w, h } };
7535
+ }
7536
+ function computeRotatedResize(el, handle, angle, world, lastWorld, aspectRatio, shiftKey) {
7537
+ const wdx = world.x - lastWorld.x;
7538
+ const wdy = world.y - lastWorld.y;
7539
+ const cosN = Math.cos(-angle);
7540
+ const sinN = Math.sin(-angle);
7541
+ const ldx = wdx * cosN - wdy * sinN;
7542
+ const ldy = wdx * sinN + wdy * cosN;
7543
+ let w = el.size.w;
7544
+ let h = el.size.h;
7545
+ switch (handle) {
7546
+ case "se":
7547
+ w += ldx;
7548
+ h += ldy;
7549
+ break;
7550
+ case "sw":
7551
+ w -= ldx;
7552
+ h += ldy;
7553
+ break;
7554
+ case "ne":
7555
+ w += ldx;
7556
+ h -= ldy;
7557
+ break;
7558
+ case "nw":
7559
+ w -= ldx;
7560
+ h -= ldy;
7561
+ break;
7562
+ }
7563
+ if (shiftKey && aspectRatio > 0) {
7564
+ const absDw = Math.abs(w - el.size.w);
7565
+ const absDh = Math.abs(h - el.size.h);
7566
+ if (absDw >= absDh) h = w / aspectRatio;
7567
+ else w = h * aspectRatio;
7568
+ }
7569
+ w = Math.max(w, MIN_ELEMENT_SIZE);
7570
+ h = Math.max(h, MIN_ELEMENT_SIZE);
7571
+ const oldCenter = { x: el.position.x + el.size.w / 2, y: el.position.y + el.size.h / 2 };
7572
+ const oldAnchorLocal = anchorOffset(handle, el.size.w, el.size.h);
7573
+ const anchorWorld = rotatePoint(
7574
+ { x: oldCenter.x + oldAnchorLocal.x, y: oldCenter.y + oldAnchorLocal.y },
7575
+ oldCenter,
7576
+ angle
7577
+ );
7578
+ const newAnchorLocal = anchorOffset(handle, w, h);
7579
+ const cos = Math.cos(angle);
7580
+ const sin = Math.sin(angle);
7581
+ const rotatedAnchor = {
7582
+ x: newAnchorLocal.x * cos - newAnchorLocal.y * sin,
7583
+ y: newAnchorLocal.x * sin + newAnchorLocal.y * cos
7584
+ };
7585
+ const newCenter = { x: anchorWorld.x - rotatedAnchor.x, y: anchorWorld.y - rotatedAnchor.y };
7586
+ const position = { x: newCenter.x - w / 2, y: newCenter.y - h / 2 };
7587
+ return { position, size: { w, h } };
7588
+ }
7589
+ function computeTemplateResize(el, world, opts) {
7590
+ const dx = world.x - el.position.x;
7591
+ const dy = world.y - el.position.y;
7592
+ let newRadius = Math.sqrt(dx * dx + dy * dy);
7593
+ if (opts.snapToGrid && opts.gridSize && opts.gridSize > 0) {
7594
+ const snapUnit = opts.gridType === "hex" ? Math.sqrt(3) * opts.gridSize : opts.gridSize;
7595
+ newRadius = Math.max(snapUnit, Math.round(newRadius / snapUnit) * snapUnit);
7596
+ }
7597
+ newRadius = Math.max(MIN_ELEMENT_SIZE, newRadius);
7598
+ const updates = { radius: newRadius };
7599
+ if (el.feetPerCell != null && opts.gridSize && opts.gridSize > 0) {
7600
+ const snapUnit = opts.gridType === "hex" ? Math.sqrt(3) * opts.gridSize : opts.gridSize;
7601
+ updates.radiusFeet = newRadius / snapUnit * el.feetPerCell;
7602
+ }
7603
+ return updates;
7604
+ }
7605
+
7606
+ // src/tools/select-tool.ts
7607
+ var SNAP_PX = 6;
7608
+ var ROTATE_SNAP = Math.PI / 12;
7106
7609
  var SelectTool = class {
7107
7610
  name = "select";
7108
7611
  _selectedIds = [];
@@ -7138,7 +7641,7 @@ var SelectTool = class {
7138
7641
  this.ctx?.requestRender();
7139
7642
  }
7140
7643
  selectAtPoint(world, ctx) {
7141
- const hit = this.hitTest(world, ctx);
7644
+ const hit = hitTest(world, ctx);
7142
7645
  if (!hit) {
7143
7646
  this.setSelectedIds([]);
7144
7647
  return;
@@ -7182,19 +7685,19 @@ var SelectTool = class {
7182
7685
  ctx.requestRender();
7183
7686
  return;
7184
7687
  }
7185
- const lineHit = this.hitTestLineHandles(world, ctx);
7688
+ const lineHit = hitTestLineHandles(world, ctx, this._selectedIds);
7186
7689
  if (lineHit) {
7187
7690
  this.mode = { type: "line-handle", elementId: lineHit.elementId, fixed: lineHit.fixed };
7188
7691
  ctx.requestRender();
7189
7692
  return;
7190
7693
  }
7191
- const templateResizeHit = this.hitTestTemplateResizeHandle(world, ctx);
7694
+ const templateResizeHit = hitTestTemplateResizeHandle(world, ctx, this._selectedIds);
7192
7695
  if (templateResizeHit) {
7193
7696
  this.mode = { type: "resizing-template", elementId: templateResizeHit };
7194
7697
  ctx.requestRender();
7195
7698
  return;
7196
7699
  }
7197
- const rotateHit = this.hitTestRotateHandle(world, ctx);
7700
+ const rotateHit = hitTestRotateHandle(world, ctx, this._selectedIds);
7198
7701
  if (rotateHit) {
7199
7702
  const el = ctx.store.getById(rotateHit.elementId);
7200
7703
  const layout = el ? this.getOverlayLayout(el, ctx.camera.zoom) : null;
@@ -7210,7 +7713,7 @@ var SelectTool = class {
7210
7713
  return;
7211
7714
  }
7212
7715
  }
7213
- const resizeHit = this.hitTestResizeHandle(world, ctx);
7716
+ const resizeHit = hitTestResizeHandle(world, ctx, this._selectedIds);
7214
7717
  if (resizeHit) {
7215
7718
  const el = ctx.store.getById(resizeHit.elementId);
7216
7719
  if (el && "size" in el) {
@@ -7226,7 +7729,7 @@ var SelectTool = class {
7226
7729
  }
7227
7730
  this.pendingSingleSelectId = null;
7228
7731
  this.hasDragged = false;
7229
- const hit = this.hitTest(world, ctx);
7732
+ const hit = hitTest(world, ctx);
7230
7733
  if (hit) {
7231
7734
  const all = ctx.store.getAll();
7232
7735
  const alreadySelected = this._selectedIds.includes(hit.id);
@@ -7364,7 +7867,7 @@ var SelectTool = class {
7364
7867
  if (this.mode.type === "marquee") {
7365
7868
  const rect = this.getMarqueeRect();
7366
7869
  if (rect) {
7367
- this.setSelectedIds(expandToGroups(this.findElementsInRect(rect, ctx), ctx.store.getAll()));
7870
+ this.setSelectedIds(expandToGroups(findElementsInRect(rect, ctx), ctx.store.getAll()));
7368
7871
  }
7369
7872
  ctx.requestRender();
7370
7873
  }
@@ -7391,8 +7894,16 @@ var SelectTool = class {
7391
7894
  this.setHovered(hoverId, ctx);
7392
7895
  }
7393
7896
  renderOverlay(canvasCtx) {
7394
- this.renderMarquee(canvasCtx);
7395
- this.renderSelectionBoxes(canvasCtx);
7897
+ if (this.mode.type === "marquee") {
7898
+ const rect = this.getMarqueeRect();
7899
+ if (rect) renderMarquee(canvasCtx, rect);
7900
+ }
7901
+ if (this.ctx)
7902
+ renderSelectionBoxes(canvasCtx, {
7903
+ selectedIds: this._selectedIds,
7904
+ store: this.ctx.store,
7905
+ zoom: this.ctx.camera.zoom
7906
+ });
7396
7907
  if (this.mode.type === "arrow-handle" && this.ctx) {
7397
7908
  const target = getArrowHandleDragTarget(
7398
7909
  this.mode.handle,
@@ -7426,32 +7937,13 @@ var SelectTool = class {
7426
7937
  }
7427
7938
  }
7428
7939
  }
7429
- this.renderGuideLines(canvasCtx);
7430
- }
7431
- renderGuideLines(canvasCtx) {
7432
- if (this.mode.type !== "dragging" || !this.ctx || this.activeGuides.length === 0) return;
7433
- const zoom = this.ctx.camera.zoom;
7434
- const rect = this.dragVisibleRect;
7435
- canvasCtx.save();
7436
- canvasCtx.strokeStyle = "#FF4081";
7437
- canvasCtx.lineWidth = 1 / zoom;
7438
- canvasCtx.setLineDash([]);
7439
- for (const g of this.activeGuides) {
7440
- canvasCtx.beginPath();
7441
- if (g.axis === "x") {
7442
- const y0 = rect ? rect.y : this.currentWorld.y - 1e5;
7443
- const y1 = rect ? rect.y + rect.h : this.currentWorld.y + 1e5;
7444
- canvasCtx.moveTo(g.position, y0);
7445
- canvasCtx.lineTo(g.position, y1);
7446
- } else {
7447
- const x0 = rect ? rect.x : this.currentWorld.x - 1e5;
7448
- const x1 = rect ? rect.x + rect.w : this.currentWorld.x + 1e5;
7449
- canvasCtx.moveTo(x0, g.position);
7450
- canvasCtx.lineTo(x1, g.position);
7451
- }
7452
- canvasCtx.stroke();
7453
- }
7454
- canvasCtx.restore();
7940
+ if (this.mode.type === "dragging" && this.ctx && this.activeGuides.length)
7941
+ renderGuideLines(canvasCtx, {
7942
+ guides: this.activeGuides,
7943
+ rect: this.dragVisibleRect,
7944
+ currentWorld: this.currentWorld,
7945
+ zoom: this.ctx.camera.zoom
7946
+ });
7455
7947
  }
7456
7948
  updateArrowsBoundTo(ids, ctx) {
7457
7949
  updateArrowsBoundToElements(ids, ctx.store);
@@ -7477,25 +7969,25 @@ var SelectTool = class {
7477
7969
  ctx.setCursor?.(getArrowHandleCursor(arrowHit.handle, false));
7478
7970
  return null;
7479
7971
  }
7480
- if (this.hitTestLineHandles(world, ctx)) {
7972
+ if (hitTestLineHandles(world, ctx, this._selectedIds)) {
7481
7973
  ctx.setCursor?.("grab");
7482
7974
  return null;
7483
7975
  }
7484
- const templateResizeHit = this.hitTestTemplateResizeHandle(world, ctx);
7976
+ const templateResizeHit = hitTestTemplateResizeHandle(world, ctx, this._selectedIds);
7485
7977
  if (templateResizeHit) {
7486
7978
  ctx.setCursor?.("nwse-resize");
7487
7979
  return null;
7488
7980
  }
7489
- if (this.hitTestRotateHandle(world, ctx)) {
7981
+ if (hitTestRotateHandle(world, ctx, this._selectedIds)) {
7490
7982
  ctx.setCursor?.("grab");
7491
7983
  return null;
7492
7984
  }
7493
- const resizeHit = this.hitTestResizeHandle(world, ctx);
7985
+ const resizeHit = hitTestResizeHandle(world, ctx, this._selectedIds);
7494
7986
  if (resizeHit) {
7495
7987
  ctx.setCursor?.(HANDLE_CURSORS[resizeHit.handle]);
7496
7988
  return null;
7497
7989
  }
7498
- const hit = this.hitTest(world, ctx);
7990
+ const hit = hitTest(world, ctx);
7499
7991
  ctx.setCursor?.(hit ? "move" : "default");
7500
7992
  return hit ? hit.id : null;
7501
7993
  }
@@ -7509,421 +8001,43 @@ var SelectTool = class {
7509
8001
  const el = ctx.store.getById(this.mode.elementId);
7510
8002
  if (!el || !("size" in el) || el.locked) return;
7511
8003
  const angle = el.rotation ?? 0;
7512
- if (angle !== 0) {
7513
- this.handleRotatedResize(world, el, angle, ctx, shiftKey);
7514
- return;
7515
- }
7516
- const { handle } = this.mode;
7517
- const dx = world.x - this.lastWorld.x;
7518
- const dy = world.y - this.lastWorld.y;
7519
- this.lastWorld = world;
7520
- let { x, y, w, h } = { x: el.position.x, y: el.position.y, w: el.size.w, h: el.size.h };
7521
- switch (handle) {
7522
- case "se":
7523
- w += dx;
7524
- h += dy;
7525
- break;
7526
- case "sw":
7527
- x += dx;
7528
- w -= dx;
7529
- h += dy;
7530
- break;
7531
- case "ne":
7532
- y += dy;
7533
- w += dx;
7534
- h -= dy;
7535
- break;
7536
- case "nw":
7537
- x += dx;
7538
- y += dy;
7539
- w -= dx;
7540
- h -= dy;
7541
- break;
7542
- }
7543
- if (shiftKey && this.resizeAspectRatio > 0) {
7544
- const absDw = Math.abs(w - el.size.w);
7545
- const absDh = Math.abs(h - el.size.h);
7546
- if (absDw >= absDh) {
7547
- h = w / this.resizeAspectRatio;
7548
- } else {
7549
- w = h * this.resizeAspectRatio;
7550
- }
7551
- if (handle === "nw" || handle === "sw") {
7552
- x = el.position.x + el.size.w - w;
7553
- }
7554
- if (handle === "nw" || handle === "ne") {
7555
- y = el.position.y + el.size.h - h;
7556
- }
7557
- }
7558
- if (w < MIN_ELEMENT_SIZE) {
7559
- if (handle === "nw" || handle === "sw") x = el.position.x + el.size.w - MIN_ELEMENT_SIZE;
7560
- w = MIN_ELEMENT_SIZE;
7561
- }
7562
- if (h < MIN_ELEMENT_SIZE) {
7563
- if (handle === "nw" || handle === "ne") y = el.position.y + el.size.h - MIN_ELEMENT_SIZE;
7564
- h = MIN_ELEMENT_SIZE;
7565
- }
7566
- ctx.store.update(this.mode.elementId, {
7567
- position: { x, y },
7568
- size: { w, h }
7569
- });
7570
- this.updateArrowsBoundTo([this.mode.elementId], ctx);
7571
- ctx.requestRender();
7572
- }
7573
- anchorOffset(handle, w, h) {
7574
- switch (handle) {
7575
- case "se":
7576
- return { x: -w / 2, y: -h / 2 };
7577
- case "sw":
7578
- return { x: w / 2, y: -h / 2 };
7579
- case "ne":
7580
- return { x: -w / 2, y: h / 2 };
7581
- case "nw":
7582
- return { x: w / 2, y: h / 2 };
7583
- default:
7584
- return { x: 0, y: 0 };
7585
- }
7586
- }
7587
- handleRotatedResize(world, el, angle, ctx, shiftKey) {
7588
- if (this.mode.type !== "resizing") return;
7589
- const { handle } = this.mode;
7590
- const wdx = world.x - this.lastWorld.x;
7591
- const wdy = world.y - this.lastWorld.y;
7592
- this.lastWorld = world;
7593
- const cosN = Math.cos(-angle);
7594
- const sinN = Math.sin(-angle);
7595
- const ldx = wdx * cosN - wdy * sinN;
7596
- const ldy = wdx * sinN + wdy * cosN;
7597
- let w = el.size.w;
7598
- let h = el.size.h;
7599
- switch (handle) {
7600
- case "se":
7601
- w += ldx;
7602
- h += ldy;
7603
- break;
7604
- case "sw":
7605
- w -= ldx;
7606
- h += ldy;
7607
- break;
7608
- case "ne":
7609
- w += ldx;
7610
- h -= ldy;
7611
- break;
7612
- case "nw":
7613
- w -= ldx;
7614
- h -= ldy;
7615
- break;
7616
- }
7617
- if (shiftKey && this.resizeAspectRatio > 0) {
7618
- const absDw = Math.abs(w - el.size.w);
7619
- const absDh = Math.abs(h - el.size.h);
7620
- if (absDw >= absDh) h = w / this.resizeAspectRatio;
7621
- else w = h * this.resizeAspectRatio;
7622
- }
7623
- w = Math.max(w, MIN_ELEMENT_SIZE);
7624
- h = Math.max(h, MIN_ELEMENT_SIZE);
7625
- const oldCenter = { x: el.position.x + el.size.w / 2, y: el.position.y + el.size.h / 2 };
7626
- const oldAnchorLocal = this.anchorOffset(handle, el.size.w, el.size.h);
7627
- const anchorWorld = rotatePoint(
7628
- { x: oldCenter.x + oldAnchorLocal.x, y: oldCenter.y + oldAnchorLocal.y },
7629
- oldCenter,
7630
- angle
8004
+ const patch = angle !== 0 ? computeRotatedResize(
8005
+ el,
8006
+ this.mode.handle,
8007
+ angle,
8008
+ world,
8009
+ this.lastWorld,
8010
+ this.resizeAspectRatio,
8011
+ shiftKey
8012
+ ) : computeResize(
8013
+ el,
8014
+ this.mode.handle,
8015
+ world,
8016
+ this.lastWorld,
8017
+ this.resizeAspectRatio,
8018
+ shiftKey
7631
8019
  );
7632
- const newAnchorLocal = this.anchorOffset(handle, w, h);
7633
- const cos = Math.cos(angle);
7634
- const sin = Math.sin(angle);
7635
- const rotatedAnchor = {
7636
- x: newAnchorLocal.x * cos - newAnchorLocal.y * sin,
7637
- y: newAnchorLocal.x * sin + newAnchorLocal.y * cos
7638
- };
7639
- const newCenter = { x: anchorWorld.x - rotatedAnchor.x, y: anchorWorld.y - rotatedAnchor.y };
7640
- const position = { x: newCenter.x - w / 2, y: newCenter.y - h / 2 };
7641
- ctx.store.update(this.mode.elementId, { position, size: { w, h } });
8020
+ this.lastWorld = world;
8021
+ ctx.store.update(this.mode.elementId, patch);
7642
8022
  this.updateArrowsBoundTo([this.mode.elementId], ctx);
7643
8023
  ctx.requestRender();
7644
8024
  }
7645
- hitTestResizeHandle(world, ctx) {
7646
- if (this._selectedIds.length === 0) return null;
7647
- const zoom = ctx.camera.zoom;
7648
- const handleHalf = (HANDLE_SIZE / 2 + HANDLE_HIT_PADDING2) / zoom;
7649
- for (const id of this._selectedIds) {
7650
- const el = ctx.store.getById(id);
7651
- if (!el || !("size" in el)) continue;
7652
- if (el.locked) continue;
7653
- if (el.type === "shape" && el.shape === "line") continue;
7654
- const layout = this.getOverlayLayout(el, zoom);
7655
- if (!layout) continue;
7656
- for (const [handle, pos] of layout.corners) {
7657
- if (Math.abs(world.x - pos.x) <= handleHalf && Math.abs(world.y - pos.y) <= handleHalf) {
7658
- return { elementId: id, handle };
7659
- }
7660
- }
7661
- }
7662
- return null;
7663
- }
7664
- hitTestRotateHandle(world, ctx) {
7665
- if (this._selectedIds.length !== 1) return null;
7666
- const id = this._selectedIds[0];
7667
- if (!id) return null;
7668
- const el = ctx.store.getById(id);
7669
- if (!el || el.locked || !ROTATABLE_TYPES.has(el.type)) return null;
7670
- const layout = this.getOverlayLayout(el, ctx.camera.zoom);
7671
- if (!layout) return null;
7672
- const r = (HANDLE_SIZE / 2 + HANDLE_HIT_PADDING2) / ctx.camera.zoom;
7673
- const dx = world.x - layout.rotateHandle.x;
7674
- const dy = world.y - layout.rotateHandle.y;
7675
- return dx * dx + dy * dy <= r * r ? { elementId: id } : null;
7676
- }
7677
- hitTestLineHandles(world, ctx) {
7678
- if (this._selectedIds.length === 0) return null;
7679
- const zoom = ctx.camera.zoom;
7680
- const r = (HANDLE_SIZE / 2 + HANDLE_HIT_PADDING2) / zoom;
7681
- const r2 = r * r;
7682
- for (const id of this._selectedIds) {
7683
- const el = ctx.store.getById(id);
7684
- if (!el || el.type !== "shape" || el.shape !== "line") continue;
7685
- const [a, b] = lineEndpoints(el);
7686
- if ((world.x - a.x) ** 2 + (world.y - a.y) ** 2 <= r2) return { elementId: id, fixed: b };
7687
- if ((world.x - b.x) ** 2 + (world.y - b.y) ** 2 <= r2) return { elementId: id, fixed: a };
7688
- }
7689
- return null;
7690
- }
7691
- getHandlePositions(bounds) {
7692
- return [
7693
- ["nw", { x: bounds.x, y: bounds.y }],
7694
- ["ne", { x: bounds.x + bounds.w, y: bounds.y }],
7695
- ["sw", { x: bounds.x, y: bounds.y + bounds.h }],
7696
- ["se", { x: bounds.x + bounds.w, y: bounds.y + bounds.h }]
7697
- ];
7698
- }
7699
8025
  getOverlayLayout(el, zoom) {
7700
- const bounds = getElementBounds(el);
7701
- if (!bounds) return null;
7702
- const angle = el.rotation ?? 0;
7703
- const pad = SELECTION_PAD / zoom;
7704
- const center2 = { x: bounds.x + bounds.w / 2, y: bounds.y + bounds.h / 2 };
7705
- const raw = [
7706
- ["nw", { x: bounds.x - pad, y: bounds.y - pad }],
7707
- ["ne", { x: bounds.x + bounds.w + pad, y: bounds.y - pad }],
7708
- ["sw", { x: bounds.x - pad, y: bounds.y + bounds.h + pad }],
7709
- ["se", { x: bounds.x + bounds.w + pad, y: bounds.y + bounds.h + pad }]
7710
- ];
7711
- const corners = raw.map(
7712
- ([h, p]) => [h, rotatePoint(p, center2, angle)]
7713
- );
7714
- const topMid = { x: center2.x, y: bounds.y - pad - ROTATE_HANDLE_OFFSET / zoom };
7715
- const rotateHandle = rotatePoint(topMid, center2, angle);
7716
- return { center: center2, corners, rotateHandle, angle };
7717
- }
7718
- topMidpoint(layout) {
7719
- const nw = layout.corners.find(([h]) => h === "nw")?.[1] ?? { x: 0, y: 0 };
7720
- const ne = layout.corners.find(([h]) => h === "ne")?.[1] ?? { x: 0, y: 0 };
7721
- return { x: (nw.x + ne.x) / 2, y: (nw.y + ne.y) / 2 };
7722
- }
7723
- renderMarquee(canvasCtx) {
7724
- if (this.mode.type !== "marquee") return;
7725
- const rect = this.getMarqueeRect();
7726
- if (!rect) return;
7727
- canvasCtx.save();
7728
- canvasCtx.strokeStyle = "#2196F3";
7729
- canvasCtx.fillStyle = "rgba(33, 150, 243, 0.08)";
7730
- canvasCtx.lineWidth = 1;
7731
- canvasCtx.setLineDash([4, 4]);
7732
- canvasCtx.strokeRect(rect.x, rect.y, rect.w, rect.h);
7733
- canvasCtx.fillRect(rect.x, rect.y, rect.w, rect.h);
7734
- canvasCtx.restore();
7735
- }
7736
- renderSelectionBoxes(canvasCtx) {
7737
- if (this._selectedIds.length === 0 || !this.ctx) return;
7738
- const zoom = this.ctx.camera.zoom;
7739
- const handleWorldSize = HANDLE_SIZE / zoom;
7740
- canvasCtx.save();
7741
- canvasCtx.strokeStyle = "#2196F3";
7742
- canvasCtx.lineWidth = 1.5 / zoom;
7743
- canvasCtx.setLineDash([4 / zoom, 4 / zoom]);
7744
- for (const id of this._selectedIds) {
7745
- const el = this.ctx.store.getById(id);
7746
- if (!el) continue;
7747
- if (el.type === "arrow") {
7748
- renderArrowHandles(canvasCtx, el, zoom);
7749
- this.renderBindingHighlights(canvasCtx, el, zoom);
7750
- continue;
7751
- }
7752
- if (el.type === "shape" && el.shape === "line") {
7753
- canvasCtx.setLineDash([]);
7754
- canvasCtx.fillStyle = "#ffffff";
7755
- const r = handleWorldSize / 2;
7756
- for (const p of lineEndpoints(el)) {
7757
- canvasCtx.beginPath();
7758
- canvasCtx.arc(p.x, p.y, r, 0, Math.PI * 2);
7759
- canvasCtx.fill();
7760
- canvasCtx.stroke();
7761
- }
7762
- canvasCtx.setLineDash([4 / zoom, 4 / zoom]);
7763
- continue;
7764
- }
7765
- const bounds = getElementBounds(el);
7766
- if (!bounds) continue;
7767
- const layout = this.getOverlayLayout(el, zoom);
7768
- if (!layout) continue;
7769
- const pad = SELECTION_PAD / zoom;
7770
- if (layout.angle === 0) {
7771
- canvasCtx.strokeRect(
7772
- bounds.x - pad,
7773
- bounds.y - pad,
7774
- bounds.w + pad * 2,
7775
- bounds.h + pad * 2
7776
- );
7777
- } else {
7778
- const ordered = ["nw", "ne", "se", "sw"].map((h) => layout.corners.find(([c]) => c === h)?.[1]).filter((p) => !!p);
7779
- const [p0, ...others] = ordered;
7780
- if (p0) {
7781
- canvasCtx.beginPath();
7782
- canvasCtx.moveTo(p0.x, p0.y);
7783
- for (const p of others) canvasCtx.lineTo(p.x, p.y);
7784
- canvasCtx.closePath();
7785
- canvasCtx.stroke();
7786
- }
7787
- }
7788
- if (!el.locked) {
7789
- if ("size" in el) {
7790
- canvasCtx.setLineDash([]);
7791
- canvasCtx.fillStyle = "#ffffff";
7792
- const corners = layout.angle === 0 ? this.getHandlePositions(bounds) : layout.corners;
7793
- for (const [, pos] of corners) {
7794
- canvasCtx.fillRect(
7795
- pos.x - handleWorldSize / 2,
7796
- pos.y - handleWorldSize / 2,
7797
- handleWorldSize,
7798
- handleWorldSize
7799
- );
7800
- canvasCtx.strokeRect(
7801
- pos.x - handleWorldSize / 2,
7802
- pos.y - handleWorldSize / 2,
7803
- handleWorldSize,
7804
- handleWorldSize
7805
- );
7806
- }
7807
- canvasCtx.setLineDash([4 / zoom, 4 / zoom]);
7808
- } else if (el.type === "template") {
7809
- canvasCtx.setLineDash([]);
7810
- canvasCtx.fillStyle = "#ffffff";
7811
- const hx = bounds.x + bounds.w;
7812
- const hy = bounds.y + bounds.h;
7813
- canvasCtx.fillRect(
7814
- hx - handleWorldSize / 2,
7815
- hy - handleWorldSize / 2,
7816
- handleWorldSize,
7817
- handleWorldSize
7818
- );
7819
- canvasCtx.strokeRect(
7820
- hx - handleWorldSize / 2,
7821
- hy - handleWorldSize / 2,
7822
- handleWorldSize,
7823
- handleWorldSize
7824
- );
7825
- canvasCtx.setLineDash([4 / zoom, 4 / zoom]);
7826
- }
7827
- if (this._selectedIds.length === 1 && ROTATABLE_TYPES.has(el.type)) {
7828
- const stemStart = this.topMidpoint(layout);
7829
- const stemEnd = layout.rotateHandle;
7830
- canvasCtx.beginPath();
7831
- canvasCtx.moveTo(stemStart.x, stemStart.y);
7832
- canvasCtx.lineTo(stemEnd.x, stemEnd.y);
7833
- canvasCtx.stroke();
7834
- canvasCtx.setLineDash([]);
7835
- canvasCtx.fillStyle = "#ffffff";
7836
- canvasCtx.beginPath();
7837
- canvasCtx.arc(stemEnd.x, stemEnd.y, handleWorldSize / 2, 0, Math.PI * 2);
7838
- canvasCtx.fill();
7839
- canvasCtx.stroke();
7840
- canvasCtx.setLineDash([4 / zoom, 4 / zoom]);
7841
- }
7842
- }
7843
- if (el.locked) {
7844
- const ne = layout.corners.find(([h]) => h === "ne")?.[1];
7845
- if (ne) this.drawLockBadge(canvasCtx, ne, zoom);
7846
- }
7847
- }
7848
- canvasCtx.restore();
7849
- }
7850
- drawLockBadge(ctx, at, zoom) {
7851
- const r = 9 / zoom;
7852
- ctx.save();
7853
- ctx.setLineDash([]);
7854
- ctx.beginPath();
7855
- ctx.arc(at.x, at.y, r, 0, Math.PI * 2);
7856
- ctx.fillStyle = "#ffffff";
7857
- ctx.fill();
7858
- ctx.strokeStyle = "#2196F3";
7859
- ctx.lineWidth = 1.5 / zoom;
7860
- ctx.stroke();
7861
- const bw = 8 / zoom;
7862
- const bh = 6 / zoom;
7863
- ctx.fillStyle = "#2196F3";
7864
- ctx.fillRect(at.x - bw / 2, at.y - bh / 2 + 1 / zoom, bw, bh);
7865
- ctx.beginPath();
7866
- ctx.arc(at.x, at.y - bh / 2 + 1 / zoom, 2.5 / zoom, Math.PI, 0);
7867
- ctx.lineWidth = 1.4 / zoom;
7868
- ctx.stroke();
7869
- ctx.restore();
7870
- }
7871
- renderBindingHighlights(canvasCtx, arrow, zoom) {
7872
- if (!this.ctx) return;
7873
- if (!arrow.fromBinding && !arrow.toBinding) return;
7874
- const pad = SELECTION_PAD / zoom;
7875
- canvasCtx.save();
7876
- canvasCtx.strokeStyle = "#2196F3";
7877
- canvasCtx.lineWidth = 2 / zoom;
7878
- canvasCtx.setLineDash([]);
7879
- const drawn = /* @__PURE__ */ new Set();
7880
- for (const binding of [arrow.fromBinding, arrow.toBinding]) {
7881
- if (!binding || drawn.has(binding.elementId)) continue;
7882
- drawn.add(binding.elementId);
7883
- const target = this.ctx.store.getById(binding.elementId);
7884
- if (!target) continue;
7885
- const bounds = getElementBounds(target);
7886
- if (!bounds) continue;
7887
- canvasCtx.strokeRect(bounds.x - pad, bounds.y - pad, bounds.w + pad * 2, bounds.h + pad * 2);
7888
- }
7889
- canvasCtx.restore();
7890
- }
7891
- hitTestTemplateResizeHandle(world, ctx) {
7892
- if (this._selectedIds.length === 0) return null;
7893
- const zoom = ctx.camera.zoom;
7894
- const handleHalf = (HANDLE_SIZE / 2 + HANDLE_HIT_PADDING2) / zoom;
7895
- for (const id of this._selectedIds) {
7896
- const el = ctx.store.getById(id);
7897
- if (!el || el.type !== "template") continue;
7898
- const bounds = getElementBounds(el);
7899
- if (!bounds) continue;
7900
- const hx = bounds.x + bounds.w;
7901
- const hy = bounds.y + bounds.h;
7902
- if (Math.abs(world.x - hx) <= handleHalf && Math.abs(world.y - hy) <= handleHalf) {
7903
- return id;
7904
- }
7905
- }
7906
- return null;
8026
+ return getOverlayLayout(el, zoom);
7907
8027
  }
7908
8028
  handleTemplateResize(world, ctx) {
7909
8029
  if (this.mode.type !== "resizing-template") return;
7910
8030
  const el = ctx.store.getById(this.mode.elementId);
7911
8031
  if (!el || el.type !== "template" || el.locked) return;
7912
- const dx = world.x - el.position.x;
7913
- const dy = world.y - el.position.y;
7914
- let newRadius = Math.sqrt(dx * dx + dy * dy);
7915
- if (ctx.snapToGrid && ctx.gridSize && ctx.gridSize > 0) {
7916
- const snapUnit = ctx.gridType === "hex" ? Math.sqrt(3) * ctx.gridSize : ctx.gridSize;
7917
- newRadius = Math.max(snapUnit, Math.round(newRadius / snapUnit) * snapUnit);
7918
- }
7919
- newRadius = Math.max(MIN_ELEMENT_SIZE, newRadius);
7920
- const updates = { radius: newRadius };
7921
- if (el.feetPerCell != null && ctx.gridSize && ctx.gridSize > 0) {
7922
- const snapUnit = ctx.gridType === "hex" ? Math.sqrt(3) * ctx.gridSize : ctx.gridSize;
7923
- updates.radiusFeet = newRadius / snapUnit * el.feetPerCell;
7924
- }
7925
- ctx.store.update(this.mode.elementId, updates);
7926
- ctx.requestRender();
8032
+ const patch = computeTemplateResize(el, world, {
8033
+ snapToGrid: ctx.snapToGrid,
8034
+ gridSize: ctx.gridSize,
8035
+ gridType: ctx.gridType
8036
+ });
8037
+ if (patch) {
8038
+ ctx.store.update(this.mode.elementId, patch);
8039
+ ctx.requestRender();
8040
+ }
7927
8041
  }
7928
8042
  getMarqueeRect() {
7929
8043
  if (this.mode.type !== "marquee") return null;
@@ -7936,65 +8050,6 @@ var SelectTool = class {
7936
8050
  if (w === 0 && h === 0) return null;
7937
8051
  return { x, y, w, h };
7938
8052
  }
7939
- findElementsInRect(marquee, ctx) {
7940
- const candidates = ctx.store.queryRect(marquee);
7941
- const ids = [];
7942
- for (const el of candidates) {
7943
- if (ctx.isLayerVisible && !ctx.isLayerVisible(el.layerId)) continue;
7944
- if (ctx.isLayerLocked && ctx.isLayerLocked(el.layerId)) continue;
7945
- if (el.type === "grid") continue;
7946
- const bounds = getElementBounds(el);
7947
- if (bounds && this.rectsOverlap(marquee, rotatedAABB(bounds, el.rotation ?? 0))) {
7948
- ids.push(el.id);
7949
- }
7950
- }
7951
- return ids;
7952
- }
7953
- rectsOverlap(a, b) {
7954
- return a.x <= b.x + b.w && a.x + a.w >= b.x && a.y <= b.y + b.h && a.y + a.h >= b.y;
7955
- }
7956
- hitTest(world, ctx) {
7957
- const r = 10;
7958
- const candidates = ctx.store.queryRect({ x: world.x - r, y: world.y - r, w: r * 2, h: r * 2 }).reverse();
7959
- for (const el of candidates) {
7960
- if (ctx.isLayerVisible && !ctx.isLayerVisible(el.layerId)) continue;
7961
- if (ctx.isLayerLocked && ctx.isLayerLocked(el.layerId)) continue;
7962
- if (el.type === "grid") continue;
7963
- if (this.isInsideBounds(world, el)) return el;
7964
- }
7965
- return null;
7966
- }
7967
- isInsideBounds(point, el) {
7968
- if (el.type === "grid") return false;
7969
- const angle = el.rotation ?? 0;
7970
- if (angle !== 0) {
7971
- const b = getElementBounds(el);
7972
- if (b) {
7973
- point = rotatePoint(point, { x: b.x + b.w / 2, y: b.y + b.h / 2 }, -angle);
7974
- }
7975
- }
7976
- if (el.type === "shape" && el.shape === "line") {
7977
- const [a, b] = lineEndpoints(el);
7978
- const threshold = Math.max(el.strokeWidth / 2, 6);
7979
- return distSqToSegment(point, a, b) <= threshold * threshold;
7980
- }
7981
- if ("size" in el) {
7982
- const s = el.size;
7983
- return point.x >= el.position.x && point.x <= el.position.x + s.w && point.y >= el.position.y && point.y <= el.position.y + s.h;
7984
- }
7985
- if (el.type === "stroke") {
7986
- return hitTestStroke(el, point, 10);
7987
- }
7988
- if (el.type === "arrow") {
7989
- return isNearBezier(point, el.from, el.to, el.bend, 10);
7990
- }
7991
- if (el.type === "template") {
7992
- const bounds = getElementBounds(el);
7993
- if (!bounds) return false;
7994
- return point.x >= bounds.x && point.x <= bounds.x + bounds.w && point.y >= bounds.y && point.y <= bounds.y + bounds.h;
7995
- }
7996
- return false;
7997
- }
7998
8053
  };
7999
8054
 
8000
8055
  // src/tools/arrow-tool.ts
@@ -8840,7 +8895,7 @@ var TemplateTool = class {
8840
8895
  };
8841
8896
 
8842
8897
  // src/index.ts
8843
- var VERSION = "0.37.0";
8898
+ var VERSION = "0.38.0";
8844
8899
  export {
8845
8900
  ArrowTool,
8846
8901
  AutoSave,