@blorkfield/overlay-core 0.5.0 → 0.5.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.
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() {
@@ -1461,7 +1517,6 @@ var OverlayScene = class {
1461
1517
  const shadowElement = entry.domElement.cloneNode(true);
1462
1518
  shadowElement.style.opacity = String(opacity);
1463
1519
  shadowElement.style.pointerEvents = "none";
1464
- shadowElement.style.transform = entry.domOriginalTransform || "";
1465
1520
  entry.domElement.parentNode?.insertBefore(shadowElement, entry.domElement);
1466
1521
  entry.domShadowElement = shadowElement;
1467
1522
  return;
@@ -1598,6 +1653,7 @@ var OverlayScene = class {
1598
1653
  this.collapsedSegments.clear();
1599
1654
  this.floorSegmentPressure.clear();
1600
1655
  Matter5.Composite.add(this.engine.world, this.boundaries);
1656
+ this.checkInitialFloorIntegrity();
1601
1657
  this.render.options.width = width;
1602
1658
  this.render.options.height = height;
1603
1659
  this.render.canvas.width = width;
@@ -1614,6 +1670,21 @@ var OverlayScene = class {
1614
1670
  * Without 'falling' tag, object is static.
1615
1671
  */
1616
1672
  spawnObject(config) {
1673
+ if (config.element) {
1674
+ const result = this.addDOMObstacleInternal({
1675
+ element: config.element,
1676
+ x: config.x,
1677
+ y: config.y,
1678
+ width: config.width,
1679
+ height: config.height,
1680
+ tags: config.tags,
1681
+ pressureThreshold: config.pressureThreshold,
1682
+ weight: config.weight,
1683
+ shadow: config.shadow === true ? { opacity: 0.3 } : config.shadow || void 0,
1684
+ clickToFall: config.clickToFall
1685
+ });
1686
+ return result.id;
1687
+ }
1617
1688
  const id = crypto.randomUUID();
1618
1689
  const tags = config.tags ?? [];
1619
1690
  const isStatic = !tags.includes("falling");
@@ -1633,6 +1704,17 @@ var OverlayScene = class {
1633
1704
  } else {
1634
1705
  body = createObstacle(id, config, isStatic);
1635
1706
  }
1707
+ let pressureThreshold;
1708
+ if (config.pressureThreshold) {
1709
+ pressureThreshold = typeof config.pressureThreshold.value === "number" ? config.pressureThreshold.value : config.pressureThreshold.value[0];
1710
+ }
1711
+ let shadow;
1712
+ if (config.shadow === true) {
1713
+ shadow = { opacity: 0.3 };
1714
+ } else if (config.shadow && typeof config.shadow === "object") {
1715
+ shadow = { opacity: config.shadow.opacity ?? 0.3 };
1716
+ }
1717
+ const clicksRemaining = config.clickToFall?.clicks;
1636
1718
  const entry = {
1637
1719
  id,
1638
1720
  body,
@@ -1640,10 +1722,17 @@ var OverlayScene = class {
1640
1722
  spawnTime: performance.now(),
1641
1723
  ttl: config.ttl,
1642
1724
  despawnEffect: config.despawnEffect,
1643
- weight: config.weight ?? 1
1725
+ weight: config.weight ?? 1,
1726
+ pressureThreshold,
1727
+ shadow,
1728
+ originalPosition: shadow || clicksRemaining !== void 0 ? { x: config.x, y: config.y } : void 0,
1729
+ clicksRemaining
1644
1730
  };
1645
1731
  this.objects.set(id, entry);
1646
1732
  Matter5.Composite.add(this.engine.world, body);
1733
+ if (isStatic && pressureThreshold !== void 0) {
1734
+ this.obstaclePressure.set(id, /* @__PURE__ */ new Set());
1735
+ }
1647
1736
  return id;
1648
1737
  }
1649
1738
  /**
@@ -1670,6 +1759,17 @@ var OverlayScene = class {
1670
1759
  } else {
1671
1760
  body = await createObstacleAsync(id, config, isStatic);
1672
1761
  }
1762
+ let pressureThreshold;
1763
+ if (config.pressureThreshold) {
1764
+ pressureThreshold = typeof config.pressureThreshold.value === "number" ? config.pressureThreshold.value : config.pressureThreshold.value[0];
1765
+ }
1766
+ let shadow;
1767
+ if (config.shadow === true) {
1768
+ shadow = { opacity: 0.3 };
1769
+ } else if (config.shadow && typeof config.shadow === "object") {
1770
+ shadow = { opacity: config.shadow.opacity ?? 0.3 };
1771
+ }
1772
+ const clicksRemaining = config.clickToFall?.clicks;
1673
1773
  const entry = {
1674
1774
  id,
1675
1775
  body,
@@ -1677,10 +1777,17 @@ var OverlayScene = class {
1677
1777
  spawnTime: performance.now(),
1678
1778
  ttl: config.ttl,
1679
1779
  despawnEffect: config.despawnEffect,
1680
- weight: config.weight ?? 1
1780
+ weight: config.weight ?? 1,
1781
+ pressureThreshold,
1782
+ shadow,
1783
+ originalPosition: shadow || clicksRemaining !== void 0 ? { x: config.x, y: config.y } : void 0,
1784
+ clicksRemaining
1681
1785
  };
1682
1786
  this.objects.set(id, entry);
1683
1787
  Matter5.Composite.add(this.engine.world, body);
1788
+ if (isStatic && pressureThreshold !== void 0) {
1789
+ this.obstaclePressure.set(id, /* @__PURE__ */ new Set());
1790
+ }
1684
1791
  return id;
1685
1792
  }
1686
1793
  /**
@@ -1939,17 +2046,12 @@ var OverlayScene = class {
1939
2046
  areFontsInitialized() {
1940
2047
  return this.fontsInitialized;
1941
2048
  }
1942
- // ==================== DOM OBSTACLE METHODS ====================
2049
+ // ==================== DOM OBSTACLE METHODS (INTERNAL) ====================
1943
2050
  /**
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.
2051
+ * Internal: Attach a DOM element to physics.
2052
+ * Called by spawnObject when element is provided.
1951
2053
  */
1952
- addDOMObstacle(config) {
2054
+ addDOMObstacleInternal(config) {
1953
2055
  const { element, x, y } = config;
1954
2056
  const width = config.width ?? element.offsetWidth;
1955
2057
  const height = config.height ?? element.offsetHeight;
@@ -1986,6 +2088,7 @@ var OverlayScene = class {
1986
2088
  };
1987
2089
  this.objects.set(id, entry);
1988
2090
  Matter5.Composite.add(this.engine.world, body);
2091
+ this.updateDOMElementTransform(entry);
1989
2092
  if (isStatic && pressureThreshold !== void 0) {
1990
2093
  this.obstaclePressure.set(id, /* @__PURE__ */ new Set());
1991
2094
  }
@@ -2092,11 +2195,61 @@ var OverlayScene = class {
2092
2195
  maxDimension = Math.max(maxDimension, dims.width, dims.height);
2093
2196
  }
2094
2197
  if (maxDimension === 0) maxDimension = 100;
2198
+ const calculateLineWidth = (line) => {
2199
+ let width = 0;
2200
+ for (const char of line) {
2201
+ if (char === " ") {
2202
+ width += 20;
2203
+ } else if (/^[A-Za-z0-9]$/.test(char)) {
2204
+ const dims = charDimensions.get(char);
2205
+ if (dims) {
2206
+ const scale = letterSize / Math.max(dims.width, dims.height);
2207
+ const scaledWidth = dims.width * scale;
2208
+ const extraSpacing = config.letterSpacing !== void 0 ? config.letterSpacing - scaledWidth : 0;
2209
+ width += scaledWidth + Math.max(0, extraSpacing);
2210
+ }
2211
+ }
2212
+ }
2213
+ return width;
2214
+ };
2215
+ const lineWidths = lines.map((line) => calculateLineWidth(line));
2216
+ const align = config.align ?? "left";
2217
+ const maxLineWidth = Math.max(...lineWidths, 0);
2218
+ let boundsLeft;
2219
+ let boundsRight;
2220
+ switch (align) {
2221
+ case "center":
2222
+ boundsLeft = config.x - maxLineWidth / 2;
2223
+ boundsRight = config.x + maxLineWidth / 2;
2224
+ break;
2225
+ case "right":
2226
+ boundsLeft = config.x - maxLineWidth;
2227
+ boundsRight = config.x;
2228
+ break;
2229
+ default:
2230
+ boundsLeft = config.x;
2231
+ boundsRight = config.x + maxLineWidth;
2232
+ }
2233
+ const boundsTop = config.y - letterSize / 2;
2234
+ const totalHeight = lines.length > 0 ? (lines.length - 1) * lineHeight + letterSize : 0;
2235
+ const boundsBottom = boundsTop + totalHeight;
2095
2236
  let currentY = config.y;
2096
2237
  let globalCharIndex = 0;
2097
- for (const line of lines) {
2238
+ for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
2239
+ const line = lines[lineIndex];
2098
2240
  const chars = line.split("");
2099
- let currentX = config.x;
2241
+ const lineWidth = lineWidths[lineIndex];
2242
+ let currentX;
2243
+ switch (align) {
2244
+ case "center":
2245
+ currentX = config.x - lineWidth / 2;
2246
+ break;
2247
+ case "right":
2248
+ currentX = config.x - lineWidth;
2249
+ break;
2250
+ default:
2251
+ currentX = config.x;
2252
+ }
2100
2253
  if (inWord) {
2101
2254
  currentWordIndex++;
2102
2255
  inWord = false;
@@ -2212,12 +2365,21 @@ var OverlayScene = class {
2212
2365
  letterColor,
2213
2366
  lineCount: lines.length
2214
2367
  });
2368
+ const bounds = {
2369
+ left: boundsLeft,
2370
+ right: boundsRight,
2371
+ top: boundsTop,
2372
+ bottom: boundsBottom,
2373
+ width: boundsRight - boundsLeft,
2374
+ height: boundsBottom - boundsTop
2375
+ };
2215
2376
  return {
2216
2377
  letterIds,
2217
2378
  stringTag,
2218
2379
  wordTags,
2219
2380
  letterMap,
2220
- letterDebugInfo: debugInfo
2381
+ letterDebugInfo: debugInfo,
2382
+ bounds
2221
2383
  };
2222
2384
  }
2223
2385
  /**
@@ -2290,10 +2452,43 @@ var OverlayScene = class {
2290
2452
  const fontInfo = this.fonts.find((f) => f.fontUrl === fontUrl);
2291
2453
  const fontFamily = fontInfo?.name ?? "sans-serif";
2292
2454
  const lines = text.split("\n");
2455
+ const lineWidths = lines.map((line) => measureText(loadedFont, line, fontSize));
2456
+ const align = config.align ?? "left";
2457
+ const maxLineWidth = Math.max(...lineWidths, 0);
2458
+ let boundsLeft;
2459
+ let boundsRight;
2460
+ switch (align) {
2461
+ case "center":
2462
+ boundsLeft = x - maxLineWidth / 2;
2463
+ boundsRight = x + maxLineWidth / 2;
2464
+ break;
2465
+ case "right":
2466
+ boundsLeft = x - maxLineWidth;
2467
+ boundsRight = x;
2468
+ break;
2469
+ default:
2470
+ boundsLeft = x;
2471
+ boundsRight = x + maxLineWidth;
2472
+ }
2473
+ const boundsTop = y - fontSize * 0.8;
2474
+ const totalHeight = lines.length > 0 ? (lines.length - 1) * lineHeight + fontSize : 0;
2475
+ const boundsBottom = boundsTop + totalHeight;
2293
2476
  let currentY = y;
2294
2477
  let globalCharIndex = 0;
2295
- for (const line of lines) {
2296
- let currentX = x;
2478
+ for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
2479
+ const line = lines[lineIndex];
2480
+ const lineWidth = lineWidths[lineIndex];
2481
+ let currentX;
2482
+ switch (align) {
2483
+ case "center":
2484
+ currentX = x - lineWidth / 2;
2485
+ break;
2486
+ case "right":
2487
+ currentX = x - lineWidth;
2488
+ break;
2489
+ default:
2490
+ currentX = x;
2491
+ }
2297
2492
  if (inWord) {
2298
2493
  currentWordIndex++;
2299
2494
  inWord = false;
@@ -2403,12 +2598,21 @@ var OverlayScene = class {
2403
2598
  wordTags,
2404
2599
  lineCount: lines.length
2405
2600
  });
2601
+ const bounds = {
2602
+ left: boundsLeft,
2603
+ right: boundsRight,
2604
+ top: boundsTop,
2605
+ bottom: boundsBottom,
2606
+ width: boundsRight - boundsLeft,
2607
+ height: boundsBottom - boundsTop
2608
+ };
2406
2609
  return {
2407
2610
  letterIds,
2408
2611
  stringTag,
2409
2612
  wordTags,
2410
2613
  letterMap,
2411
- letterDebugInfo: []
2614
+ letterDebugInfo: [],
2615
+ bounds
2412
2616
  };
2413
2617
  }
2414
2618
  /**