applyTransforms
-
Hello,
In the documentation here, it says:
(static) transform(matrix, geometry) → {geom3}
Source:
modeling/src/geometries/geom3/transform.js, line 15
Transform the given geometry using the given matrix. This is a lazy transform of the polygons, as this function only adjusts the transforms. See applyTransforms() for the actual application of the transforms to the polygons.I can't find any other reference to applyTransforms. When I add objects to a union, it doesn't seem to apply the transforms. If I return an array of objects from my main function, it transforms them before rendering. That's fine I guess, but shouldn't union() work? And is there an applyTransforms() somewhere?
-
@z3dev if there is anything I can do to help, I'm willing.
I'm happy to share my code as well.
Here is PosableGeom3.js:
"use strict"; const jscad = require('@jscad/modeling'); const { Pose } = require('./Pose'); const { mat4, vec3, vec4 } = jscad.maths; const { geom3 } = jscad.geometries; const { PI } = Math; // Base class for geom3 objects with named Poses class PosableGeom3 { constructor(geometry, poses) { this._geometry = geometry || geom3.create(); this._poses = {}; if (poses) { Object.keys(poses).forEach(key => { this._poses[key] = poses[key].clone(); }); } } get polygons() { return this._geometry.polygons; } set polygons() { this._geometry.polygons = value; } get transforms() { return this._geometry.transforms; } set transforms(value) { this._geometry.transforms = value; } clone() { // Create a new geom3 with deep-copied polygons and transforms const clonedGeom = geom3.clone(this._geometry); // Clone the poses const clonedPoses = {}; Object.keys(this._poses).forEach(key => { clonedPoses[key] = this._poses[key].clone(); }); // Create a new PosableGeom3 with the cloned geometry and poses return new PosableGeom3(clonedGeom, clonedPoses); } transform(matrix) { this._geometry = geom3.transform(matrix, this._geometry); Object.keys(this._poses).forEach(key => { this._poses[key].transform(matrix); }); return this; } getPose(name) { return this._poses[name] || new Pose(); } applyTransforms(){ this._geometry = geom3.create(geom3.toPolygons(this._geometry)); return this; } alignTo(port, targetPose) { const sourcePose = this.getPose(port); if (!sourcePose) { throw new Error(`Invalid port ${port}`); } if (!targetPose) { throw new Error(`Invalid targetPose`); } return this.transform( sourcePose.getMatrix(targetPose) ); } } module.exports = { PosableGeom3 };
And here is Pose.js:
"use strict"; const jscad = require('@jscad/modeling'); const { vec3, vec4, mat4 } = jscad.maths; const { geom3 } = jscad.geometries; const { abs } = Math; const { translate } = jscad.transforms; const { union } = jscad.booleans; const { cylinder, sphere, cylinderElliptic } = jscad.primitives; const x = 0; const y = 1; const z = 2; const w = 3; class Pose { constructor(point, heading, up) { // Default position to origin if not provided this._point = point ? vec3.clone(point) : vec3.create(); // Default heading to Y+ (0, 1, 0) if not provided this._heading = vec3.normalize( vec3.create(), heading ? vec3.clone(heading) : [0, 1, 0] ); // Default up to Z+ (0, 0, 1) if not provided this._up = up ? vec3.clone(up) : [0, 0, 1]; // Project up onto the plane perpendicular to heading const dot = vec3.dot(this._up, this._heading); const projectedUp = vec3.subtract( vec3.create(), this._up, vec3.scale(vec3.create(), this._heading, dot) ); // Check if up is parallel to heading // (length of projectedUp near zero) if (vec3.length(projectedUp) < 0.00001) { // Choose a perpendicular vector based on heading if (abs(this._heading[z]) < 0.99999) { this._up = vec3.cross( vec3.create(), this._heading, [0, 0, 1] ); } else { this._up = vec3.cross( vec3.create(), this._heading, [1, 0, 0] ); } } else { this._up = vec3.normalize(vec3.create(), projectedUp); } // Ensure up is normalized vec3.normalize(this._up, this._up); } get point() { return vec3.clone(this._point); } get heading() { return vec3.clone(this._heading); } get up() { return vec3.clone(this._up); } clone() { return new Pose(this._point, this._heading, this._up); } transform(matrix) { // Transform point (w = 1) let v = this._point.concat(1); // Convert vec3 to vec4 v = vec4.transform(vec4.create(), v, matrix); this._point = vec3.clone(v); // Convert back to vec3 // Transform heading (w = 0) v = this._heading.concat(0); // Convert vec3 to vec4 v = vec4.transform(vec4.create(), v, matrix); this._heading = vec3.clone(v); // Convert back to vec3 // Transform up (w = 0) v = this._up.concat(0); // Convert vec3 to vec4 v = vec4.transform(vec4.create(), v, matrix); this._up = vec3.clone(v); // Convert back to vec3 return this; } getMatrix(targetPose) { let t = mat4.create(); // Step 1: Translate to origin t = mat4.multiply( mat4.create(), mat4.fromTranslation(mat4.create(), vec3.scale(vec3.create(), this._point, -1)), t ); // Step 2: Align heading with targetPose t = mat4.multiply( mat4.create(), mat4.fromVectorRotation(mat4.create(), this._heading, targetPose._heading), t ); // Step 3: Roll around heading to align up vector const p = this.clone().transform(t); const dot = vec3.dot( vec3.cross(vec3.create(), p._up, targetPose._up), p._heading ); let angle = vec3.angle(p._up, targetPose._up); if (dot < 0) angle = -angle; if (Math.abs(angle) > 1e-6) { t = mat4.multiply( mat4.create(), mat4.fromRotation(mat4.create(), angle, targetPose._heading), t ); } // Step 4: Translate to targetPose.point t = mat4.multiply( mat4.create(), mat4.fromTranslation(mat4.create(), targetPose._point), t ); return t; } render() { const vecGeom = (vector) => { const vectorLength = vec3.length(vector) || 1;; const vectorRadius = vectorLength / 10; const arrowLength = vectorLength / 5; const arrowRadius = vectorLength / 5; let out = union( cylinder({ // arrow body center: [0, 0, vectorLength / 2], height: vectorLength, radius: vectorRadius }), cylinderElliptic({ // arrow head center: [0, 0, vectorLength], startRadius: [arrowRadius, arrowRadius], endRadius: [0, 0], height: arrowLength }) ); out = geom3.transform( mat4.fromVectorRotation( mat4.create(), [0, 0, vectorLength], vector ), out ); return out; }; let g = translate( this._point, union( sphere({ radius: 0.2 }), vecGeom(this._heading), vecGeom(this._up) ) ); return g; } roll(angle) { if (angle === 0) return this; // Create a rotation matrix around the heading vector const rotationMatrix = mat4.create(); mat4.rotate( rotationMatrix, rotationMatrix, angle, this._heading ); // Transform the up vector using the rotation matrix (w = 0) const transformedUp = vec4.transform( vec4.create(), this._up.concat(0), rotationMatrix ); this._up = vec3.clone(transformedUp); return this; } translate(vector) { this._point = vec3.add( vec3.create(), vector, this._point ); } } module.exports = { Pose }; // end of file
-
-
@z3dev hello and thank you for the reply.
I am extending jscad with some custom classes Pose, PosableGeom3 which wraps geom3, and derivative what-not, that allows me to snap different pieces of a composition to each other based on Poses, which is an object consisting of two vectors and a point which represent a position and orientation. So for example I create tubes with different types of bends (curve, straight, U, dogleg) each having a start pose and an end pose. I do something like myGeom1. alignTo('start', myGeom2.getPose('end')); to snap one's start to another's end using a transform. I can then snap the next piece to the end of that one, etc. It's a heck of a lot easier to do this than calculate every object's individual 3D position, orientation, and rotation separately. It's very powerful and solves problems I was struggling with in OpenSCAD.
Anyway alignTo calls this.geometry = geom3.transform(matrix, this. geometry) but I was finding my geometry was rendering as if the transforms were never applied. Incidentally PosableGeom3 exposes the underlying geom3's polygons and transforms properties through getters as well as transforms through a getter and a setter so I think that implies a PosableGeom3 "Is_a" geom3. Certainly when I pass it to library functions expecting a geom3 it "just works". I'm not sure why that's not enough but I guess this is an unconventional usage of the library hence why I was needing to apply transforms at times other than what the library expects. Again, that's a guess. In any case I was able to work around it by adding an applyTransforms method to my class which contains this one line: this._geometry = geom3.create(geom3.toPolygons(this._geometry));
Thanks again for all your effort on this. It's been such a fun, useful, and powerful tool.
-
@sopatt Welcome
You don't have to worry about applying the transforms. That's done automatically before other operations like booleans, etc
You just have to position the shapes properly.
If you provide an example then we can help out. Also see some of the more complex examples.