@pascal-app/viewer 0.7.0 → 0.8.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.
Files changed (55) hide show
  1. package/dist/components/renderers/column/column-renderer.d.ts.map +1 -1
  2. package/dist/components/renderers/column/column-renderer.js +5 -2
  3. package/dist/components/renderers/fence/fence-renderer.d.ts.map +1 -1
  4. package/dist/components/renderers/fence/fence-renderer.js +2 -16
  5. package/dist/components/renderers/slab/slab-renderer.js +1 -1
  6. package/dist/components/renderers/stair/stair-renderer.js +3 -3
  7. package/dist/components/renderers/stair-segment/stair-segment-renderer.d.ts.map +1 -1
  8. package/dist/components/renderers/stair-segment/stair-segment-renderer.js +20 -1
  9. package/dist/components/viewer/frame-limiter.d.ts.map +1 -1
  10. package/dist/components/viewer/frame-limiter.js +5 -1
  11. package/dist/components/viewer/index.d.ts +1 -0
  12. package/dist/components/viewer/index.d.ts.map +1 -1
  13. package/dist/components/viewer/index.js +7 -7
  14. package/dist/components/viewer/post-processing.d.ts.map +1 -1
  15. package/dist/components/viewer/post-processing.js +0 -21
  16. package/dist/index.d.ts +1 -1
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +1 -1
  19. package/dist/lib/materials.d.ts.map +1 -1
  20. package/dist/lib/materials.js +2 -3
  21. package/dist/lib/merged-outline-node.d.ts.map +1 -1
  22. package/dist/lib/merged-outline-node.js +4 -11
  23. package/dist/systems/ceiling/ceiling-system.d.ts.map +1 -1
  24. package/dist/systems/ceiling/ceiling-system.js +1 -1
  25. package/dist/systems/fence/fence-system.d.ts.map +1 -1
  26. package/dist/systems/fence/fence-system.js +1 -1
  27. package/dist/systems/guide/guide-system.d.ts.map +1 -1
  28. package/dist/systems/guide/guide-system.js +1 -26
  29. package/dist/systems/item/item-system.js +1 -1
  30. package/dist/systems/roof/roof-system.d.ts.map +1 -1
  31. package/dist/systems/roof/roof-system.js +16 -13
  32. package/dist/systems/scan/scan-system.d.ts.map +1 -1
  33. package/dist/systems/scan/scan-system.js +1 -26
  34. package/dist/systems/slab/slab-system.d.ts.map +1 -1
  35. package/dist/systems/slab/slab-system.js +1 -1
  36. package/dist/systems/stair/stair-system.js +9 -11
  37. package/dist/systems/wall/wall-system.d.ts +1 -1
  38. package/dist/systems/wall/wall-system.d.ts.map +1 -1
  39. package/dist/systems/wall/wall-system.js +8 -5
  40. package/package.json +3 -3
  41. package/dist/lib/suppress-three-clock-warning.d.ts +0 -25
  42. package/dist/lib/suppress-three-clock-warning.d.ts.map +0 -1
  43. package/dist/lib/suppress-three-clock-warning.js +0 -51
  44. package/dist/systems/export/export-system.d.ts +0 -2
  45. package/dist/systems/export/export-system.d.ts.map +0 -1
  46. package/dist/systems/export/export-system.js +0 -60
  47. package/dist/systems/item-mesh-metadata/compute-item-mesh-metadata.d.ts +0 -14
  48. package/dist/systems/item-mesh-metadata/compute-item-mesh-metadata.d.ts.map +0 -1
  49. package/dist/systems/item-mesh-metadata/compute-item-mesh-metadata.js +0 -155
  50. package/dist/systems/item-mesh-metadata/item-mesh-metadata-system.d.ts +0 -6
  51. package/dist/systems/item-mesh-metadata/item-mesh-metadata-system.d.ts.map +0 -1
  52. package/dist/systems/item-mesh-metadata/item-mesh-metadata-system.js +0 -75
  53. package/dist/systems/item-mesh-metadata/sync-request.d.ts +0 -7
  54. package/dist/systems/item-mesh-metadata/sync-request.d.ts.map +0 -1
  55. package/dist/systems/item-mesh-metadata/sync-request.js +0 -25
@@ -1,5 +1,5 @@
1
- import { useFrame } from '@react-three/fiber';
2
1
  import { resolveLevelId, sceneRegistry, spatialGridManager, syncAutoStairOpenings, useScene, } from '@pascal-app/core';
2
+ import { useFrame } from '@react-three/fiber';
3
3
  import { useEffect, useRef } from 'react';
4
4
  import * as THREE from 'three';
5
5
  import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js';
@@ -362,7 +362,7 @@ function applyStraightStairMaterialGroups(geometry) {
362
362
  function applyStairSegmentUvs(geometry) {
363
363
  const position = geometry.getAttribute('position');
364
364
  const normal = geometry.getAttribute('normal');
365
- if (!position || !normal || position.count === 0) {
365
+ if (!(position && normal) || position.count === 0) {
366
366
  geometry.deleteAttribute('uv');
367
367
  return;
368
368
  }
@@ -554,9 +554,9 @@ function buildStairRailPaths(segments, transforms, railingMode, inset, landingIn
554
554
  return {
555
555
  segment,
556
556
  sidePaths: isStraightLineDoubleLandingLayout && index === 1
557
- ? (['left', 'right']
557
+ ? ['left', 'right']
558
558
  .map((side) => buildSegmentRailPath(layout, side, previousSegment, nextSegment, inset, landingInset))
559
- .filter((entry) => entry !== null))
559
+ .filter((entry) => entry !== null)
560
560
  : sidePaths,
561
561
  connectFromPrevious: index > 0 &&
562
562
  !(previousSegment?.segmentType === 'landing' && segment.segmentType === 'landing'),
@@ -652,7 +652,9 @@ function buildSegmentRailPath(layout, side, previousSegment, nextSegment, inset,
652
652
  const segmentStepHeight = segment.segmentType === 'landing' ? 0 : segment.height / segmentSteps;
653
653
  const segmentTopThickness = getSegmentTopThickness(segment);
654
654
  const flightSideOffset = side === 'left' ? segment.width / 2 - 0.045 : -segment.width / 2 + 0.045;
655
- const flightStartX = previousSegment?.segmentType === 'landing' ? -segment.length / 2 + landingInset : -segment.length / 2;
655
+ const flightStartX = previousSegment?.segmentType === 'landing'
656
+ ? -segment.length / 2 + landingInset
657
+ : -segment.length / 2;
656
658
  const flightEndX = nextSegment?.segmentType === 'landing' ? segment.length / 2 - landingInset : segment.length / 2;
657
659
  if (segment.segmentType === 'landing') {
658
660
  return buildLandingRailPathFromScratch(layout, side, previousSegment, nextSegment, segmentTopThickness, landingInset);
@@ -662,15 +664,11 @@ function buildSegmentRailPath(layout, side, previousSegment, nextSegment, inset,
662
664
  points: [
663
665
  ...(previousSegment?.segmentType === 'landing'
664
666
  ? []
665
- : [
666
- toRailLayoutWorldPoint(layout, flightStartX, segmentTopThickness, flightSideOffset),
667
- ]),
667
+ : [toRailLayoutWorldPoint(layout, flightStartX, segmentTopThickness, flightSideOffset)]),
668
668
  ...Array.from({ length: segmentSteps }).map((_, index) => toRailLayoutWorldPoint(layout, -segment.length / 2 + segmentStepDepth * index + segmentStepDepth / 2, segmentStepHeight * (index + 1), flightSideOffset)),
669
669
  ...(nextSegment?.segmentType === 'landing'
670
670
  ? []
671
- : [
672
- toRailLayoutWorldPoint(layout, flightEndX, segment.height, flightSideOffset),
673
- ]),
671
+ : [toRailLayoutWorldPoint(layout, flightEndX, segment.height, flightSideOffset)]),
674
672
  ],
675
673
  };
676
674
  }
@@ -7,5 +7,5 @@ export declare const WallSystem: () => null;
7
7
  * Key insight from demo: polygon is built in WORLD coordinates first,
8
8
  * then we transform to wall-local for the 3D mesh.
9
9
  */
10
- export declare function generateExtrudedWall(wallNode: WallNode, childrenNodes: AnyNode[], miterData: WallMiterData, slabElevation?: number): THREE.BufferGeometry<THREE.NormalBufferAttributes, THREE.BufferGeometryEventMap>;
10
+ export declare function generateExtrudedWall(wallNode: WallNode, childrenNodes: AnyNode[], miterData: WallMiterData, slabElevation?: number): THREE.BufferGeometry;
11
11
  //# sourceMappingURL=wall-system.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"wall-system.d.ts","sourceRoot":"","sources":["../../../src/systems/wall/wall-system.tsx"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,OAAO,EAkBZ,KAAK,aAAa,EAClB,KAAK,QAAQ,EAEd,MAAM,kBAAkB,CAAA;AAEzB,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AA6R9B,eAAO,MAAM,UAAU,YAuDtB,CAAA;AA0DD;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,QAAQ,EAClB,aAAa,EAAE,OAAO,EAAE,EACxB,SAAS,EAAE,aAAa,EACxB,aAAa,SAAI,oFA0GlB"}
1
+ {"version":3,"file":"wall-system.d.ts","sourceRoot":"","sources":["../../../src/systems/wall/wall-system.tsx"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,OAAO,EAkBZ,KAAK,aAAa,EAClB,KAAK,QAAQ,EAEd,MAAM,kBAAkB,CAAA;AAEzB,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAiS9B,eAAO,MAAM,UAAU,YAuDtB,CAAA;AA0DD;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,QAAQ,EAClB,aAAa,EAAE,OAAO,EAAE,EACxB,SAAS,EAAE,aAAa,EACxB,aAAa,SAAI,GAChB,KAAK,CAAC,cAAc,CAyGtB"}
@@ -13,6 +13,9 @@ function computeGeometryBoundsTree(geometry) {
13
13
  geometry.computeBoundsTree = computeBoundsTree;
14
14
  geometry.computeBoundsTree({ maxLeafSize: 10 });
15
15
  }
16
+ function csgGeometry(brush) {
17
+ return brush.geometry;
18
+ }
16
19
  function ensureUv2Attribute(geometry) {
17
20
  const uv = geometry.getAttribute('uv');
18
21
  if (!uv)
@@ -20,7 +23,7 @@ function ensureUv2Attribute(geometry) {
20
23
  geometry.setAttribute('uv2', new THREE.Float32BufferAttribute(Array.from(uv.array), 2));
21
24
  }
22
25
  function insetCurvedWallBoundaryPointsFor3D(wall, boundaryPoints, miterData) {
23
- if (!boundaryPoints || !isCurvedWall(wall)) {
26
+ if (!(boundaryPoints && isCurvedWall(wall))) {
24
27
  return boundaryPoints;
25
28
  }
26
29
  const insetDistance = Math.min(CURVED_WALL_3D_ENDPOINT_INSET, Math.max((wall.thickness ?? 0.1) * 0.01, 0.0005));
@@ -380,16 +383,16 @@ export function generateExtrudedWall(wallNode, childrenNodes, miterData, slabEle
380
383
  cutoutBrush.updateMatrixWorld();
381
384
  const newResult = csgEvaluator.evaluate(resultBrush, cutoutBrush, SUBTRACTION);
382
385
  if (resultBrush !== wallBrush) {
383
- resultBrush.geometry.dispose();
386
+ csgGeometry(resultBrush).dispose();
384
387
  }
385
388
  resultBrush = newResult;
386
389
  }
387
390
  // Clean up
388
- wallBrush.geometry.dispose();
391
+ csgGeometry(wallBrush).dispose();
389
392
  for (const brush of cutoutBrushes) {
390
- brush.geometry.dispose();
393
+ csgGeometry(brush).dispose();
391
394
  }
392
- const resultGeometry = resultBrush.geometry;
395
+ const resultGeometry = csgGeometry(resultBrush);
393
396
  resultGeometry.computeVertexNormals();
394
397
  assignWallMaterialGroups(resultGeometry, wallNode, boundaryEdges);
395
398
  ensureUv2Attribute(resultGeometry);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pascal-app/viewer",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "3D viewer component for Pascal building editor",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -36,10 +36,10 @@
36
36
  },
37
37
  "devDependencies": {
38
38
  "@pascal/typescript-config": "*",
39
- "@types/node": "^25.5.0",
39
+ "@types/node": "^22",
40
40
  "@types/react": "^19.2.2",
41
41
  "@types/three": "^0.184.0",
42
- "typescript": "5.9.3"
42
+ "typescript": "6.0.2"
43
43
  },
44
44
  "keywords": [
45
45
  "3d",
@@ -1,25 +0,0 @@
1
- /**
2
- * Suppresses the `THREE.Clock: This module has been deprecated` warning
3
- * emitted by three.js r183+ on every `new THREE.Clock()` call.
4
- *
5
- * We don't instantiate `Clock` ourselves — `@react-three/fiber` 9.x
6
- * (current stable) creates one internally per `<Canvas>` mount. The
7
- * migration to `THREE.Timer` lands in R3F 10.x (still alpha).
8
- *
9
- * Uses three's `setConsoleFunction` hook so we don't touch `console.warn`
10
- * globally. Only the exact Clock deprecation message is suppressed; all
11
- * other three.js logs (including TSL stack-trace warnings) pass through
12
- * untouched.
13
- *
14
- * Runs in production as well as dev — the suppressed message is a pure
15
- * deprecation notice with no user-actionable content.
16
- *
17
- * REMOVAL: safe to delete once `@react-three/fiber` no longer constructs
18
- * `new THREE.Clock()`. To verify after an R3F upgrade:
19
- * grep -n "new THREE.Clock" node_modules/@react-three/fiber/dist/*.js
20
- * No hits → delete this file and its import in components/viewer/index.tsx.
21
- *
22
- * @see https://github.com/pascalorg/editor/issues/213
23
- */
24
- export {};
25
- //# sourceMappingURL=suppress-three-clock-warning.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"suppress-three-clock-warning.d.ts","sourceRoot":"","sources":["../../src/lib/suppress-three-clock-warning.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG"}
@@ -1,51 +0,0 @@
1
- /**
2
- * Suppresses the `THREE.Clock: This module has been deprecated` warning
3
- * emitted by three.js r183+ on every `new THREE.Clock()` call.
4
- *
5
- * We don't instantiate `Clock` ourselves — `@react-three/fiber` 9.x
6
- * (current stable) creates one internally per `<Canvas>` mount. The
7
- * migration to `THREE.Timer` lands in R3F 10.x (still alpha).
8
- *
9
- * Uses three's `setConsoleFunction` hook so we don't touch `console.warn`
10
- * globally. Only the exact Clock deprecation message is suppressed; all
11
- * other three.js logs (including TSL stack-trace warnings) pass through
12
- * untouched.
13
- *
14
- * Runs in production as well as dev — the suppressed message is a pure
15
- * deprecation notice with no user-actionable content.
16
- *
17
- * REMOVAL: safe to delete once `@react-three/fiber` no longer constructs
18
- * `new THREE.Clock()`. To verify after an R3F upgrade:
19
- * grep -n "new THREE.Clock" node_modules/@react-three/fiber/dist/*.js
20
- * No hits → delete this file and its import in components/viewer/index.tsx.
21
- *
22
- * @see https://github.com/pascalorg/editor/issues/213
23
- */
24
- import { setConsoleFunction } from 'three';
25
- // three's warn() prepends 'THREE.' to its first argument, and the Clock
26
- // constructor passes 'THREE.Clock: ...', producing a double-prefixed
27
- // string. Exact equality keeps the suppression surgical — if three ever
28
- // rewords this, the filter stops matching and the message resurfaces.
29
- const CLOCK_DEPRECATION_MESSAGE = 'THREE.THREE.Clock: This module has been deprecated. Please use THREE.Timer instead.';
30
- // HMR-idempotent install guard. setConsoleFunction is a single-slot global
31
- // in three, so re-evaluating this module on HMR would reinstall the hook.
32
- const INSTALLED = Symbol.for('@pascal-app/viewer/suppress-three-clock-warning');
33
- if (!globalThis[INSTALLED]) {
34
- ;
35
- globalThis[INSTALLED] = true;
36
- setConsoleFunction((method, message, ...params) => {
37
- if (method === 'warn' && message === CLOCK_DEPRECATION_MESSAGE) {
38
- return;
39
- }
40
- // Mirror three's default stack-trace handling so TSL warnings/errors
41
- // keep their clickable stack frames.
42
- if (method !== 'log') {
43
- const first = params[0];
44
- if (first?.isStackTrace && typeof first.getError === 'function') {
45
- console[method](first.getError(message));
46
- return;
47
- }
48
- }
49
- console[method](message, ...params);
50
- });
51
- }
@@ -1,2 +0,0 @@
1
- export declare const ExportSystem: () => null;
2
- //# sourceMappingURL=export-system.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"export-system.d.ts","sourceRoot":"","sources":["../../../src/systems/export/export-system.tsx"],"names":[],"mappings":"AAwBA,eAAO,MAAM,YAAY,YAgDxB,CAAA"}
@@ -1,60 +0,0 @@
1
- 'use client';
2
- import { useThree } from '@react-three/fiber';
3
- import { useEffect } from 'react';
4
- import * as THREE from 'three';
5
- import { STLExporter } from 'three/examples/jsm/exporters/STLExporter.js';
6
- import { GLTFExporter } from 'three/examples/jsm/exporters/GLTFExporter.js';
7
- import { OBJExporter } from 'three/examples/jsm/exporters/OBJExporter.js';
8
- import useViewer from '../../store/use-viewer';
9
- const EDITOR_LAYER = 1; // same constant used across the editor
10
- function downloadBlob(blob, filename) {
11
- const url = URL.createObjectURL(blob);
12
- const a = document.createElement('a');
13
- a.href = url;
14
- a.download = filename;
15
- document.body.appendChild(a);
16
- a.click();
17
- document.body.removeChild(a);
18
- URL.revokeObjectURL(url);
19
- }
20
- export const ExportSystem = () => {
21
- const { scene } = useThree();
22
- const setExportScene = useViewer((state) => state.setExportScene);
23
- useEffect(() => {
24
- const exportFn = async (format = 'glb') => {
25
- const date = new Date().toISOString().split('T')[0];
26
- const filename = `pascal-export-${date}`;
27
- // Clone scene and strip editor-only objects (layer 1 = EDITOR_LAYER)
28
- const exportRoot = scene.clone(true);
29
- const toRemove = [];
30
- exportRoot.traverse((obj) => {
31
- if (obj.layers.isEnabled(EDITOR_LAYER)) {
32
- toRemove.push(obj);
33
- }
34
- });
35
- for (const obj of toRemove) {
36
- obj.parent?.remove(obj);
37
- }
38
- if (format === 'glb') {
39
- const exporter = new GLTFExporter();
40
- const result = await new Promise((resolve, reject) => {
41
- exporter.parse(exportRoot, (output) => resolve(output), (err) => reject(err), { binary: true });
42
- });
43
- downloadBlob(new Blob([result], { type: 'model/gltf-binary' }), `${filename}.glb`);
44
- }
45
- else if (format === 'stl') {
46
- const exporter = new STLExporter();
47
- const result = exporter.parse(exportRoot, { binary: true });
48
- downloadBlob(new Blob([result.buffer], { type: 'model/stl' }), `${filename}.stl`);
49
- }
50
- else if (format === 'obj') {
51
- const exporter = new OBJExporter();
52
- const result = exporter.parse(exportRoot);
53
- downloadBlob(new Blob([result], { type: 'model/obj' }), `${filename}.obj`);
54
- }
55
- };
56
- setExportScene(exportFn);
57
- return () => setExportScene(null);
58
- }, [scene, setExportScene]);
59
- return null;
60
- };
@@ -1,14 +0,0 @@
1
- import type { Object3D } from 'three';
2
- type Point = {
3
- x: number;
4
- y: number;
5
- };
6
- export type MeshLocalBounds = {
7
- min: [number, number, number];
8
- max: [number, number, number];
9
- };
10
- /** Plan footprint in the item root's horizontal (x, z) plane — stored as floorplan polygon. */
11
- export declare function computePlanFootprintPolygonLocal(object: Object3D): Point[];
12
- export declare function computeMeshLocalBoundsFromObject(object: Object3D): MeshLocalBounds | null;
13
- export {};
14
- //# sourceMappingURL=compute-item-mesh-metadata.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"compute-item-mesh-metadata.d.ts","sourceRoot":"","sources":["../../../src/systems/item-mesh-metadata/compute-item-mesh-metadata.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAGrC,KAAK,KAAK,GAAG;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,CAAA;AAErC,MAAM,MAAM,eAAe,GAAG;IAC5B,GAAG,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAA;IAC7B,GAAG,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAA;CAC9B,CAAA;AAED,+FAA+F;AAC/F,wBAAgB,gCAAgC,CAAC,MAAM,EAAE,QAAQ,GAAG,KAAK,EAAE,CA0E1E;AAED,wBAAgB,gCAAgC,CAAC,MAAM,EAAE,QAAQ,GAAG,eAAe,GAAG,IAAI,CAmDzF"}
@@ -1,155 +0,0 @@
1
- import { Box3, Matrix4, Vector3 } from 'three';
2
- /** Plan footprint in the item root's horizontal (x, z) plane — stored as floorplan polygon. */
3
- export function computePlanFootprintPolygonLocal(object) {
4
- object.updateWorldMatrix(true, true);
5
- const inverseRootMatrix = new Matrix4().copy(object.matrixWorld).invert();
6
- const localMatrix = new Matrix4();
7
- const scratchBounds = new Box3();
8
- const scratchPosition = new Vector3();
9
- const footprintPoints = [];
10
- const collectPoints = (child) => {
11
- const mesh = child;
12
- if (mesh.isMesh && mesh.name !== 'cutout' && mesh.geometry) {
13
- if (!mesh.geometry.boundingBox && mesh.geometry.computeBoundingBox) {
14
- mesh.geometry.computeBoundingBox();
15
- }
16
- localMatrix.copy(inverseRootMatrix).multiply(mesh.matrixWorld);
17
- const vertexPositions = mesh.geometry.attributes?.position;
18
- if (vertexPositions && vertexPositions.count > 0) {
19
- for (let index = 0; index < vertexPositions.count; index += 1) {
20
- scratchPosition
21
- .set(vertexPositions.getX(index), vertexPositions.getY(index), vertexPositions.getZ(index))
22
- .applyMatrix4(localMatrix);
23
- if (Number.isFinite(scratchPosition.x) && Number.isFinite(scratchPosition.z)) {
24
- footprintPoints.push({ x: scratchPosition.x, y: scratchPosition.z });
25
- }
26
- }
27
- }
28
- else if (mesh.geometry.boundingBox) {
29
- scratchBounds.copy(mesh.geometry.boundingBox);
30
- scratchBounds.applyMatrix4(localMatrix);
31
- if (Number.isFinite(scratchBounds.min.x) && Number.isFinite(scratchBounds.max.x)) {
32
- footprintPoints.push({ x: scratchBounds.min.x, y: scratchBounds.min.z }, { x: scratchBounds.max.x, y: scratchBounds.min.z }, { x: scratchBounds.max.x, y: scratchBounds.max.z }, { x: scratchBounds.min.x, y: scratchBounds.max.z });
33
- }
34
- }
35
- }
36
- for (const grandchild of child.children) {
37
- collectPoints(grandchild);
38
- }
39
- };
40
- for (const child of object.children) {
41
- collectPoints(child);
42
- }
43
- return getMinimumAreaBoundingRect(footprintPoints) ?? [];
44
- }
45
- export function computeMeshLocalBoundsFromObject(object) {
46
- object.updateWorldMatrix(true, true);
47
- const inverseRootMatrix = new Matrix4().copy(object.matrixWorld).invert();
48
- const localMatrix = new Matrix4();
49
- const localBounds = new Box3();
50
- const scratchBounds = new Box3();
51
- let hasBounds = false;
52
- const expandBounds = (child) => {
53
- const mesh = child;
54
- if (mesh.isMesh && mesh.name !== 'cutout' && mesh.geometry) {
55
- if (!mesh.geometry.boundingBox && mesh.geometry.computeBoundingBox) {
56
- mesh.geometry.computeBoundingBox();
57
- }
58
- if (mesh.geometry.boundingBox) {
59
- localMatrix.copy(inverseRootMatrix).multiply(mesh.matrixWorld);
60
- scratchBounds.copy(mesh.geometry.boundingBox).applyMatrix4(localMatrix);
61
- if (!hasBounds) {
62
- localBounds.copy(scratchBounds);
63
- hasBounds = true;
64
- }
65
- else {
66
- localBounds.union(scratchBounds);
67
- }
68
- }
69
- }
70
- for (const grandchild of child.children) {
71
- expandBounds(grandchild);
72
- }
73
- };
74
- for (const child of object.children) {
75
- expandBounds(child);
76
- }
77
- if (!hasBounds)
78
- return null;
79
- return {
80
- min: [localBounds.min.x, localBounds.min.y, localBounds.min.z],
81
- max: [localBounds.max.x, localBounds.max.y, localBounds.max.z],
82
- };
83
- }
84
- function getMinimumAreaBoundingRect(points) {
85
- if (points.length === 0)
86
- return null;
87
- if (points.length < 3)
88
- return points;
89
- const hull = getConvexHull(points);
90
- if (hull.length < 3)
91
- return hull;
92
- let bestArea = Number.POSITIVE_INFINITY;
93
- let bestRect = null;
94
- for (let index = 0; index < hull.length; index += 1) {
95
- const nextIndex = (index + 1) % hull.length;
96
- const current = hull[index];
97
- const next = hull[nextIndex];
98
- const angle = Math.atan2(next.y - current.y, next.x - current.x);
99
- const cos = Math.cos(-angle);
100
- const sin = Math.sin(-angle);
101
- let minX = Number.POSITIVE_INFINITY;
102
- let maxX = Number.NEGATIVE_INFINITY;
103
- let minY = Number.POSITIVE_INFINITY;
104
- let maxY = Number.NEGATIVE_INFINITY;
105
- for (const point of hull) {
106
- const rx = point.x * cos - point.y * sin;
107
- const ry = point.x * sin + point.y * cos;
108
- minX = Math.min(minX, rx);
109
- maxX = Math.max(maxX, rx);
110
- minY = Math.min(minY, ry);
111
- maxY = Math.max(maxY, ry);
112
- }
113
- const area = (maxX - minX) * (maxY - minY);
114
- if (area >= bestArea)
115
- continue;
116
- bestArea = area;
117
- const unrotate = (x, y) => ({
118
- x: x * Math.cos(angle) - y * Math.sin(angle),
119
- y: x * Math.sin(angle) + y * Math.cos(angle),
120
- });
121
- bestRect = [
122
- unrotate(minX, minY),
123
- unrotate(maxX, minY),
124
- unrotate(maxX, maxY),
125
- unrotate(minX, maxY),
126
- ];
127
- }
128
- return bestRect;
129
- }
130
- function getConvexHull(points) {
131
- if (points.length <= 1)
132
- return points;
133
- const sorted = [...points].sort((a, b) => (a.x === b.x ? a.y - b.y : a.x - b.x));
134
- const cross = (o, a, b) => (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x);
135
- const lower = [];
136
- for (const point of sorted) {
137
- while (lower.length >= 2 &&
138
- cross(lower[lower.length - 2], lower[lower.length - 1], point) <= 0) {
139
- lower.pop();
140
- }
141
- lower.push(point);
142
- }
143
- const upper = [];
144
- for (let index = sorted.length - 1; index >= 0; index -= 1) {
145
- const point = sorted[index];
146
- while (upper.length >= 2 &&
147
- cross(upper[upper.length - 2], upper[upper.length - 1], point) <= 0) {
148
- upper.pop();
149
- }
150
- upper.push(point);
151
- }
152
- lower.pop();
153
- upper.pop();
154
- return [...lower, ...upper];
155
- }
@@ -1,6 +0,0 @@
1
- /**
2
- * Writes `meshLocalPlanPolygon` / `meshLocalBounds` from loaded item meshes.
3
- * ModelRenderer requests sync via `requestItemMeshMetadataSync` when GLTF is ready.
4
- */
5
- export declare function ItemMeshMetadataSystem(): null;
6
- //# sourceMappingURL=item-mesh-metadata-system.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"item-mesh-metadata-system.d.ts","sourceRoot":"","sources":["../../../src/systems/item-mesh-metadata/item-mesh-metadata-system.tsx"],"names":[],"mappings":"AAoFA;;;GAGG;AACH,wBAAgB,sBAAsB,SAYrC"}
@@ -1,75 +0,0 @@
1
- 'use client';
2
- import { sceneRegistry, useScene } from '@pascal-app/core';
3
- import { useFrame } from '@react-three/fiber';
4
- import { computeMeshLocalBoundsFromObject, computePlanFootprintPolygonLocal, } from './compute-item-mesh-metadata';
5
- import { drainItemMeshMetadataSyncRequests, getItemMeshMetadataSourceRoot } from './sync-request';
6
- function isMetadataUnchanged(nextPolygon, nextBounds, metadata) {
7
- const currentPolygon = metadata.meshLocalPlanPolygon;
8
- const currentBounds = typeof metadata.meshLocalBounds === 'object' &&
9
- metadata.meshLocalBounds !== null &&
10
- !Array.isArray(metadata.meshLocalBounds)
11
- ? metadata.meshLocalBounds
12
- : null;
13
- const polygonUnchanged = (nextPolygon === null &&
14
- (currentPolygon === undefined || currentPolygon === null || currentPolygon === false)) ||
15
- (Array.isArray(currentPolygon) &&
16
- nextPolygon !== null &&
17
- currentPolygon.length === nextPolygon.length &&
18
- currentPolygon.every((point, index) => Array.isArray(point) &&
19
- point[0] === nextPolygon[index]?.[0] &&
20
- point[1] === nextPolygon[index]?.[1]));
21
- const boundsUnchanged = (nextBounds === null && (currentBounds === undefined || currentBounds === null)) ||
22
- (nextBounds !== null &&
23
- Array.isArray(currentBounds?.min) &&
24
- Array.isArray(currentBounds?.max) &&
25
- currentBounds.min[0] === nextBounds.min[0] &&
26
- currentBounds.min[1] === nextBounds.min[1] &&
27
- currentBounds.min[2] === nextBounds.min[2] &&
28
- currentBounds.max[0] === nextBounds.max[0] &&
29
- currentBounds.max[1] === nextBounds.max[1] &&
30
- currentBounds.max[2] === nextBounds.max[2]);
31
- return polygonUnchanged && boundsUnchanged;
32
- }
33
- function trySyncItemMeshMetadata(itemId, nodes) {
34
- const node = nodes[itemId];
35
- if (!node || node.type !== 'item')
36
- return;
37
- const root = getItemMeshMetadataSourceRoot(itemId) ??
38
- sceneRegistry.nodes.get(itemId);
39
- if (!root)
40
- return;
41
- const polygon = computePlanFootprintPolygonLocal(root);
42
- const bounds = computeMeshLocalBoundsFromObject(root);
43
- if (polygon.length < 3 && !bounds)
44
- return;
45
- const nextPolygon = polygon.length >= 3 ? polygon.map(({ x, y }) => [x, y]) : null;
46
- const nextBounds = bounds ? { min: bounds.min, max: bounds.max } : null;
47
- const metadata = typeof node.metadata === 'object' && node.metadata !== null && !Array.isArray(node.metadata)
48
- ? node.metadata
49
- : {};
50
- if (isMetadataUnchanged(nextPolygon, nextBounds, metadata))
51
- return;
52
- useScene.getState().updateNode(itemId, {
53
- metadata: {
54
- ...metadata,
55
- ...(nextPolygon ? { meshLocalPlanPolygon: nextPolygon } : {}),
56
- ...(nextBounds ? { meshLocalBounds: nextBounds } : {}),
57
- },
58
- });
59
- }
60
- /**
61
- * Writes `meshLocalPlanPolygon` / `meshLocalBounds` from loaded item meshes.
62
- * ModelRenderer requests sync via `requestItemMeshMetadataSync` when GLTF is ready.
63
- */
64
- export function ItemMeshMetadataSystem() {
65
- useFrame(() => {
66
- const ids = drainItemMeshMetadataSyncRequests();
67
- if (ids.length === 0)
68
- return;
69
- const nodes = useScene.getState().nodes;
70
- for (const id of ids) {
71
- trySyncItemMeshMetadata(id, nodes);
72
- }
73
- });
74
- return null;
75
- }
@@ -1,7 +0,0 @@
1
- import type { Object3D } from 'three';
2
- /** Called when an item's loaded GLTF (or metadata driving footprint) may need re-syncing. */
3
- export declare function requestItemMeshMetadataSync(itemId: string): void;
4
- export declare function setItemMeshMetadataSourceRoot(itemId: string, root: Object3D | null): void;
5
- export declare function getItemMeshMetadataSourceRoot(itemId: string): Object3D | undefined;
6
- export declare function drainItemMeshMetadataSyncRequests(): string[];
7
- //# sourceMappingURL=sync-request.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"sync-request.d.ts","sourceRoot":"","sources":["../../../src/systems/item-mesh-metadata/sync-request.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAMrC,6FAA6F;AAC7F,wBAAgB,2BAA2B,CAAC,MAAM,EAAE,MAAM,QAEzD;AAED,wBAAgB,6BAA6B,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,GAAG,IAAI,QAMlF;AAED,wBAAgB,6BAA6B,CAAC,MAAM,EAAE,MAAM,GAAG,QAAQ,GAAG,SAAS,CAElF;AAED,wBAAgB,iCAAiC,IAAI,MAAM,EAAE,CAK5D"}
@@ -1,25 +0,0 @@
1
- const pendingIds = new Set();
2
- /** Preferred root for footprint math (Clone root). Falls back to sceneRegistry item root. */
3
- const sourceRoots = new Map();
4
- /** Called when an item's loaded GLTF (or metadata driving footprint) may need re-syncing. */
5
- export function requestItemMeshMetadataSync(itemId) {
6
- pendingIds.add(itemId);
7
- }
8
- export function setItemMeshMetadataSourceRoot(itemId, root) {
9
- if (root) {
10
- sourceRoots.set(itemId, root);
11
- }
12
- else {
13
- sourceRoots.delete(itemId);
14
- }
15
- }
16
- export function getItemMeshMetadataSourceRoot(itemId) {
17
- return sourceRoots.get(itemId);
18
- }
19
- export function drainItemMeshMetadataSyncRequests() {
20
- if (pendingIds.size === 0)
21
- return [];
22
- const ids = [...pendingIds];
23
- pendingIds.clear();
24
- return ids;
25
- }