サンプル

2 つの立体の重なりと離れを調べる

架空の建物と設備をレベル 24 のセルの集合にして、重なっているセルの数と、離れている距離を求めます。設備はスライダーで動かせます。

新しいタブで開く

コード

ビルドせずにブラウザで読み込める形で書いています。index.html の import map で、@mirgrid/web-sdk をこのサイトに置いた SDK に結び付けています。npm でインストールしてバンドラを使うプロジェクトでは、import map は要りません。

main.js

import init, { CellSetJs, VoxelizerJs, encode_voxel_packet, voxel_faces_from_packet } from '@mirgrid/web-sdk';
import { createVoxelRenderer } from '@mirgrid/web-sdk/render.mjs';

await init();

const LEVEL = 24; // セルの一辺 1 m
const ORIGIN = [139.7671248, 35.6812362, 40]; // 東京駅のあたり(経度, 緯度, 楕円体高)
const MAX_STEPS = 20; // 離れはこのセル数まで測る(スライダーで動かせる範囲はこれで足りる)。大きくするほど計算に時間がかかる

// 東京駅のあたりに、地面に水平な「現場に揃えた格子」を作る。
// この格子のセルの番号は x が東、y が北、z が上に増えるので、箱を番号で並べられる。
const locator = VoxelizerJs.siteAuto(LEVEL);
locator.setInput({ coordSystem: 'latlon' });
locator.pushPoints(Float64Array.of(...ORIGIN));
const frame = locator.frameDescriptor();
const [bx, by, bz] = locator.cellInfo(0).index;
locator.free();

// 基準のセルから (x, y) ずらした位置に、東西 w × 南北 d × 高さ h セルの箱を作る
function box(x, y, w, d, h) {
  const flat = [];
  for (let k = 0; k < h; k++) {
    for (let j = 0; j < d; j++) {
      for (let i = 0; i < w; i++) flat.push(bx + x + i, by + y + j, bz + k);
    }
  }
  return CellSetJs.from_indices(LEVEL, new Float64Array(flat));
}

const canvas = document.querySelector('canvas');
const renderer = createVoxelRenderer(canvas);
const east = document.querySelector('#east');
const north = document.querySelector('#north');

// 建物は動かないので 1 回だけ作る: 20 × 12 × 9 セル。中心が基準のセルに来るように置く
const building = box(-10, -6, 20, 12, 9);

// 集合のセルを 1 色で packet にして、描画器のレイヤとして置く
function showLayer(id, set, [r, g, b]) {
  if (set.size === 0) {
    renderer.removeLayer(id);
    return;
  }
  const flat = set.indices(); // [vx, vy, vz, vx, vy, vz, …]
  const records = [];
  for (let i = 0; i < flat.length; i += 3) {
    records.push({ vx: flat[i], vy: flat[i + 1], vz: flat[i + 2], r, g, b });
  }
  const bytes = encode_voxel_packet(records, LEVEL);
  // 3 つのレイヤは同じ格子・同じ原点で面を作る(原点が違うと setLayer が Error になる)
  const faces = voxel_faces_from_packet(bytes, 0, frame, ORIGIN);
  renderer.setLayer(id, faces, { visible: toggle(id).checked });
}

// 凡例のチェックボックスで、レイヤを出したり隠したりする
const toggle = (id) => document.querySelector(`[data-layer="${id}"]`);
for (const id of ['building', 'equipment', 'overlap']) {
  toggle(id).addEventListener('change', () => {
    // 空の集合のレイヤは取り除いてあるので、あるときだけ切り替える
    if (renderer.layerIds.includes(id)) renderer.setVisible(id, toggle(id).checked);
  });
}

function update() {
  const dx = Number(east.value);
  const dy = Number(north.value);
  document.querySelector('#east-value').value = `${dx > 0 ? '+' : ''}${dx} m`;
  document.querySelector('#north-value').value = `${dy > 0 ? '+' : ''}${dy} m`;

  // 設備: 6 × 4 × 9 セル。スライダーの値だけ、中心を建物の中心からずらす。
  // 建物と同じ高さなので、重なったところは上から見える
  const equipment = box(dx - 3, dy - 2, 6, 4, 9);
  const overlap = building.intersect(equipment);
  const buildingOnly = building.difference(equipment);
  const equipmentOnly = equipment.difference(building);
  const union = building.union(equipment);
  const distance = building.distance_m(equipment, MAX_STEPS);

  showLayer('building', buildingOnly, [160, 166, 176]);
  showLayer('equipment', equipmentOnly, [0, 113, 251]);
  showLayer('overlap', overlap, [222, 52, 40]);

  // distance_m は重なっていても接していても 0。重なりのセル数で見分ける
  let clearance;
  if (overlap.size > 0) clearance = '重なっています';
  else if (distance === 0) clearance = '接しています';
  else if (distance === null) clearance = `${MAX_STEPS} m より離れています`;
  else clearance = `${distance} m 離れています`;

  const n = (value) => value.toLocaleString('ja-JP');
  document.querySelector('#clearance').textContent = clearance;
  document.querySelector('#a-size').textContent = `${n(building.size)} セル`;
  document.querySelector('#b-size').textContent = `${n(equipment.size)} セル`;
  document.querySelector('#overlap').textContent = `${n(overlap.size)} セル(${n(overlap.size)} m³)`;
  document.querySelector('#union').textContent = `${n(union.size)} セル`;

  // この更新で作った集合は wasm のメモリにあるので、毎回解放する
  for (const set of [equipment, overlap, buildingOnly, equipmentOnly, union]) set.free();
}

update();
// 視点は最初に 1 回だけ決め、スライダーを動かしても保つ。設備が動く範囲全体が入るように南東の上から見る
renderer.setView({ targetEnu: [0, 0, 4], distanceM: 66, azimuthDeg: -55, elevationDeg: 35 });
east.addEventListener('input', update);
north.addEventListener('input', update);

index.html

<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>2 つの立体の重なりと離れを調べる</title>
<link rel="stylesheet" href="/assets/sample.css">
<style>
  output { font-family: var(--mono); color: var(--ink); }
  .legend { display: flex; flex-wrap: wrap; gap: 4px 14px; margin-bottom: 6px; }
  .legend label { display: flex; align-items: center; gap: 6px; color: var(--ink); }
  .legend .swatch { margin: 0; }
  #clearance { font-weight: 600; }
</style>
<script type="importmap">
{ "imports": { "@mirgrid/web-sdk": "/sdk/web-sdk/voxel_core.js", "@mirgrid/web-sdk/": "/sdk/web-sdk/" } }
</script>
<script type="module" src="main.js"></script>
</head>
<body>
<main class="viewer">
  <canvas aria-label="建物と設備のセルの 3D 表示"></canvas>
  <aside>
    <section>
      <h1>2 つの立体の重なりと離れ</h1>
      <p>東京駅のあたりに置いた建物と設備を、レベル 24(セルの一辺 1 m)のセルの集合にして比べます。建物と設備は、このサンプルのために作った架空のものです。</p>
      <div class="legend" role="group" aria-label="表示するもの">
        <label><input type="checkbox" data-layer="building" checked><span class="swatch" style="background: rgb(160 166 176)"></span>建物だけ</label>
        <label><input type="checkbox" data-layer="equipment" checked><span class="swatch" style="background: rgb(0 113 251)"></span>設備だけ</label>
        <label><input type="checkbox" data-layer="overlap" checked><span class="swatch" style="background: rgb(222 52 40)"></span>重なり</label>
      </div>
      <p class="note">重なったところは、上から見ると赤く見えます。チェックを外すと、そのレイヤを隠せます。</p>
    </section>
    <section>
      <h2>設備の位置(建物の中心から)</h2>
      <div class="fields" style="margin: 0">
        <label><span>東西(東が +) <output id="east-value"></output></span>
          <input id="east" type="range" min="-20" max="20" step="1" value="8"></label>
        <label><span>南北(北が +) <output id="north-value"></output></span>
          <input id="north" type="range" min="-12" max="12" step="1" value="3"></label>
      </div>
    </section>
    <section>
      <h2>結果</h2>
      <dl class="facts">
        <dt>離れ</dt><dd id="clearance"></dd>
        <dt>建物</dt><dd id="a-size"></dd>
        <dt>設備</dt><dd id="b-size"></dd>
        <dt>重なり</dt><dd id="overlap"></dd>
        <dt>合わせた範囲</dt><dd id="union"></dd>
      </dl>
      <p class="note">離れは、建物と設備の間にある空いたセルの数(1 セル = 1 m)です。斜めに離れているときは、東西と南北のうち大きいほうの数になります。</p>
    </section>
    <section>
      <p class="note">左ドラッグで回転、右ドラッグか Shift + ドラッグで移動、ホイールで拡大・縮小できます。</p>
    </section>
  </aside>
</main>
</body>
</html>