Skip to content

Commit 4e62c48

Browse files
set up cup example in threejs
1 parent b4bc318 commit 4e62c48

File tree

6 files changed

+576
-7
lines changed

6 files changed

+576
-7
lines changed

webpack/threejs/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"scripts": {
77
"test": "echo \"Error: no test specified\" && exit 1",
88
"build": "webpack",
9-
"watch": "webpack serve"
9+
"start": "webpack serve"
1010
},
1111
"keywords": [],
1212
"author": "",

webpack/threejs/src/code/cup-three.ts

Lines changed: 375 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,375 @@
1+
2+
import { BitByBitBase } from "@bitbybit-dev/threejs";
3+
import { OccStateEnum } from '@bitbybit-dev/occt-worker';
4+
import { Inputs } from '@bitbybit-dev/threejs';
5+
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
6+
import { STLExporter } from 'three/examples/jsm/exporters/STLExporter'
7+
import { Color, DirectionalLight, Fog, Group, HemisphereLight, Mesh, MeshPhongMaterial, PerspectiveCamera, PlaneGeometry, Scene, Vector3, VSMShadowMap, WebGLRenderer } from 'three';
8+
import { GUI } from 'lil-gui';
9+
10+
function component() {
11+
12+
const showSpinner = () => {
13+
const element = document.createElement('div');
14+
element.id = "spinner";
15+
element.className = "lds-ellipsis";
16+
element.innerHTML = `
17+
<div></div>
18+
<div></div>
19+
<div></div>
20+
`;
21+
22+
document.body.appendChild(
23+
element
24+
);
25+
}
26+
27+
const hideSpinner = () => {
28+
const el = document.getElementById('spinner');
29+
if (el) {
30+
el.remove();
31+
}
32+
}
33+
34+
let current: { group: Group | undefined, ground: Mesh | undefined, gui: GUI | undefined } = {
35+
group: undefined,
36+
ground: undefined,
37+
gui: undefined,
38+
};
39+
40+
type Model = {
41+
cupHeight: number,
42+
cupRadius: number,
43+
cupThickness: number,
44+
cupHandleDistance: number,
45+
cupHandleHeight: number,
46+
color: string,
47+
downloadSTL?: () => void,
48+
downloadStep?: () => void
49+
}
50+
51+
52+
const model = {
53+
cupHeight: 13,
54+
cupRadius: 4.5,
55+
cupThickness: 1,
56+
cupHandleDistance: 2,
57+
cupHandleHeight: 0.5,
58+
color: '#000000',
59+
} as Model;
60+
61+
let shapesToClean: Inputs.OCCT.TopoDSShapePointer[] = [];
62+
let finalShape: Inputs.OCCT.TopoDSShapePointer | undefined;
63+
let bitbybit: BitByBitBase | undefined;
64+
let scene: Scene | undefined;
65+
66+
const rotateGroup = () => {
67+
if (current.group) {
68+
current.group.rotation.y -= 0.01;
69+
}
70+
}
71+
72+
const createShape = async (bitbybit?: BitByBitBase, scene?: Scene) => {
73+
if (scene && bitbybit) {
74+
if (shapesToClean.length > 0) {
75+
await bitbybit.occt.deleteShapes({ shapes: shapesToClean });
76+
}
77+
78+
const faceColour = '#444444';
79+
80+
const roundingRadius = model.cupThickness / 3;
81+
const cupHolderLength = 2;
82+
const cupHolderThickness = model.cupThickness * 1.5;
83+
const cupHolderHeight = bitbybit.math.remap(
84+
{
85+
number: model.cupHandleHeight,
86+
fromLow: 0,
87+
fromHigh: 1,
88+
toLow: cupHolderThickness * 2 + roundingRadius * 2.5,
89+
toHigh: (model.cupHeight - model.cupThickness * 2)
90+
}
91+
)
92+
// .mapRange(model.cupHandleHeight, 0, 1, cupHolderThickness * 2 + roundingRadius * 2.5, (model.cupHeight - model.cupThickness * 2));
93+
94+
const cupHolderWidth = model.cupHandleDistance + model.cupThickness * 2;
95+
const edgeColour = "#ffffff";
96+
97+
const box = await bitbybit.occt.shapes.solid.createBox({
98+
width: cupHolderWidth * 2,
99+
height: cupHolderHeight,
100+
length: cupHolderLength,
101+
center: [model.cupRadius, model.cupHeight / 2, 0]
102+
});
103+
104+
const boxInside = await bitbybit.occt.shapes.solid.createBox({
105+
width: (cupHolderWidth * 2) - (cupHolderThickness * 2),
106+
height: cupHolderHeight - (cupHolderThickness * 2),
107+
length: cupHolderLength * 1.2,
108+
center: [model.cupRadius, model.cupHeight / 2, 0]
109+
});
110+
111+
112+
const boolHolder = await bitbybit.occt.booleans.difference({
113+
shape: box,
114+
shapes: [boxInside],
115+
keepEdges: false
116+
});
117+
118+
const cylinder = await bitbybit.occt.shapes.solid.createCylinder({
119+
center: [0, 0, 0],
120+
radius: model.cupRadius,
121+
height: model.cupHeight
122+
});
123+
124+
const baseUnion = await bitbybit.occt.booleans.union({
125+
shapes: [cylinder, boolHolder],
126+
keepEdges: false
127+
});
128+
129+
const cylinderInside = await bitbybit.occt.shapes.solid.createCylinder({
130+
center: [0, model.cupThickness, 0],
131+
radius: model.cupRadius - model.cupThickness,
132+
height: model.cupHeight
133+
});
134+
135+
const cupBase = await bitbybit.occt.booleans.difference({
136+
shape: baseUnion,
137+
shapes: [cylinderInside],
138+
keepEdges: false
139+
});
140+
141+
finalShape = await bitbybit.occt.fillets.filletEdges({
142+
radius: roundingRadius,
143+
shape: cupBase
144+
});
145+
146+
shapesToClean = [box, boxInside, boolHolder, cylinder, baseUnion, cylinderInside, cupBase, finalShape];
147+
148+
const options = new Inputs.Draw.DrawOcctShapeOptions();
149+
options.faceColour = faceColour;
150+
options.faceOpacity = 1;
151+
options.edgeOpacity = 1;
152+
options.edgeWidth = 3;
153+
options.drawEdges = true;
154+
options.edgeColour = edgeColour;
155+
options.precision = 0.001;
156+
157+
const mat = new MeshPhongMaterial({ color: new Color(model.color) });
158+
mat.polygonOffset = true;
159+
mat.polygonOffsetFactor = 3;
160+
options.faceMaterial = mat;
161+
162+
const group = await bitbybit.draw.drawAnyAsync({ entity: finalShape, options });
163+
164+
group.children[0].children.forEach((child) => {
165+
child.castShadow = true;
166+
child.receiveShadow = true;
167+
});
168+
169+
current.group = group;
170+
}
171+
}
172+
173+
const downloadStep = async () => {
174+
if (bitbybit && finalShape) {
175+
await bitbybit.occt.io.saveShapeSTEP({
176+
shape: finalShape,
177+
fileName: 'shape.stp',
178+
adjustYtoZ: true,
179+
tryDownload: true
180+
});
181+
}
182+
}
183+
184+
const downloadSTL = () => {
185+
if (scene) {
186+
var exporter = new STLExporter();
187+
var str = exporter.parse(scene);
188+
var blob = new Blob([str], { type: 'text/plain' });
189+
var link = document.createElement('a');
190+
link.style.display = 'none';
191+
document.body.appendChild(link);
192+
link.href = URL.createObjectURL(blob);
193+
link.download = 'Scene.stl';
194+
link.click();
195+
}
196+
}
197+
198+
model.downloadSTL = downloadSTL;
199+
model.downloadStep = downloadStep;
200+
201+
const updateShape = async () => {
202+
showSpinner();
203+
disableGUI();
204+
current.group?.traverse((obj) => {
205+
scene?.remove(obj);
206+
});
207+
await createShape(bitbybit, scene);
208+
enableGUI();
209+
hideSpinner();
210+
}
211+
212+
const init = async () => {
213+
showSpinner();
214+
bitbybit = new BitByBitBase();
215+
216+
const domNode = document.getElementById('three-canvas') as HTMLCanvasElement;
217+
const occt = new Worker(new URL('../occ.worker', import.meta.url), { name: 'OCC', type: 'module' });
218+
219+
const camera = new PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.01, 1000);
220+
scene = new Scene();
221+
scene.fog = new Fog(0x1a1c1f, 15, 60);
222+
const light = new HemisphereLight(0xffffff, 0x000000, 10);
223+
scene.add(light);
224+
await bitbybit.init(scene, occt, undefined);
225+
226+
const renderer = new WebGLRenderer({ antialias: true, canvas: domNode });
227+
camera.aspect = window.innerWidth / window.innerHeight;
228+
camera.updateProjectionMatrix();
229+
renderer.setSize(window.innerWidth, window.innerHeight);
230+
renderer.setPixelRatio(window.devicePixelRatio / 1.5)
231+
const animation = (time: number) => {
232+
renderer.render(scene, camera);
233+
rotateGroup();
234+
controls.update();
235+
}
236+
237+
const controls = new OrbitControls(camera, renderer.domElement);
238+
camera.position.set(10, 20, 10);
239+
240+
controls.update();
241+
controls.target = new Vector3(0, 5, 0);
242+
controls.enableDamping = true;
243+
controls.dampingFactor = 0.1
244+
controls.zoomSpeed = 0.1;
245+
246+
renderer.shadowMap.enabled = true;
247+
renderer.shadowMap.type = VSMShadowMap;
248+
249+
const onWindowResize = () => {
250+
camera.aspect = window.innerWidth / window.innerHeight;
251+
camera.updateProjectionMatrix();
252+
renderer.setSize(window.innerWidth, window.innerHeight);
253+
}
254+
window.addEventListener("resize", onWindowResize, false);
255+
256+
renderer.setClearColor(new Color(0x1a1c1f), 1);
257+
258+
bitbybit.occtWorkerManager.occWorkerState$.subscribe(async s => {
259+
if (s.state === OccStateEnum.initialised) {
260+
await createShape(bitbybit, scene);
261+
262+
renderer.setAnimationLoop(animation);
263+
264+
const dirLight = new DirectionalLight(0xffffff, 50);
265+
dirLight.position.set(15, 40, -15);
266+
dirLight.castShadow = true;
267+
dirLight.shadow.camera.near = 0;
268+
dirLight.shadow.camera.far = 300;
269+
const dist = 100;
270+
dirLight.shadow.camera.right = dist;
271+
dirLight.shadow.camera.left = - dist;
272+
dirLight.shadow.camera.top = dist;
273+
dirLight.shadow.camera.bottom = - dist;
274+
dirLight.shadow.mapSize.width = 3000;
275+
dirLight.shadow.mapSize.height = 3000;
276+
dirLight.shadow.blurSamples = 8;
277+
dirLight.shadow.radius = 2;
278+
dirLight.shadow.bias = -0.0005;
279+
280+
scene.add(dirLight);
281+
282+
const material = new MeshPhongMaterial({ color: 0x000000 })
283+
material.shininess = 0;
284+
material.specular = new Color(0x222222);
285+
const ground = new Mesh(new PlaneGeometry(50, 50, 1, 1), material);
286+
ground.rotateX(-Math.PI / 2);
287+
ground.receiveShadow = true;
288+
current.ground = ground;
289+
scene.add(ground);
290+
291+
createGui();
292+
hideSpinner();
293+
}
294+
});
295+
}
296+
297+
const disableGUI = () => {
298+
const lilGui = document.getElementsByClassName('lil-gui')[0] as HTMLElement;
299+
lilGui.style.pointerEvents = "none";
300+
lilGui.style.opacity = "0.5";
301+
}
302+
303+
const enableGUI = () => {
304+
const lilGui = document.getElementsByClassName('lil-gui')[0] as HTMLElement;
305+
lilGui.style.pointerEvents = "all";
306+
lilGui.style.opacity = "1";
307+
}
308+
309+
const createGui = () => {
310+
311+
const gui = new GUI();
312+
current.gui = gui;
313+
gui.$title.innerHTML = "Cup";
314+
315+
gui
316+
.add(model, "cupHeight", 6, 16, 0.01)
317+
.name("Height")
318+
.onFinishChange((value: number) => {
319+
model.cupHeight = value;
320+
updateShape();
321+
});
322+
323+
gui
324+
.add(model, "cupRadius", 3, 8, 0.01)
325+
.name("Radius")
326+
.onFinishChange((value: number) => {
327+
model.cupRadius = value;
328+
updateShape();
329+
});
330+
331+
gui
332+
.add(model, "cupThickness", 0.5, 1, 0.01)
333+
.name("Thickness")
334+
.onFinishChange((value: number) => {
335+
model.cupThickness = value;
336+
updateShape();
337+
});
338+
339+
gui
340+
.add(model, "cupHandleDistance", 0.3, 3, 0.01)
341+
.name("Handle Distance")
342+
.onFinishChange((value: number) => {
343+
model.cupHandleDistance = value;
344+
updateShape();
345+
});
346+
347+
gui
348+
.add(model, "cupHandleHeight", 0, 1, 0.01)
349+
.name("Handle Height")
350+
.onFinishChange((value: number) => {
351+
model.cupHandleHeight = value;
352+
updateShape();
353+
});
354+
355+
gui
356+
.addColor(model, "color")
357+
.name("Color")
358+
.onChange((value: string) => {
359+
const children = current.group?.children[0].children as Mesh[];
360+
[...children, current.ground].forEach((child) => {
361+
const material = (child as Mesh).material as MeshPhongMaterial;
362+
material.color.setHex(parseInt(value.replace('#', '0x')));
363+
});
364+
365+
})
366+
367+
gui.add(model, "downloadSTL").name("Download STL");
368+
gui.add(model, "downloadStep").name("Download STEP");
369+
}
370+
371+
init();
372+
373+
}
374+
375+
component();

0 commit comments

Comments
 (0)