Files
lego-builder/src/components/3D/BrickOutline.jsx
T
L'électron rare 529e74c1a6 feat: ldraw lego parts in builder
Add the real LDraw LEGO parts (LeoCAD-style) as the posable objects.

- Vendor the curated 15-part LDraw closure under public/ldraw so it is
  served at /blocs-pro/ldraw/ after build (CC BY 4.0, credits kept).
- src/utils/ldraw.js loads each part via three's LDrawLoader, caches the
  geometry per type, applies the LDU->world scale and Y flip to match the
  existing grid, and tints surfaces with the active colour.
- A LeoCAD-style PartsBar (colour row + category tabs + part grid) picks
  the active part; Create mode places it at the grid cell and stores a
  JSON-only brick { uID, type, color, position/dimensions, rotation } so
  the piece syncs to peers like any other brick.
- Brick.jsx renders the cached LDraw clone for typed bricks and keeps the
  parametric box as a fallback. Edit-mode tooling (select / move / colour
  / delete) walks up to the brick group so it works on multi-mesh pieces;
  selection is shown with an emissive tint instead of the stud wireframe.
2026-06-13 18:43:44 +02:00

119 lines
3.3 KiB
React

/* eslint-disable react/no-unknown-property */
/* eslint-disable no-unused-vars */
/* eslint-disable react/prop-types */
/* eslint-disable react/display-name */
// import { useSelect } from ".";
import React, { useLayoutEffect, useMemo, useRef } from "react";
import {
createGeometry,
getMeasurementsFromDimensions,
knobSize,
outlineWidth,
LDRAW_PARTS,
} from "../../utils";
import { BackSide, Object3D } from "three";
import { useStore } from "../../store";
const dummy = new Object3D();
const OutlineMesh = ({ meshesData }) => {
const ref = useRef();
const dimensions = meshesData[0]?.dimensions;
const { height, width, depth } = dimensions
? getMeasurementsFromDimensions(dimensions)
: {};
const outlineGeometry = useMemo(() => {
return createGeometry({
width: width + outlineWidth * 2,
height: height + outlineWidth * 2,
depth: depth + outlineWidth * 2,
dimensions,
knobDim: knobSize + outlineWidth,
});
}, [width, height, depth, dimensions]);
useLayoutEffect(() => {
if (!ref.current) return;
meshesData.forEach((meshData, i) => {
const compansate = {
x: dimensions.x % 2 === 0 ? dimensions.x / 2 : (dimensions.x - 1) / 2,
z: dimensions.z % 2 === 0 ? dimensions.z / 2 : (dimensions.z - 1) / 2,
};
const translation = meshData.translation;
const offset = {
x:
Math.sign(translation.x) < 0
? Math.max(translation.x, -compansate.x)
: Math.min(translation.x, compansate.x),
z:
Math.sign(translation.z) < 0
? Math.max(translation.z, -compansate.z)
: Math.min(translation.z, compansate.z),
};
dummy.rotation.set(0, meshData.rotation, 0);
dummy.position.set(
meshData.position.x + (offset.x * width) / dimensions.x,
Math.abs(meshData.position.y),
meshData.position.z + (offset.z * width) / dimensions.z
);
dummy.updateMatrix();
ref.current.setMatrixAt(i, dummy.matrix);
});
ref.current.instanceMatrix.needsUpdate = true;
}, [meshesData]);
return (
<>
<instancedMesh
ref={ref}
position={[0, 0.5, 0]}
args={[outlineGeometry, null, meshesData.length]}
raycast={() => {}}
>
<meshBasicMaterial color={"white"} side={BackSide} />
</instancedMesh>
</>
);
};
export const BrickOutline = () => {
const selected = useStore((state) => state.selectedBricks).map(
(sel) => sel.userData
);
const selectedMeshes = useMemo(() => {
const meshesAccrodingToType = {};
for (let i = 0; i < selected.length; i++) {
const currentSelected = selected[i];
// LDraw pieces are highlighted via their own emissive tint, not the
// box-stud wireframe outline (whose maths only fit parametric bricks).
if (currentSelected.ldrawType) continue;
if (Object.keys(currentSelected).length > 0) {
meshesAccrodingToType[currentSelected.type] = meshesAccrodingToType[
currentSelected.type
]
? [...meshesAccrodingToType[currentSelected.type], currentSelected]
: [currentSelected];
}
}
return meshesAccrodingToType;
}, [selected]);
return (
<>
{Object.entries(selectedMeshes).map(([key, value]) => (
<OutlineMesh key={key} meshesData={value} />
))}
</>
);
};