2022-02-20 22:47:04 -05:00
|
|
|
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
2017-04-18 22:22:29 -04:00
|
|
|
|
2026-01-03 19:43:51 -05:00
|
|
|
import { decode } from '../core/codec.js';
|
2025-07-09 13:00:52 -04:00
|
|
|
import { util as mesh_util } from '../../mesh/util.js';
|
2025-12-13 15:48:30 -05:00
|
|
|
import { tool as mesh_tool } from '../../mesh/tool.js';
|
2025-12-06 00:09:17 -05:00
|
|
|
import { verticesToPoints } from '../../geo/points.js';
|
|
|
|
|
import { newPoint } from '../../geo/point.js';
|
|
|
|
|
import { newPolygon } from '../../geo/polygon.js';
|
|
|
|
|
import { polygons as POLY } from '../../geo/polygons.js';
|
|
|
|
|
import { checkOverUnderOn, intersectPoints } from '../../geo/slicer.js';
|
2022-02-18 11:30:00 -05:00
|
|
|
|
2023-02-07 15:09:28 -05:00
|
|
|
const sharedArrayClass = self.SharedArrayBuffer || undefined;
|
2023-02-07 14:54:55 -05:00
|
|
|
const hasSharedArrays = sharedArrayClass ? true : false;
|
2025-12-27 13:24:04 -05:00
|
|
|
const solid_opacity = 1.0;
|
|
|
|
|
const groups = [];
|
2026-01-03 19:43:51 -05:00
|
|
|
const debug_shadow = false;
|
2023-02-07 14:54:55 -05:00
|
|
|
|
2022-02-18 12:08:08 -05:00
|
|
|
let nextId = 0;
|
2017-04-18 17:09:33 -04:00
|
|
|
|
2022-02-18 12:08:08 -05:00
|
|
|
class Widget {
|
|
|
|
|
constructor(id, group) {
|
2025-07-06 23:44:33 -04:00
|
|
|
this.api = self.kiri_api;
|
|
|
|
|
|
2024-06-30 23:15:03 -04:00
|
|
|
this.id = id || Date.now().toString(36)+(nextId++);
|
2021-12-07 18:28:49 -05:00
|
|
|
this.grouped = group ? true : false;
|
2026-01-05 20:09:00 -05:00
|
|
|
// persisted
|
2020-04-07 19:56:30 -04:00
|
|
|
this.group = group || [];
|
|
|
|
|
this.group.push(this);
|
2020-04-10 11:53:07 -04:00
|
|
|
if (!this.group.id) {
|
|
|
|
|
this.group.id = this.id;
|
|
|
|
|
}
|
2020-04-07 19:56:30 -04:00
|
|
|
if (groups.indexOf(this.group) < 0) {
|
|
|
|
|
groups.push(this.group);
|
|
|
|
|
}
|
2020-11-22 17:01:36 -05:00
|
|
|
// rotation stack (for undo)
|
2020-06-14 10:00:05 -04:00
|
|
|
this.roto = [];
|
2026-01-05 20:09:00 -05:00
|
|
|
// overlay meshes (supports, tabs, etc)
|
2020-11-22 20:06:11 -05:00
|
|
|
this.adds = [];
|
|
|
|
|
// THREE Mesh and points
|
2017-04-18 17:09:33 -04:00
|
|
|
this.mesh = null;
|
|
|
|
|
this.points = null;
|
|
|
|
|
// todo resolve use of this vs. mesh.bounds
|
|
|
|
|
this.bounds = null;
|
2020-11-22 20:06:11 -05:00
|
|
|
// wireframe
|
2017-04-18 17:09:33 -04:00
|
|
|
this.wire = null;
|
|
|
|
|
this.slices = null;
|
2022-11-02 23:30:40 -04:00
|
|
|
this.settings = null; // used??
|
2017-04-18 17:09:33 -04:00
|
|
|
this.modified = true;
|
2025-04-28 19:51:50 -06:00
|
|
|
this.boundingBoxNeedsUpdate = true
|
2026-01-05 20:09:00 -05:00
|
|
|
// cache shadow geo
|
|
|
|
|
this.cache = {};
|
2026-01-08 14:35:58 -05:00
|
|
|
// geometry version for edge cache invalidation
|
|
|
|
|
this.geomVersion = 0;
|
2026-01-05 20:09:00 -05:00
|
|
|
this.stats = {
|
|
|
|
|
slice_time: 0,
|
|
|
|
|
load_time: 0,
|
|
|
|
|
progress: 0
|
|
|
|
|
};
|
|
|
|
|
// if this is a synthesized support widget
|
|
|
|
|
this.support = false;
|
|
|
|
|
// persisted: client annotations (cam tabs, fdm supports)
|
|
|
|
|
this.anno = {};
|
|
|
|
|
// persisted: file meta-data
|
|
|
|
|
this.meta = {
|
|
|
|
|
url: null,
|
|
|
|
|
file: null,
|
|
|
|
|
saved: false
|
|
|
|
|
};
|
|
|
|
|
// persisted: location state
|
2020-04-07 19:56:30 -04:00
|
|
|
this.track = {
|
|
|
|
|
// box size for packer
|
|
|
|
|
box: {
|
|
|
|
|
w: 0,
|
|
|
|
|
h: 0,
|
|
|
|
|
d: 0
|
|
|
|
|
},
|
2017-04-18 17:09:33 -04:00
|
|
|
scale: {
|
|
|
|
|
x: 1.0,
|
|
|
|
|
y: 1.0,
|
|
|
|
|
z: 1.0
|
|
|
|
|
},
|
|
|
|
|
rot: {
|
|
|
|
|
x: 0,
|
|
|
|
|
y: 0,
|
|
|
|
|
z: 0
|
|
|
|
|
},
|
|
|
|
|
pos: {
|
|
|
|
|
x: 0,
|
|
|
|
|
y: 0,
|
|
|
|
|
z: 0
|
|
|
|
|
},
|
2020-12-22 15:05:50 -05:00
|
|
|
top: 0, // z top
|
2023-01-14 10:57:32 -05:00
|
|
|
mirror: false,
|
2025-12-30 22:29:30 -05:00
|
|
|
indexed: false,
|
|
|
|
|
indexRad: 0
|
2017-04-18 17:09:33 -04:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2022-02-18 12:08:08 -05:00
|
|
|
annotations() {
|
2021-10-18 23:36:19 -04:00
|
|
|
let o = Object.clone(this.anno);
|
|
|
|
|
if (o.support) {
|
|
|
|
|
// clear out THREE.Box temps
|
|
|
|
|
for (let s of o.support) {
|
|
|
|
|
delete s.box;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return o;
|
2022-02-18 12:08:08 -05:00
|
|
|
}
|
2021-10-18 23:36:19 -04:00
|
|
|
|
2023-02-07 14:54:55 -05:00
|
|
|
#newBuffer(length) {
|
|
|
|
|
if (hasSharedArrays) {
|
|
|
|
|
return new SharedArrayBuffer(length);
|
|
|
|
|
} else {
|
|
|
|
|
return new ArrayBuffer(length);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2017-04-18 17:09:33 -04:00
|
|
|
/**
|
|
|
|
|
*
|
|
|
|
|
* @param {Float32Array} vertices
|
|
|
|
|
* @returns {Widget}
|
|
|
|
|
*/
|
2025-12-13 15:48:30 -05:00
|
|
|
loadVertices(data, options = { normalize: false }) {
|
|
|
|
|
if (options.normalize) {
|
|
|
|
|
console.time('mesh normalize')
|
|
|
|
|
data = new mesh_tool({ precision: 0.001 }).normalizeVertices(data).toFloat32();
|
|
|
|
|
console.timeEnd('mesh normalize');
|
|
|
|
|
}
|
2025-09-01 16:56:43 -04:00
|
|
|
// console.trace({ loadVertices: this.id, worker: this.inWorker, data });
|
2021-09-20 21:43:06 -04:00
|
|
|
let vertices,
|
|
|
|
|
autoscale = false;
|
|
|
|
|
if (ArrayBuffer.isView(data) || typeof(data) != 'object') {
|
|
|
|
|
vertices = data;
|
|
|
|
|
} else {
|
|
|
|
|
vertices = data.vertices;
|
2022-05-15 15:20:39 -04:00
|
|
|
throw "deprecated vertex data format";
|
|
|
|
|
}
|
2023-02-07 15:09:28 -05:00
|
|
|
if (vertices.buffer) {
|
|
|
|
|
if (hasSharedArrays && vertices.buffer instanceof sharedArrayClass) {
|
|
|
|
|
// console.log('converting to shared vertices');
|
|
|
|
|
let newvert = new Float32Array(this.#newBuffer(vertices.buffer.byteLength));
|
|
|
|
|
newvert.set(vertices);
|
|
|
|
|
vertices = newvert;
|
|
|
|
|
}
|
2023-01-09 14:55:17 -05:00
|
|
|
}
|
2021-09-20 21:43:06 -04:00
|
|
|
switch (typeof(autoscale)) {
|
|
|
|
|
case 'boolean':
|
|
|
|
|
autoscale = options;
|
|
|
|
|
break;
|
|
|
|
|
case 'object':
|
|
|
|
|
autoscale = options.autoscale;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
if (!vertices) {
|
|
|
|
|
console.log('missing vertices', {data, options});
|
|
|
|
|
return;
|
|
|
|
|
}
|
2021-08-28 19:20:37 -04:00
|
|
|
if (autoscale === true) {
|
|
|
|
|
// onshape exports obj in meters by default :/
|
|
|
|
|
let maxv = 0;
|
|
|
|
|
for (let i=0; i<vertices.length; i++) {
|
2021-12-12 19:54:55 -05:00
|
|
|
maxv = Math.max(maxv,Math.abs(vertices[i]));
|
2021-08-28 19:20:37 -04:00
|
|
|
}
|
|
|
|
|
if (maxv < 1) {
|
|
|
|
|
for (let i=0; i<vertices.length; i++) {
|
|
|
|
|
vertices[i] *= 1000;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2017-04-18 17:09:33 -04:00
|
|
|
if (this.mesh) {
|
2021-09-06 21:47:27 -04:00
|
|
|
let geo = this.mesh.geometry;
|
|
|
|
|
geo.setAttribute('position', new THREE.BufferAttribute(vertices, 3));
|
|
|
|
|
geo.attributes.position.needsUpdate = true;
|
2022-10-14 15:06:06 -04:00
|
|
|
// geo.computeVertexNormals();
|
2020-11-25 09:10:00 -05:00
|
|
|
this.meta.vertices = vertices.length / 3;
|
2021-09-20 21:43:06 -04:00
|
|
|
this.points = null;
|
2017-04-18 17:09:33 -04:00
|
|
|
return this;
|
|
|
|
|
} else {
|
2021-09-20 21:43:06 -04:00
|
|
|
let geo = new THREE.BufferGeometry();
|
|
|
|
|
geo.setAttribute('position', new THREE.BufferAttribute(vertices, 3));
|
2024-09-06 10:00:47 -04:00
|
|
|
// geo.computeVertexNormals();
|
2020-11-25 09:10:00 -05:00
|
|
|
this.meta.vertices = vertices.length / 3;
|
2021-09-20 21:43:06 -04:00
|
|
|
this.points = null;
|
|
|
|
|
return this.loadGeometry(geo);
|
2017-04-18 17:09:33 -04:00
|
|
|
}
|
2022-02-18 12:08:08 -05:00
|
|
|
}
|
2017-04-18 17:09:33 -04:00
|
|
|
|
2022-02-18 12:08:08 -05:00
|
|
|
loadData() {
|
|
|
|
|
return this.loadVertices(...arguments);
|
|
|
|
|
}
|
2021-09-20 21:43:06 -04:00
|
|
|
|
2025-12-28 19:55:44 -05:00
|
|
|
setModified(reason) {
|
2021-10-07 14:22:13 -04:00
|
|
|
this.modified = true;
|
2025-04-29 09:07:56 -06:00
|
|
|
this.boundingBoxNeedsUpdate = true;
|
2025-12-28 19:55:44 -05:00
|
|
|
this.clearShadows();
|
|
|
|
|
if (reason !== 'axis') {
|
|
|
|
|
this.cache.geo = undefined;
|
2026-01-08 14:35:58 -05:00
|
|
|
this.geomVersion++; // Increment version to invalidate edge cache
|
2025-12-28 19:55:44 -05:00
|
|
|
}
|
2021-10-07 14:22:13 -04:00
|
|
|
if (this.mesh && this.mesh.geometry) {
|
|
|
|
|
// this fixes ray intersections after the mesh is modified
|
|
|
|
|
this.mesh.geometry.boundingSphere = null;
|
|
|
|
|
}
|
2022-02-18 12:08:08 -05:00
|
|
|
}
|
2021-10-07 14:22:13 -04:00
|
|
|
|
2022-11-03 23:16:07 -04:00
|
|
|
// should go away and be replaced by getGeoVertices
|
2022-02-18 12:08:08 -05:00
|
|
|
getVertices() {
|
2021-01-26 20:07:17 -05:00
|
|
|
return this.mesh.geometry.attributes.position;
|
2022-02-18 12:08:08 -05:00
|
|
|
}
|
2021-01-26 20:07:17 -05:00
|
|
|
|
2017-04-18 17:09:33 -04:00
|
|
|
/**
|
|
|
|
|
* @param {THREE.Geometry} geometry
|
|
|
|
|
* @returns {Widget}
|
|
|
|
|
*/
|
2022-02-18 12:08:08 -05:00
|
|
|
loadGeometry(geometry) {
|
|
|
|
|
const mesh = new THREE.Mesh(
|
2022-04-04 21:37:15 -04:00
|
|
|
geometry, [
|
|
|
|
|
new THREE.MeshPhongMaterial({
|
2022-04-27 17:56:07 -04:00
|
|
|
side: THREE.DoubleSide,
|
2017-04-18 17:09:33 -04:00
|
|
|
color: 0xffff00,
|
2022-01-19 22:06:55 -05:00
|
|
|
specular: 0x202020,
|
2022-09-26 13:28:31 -04:00
|
|
|
shininess: 120,
|
2017-04-18 17:09:33 -04:00
|
|
|
transparent: true,
|
2022-04-27 17:56:07 -04:00
|
|
|
opacity: solid_opacity,
|
2022-10-14 15:06:06 -04:00
|
|
|
clipIntersection: false,
|
|
|
|
|
flatShading: true
|
2022-04-04 21:37:15 -04:00
|
|
|
}),
|
|
|
|
|
new THREE.MeshPhongMaterial({
|
2022-04-27 17:56:07 -04:00
|
|
|
side: THREE.DoubleSide,
|
2022-04-04 21:37:15 -04:00
|
|
|
color: 0x0088ee,
|
|
|
|
|
specular: 0x202020,
|
|
|
|
|
shininess: 100,
|
|
|
|
|
transparent: true,
|
2022-10-14 15:06:06 -04:00
|
|
|
opacity: solid_opacity,
|
|
|
|
|
flatShading: true
|
2022-04-04 21:37:15 -04:00
|
|
|
}),
|
|
|
|
|
]);
|
2020-11-13 23:09:53 -05:00
|
|
|
mesh.renderOrder = 1;
|
2022-10-14 15:06:06 -04:00
|
|
|
// geometry.computeVertexNormals();
|
2026-01-08 20:18:56 -05:00
|
|
|
// Clear existing groups (e.g., from cloned geometry) before adding new one
|
|
|
|
|
geometry.clearGroups();
|
2022-04-03 14:56:58 -04:00
|
|
|
geometry.addGroup(0, Infinity, 0);
|
2022-10-14 12:42:51 -04:00
|
|
|
// mesh.castShadow = true;
|
|
|
|
|
// mesh.receiveShadow = true;
|
2017-04-18 17:09:33 -04:00
|
|
|
mesh.widget = this;
|
|
|
|
|
this.mesh = mesh;
|
|
|
|
|
// invalidates points cache (like any scale/rotation)
|
2020-04-07 19:56:30 -04:00
|
|
|
this.center(true);
|
2017-04-18 17:09:33 -04:00
|
|
|
return this;
|
2022-02-18 12:08:08 -05:00
|
|
|
}
|
2017-04-18 17:09:33 -04:00
|
|
|
|
2022-02-18 12:08:08 -05:00
|
|
|
groupBounds() {
|
2020-04-07 19:56:30 -04:00
|
|
|
return Group.bounds(this.group);
|
2022-02-18 12:08:08 -05:00
|
|
|
}
|
2020-04-07 19:56:30 -04:00
|
|
|
|
2017-04-18 17:09:33 -04:00
|
|
|
/**
|
|
|
|
|
* @param {Point[]} points
|
|
|
|
|
* @returns {Widget}
|
|
|
|
|
*/
|
2022-02-18 12:08:08 -05:00
|
|
|
setPoints(points) {
|
2017-04-18 17:09:33 -04:00
|
|
|
this.points = points || null;
|
|
|
|
|
return this;
|
2022-02-18 12:08:08 -05:00
|
|
|
}
|
2017-04-18 17:09:33 -04:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* remove slice data and their views
|
|
|
|
|
*/
|
2022-02-18 12:08:08 -05:00
|
|
|
clearSlices() {
|
2020-04-07 19:56:30 -04:00
|
|
|
let slices = this.slices,
|
2017-04-18 17:09:33 -04:00
|
|
|
mesh = this.mesh;
|
2020-11-30 22:36:06 -05:00
|
|
|
if (slices && mesh && mesh.remove) {
|
2017-04-18 17:09:33 -04:00
|
|
|
slices.forEach(function(slice) {
|
|
|
|
|
mesh.remove(slice.view);
|
|
|
|
|
});
|
|
|
|
|
}
|
2020-11-30 18:01:32 -05:00
|
|
|
this.slices = null;
|
2022-01-24 21:54:50 -05:00
|
|
|
}
|
|
|
|
|
|
2022-02-18 12:08:08 -05:00
|
|
|
getColor() {
|
2021-10-09 01:13:41 -04:00
|
|
|
return this.color;
|
2022-02-18 12:08:08 -05:00
|
|
|
}
|
2021-10-09 01:13:41 -04:00
|
|
|
|
2022-04-03 14:56:58 -04:00
|
|
|
getMaterial() {
|
|
|
|
|
return this.mesh.material[0];
|
|
|
|
|
}
|
|
|
|
|
|
2022-04-27 17:56:07 -04:00
|
|
|
setZClip(from, to) {
|
|
|
|
|
let mat = this.getMaterial();
|
|
|
|
|
mat.clippingPlanes = (from >= 0 && to >= 0) ? [
|
|
|
|
|
new THREE.Plane(new THREE.Vector3(0, 1, 0), -from),
|
|
|
|
|
new THREE.Plane(new THREE.Vector3(0, -1, 0), to),
|
|
|
|
|
] : null;
|
|
|
|
|
moto.space.refresh();
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-27 12:40:27 -05:00
|
|
|
isSynth() {
|
|
|
|
|
return this.track.synth ?? false;
|
|
|
|
|
}
|
|
|
|
|
|
2022-04-03 14:56:58 -04:00
|
|
|
isVisible() {
|
|
|
|
|
return this.getMaterial().visible;
|
|
|
|
|
}
|
|
|
|
|
|
2022-04-04 21:37:15 -04:00
|
|
|
selectFaces(faces) {
|
2025-07-06 23:44:33 -04:00
|
|
|
let groups = mesh_util.facesToGroups(faces || []);
|
2022-04-04 21:37:15 -04:00
|
|
|
let geo = this.mesh.geometry;
|
|
|
|
|
geo.clearGroups();
|
|
|
|
|
for (let group of groups) {
|
|
|
|
|
geo.addGroup(group.start*3, group.count*3, group.mat || 0);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2017-04-18 17:09:33 -04:00
|
|
|
|
2024-09-03 18:21:57 -04:00
|
|
|
toggleVisibility(bool) {
|
2024-09-03 12:44:06 -04:00
|
|
|
const mat = this.getMaterial();
|
2024-09-03 18:21:57 -04:00
|
|
|
mat.visible = bool ?? !mat.visible;
|
2024-09-03 12:44:06 -04:00
|
|
|
}
|
|
|
|
|
|
2017-04-18 17:09:33 -04:00
|
|
|
/**
|
|
|
|
|
* center geometry bottom (on platform) at 0,0,0
|
|
|
|
|
*/
|
2022-02-18 12:08:08 -05:00
|
|
|
center(init) {
|
2020-04-07 19:56:30 -04:00
|
|
|
let bb = init ? this.mesh.getBoundingBox(true) : this.groupBounds(),
|
2017-04-18 17:09:33 -04:00
|
|
|
bm = bb.min.clone(),
|
|
|
|
|
bM = bb.max.clone(),
|
|
|
|
|
bd = bM.sub(bm).multiplyScalar(0.5),
|
2020-04-07 19:56:30 -04:00
|
|
|
dx = bm.x + bd.x,
|
|
|
|
|
dy = bm.y + bd.y,
|
|
|
|
|
dz = bm.z;
|
2022-11-02 21:08:55 -04:00
|
|
|
if (this.track.indexed) {
|
|
|
|
|
dz += bd.z;
|
|
|
|
|
}
|
2021-01-11 12:26:19 -05:00
|
|
|
this.track.center = { dx, dy, dz };
|
2020-04-07 19:56:30 -04:00
|
|
|
// move mesh for each widget in group
|
|
|
|
|
if (!init) {
|
|
|
|
|
this.group.forEach(w => {
|
|
|
|
|
w.moveMesh(dx,dy,dz);
|
|
|
|
|
});
|
|
|
|
|
}
|
2020-11-27 15:50:22 -05:00
|
|
|
return this;
|
2022-02-18 12:08:08 -05:00
|
|
|
}
|
2020-04-07 19:56:30 -04:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* called by center() and Group.center()
|
2022-01-10 11:53:23 -05:00
|
|
|
* todo use new prototype.moveMesh()
|
2020-04-07 19:56:30 -04:00
|
|
|
*/
|
2022-02-18 12:08:08 -05:00
|
|
|
moveMesh(x, y, z) {
|
2022-11-13 13:36:33 -05:00
|
|
|
// if (!(x || y || z)) {
|
|
|
|
|
// return;
|
|
|
|
|
// }
|
2020-04-07 19:56:30 -04:00
|
|
|
let gap = this.mesh.geometry.attributes.position,
|
2017-04-18 17:09:33 -04:00
|
|
|
pa = gap.array;
|
|
|
|
|
// center point array on 0,0,0
|
2022-11-13 13:36:33 -05:00
|
|
|
if (x || y || z) {
|
|
|
|
|
for (let i=0; i < pa.length; i += 3) {
|
|
|
|
|
pa[i ] -= x;
|
|
|
|
|
pa[i + 1] -= y;
|
|
|
|
|
pa[i + 2] -= z;
|
|
|
|
|
}
|
|
|
|
|
gap.needsUpdate = true;
|
2017-04-18 17:09:33 -04:00
|
|
|
}
|
2020-04-07 19:56:30 -04:00
|
|
|
let bb = this.groupBounds();
|
2021-12-27 09:03:17 -05:00
|
|
|
// critical to layout and grouping
|
2020-04-07 19:56:30 -04:00
|
|
|
this.track.box = {
|
|
|
|
|
w: (bb.max.x - bb.min.x),
|
|
|
|
|
h: (bb.max.y - bb.min.y),
|
|
|
|
|
d: (bb.max.z - bb.min.z)
|
|
|
|
|
};
|
2017-04-18 17:09:33 -04:00
|
|
|
// for use with the packer
|
|
|
|
|
// invalidate cached points
|
2022-11-13 13:36:33 -05:00
|
|
|
if (x || y || z) {
|
|
|
|
|
this.points = null;
|
2025-12-28 19:55:44 -05:00
|
|
|
this.setModified('moveMesh');
|
2022-11-13 13:36:33 -05:00
|
|
|
}
|
2022-02-18 12:08:08 -05:00
|
|
|
}
|
2017-04-18 17:09:33 -04:00
|
|
|
|
2023-02-14 19:37:51 -05:00
|
|
|
get isIndexed() {
|
|
|
|
|
return this.track.indexed ? true : false;
|
|
|
|
|
}
|
|
|
|
|
|
2022-10-28 17:18:01 -04:00
|
|
|
setIndexed(z) {
|
2022-11-02 21:08:55 -04:00
|
|
|
if (z !== this.track.indexed) {
|
|
|
|
|
this.track.indexed = z;
|
|
|
|
|
this.center(false);
|
|
|
|
|
this._updateMeshPosition();
|
|
|
|
|
}
|
2022-10-31 14:39:51 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setAxisIndex(deg) {
|
2025-12-28 19:55:44 -05:00
|
|
|
// console.trace(deg);
|
2022-10-31 14:39:51 -04:00
|
|
|
let rad = deg * (Math.PI / 180);
|
2022-11-02 21:08:55 -04:00
|
|
|
if (rad !== this.track.indexRad) {
|
|
|
|
|
this.track.indexRad = rad;
|
2025-12-28 19:55:44 -05:00
|
|
|
this.setModified('axis');
|
2022-11-02 21:08:55 -04:00
|
|
|
this._updateMeshPosition();
|
|
|
|
|
}
|
2022-10-28 17:18:01 -04:00
|
|
|
}
|
|
|
|
|
|
2017-04-18 17:09:33 -04:00
|
|
|
/**
|
|
|
|
|
* moves top of widget to given Z
|
2025-11-22 11:47:47 -05:00
|
|
|
* only non-zero in CAM mode
|
2017-04-18 17:09:33 -04:00
|
|
|
*
|
|
|
|
|
* @param {number} z position
|
2025-11-22 11:47:47 -05:00
|
|
|
* @param {boolean} cam mode
|
2017-04-18 17:09:33 -04:00
|
|
|
*/
|
2025-11-22 11:47:47 -05:00
|
|
|
setTopZ(z, cam) {
|
2020-04-07 19:56:30 -04:00
|
|
|
let mesh = this.mesh,
|
2022-10-31 14:39:51 -04:00
|
|
|
track = this.track,
|
2022-11-04 12:46:06 -04:00
|
|
|
mbb = mesh.getBoundingBox(),
|
2023-02-14 22:23:30 -05:00
|
|
|
mbz = mbb.max.z,
|
|
|
|
|
idx = this.isIndexed;
|
|
|
|
|
if ((idx && z != undefined) || (!idx && z)) {
|
2022-10-31 14:39:51 -04:00
|
|
|
track.top = z;
|
2017-04-18 17:09:33 -04:00
|
|
|
} else {
|
2022-10-31 14:39:51 -04:00
|
|
|
track.top = mbz;
|
2017-04-18 17:09:33 -04:00
|
|
|
}
|
2022-10-31 14:39:51 -04:00
|
|
|
// difference between top of stock/bounds and top of widget (cam mode)
|
2025-11-22 11:47:47 -05:00
|
|
|
track.tzoff = cam ? mbz - z : 0;
|
2022-10-31 14:39:51 -04:00
|
|
|
this._updateMeshPosition();
|
2022-02-18 12:08:08 -05:00
|
|
|
}
|
2017-04-18 17:09:33 -04:00
|
|
|
|
2022-02-18 12:08:08 -05:00
|
|
|
move(x, y, z, abs) {
|
2020-04-07 19:56:30 -04:00
|
|
|
this.group.forEach(w => {
|
|
|
|
|
w._move(x, y, z, abs);
|
|
|
|
|
});
|
2022-11-04 12:46:06 -04:00
|
|
|
// allow for use in engine / cli
|
2025-07-06 23:44:33 -04:00
|
|
|
if ((x || y || z) && this.api && this.api.event) {
|
|
|
|
|
this.api.event.emit('widget.move', {widget: this, pos: {x, y, z}, abs});
|
2022-11-04 12:46:06 -04:00
|
|
|
}
|
2022-02-18 12:08:08 -05:00
|
|
|
}
|
2020-04-07 19:56:30 -04:00
|
|
|
|
2022-02-18 12:08:08 -05:00
|
|
|
_move(x, y, z, abs) {
|
2025-11-22 11:47:47 -05:00
|
|
|
let mat = this.getMaterial(),
|
2022-10-31 14:39:51 -04:00
|
|
|
pos = this.track.pos;
|
|
|
|
|
// do not allow moves in pure slice view
|
2022-04-03 14:56:58 -04:00
|
|
|
if (!mat.visible) return;
|
2017-04-18 17:09:33 -04:00
|
|
|
if (abs) {
|
|
|
|
|
pos.x = (x || 0);
|
|
|
|
|
pos.y = (y || 0);
|
|
|
|
|
pos.z = (z || 0);
|
|
|
|
|
} else {
|
|
|
|
|
pos.x += (x || 0);
|
|
|
|
|
pos.y += (y || 0);
|
|
|
|
|
pos.z += (z || 0);
|
|
|
|
|
}
|
2020-03-28 14:39:24 -04:00
|
|
|
if (x || y || z) {
|
2025-12-28 19:55:44 -05:00
|
|
|
this.setModified('_move');
|
2022-10-31 14:39:51 -04:00
|
|
|
this._updateMeshPosition();
|
2020-03-28 14:39:24 -04:00
|
|
|
}
|
2022-02-18 12:08:08 -05:00
|
|
|
}
|
2017-04-18 17:09:33 -04:00
|
|
|
|
2022-10-31 14:39:51 -04:00
|
|
|
_updateMeshPosition() {
|
2026-01-05 15:33:29 -05:00
|
|
|
let { cache, mesh, track } = this,
|
|
|
|
|
{ top, tzoff } = track,
|
|
|
|
|
{ x, y, z } = track.pos,
|
|
|
|
|
tz = -tzoff;
|
2022-11-02 21:08:55 -04:00
|
|
|
if (track.indexed) {
|
2025-12-30 22:29:30 -05:00
|
|
|
this.mesh.rotation.x = -track.indexRad;
|
2023-02-14 19:37:51 -05:00
|
|
|
z = top;
|
2022-11-02 21:08:55 -04:00
|
|
|
} else {
|
|
|
|
|
this.mesh.rotation.x = 0;
|
|
|
|
|
z += tz;
|
|
|
|
|
}
|
2026-01-05 15:33:29 -05:00
|
|
|
let { pos } = cache;
|
|
|
|
|
if (!pos || pos.x !== x || pos.y !==y || pos.z !== z) {
|
|
|
|
|
mesh.position.set(x, y, z);
|
|
|
|
|
this._updateEdges();
|
|
|
|
|
cache.pos = { x, y, z };
|
|
|
|
|
}
|
2025-12-30 22:29:30 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_updateEdges() {
|
|
|
|
|
if (this.outline && this.setEdges) {
|
|
|
|
|
this.setEdges(true);
|
|
|
|
|
}
|
2022-10-31 14:39:51 -04:00
|
|
|
}
|
|
|
|
|
|
2022-02-18 12:08:08 -05:00
|
|
|
scale(x, y, z) {
|
2020-04-07 19:56:30 -04:00
|
|
|
this.group.forEach(w => {
|
|
|
|
|
w._scale(x, y, z);
|
|
|
|
|
});
|
2022-11-02 21:08:55 -04:00
|
|
|
this.center(false);
|
2025-07-06 23:44:33 -04:00
|
|
|
if (this.api && this.api.event) {
|
|
|
|
|
this.api.event.emit('widget.scale', {widget: this, x, y, z});
|
2022-11-04 12:46:06 -04:00
|
|
|
}
|
2022-02-18 12:08:08 -05:00
|
|
|
}
|
2020-04-07 19:56:30 -04:00
|
|
|
|
2022-02-18 12:08:08 -05:00
|
|
|
_scale(x, y, z) {
|
2020-03-16 17:26:12 -04:00
|
|
|
let mesh = this.mesh,
|
2020-04-07 19:56:30 -04:00
|
|
|
scale = this.track.scale;
|
2020-03-16 17:26:12 -04:00
|
|
|
this.bounds = null;
|
2025-12-27 13:24:04 -05:00
|
|
|
if (this.setWireframe) this.setWireframe(false);
|
2017-04-18 17:09:33 -04:00
|
|
|
this.clearSlices();
|
2020-03-16 23:57:14 -04:00
|
|
|
mesh.geometry.applyMatrix4(new THREE.Matrix4().makeScale(x, y, z));
|
2025-12-30 22:29:30 -05:00
|
|
|
this._updateEdges();
|
2017-04-18 17:09:33 -04:00
|
|
|
scale.x *= (x || 1.0);
|
|
|
|
|
scale.y *= (y || 1.0);
|
|
|
|
|
scale.z *= (z || 1.0);
|
2025-12-28 19:55:44 -05:00
|
|
|
this.setModified('_scale');
|
2022-02-18 12:08:08 -05:00
|
|
|
}
|
2017-04-18 17:09:33 -04:00
|
|
|
|
2022-11-01 23:53:22 -04:00
|
|
|
rotate(x, y, z, temp, center = true) {
|
2021-04-27 21:53:21 -04:00
|
|
|
this.group.forEach(w => {
|
|
|
|
|
w._rotate(x, y, z, temp);
|
|
|
|
|
});
|
2022-11-01 23:53:22 -04:00
|
|
|
if (center) {
|
|
|
|
|
this.center(false);
|
|
|
|
|
}
|
2025-12-30 22:29:30 -05:00
|
|
|
this._updateEdges();
|
2025-07-06 23:44:33 -04:00
|
|
|
if ((x || y || z) && this.api && this.api.event) {
|
|
|
|
|
this.api.event.emit('widget.rotate', {widget: this, x, y, z});
|
2022-11-04 12:46:06 -04:00
|
|
|
}
|
2022-02-18 12:08:08 -05:00
|
|
|
}
|
2021-04-27 21:53:21 -04:00
|
|
|
|
2022-02-18 12:08:08 -05:00
|
|
|
_rotate(x, y, z, temp) {
|
2020-05-06 14:04:43 -04:00
|
|
|
if (!temp) {
|
|
|
|
|
this.bounds = null;
|
2025-12-27 13:24:04 -05:00
|
|
|
if (this.setWireframe) this.setWireframe(false);
|
2020-05-06 14:04:43 -04:00
|
|
|
this.clearSlices();
|
|
|
|
|
}
|
2020-03-30 14:10:36 -04:00
|
|
|
let m4 = new THREE.Matrix4();
|
|
|
|
|
let euler = typeof(x) === 'number';
|
|
|
|
|
if (euler) {
|
|
|
|
|
m4 = m4.makeRotationFromEuler(new THREE.Euler(x || 0, y || 0, z || 0));
|
|
|
|
|
} else {
|
|
|
|
|
m4 = m4.makeRotationFromQuaternion(x);
|
|
|
|
|
}
|
2020-06-14 10:00:05 -04:00
|
|
|
this.roto.push(m4);
|
|
|
|
|
this.mesh.geometry.applyMatrix4(m4);
|
2020-05-06 14:04:43 -04:00
|
|
|
if (!temp && euler) {
|
2020-04-07 19:56:30 -04:00
|
|
|
let rot = this.track.rot;
|
2020-03-30 14:10:36 -04:00
|
|
|
rot.x += (x || 0);
|
|
|
|
|
rot.y += (y || 0);
|
|
|
|
|
rot.z += (z || 0);
|
|
|
|
|
}
|
2025-12-28 19:55:44 -05:00
|
|
|
this.setModified('_rotate');
|
2022-02-18 12:08:08 -05:00
|
|
|
}
|
2017-04-18 17:09:33 -04:00
|
|
|
|
2022-11-02 21:08:55 -04:00
|
|
|
// undo all accumulated rotations
|
2022-02-18 12:08:08 -05:00
|
|
|
unrotate() {
|
2020-06-14 10:00:05 -04:00
|
|
|
this.roto.reverse().forEach(m => {
|
2021-03-31 11:29:58 -04:00
|
|
|
this.mesh.geometry.applyMatrix4(m.clone().invert());
|
2020-06-14 10:00:05 -04:00
|
|
|
});
|
|
|
|
|
this.roto = [];
|
|
|
|
|
this.center();
|
2025-12-28 19:55:44 -05:00
|
|
|
this.setModified('unrotate');
|
2025-12-27 13:24:04 -05:00
|
|
|
if (this.refreshVisualState) this.refreshVisualState();
|
2022-02-18 12:08:08 -05:00
|
|
|
}
|
2020-06-13 22:52:46 -04:00
|
|
|
|
2022-02-18 12:08:08 -05:00
|
|
|
mirror() {
|
2020-04-07 19:56:30 -04:00
|
|
|
this.group.forEach(w => {
|
|
|
|
|
w._mirror();
|
|
|
|
|
});
|
|
|
|
|
this.center();
|
2025-07-06 23:44:33 -04:00
|
|
|
if (this.api && this.api.event) {
|
|
|
|
|
this.api.event.emit('widget.mirror', {widget: this});
|
2022-11-04 12:46:06 -04:00
|
|
|
}
|
2022-02-18 12:08:08 -05:00
|
|
|
}
|
2020-04-07 19:56:30 -04:00
|
|
|
|
2022-02-18 12:08:08 -05:00
|
|
|
_mirror() {
|
2017-04-18 17:09:33 -04:00
|
|
|
this.clearSlices();
|
2025-12-27 13:24:04 -05:00
|
|
|
if (this.setWireframe) this.setWireframe(false);
|
2021-03-02 22:49:01 -05:00
|
|
|
let geo = this.mesh.geometry, ot = this.track;
|
|
|
|
|
let pos = geo.attributes.position;
|
|
|
|
|
let arr = pos.array;
|
|
|
|
|
let count = pos.count;
|
|
|
|
|
// invert x
|
|
|
|
|
for (let i=0; i<count; i++) {
|
|
|
|
|
arr[i*3] = -arr[i*3];
|
2021-02-14 16:21:51 -05:00
|
|
|
}
|
2022-02-18 12:08:08 -05:00
|
|
|
// invert face vertex order
|
2021-03-02 22:49:01 -05:00
|
|
|
for (let i=0; i<count; i+=3) {
|
|
|
|
|
let x = arr[i*3+0];
|
|
|
|
|
let y = arr[i*3+1];
|
|
|
|
|
let z = arr[i*3+2];
|
|
|
|
|
arr[i*3+0] = arr[i*3+6];
|
|
|
|
|
arr[i*3+1] = arr[i*3+7];
|
|
|
|
|
arr[i*3+2] = arr[i*3+8];
|
|
|
|
|
arr[i*3+6] = x;
|
|
|
|
|
arr[i*3+7] = y;
|
|
|
|
|
arr[i*3+8] = z;
|
|
|
|
|
}
|
|
|
|
|
pos.needsUpdate = true;
|
|
|
|
|
ot.mirror = !ot.mirror;
|
2025-12-28 19:55:44 -05:00
|
|
|
this.setModified('_mirror');
|
2021-03-02 22:49:01 -05:00
|
|
|
this.points = null;
|
2022-02-18 12:08:08 -05:00
|
|
|
}
|
2017-04-18 17:09:33 -04:00
|
|
|
|
2023-02-14 11:03:06 -05:00
|
|
|
getTabVertices() {
|
|
|
|
|
const vert = [];
|
|
|
|
|
if (this.anno && this.anno.tab) {
|
|
|
|
|
for (let tab of this.anno.tab) {
|
|
|
|
|
const { pos, dim, rot } = tab;
|
|
|
|
|
const box = new THREE.BoxGeometry(dim.x, dim.y, dim.z).toNonIndexed();
|
2025-02-16 12:31:45 -05:00
|
|
|
const quat = new THREE.Quaternion(rot.x, rot.y, rot.z, rot.w);
|
2023-02-14 11:03:06 -05:00
|
|
|
box.applyMatrix4(
|
2025-02-16 12:31:45 -05:00
|
|
|
new THREE.Matrix4().makeRotationFromQuaternion(quat)
|
2023-02-14 11:03:06 -05:00
|
|
|
);
|
|
|
|
|
box.translate(pos.x, pos.y, pos.z);
|
|
|
|
|
vert.appendAll(box.attributes.position.array);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return vert;
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-09 14:55:17 -05:00
|
|
|
getGeoVertices(opt = {}) {
|
|
|
|
|
const { unroll, translate } = opt;
|
2025-12-28 19:55:44 -05:00
|
|
|
let cacheKey = [
|
|
|
|
|
unroll ? 1 : 0,
|
|
|
|
|
translate ? 1 : 0,
|
|
|
|
|
((this.track.indexRad ?? 0) * 100000) | 0
|
|
|
|
|
].join(',');
|
|
|
|
|
let marked = Date.now();
|
|
|
|
|
let geoCache = this.cache.geo = (this.cache.geo || {});
|
|
|
|
|
let cached = geoCache[cacheKey];
|
|
|
|
|
if (cached) {
|
|
|
|
|
return cached.pos;
|
|
|
|
|
}
|
2021-03-01 22:41:29 -05:00
|
|
|
let geo = this.mesh.geometry;
|
2022-11-01 19:55:30 -04:00
|
|
|
let pos = geo.getAttribute('position');
|
|
|
|
|
// if indexed, return points rotated about X and then offset
|
|
|
|
|
let track = this.track;
|
2022-11-02 10:43:58 -04:00
|
|
|
if (translate && track.indexed) {
|
2022-11-03 10:44:58 -04:00
|
|
|
pos = pos.clone()
|
|
|
|
|
.applyMatrix4( new THREE.Matrix4().makeRotationX(-track.indexRad) );
|
2022-11-01 19:55:30 -04:00
|
|
|
}
|
|
|
|
|
pos = pos.array;
|
2025-12-28 19:55:44 -05:00
|
|
|
// unroll indexed geometry
|
2021-09-20 23:40:49 -04:00
|
|
|
if (geo.index && unroll !== false) {
|
2021-03-01 22:41:29 -05:00
|
|
|
let idx = geo.index.array;
|
|
|
|
|
let len = idx.length;
|
2022-11-01 19:55:30 -04:00
|
|
|
let pp2 = new Float32Array(len * 3);
|
2021-03-01 22:41:29 -05:00
|
|
|
let inc = 0;
|
|
|
|
|
for (let i=0; i<len; i++) {
|
|
|
|
|
let iv = idx[i];
|
|
|
|
|
let ip = iv * 3;
|
2022-11-01 19:55:30 -04:00
|
|
|
pp2[inc++] = pos[ip++];
|
|
|
|
|
pp2[inc++] = pos[ip++];
|
|
|
|
|
pp2[inc++] = pos[ip];
|
2021-03-01 22:41:29 -05:00
|
|
|
}
|
2022-11-01 19:55:30 -04:00
|
|
|
pos = pp2;
|
2021-03-01 22:41:29 -05:00
|
|
|
}
|
2025-12-28 19:55:44 -05:00
|
|
|
geoCache[cacheKey] = { pos, marked };
|
2022-11-01 19:55:30 -04:00
|
|
|
return pos;
|
2022-02-18 12:08:08 -05:00
|
|
|
}
|
2021-09-20 21:43:06 -04:00
|
|
|
|
2022-02-18 12:08:08 -05:00
|
|
|
getPoints() {
|
2017-04-18 17:09:33 -04:00
|
|
|
if (!this.points) {
|
|
|
|
|
// convert and cache points from geometry vertices
|
2022-02-18 11:30:00 -05:00
|
|
|
this.points = verticesToPoints(this.getGeoVertices(), {
|
2020-11-06 10:47:49 -05:00
|
|
|
maxpass: 0 // disable decimation
|
|
|
|
|
});
|
2017-04-18 17:09:33 -04:00
|
|
|
}
|
|
|
|
|
return this.points;
|
2022-02-18 12:08:08 -05:00
|
|
|
}
|
2017-04-18 17:09:33 -04:00
|
|
|
|
2022-02-18 12:08:08 -05:00
|
|
|
getBoundingBox(refresh) {
|
2025-04-28 19:51:50 -06:00
|
|
|
if (!this.bounds || refresh || this.boundingBoxNeedsUpdate) {
|
2025-05-26 17:33:12 -06:00
|
|
|
this.bounds = new THREE.Box3().setFromArray(this.getGeoVertices({
|
|
|
|
|
translate: true,
|
2025-12-28 19:55:44 -05:00
|
|
|
unroll: true
|
2025-05-26 17:33:12 -06:00
|
|
|
}));
|
2025-04-28 19:51:50 -06:00
|
|
|
this.boundingBoxNeedsUpdate = false;
|
2017-04-18 17:09:33 -04:00
|
|
|
}
|
|
|
|
|
return this.bounds;
|
2022-02-18 12:08:08 -05:00
|
|
|
}
|
2017-04-18 17:09:33 -04:00
|
|
|
|
2022-02-18 12:08:08 -05:00
|
|
|
getPositionBox() {
|
2021-09-27 11:28:07 -04:00
|
|
|
let bounds = this.getBoundingBox().clone();
|
|
|
|
|
let pos = this.track.pos;
|
|
|
|
|
bounds.min.x += pos.x;
|
|
|
|
|
bounds.max.x += pos.x;
|
|
|
|
|
bounds.min.y += pos.y;
|
|
|
|
|
bounds.max.y += pos.y;
|
|
|
|
|
return bounds;
|
2022-02-18 12:08:08 -05:00
|
|
|
}
|
2021-09-27 11:28:07 -04:00
|
|
|
|
2022-02-18 12:08:08 -05:00
|
|
|
getExtruder(settings) {
|
2021-10-18 23:36:19 -04:00
|
|
|
if (settings) {
|
|
|
|
|
console.trace('legacy call with settings');
|
2020-04-10 23:17:22 -04:00
|
|
|
}
|
2021-10-18 23:36:19 -04:00
|
|
|
return this.anno.extruder || 0;
|
2022-02-18 12:08:08 -05:00
|
|
|
}
|
2020-04-10 23:17:22 -04:00
|
|
|
|
2021-05-05 19:11:05 -04:00
|
|
|
// allow worker code to run in same memspace as client
|
2022-02-18 12:08:08 -05:00
|
|
|
setInWorker() {
|
2021-05-05 19:11:05 -04:00
|
|
|
this.inWorker = true;
|
|
|
|
|
return this;
|
2022-02-18 12:08:08 -05:00
|
|
|
}
|
2021-05-05 19:11:05 -04:00
|
|
|
|
2017-04-18 17:09:33 -04:00
|
|
|
/**
|
2020-11-08 10:55:36 -05:00
|
|
|
* render to provided stack
|
2017-04-18 17:09:33 -04:00
|
|
|
*/
|
2022-02-18 12:08:08 -05:00
|
|
|
render(stack) {
|
2020-11-07 19:57:11 -05:00
|
|
|
const mark = Date.now();
|
2025-07-06 23:44:33 -04:00
|
|
|
for (let slice of this.slices || []) {
|
2020-11-30 22:36:06 -05:00
|
|
|
if (slice.layers) {
|
|
|
|
|
stack.add(slice.layers);
|
|
|
|
|
}
|
2025-07-06 23:44:33 -04:00
|
|
|
}
|
2020-11-08 10:55:36 -05:00
|
|
|
return Date.now() - mark;
|
2022-02-18 12:08:08 -05:00
|
|
|
}
|
2017-04-18 17:09:33 -04:00
|
|
|
|
|
|
|
|
|
2022-02-18 12:08:08 -05:00
|
|
|
show() {
|
2020-10-30 17:36:27 -04:00
|
|
|
this.mesh.visible = true;
|
2022-02-18 12:08:08 -05:00
|
|
|
}
|
2020-10-30 17:36:27 -04:00
|
|
|
|
2022-02-18 12:08:08 -05:00
|
|
|
hide() {
|
2020-10-30 17:36:27 -04:00
|
|
|
this.mesh.visible = false;
|
2022-02-18 12:08:08 -05:00
|
|
|
}
|
2025-12-06 00:09:17 -05:00
|
|
|
|
2025-12-09 15:40:09 -05:00
|
|
|
clearShadows() {
|
|
|
|
|
delete this.cache.shadow;
|
|
|
|
|
delete this.cache.shadows;
|
|
|
|
|
return this;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-06 14:06:12 -05:00
|
|
|
/**
|
|
|
|
|
* @param {number} z height for shadow computation
|
|
|
|
|
* @param {number | undefined} pocket normal value to match faces
|
|
|
|
|
* @returns {Polygon[]}
|
|
|
|
|
*/
|
2026-01-06 00:00:29 -05:00
|
|
|
async shadowAt(z, pocket) {
|
2025-12-06 00:09:17 -05:00
|
|
|
let shadows = this.cache.shadows;
|
|
|
|
|
if (!shadows) {
|
|
|
|
|
shadows = this.cache.shadows = {};
|
|
|
|
|
}
|
|
|
|
|
let cached = shadows[z];
|
|
|
|
|
if (cached) {
|
|
|
|
|
return cached;
|
|
|
|
|
}
|
|
|
|
|
// find closest shadow above and use to speed up delta shadow gen
|
|
|
|
|
let zover = Object.keys(shadows).map(v => parseFloat(v)).filter(v => v > z);
|
|
|
|
|
let minZabove = Math.min(Infinity, ...zover);
|
2025-12-28 19:55:44 -05:00
|
|
|
// shift shadow probeline down a fraction to capture z flats with FP noise
|
2026-01-06 00:00:29 -05:00
|
|
|
let shadow = this.#computeShadowAt(z - 0.005, minZabove, undefined, pocket);
|
2025-12-06 00:09:17 -05:00
|
|
|
if (minZabove < Infinity) {
|
2025-12-27 19:11:55 -05:00
|
|
|
shadow = POLY.union([...shadow, ...shadows[minZabove]], 0, true, { wasm: false });
|
|
|
|
|
// cull interior sliver voids
|
2026-01-06 00:00:29 -05:00
|
|
|
if (!pocket)
|
2025-12-27 19:11:55 -05:00
|
|
|
for (let poly of shadow) {
|
|
|
|
|
if (poly.inner) {
|
|
|
|
|
poly.inner = poly.inner.filter(inr => {
|
|
|
|
|
let A = inr.area();
|
|
|
|
|
let P = inr.perimeter();
|
|
|
|
|
let R = (2 * A / P);
|
|
|
|
|
return R > 0.05;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-12-06 00:09:17 -05:00
|
|
|
}
|
|
|
|
|
return shadows[z] = POLY.setZ(shadow, z);
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-15 21:06:19 -05:00
|
|
|
// create a stack of faces in 1mm increments
|
|
|
|
|
// the stacks are then used to produce shadowlines
|
2026-01-06 00:00:29 -05:00
|
|
|
#ensureShadowCache(pocket) {
|
2025-12-06 00:09:17 -05:00
|
|
|
if (this.cache.shadow) {
|
2026-01-03 19:43:51 -05:00
|
|
|
return this.cache.shadow;
|
2025-12-06 00:09:17 -05:00
|
|
|
}
|
2026-01-03 19:43:51 -05:00
|
|
|
if (debug_shadow) console.time('shadow buckets');
|
2025-12-28 19:55:44 -05:00
|
|
|
const geo = this.getGeoVertices({ unroll: true, translate: true });
|
2025-12-06 00:09:17 -05:00
|
|
|
const length = geo.length;
|
|
|
|
|
const bounds = this.getBoundingBox();
|
|
|
|
|
const stack = {};
|
|
|
|
|
for (let i = Math.floor(bounds.min.z); i <= Math.ceil(bounds.max.z); i++) {
|
|
|
|
|
stack[i] = [];
|
|
|
|
|
}
|
|
|
|
|
for (let i = 0, ip = 0; i < length; i += 3) {
|
|
|
|
|
const a = new THREE.Vector3(geo[ip++], geo[ip++], geo[ip++]);
|
|
|
|
|
const b = new THREE.Vector3(geo[ip++], geo[ip++], geo[ip++]);
|
|
|
|
|
const c = new THREE.Vector3(geo[ip++], geo[ip++], geo[ip++]);
|
|
|
|
|
const n = THREE.computeFaceNormal(a, b, c);
|
2026-01-06 14:06:12 -05:00
|
|
|
// todo: use pocket to match normal values when set
|
|
|
|
|
if ((pocket && n.z > -pocket) || (!pocket && n.z < 0.001)) {
|
2025-12-06 00:09:17 -05:00
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
const minZ = Math.floor(Math.min(a.z, b.z, c.z));
|
|
|
|
|
const maxZ = Math.ceil(Math.max(a.z, b.z, c.z));
|
|
|
|
|
for (let z = minZ; z <= maxZ; z++) {
|
|
|
|
|
stack[z].push(a, b, c);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-03 19:43:51 -05:00
|
|
|
if (debug_shadow) console.timeEnd('shadow buckets');
|
|
|
|
|
return this.cache.shadow = stack;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-12 19:14:08 -05:00
|
|
|
async computeShadowStack(zlist, progress, pocket, up = 1) {
|
2026-01-03 19:43:51 -05:00
|
|
|
let shadow_stack = this.cache.shadow_stack;
|
|
|
|
|
if (!shadow_stack) {
|
|
|
|
|
shadow_stack = this.cache.shadow_stack = {};
|
|
|
|
|
}
|
|
|
|
|
let work = self.kiri_worker;
|
2026-01-06 00:00:29 -05:00
|
|
|
let stack = this.#ensureShadowCache(pocket);
|
2026-01-03 19:43:51 -05:00
|
|
|
let plist = [];
|
|
|
|
|
if (debug_shadow) console.time('seed minion buckets');
|
|
|
|
|
work.minions.broadcast('cam_shadow_stack', stack);
|
|
|
|
|
if (debug_shadow) console.timeEnd('seed minion buckets');
|
|
|
|
|
let pinc = 1 / zlist.length;
|
|
|
|
|
let pval = 0;
|
|
|
|
|
for (let z of zlist) {
|
|
|
|
|
let p = work.minions.queueAsync({
|
|
|
|
|
cmd: 'cam_shadow_z',
|
|
|
|
|
z: z - 0.005,
|
2026-01-12 19:14:08 -05:00
|
|
|
t: z + up
|
2026-01-03 19:43:51 -05:00
|
|
|
}).then(reply => {
|
|
|
|
|
shadow_stack[z - 0.005] = decode(reply.data);
|
|
|
|
|
pval += pinc;
|
|
|
|
|
progress(pval);
|
|
|
|
|
});
|
|
|
|
|
plist.push(p);
|
|
|
|
|
}
|
|
|
|
|
await Promise.all(plist);
|
|
|
|
|
work.minions.broadcast('cam_shadow_stack', { clear: true });
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-06 00:00:29 -05:00
|
|
|
// called from minion
|
|
|
|
|
computeShadowAtZ(z, ztop, cached, pocket) {
|
|
|
|
|
return this.#computeShadowAt(z, ztop, cached, pocket);
|
2025-12-06 00:09:17 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// union triangles > z (opt cap < ztop) into polygon(s)
|
2025-12-15 21:06:19 -05:00
|
|
|
// slice the triangle stack matchingg z then union the results
|
2026-01-06 00:00:29 -05:00
|
|
|
#computeShadowAt(z, ztop, cached, pocket) {
|
2026-01-03 19:43:51 -05:00
|
|
|
let shadow_stack = this.cache.shadow_stack;
|
|
|
|
|
if (shadow_stack && shadow_stack[z]) {
|
|
|
|
|
return shadow_stack[z];
|
|
|
|
|
}
|
|
|
|
|
let label = `compute shadow ${z.round(2)}`;
|
|
|
|
|
if (debug_shadow) console.time(label);
|
2025-12-06 00:09:17 -05:00
|
|
|
const found = [];
|
2026-01-06 00:00:29 -05:00
|
|
|
const stack = cached ?? this.#ensureShadowCache(pocket);
|
2025-12-06 00:09:17 -05:00
|
|
|
let minZ = Math.floor(z);
|
|
|
|
|
let maxZ = Math.ceil(z);
|
|
|
|
|
let slices = [];
|
|
|
|
|
for (let sz = minZ; sz <= maxZ; sz++) {
|
|
|
|
|
slices.push(stack[sz] ?? []);
|
|
|
|
|
}
|
|
|
|
|
for (let faces of slices)
|
|
|
|
|
for (let i = 0; i < faces.length; ) {
|
|
|
|
|
const a = faces[i++];
|
|
|
|
|
const b = faces[i++];
|
|
|
|
|
const c = faces[i++];
|
|
|
|
|
if (ztop && a.z > ztop && b.z > ztop && c.z > ztop) {
|
|
|
|
|
// skip faces over top threshold
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if (a.z < z && b.z < z && c.z < z) {
|
|
|
|
|
// skip faces under threshold
|
|
|
|
|
continue;
|
2025-12-09 18:31:41 -05:00
|
|
|
// } else if (a.z === z && a.z === b.z && a.z === c.z) {
|
2025-12-08 17:35:21 -05:00
|
|
|
// skip faces coplanar with z (shadow looks up)
|
2025-12-06 00:09:17 -05:00
|
|
|
} else if (a.z >= z && b.z >= z && c.z >= z) {
|
|
|
|
|
found.push([a, b, c]);
|
|
|
|
|
} else {
|
|
|
|
|
// check faces straddling threshold
|
|
|
|
|
const where = { under: [], over: [], on: [] };
|
|
|
|
|
checkOverUnderOn(newPoint(a.x, a.y, a.z), z, where);
|
|
|
|
|
checkOverUnderOn(newPoint(b.x, b.y, b.z), z, where);
|
|
|
|
|
checkOverUnderOn(newPoint(c.x, c.y, c.z), z, where);
|
|
|
|
|
if (where.on.length === 0 && (where.over.length === 2 || where.under.length === 2)) {
|
|
|
|
|
// compute two point intersections and construct line
|
|
|
|
|
let line = intersectPoints(where.over, where.under, z);
|
|
|
|
|
if (line.length === 2) {
|
|
|
|
|
if (where.over.length === 2) {
|
|
|
|
|
found.push([where.over[1], line[0], line[1]]);
|
|
|
|
|
found.push([where.over[0], where.over[1], line[0]]);
|
|
|
|
|
} else {
|
|
|
|
|
found.push([where.over[0], line[0], line[1]]);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
console.log({ msg: "invalid ips", line: line, where: where });
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-15 18:24:21 -05:00
|
|
|
// map found tris to polygons
|
2025-12-06 00:09:17 -05:00
|
|
|
let polys = found.map(a => {
|
|
|
|
|
return newPolygon()
|
|
|
|
|
.add(a[0].x, a[0].y, a[0].z)
|
|
|
|
|
.add(a[1].x, a[1].y, a[1].z)
|
|
|
|
|
.add(a[2].x, a[2].y, a[2].z);
|
|
|
|
|
});
|
2025-12-15 15:29:08 -05:00
|
|
|
|
2025-12-15 18:24:21 -05:00
|
|
|
// recursively merge grid constrained subsets of polygons
|
2025-12-23 21:35:30 -05:00
|
|
|
polys = POLY.unionFaces(polys);
|
2025-12-06 00:09:17 -05:00
|
|
|
|
2025-12-07 21:37:34 -05:00
|
|
|
// for a more perfect union, pump shadows to merge very close lines
|
|
|
|
|
// todo: create clipper only version that avoids round trip thru geo classes
|
|
|
|
|
polys = POLY.offset(polys, 0.01);
|
|
|
|
|
polys = POLY.offset(polys, -0.01);
|
|
|
|
|
|
2026-01-03 19:43:51 -05:00
|
|
|
if (debug_shadow) console.timeEnd(label);
|
2025-12-06 00:09:17 -05:00
|
|
|
return polys;
|
|
|
|
|
}
|
2022-02-18 12:08:08 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Widget Grouping API
|
|
|
|
|
const Group = Widget.Groups = {
|
|
|
|
|
list() {
|
|
|
|
|
return groups.slice()
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
merge(widgets) {
|
|
|
|
|
let grps = widgets.map(w => w.group).uniq();
|
|
|
|
|
if (grps.length > 1) {
|
|
|
|
|
let root = grps.shift();
|
|
|
|
|
let rpos = root[0].track.pos;
|
|
|
|
|
for (let grp of grps) {
|
|
|
|
|
for (let w of grp) {
|
|
|
|
|
let wpos = w.track.pos;
|
|
|
|
|
w.group = root;
|
|
|
|
|
w.moveMesh(rpos.x - wpos.x, rpos.y - wpos.y, rpos.z - wpos.z);
|
|
|
|
|
w._move(rpos.x, rpos.y, rpos.z, true);
|
|
|
|
|
root.push(w);
|
|
|
|
|
}
|
|
|
|
|
groups.splice(groups.indexOf(grp),1);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
split(widgets) {
|
|
|
|
|
for (let group of widgets.map(w => w.group).uniq()) {
|
|
|
|
|
groups.splice(groups.indexOf(group),1);
|
|
|
|
|
for (let widget of group) {
|
|
|
|
|
let nugroup = Group.forid(widget.id);
|
|
|
|
|
nugroup.push(widget);
|
|
|
|
|
widget.group = nugroup;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
forid(id) {
|
|
|
|
|
for (let i=0; i<groups.length; i++) {
|
|
|
|
|
if (groups[i].id === id) return groups[i];
|
|
|
|
|
}
|
|
|
|
|
let group = [];
|
|
|
|
|
group.id = id;
|
|
|
|
|
groups.push(group);
|
|
|
|
|
return group;
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
remove(widget) {
|
|
|
|
|
groups.slice().forEach(group => {
|
|
|
|
|
let pos = group.indexOf(widget);
|
|
|
|
|
if (pos >= 0) {
|
|
|
|
|
group.splice(pos,1);
|
|
|
|
|
}
|
|
|
|
|
if (group.length === 0) {
|
|
|
|
|
pos = groups.indexOf(group);
|
|
|
|
|
groups.splice(pos,1);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
blocks() {
|
|
|
|
|
return groups.map(group => {
|
|
|
|
|
return {
|
|
|
|
|
w: group[0].track.box.w,
|
|
|
|
|
h: group[0].track.box.h,
|
|
|
|
|
move: (x,y,z,abs) => {
|
|
|
|
|
group.forEach(widget => {
|
2022-04-03 14:56:58 -04:00
|
|
|
widget.getMaterial().visible = true;
|
2022-02-18 12:08:08 -05:00
|
|
|
widget._move(x, y, z, abs);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
loadDone() {
|
|
|
|
|
groups.forEach(group => {
|
|
|
|
|
if (!group.centered) {
|
|
|
|
|
group[0].center();
|
|
|
|
|
group.centered = true;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
bounds(group) {
|
|
|
|
|
let bounds = null;
|
|
|
|
|
group.forEach(widget => {
|
|
|
|
|
let wb = widget.mesh.getBoundingBox(true);
|
|
|
|
|
if (bounds) {
|
|
|
|
|
bounds = bounds.union(wb);
|
|
|
|
|
} else {
|
|
|
|
|
bounds = wb;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
return bounds;
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
clear() {
|
|
|
|
|
groups.length = 0;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-12-27 13:24:04 -05:00
|
|
|
function newWidget(id,group) {
|
|
|
|
|
return new Widget(id,group);
|
|
|
|
|
}
|
2022-02-18 12:08:08 -05:00
|
|
|
|
2025-07-06 23:44:33 -04:00
|
|
|
export { Widget, newWidget };
|