abstract-3d 4.1.5 → 4.2.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.
- package/lib/abstract-3d.d.ts +63 -6
- package/lib/abstract-3d.d.ts.map +1 -1
- package/lib/abstract-3d.js +277 -74
- package/lib/abstract-3d.js.map +1 -1
- package/lib/renderers/dxf/dxf-encoding/dxf-dimension.d.ts +2 -2
- package/lib/renderers/dxf/dxf-encoding/dxf-dimension.d.ts.map +1 -1
- package/lib/renderers/dxf/dxf-encoding/dxf-dimension.js +6 -6
- package/lib/renderers/dxf/dxf-encoding/dxf-dimension.js.map +1 -1
- package/lib/renderers/dxf/dxf-geometries/dxf-dimension.d.ts +2 -2
- package/lib/renderers/dxf/dxf-geometries/dxf-dimension.d.ts.map +1 -1
- package/lib/renderers/dxf/dxf-geometries/dxf-dimension.js +3 -3
- package/lib/renderers/dxf/dxf-geometries/dxf-dimension.js.map +1 -1
- package/lib/renderers/dxf/dxf.d.ts.map +1 -1
- package/lib/renderers/dxf/dxf.js +12 -8
- package/lib/renderers/dxf/dxf.js.map +1 -1
- package/lib/renderers/ifc/ifc.d.ts.map +1 -1
- package/lib/renderers/ifc/ifc.js +5 -3
- package/lib/renderers/ifc/ifc.js.map +1 -1
- package/lib/renderers/step/step.d.ts.map +1 -1
- package/lib/renderers/step/step.js +3 -2
- package/lib/renderers/step/step.js.map +1 -1
- package/lib/renderers/stl/stl.d.ts.map +1 -1
- package/lib/renderers/stl/stl.js +7 -3
- package/lib/renderers/stl/stl.js.map +1 -1
- package/lib/renderers/svg/svg-geometries/svg-text.d.ts +1 -1
- package/lib/renderers/svg/svg-geometries/svg-text.d.ts.map +1 -1
- package/lib/renderers/svg/svg-geometries/svg-text.js +2 -2
- package/lib/renderers/svg/svg-geometries/svg-text.js.map +1 -1
- package/lib/renderers/svg/svg.d.ts.map +1 -1
- package/lib/renderers/svg/svg.js +13 -10
- package/lib/renderers/svg/svg.js.map +1 -1
- package/lib/renderers/svg-bsp/svg-geometries/svg-text.d.ts +1 -1
- package/lib/renderers/svg-bsp/svg-geometries/svg-text.d.ts.map +1 -1
- package/lib/renderers/svg-bsp/svg-geometries/svg-text.js +2 -2
- package/lib/renderers/svg-bsp/svg-geometries/svg-text.js.map +1 -1
- package/lib/renderers/svg-bsp/svg.d.ts.map +1 -1
- package/lib/renderers/svg-bsp/svg.js +13 -10
- package/lib/renderers/svg-bsp/svg.js.map +1 -1
- package/lib/renderers/three/build/build.js +1 -1
- package/lib/renderers/three/build/build.js.map +1 -1
- package/lib/renderers/three/compile/compile.d.ts +2 -0
- package/lib/renderers/three/compile/compile.d.ts.map +1 -1
- package/lib/renderers/three/compile/compile.js +6 -4
- package/lib/renderers/three/compile/compile.js.map +1 -1
- package/package.json +1 -1
- package/src/abstract-3d.test.ts +254 -0
- package/src/abstract-3d.ts +427 -85
- package/src/renderers/dxf/dxf-encoding/dxf-dimension.ts +22 -2
- package/src/renderers/dxf/dxf-geometries/dxf-dimension.ts +12 -2
- package/src/renderers/dxf/dxf.ts +21 -4
- package/src/renderers/ifc/ifc.ts +16 -3
- package/src/renderers/step/step.ts +3 -2
- package/src/renderers/stl/stl.ts +12 -4
- package/src/renderers/svg/svg-geometries/svg-text.ts +3 -2
- package/src/renderers/svg/svg.ts +29 -9
- package/src/renderers/svg-bsp/svg-geometries/svg-text.ts +3 -2
- package/src/renderers/svg-bsp/svg.ts +29 -9
- package/src/renderers/three/build/build.test.ts +2 -2
- package/src/renderers/three/build/build.ts +1 -1
- package/src/renderers/three/compile/compile.ts +22 -4
package/src/abstract-3d.ts
CHANGED
|
@@ -24,6 +24,26 @@ export type Dimensions = {
|
|
|
24
24
|
readonly material: Material;
|
|
25
25
|
};
|
|
26
26
|
|
|
27
|
+
/** Where a dimension's line and text are drawn, see `dimensionPlacements` */
|
|
28
|
+
export type DimensionPlacement = {
|
|
29
|
+
/** The dimension's `linePosition`, moved out where its line is too near the line inside it or the measured points for the texts between them */
|
|
30
|
+
readonly linePosition: Vec3;
|
|
31
|
+
readonly text: DimensionTextPlacement;
|
|
32
|
+
/**
|
|
33
|
+
* The lines at the dimension's ends: the marks across its dimension line and the extension lines from the measured points to them, broken around
|
|
34
|
+
* the texts they'd cross
|
|
35
|
+
*/
|
|
36
|
+
readonly endLines: ReadonlyArray<readonly [Vec3, Vec3]>;
|
|
37
|
+
/** Everything the dimension draws: its dimension line, the lines at its ends and its text */
|
|
38
|
+
readonly bounds: Bounds3;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The text a gap from the dimension line, out from its middle, out from the line or, `inward`, towards the measured points; a text of several rows
|
|
43
|
+
* has its first row over the line, as the text reads, and the rest under it
|
|
44
|
+
*/
|
|
45
|
+
export type DimensionTextPlacement = { readonly inward: boolean };
|
|
46
|
+
|
|
27
47
|
export type Dimension = DimensionMesh | DimensionAligned;
|
|
28
48
|
|
|
29
49
|
export type DimensionMesh = {
|
|
@@ -298,7 +318,19 @@ export type Text = {
|
|
|
298
318
|
readonly pos: Vec3;
|
|
299
319
|
readonly rot?: Vec3;
|
|
300
320
|
readonly text: string;
|
|
301
|
-
|
|
321
|
+
/** The scene's text height, see `sceneTextHeight`, if not set */
|
|
322
|
+
readonly fontSize?: number;
|
|
323
|
+
readonly background?: TextBackground;
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
/** A circle or a rectangle `depth` behind the text, half as thick, `padding` past it on every side, its edge an `outline` wide band */
|
|
327
|
+
export type TextBackground = {
|
|
328
|
+
readonly shape: "circle" | "rectangle";
|
|
329
|
+
readonly padding: number;
|
|
330
|
+
readonly outline: number;
|
|
331
|
+
readonly depth: number;
|
|
332
|
+
readonly material: Material;
|
|
333
|
+
readonly outlineMaterial: Material;
|
|
302
334
|
};
|
|
303
335
|
|
|
304
336
|
export type Box = {
|
|
@@ -630,14 +662,16 @@ export function bounds3FromVec3Array(vec3Array: ReadonlyArray<Vec3>): Bounds3 {
|
|
|
630
662
|
}
|
|
631
663
|
|
|
632
664
|
export function boundsScene(scene: Scene): Bounds3 {
|
|
665
|
+
const textHeight = sceneTextHeight(scene.groups);
|
|
633
666
|
const bounds: Array<Bounds3> = [];
|
|
634
667
|
for (const group of scene.groups) {
|
|
635
|
-
bounds.push(...boundsGroup(group, vec3Zero, vec3Zero));
|
|
668
|
+
bounds.push(...boundsGroup(group, vec3Zero, vec3Zero, textHeight));
|
|
636
669
|
}
|
|
637
670
|
return bounds3Merge(...bounds);
|
|
638
671
|
}
|
|
639
672
|
|
|
640
|
-
|
|
673
|
+
/** The bounds of the group's meshes, its texts without a font size `textHeight` high */
|
|
674
|
+
export function boundsGroup(group: Group, parentPos: Vec3, parentRot: Vec3, textHeight: number = defaultDimensionTextHeight): ReadonlyArray<Bounds3> {
|
|
641
675
|
const pos = vec3TransRot(group.pos, parentPos, parentRot);
|
|
642
676
|
const rot = vec3RotCombine(parentRot, group.rot ?? vec3Zero);
|
|
643
677
|
const bounds: Array<Bounds3> = [];
|
|
@@ -665,7 +699,7 @@ export function boundsGroup(group: Group, parentPos: Vec3, parentRot: Vec3): Rea
|
|
|
665
699
|
break;
|
|
666
700
|
}
|
|
667
701
|
case "Text": {
|
|
668
|
-
bounds.push(boundsText(childMesh.geometry, pos, rot));
|
|
702
|
+
bounds.push(boundsText(childMesh.geometry, pos, rot, textHeight));
|
|
669
703
|
break;
|
|
670
704
|
}
|
|
671
705
|
default:
|
|
@@ -674,7 +708,7 @@ export function boundsGroup(group: Group, parentPos: Vec3, parentRot: Vec3): Rea
|
|
|
674
708
|
}
|
|
675
709
|
|
|
676
710
|
for (const childGroup of group.groups ?? []) {
|
|
677
|
-
bounds.push(...boundsGroup(childGroup, pos, rot));
|
|
711
|
+
bounds.push(...boundsGroup(childGroup, pos, rot, textHeight));
|
|
678
712
|
}
|
|
679
713
|
return bounds;
|
|
680
714
|
}
|
|
@@ -751,22 +785,83 @@ export function boundsCone(c: Cone, parentPos: Vec3, parentRot: Vec3): Bounds3 {
|
|
|
751
785
|
return bounds3FromVec3Array(points);
|
|
752
786
|
}
|
|
753
787
|
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
const
|
|
757
|
-
const
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
788
|
+
/** The bounds of the text and its background, `textHeight` high without a font size */
|
|
789
|
+
export function boundsText(t: Text, parentPos: Vec3, parentRot: Vec3, textHeight: number = defaultDimensionTextHeight): Bounds3 {
|
|
790
|
+
const size = textSize(t.text, t.fontSize ?? textHeight);
|
|
791
|
+
const padding = 2 * (t.background?.padding ?? 0);
|
|
792
|
+
return boundsPlane({ type: "Plane", pos: t.pos, size: vec2(size.x + padding, size.y + padding), rot: t.rot }, parentPos, parentRot);
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
/** How wide and high a text is drawn at the font size, its widest row by a rough character width and its rows a fifth of the font size apart */
|
|
796
|
+
export function textSize(text: string, fontSize: number): Vec2 {
|
|
797
|
+
const rows = text.split("\n");
|
|
798
|
+
return vec2(Math.max(...rows.map((v) => v.length)) * fontSize * (9 / 16), rows.length * fontSize * 1.2);
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
/** The longest side of the scene's objects that the text height 32 is for */
|
|
802
|
+
const textHeightBaseSize = 3600;
|
|
803
|
+
const textHeightBase = 32;
|
|
804
|
+
/** A step of the text height, half of ISO 3098's √2, so a scene just past a step has its text at most 19% larger than in proportion to its size */
|
|
805
|
+
const textHeightStep = 2 ** (1 / 4);
|
|
806
|
+
|
|
807
|
+
/**
|
|
808
|
+
* The text height for a scene whose objects take `size`, without its dimensions and texts: 32 up to the base size, else as many steps of 2^¼ above it
|
|
809
|
+
* as the longest side needs, rounded up, so a text is never smaller on the drawing than 32 on the base size
|
|
810
|
+
*/
|
|
811
|
+
export function textHeightForSize(size: Vec3): number {
|
|
812
|
+
const steps = Math.ceil(Math.log(Math.max(size.x, size.y, size.z) / textHeightBaseSize) / Math.log(textHeightStep) - 1e-9);
|
|
813
|
+
return textHeightBase * textHeightStep ** Math.max(0, steps);
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
/**
|
|
817
|
+
* The text height of a scene of the groups, its texts without a font size and its dimensions': `textHeightForSize` of its objects' size, without its
|
|
818
|
+
* texts, which the text height would size
|
|
819
|
+
*/
|
|
820
|
+
export function sceneTextHeight(groups: ReadonlyArray<Group>): number {
|
|
821
|
+
const withoutTexts = (g: Group): Group => ({
|
|
822
|
+
...g,
|
|
823
|
+
meshes: g.meshes?.filter((m) => m.geometry.type !== "Text"),
|
|
824
|
+
groups: g.groups?.map(withoutTexts),
|
|
825
|
+
});
|
|
826
|
+
return textHeightForSize(bounds3ToSize(bounds3Merge(...groups.flatMap((g) => boundsGroup(withoutTexts(g), vec3Zero, vec3Zero)))));
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
/** The group with its texts' backgrounds as the meshes just behind each text, in a group after its own, sized by its font size or else `textHeight` */
|
|
830
|
+
export function groupTextBackgrounds(g: Group, textHeight: number): Group {
|
|
831
|
+
const backgrounds = (g.meshes ?? []).flatMap((m): ReadonlyArray<Group> => {
|
|
832
|
+
if (m.geometry.type !== "Text" || !m.geometry.background) {
|
|
833
|
+
return [];
|
|
834
|
+
}
|
|
835
|
+
const { padding, outline, depth, material, outlineMaterial, shape } = m.geometry.background;
|
|
836
|
+
const size = textSize(m.geometry.text, m.geometry.fontSize ?? textHeight);
|
|
837
|
+
const width = size.x + 2 * padding;
|
|
838
|
+
const height = size.y + 2 * padding;
|
|
839
|
+
const thickness = depth / 2;
|
|
840
|
+
const back = vec3(0, 0, -depth);
|
|
841
|
+
const radius = Math.max(width, height) / 2;
|
|
842
|
+
return [
|
|
843
|
+
group(
|
|
844
|
+
shape === "circle"
|
|
845
|
+
? [
|
|
846
|
+
cylinder(back, radius - outline, thickness, material, vec3(Math.PI / 2, 0, 0)),
|
|
847
|
+
cylinder(back, radius, thickness, outlineMaterial, vec3(Math.PI / 2, 0, 0), false, [
|
|
848
|
+
{ type: "RoundHole", radius: radius - outline, pos: vec2Zero },
|
|
849
|
+
]),
|
|
850
|
+
]
|
|
851
|
+
: [
|
|
852
|
+
plane(back, vec2(width - 2 * outline, height - 2 * outline), material),
|
|
853
|
+
box(vec3(width, outline, thickness), outlineMaterial, vec3Add(back, vec3(0, (height - outline) / 2, 0))),
|
|
854
|
+
box(vec3(width, outline, thickness), outlineMaterial, vec3Add(back, vec3(0, -(height - outline) / 2, 0))),
|
|
855
|
+
box(vec3(outline, height - 2 * outline, thickness), outlineMaterial, vec3Add(back, vec3((width - outline) / 2, 0, 0))),
|
|
856
|
+
box(vec3(outline, height - 2 * outline, thickness), outlineMaterial, vec3Add(back, vec3(-(width - outline) / 2, 0, 0))),
|
|
857
|
+
],
|
|
858
|
+
m.geometry.pos,
|
|
859
|
+
m.geometry.rot ?? vec3Zero
|
|
860
|
+
),
|
|
861
|
+
];
|
|
862
|
+
});
|
|
863
|
+
const groups = (g.groups ?? []).map((c) => groupTextBackgrounds(c, textHeight));
|
|
864
|
+
return backgrounds.length === 0 && g.groups === undefined ? g : { ...g, groups: [...groups, ...backgrounds] };
|
|
770
865
|
}
|
|
771
866
|
|
|
772
867
|
// -- Transformations
|
|
@@ -1099,8 +1194,15 @@ export const culledLine = (start: Vec3, end: Vec3, normal: Vec3, thickness: numb
|
|
|
1099
1194
|
material,
|
|
1100
1195
|
});
|
|
1101
1196
|
|
|
1102
|
-
export const text = (
|
|
1103
|
-
|
|
1197
|
+
export const text = (
|
|
1198
|
+
pos: Vec3,
|
|
1199
|
+
text: string,
|
|
1200
|
+
fontSize: number | undefined,
|
|
1201
|
+
material: Material,
|
|
1202
|
+
rot = vec3Zero,
|
|
1203
|
+
background?: TextBackground
|
|
1204
|
+
): TextMesh => ({
|
|
1205
|
+
geometry: { type: "Text", pos, text, fontSize, rot, background },
|
|
1104
1206
|
material,
|
|
1105
1207
|
});
|
|
1106
1208
|
|
|
@@ -1143,6 +1245,236 @@ export function dimensionIsOfTypeMesh(dimension: Dimension): dimension is Dimens
|
|
|
1143
1245
|
return "meshes" in dimension;
|
|
1144
1246
|
}
|
|
1145
1247
|
|
|
1248
|
+
const defaultDimensionTextHeight = 44;
|
|
1249
|
+
// The sizes as shares of the text height
|
|
1250
|
+
const dimensionGap = 8 / defaultDimensionTextHeight;
|
|
1251
|
+
/** How far the marks at a dimension's ends go to each side of the dimension line, short of the text's digits a gap from it */
|
|
1252
|
+
const dimensionMark = 10 / defaultDimensionTextHeight;
|
|
1253
|
+
const dimensionArrowLength = 22.5 / defaultDimensionTextHeight;
|
|
1254
|
+
const dimensionArrowHalfBase = 5.625 / defaultDimensionTextHeight;
|
|
1255
|
+
/** How far apart the rows of a dimension's text are, as `textSize` measures them, each drawn as a text of its own so every renderer spaces them alike */
|
|
1256
|
+
const dimensionTextRowStep = 1.2;
|
|
1257
|
+
/** The lines' thickness, whatever the text height, as the three renderer draws it in pixels */
|
|
1258
|
+
const dimensionLineThickness = 0.5;
|
|
1259
|
+
|
|
1260
|
+
/** A dimension's measured length, its text and how wide the text is drawn, as `textSize` measures it */
|
|
1261
|
+
function dimensionMeasure(
|
|
1262
|
+
dimension: DimensionAligned,
|
|
1263
|
+
textHeight: number
|
|
1264
|
+
): { readonly length: number; readonly text: string; readonly rows: ReadonlyArray<string>; readonly textWidth: number } {
|
|
1265
|
+
const length = vec3Length(vec3Sub(dimension.measurementEnd, dimension.measurementStart));
|
|
1266
|
+
const text = dimension.text ?? length.toFixed(1).replace(/[.,]0$/, "");
|
|
1267
|
+
const rows = text.split("\n");
|
|
1268
|
+
return { length, text, rows, textWidth: textSize(text, textHeight).x };
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
type DimensionSpan = {
|
|
1272
|
+
readonly index: number;
|
|
1273
|
+
readonly view: View;
|
|
1274
|
+
/** The direction along the line, the same for the dimensions measured either way along it */
|
|
1275
|
+
readonly along: Vec3;
|
|
1276
|
+
/** Where the dimension line crosses the plane through the origin across it, the same for the dimensions on one line */
|
|
1277
|
+
readonly linePoint: Vec3;
|
|
1278
|
+
readonly linePosition: Vec3;
|
|
1279
|
+
readonly measurementStart: Vec3;
|
|
1280
|
+
readonly measurementEnd: Vec3;
|
|
1281
|
+
/** The direction from the measured points out to the line */
|
|
1282
|
+
readonly outward: Vec3;
|
|
1283
|
+
/** How far out the line is */
|
|
1284
|
+
readonly row: number;
|
|
1285
|
+
/** How far out the outermost measured point is */
|
|
1286
|
+
readonly measured: number;
|
|
1287
|
+
readonly min: number;
|
|
1288
|
+
readonly max: number;
|
|
1289
|
+
readonly startIsMin: boolean;
|
|
1290
|
+
readonly textWidth: number;
|
|
1291
|
+
/** A text of several rows, on both sides of the line */
|
|
1292
|
+
readonly split: boolean;
|
|
1293
|
+
/** How far the text takes out from each side of the line it's on */
|
|
1294
|
+
readonly textBlockHeight: number;
|
|
1295
|
+
};
|
|
1296
|
+
|
|
1297
|
+
/**
|
|
1298
|
+
* Where each dimension's line and text are drawn, decided for the dimensions on the same line together, undefined for a mesh dimension:
|
|
1299
|
+
*
|
|
1300
|
+
* - The text a gap out from the line, out from the dimension's middle, wider than the dimension where nothing's in the way
|
|
1301
|
+
* - Else on the other side of the line, towards the measured points, alternating with its neighbours'
|
|
1302
|
+
* - Placed together for the dimensions on a line, whichever together are the clearest: texts out from the line, and apart
|
|
1303
|
+
* - Each line moved out where it's too near the measured points or the line inside it on the same side for the texts between them
|
|
1304
|
+
*/
|
|
1305
|
+
export function dimensionPlacements(
|
|
1306
|
+
dimensions: ReadonlyArray<Dimension>,
|
|
1307
|
+
textHeight: number = defaultDimensionTextHeight
|
|
1308
|
+
): ReadonlyArray<DimensionPlacement | undefined> {
|
|
1309
|
+
const tolerance = textHeight * 0.01;
|
|
1310
|
+
const gap = textHeight * dimensionGap;
|
|
1311
|
+
const lines = Array<Array<DimensionSpan>>();
|
|
1312
|
+
dimensions.forEach((d, index) => {
|
|
1313
|
+
if (dimensionIsOfTypeMesh(d)) {
|
|
1314
|
+
return;
|
|
1315
|
+
}
|
|
1316
|
+
const direction = vec3Normalize(vec3Sub(d.measurementEnd, d.measurementStart));
|
|
1317
|
+
const flip = Math.abs(direction.x) > 1e-6 ? direction.x < 0 : Math.abs(direction.y) > 1e-6 ? direction.y < 0 : direction.z < 0;
|
|
1318
|
+
const along = flip ? vec3Scale(direction, -1) : direction;
|
|
1319
|
+
const start = vec3Dot(d.measurementStart, along);
|
|
1320
|
+
const end = vec3Dot(d.measurementEnd, along);
|
|
1321
|
+
const linePoint = vec3Sub(d.linePosition, vec3Scale(along, vec3Dot(d.linePosition, along)));
|
|
1322
|
+
const outward = vec3Normalize(vec3Sub(linePoint, vec3Sub(d.measurementStart, vec3Scale(along, start))));
|
|
1323
|
+
const { textWidth, rows } = dimensionMeasure(d, textHeight);
|
|
1324
|
+
const span: DimensionSpan = {
|
|
1325
|
+
index,
|
|
1326
|
+
view: d.views?.[0] ?? "front",
|
|
1327
|
+
along,
|
|
1328
|
+
linePoint,
|
|
1329
|
+
linePosition: d.linePosition,
|
|
1330
|
+
measurementStart: d.measurementStart,
|
|
1331
|
+
measurementEnd: d.measurementEnd,
|
|
1332
|
+
outward,
|
|
1333
|
+
row: vec3Dot(d.linePosition, outward),
|
|
1334
|
+
measured: Math.max(vec3Dot(d.measurementStart, outward), vec3Dot(d.measurementEnd, outward)),
|
|
1335
|
+
min: Math.min(start, end),
|
|
1336
|
+
max: Math.max(start, end),
|
|
1337
|
+
startIsMin: start <= end,
|
|
1338
|
+
textWidth,
|
|
1339
|
+
split: rows.length > 1,
|
|
1340
|
+
// Its first row on one side, the rest on the other
|
|
1341
|
+
textBlockHeight: textHeight * (1 + dimensionTextRowStep * Math.max(0, rows.length - 2)),
|
|
1342
|
+
};
|
|
1343
|
+
const line = lines.find(
|
|
1344
|
+
([s]) => s && s.view === span.view && vec3Dot(s.along, along) > 1 - 1e-6 && vec3Length(vec3Sub(s.linePoint, span.linePoint)) < tolerance
|
|
1345
|
+
);
|
|
1346
|
+
if (line) {
|
|
1347
|
+
line.push(span);
|
|
1348
|
+
} else {
|
|
1349
|
+
lines.push([span]);
|
|
1350
|
+
}
|
|
1351
|
+
});
|
|
1352
|
+
|
|
1353
|
+
const sameSide = (a: DimensionSpan | undefined, b: DimensionSpan): boolean =>
|
|
1354
|
+
a !== undefined && a.view === b.view && vec3Dot(a.along, b.along) > 1 - 1e-6 && vec3Dot(a.outward, b.outward) > 1 - 1e-6;
|
|
1355
|
+
// The lines' texts, placed before the lines are moved out, as the room on each side of a line is left for its texts
|
|
1356
|
+
const decisions = lines.flatMap((line) => {
|
|
1357
|
+
const [first] = line;
|
|
1358
|
+
if (!first) {
|
|
1359
|
+
return [];
|
|
1360
|
+
}
|
|
1361
|
+
type Placed = { readonly s: DimensionSpan; readonly inward: boolean };
|
|
1362
|
+
// How far apart two texts out from their dimensions' middles are
|
|
1363
|
+
const apart = (l: Placed, o: Placed): number => Math.abs((l.s.min + l.s.max) / 2 - (o.s.min + o.s.max) / 2) - (l.s.textWidth + o.s.textWidth) / 2;
|
|
1364
|
+
const sorted = line.toSorted((a, b) => a.min - b.min);
|
|
1365
|
+
// What makes a text unclear, worst first: across its neighbour's, so they alternate even where every other one's still clash, across another text,
|
|
1366
|
+
// two texts on one side so close they read as one, and a text towards the measured points, more at an end of the line, with room past it
|
|
1367
|
+
const placeCost = (l: Placed, placed: ReadonlyArray<Placed>): number =>
|
|
1368
|
+
(l.inward ? (l.s === sorted[0] || l.s === sorted.at(-1) ? 5 : 4) : 0) +
|
|
1369
|
+
placed
|
|
1370
|
+
.filter((o) => o.inward === l.inward || o.s.split || l.s.split)
|
|
1371
|
+
.reduce(
|
|
1372
|
+
(sum, o) =>
|
|
1373
|
+
sum +
|
|
1374
|
+
(apart(l, o) < 2 * gap - tolerance
|
|
1375
|
+
? o === placed.at(-1)
|
|
1376
|
+
? 100000
|
|
1377
|
+
: 1000
|
|
1378
|
+
: apart(l, o) < textHeight / 2
|
|
1379
|
+
? 10 * (1 - (2 * apart(l, o)) / textHeight)
|
|
1380
|
+
: 0),
|
|
1381
|
+
0
|
|
1382
|
+
);
|
|
1383
|
+
const placed =
|
|
1384
|
+
sorted
|
|
1385
|
+
.reduce<ReadonlyArray<{ readonly placed: ReadonlyArray<Placed>; readonly cost: number }>>(
|
|
1386
|
+
(states, s) =>
|
|
1387
|
+
states
|
|
1388
|
+
.flatMap(({ placed, cost }) =>
|
|
1389
|
+
(s.split ? [false] : [false, true]).map((inward) => ({
|
|
1390
|
+
placed: [...placed, { s, inward }],
|
|
1391
|
+
cost: cost + placeCost({ s, inward }, placed),
|
|
1392
|
+
}))
|
|
1393
|
+
)
|
|
1394
|
+
.toSorted((a, b) => a.cost - b.cost)
|
|
1395
|
+
.slice(0, 64),
|
|
1396
|
+
[{ placed: [], cost: 0 }]
|
|
1397
|
+
)
|
|
1398
|
+
.at(0)?.placed ?? [];
|
|
1399
|
+
return [{ line, first, placed }];
|
|
1400
|
+
});
|
|
1401
|
+
|
|
1402
|
+
// How far a line's texts, or the marks at its ends, take on each side of it
|
|
1403
|
+
const mark = textHeight * dimensionMark;
|
|
1404
|
+
const reach = (placed: ReadonlyArray<{ readonly s: DimensionSpan; readonly inward: boolean }>, inward: boolean): number =>
|
|
1405
|
+
Math.max(mark, ...placed.filter((l) => l.inward === inward || l.s.split).map((l) => gap + l.s.textBlockHeight));
|
|
1406
|
+
// How far out each line is moved to: its texts towards the measured points a gap past them or past the texts out from the line inside it on the same
|
|
1407
|
+
// side
|
|
1408
|
+
const rows = new Map<DimensionSpan, number>();
|
|
1409
|
+
for (const { line, first, placed } of decisions.toSorted((a, b) => a.first.row - b.first.row)) {
|
|
1410
|
+
rows.set(
|
|
1411
|
+
first,
|
|
1412
|
+
Math.max(
|
|
1413
|
+
first.row,
|
|
1414
|
+
Math.max(...line.map((s) => s.measured)) + gap + reach(placed, true),
|
|
1415
|
+
...decisions
|
|
1416
|
+
.filter((d) => sameSide(d.first, first) && d.first.row < first.row - tolerance)
|
|
1417
|
+
.map((d) => (rows.get(d.first) ?? 0) + reach(d.placed, false) + gap + reach(placed, true))
|
|
1418
|
+
)
|
|
1419
|
+
);
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
// Every text's rectangle on each side it's on, along its line from its left to its right and out from its bottom to its top
|
|
1423
|
+
const texts = decisions.flatMap(({ first, placed }) =>
|
|
1424
|
+
placed.flatMap(({ s, inward }) =>
|
|
1425
|
+
(s.split ? [false, true] : [inward]).map((side) => {
|
|
1426
|
+
const row = rows.get(first) ?? first.row;
|
|
1427
|
+
const middle = (s.min + s.max) / 2;
|
|
1428
|
+
return {
|
|
1429
|
+
s,
|
|
1430
|
+
left: middle - s.textWidth / 2,
|
|
1431
|
+
right: middle + s.textWidth / 2,
|
|
1432
|
+
bottom: side ? row - gap - s.textBlockHeight : row + gap,
|
|
1433
|
+
top: side ? row - gap : row + gap + s.textBlockHeight,
|
|
1434
|
+
};
|
|
1435
|
+
})
|
|
1436
|
+
)
|
|
1437
|
+
);
|
|
1438
|
+
const placements = new Map<number, DimensionPlacement>();
|
|
1439
|
+
for (const { first, placed } of decisions) {
|
|
1440
|
+
const row = rows.get(first) ?? first.row;
|
|
1441
|
+
for (const { s, inward } of placed) {
|
|
1442
|
+
const linePoint = vec3Add(s.linePoint, vec3Scale(s.outward, row - s.row));
|
|
1443
|
+
const at = (t: number, u: number): Vec3 => vec3Add(linePoint, vec3Add(vec3Scale(s.along, t), vec3Scale(s.outward, u - row)));
|
|
1444
|
+
// At each end the mark across the line, and the extension line from the measured point to it, broken half a gap short of each text
|
|
1445
|
+
// it'd cross on the same side
|
|
1446
|
+
const endLines = [
|
|
1447
|
+
[s.min, s.startIsMin ? s.measurementStart : s.measurementEnd] as const,
|
|
1448
|
+
[s.max, s.startIsMin ? s.measurementEnd : s.measurementStart] as const,
|
|
1449
|
+
].flatMap(([t, point]) => {
|
|
1450
|
+
const extension = texts
|
|
1451
|
+
.filter((o) => sameSide(o.s, s) && t > o.left && t < o.right)
|
|
1452
|
+
.reduce<ReadonlyArray<readonly [number, number]>>(
|
|
1453
|
+
(pieces, o) =>
|
|
1454
|
+
pieces.flatMap(([from, to]) => [[from, Math.min(to, o.bottom - gap / 2)] as const, [Math.max(from, o.top + gap / 2), to] as const]),
|
|
1455
|
+
[[vec3Dot(point, s.outward), row - mark]]
|
|
1456
|
+
)
|
|
1457
|
+
.filter(([from, to]) => to - from >= gap);
|
|
1458
|
+
return [...extension, [row - mark, row + mark] as const].map(([from, to]) => [at(t, from), at(t, to)] as const);
|
|
1459
|
+
});
|
|
1460
|
+
// The rectangle the dimension line, the marks at its ends and its text take, and the extension lines
|
|
1461
|
+
const middle = (s.min + s.max) / 2;
|
|
1462
|
+
const corners = [Math.min(s.min, middle - s.textWidth / 2), Math.max(s.max, middle + s.textWidth / 2)].flatMap((t) =>
|
|
1463
|
+
[inward || s.split ? -gap - s.textBlockHeight : -mark, inward && !s.split ? mark : gap + s.textBlockHeight].map((u) => at(t, row + u))
|
|
1464
|
+
);
|
|
1465
|
+
const linePosition = vec3Add(s.linePosition, vec3Scale(s.outward, row - s.row));
|
|
1466
|
+
placements.set(s.index, {
|
|
1467
|
+
linePosition,
|
|
1468
|
+
text: { inward },
|
|
1469
|
+
endLines,
|
|
1470
|
+
bounds: bounds3FromVec3Array([...corners, ...endLines.flat()]),
|
|
1471
|
+
});
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
return dimensions.map((_, i) => placements.get(i));
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
/** Draws the dimension as `placement` places it, its text `textHeight` high; as if alone on its line if not given */
|
|
1146
1478
|
export function dimensionMeshifyAlignedDimension(
|
|
1147
1479
|
dimension: DimensionAligned,
|
|
1148
1480
|
sceneRotation: Vec3,
|
|
@@ -1150,25 +1482,23 @@ export function dimensionMeshifyAlignedDimension(
|
|
|
1150
1482
|
onCreateLine: (start: Vec3, end: Vec3, norm: Vec3, thickness: number, mat: Material) => void,
|
|
1151
1483
|
onCreateText: (pos: Vec3, measurement: string, fontSize: number, mat: Material, rot: Vec3, dir: Vec3, normal: Vec3) => void,
|
|
1152
1484
|
onCreatePolygon: (p1: Vec3, p2: Vec3, p3: Vec3, mat: Material) => void,
|
|
1153
|
-
dimensionsMaterial?: Material
|
|
1485
|
+
dimensionsMaterial?: Material,
|
|
1486
|
+
textHeight: number = defaultDimensionTextHeight,
|
|
1487
|
+
placement?: DimensionPlacement
|
|
1154
1488
|
): void {
|
|
1155
1489
|
const defaultMaterial = { normal: "rgb(0, 0, 0)", opacity: 1.0, roughness: 1.0, metalness: 0.0 };
|
|
1156
1490
|
const material = dimension.material ?? dimensionsMaterial ?? defaultMaterial;
|
|
1157
|
-
const
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
const
|
|
1162
|
-
const
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
const lp = dimension.linePosition;
|
|
1491
|
+
const place = placement ?? dimensionPlacements([dimension], textHeight)[0];
|
|
1492
|
+
if (!place) {
|
|
1493
|
+
return;
|
|
1494
|
+
}
|
|
1495
|
+
const gap = textHeight * dimensionGap;
|
|
1496
|
+
const lineThickness = dimensionLineThickness;
|
|
1497
|
+
|
|
1498
|
+
const lp = place.linePosition;
|
|
1166
1499
|
const ms = dimension.measurementStart;
|
|
1167
1500
|
const me = dimension.measurementEnd;
|
|
1168
|
-
const
|
|
1169
|
-
const measurement = dimension.text ?? measurementLength.toFixed(1).replace(/[.,]0$/, "");
|
|
1170
|
-
const measurementRows = measurement.split("\n");
|
|
1171
|
-
const textHeight = measurementRows.length * textSize;
|
|
1501
|
+
const { rows: measurementRows } = dimensionMeasure(dimension, textHeight);
|
|
1172
1502
|
|
|
1173
1503
|
const view = dimension.views?.[0] ?? "front";
|
|
1174
1504
|
const basis = dimensionViewBasises[view];
|
|
@@ -1181,17 +1511,32 @@ export function dimensionMeshifyAlignedDimension(
|
|
|
1181
1511
|
const offset = vec3Sub(lp, projected);
|
|
1182
1512
|
const ls = vec3Add(ms, offset);
|
|
1183
1513
|
const le = vec3Add(me, offset);
|
|
1184
|
-
const
|
|
1185
|
-
const dirBetweenLines = vec3Normalize(
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1514
|
+
const lc = vec3Scale(vec3Add(ls, le), 0.5);
|
|
1515
|
+
const dirBetweenLines = vec3Normalize(offset);
|
|
1516
|
+
|
|
1517
|
+
// The lines at its ends and the dimension line
|
|
1518
|
+
place.endLines.forEach(([start, end]) => onCreateLine(start, end, basis.normal, lineThickness, material));
|
|
1519
|
+
onCreateLine(ls, le, basis.normal, lineThickness, material);
|
|
1520
|
+
|
|
1521
|
+
// An arrow inside each end, pointing at its mark, where both fit with a gap of the line between them
|
|
1522
|
+
const arrowLength = textHeight * dimensionArrowLength;
|
|
1523
|
+
const arrowHalfBase = textHeight * dimensionArrowHalfBase;
|
|
1524
|
+
if (vec3Length(vec3Sub(le, ls)) >= 2 * arrowLength + gap) {
|
|
1525
|
+
for (const [tip, inward] of [
|
|
1526
|
+
[ls, direction],
|
|
1527
|
+
[le, vec3Scale(direction, -1)],
|
|
1528
|
+
] as const) {
|
|
1529
|
+
const base = vec3Add(tip, vec3Scale(inward, arrowLength));
|
|
1530
|
+
const a = vec3Add(base, vec3Scale(dirBetweenLines, arrowHalfBase));
|
|
1531
|
+
const b = vec3Sub(base, vec3Scale(dirBetweenLines, arrowHalfBase));
|
|
1532
|
+
// The winding order facing the view's normal
|
|
1533
|
+
if (vec3Dot(vec3Cross(vec3Sub(a, tip), vec3Sub(b, tip)), basis.normal) >= 0) {
|
|
1534
|
+
onCreatePolygon(tip, a, b, material);
|
|
1535
|
+
} else {
|
|
1536
|
+
onCreatePolygon(tip, b, a, material);
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1195
1540
|
|
|
1196
1541
|
const rotatedDirBetweenLines = vec3TransRot(dirBetweenLines, vec3Zero, sceneRotation);
|
|
1197
1542
|
const rotatedNormal = vec3TransRot(basis.normal, vec3Zero, sceneRotation);
|
|
@@ -1205,44 +1550,31 @@ export function dimensionMeshifyAlignedDimension(
|
|
|
1205
1550
|
const finalBasis = dimensionViewBasises.front;
|
|
1206
1551
|
const finalSide = getDimensionSide(dirBetweenLines, finalBasis);
|
|
1207
1552
|
const finalDir = getDimensionTextDirection(finalSide, finalBasis);
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
const arrowBaseStart2 = vec3Add(arrowBaseStart, vec3Scale(dirBetweenLines, -arrowHalfBase));
|
|
1225
|
-
const arrowBaseEnd1 = vec3Add(arrowBaseEnd, vec3Scale(dirBetweenLines, arrowHalfBase));
|
|
1226
|
-
const arrowBaseEnd2 = vec3Add(arrowBaseEnd, vec3Scale(dirBetweenLines, -arrowHalfBase));
|
|
1227
|
-
|
|
1228
|
-
//the winding order needs to be correct according to the normal
|
|
1229
|
-
const polygonFacing = (a: Vec3, b: Vec3, c: Vec3): void => {
|
|
1230
|
-
const ab = vec3Sub(b, a);
|
|
1231
|
-
const ac = vec3Sub(c, a);
|
|
1232
|
-
const triangleNormal = vec3Cross(ab, ac);
|
|
1233
|
-
|
|
1234
|
-
if (vec3Dot(triangleNormal, basis.normal) >= 0) {
|
|
1235
|
-
onCreatePolygon(a, b, c, material);
|
|
1236
|
-
} else {
|
|
1237
|
-
onCreatePolygon(a, c, b, material);
|
|
1238
|
-
}
|
|
1239
|
-
};
|
|
1240
|
-
polygonFacing(lsDisplaced, arrowBaseStart2, arrowBaseStart1);
|
|
1241
|
-
polygonFacing(leDisplaced, arrowBaseEnd1, arrowBaseEnd2);
|
|
1242
|
-
}
|
|
1553
|
+
// Each row a text of its own: one row a gap out from the line or towards the measured points, of several the first a gap over the line, as the
|
|
1554
|
+
// text reads, and the rest from a gap under it
|
|
1555
|
+
const textUp = vec3Normalize(vec3Cross(basis.normal, textDir));
|
|
1556
|
+
measurementRows.forEach((row, i) =>
|
|
1557
|
+
onCreateText(
|
|
1558
|
+
measurementRows.length === 1
|
|
1559
|
+
? vec3Add(lc, vec3Scale(dirBetweenLines, (place.text.inward ? -1 : 1) * (gap + textHeight / 2)))
|
|
1560
|
+
: vec3Add(lc, vec3Scale(textUp, i === 0 ? gap + textHeight / 2 : -(gap + textHeight / 2 + (i - 1) * dimensionTextRowStep * textHeight))),
|
|
1561
|
+
row,
|
|
1562
|
+
textHeight,
|
|
1563
|
+
material,
|
|
1564
|
+
textRot,
|
|
1565
|
+
finalDir,
|
|
1566
|
+
vec3(0, 0, 1)
|
|
1567
|
+
)
|
|
1568
|
+
);
|
|
1243
1569
|
}
|
|
1244
1570
|
|
|
1245
|
-
export function dimensionConvertToTypeMesh(
|
|
1571
|
+
export function dimensionConvertToTypeMesh(
|
|
1572
|
+
dimension: Dimension,
|
|
1573
|
+
sceneRotation: Vec3,
|
|
1574
|
+
dimensionsMaterial?: Material,
|
|
1575
|
+
textHeight?: number,
|
|
1576
|
+
placement?: DimensionPlacement
|
|
1577
|
+
): DimensionMesh {
|
|
1246
1578
|
if (dimensionIsOfTypeMesh(dimension)) {
|
|
1247
1579
|
return dimension;
|
|
1248
1580
|
}
|
|
@@ -1257,7 +1589,17 @@ export function dimensionConvertToTypeMesh(dimension: Dimension, sceneRotation:
|
|
|
1257
1589
|
const onCreatePolygon = (p1: Vec3, p2: Vec3, p3: Vec3, mat: Material): void => {
|
|
1258
1590
|
meshes.push(polygon([p1, p2, p3], mat));
|
|
1259
1591
|
};
|
|
1260
|
-
dimensionMeshifyAlignedDimension(
|
|
1592
|
+
dimensionMeshifyAlignedDimension(
|
|
1593
|
+
dimension,
|
|
1594
|
+
sceneRotation,
|
|
1595
|
+
vec3Zero,
|
|
1596
|
+
onCreateLine,
|
|
1597
|
+
onCreateText,
|
|
1598
|
+
onCreatePolygon,
|
|
1599
|
+
dimensionsMaterial,
|
|
1600
|
+
textHeight,
|
|
1601
|
+
placement
|
|
1602
|
+
);
|
|
1261
1603
|
|
|
1262
1604
|
return {
|
|
1263
1605
|
meshes,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { dimensionMeshifyAlignedDimension } from "../../../abstract-3d.js";
|
|
2
|
-
import type { DimensionAligned, View, Material, Vec3 } from "../../../abstract-3d.js";
|
|
2
|
+
import type { DimensionAligned, DimensionPlacement, View, Material, Vec3 } from "../../../abstract-3d.js";
|
|
3
3
|
import { type DxfColor, dxfColor } from "./dxf-color.js";
|
|
4
4
|
import { DXF_BLOCK_RECORD_TABLE_HANDLE, DXF_MODEL_SPACE_HANDLE, dxfHandleNext, dxfRound, type Handle } from "./dxf-common.js";
|
|
5
5
|
import { dxfEncLine } from "./dxf-line.js";
|
|
@@ -21,6 +21,8 @@ export function dxfEncDimension(
|
|
|
21
21
|
sceneRotation: Vec3,
|
|
22
22
|
viewRotation: Vec3,
|
|
23
23
|
views: ReadonlyArray<View>,
|
|
24
|
+
textHeight: number | undefined,
|
|
25
|
+
placement: DimensionPlacement | undefined,
|
|
24
26
|
handleRef: Handle
|
|
25
27
|
): DxfDimensionDefinition {
|
|
26
28
|
const blockRecordHandle = dxfHandleNext(handleRef);
|
|
@@ -38,6 +40,8 @@ export function dxfEncDimension(
|
|
|
38
40
|
sceneRotation,
|
|
39
41
|
viewRotation,
|
|
40
42
|
views,
|
|
43
|
+
textHeight,
|
|
44
|
+
placement,
|
|
41
45
|
handleRef
|
|
42
46
|
),
|
|
43
47
|
blockRecord: dxfEncodeDimensionBlockRecord(blockName, blockRecordHandle),
|
|
@@ -128,6 +132,8 @@ function dxfEncodeDimensionBlock(
|
|
|
128
132
|
sceneRotation: Vec3,
|
|
129
133
|
viewRotation: Vec3,
|
|
130
134
|
views: ReadonlyArray<View>,
|
|
135
|
+
textHeight: number | undefined,
|
|
136
|
+
placement: DimensionPlacement | undefined,
|
|
131
137
|
handleRef: Handle
|
|
132
138
|
): string {
|
|
133
139
|
return `0
|
|
@@ -166,6 +172,8 @@ ${dxfEncodeDimensionGeometry(
|
|
|
166
172
|
sceneRotation,
|
|
167
173
|
viewRotation,
|
|
168
174
|
views,
|
|
175
|
+
textHeight,
|
|
176
|
+
placement,
|
|
169
177
|
handleRef
|
|
170
178
|
)}0
|
|
171
179
|
ENDBLK
|
|
@@ -194,6 +202,8 @@ function dxfEncodeDimensionGeometry(
|
|
|
194
202
|
sceneRotation: Vec3,
|
|
195
203
|
viewRotation: Vec3,
|
|
196
204
|
views: ReadonlyArray<View>,
|
|
205
|
+
textHeight: number | undefined,
|
|
206
|
+
placement: DimensionPlacement | undefined,
|
|
197
207
|
handleRef: Handle
|
|
198
208
|
): string {
|
|
199
209
|
let entities = "";
|
|
@@ -214,6 +224,16 @@ function dxfEncodeDimensionGeometry(
|
|
|
214
224
|
text,
|
|
215
225
|
views,
|
|
216
226
|
};
|
|
217
|
-
dimensionMeshifyAlignedDimension(
|
|
227
|
+
dimensionMeshifyAlignedDimension(
|
|
228
|
+
dimension,
|
|
229
|
+
sceneRotation,
|
|
230
|
+
viewRotation,
|
|
231
|
+
onCreateLine,
|
|
232
|
+
onCreateText,
|
|
233
|
+
onCreatePolygon,
|
|
234
|
+
undefined,
|
|
235
|
+
textHeight,
|
|
236
|
+
placement
|
|
237
|
+
);
|
|
218
238
|
return entities;
|
|
219
239
|
}
|