@drawcall/charta 0.1.18 → 0.1.20

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.
@@ -1,13 +1,20 @@
1
- import { LoadingManager, Loader } from 'three';
1
+ import { LoadingManager, Loader, Vector3 } from 'three';
2
2
  import { Interpreter } from '../interpreter.js';
3
+ /**
4
+ * Function type for measuring a GLB model's bounding box size.
5
+ * Takes a URL and returns a Vector3 with the model's dimensions (x, y, z).
6
+ */
7
+ export type MeasureGlbFunction = (url: string) => Promise<Vector3>;
3
8
  export type AssetLoaderOptions = {
4
9
  /**
5
- * Mock mode: skips actual asset loading and creates placeholder objects.
6
- * Useful for server-side validation. Default: false.
10
+ * Measure-only mode: uses the provided function to measure model sizes,
11
+ * then creates lightweight PrefabBatchBuilders with correct sizes.
12
+ * The models themselves are not kept in memory.
13
+ * Useful for server-side validation where model sizes are needed.
7
14
  */
8
- mock?: boolean;
15
+ measureModelsOnlyWith?: MeasureGlbFunction;
9
16
  /**
10
- * Skip texture loading entirely. When true, loadTexture calls are ignored.
17
+ * Skip texture loading entirely. When true, loadTexture calls create placeholder textures.
11
18
  * Default: false.
12
19
  */
13
20
  skipTextureLoading?: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../../src/assets/loader.ts"],"names":[],"mappings":"AAAA,OAAO,EAAiC,cAAc,EAAqB,MAAM,EAAW,MAAM,OAAO,CAAA;AAEzG,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAM/C,MAAM,MAAM,kBAAkB,GAAG;IAC/B;;;OAGG;IACH,IAAI,CAAC,EAAE,OAAO,CAAA;IACd;;;OAGG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAA;CAC7B,CAAA;AAED,qBAAa,WAAY,SAAQ,MAAM,CAAC,IAAI,EAAE,WAAW,CAAC;IACxD,OAAO,CAAC,aAAa,CAAe;IACpC,OAAO,CAAC,UAAU,CAAY;IAC9B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAoB;gBAEhC,OAAO,CAAC,EAAE,cAAc,EAAE,OAAO,GAAE,kBAAuB;IAOtE,IAAI,CACF,WAAW,EAAE,WAAW,EACxB,MAAM,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,IAAI,EAC5B,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,EAC3C,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,GAC/B,IAAI;YAyBO,iBAAiB;YA8BjB,eAAe;CAuC9B"}
1
+ {"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../../src/assets/loader.ts"],"names":[],"mappings":"AAAA,OAAO,EAAiC,cAAc,EAAqB,MAAM,EAAE,OAAO,EAAE,MAAM,OAAO,CAAA;AAEzG,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAM/C;;;GAGG;AACH,MAAM,MAAM,kBAAkB,GAAG,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;AAElE,MAAM,MAAM,kBAAkB,GAAG;IAC/B;;;;;OAKG;IACH,qBAAqB,CAAC,EAAE,kBAAkB,CAAA;IAC1C;;;OAGG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAA;CAC7B,CAAA;AAED,qBAAa,WAAY,SAAQ,MAAM,CAAC,IAAI,EAAE,WAAW,CAAC;IACxD,OAAO,CAAC,aAAa,CAAe;IACpC,OAAO,CAAC,UAAU,CAAY;IAC9B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAoB;gBAEhC,OAAO,CAAC,EAAE,cAAc,EAAE,OAAO,GAAE,kBAAuB;IAOtE,IAAI,CACF,WAAW,EAAE,WAAW,EACxB,MAAM,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,IAAI,EAC5B,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,EAC3C,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,GAC/B,IAAI;YAuBO,iBAAiB;YA8BjB,eAAe;CAqD9B"}
@@ -1,4 +1,4 @@
1
- import { Texture, TextureLoader, Object3D, Loader, Vector3 } from 'three';
1
+ import { Texture, TextureLoader, Object3D, Loader } from 'three';
2
2
  import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
3
3
  import { ChartaError } from '../errors.js';
4
4
  import { InstancedMeshGroup } from '../utils/instanced-mesh-group.js';
@@ -27,9 +27,7 @@ export class AssetLoader extends Loader {
27
27
  basePath = args.path.length === 0 ? undefined : args.path;
28
28
  }
29
29
  else if (key === 'loadTexture') {
30
- if (!this.options.skipTextureLoading) {
31
- promises.push(this.handleLoadTexture(interpreter, args.name, basePath, args.path, location));
32
- }
30
+ promises.push(this.handleLoadTexture(interpreter, args.name, basePath, args.path, location));
33
31
  }
34
32
  else if (key === 'loadModel') {
35
33
  promises.push(this.handleLoadModel(interpreter, args.name, basePath, args.path, location));
@@ -40,7 +38,7 @@ export class AssetLoader extends Loader {
40
38
  .catch((reason) => onError?.(reason));
41
39
  }
42
40
  async handleLoadTexture(interpreter, name, basePath, path, location) {
43
- if (this.options.mock) {
41
+ if (this.options.skipTextureLoading) {
44
42
  const texture = new Texture();
45
43
  texture.name = name;
46
44
  interpreter.setAsset(`${name}BaseColorTexture`, texture);
@@ -57,12 +55,20 @@ export class AssetLoader extends Loader {
57
55
  }
58
56
  }
59
57
  async handleLoadModel(interpreter, name, basePath, path, location) {
60
- if (this.options.mock) {
61
- const batchBuilder = new PrefabBatchBuilder(new Vector3(), () => new Object3D());
62
- interpreter.setAsset(`${name}Prefab`, batchBuilder);
58
+ const fullPath = new URL(path, basePath).href;
59
+ // Measure-only mode: use custom function to get size, don't load full model
60
+ if (this.options.measureModelsOnlyWith) {
61
+ try {
62
+ const size = await this.options.measureModelsOnlyWith(fullPath);
63
+ const batchBuilder = new PrefabBatchBuilder(size, () => new Object3D());
64
+ interpreter.setAsset(`${name}Prefab`, batchBuilder);
65
+ }
66
+ catch (err) {
67
+ interpreter.reportError(new ChartaError(`Failed to measure model ${name} at ${fullPath}: ${err instanceof Error ? err.message : String(err)}`, interpreter.getSource(), location));
68
+ }
63
69
  return;
64
70
  }
65
- const fullPath = new URL(path, basePath).href;
71
+ // Full mode: load model with Three.js (browser only)
66
72
  try {
67
73
  const gltf = await this.gltfLoader.loadAsync(fullPath);
68
74
  const scene = gltf.scene;
@@ -19,10 +19,10 @@ export type GrassOptions = GrassMaterialOptions & {
19
19
  */
20
20
  variationStrength?: number;
21
21
  /**
22
- * Mock mode: validates grass() calls exist but skips expensive blade generation.
22
+ * Validate-only mode: validates grass() calls exist but skips expensive blade generation.
23
23
  * Useful for server-side validation. Default: false.
24
24
  */
25
- mock?: boolean;
25
+ validateOnly?: boolean;
26
26
  };
27
27
  export declare class GrassMesh extends InstancedMesh {
28
28
  constructor(interpreter: Interpreter, material?: Material, opts?: GrassOptions);
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/grass/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,aAAa,EACb,QAAQ,EAMT,MAAM,OAAO,CAAC;AACf,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAsB,KAAK,oBAAoB,EAAE,MAAM,eAAe,CAAC;AAO9E,MAAM,MAAM,YAAY,GAAG,oBAAoB,GAAG;IAChD,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;OAGG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B;;;OAGG;IACH,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB,CAAC;AAEF,qBAAa,SAAU,SAAQ,aAAa;gBAExC,WAAW,EAAE,WAAW,EACxB,QAAQ,GAAE,QAAkC,EAC5C,IAAI,GAAE,YAAiB;CAoP1B"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/grass/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,aAAa,EACb,QAAQ,EAMT,MAAM,OAAO,CAAC;AACf,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAsB,KAAK,oBAAoB,EAAE,MAAM,eAAe,CAAC;AAO9E,MAAM,MAAM,YAAY,GAAG,oBAAoB,GAAG;IAChD,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;OAGG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B;;;OAGG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB,CAAC;AAEF,qBAAa,SAAU,SAAQ,aAAa;gBAExC,WAAW,EAAE,WAAW,EACxB,QAAQ,GAAE,QAAkC,EAC5C,IAAI,GAAE,YAAiB;CAuP1B"}
@@ -6,8 +6,8 @@ const UpVector = new Vector3(0, 1, 0);
6
6
  const colorHelper = new Color();
7
7
  export class GrassMesh extends InstancedMesh {
8
8
  constructor(interpreter, material = new MeshPhongMaterial(), opts = {}) {
9
- // Mock mode: validate grass calls exist, skip geometry allocation entirely
10
- if (opts.mock) {
9
+ // Validate-only mode: validate grass calls exist, skip geometry allocation entirely
10
+ if (opts.validateOnly) {
11
11
  super(undefined, material, 0);
12
12
  // Still validate all grass() calls to catch errors
13
13
  const rows = interpreter.getRows();
@@ -182,5 +182,7 @@ export class GrassMesh extends InstancedMesh {
182
182
  this.instanceMatrix.needsUpdate = true;
183
183
  if (this.instanceColor)
184
184
  this.instanceColor.needsUpdate = true;
185
+ // Disable raycasting entirely for grass (no-op)
186
+ this.raycast = () => { };
185
187
  }
186
188
  }
package/dist/index.d.ts CHANGED
@@ -10,5 +10,6 @@ export * from "./pillars/index.js";
10
10
  export * from "./water/index.js";
11
11
  export * from "./locations.js";
12
12
  export * from "./utils/instanced-mesh-group.js";
13
+ export * from "./utils/bvh.js";
13
14
  export * from "./assets/loader.js";
14
15
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAA;AAC3B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,kBAAkB,CAAA;AAChC,cAAc,kBAAkB,CAAA;AAChC,cAAc,kBAAkB,CAAA;AAChC,cAAc,kBAAkB,CAAA;AAChC,cAAc,kBAAkB,CAAA;AAChC,cAAc,aAAa,CAAA;AAC3B,cAAc,oBAAoB,CAAA;AAClC,cAAc,kBAAkB,CAAA;AAChC,cAAc,gBAAgB,CAAA;AAC9B,cAAc,iCAAiC,CAAA;AAC/C,cAAc,oBAAoB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAA;AAC3B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,kBAAkB,CAAA;AAChC,cAAc,kBAAkB,CAAA;AAChC,cAAc,kBAAkB,CAAA;AAChC,cAAc,kBAAkB,CAAA;AAChC,cAAc,kBAAkB,CAAA;AAChC,cAAc,aAAa,CAAA;AAC3B,cAAc,oBAAoB,CAAA;AAClC,cAAc,kBAAkB,CAAA;AAChC,cAAc,gBAAgB,CAAA;AAC9B,cAAc,iCAAiC,CAAA;AAC/C,cAAc,gBAAgB,CAAA;AAC9B,cAAc,oBAAoB,CAAA"}
package/dist/index.js CHANGED
@@ -10,4 +10,5 @@ export * from "./pillars/index.js";
10
10
  export * from "./water/index.js";
11
11
  export * from "./locations.js";
12
12
  export * from "./utils/instanced-mesh-group.js";
13
+ export * from "./utils/bvh.js";
13
14
  export * from "./assets/loader.js";
package/dist/parser.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { ChartaError } from "./errors.js";
1
2
  export type KeyParam = [key: string, value: string];
2
3
  export type Call = {
3
4
  name: string;
@@ -5,5 +6,7 @@ export type Call = {
5
6
  keyParams: KeyParam[];
6
7
  offset: number;
7
8
  };
9
+ export type ParseResult = [result: Call[][][], error: null] | [result: null, error: ChartaError];
10
+ export declare function safeParse(input: string): ParseResult;
8
11
  export declare function parse(input: string): Call[][][];
9
12
  //# sourceMappingURL=parser.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["../src/parser.ts"],"names":[],"mappings":"AAKA,MAAM,MAAM,QAAQ,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC;AACpD,MAAM,MAAM,IAAI,GAAG;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,SAAS,EAAE,QAAQ,EAAE,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAKF,wBAAgB,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,EAAE,EAAE,EAAE,CA0C/C"}
1
+ {"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["../src/parser.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,WAAW,EAAiB,MAAM,aAAa,CAAC;AAEzD,MAAM,MAAM,QAAQ,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC;AACpD,MAAM,MAAM,IAAI,GAAG;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,SAAS,EAAE,QAAQ,EAAE,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAIF,MAAM,MAAM,WAAW,GACnB,CAAC,MAAM,EAAE,IAAI,EAAE,EAAE,EAAE,EAAE,KAAK,EAAE,IAAI,CAAC,GACjC,CAAC,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,WAAW,CAAC,CAAC;AAYvC,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,WAAW,CAqDpD;AAED,wBAAgB,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,EAAE,EAAE,EAAE,CAI/C"}
package/dist/parser.js CHANGED
@@ -2,7 +2,16 @@ import nearley from "nearley";
2
2
  const { Parser, Grammar } = nearley;
3
3
  import grammar from "./grammar.js";
4
4
  import { ChartaError } from "./errors.js";
5
- export function parse(input) {
5
+ function getLocation(input, offset) {
6
+ const clampedOffset = Math.max(0, Math.min(offset, input.length));
7
+ const prefix = input.slice(0, clampedOffset);
8
+ const lines = prefix.split("\n");
9
+ return {
10
+ line: lines.length,
11
+ column: lines[lines.length - 1].length + 1,
12
+ };
13
+ }
14
+ export function safeParse(input) {
6
15
  const normalized = input.replace(/\r\n?/g, "\n");
7
16
  const parser = new Parser(Grammar.fromCompiled(grammar));
8
17
  let rows;
@@ -10,38 +19,39 @@ export function parse(input) {
10
19
  rows = parser.feed(normalized).finish()[0];
11
20
  }
12
21
  catch (error) {
13
- const loc = typeof error.offset === 'number'
14
- ? getLoc(normalized, error.offset)
15
- : undefined;
16
- throw new ChartaError(error.message || "Parse error", input, loc);
22
+ const offset = typeof error.offset === "number" ? error.offset : 0;
23
+ return [
24
+ null,
25
+ new ChartaError(error.message || "Parse error", normalized, getLocation(normalized, offset)),
26
+ ];
17
27
  }
18
28
  if (!rows || rows.length === 0)
19
- return [];
20
- // Validate Meta Row (first row)
29
+ return [[], null];
30
+ // Validate Meta Row (first row must contain exactly one cell)
21
31
  if (rows[0].length !== 1) {
22
- throwError(input, rows[0].offset, `First row must contain exactly one cell (meta row), found ${rows[0].length}`);
32
+ return [
33
+ null,
34
+ new ChartaError(`First row must contain exactly one cell (meta row), found ${rows[0].length}`, normalized, getLocation(normalized, rows[0].offset)),
35
+ ];
23
36
  }
24
- // Validate Grid Rows (subsequent rows)
25
- // All grid rows must have the same number of cells as the first grid row
37
+ // Validate Grid Rows (all must have same width as first grid row)
26
38
  if (rows.length > 1) {
27
39
  const gridRows = rows.slice(1);
28
40
  const expectedWidth = gridRows[0].length;
29
- const badRowIdx = gridRows.findIndex(r => r.length !== expectedWidth);
41
+ const badRowIdx = gridRows.findIndex((r) => r.length !== expectedWidth);
30
42
  if (badRowIdx !== -1) {
31
- throwError(input, gridRows[badRowIdx].offset, `Row ${badRowIdx + 2} has ${gridRows[badRowIdx].length} cells; expected ${expectedWidth}`);
43
+ const badRow = gridRows[badRowIdx];
44
+ return [
45
+ null,
46
+ new ChartaError(`Row ${badRowIdx + 2} has ${badRow.length} cells; expected ${expectedWidth}`, normalized, getLocation(normalized, badRow.offset)),
47
+ ];
32
48
  }
33
49
  }
34
- return rows;
50
+ return [rows, null];
35
51
  }
36
- function throwError(input, offset, message) {
37
- throw new ChartaError(message, input, getLoc(input, offset));
38
- }
39
- function getLoc(input, offset) {
40
- const clampedOffset = Math.max(0, Math.min(offset, input.length));
41
- const prefix = input.slice(0, clampedOffset);
42
- const lines = prefix.split('\n');
43
- return {
44
- line: lines.length,
45
- column: lines[lines.length - 1].length + 1
46
- };
52
+ export function parse(input) {
53
+ const [result, error] = safeParse(input);
54
+ if (error)
55
+ throw error;
56
+ return result;
47
57
  }
@@ -3,10 +3,10 @@ import { Interpreter } from '../interpreter.js';
3
3
  export type PillarCorner = 'topleft' | 'topright' | 'bottomright' | 'bottomleft';
4
4
  export type PillarMeshOptions = {
5
5
  /**
6
- * Mock mode: validates pillar calls but skips geometry creation.
6
+ * Validate-only mode: validates pillar calls but skips geometry creation.
7
7
  * Useful for server-side validation. Default: false.
8
8
  */
9
- mock?: boolean;
9
+ validateOnly?: boolean;
10
10
  };
11
11
  export declare class PillarMesh extends InstancedMesh {
12
12
  constructor(interpreter: Interpreter, material?: Material, options?: PillarMeshOptions);
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/pillars/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,aAAa,EAEb,QAAQ,EAMT,MAAM,OAAO,CAAA;AACd,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAO/C,MAAM,MAAM,YAAY,GAAG,SAAS,GAAG,UAAU,GAAG,aAAa,GAAG,YAAY,CAAA;AAYhF,MAAM,MAAM,iBAAiB,GAAG;IAC9B;;;OAGG;IACH,IAAI,CAAC,EAAE,OAAO,CAAA;CACf,CAAA;AAED,qBAAa,UAAW,SAAQ,aAAa;gBAC/B,WAAW,EAAE,WAAW,EAAE,QAAQ,GAAE,QAAkC,EAAE,OAAO,GAAE,iBAAsB;CAoLpH"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/pillars/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,aAAa,EAEb,QAAQ,EAMT,MAAM,OAAO,CAAA;AACd,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAQ/C,MAAM,MAAM,YAAY,GAAG,SAAS,GAAG,UAAU,GAAG,aAAa,GAAG,YAAY,CAAA;AAYhF,MAAM,MAAM,iBAAiB,GAAG;IAC9B;;;OAGG;IACH,YAAY,CAAC,EAAE,OAAO,CAAA;CACvB,CAAA;AAED,qBAAa,UAAW,SAAQ,aAAa;gBAC/B,WAAW,EAAE,WAAW,EAAE,QAAQ,GAAE,QAAkC,EAAE,OAAO,GAAE,iBAAsB;CA4LpH"}
@@ -4,34 +4,22 @@ import { buildTextureArrayFromAssets } from '../utils/texture.js';
4
4
  import { buildPillarMeshMaterial } from './material.js';
5
5
  import { ChartaError } from '../errors.js';
6
6
  import { pillarSchema } from '../schemas.js';
7
+ import { setupBVH, acceleratedRaycast } from '../utils/bvh.js';
7
8
  export class PillarMesh extends InstancedMesh {
8
9
  constructor(interpreter, material = new MeshBasicMaterial(), options = {}) {
9
10
  const rows = interpreter.getRows();
10
11
  const cols = interpreter.getCols();
11
12
  const cellSize = interpreter.getCellSize();
12
- // Mock mode: validate calls exist but skip geometry creation
13
- if (options.mock) {
14
- // Use undefined geometry with 0 instances - no memory allocation
15
- super(undefined, material, 0);
16
- for (let row = 0; row < rows; row++) {
17
- for (let col = 0; col < cols; col++) {
18
- const entries = interpreter.getCalls([row, col], { pillar: pillarSchema });
19
- for (const [, parsed, , loc] of entries) {
20
- // Validate texture reference
21
- interpreter.getAsset(Texture, `${parsed.texture}BaseColorTexture`, loc);
22
- }
23
- }
24
- }
25
- return;
26
- }
27
- // Collect pillar instances with resolved heights
13
+ // Collect pillar instances only in non-validateOnly mode
28
14
  const pillars = [];
29
- // tilesGeometry is only required for height sampling; fetch lazily where needed
30
15
  const usedTextures = [];
31
- const getTextureId = (name) => {
32
- const texture = interpreter.getAsset(Texture, `${name}BaseColorTexture`);
16
+ const getTextureId = (name, loc) => {
17
+ const texture = interpreter.getAsset(Texture, `${name}BaseColorTexture`, loc);
33
18
  if (!texture)
34
19
  return 0;
20
+ // Only track textures in non-validateOnly mode
21
+ if (options.validateOnly)
22
+ return 0;
35
23
  let idx = usedTextures.indexOf(texture);
36
24
  if (idx === -1) {
37
25
  idx = usedTextures.length;
@@ -61,6 +49,7 @@ export class PillarMesh extends InstancedMesh {
61
49
  }
62
50
  }
63
51
  }
52
+ // Validation loop runs in both modes
64
53
  for (let row = 0; row < rows; row++) {
65
54
  for (let col = 0; col < cols; col++) {
66
55
  const entries = interpreter.getCalls([row, col], {
@@ -76,11 +65,13 @@ export class PillarMesh extends InstancedMesh {
76
65
  const [cx, cz] = interpreter.getWorldCellCenter(row, col);
77
66
  const offsetX = px - cx;
78
67
  const offsetZ = pz - cz;
79
- const textureId = getTextureId(parsed.texture);
68
+ // Validate texture reference (runs in both modes)
69
+ const textureId = getTextureId(parsed.texture, loc);
80
70
  // Derive default layers relative to call position (like walls)
81
71
  const isLayer = (c) => c.name === 'ground' || c.name === 'ceiling';
82
72
  let bottomY = parsed.bottomY;
83
73
  let topY = parsed.topY;
74
+ // Validate layer existence (runs in both modes)
84
75
  let bottomStackIndex;
85
76
  if (bottomY == null) {
86
77
  const layersBefore = interpreter.countCalls([row, col], isLayer, 0, idx);
@@ -89,10 +80,13 @@ export class PillarMesh extends InstancedMesh {
89
80
  interpreter.reportError(new ChartaError(`pillar at ${row}/${col}: missing bottomY and no preceding layer`, interpreter.getSource(), loc));
90
81
  continue;
91
82
  }
92
- const tilesGeometry = interpreter.getAsset(TilesGeometry, 'tilesGeometry', loc);
93
- if (!tilesGeometry)
94
- continue;
95
- bottomY = tilesGeometry.getHeight(row, col, bottomStackIndex, offsetX, offsetZ);
83
+ // Height sampling only in non-validateOnly mode
84
+ if (!options.validateOnly) {
85
+ const tilesGeometry = interpreter.getAsset(TilesGeometry, 'tilesGeometry', loc);
86
+ if (!tilesGeometry)
87
+ continue;
88
+ bottomY = tilesGeometry.getHeight(row, col, bottomStackIndex, offsetX, offsetZ);
89
+ }
96
90
  }
97
91
  let topStackIndex;
98
92
  if (topY == null) {
@@ -101,31 +95,42 @@ export class PillarMesh extends InstancedMesh {
101
95
  interpreter.reportError(new ChartaError(`pillar at ${row}/${col}: missing topY and no subsequent layer`, interpreter.getSource(), loc));
102
96
  continue;
103
97
  }
104
- const layersBefore = interpreter.countCalls([row, col], isLayer, 0, idx);
105
- topStackIndex = layersBefore;
106
- const tilesGeometry = interpreter.getAsset(TilesGeometry, 'tilesGeometry', loc);
107
- if (!tilesGeometry)
108
- continue;
109
- topY = tilesGeometry.getHeight(row, col, topStackIndex, offsetX, offsetZ);
98
+ // Height sampling only in non-validateOnly mode
99
+ if (!options.validateOnly) {
100
+ const layersBefore = interpreter.countCalls([row, col], isLayer, 0, idx);
101
+ topStackIndex = layersBefore;
102
+ const tilesGeometry = interpreter.getAsset(TilesGeometry, 'tilesGeometry', loc);
103
+ if (!tilesGeometry)
104
+ continue;
105
+ topY = tilesGeometry.getHeight(row, col, topStackIndex, offsetX, offsetZ);
106
+ }
110
107
  }
111
- // Support "negative pillars" by swapping
112
- if (topY < bottomY) {
113
- const tmp = bottomY;
114
- bottomY = topY;
115
- topY = tmp;
108
+ // Only collect pillar data in non-validateOnly mode
109
+ if (!options.validateOnly) {
110
+ // Support "negative pillars" by swapping
111
+ if (topY < bottomY) {
112
+ const tmp = bottomY;
113
+ bottomY = topY;
114
+ topY = tmp;
115
+ }
116
+ pillars.push({
117
+ x: px,
118
+ z: pz,
119
+ sizeX,
120
+ sizeZ,
121
+ bottomY: bottomY,
122
+ topY: topY,
123
+ textureId,
124
+ });
116
125
  }
117
- pillars.push({
118
- x: px,
119
- z: pz,
120
- sizeX,
121
- sizeZ,
122
- bottomY: bottomY,
123
- topY: topY,
124
- textureId,
125
- });
126
126
  }
127
127
  }
128
128
  }
129
+ // ValidateOnly mode: skip geometry creation
130
+ if (options.validateOnly) {
131
+ super(undefined, material, 0);
132
+ return;
133
+ }
129
134
  // Base cube with base at y=0 for easy scaling
130
135
  const geometry = new BoxGeometry(1, 1, 1);
131
136
  geometry.translate(0, 0.5, 0);
@@ -156,5 +161,8 @@ export class PillarMesh extends InstancedMesh {
156
161
  const textureArray = buildTextureArrayFromAssets(usedTextures);
157
162
  buildPillarMeshMaterial(this.material, textureArray);
158
163
  }
164
+ // Setup BVH for accelerated raycasting
165
+ setupBVH(this.geometry);
166
+ this.raycast = acceleratedRaycast;
159
167
  }
160
168
  }
@@ -24,10 +24,10 @@ export declare class PrefabBatchBuilder {
24
24
  }
25
25
  export type PlaceGroupOptions = {
26
26
  /**
27
- * Mock mode: validates scatter/place calls and assets but skips expensive transform
27
+ * Validate-only mode: validates scatter/place calls and assets but skips expensive transform
28
28
  * computation and instance generation. Useful for server-side validation. Default: false.
29
29
  */
30
- mock?: boolean;
30
+ validateOnly?: boolean;
31
31
  skipModelInstantiation?: boolean;
32
32
  };
33
33
  export declare class PlaceGroup extends Group {
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/place/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,OAAO,EAAQ,MAAM,OAAO,CAAA;AACrF,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAO/C,MAAM,MAAM,eAAe,GAAG,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAA;AAEvD,MAAM,MAAM,mBAAmB,GAAG,CAAC,IAAI,EAAE;IACvC,QAAQ,EAAE,OAAO,CAAA;IACjB,QAAQ,EAAE,UAAU,CAAA;IACpB,KAAK,EAAE,OAAO,CAAA;IACd,WAAW,EAAE,WAAW,CAAA;IACxB,IAAI,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KAAE,CAAA;IAC5D,IAAI,EAAE,MAAM,CAAA;CACb,KAAK,IAAI,CAAA;AAKV,wBAAgB,aAAa,CAAC,MAAM,EAAE,QAAQ,GAAG,OAAO,CAOvD;AAED,qBAAa,kBAAkB;aAIX,UAAU,EAAE,OAAO;IACnC,OAAO,CAAC,QAAQ,CAAC,iBAAiB;IAJpC,SAAgB,QAAQ,EAAE,KAAK,CAAC,OAAO,CAAC,CAAK;gBAG3B,UAAU,EAAE,OAAO,EAClB,iBAAiB,EAAE,CAAC,QAAQ,EAAE,KAAK,CAAC,OAAO,CAAC,KAAK,QAAQ;IAG5E,GAAG,CAAC,MAAM,EAAE,OAAO;IAKZ,KAAK,IAAI,QAAQ;CAGzB;AAED,MAAM,MAAM,iBAAiB,GAAG;IAC9B;;;OAGG;IACH,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,sBAAsB,CAAC,EAAE,OAAO,CAAA;CACjC,CAAA;AAED,qBAAa,UAAW,SAAQ,KAAK;IACnC,SAAgB,aAAa,kCAAwC;gBAEzD,WAAW,EAAE,WAAW,EAAE,iBAAiB,CAAC,EAAE,QAAQ,EAAE,IAAI,GAAE,iBAAsB;IAuPhG,OAAO;CAOR"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/place/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,OAAO,EAAQ,MAAM,OAAO,CAAA;AACrF,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAO/C,MAAM,MAAM,eAAe,GAAG,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAA;AAEvD,MAAM,MAAM,mBAAmB,GAAG,CAAC,IAAI,EAAE;IACvC,QAAQ,EAAE,OAAO,CAAA;IACjB,QAAQ,EAAE,UAAU,CAAA;IACpB,KAAK,EAAE,OAAO,CAAA;IACd,WAAW,EAAE,WAAW,CAAA;IACxB,IAAI,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KAAE,CAAA;IAC5D,IAAI,EAAE,MAAM,CAAA;CACb,KAAK,IAAI,CAAA;AAKV,wBAAgB,aAAa,CAAC,MAAM,EAAE,QAAQ,GAAG,OAAO,CAOvD;AAED,qBAAa,kBAAkB;aAIX,UAAU,EAAE,OAAO;IACnC,OAAO,CAAC,QAAQ,CAAC,iBAAiB;IAJpC,SAAgB,QAAQ,EAAE,KAAK,CAAC,OAAO,CAAC,CAAK;gBAG3B,UAAU,EAAE,OAAO,EAClB,iBAAiB,EAAE,CAAC,QAAQ,EAAE,KAAK,CAAC,OAAO,CAAC,KAAK,QAAQ;IAG5E,GAAG,CAAC,MAAM,EAAE,OAAO;IAKZ,KAAK,IAAI,QAAQ;CAGzB;AAED,MAAM,MAAM,iBAAiB,GAAG;IAC9B;;;OAGG;IACH,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,sBAAsB,CAAC,EAAE,OAAO,CAAA;CACjC,CAAA;AAED,qBAAa,UAAW,SAAQ,KAAK;IACnC,SAAgB,aAAa,kCAAwC;gBAEzD,WAAW,EAAE,WAAW,EAAE,iBAAiB,CAAC,EAAE,QAAQ,EAAE,IAAI,GAAE,iBAAsB;IAuPhG,OAAO;CAOR"}
@@ -34,14 +34,14 @@ export class PlaceGroup extends Group {
34
34
  batchBuilders = new Map();
35
35
  constructor(interpreter, _materialFallback, opts = {}) {
36
36
  super();
37
- const { mock = false, skipModelInstantiation: skipBuild = false } = opts;
37
+ const { validateOnly = false, skipModelInstantiation = false } = opts;
38
38
  const rows = interpreter.getRows();
39
39
  const cols = interpreter.getCols();
40
40
  const cellSize = interpreter.getCellSize();
41
- // Only get tilesGeometry if not in mock mode (needed for height calculations)
42
- const tilesGeometry = mock ? null : interpreter.getAsset(TilesGeometry, 'tilesGeometry');
43
- // Return early if not mock and tilesGeometry is missing
44
- if (!mock && !tilesGeometry) {
41
+ // Only get tilesGeometry if not in validateOnly mode (needed for height calculations)
42
+ const tilesGeometry = validateOnly ? null : interpreter.getAsset(TilesGeometry, 'tilesGeometry');
43
+ // Return early if not validateOnly and tilesGeometry is missing
44
+ if (!validateOnly && !tilesGeometry) {
45
45
  return;
46
46
  }
47
47
  const computeScale = (usualSize, sizeX, sizeZ) => {
@@ -109,8 +109,8 @@ export class PlaceGroup extends Group {
109
109
  const batchBuilder = interpreter.getAsset(PrefabBatchBuilder, prefabName, loc);
110
110
  if (!batchBuilder)
111
111
  continue;
112
- // In mock mode, we only validate asset existence - skip expensive placement computation
113
- if (mock)
112
+ // In validateOnly mode, we only validate asset existence - skip expensive placement computation
113
+ if (validateOnly)
114
114
  continue;
115
115
  this.batchBuilders.set(parsed.model, batchBuilder);
116
116
  const usualSize = new Vector3();
@@ -176,7 +176,7 @@ export class PlaceGroup extends Group {
176
176
  const usualSize = batchBuilder.prefabSize;
177
177
  const px = cx + (parsed.offsetX ?? 0);
178
178
  const pz = cz + (parsed.offsetZ ?? 0);
179
- // Compute scale for footprint validation (needed for both mock and normal mode)
179
+ // Compute scale for footprint validation (needed for both validateOnly and normal mode)
180
180
  const scale = computeScale(usualSize, parsed.sizeX, parsed.sizeZ);
181
181
  const halfX = (usualSize.x * scale.x) / 2;
182
182
  const halfZ = (usualSize.z * scale.z) / 2;
@@ -186,8 +186,8 @@ export class PlaceGroup extends Group {
186
186
  interpreter.reportError(new ChartaError(`location is inside place(${parsed.model}) footprint`, interpreter.getSource(), loc));
187
187
  }
188
188
  }
189
- // In mock mode, skip expensive transform computation and placement
190
- if (mock)
189
+ // In validateOnly mode, skip expensive transform computation and placement
190
+ if (validateOnly)
191
191
  continue;
192
192
  this.batchBuilders.set(parsed.model, batchBuilder);
193
193
  const layerIndex = interpreter.countCalls([row, col], (c) => c.name === 'ground' || c.name === 'ceiling', 0, callIdx) - 1;
@@ -205,8 +205,8 @@ export class PlaceGroup extends Group {
205
205
  }
206
206
  }
207
207
  }
208
- // Build step: skip if mock or skipBuild is enabled
209
- if (!mock && !skipBuild) {
208
+ // Build step: skip if validateOnly or skipBuild is enabled
209
+ if (!validateOnly && !skipModelInstantiation) {
210
210
  for (const batchBuilder of this.batchBuilders.values()) {
211
211
  this.add(batchBuilder.build());
212
212
  }
@@ -5,10 +5,10 @@ import { groundSchema, ceilingSchema } from '../schemas.js';
5
5
  export { groundSchema, ceilingSchema };
6
6
  export type TilesMeshOptions = {
7
7
  /**
8
- * Mock mode: validates ground/ceiling/wall calls but skips geometry creation.
8
+ * Validate-only mode: validates ground/ceiling/wall calls but skips geometry creation.
9
9
  * Useful for server-side validation. Default: false.
10
10
  */
11
- mock?: boolean;
11
+ validateOnly?: boolean;
12
12
  skipTextureLoading?: boolean;
13
13
  };
14
14
  export declare class TilesMesh extends Mesh<TilesGeometry> {
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/tiles/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,QAAQ,EACR,IAAI,EAOL,MAAM,OAAO,CAAA;AACd,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAC/C,OAAO,EAAE,aAAa,EAAuB,MAAM,eAAe,CAAA;AAIlE,OAAO,EAAE,YAAY,EAAE,aAAa,EAAc,MAAM,eAAe,CAAA;AAEvE,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,CAAA;AAEtC,MAAM,MAAM,gBAAgB,GAAG;IAC7B;;;OAGG;IACH,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,kBAAkB,CAAC,EAAE,OAAO,CAAA;CAC7B,CAAA;AAyCD,qBAAa,SAAU,SAAQ,IAAI,CAAC,aAAa,CAAC;gBACpC,WAAW,EAAE,WAAW,EAAE,QAAQ,GAAE,QAAkC,EAAE,OAAO,GAAE,gBAAqB;CAiInH"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/tiles/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,QAAQ,EACR,IAAI,EAOL,MAAM,OAAO,CAAA;AACd,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAC/C,OAAO,EAAE,aAAa,EAAuB,MAAM,eAAe,CAAA;AAIlE,OAAO,EAAE,YAAY,EAAE,aAAa,EAAc,MAAM,eAAe,CAAA;AAGvE,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,CAAA;AAEtC,MAAM,MAAM,gBAAgB,GAAG;IAC7B;;;OAGG;IACH,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,kBAAkB,CAAC,EAAE,OAAO,CAAA;CAC7B,CAAA;AAyCD,qBAAa,SAAU,SAAQ,IAAI,CAAC,aAAa,CAAC;gBACpC,WAAW,EAAE,WAAW,EAAE,QAAQ,GAAE,QAAkC,EAAE,OAAO,GAAE,gBAAqB;CAqInH"}
@@ -4,6 +4,7 @@ import { buildTilesMaterial } from './material.js';
4
4
  import { ChartaError } from '../errors.js';
5
5
  import { buildTextureArrayFromAssets } from '../utils/texture.js';
6
6
  import { groundSchema, ceilingSchema, wallSchema } from '../schemas.js';
7
+ import { setupBVH, acceleratedRaycast } from '../utils/bvh.js';
7
8
  export { groundSchema, ceilingSchema };
8
9
  function resolveHeights(position, items, interpreter, reverse) {
9
10
  items = [...items];
@@ -34,9 +35,9 @@ export class TilesMesh extends Mesh {
34
35
  constructor(interpreter, material = new MeshBasicMaterial(), options = {}) {
35
36
  const rows = interpreter.getRows();
36
37
  const cols = interpreter.getCols();
37
- const { mock = false, skipTextureLoading = false } = options;
38
- const createGeometry = !mock;
39
- const createMaterial = !mock && !skipTextureLoading;
38
+ const { validateOnly = false, skipTextureLoading = false } = options;
39
+ const createGeometry = !validateOnly;
40
+ const createMaterial = !validateOnly && !skipTextureLoading;
40
41
  // Texture index tracking (only used when creating materials)
41
42
  const textureIndex = new Map();
42
43
  const getTextureIndex = (name, loc) => {
@@ -111,7 +112,7 @@ export class TilesMesh extends Mesh {
111
112
  });
112
113
  }
113
114
  else if (!skipTextureLoading) {
114
- // Validate texture exists in mock mode (unless mockTexture is set)
115
+ // Validate texture exists in validateOnly mode (unless mockTexture is set)
115
116
  interpreter.getAsset(Texture, `${parsed.texture}BaseColorTexture`, loc);
116
117
  }
117
118
  }
@@ -133,5 +134,8 @@ export class TilesMesh extends Mesh {
133
134
  this.material = buildTilesMaterial(material, buildTextureArrayFromAssets(texturesInOrder));
134
135
  }
135
136
  }
137
+ // Setup BVH for accelerated raycasting
138
+ setupBVH(this.geometry);
139
+ this.raycast = acceleratedRaycast;
136
140
  }
137
141
  }
@@ -0,0 +1,11 @@
1
+ import { acceleratedRaycast } from 'three-mesh-bvh';
2
+ import type { BufferGeometry } from 'three';
3
+ /**
4
+ * Sets up BVH (Bounding Volume Hierarchy) on a geometry for accelerated raycasting.
5
+ * Call this after geometry is created, then assign acceleratedRaycast to the mesh's raycast method.
6
+ *
7
+ * @param geometry - The BufferGeometry to compute BVH for. If undefined or empty, this is a no-op.
8
+ */
9
+ export declare function setupBVH(geometry: BufferGeometry | undefined): void;
10
+ export { acceleratedRaycast };
11
+ //# sourceMappingURL=bvh.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bvh.d.ts","sourceRoot":"","sources":["../../src/utils/bvh.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAwC,MAAM,gBAAgB,CAAA;AACzF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,OAAO,CAAA;AAE3C;;;;;GAKG;AACH,wBAAgB,QAAQ,CAAC,QAAQ,EAAE,cAAc,GAAG,SAAS,GAAG,IAAI,CAWnE;AAED,OAAO,EAAE,kBAAkB,EAAE,CAAA"}
@@ -0,0 +1,20 @@
1
+ import { acceleratedRaycast, computeBoundsTree, disposeBoundsTree } from 'three-mesh-bvh';
2
+ /**
3
+ * Sets up BVH (Bounding Volume Hierarchy) on a geometry for accelerated raycasting.
4
+ * Call this after geometry is created, then assign acceleratedRaycast to the mesh's raycast method.
5
+ *
6
+ * @param geometry - The BufferGeometry to compute BVH for. If undefined or empty, this is a no-op.
7
+ */
8
+ export function setupBVH(geometry) {
9
+ if (!geometry)
10
+ return;
11
+ // Skip BVH for empty geometries (no index or no position attribute)
12
+ const index = geometry.getIndex();
13
+ const position = geometry.getAttribute('position');
14
+ if (!index || index.count === 0 || !position || position.count === 0)
15
+ return;
16
+ geometry.computeBoundsTree = computeBoundsTree;
17
+ geometry.disposeBoundsTree = disposeBoundsTree;
18
+ geometry.computeBoundsTree();
19
+ }
20
+ export { acceleratedRaycast };
@@ -1 +1 @@
1
- {"version":3,"file":"instanced-mesh-group.d.ts","sourceRoot":"","sources":["../../src/utils/instanced-mesh-group.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,aAAa,EAAE,OAAO,EAAQ,QAAQ,EAAS,MAAM,OAAO,CAAC;AAE7E,UAAU,kBAAkB;IAC1B,IAAI,EAAE,aAAa,CAAC;IACpB,YAAY,EAAE,OAAO,CAAC;CACvB;AAED,qBAAa,kBAAmB,SAAQ,KAAK;IAC3C,OAAO,EAAE,kBAAkB,EAAE,CAAM;gBAEvB,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,KAAK,CAAC,OAAO,CAAC;IA6CtE,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO;IAiB1C;;;OAGG;IACH,OAAO;CAOR"}
1
+ {"version":3,"file":"instanced-mesh-group.d.ts","sourceRoot":"","sources":["../../src/utils/instanced-mesh-group.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,aAAa,EAAE,OAAO,EAAQ,QAAQ,EAAS,MAAM,OAAO,CAAC;AAG7E,UAAU,kBAAkB;IAC1B,IAAI,EAAE,aAAa,CAAC;IACpB,YAAY,EAAE,OAAO,CAAC;CACvB;AAED,qBAAa,kBAAmB,SAAQ,KAAK;IAC3C,OAAO,EAAE,kBAAkB,EAAE,CAAM;gBAEvB,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,KAAK,CAAC,OAAO,CAAC;IAmDtE,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO;IAiB1C;;;OAGG;IACH,OAAO;CAOR"}
@@ -1,4 +1,5 @@
1
1
  import { Group, InstancedMesh, Matrix4 } from "three";
2
+ import { setupBVH, acceleratedRaycast } from "./bvh.js";
2
3
  export class InstancedMeshGroup extends Group {
3
4
  entries = [];
4
5
  constructor(object, count, matrices) {
@@ -29,6 +30,11 @@ export class InstancedMeshGroup extends Group {
29
30
  this.setMatrixAt(i, matrices[i]);
30
31
  }
31
32
  }
33
+ // Setup BVH for accelerated raycasting on all child InstancedMeshes
34
+ for (const entry of this.entries) {
35
+ setupBVH(entry.mesh.geometry);
36
+ entry.mesh.raycast = acceleratedRaycast;
37
+ }
32
38
  }
33
39
  setMatrixAt(index, matrix) {
34
40
  const instanceMatrix = new Matrix4();
@@ -47,7 +47,7 @@ export declare const WALL_CONFIG: {
47
47
  };
48
48
  };
49
49
  export declare const WALL_CONNECTION_TOLERANCE_RATIO: number;
50
- export declare function computeWallVerticalBounds(interpreter: Interpreter, tilesGeometry: TilesGeometry, row: number, col: number, wallIdx: number, parsed: {
50
+ export declare function computeWallVerticalBounds(interpreter: Interpreter, tilesGeometry: TilesGeometry | undefined, row: number, col: number, wallIdx: number, parsed: {
51
51
  bottomY?: number;
52
52
  topY?: number;
53
53
  dir: string;
@@ -59,10 +59,10 @@ export declare function computeWallVerticalBounds(interpreter: Interpreter, tile
59
59
  } | undefined;
60
60
  export type WallMeshOptions = {
61
61
  /**
62
- * Mock mode: validates wall/window/door calls but skips geometry creation.
62
+ * Validate-only mode: validates wall/window/door calls but skips geometry creation.
63
63
  * Useful for server-side validation. Default: false.
64
64
  */
65
- mock?: boolean;
65
+ validateOnly?: boolean;
66
66
  };
67
67
  export declare class WallMesh extends Mesh {
68
68
  constructor(interpreter: Interpreter, material?: Material, options?: WallMeshOptions);
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/walls/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,QAAQ,EAER,IAAI,EAGJ,YAAY,EAEZ,YAAY,EAOb,MAAM,OAAO,CAAA;AAEd,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAE/C,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAA;AAGpD,OAAO,EAAe,aAAa,EAAE,MAAM,cAAc,CAAA;AACzD,OAAO,EAGL,UAAU,EACV,YAAY,EACZ,UAAU,EACX,MAAM,eAAe,CAAA;AAEtB,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,UAAU,EAAE,CAAA;AAK/C,MAAM,MAAM,UAAU,GAAG;IACvB,OAAO,EAAE,MAAM,CAAA;IACf,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;CACd,CAAA;AAED,MAAM,MAAM,QAAQ,GAAG;IACrB,CAAC,EAAE,MAAM,CAAA;IACT,CAAC,EAAE,MAAM,CAAA;IACT,MAAM,EAAE,YAAY,CAAA;IACpB,SAAS,EAAE,MAAM,CAAA;IACjB,MAAM,EAAE,YAAY,CAAA;IACpB,IAAI,EAAE,YAAY,CAAA;IAClB,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,UAAU,EAAE,CAAA;CACtB,CAAA;AAED,eAAO,MAAM,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;CAed,CAAA;AAEV,eAAO,MAAM,+BAA+B,QAAQ,CAAA;AAEpD,wBAAgB,yBAAyB,CACvC,WAAW,EAAE,WAAW,EACxB,aAAa,EAAE,aAAa,EAC5B,GAAG,EAAE,MAAM,EACX,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,MAAM,EACf,MAAM,EAAE;IAAE,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,EACxD,MAAM,EAAE,CAAC,OAAO,WAAW,CAAC,CAAC,MAAM,OAAO,WAAW,CAAC,EACtD,GAAG,EAAE,aAAa;;;;;cAqInB;AAED,MAAM,MAAM,eAAe,GAAG;IAC5B;;;OAGG;IACH,IAAI,CAAC,EAAE,OAAO,CAAA;CACf,CAAA;AAED,qBAAa,QAAS,SAAQ,IAAI;gBACpB,WAAW,EAAE,WAAW,EAAE,QAAQ,GAAE,QAAkC,EAAE,OAAO,GAAE,eAAoB;CA6SlH;AAED,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,WAGrG"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/walls/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,QAAQ,EAER,IAAI,EAGJ,YAAY,EAEZ,YAAY,EAOb,MAAM,OAAO,CAAA;AAEd,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAE/C,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAA;AAGpD,OAAO,EAAe,aAAa,EAAE,MAAM,cAAc,CAAA;AACzD,OAAO,EAGL,UAAU,EACV,YAAY,EACZ,UAAU,EACX,MAAM,eAAe,CAAA;AAGtB,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,UAAU,EAAE,CAAA;AAK/C,MAAM,MAAM,UAAU,GAAG;IACvB,OAAO,EAAE,MAAM,CAAA;IACf,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;CACd,CAAA;AAED,MAAM,MAAM,QAAQ,GAAG;IACrB,CAAC,EAAE,MAAM,CAAA;IACT,CAAC,EAAE,MAAM,CAAA;IACT,MAAM,EAAE,YAAY,CAAA;IACpB,SAAS,EAAE,MAAM,CAAA;IACjB,MAAM,EAAE,YAAY,CAAA;IACpB,IAAI,EAAE,YAAY,CAAA;IAClB,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,UAAU,EAAE,CAAA;CACtB,CAAA;AAED,eAAO,MAAM,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;CAed,CAAA;AAEV,eAAO,MAAM,+BAA+B,QAAQ,CAAA;AAEpD,wBAAgB,yBAAyB,CACvC,WAAW,EAAE,WAAW,EACxB,aAAa,EAAE,aAAa,GAAG,SAAS,EACxC,GAAG,EAAE,MAAM,EACX,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,MAAM,EACf,MAAM,EAAE;IAAE,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,EACxD,MAAM,EAAE,CAAC,OAAO,WAAW,CAAC,CAAC,MAAM,OAAO,WAAW,CAAC,EACtD,GAAG,EAAE,aAAa;YAsFO,YAAY;UACd,YAAY;;;cAwDpC;AAED,MAAM,MAAM,eAAe,GAAG;IAC5B;;;OAGG;IACH,YAAY,CAAC,EAAE,OAAO,CAAA;CACvB,CAAA;AAED,qBAAa,QAAS,SAAQ,IAAI;gBACpB,WAAW,EAAE,WAAW,EAAE,QAAQ,GAAE,QAAkC,EAAE,OAAO,GAAE,eAAoB;CAkTlH;AAED,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,WAGrG"}
@@ -5,6 +5,7 @@ import { buildTextureArrayFromAssets } from '../utils/texture.js';
5
5
  import { buildWallMeshMaterial } from './material.js';
6
6
  import { ChartaError } from '../errors.js';
7
7
  import { groundSchema, ceilingSchema, wallSchema, windowSchema, doorSchema, } from '../schemas.js';
8
+ import { setupBVH, acceleratedRaycast } from '../utils/bvh.js';
8
9
  export { wallSchema, windowSchema, doorSchema };
9
10
  const eulerHelper = new Euler();
10
11
  const scaleHelper = new Vector3(1, 1, 1);
@@ -92,6 +93,15 @@ export function computeWallVerticalBounds(interpreter, tilesGeometry, row, col,
92
93
  return undefined;
93
94
  if (topLayer === undefined && parsed.topY === undefined)
94
95
  return undefined;
96
+ // In validateOnly mode (no tilesGeometry), skip height sampling - validation is complete
97
+ if (!tilesGeometry) {
98
+ return {
99
+ yStart: [0, 0, 0],
100
+ yEnd: [0, 0, 0],
101
+ worldWallX,
102
+ worldWallZ,
103
+ };
104
+ }
95
105
  const sampleOffsets = [-cellSize / 2, 0, cellSize / 2];
96
106
  const yStart = [0, 0, 0];
97
107
  const yEnd = [0, 0, 0];
@@ -145,40 +155,15 @@ export class WallMesh extends Mesh {
145
155
  const rows = interpreter.getRows();
146
156
  const cols = interpreter.getCols();
147
157
  const cellSize = interpreter.getCellSize();
148
- // Mock mode: validate calls exist but skip geometry creation
149
- if (options.mock) {
158
+ // Get tilesGeometry only in non-validateOnly mode (needed for height sampling)
159
+ const tilesGeometry = options.validateOnly ? undefined : interpreter.getAsset(TilesGeometry, 'tilesGeometry');
160
+ // In non-validateOnly mode, if no tilesGeometry exists, early return
161
+ if (!options.validateOnly && !tilesGeometry) {
150
162
  super();
151
- for (let row = 0; row < rows; row++) {
152
- for (let col = 0; col < cols; col++) {
153
- // Validate all wall/window/door calls
154
- const entries = interpreter.getCalls([row, col], {
155
- wall: wallSchema,
156
- window: windowSchema,
157
- door: doorSchema,
158
- });
159
- // Validate texture references and wall ordering
160
- let hasWall = false;
161
- for (const [name, parsed, , loc] of entries) {
162
- if (name === 'wall') {
163
- hasWall = true;
164
- interpreter.getAsset(Texture, `${parsed.texture}BaseColorTexture`, loc);
165
- }
166
- else if (name === 'window' || name === 'door') {
167
- if (!hasWall) {
168
- interpreter.reportError(new ChartaError(`${name} without preceding wall`, interpreter.getSource(), loc));
169
- }
170
- }
171
- }
172
- }
173
- }
174
163
  return;
175
164
  }
176
- const tilesGeometry = interpreter.getAsset(TilesGeometry, 'tilesGeometry');
177
- if (!tilesGeometry) {
178
- super();
179
- return;
180
- }
181
- const wallData = [];
165
+ // Allocate data structures only in non-validateOnly mode
166
+ const wallData = options.validateOnly ? [] : [];
182
167
  const usedTextures = [];
183
168
  const getWallTextureId = (texture) => {
184
169
  let idx = usedTextures.indexOf(texture);
@@ -188,6 +173,7 @@ export class WallMesh extends Mesh {
188
173
  }
189
174
  return idx;
190
175
  };
176
+ // Validation loop runs in both modes
191
177
  for (let row = 0; row < rows; row++) {
192
178
  for (let col = 0; col < cols; col++) {
193
179
  const entries = interpreter.getCalls([row, col], {
@@ -196,8 +182,17 @@ export class WallMesh extends Mesh {
196
182
  door: doorSchema,
197
183
  });
198
184
  let currentWall;
185
+ // Track wall validity separately for window/door validation in validateOnly mode
186
+ let hasValidWall = false;
199
187
  for (const [name, winOrDoor, wallIdx, loc] of entries) {
200
188
  if (name === 'window' || name === 'door') {
189
+ // In validateOnly mode, we only validate wall ordering (not geometry bounds)
190
+ if (options.validateOnly) {
191
+ if (!hasValidWall) {
192
+ interpreter.reportError(new ChartaError(`${name} without preceding wall`, interpreter.getSource(), loc));
193
+ }
194
+ continue;
195
+ }
201
196
  if (!currentWall) {
202
197
  interpreter.reportError(new ChartaError(`${name} without preceding wall`, interpreter.getSource(), loc));
203
198
  continue;
@@ -260,31 +255,45 @@ export class WallMesh extends Mesh {
260
255
  }
261
256
  const wallParsed = winOrDoor; // Type assertion since it could be wall or window in loop
262
257
  const config = WALL_CONFIG[wallParsed.dir];
258
+ // computeWallVerticalBounds validates layer existence and reports errors
259
+ // In validateOnly mode (tilesGeometry=undefined), it skips height sampling but still validates
263
260
  const bounds = computeWallVerticalBounds(interpreter, tilesGeometry, row, col, wallIdx, wallParsed, config, loc);
264
261
  if (!bounds) {
265
262
  currentWall = undefined;
263
+ hasValidWall = false;
266
264
  continue;
267
265
  }
268
- const texture = interpreter.getAsset(Texture, `${wallParsed.texture}BaseColorTexture`);
266
+ // Validate texture reference (runs in both modes)
267
+ const texture = interpreter.getAsset(Texture, `${wallParsed.texture}BaseColorTexture`, loc);
269
268
  if (!texture) {
270
269
  currentWall = undefined;
270
+ hasValidWall = false;
271
271
  continue;
272
272
  }
273
- const newWall = {
274
- x: bounds.worldWallX,
275
- z: bounds.worldWallZ,
276
- rotationY: config.rotation,
277
- xzSize: [cellSize, 0.1], // Walls are always cellSize width, 0.1 depth
278
- yStart: bounds.yStart,
279
- yEnd: bounds.yEnd,
280
- textureId: getWallTextureId(texture),
281
- windows: [],
282
- };
283
- wallData.push(newWall);
284
- currentWall = newWall;
273
+ hasValidWall = true;
274
+ // Only build wall data in non-validateOnly mode
275
+ if (!options.validateOnly) {
276
+ const newWall = {
277
+ x: bounds.worldWallX,
278
+ z: bounds.worldWallZ,
279
+ rotationY: config.rotation,
280
+ xzSize: [cellSize, 0.1], // Walls are always cellSize width, 0.1 depth
281
+ yStart: bounds.yStart,
282
+ yEnd: bounds.yEnd,
283
+ textureId: getWallTextureId(texture),
284
+ windows: [],
285
+ };
286
+ wallData.push(newWall);
287
+ currentWall = newWall;
288
+ }
285
289
  }
286
290
  }
287
291
  }
292
+ // ValidateOnly mode: skip geometry creation
293
+ if (options.validateOnly) {
294
+ super();
295
+ return;
296
+ }
288
297
  // Build per-wall geometries (baked) and merge
289
298
  const bakedGeometries = [];
290
299
  const tmpMatrix = new Matrix4();
@@ -377,6 +386,9 @@ export class WallMesh extends Mesh {
377
386
  const textureArray = buildTextureArrayFromAssets(usedTextures);
378
387
  buildWallMeshMaterial(this.material, textureArray);
379
388
  }
389
+ // Setup BVH for accelerated raycasting
390
+ setupBVH(this.geometry);
391
+ this.raycast = acceleratedRaycast;
380
392
  }
381
393
  }
382
394
  export function isLayerConnectedToWall(wallTopY, wallBotY, y, tileSize) {
@@ -2,10 +2,10 @@ import { Mesh } from "three";
2
2
  import { Interpreter } from "../interpreter.js";
3
3
  export type WaterMeshOptions = {
4
4
  /**
5
- * Mock mode: validates water calls but skips geometry creation.
5
+ * Validate-only mode: validates water calls but skips geometry creation.
6
6
  * Useful for server-side validation. Default: false.
7
7
  */
8
- mock?: boolean;
8
+ validateOnly?: boolean;
9
9
  };
10
10
  export declare class WaterMesh extends Mesh {
11
11
  constructor(interpreter: Interpreter, options?: WaterMeshOptions);
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/water/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAY,MAAM,OAAO,CAAC;AACvC,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAIhD,MAAM,MAAM,gBAAgB,GAAG;IAC7B;;;OAGG;IACH,IAAI,CAAC,EAAE,OAAO,CAAA;CACf,CAAC;AAEF,qBAAa,SAAU,SAAQ,IAAI;gBACrB,WAAW,EAAE,WAAW,EAAE,OAAO,GAAE,gBAAqB;IAkDpE,OAAO,IAAI,IAAI;CAMhB;AAED,cAAc,cAAc,CAAA;AAC5B,cAAc,eAAe,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/water/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAY,MAAM,OAAO,CAAC;AACvC,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAKhD,MAAM,MAAM,gBAAgB,GAAG;IAC7B;;;OAGG;IACH,YAAY,CAAC,EAAE,OAAO,CAAA;CACvB,CAAC;AAEF,qBAAa,SAAU,SAAQ,IAAI;gBACrB,WAAW,EAAE,WAAW,EAAE,OAAO,GAAE,gBAAqB;IAwDpE,OAAO,IAAI,IAAI;CAMhB;AAED,cAAc,cAAc,CAAA;AAC5B,cAAc,eAAe,CAAA"}
@@ -1,41 +1,43 @@
1
1
  import { Mesh, Material } from "three";
2
2
  import { TilesGeometry } from "../tiles/geometry.js";
3
3
  import { waterSchema } from "../schemas.js";
4
+ import { setupBVH, acceleratedRaycast } from "../utils/bvh.js";
4
5
  export class WaterMesh extends Mesh {
5
6
  constructor(interpreter, options = {}) {
6
7
  const rows = interpreter.getRows();
7
8
  const cols = interpreter.getCols();
8
- // Mock mode: validate calls exist but skip geometry creation
9
- if (options.mock) {
10
- super();
11
- for (let row = 0; row < rows; row++) {
12
- for (let col = 0; col < cols; col++) {
13
- // Validate water calls
14
- interpreter.getCalls([row, col], { water: waterSchema });
15
- }
16
- }
17
- return;
9
+ // Build tiles only in non-validateOnly mode (expensive allocation)
10
+ let tiles;
11
+ if (!options.validateOnly) {
12
+ tiles = new Array(rows)
13
+ .fill(undefined)
14
+ .map(() => new Array(cols).fill(undefined).map(() => []));
18
15
  }
19
- // Build tiles[row][col] => Array<Tile>, containing only water tiles
20
- const tiles = new Array(rows)
21
- .fill(undefined)
22
- .map(() => new Array(cols).fill(undefined).map(() => []));
16
+ // Validation loop runs in both modes
23
17
  for (let row = 0; row < rows; row++) {
24
18
  for (let col = 0; col < cols; col++) {
25
19
  const entries = interpreter.getCalls([row, col], { water: waterSchema });
26
- const stack = [];
27
- for (const [, parsed] of entries) {
28
- stack.push({
29
- type: "water",
30
- y: parsed.y,
31
- textureId: 0,
32
- row,
33
- col,
34
- });
20
+ // Only build tile stack in non-validateOnly mode
21
+ if (!options.validateOnly) {
22
+ const stack = [];
23
+ for (const [, parsed] of entries) {
24
+ stack.push({
25
+ type: "water",
26
+ y: parsed.y,
27
+ textureId: 0,
28
+ row,
29
+ col,
30
+ });
31
+ }
32
+ tiles[row][col] = stack;
35
33
  }
36
- tiles[row][col] = stack;
37
34
  }
38
35
  }
36
+ // ValidateOnly mode: skip geometry creation
37
+ if (options.validateOnly) {
38
+ super();
39
+ return;
40
+ }
39
41
  const cellSize = interpreter.getCellSize();
40
42
  const mapSizeX = cols * cellSize;
41
43
  const mapSizeZ = rows * cellSize;
@@ -44,6 +46,9 @@ export class WaterMesh extends Mesh {
44
46
  super(geometry);
45
47
  this.renderOrder = 1;
46
48
  this.frustumCulled = true;
49
+ // Setup BVH for accelerated raycasting
50
+ setupBVH(this.geometry);
51
+ this.raycast = acceleratedRaycast;
47
52
  }
48
53
  dispose() {
49
54
  this.geometry.dispose();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drawcall/charta",
3
- "version": "0.1.18",
3
+ "version": "0.1.20",
4
4
  "author": "Bela Bohlender",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "homepage": "https://drawcall.ai",
@@ -25,6 +25,7 @@
25
25
  "fft.js": "^4.0.4",
26
26
  "imurmurhash": "^0.1.4",
27
27
  "seedrandom": "^3.0.5",
28
+ "three-mesh-bvh": "^0.8.0",
28
29
  "zod": "^4.1.12"
29
30
  },
30
31
  "peerDependencies": {