// Path display — a Meep example for PathDisplay + PathDisplaySystem.
//
// Four paths, four ways to draw them (far to near):
//
// 1 ENTITY MARKERS a blueprint-stamped start cube, sphere
// waypoints, and an arrowhead cone at the end
// 2 STACKED SPECS one zigzag path, TWO tube specs: a wide
// translucent sheath over a thin solid core
// 3 TUBE, animated path_mask marching dashes (round caps) on a wave —
// the mask is rewritten every frame, re-tessellated
// 4 TUBE, static a smooth Catmull-Rom wave
//
// The pieces:
//
// - `Path` (from the navigation package) is the curve itself: a point list
// plus an interpolation mode (Linear or CatmullRom). It's the same
// component path-following agents consume — PathDisplay just draws it.
// - `PathDisplay` holds a list of `PathDisplaySpec`s — (type, style) pairs.
// One path can carry several specs at once (lane 2).
// - `PathDisplaySystem` watches Path + PathDisplay entities and builds the
// geometry. After mutating a style, send `PathEvents.Changed` to the
// entity and the display rebuilds (lane 3 does this every frame).
//
// How Shade draws a tube — and what that costs the style:
//
// A tube is dynamic by definition: lane 3 rewrites its mask every frame, so
// the geometry is rebuilt every frame. Meshlet clustering (what a
// `ShadedGeometry` carries) is a per-shape PRECOMPUTE, so tubes go down
// Shade's separate dynamic-mesh path instead: plain attribute arrays,
// re-uploaded as they change, drawn after the scene in one flat colour with
// straight alpha. That path is unlit and depth-tested but not depth-writing.
// `TubePathStyle` still carries `material_type`, `material`, `cast_shadow`
// and `receive_shadow` from the three.js renderer — under Shade nothing
// reads them, so this file no longer sets them. What still drives the look
// is `color`, `opacity`, `width`, `resolution`, `cap_type`, `shape` and
// `path_mask`.
//
// `PathDisplayType.Entity` went the same way from the other end: the system
// draws tubes and only tubes, because markers are entities carrying their
// own components and something else already knows how to render those.
// Lane 1 therefore stamps its markers itself, from the same `EntityPath` +
// `EntityPathStyle` pair the display used to build internally — the markers
// are `SGMesh` entities, so `MeshSystem` and the glTF loader are registered
// alongside `PathDisplaySystem`.
//
// Sections:
// §1 Tuning constants
// §2 Engine bootstrap
// §3 Ground
// §4 Path builders the curves
// §5 The four lanes one display style each
// §6 Per-frame march the dashes, HUD
import { EngineHarness } from "@woosh/meep-engine/src/engine/EngineHarness.js";
import { Light } from "@woosh/meep-engine/src/engine/graphics/ecs/light/Light.js";
import { LightType } from "@woosh/meep-engine/src/engine/graphics/ecs/light/LightType.js";
import { GameAssetType } from "@woosh/meep-engine/src/engine/asset/GameAssetType.js";
import { GLTFSceneBundleAssetLoader } from "@woosh/meep-engine/src/engine/asset/loaders/GLTFSceneBundleAssetLoader.js";
import { ArrayBufferLoader } from "@woosh/meep-engine/src/engine/asset/loaders/ArrayBufferLoader.js";
import { load_model_scene_bundle } from "@woosh/meep-engine/src/engine/asset/load_model_scene_bundle.js";
import Entity from "@woosh/meep-engine/src/engine/ecs/Entity.js";
import { EntityBlueprint } from "@woosh/meep-engine/src/engine/ecs/EntityBlueprint.js";
import { Transform64 } from "@woosh/meep-engine/src/engine/ecs/transform/Transform64.js";
import { TransformAttachmentSystem } from "@woosh/meep-engine/src/engine/ecs/transform-attachment/TransformAttachmentSystem.js";
import { ParentEntitySystem } from "@woosh/meep-engine/src/engine/ecs/parent/ParentEntitySystem.js";
import { ShadedGeometry } from "@woosh/meep-engine/src/engine/graphics/ecs/mesh-v2/ShadedGeometry.js";
import { ShadedGeometrySystem } from "@woosh/meep-engine/src/engine/graphics3/ShadedGeometrySystem.js";
import { SGMesh } from "@woosh/meep-engine/src/engine/graphics/ecs/mesh-v2/aggregate/SGMesh.js";
import { MeshSystem } from "@woosh/meep-engine/src/engine/graphics3/MeshSystem.js";
import { make_box_geometry } from "@woosh/meep-engine/src/shade/renderer/geometry/primitives/make_box_geometry.js";
import { meshlet_geometry_build_from_geometry } from "@woosh/meep-engine/src/shade/renderer/geometry/meshlet_geometry_build_from_geometry.js";
import { StandardShadeMaterial } from "@woosh/meep-engine/src/shade/renderer/material/StandardShadeMaterial.js";
import Path from "@woosh/meep-engine/src/engine/navigation/ecs/components/Path.js";
import { PathEvents } from "@woosh/meep-engine/src/engine/navigation/ecs/components/PathEvents.js";
import { InterpolationType } from "@woosh/meep-engine/src/engine/navigation/ecs/components/InterpolationType.js";
import { PathDisplay } from "@woosh/meep-engine/src/engine/graphics/ecs/path/PathDisplay.js";
import { PathDisplaySpec } from "@woosh/meep-engine/src/engine/graphics/ecs/path/PathDisplaySpec.js";
import { PathDisplaySystem } from "@woosh/meep-engine/src/engine/graphics3/PathDisplaySystem.js";
import { PathDisplayType } from "@woosh/meep-engine/src/engine/graphics/ecs/path/PathDisplayType.js";
import { TubePathStyle } from "@woosh/meep-engine/src/engine/graphics/ecs/path/tube/TubePathStyle.js";
import { CapType } from "@woosh/meep-engine/src/engine/graphics/ecs/path/tube/CapType.js";
import { EntityPath } from "@woosh/meep-engine/src/engine/graphics/ecs/path/entity/EntityPath.js";
import { EntityPathStyle } from "@woosh/meep-engine/src/engine/graphics/ecs/path/entity/EntityPathStyle.js";
import { EntityPathMarkerDefinition } from "@woosh/meep-engine/src/engine/graphics/ecs/path/entity/EntityPathMarkerDefinition.js";
import { clamp01 } from "@woosh/meep-engine/src/core/math/clamp01.js";
import { Color } from "@woosh/meep-engine/src/core/color/Color.js";
import Vector3 from "@woosh/meep-engine/src/core/geom/Vector3.js";
// ─── §1 Tuning constants ────────────────────────────────────────────────────
const LANE_LENGTH = 16; // each path runs this far along X
const LANE_SPACING = 3.2; // metres between lanes, along Z
const PATH_Y = 1.0; // hover height
const DASH_COUNT = 9; // marching dashes on lane 3
const DASH_FILL = 0.5; // fraction of each dash period that is "on"
const DASH_SPEED = 0.12; // mask offset per second
// Tube colours are written as sRGB hex — what a designer types — and decoded
// once. The dynamic-mesh pass writes its flat colour straight into the scene's
// linear HDR colour, so an encoded value handed over as-is would come out
// washed out after tone mapping.
const CORE_COLOR = "#ff8c26"; // lane 2, sheath and core share it
const DASH_COLOR = "#4ef0a8"; // lane 3, Meep brand green
const WAVE_COLOR = "#c7d1e0"; // lane 4
// ─── §2 Engine bootstrap ────────────────────────────────────────────────────
const engine = await EngineHarness.bootstrap({
configuration: (config, engine) => {
const scene = EngineHarness.shadeScene(engine);
config.addSystem(new ShadedGeometrySystem(engine.graphics, scene));
// Builds tube geometry for Path + PathDisplay entities. It draws them
// itself, through a render extension of its own, so it needs the
// graphics facade and nothing else.
config.addSystem(new PathDisplaySystem(engine.graphics));
// The marker entities of lane 1 are SGMesh models: MeshSystem puts
// them in the same Shade scene, loading each glTF through the asset
// manager (once per url, however many placements share it).
config.addSystem(new MeshSystem(
engine.graphics,
scene,
(url) => load_model_scene_bundle(engine.assetManager, url),
));
// A model with no skin and no clips is *expanded into entities*: its
// glTF nodes become children of the entity that placed it, each with a
// local Transform64 and a ShadedGeometry of its own. Those children are
// parented through TransformAttachment + ParentEntity, so without
// these two systems every node draws at its local transform — which
// for these markers is the world origin, all eleven of them stacked.
config.addSystem(new TransformAttachmentSystem());
config.addSystem(new ParentEntitySystem());
const gltf = new GLTFSceneBundleAssetLoader();
config.addLoader(GameAssetType.ModelGLTF, gltf);
config.addLoader(GameAssetType.ModelGLTF_JSON, gltf);
// The glTF parse asks the manager back for the file's own bytes — the
// `.glb` container here, a `.bin` buffer for a `.gltf` — so the type
// it requests them under needs a loader too. (A textured model would
// also want an `ImageBitmapAssetLoader` on GameAssetType.ImageBitmap;
// these markers are flat-coloured, so nothing asks for one.)
config.addLoader(GameAssetType.ArrayBuffer, new ArrayBufferLoader());
},
});
await EngineHarness.buildBasics({
engine,
enableTerrain: false,
enableWater: false,
enableLights: true,
enableShadows: true,
shadowmapResolution: 2048,
focus: new Vector3(0, 0.6, 0),
distance: 21,
pitch: 0.7,
yaw: 0,
cameraFarDistance: 200,
showFps: false,
});
// Exposure and key light, the same in every demo on this site. Shade's automatic
// exposure is off: these scenes are lit to be looked at, not adapted to, and an
// adapting frame makes two screenshots of the same demo disagree. That leaves
// the light doing the work - and a directional light's intensity is read as lux,
// where the harness's default of 0.9 is a value from the three.js era that only
// ever looked lit because auto-exposure opened up to meet it.
engine.graphics.renderer.feature_automatic_exposure_enabled = false;
engine.entityManager.dataset.traverseEntities([Light], (light) => {
if (light.type.getValue() === LightType.DIRECTION) {
light.intensity.set(6);
}
});
const ecd = engine.entityManager.dataset;
// ─── §3 Ground ──────────────────────────────────────────────────────────────
{
const t = new Transform64();
t.setTranslation(0, -0.25, 0);
const geometry = meshlet_geometry_build_from_geometry(
make_box_geometry(LANE_LENGTH + 6, 0.5, 4 * LANE_SPACING + 4),
);
const material = new StandardShadeMaterial();
material.diffuse_color.copy(Color.from_sRGB_to_linear(Color.parse("#161c24")));
material.roughness_factor = 1;
material.metallic_factor = 0;
new Entity()
.add(t)
.add(ShadedGeometry.from(geometry, material))
.build(ecd);
}
// ─── §4 Path builders ───────────────────────────────────────────────────────
//
// A Path is just points + an interpolation mode. CatmullRom turns sparse
// control points into a smooth spline at display time; Linear keeps the
// corners. These are the same components a PathFollowingSystem agent would
// walk — the display layer is purely additive.
function makePath(points, interpolation) {
const p = new Path();
p.interpolation = interpolation;
p.setPositionsFromVectorArray(points);
return p;
}
function wave(z, amplitude, cycles, n = 9) {
const pts = [];
for (let i = 0; i < n; i++) {
const f = i / (n - 1);
pts.push(new Vector3(
(f - 0.5) * LANE_LENGTH,
PATH_Y,
z + Math.sin(f * Math.PI * cycles) * amplitude,
));
}
return pts;
}
function zigzag(z, amplitude, steps) {
const pts = [];
for (let i = 0; i <= steps; i++) {
const f = i / steps;
pts.push(new Vector3(
(f - 0.5) * LANE_LENGTH,
PATH_Y,
z + (i % 2 === 0 ? -amplitude : amplitude),
));
}
return pts;
}
// ─── §5 The four lanes ──────────────────────────────────────────────────────
// Lane 1 (far) — ENTITY MARKERS: a cube at the start, spheres along the way,
// an arrowhead cone at the end. Each marker is stamped from an
// EntityBlueprint (Transform64 + SGMesh templates — anything serializable
// works); `align` orients the marker's +Z along the travel direction, which is
// what points the cone forward.
//
// `EntityPath` is the thing that walks the curve and stamps one blueprint per
// waypoint. PathDisplaySystem used to own an instance of it per entity-typed
// spec; under Shade it draws tubes only, so the demo drives EntityPath itself
// and builds the entities it produced into the dataset. The style — spacing,
// three marker definitions — is unchanged.
{
const markerBlueprint = (url) => {
const bp = new EntityBlueprint();
bp.addJSON(Transform64, new Transform64().toJSON());
bp.addJSON(SGMesh, { url, castShadow: true, receiveShadow: true });
return bp;
};
const style = new EntityPathStyle();
style.marker_start = EntityPathMarkerDefinition.from({ blueprint: markerBlueprint("./models/marker-start.glb") });
style.marker_main = EntityPathMarkerDefinition.from({ blueprint: markerBlueprint("./models/marker-mid.glb") });
style.marker_end = EntityPathMarkerDefinition.from({ blueprint: markerBlueprint("./models/marker-end.glb") });
style.spacing = 1.4; // one waypoint sphere every ~1.4 m
const path = makePath(wave(-1.5 * LANE_SPACING, 0.8, 1.5), InterpolationType.CatmullRom);
const markers = new EntityPath();
markers.setPath(path);
markers.setStyle(style);
markers.build(); // positions + orients every marker
for (let i = 0; i < markers.markers.length; i++) {
markers.markers[i].entity.build(ecd);
}
}
// Lane 2 — STACKED SPECS on one path: a hard zigzag (Linear interpolation —
// note the corners survive) drawn TWICE: a wide translucent sheath with a
// thin solid core inside. PathDisplay.specs is a list; this is its point.
{
const core = new TubePathStyle();
core.color.copy(Color.from_sRGB_to_linear(Color.parse(CORE_COLOR)));
core.width = 0.12;
core.resolution = 4; // Linear path — no curvature to chase
core.cap_type = CapType.Round;
const sheath = new TubePathStyle();
sheath.color.copy(Color.from_sRGB_to_linear(Color.parse(CORE_COLOR)));
sheath.opacity = 0.22; // translucent over-tube
sheath.width = 0.45;
sheath.resolution = 4;
sheath.cap_type = CapType.Round;
new Entity()
.add(makePath(zigzag(-0.5 * LANE_SPACING, 0.9, 8), InterpolationType.Linear))
.add(PathDisplay.from(
PathDisplaySpec.from(PathDisplayType.Tube, sheath),
PathDisplaySpec.from(PathDisplayType.Tube, core),
))
.build(ecd);
}
// Lane 3 — TUBE with an ANIMATED path_mask: marching dashes. The mask is a
// flat list of [start, end] fractions along the path; rewrite it and send
// PathEvents.Changed and the tube re-tessellates to match — one span is one
// tube with its own caps, so a nine-dash mask is nine little pills. §6 does
// that every frame.
const dashStyle = new TubePathStyle();
dashStyle.color.copy(Color.from_sRGB_to_linear(Color.parse(DASH_COLOR)));
dashStyle.width = 0.3;
dashStyle.resolution = 32;
dashStyle.cap_type = CapType.Round;
// NOTE: keep the Entity INSTANCE — build() returns the entity id, but
// sendEvent() (used in §6 to trigger re-tessellation) lives on the instance.
const dashEntity = new Entity()
.add(makePath(wave(0.5 * LANE_SPACING, 1.2, 3), InterpolationType.CatmullRom))
.add(PathDisplay.from(PathDisplaySpec.from(PathDisplayType.Tube, dashStyle)));
dashEntity.build(ecd);
// Lane 4 (near) — TUBE, plain: a smooth wave at full width, the baseline the
// other three vary from.
{
const style = new TubePathStyle();
style.color.copy(Color.from_sRGB_to_linear(Color.parse(WAVE_COLOR)));
style.width = 0.45; // tube diameter, world units
style.resolution = 24; // spline samples per segment
style.cap_type = CapType.Round;
new Entity()
.add(makePath(wave(1.5 * LANE_SPACING, 1.0, 2), InterpolationType.CatmullRom))
.add(PathDisplay.from(PathDisplaySpec.from(PathDisplayType.Tube, style)))
.build(ecd);
}
// ─── §6 Per-frame: march the dashes, HUD ────────────────────────────────────
document.getElementById("lanes").textContent = "4";
const fpsEl = document.getElementById("fps");
let time = 0;
let fpsWindow = 0, fpsFrames = 0;
let lastFrameMs = performance.now();
engine.graphics.on.postRender.add(() => {
const nowMs = performance.now();
const dt = Math.min((nowMs - lastFrameMs) / 1000, 0.1);
lastFrameMs = nowMs;
time += dt;
// Rebuild lane 3's mask: DASH_COUNT windows, each DASH_FILL of a period
// wide, all sliding along the path and wrapping at the ends.
const mask = [];
const period = 1 / DASH_COUNT;
const offset = (time * DASH_SPEED) % period;
for (let i = 0; i < DASH_COUNT + 1; i++) {
const start = i * period + offset - period;
const end = start + period * DASH_FILL;
if (end <= 0 || start >= 1) continue;
mask.push(clamp01(start), clamp01(end));
}
dashStyle.path_mask = mask;
dashEntity.sendEvent(PathEvents.Changed); // ask the system to re-tessellate
// HUD
fpsWindow += dt;
fpsFrames++;
if (fpsWindow >= 0.5) {
fpsEl.textContent = (fpsFrames / fpsWindow).toFixed(0);
fpsWindow = 0;
fpsFrames = 0;
}
});
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Path display · Meep</title>
<meta name="robots" content="noindex">
<style>
*, *::before, *::after { box-sizing: border-box; }
html, body {
margin: 0; padding: 0;
width: 100%; height: 100%;
overflow: hidden;
background: #07090c;
color: #e6edf3;
font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
}
.panel {
position: fixed;
z-index: 100;
background: rgba(7, 9, 12, 0.72);
border: 1px solid #1f2731;
border-radius: 10px;
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
box-shadow: 0 12px 32px rgba(0,0,0,0.4);
}
.hud {
top: 1rem; left: 1rem;
padding: 0.8rem 1rem;
font-family: ui-monospace, "JetBrains Mono", monospace;
font-size: 0.82rem;
line-height: 1.7;
color: #9aa5b1;
min-width: 180px;
}
.hud .label {
color: #6b7785;
text-transform: uppercase;
letter-spacing: 0.1em;
font-size: 0.65rem;
margin-right: 0.5rem;
}
.hud .value { color: #4ef0a8; }
.legend {
bottom: 1rem; left: 1rem;
padding: 0.8rem 1rem;
font-size: 0.82rem;
line-height: 1.55;
max-width: 460px;
color: #9aa5b1;
}
.legend strong { color: #e6edf3; }
</style>
</head>
<body>
<div class="panel hud">
<div><span class="label">fps</span><span class="value" id="fps">--</span></div>
<div><span class="label">lanes</span><span class="value" id="lanes">--</span></div>
</div>
<div class="panel legend">
Four paths drawn by <strong>PathDisplay</strong>: blueprint-stamped
<strong>entity markers</strong> (cube start, sphere waypoints, arrowhead end);
a zigzag with <strong>two stacked tube specs</strong> — translucent sheath over
a solid core; a wave of <strong>marching dashes</strong> (an animated
<code>path_mask</code>); and a plain round-capped <strong>tube</strong>.
Drag to orbit · scroll to zoom.
</div>
<script type="module" src="./src/main.js"></script>
</body>
</html>
{
"title": "Path display",
"description": "Four paths, four display styles: blueprint-stamped entity markers, stacked tube specs, a wave of lit marching dashes animated through path_mask, and a lit shadow-casting tube.",
"category": "Rendering",
"status": "live",
"order": 7,
"tags": ["rendering", "path", "tube", "spline", "navigation", "ecs"],
"sourceHint": "examples-src/path-display/",
"demoUrl": "/examples/path-display/demo.html",
"defaultFile": "src/main.js"
}
{
"name": "@meep-examples/path-display",
"version": "0.1.0",
"private": true,
"type": "module",
"description": "Four paths, four display styles: entity markers, stacked tube specs, a wave of animated marching dashes via path_mask, and a lit tube.",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@woosh/meep-engine": "3.41.0"
},
"devDependencies": {
"@rollup/plugin-strip": "^3.0.4",
"vite": "^8.0.13"
}
}
# Path display
Four paths, four ways to draw them with `PathDisplay` + `PathDisplaySystem`:
1. **Entity markers** — a cube at the start, sphere waypoints every ~1.4 m,
and an arrowhead cone at the end, each stamped from an `EntityBlueprint`
(Transform64 + `SGMesh` templates pointing at three tiny bundled GLBs).
`align` orients each marker's +Z along the travel direction — that's what
points the cone forward.
2. **Stacked specs** — one zigzag path (Linear interpolation, corners intact)
carrying TWO tube `PathDisplaySpec`s: a wide translucent sheath over a
thin solid core.
3. **Tube, animated `path_mask`** — a wave of round-capped marching dashes.
The mask is a flat list of `[start, end]` fractions along the path; the
demo rewrites it every frame and sends `PathEvents.Changed` so the system
re-tessellates. One span is one tube with its own caps, so a nine-dash mask
is nine separate little pills.
4. **Tube, plain** — a Catmull-Rom wave at full width, the baseline the other
three vary from.
What it demonstrates:
- `Path` (from the navigation package) is the curve: points + interpolation
mode. It's the same component path-following agents consume — the display
layer is purely additive.
- `PathDisplay.from(...specs)` — (type, style) pairs; one path can carry any
number of displays.
- `TubePathStyle` knobs: width, spline resolution, round / flat / no caps,
colour, opacity, the swept profile `shape`, and `path_mask` for partial or
dashed display.
- Live mutation: change a style, send `PathEvents.Changed` **to the Entity
instance** (`build()` returns the entity id — keep the instance around),
and the geometry rebuilds.
## How Shade draws a tube
A tube is dynamic by definition — lane 3 rewrites its mask every frame — and
meshlet clustering (what a `ShadedGeometry` carries) is a per-shape
*precompute*. So tubes go down Shade's separate dynamic-mesh path: plain
attribute arrays re-uploaded as they change, drawn after the scene in one flat
colour with straight alpha, depth-tested but not depth-writing.
Two consequences a reader should know about:
- **Tubes are unlit and cast no shadows.** `TubePathStyle` carries
`material_type`, `material`, `cast_shadow` and `receive_shadow`, but nothing
reads them, so this demo does not set them.
Colour is taken straight from `style.color × style.opacity`, and it is
written into a linear HDR target — so decode sRGB hex through
`Color.from_sRGB_to_linear` before assigning it.
- **`PathDisplayType.Entity` is not drawn by the system.** It builds marker
entities, which carry their own components and are rendered by whatever
renders those. Lane 1 therefore drives `EntityPath` itself and builds the
entities it stamps into the dataset.
Wiring note: the markers are `SGMesh` models, so `MeshSystem` and the glTF
loaders are registered alongside `PathDisplaySystem`. A model with no skin and
no clips is *expanded into entities* — its glTF nodes become children of the
entity that placed it — so `TransformAttachmentSystem` and `ParentEntitySystem`
have to be registered too, or every node draws at the world origin.
## Run
```sh
npm install
npm run dev
```
## Build
`npm run build` emits the static demo into `public/examples/path-display/`.
import { defineConfig } from "vite";
import { copyFileSync, existsSync, mkdirSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { resolve, dirname } from "node:path";
import strip from "@rollup/plugin-strip";
const __dirname = dirname(fileURLToPath(import.meta.url));
export default defineConfig({
// Vite's dependency pre-bundling rewrites the engine's own asset imports and
// loses their URLs, so a dev server ends up serving ".../deps/undefined" for
// the renderer's data files. The engine is source-only ESM and needs no
// pre-bundling - but its CommonJS dependencies do, or the import of them from inside
// the excluded engine finds no default export.
optimizeDeps: {
exclude: ["@woosh/meep-engine"],
include: ["dat.gui", "opentype.js"],
},
plugins: [
{
// Copy the committed source thumbnail into the generated gallery folder.
// public/examples/<id>/ is build output (gitignored); thumbnail.png is
// kept in source here and copied through on every build so the gallery
// (src/data/examples.ts) can resolve it.
name: "copy-thumbnail",
apply: "build",
closeBundle() {
const thumb = resolve(__dirname, "thumbnail.png");
const dst = resolve(__dirname, "../../public/examples/path-display");
if (existsSync(thumb)) {
mkdirSync(dst, { recursive: true });
copyFileSync(thumb, resolve(dst, "thumbnail.png"));
}
},
},
],
base: "./",
build: {
outDir: resolve(__dirname, "../../public/examples/path-display"),
emptyOutDir: false,
rollupOptions: {
input: resolve(__dirname, "demo.html"),
plugins: [
{
// this will remove all assert statements from the production build
...strip(),
apply: 'build'
}
],
},
target: "es2022",
},
});