@blorkfield/overlay-core 0.5.0 → 0.5.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
@@ -464,7 +464,7 @@ function createBodyFromVertices(id, x, y, vertices, renderOptions) {
464
464
  function createBoundariesWithFloorConfig(bounds, floorConfig) {
465
465
  const width = bounds.right - bounds.left;
466
466
  const height = bounds.bottom - bounds.top;
467
- const options = { isStatic: true, render: { visible: false } };
467
+ const wallOptions = { isStatic: true, render: { visible: false } };
468
468
  const walls = [
469
469
  // Left wall
470
470
  Matter2.Bodies.rectangle(
@@ -472,7 +472,7 @@ function createBoundariesWithFloorConfig(bounds, floorConfig) {
472
472
  bounds.top + height / 2,
473
473
  BOUNDARY_THICKNESS,
474
474
  height,
475
- { ...options, label: "leftWall" }
475
+ { ...wallOptions, label: "leftWall" }
476
476
  ),
477
477
  // Right wall
478
478
  Matter2.Bodies.rectangle(
@@ -480,23 +480,44 @@ function createBoundariesWithFloorConfig(bounds, floorConfig) {
480
480
  bounds.top + height / 2,
481
481
  BOUNDARY_THICKNESS,
482
482
  height,
483
- { ...options, label: "rightWall" }
483
+ { ...wallOptions, label: "rightWall" }
484
484
  )
485
485
  ];
486
486
  const segmentCount = floorConfig?.segments ?? 1;
487
- const segmentWidth = width / segmentCount;
488
487
  const floorSegments = [];
488
+ let segmentWidths;
489
+ if (floorConfig?.segmentWidths && floorConfig.segmentWidths.length === segmentCount) {
490
+ const sum = floorConfig.segmentWidths.reduce((a, b) => a + b, 0);
491
+ segmentWidths = floorConfig.segmentWidths.map((w) => w / sum * width);
492
+ } else {
493
+ const equalWidth = width / segmentCount;
494
+ segmentWidths = Array(segmentCount).fill(equalWidth);
495
+ }
496
+ let currentX = bounds.left;
489
497
  for (let i = 0; i < segmentCount; i++) {
490
- const segmentX = bounds.left + (i + 0.5) * segmentWidth;
498
+ const segmentWidth = segmentWidths[i];
499
+ const thickness = floorConfig?.thickness !== void 0 ? Array.isArray(floorConfig.thickness) ? floorConfig.thickness[i] ?? BOUNDARY_THICKNESS : floorConfig.thickness : BOUNDARY_THICKNESS;
500
+ const color = floorConfig?.color !== void 0 ? Array.isArray(floorConfig.color) ? floorConfig.color[i] : floorConfig.color : void 0;
501
+ const segmentX = currentX + segmentWidth / 2;
502
+ const segmentY = bounds.bottom - thickness / 2;
503
+ const segmentOptions = {
504
+ isStatic: true,
505
+ label: `floor-segment-${i}`,
506
+ render: {
507
+ visible: color !== void 0,
508
+ fillStyle: color ?? "#888888"
509
+ }
510
+ };
491
511
  floorSegments.push(
492
512
  Matter2.Bodies.rectangle(
493
513
  segmentX,
494
- bounds.bottom + BOUNDARY_THICKNESS / 2,
514
+ segmentY,
495
515
  segmentWidth,
496
- BOUNDARY_THICKNESS,
497
- { ...options, label: `floor-segment-${i}` }
516
+ thickness,
517
+ segmentOptions
498
518
  )
499
519
  );
520
+ currentX += segmentWidth;
500
521
  }
501
522
  return { walls, floorSegments };
502
523
  }
@@ -1188,6 +1209,7 @@ var OverlayScene = class {
1188
1209
  this.boundaries = [...boundariesResult.walls, ...boundariesResult.floorSegments];
1189
1210
  this.floorSegments = boundariesResult.floorSegments;
1190
1211
  Matter5.Composite.add(this.engine.world, this.boundaries);
1212
+ this.checkInitialFloorIntegrity();
1191
1213
  this.mouse = Matter5.Mouse.create(canvas);
1192
1214
  this.mouseConstraint = Matter5.MouseConstraint.create(this.engine, {
1193
1215
  mouse: this.mouse,
@@ -1306,7 +1328,8 @@ var OverlayScene = class {
1306
1328
  if (onObstacles.has(dyn.id)) continue;
1307
1329
  const dynBounds = dyn.body.bounds;
1308
1330
  const horizontalOverlap = dynBounds.max.x > segmentBounds.min.x && dynBounds.min.x < segmentBounds.max.x;
1309
- if (horizontalOverlap) {
1331
+ const nearFloor = dynBounds.max.y >= segmentBounds.min.y - 10;
1332
+ if (horizontalOverlap && nearFloor) {
1310
1333
  resting.add(dyn.id);
1311
1334
  }
1312
1335
  }
@@ -1332,17 +1355,50 @@ var OverlayScene = class {
1332
1355
  const objectIds = this.floorSegmentPressure.get(i);
1333
1356
  const pressure = objectIds ? this.calculateWeightedPressure(objectIds) : 0;
1334
1357
  if (pressure >= threshold) {
1335
- this.collapseFloorSegment(i, pressure, threshold);
1358
+ this.collapseFloorSegment(i, `pressure ${pressure} >= threshold ${threshold}`);
1336
1359
  }
1337
1360
  }
1338
1361
  }
1339
1362
  /** Collapse a single floor segment */
1340
- collapseFloorSegment(index, pressure, threshold) {
1363
+ collapseFloorSegment(index, reason) {
1341
1364
  if (this.collapsedSegments.has(index)) return;
1342
1365
  this.collapsedSegments.add(index);
1343
1366
  const segment = this.floorSegments[index];
1344
1367
  Matter5.Composite.remove(this.engine.world, segment);
1345
- console.log(`[Pressure] Floor segment ${index} collapsed! (pressure: ${pressure} >= ${threshold})`);
1368
+ logger.debug("OverlayScene", `Floor segment ${index} collapsed: ${reason}`);
1369
+ this.checkFloorIntegrity();
1370
+ }
1371
+ /** Check if floor integrity requirement is violated and collapse all remaining if so */
1372
+ checkFloorIntegrity() {
1373
+ const minIntegrity = this.config.floorConfig?.minIntegrity;
1374
+ if (minIntegrity === void 0) return;
1375
+ const totalSegments = this.floorSegments.length;
1376
+ const remainingSegments = totalSegments - this.collapsedSegments.size;
1377
+ if (remainingSegments < minIntegrity && remainingSegments > 0) {
1378
+ logger.debug("OverlayScene", `Floor integrity failed: ${remainingSegments} remaining < ${minIntegrity} required. Collapsing all.`);
1379
+ for (let i = 0; i < totalSegments; i++) {
1380
+ if (!this.collapsedSegments.has(i)) {
1381
+ this.collapsedSegments.add(i);
1382
+ const segment = this.floorSegments[i];
1383
+ Matter5.Composite.remove(this.engine.world, segment);
1384
+ logger.debug("OverlayScene", `Floor segment ${i} collapsed: integrity failure cascade`);
1385
+ }
1386
+ }
1387
+ }
1388
+ }
1389
+ /** Check floor integrity on initialization (handles minIntegrity > segments) */
1390
+ checkInitialFloorIntegrity() {
1391
+ const minIntegrity = this.config.floorConfig?.minIntegrity;
1392
+ if (minIntegrity === void 0) return;
1393
+ const totalSegments = this.floorSegments.length;
1394
+ if (totalSegments < minIntegrity) {
1395
+ logger.debug("OverlayScene", `Floor integrity impossible: ${totalSegments} segments < ${minIntegrity} required. Collapsing all immediately.`);
1396
+ for (let i = 0; i < totalSegments; i++) {
1397
+ this.collapsedSegments.add(i);
1398
+ const segment = this.floorSegments[i];
1399
+ Matter5.Composite.remove(this.engine.world, segment);
1400
+ }
1401
+ }
1346
1402
  }
1347
1403
  /** Log a summary of pressure on all obstacles, grouped by word */
1348
1404
  logPressureSummary() {
@@ -1598,6 +1654,7 @@ var OverlayScene = class {
1598
1654
  this.collapsedSegments.clear();
1599
1655
  this.floorSegmentPressure.clear();
1600
1656
  Matter5.Composite.add(this.engine.world, this.boundaries);
1657
+ this.checkInitialFloorIntegrity();
1601
1658
  this.render.options.width = width;
1602
1659
  this.render.options.height = height;
1603
1660
  this.render.canvas.width = width;
@@ -1614,6 +1671,21 @@ var OverlayScene = class {
1614
1671
  * Without 'falling' tag, object is static.
1615
1672
  */
1616
1673
  spawnObject(config) {
1674
+ if (config.element) {
1675
+ const result = this.addDOMObstacleInternal({
1676
+ element: config.element,
1677
+ x: config.x,
1678
+ y: config.y,
1679
+ width: config.width,
1680
+ height: config.height,
1681
+ tags: config.tags,
1682
+ pressureThreshold: config.pressureThreshold,
1683
+ weight: config.weight,
1684
+ shadow: config.shadow === true ? { opacity: 0.3 } : config.shadow || void 0,
1685
+ clickToFall: config.clickToFall
1686
+ });
1687
+ return result.id;
1688
+ }
1617
1689
  const id = crypto.randomUUID();
1618
1690
  const tags = config.tags ?? [];
1619
1691
  const isStatic = !tags.includes("falling");
@@ -1633,6 +1705,17 @@ var OverlayScene = class {
1633
1705
  } else {
1634
1706
  body = createObstacle(id, config, isStatic);
1635
1707
  }
1708
+ let pressureThreshold;
1709
+ if (config.pressureThreshold) {
1710
+ pressureThreshold = typeof config.pressureThreshold.value === "number" ? config.pressureThreshold.value : config.pressureThreshold.value[0];
1711
+ }
1712
+ let shadow;
1713
+ if (config.shadow === true) {
1714
+ shadow = { opacity: 0.3 };
1715
+ } else if (config.shadow && typeof config.shadow === "object") {
1716
+ shadow = { opacity: config.shadow.opacity ?? 0.3 };
1717
+ }
1718
+ const clicksRemaining = config.clickToFall?.clicks;
1636
1719
  const entry = {
1637
1720
  id,
1638
1721
  body,
@@ -1640,10 +1723,17 @@ var OverlayScene = class {
1640
1723
  spawnTime: performance.now(),
1641
1724
  ttl: config.ttl,
1642
1725
  despawnEffect: config.despawnEffect,
1643
- weight: config.weight ?? 1
1726
+ weight: config.weight ?? 1,
1727
+ pressureThreshold,
1728
+ shadow,
1729
+ originalPosition: shadow || clicksRemaining !== void 0 ? { x: config.x, y: config.y } : void 0,
1730
+ clicksRemaining
1644
1731
  };
1645
1732
  this.objects.set(id, entry);
1646
1733
  Matter5.Composite.add(this.engine.world, body);
1734
+ if (isStatic && pressureThreshold !== void 0) {
1735
+ this.obstaclePressure.set(id, /* @__PURE__ */ new Set());
1736
+ }
1647
1737
  return id;
1648
1738
  }
1649
1739
  /**
@@ -1670,6 +1760,17 @@ var OverlayScene = class {
1670
1760
  } else {
1671
1761
  body = await createObstacleAsync(id, config, isStatic);
1672
1762
  }
1763
+ let pressureThreshold;
1764
+ if (config.pressureThreshold) {
1765
+ pressureThreshold = typeof config.pressureThreshold.value === "number" ? config.pressureThreshold.value : config.pressureThreshold.value[0];
1766
+ }
1767
+ let shadow;
1768
+ if (config.shadow === true) {
1769
+ shadow = { opacity: 0.3 };
1770
+ } else if (config.shadow && typeof config.shadow === "object") {
1771
+ shadow = { opacity: config.shadow.opacity ?? 0.3 };
1772
+ }
1773
+ const clicksRemaining = config.clickToFall?.clicks;
1673
1774
  const entry = {
1674
1775
  id,
1675
1776
  body,
@@ -1677,10 +1778,17 @@ var OverlayScene = class {
1677
1778
  spawnTime: performance.now(),
1678
1779
  ttl: config.ttl,
1679
1780
  despawnEffect: config.despawnEffect,
1680
- weight: config.weight ?? 1
1781
+ weight: config.weight ?? 1,
1782
+ pressureThreshold,
1783
+ shadow,
1784
+ originalPosition: shadow || clicksRemaining !== void 0 ? { x: config.x, y: config.y } : void 0,
1785
+ clicksRemaining
1681
1786
  };
1682
1787
  this.objects.set(id, entry);
1683
1788
  Matter5.Composite.add(this.engine.world, body);
1789
+ if (isStatic && pressureThreshold !== void 0) {
1790
+ this.obstaclePressure.set(id, /* @__PURE__ */ new Set());
1791
+ }
1684
1792
  return id;
1685
1793
  }
1686
1794
  /**
@@ -1939,17 +2047,12 @@ var OverlayScene = class {
1939
2047
  areFontsInitialized() {
1940
2048
  return this.fontsInitialized;
1941
2049
  }
1942
- // ==================== DOM OBSTACLE METHODS ====================
2050
+ // ==================== DOM OBSTACLE METHODS (INTERNAL) ====================
1943
2051
  /**
1944
- * Attach a DOM element to physics. The element will follow the physics body
1945
- * and can have pressure threshold, shadow, and click-to-fall behavior.
1946
- *
1947
- * When the element collapses (becomes dynamic), its CSS transform will be
1948
- * updated each frame to match the physics body position and rotation.
1949
- *
1950
- * Shadow creates a cloned DOM element that stays at the original position.
2052
+ * Internal: Attach a DOM element to physics.
2053
+ * Called by spawnObject when element is provided.
1951
2054
  */
1952
- addDOMObstacle(config) {
2055
+ addDOMObstacleInternal(config) {
1953
2056
  const { element, x, y } = config;
1954
2057
  const width = config.width ?? element.offsetWidth;
1955
2058
  const height = config.height ?? element.offsetHeight;
@@ -1986,6 +2089,7 @@ var OverlayScene = class {
1986
2089
  };
1987
2090
  this.objects.set(id, entry);
1988
2091
  Matter5.Composite.add(this.engine.world, body);
2092
+ this.updateDOMElementTransform(entry);
1989
2093
  if (isStatic && pressureThreshold !== void 0) {
1990
2094
  this.obstaclePressure.set(id, /* @__PURE__ */ new Set());
1991
2095
  }
@@ -2092,11 +2196,61 @@ var OverlayScene = class {
2092
2196
  maxDimension = Math.max(maxDimension, dims.width, dims.height);
2093
2197
  }
2094
2198
  if (maxDimension === 0) maxDimension = 100;
2199
+ const calculateLineWidth = (line) => {
2200
+ let width = 0;
2201
+ for (const char of line) {
2202
+ if (char === " ") {
2203
+ width += 20;
2204
+ } else if (/^[A-Za-z0-9]$/.test(char)) {
2205
+ const dims = charDimensions.get(char);
2206
+ if (dims) {
2207
+ const scale = letterSize / Math.max(dims.width, dims.height);
2208
+ const scaledWidth = dims.width * scale;
2209
+ const extraSpacing = config.letterSpacing !== void 0 ? config.letterSpacing - scaledWidth : 0;
2210
+ width += scaledWidth + Math.max(0, extraSpacing);
2211
+ }
2212
+ }
2213
+ }
2214
+ return width;
2215
+ };
2216
+ const lineWidths = lines.map((line) => calculateLineWidth(line));
2217
+ const align = config.align ?? "left";
2218
+ const maxLineWidth = Math.max(...lineWidths, 0);
2219
+ let boundsLeft;
2220
+ let boundsRight;
2221
+ switch (align) {
2222
+ case "center":
2223
+ boundsLeft = config.x - maxLineWidth / 2;
2224
+ boundsRight = config.x + maxLineWidth / 2;
2225
+ break;
2226
+ case "right":
2227
+ boundsLeft = config.x - maxLineWidth;
2228
+ boundsRight = config.x;
2229
+ break;
2230
+ default:
2231
+ boundsLeft = config.x;
2232
+ boundsRight = config.x + maxLineWidth;
2233
+ }
2234
+ const boundsTop = config.y - letterSize / 2;
2235
+ const totalHeight = lines.length > 0 ? (lines.length - 1) * lineHeight + letterSize : 0;
2236
+ const boundsBottom = boundsTop + totalHeight;
2095
2237
  let currentY = config.y;
2096
2238
  let globalCharIndex = 0;
2097
- for (const line of lines) {
2239
+ for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
2240
+ const line = lines[lineIndex];
2098
2241
  const chars = line.split("");
2099
- let currentX = config.x;
2242
+ const lineWidth = lineWidths[lineIndex];
2243
+ let currentX;
2244
+ switch (align) {
2245
+ case "center":
2246
+ currentX = config.x - lineWidth / 2;
2247
+ break;
2248
+ case "right":
2249
+ currentX = config.x - lineWidth;
2250
+ break;
2251
+ default:
2252
+ currentX = config.x;
2253
+ }
2100
2254
  if (inWord) {
2101
2255
  currentWordIndex++;
2102
2256
  inWord = false;
@@ -2212,12 +2366,21 @@ var OverlayScene = class {
2212
2366
  letterColor,
2213
2367
  lineCount: lines.length
2214
2368
  });
2369
+ const bounds = {
2370
+ left: boundsLeft,
2371
+ right: boundsRight,
2372
+ top: boundsTop,
2373
+ bottom: boundsBottom,
2374
+ width: boundsRight - boundsLeft,
2375
+ height: boundsBottom - boundsTop
2376
+ };
2215
2377
  return {
2216
2378
  letterIds,
2217
2379
  stringTag,
2218
2380
  wordTags,
2219
2381
  letterMap,
2220
- letterDebugInfo: debugInfo
2382
+ letterDebugInfo: debugInfo,
2383
+ bounds
2221
2384
  };
2222
2385
  }
2223
2386
  /**
@@ -2290,10 +2453,43 @@ var OverlayScene = class {
2290
2453
  const fontInfo = this.fonts.find((f) => f.fontUrl === fontUrl);
2291
2454
  const fontFamily = fontInfo?.name ?? "sans-serif";
2292
2455
  const lines = text.split("\n");
2456
+ const lineWidths = lines.map((line) => measureText(loadedFont, line, fontSize));
2457
+ const align = config.align ?? "left";
2458
+ const maxLineWidth = Math.max(...lineWidths, 0);
2459
+ let boundsLeft;
2460
+ let boundsRight;
2461
+ switch (align) {
2462
+ case "center":
2463
+ boundsLeft = x - maxLineWidth / 2;
2464
+ boundsRight = x + maxLineWidth / 2;
2465
+ break;
2466
+ case "right":
2467
+ boundsLeft = x - maxLineWidth;
2468
+ boundsRight = x;
2469
+ break;
2470
+ default:
2471
+ boundsLeft = x;
2472
+ boundsRight = x + maxLineWidth;
2473
+ }
2474
+ const boundsTop = y - fontSize * 0.8;
2475
+ const totalHeight = lines.length > 0 ? (lines.length - 1) * lineHeight + fontSize : 0;
2476
+ const boundsBottom = boundsTop + totalHeight;
2293
2477
  let currentY = y;
2294
2478
  let globalCharIndex = 0;
2295
- for (const line of lines) {
2296
- let currentX = x;
2479
+ for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
2480
+ const line = lines[lineIndex];
2481
+ const lineWidth = lineWidths[lineIndex];
2482
+ let currentX;
2483
+ switch (align) {
2484
+ case "center":
2485
+ currentX = x - lineWidth / 2;
2486
+ break;
2487
+ case "right":
2488
+ currentX = x - lineWidth;
2489
+ break;
2490
+ default:
2491
+ currentX = x;
2492
+ }
2297
2493
  if (inWord) {
2298
2494
  currentWordIndex++;
2299
2495
  inWord = false;
@@ -2403,12 +2599,21 @@ var OverlayScene = class {
2403
2599
  wordTags,
2404
2600
  lineCount: lines.length
2405
2601
  });
2602
+ const bounds = {
2603
+ left: boundsLeft,
2604
+ right: boundsRight,
2605
+ top: boundsTop,
2606
+ bottom: boundsBottom,
2607
+ width: boundsRight - boundsLeft,
2608
+ height: boundsBottom - boundsTop
2609
+ };
2406
2610
  return {
2407
2611
  letterIds,
2408
2612
  stringTag,
2409
2613
  wordTags,
2410
2614
  letterMap,
2411
- letterDebugInfo: []
2615
+ letterDebugInfo: [],
2616
+ bounds
2412
2617
  };
2413
2618
  }
2414
2619
  /**