@adatechnology/game-floorplan-three 0.4.2 → 0.6.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.
package/dist/index.js CHANGED
@@ -66,8 +66,8 @@ function createTranslator(locale) {
66
66
  var OPEN_ROOM_TYPES = /* @__PURE__ */ new Set(["balcony", "terrace", "veranda", "balcony_gourmet"]);
67
67
  var rectsOf = (c) => c.parts && c.parts.length ? c.parts : [[c.u0, c.v0, c.u1, c.v1]];
68
68
  var isOpenArea = (c) => OPEN_ROOM_TYPES.has(c.type) || c.outdoor === true;
69
- function planLod(houses, focus, budget) {
70
- const l = houses.map((c) => ({ c, d: c.distance(focus), n0: c.stats.drawCalls, n1: c.stats.drawCallsLOD1, level: 0 })).sort((a, b) => a.d - b.d);
69
+ function planLod(houses2, focus, budget) {
70
+ const l = houses2.map((c) => ({ c, d: c.distance(focus), n0: c.stats.drawCalls, n1: c.stats.drawCallsLOD1, level: 0 })).sort((a, b) => a.d - b.d);
71
71
  let total = l.reduce((s, x) => s + x.n0, 0);
72
72
  for (let i = l.length - 1; i >= 0 && total > budget; i--) {
73
73
  total -= l[i].n0 - l[i].n1;
@@ -5872,12 +5872,12 @@ function installLightsRoomLight(K, shared) {
5872
5872
  A.invalidate = () => {
5873
5873
  for (let i = 0; i < NS; i++) PL[i].shadow.needsUpdate = true;
5874
5874
  };
5875
- A.update = function(houses, focus, op) {
5875
+ A.update = function(houses2, focus, op) {
5876
5876
  op = op || {};
5877
- houses = (Array.isArray(houses) ? houses : [houses]).filter(Boolean);
5877
+ houses2 = (Array.isArray(houses2) ? houses2 : [houses2]).filter(Boolean);
5878
5878
  _f.set(focus.x, focus.y, focus.z);
5879
5879
  const rooms = [], exterior = [];
5880
- for (const house of houses) {
5880
+ for (const house of houses2) {
5881
5881
  if (house.group && !house.group.visible) continue;
5882
5882
  house.house.updateMatrixWorld(true);
5883
5883
  const M = house.house.matrixWorld, by = /* @__PURE__ */ new Map();
@@ -6030,7 +6030,7 @@ function installLightsRoomLight(K, shared) {
6030
6030
 
6031
6031
  // src/destruction/pieces.ts
6032
6032
  import { assemblyPart, getAssembly } from "@adatechnology/game-destruction";
6033
- import { DENSITY, toDestructibleSpec } from "@adatechnology/game-floorplan-structure";
6033
+ import { DENSITY, MEMBER_CAGE, toDestructibleSpec } from "@adatechnology/game-floorplan-structure";
6034
6034
 
6035
6035
  // src/destruction/panels.ts
6036
6036
  import { resolveSlabPanels, slabPanelView } from "@adatechnology/game-floorplan-structure";
@@ -6210,7 +6210,11 @@ function installDestructionPieces(K, shared) {
6210
6210
  if (!pc || pc.unit) return sp;
6211
6211
  if (pc.role !== "wall" && pc.role !== "partition") {
6212
6212
  const def = getAssembly(pc.assembly || (pc.material === "concrete" && ["slab", "beam", "column", "stair"].includes(pc.role) ? "reinforced_concrete" : ""));
6213
- return def && def.reinforcement && !sp.reinforcement ? { ...sp, reinforcement: { ...def.reinforcement } } : sp;
6213
+ const out = def && def.reinforcement && !sp.reinforcement ? { ...sp, reinforcement: { ...def.reinforcement } } : sp;
6214
+ if (out.reinforcement && !out.units && pc.material === "concrete" && (pc.role === "beam" || pc.role === "column") && sp.shape.kind === "box") {
6215
+ return { ...out, units: { size: [MEMBER_CAGE.unit, MEMBER_CAGE.unit, Math.min(...sp.shape.size)], bond: "stack", mortar: "concrete" } };
6216
+ }
6217
+ return out;
6214
6218
  }
6215
6219
  if (!pc.assembly) return sp;
6216
6220
  const [u0, v0, u1, v1] = pc.aabb, alongU = u1 - u0 >= v1 - v0;
@@ -6267,7 +6271,12 @@ function installDestructionBreach(K, _shared) {
6267
6271
  }
6268
6272
  return null;
6269
6273
  };
6270
- function courses(part, units, opening, paint) {
6274
+ const local = (p) => {
6275
+ house.updateMatrixWorld(true);
6276
+ const v = house.worldToLocal(tmp.p.set(p[0], p[1], p[2]));
6277
+ return [v.x, v.y, v.z];
6278
+ };
6279
+ function courses(part, units, opening, chips, paint) {
6271
6280
  const [u0, v0, u1, v1] = part.aabb;
6272
6281
  const alongU = u1 - u0 >= v1 - v0;
6273
6282
  const len = alongU ? u1 - u0 : v1 - v0;
@@ -6291,23 +6300,54 @@ function installDestructionBreach(K, _shared) {
6291
6300
  const x = alongU ? u0 + mid : (u0 + u1) / 2;
6292
6301
  const z = alongU ? (v0 + v1) / 2 : v0 + mid;
6293
6302
  const y = part.y0 + (r + 0.5) * rowH;
6294
- const d = Math.hypot(x - opening.center[0], y - opening.center[1], z - opening.center[2]);
6295
- if (d < opening.radius) continue;
6296
- list.push({ x, y, z, sx: alongU ? b - a : thick, sy: rowH, sz: alongU ? thick : b - a, colour: d < opening.radius + 0.25 || paint === null ? edge : paint });
6303
+ const d = opening ? Math.hypot(x - opening.center[0], y - opening.center[1], z - opening.center[2]) : Infinity;
6304
+ if (opening && d < opening.radius) continue;
6305
+ const gone = (c) => Math.abs(c[1] - y) <= rowH / 2 && (alongU ? c[0] >= u0 + a && c[0] <= u0 + b : c[2] >= v0 + a && c[2] <= v0 + b);
6306
+ if (chips.some(gone)) continue;
6307
+ const near = opening && d < opening.radius + 0.25 || chips.some((c) => Math.hypot(c[0] - x, c[1] - y, c[2] - z) < 0.15);
6308
+ list.push({ x, y, z, sx: alongU ? b - a : thick, sy: rowH, sz: alongU ? thick : b - a, colour: near || paint === null ? edge : paint });
6297
6309
  }
6298
6310
  }
6299
6311
  return list;
6300
6312
  }
6301
- function open(event) {
6302
- if (lod !== 0 || views.has(event.part) || !event.opening) return false;
6303
- const part = structure.byId.get(event.part);
6304
- const spec = opts.spec()?.parts?.find((q) => q.id === event.part);
6305
- if (!part || !spec || !spec.units) return false;
6306
- const items = structure.meshesOf(event.part).filter((it) => it.mesh);
6307
- if (!items.length) return false;
6308
- const list = courses(part, spec.units, event.opening, wallColour(items));
6313
+ const specOf = (id) => opts.spec()?.parts?.find((q) => q.id === id);
6314
+ const same = (a, b) => a.every((x, k) => Math.abs(x - b[k]) < 1e-3);
6315
+ function hostOf(partId) {
6316
+ if (structure.meshesOf(partId).some((it) => it.mesh)) return partId;
6317
+ const part = structure.byId.get(partId);
6318
+ if (!part) return null;
6319
+ for (const q of structure.parts) {
6320
+ if (q.id !== partId && same(q.aabb, part.aabb) && Math.abs(q.y1 - part.y0) < 0.01 && structure.meshesOf(q.id).some((it) => it.mesh)) return q.id;
6321
+ }
6322
+ return null;
6323
+ }
6324
+ const riders = (hostId) => structure.parts.filter((q) => q.id !== hostId && specOf(q.id)?.units && hostOf(q.id) === hostId && !structure.meshesOf(q.id).some((it) => it.mesh)).map((q) => q.id);
6325
+ function draw2(partId, change) {
6326
+ if (lod !== 0) return false;
6327
+ const hostId = hostOf(partId);
6328
+ if (!hostId || !specOf(partId)?.units) return false;
6329
+ let view = views.get(hostId);
6330
+ if (!view) {
6331
+ const items = structure.meshesOf(hostId).filter((it) => it.mesh);
6332
+ if (!items.length) return false;
6333
+ const parts = [hostId, ...riders(hostId)].filter((id) => structure.byId.get(id) && specOf(id)?.units);
6334
+ view = { mesh: null, saved: null, items, paint: wallColour(items), units: 0, opening: null, chips: new Map(parts.map((id) => [id, []])), openings: /* @__PURE__ */ new Map() };
6335
+ }
6336
+ if (!view.chips.has(partId)) return false;
6337
+ if (change.opening) {
6338
+ if (view.openings.has(partId)) return false;
6339
+ view.openings.set(partId, { center: local(change.opening.center), radius: change.opening.radius });
6340
+ if (partId === hostId) view.opening = view.openings.get(partId);
6341
+ }
6342
+ if (change.chip) view.chips.get(partId).push(local(change.chip));
6343
+ const list = [];
6344
+ for (const [id, chips] of view.chips) list.push(...courses(structure.byId.get(id), specOf(id).units, view.openings.get(id) ?? null, chips, view.paint));
6345
+ if (view.mesh) {
6346
+ view.mesh.removeFromParent();
6347
+ view.mesh.dispose();
6348
+ }
6309
6349
  const mesh = new THREE.InstancedMesh(unitGeometry, unitMaterial, Math.max(1, list.length));
6310
- mesh.name = "breach:" + event.part;
6350
+ mesh.name = "breach:" + hostId;
6311
6351
  list.forEach((u, i) => {
6312
6352
  tmp.m.compose(tmp.p.set(u.x, u.y, u.z), tmp.q.identity(), tmp.s.set(u.sx, u.sy, u.sz));
6313
6353
  mesh.setMatrixAt(i, tmp.m);
@@ -6319,8 +6359,10 @@ function installDestructionBreach(K, _shared) {
6319
6359
  mesh.castShadow = true;
6320
6360
  mesh.receiveShadow = true;
6321
6361
  house.add(mesh);
6322
- const saved = items.map((it) => ({ it, v0: it.v0, data: collapseRange(it.mesh, it.v0, it.v1) }));
6323
- views.set(event.part, { mesh, saved, units: list.length, opening: event.opening });
6362
+ if (!view.saved) view.saved = view.items.map((it) => ({ it, v0: it.v0, data: collapseRange(it.mesh, it.v0, it.v1) }));
6363
+ view.mesh = mesh;
6364
+ view.units = list.length;
6365
+ views.set(hostId, view);
6324
6366
  invalidate();
6325
6367
  invalidateShadow();
6326
6368
  return true;
@@ -6334,7 +6376,11 @@ function installDestructionBreach(K, _shared) {
6334
6376
  return {
6335
6377
  /** Handles a `stage` event of the house's object. Returns true when the wall was redrawn as courses. */
6336
6378
  stage(event) {
6337
- return event.stage === 2 ? open(event) : false;
6379
+ return event.stage === 2 && !!event.opening ? draw2(event.part, { opening: event.opening }) : false;
6380
+ },
6381
+ /** Handles a `chip` event: the unit that came out leaves a gap in the courses. Returns true when the wall was redrawn. */
6382
+ chip(event) {
6383
+ return draw2(event.part, { chip: event.center });
6338
6384
  },
6339
6385
  /** The part broke or fell: the courses go (the plain range stays collapsed, the renderer hides the part). */
6340
6386
  drop(partId) {
@@ -6566,6 +6612,9 @@ function installDestructionWorld(K, shared) {
6566
6612
  case "stage":
6567
6613
  breach.stage(e);
6568
6614
  break;
6615
+ case "chip":
6616
+ breach.chip(e);
6617
+ break;
6569
6618
  case "collapse":
6570
6619
  for (const id of e.parts) {
6571
6620
  breach.drop(id);
@@ -6620,6 +6669,32 @@ function installDestructionWorld(K, shared) {
6620
6669
  if (D.active && D.world.has(D.id)) D.world.remove(D.id);
6621
6670
  D.active = false;
6622
6671
  };
6672
+ D.follow = (world, id, spec) => {
6673
+ D.spec = spec;
6674
+ const offFollow = world.on("*", (e) => {
6675
+ if (e.object !== id) return;
6676
+ switch (e.type) {
6677
+ case "stage":
6678
+ breach.stage(e);
6679
+ break;
6680
+ case "chip":
6681
+ breach.chip(e);
6682
+ break;
6683
+ case "fracture":
6684
+ breach.drop(e.part);
6685
+ break;
6686
+ case "collapse":
6687
+ for (const p of e.parts) breach.drop(p);
6688
+ break;
6689
+ default:
6690
+ break;
6691
+ }
6692
+ });
6693
+ return { restore: () => breach.restore(), dispose: () => {
6694
+ offFollow();
6695
+ breach.restore();
6696
+ } };
6697
+ };
6623
6698
  D.piece = (id) => D.active ? D.world.part(D.id, id) : null;
6624
6699
  D.state = () => D.active ? D.world.state(D.id) : null;
6625
6700
  D.reset = () => {
@@ -8459,6 +8534,7 @@ function createFloorplanTestModule(options = {}) {
8459
8534
  return {
8460
8535
  id: FLOORPLAN_AREA_ID,
8461
8536
  name: text.name,
8537
+ ground: { surface: "lawn" },
8462
8538
  group: text.group,
8463
8539
  get houses() {
8464
8540
  return placed;
@@ -8566,6 +8642,56 @@ import { inferJoints } from "@adatechnology/game-destruction";
8566
8642
  import { markKitOrigin as markKitOrigin2 } from "@adatechnology/game-contracts";
8567
8643
  import { residentialItems } from "@adatechnology/game-floorplan-residential";
8568
8644
  var WOOD_THIN = 0.035;
8645
+ var BOARD_TOP_THICK = 0.07;
8646
+ var BOARD_TOP_WIDTH = 0.5;
8647
+ var BOARD_WIDTH = 0.16;
8648
+ var SHELF_SPAN = 1.5;
8649
+ var SHELVING = /shelf|bookcase|rack/;
8650
+ function carcass(parts, drawn) {
8651
+ const all = [...drawn.values()];
8652
+ const width = Math.max(...all.map((p) => p.max[0])) - Math.min(...all.map((p) => p.min[0]));
8653
+ for (const part of parts) {
8654
+ if (LOOSE.has(part.material) || part.shape.kind !== "box" || part.rotation) continue;
8655
+ const [x, y, z] = part.shape.size;
8656
+ if (y >= x && y >= z) {
8657
+ if (z < x && x >= 0.6 * width) continue;
8658
+ part.structure = { role: "column" };
8659
+ } else if (y <= x && y <= z) part.structure = { role: "beam", maxSpan: SHELF_SPAN * Math.max(x, z) };
8660
+ }
8661
+ }
8662
+ function splitTopsIntoBoards(drawn, materialOf, carcassed) {
8663
+ const tops = /* @__PURE__ */ new Map();
8664
+ if (carcassed) return tops;
8665
+ for (let i = drawn.length - 1; i >= 0; i--) {
8666
+ const p = drawn[i];
8667
+ if (p.key !== "wood" || materialOf.wood !== "wood_hard" || p.shape.kind !== "box" || p.rotation) continue;
8668
+ const [sx, sy, sz] = p.shape.size;
8669
+ const across = sx < sz ? 0 : 2;
8670
+ const width = Math.min(sx, sz);
8671
+ if (sy < WOOD_THIN || sy > BOARD_TOP_THICK || sy >= width || width < BOARD_TOP_WIDTH) continue;
8672
+ const count = Math.round(width / BOARD_WIDTH);
8673
+ const boards = [];
8674
+ for (let b = 0; b < count; b++) {
8675
+ const size = [sx, sy, sz];
8676
+ size[across] = width / count;
8677
+ const position = [...p.position];
8678
+ position[across] += (b + 0.5) * size[across] - width / 2;
8679
+ const min = [...p.min];
8680
+ const max = [...p.max];
8681
+ min[across] = position[across] - size[across] / 2;
8682
+ max[across] = position[across] + size[across] / 2;
8683
+ boards.push({ ...p, id: `${p.id}_b${b}`, shape: { kind: "box", size }, position, min, max, color: shadeBoard(p.color, b) });
8684
+ }
8685
+ drawn.splice(i, 1, ...boards);
8686
+ tops.set(p.id, boards);
8687
+ }
8688
+ return tops;
8689
+ }
8690
+ function shadeBoard(color, board) {
8691
+ const k = [1, 0.94, 1.05, 0.97, 1.03][board % 5];
8692
+ const channel = (shift) => Math.min(255, Math.round((color >> shift & 255) * k)) << shift;
8693
+ return channel(16) | channel(8) | channel(0);
8694
+ }
8569
8695
  var FLOOR_TOUCH = 5e-3;
8570
8696
  var LOOSE = /* @__PURE__ */ new Set(["paper"]);
8571
8697
  var BASE_MATERIAL = {
@@ -8643,10 +8769,12 @@ function recordSpec(THREE, name, root, run, opts) {
8643
8769
  };
8644
8770
  run(A);
8645
8771
  if (!drawn.length) throw new Error(`game-floorplan-three: the builder of "${name}" drew no box or cylinder`);
8772
+ const boardsOf = splitTopsIntoBoards(drawn, materialOf, Boolean(opts.carcass));
8646
8773
  const parts = drawn.map((p) => {
8647
8774
  const thin = Math.min(...p.shape.kind === "box" ? p.shape.size : p.shape.kind === "cylinder" ? [p.shape.radius * 2, p.shape.height] : [p.shape.radius * 2]);
8648
8775
  let material = materialOf[p.key] ?? "plastic";
8649
- if (p.key === "wood" && material === "wood_hard" && thin < WOOD_THIN) material = "wood_soft";
8776
+ if (p.key === "wood" && material === "wood_hard" && opts.carcass && !opts.materials?.wood) material = "mdf";
8777
+ else if (p.key === "wood" && material === "wood_hard" && thin < WOOD_THIN) material = "wood_soft";
8650
8778
  const part = { id: p.id, material, shape: p.shape, position: p.position };
8651
8779
  if (p.rotation) part.rotation = p.rotation;
8652
8780
  if (!LOOSE.has(material) && p.min[1] <= FLOOR_TOUCH) part.anchored = true;
@@ -8681,6 +8809,14 @@ function recordSpec(THREE, name, root, run, opts) {
8681
8809
  if (area < 0.6 * footprint || p.max[1] < 0.2) continue;
8682
8810
  if (!top || p.max[1] > top.y + 1e-6) top = { part: p.id, y: p.max[1] };
8683
8811
  }
8812
+ for (const boards of boardsOf.values()) {
8813
+ if (top && boards[0].max[1] <= top.y - 1e-6) continue;
8814
+ const mid = boards[boards.length >> 1];
8815
+ const min = [Math.min(...boards.map((b) => b.min[0])), Math.min(...boards.map((b) => b.min[2]))];
8816
+ const max = [Math.max(...boards.map((b) => b.max[0])), Math.max(...boards.map((b) => b.max[2]))];
8817
+ if ((max[0] - min[0]) * (max[1] - min[1]) >= 0.6 * footprint && mid.max[1] >= 0.2) top = { part: mid.id, y: mid.max[1], parts: boards.map((b) => b.id) };
8818
+ }
8819
+ if (opts.carcass) carcass(parts, byId);
8684
8820
  const spec = { name, parts, joints, tags: ["furniture"] };
8685
8821
  const colors = new Map(drawn.map((p) => [p.id, p.color]));
8686
8822
  const build = (T3) => {
@@ -8724,7 +8860,7 @@ function furnitureSpec(THREE, type, opts = {}) {
8724
8860
  if (w === void 0 || d === void 0 || h === void 0) throw new Error(`game-floorplan-three: furniture type "${type}" is not in the catalogue, so give w, d and h`);
8725
8861
  const o = opts.options ?? {};
8726
8862
  const seed = Math.round(opts.seed ?? 1);
8727
- const plain = /shelf|bookcase|rack/.test(type) ? "paper" : "plastic";
8863
+ const plain = SHELVING.test(type) ? "paper" : "plastic";
8728
8864
  const root = { x: seed, z: 0, ry: 0, w, d, h, y: 0, base: 0, palette: o.palette, seed: `furniture_${type}_${seed}` };
8729
8865
  const { K, shared } = renderer.internals;
8730
8866
  const before = K.PARTS.length;
@@ -8743,7 +8879,7 @@ function furnitureSpec(THREE, type, opts = {}) {
8743
8879
  shared.ctx = saved.ctx;
8744
8880
  }
8745
8881
  },
8746
- { materials: { plain, ...o.materials }, chairs: o.chairs }
8882
+ { materials: { plain, ...o.materials }, chairs: o.chairs, carcass: SHELVING.test(type) }
8747
8883
  );
8748
8884
  const spouts = K.PARTS.slice(before).filter((p) => p.type === "water_tap").map((p) => [p.pos[0] - root.x, p.pos[1], p.pos[2] - root.z]);
8749
8885
  K.PARTS.length = before;
@@ -8811,7 +8947,9 @@ function wallPlan(options) {
8811
8947
  function draw(THREE, plan, options) {
8812
8948
  return create(THREE, { ...options, fleet: null }).build(plan, { quality: "game", terrain: false, noPerimeter: true, pieces: "meshes" });
8813
8949
  }
8950
+ var houses = /* @__PURE__ */ new WeakMap();
8814
8951
  function label(house) {
8952
+ houses.set(house.group, house);
8815
8953
  for (const [id, items] of Object.entries(house.destruction.ranges())) {
8816
8954
  for (const item of items) {
8817
8955
  const object = item.isObject3D ? item : item.mesh;
@@ -8824,12 +8962,13 @@ function wallPiece(THREE, options) {
8824
8962
  const plan = wallPlan(options);
8825
8963
  const houseOptions = options.house ?? {};
8826
8964
  const probe = draw(THREE, plan, { ...houseOptions, destruction: void 0, destructionRenderer: void 0 });
8827
- const spec = probe.structure.spec({ wallDetail: "coarse" });
8965
+ const spec = probe.structure.spec();
8828
8966
  probe.dispose();
8829
8967
  return {
8830
8968
  plan,
8831
8969
  spec,
8832
- build: (T) => label(draw(T, plan, houseOptions))
8970
+ build: (T) => label(draw(T, plan, houseOptions)),
8971
+ follow: (root, world, objectId) => houses.get(root)?.destruction?.follow?.(world, objectId, spec) ?? null
8833
8972
  };
8834
8973
  }
8835
8974
  export {