Organize support files and simplify imports
This commit is contained in:
parent
70f41352c1
commit
ef203af85a
178 changed files with 61 additions and 269 deletions
16
font-src/support/geometry/anchor.js
Normal file
16
font-src/support/geometry/anchor.js
Normal file
|
@ -0,0 +1,16 @@
|
|||
"use strict";
|
||||
|
||||
module.exports = class Anchor {
|
||||
constructor(x, y) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
transform(tfm) {
|
||||
return Anchor.transform(tfm, this);
|
||||
}
|
||||
static transform(tfm, a) {
|
||||
const x = a.x * tfm.xx + a.y * tfm.yx + tfm.x;
|
||||
const y = a.x * tfm.xy + a.y * tfm.yy + tfm.y;
|
||||
return new Anchor(x, y);
|
||||
}
|
||||
};
|
49
font-src/support/geometry/box.js
Normal file
49
font-src/support/geometry/box.js
Normal file
|
@ -0,0 +1,49 @@
|
|||
"use strict";
|
||||
|
||||
const { mix } = require("../utils");
|
||||
|
||||
class Box {
|
||||
constructor(t, b, l, r) {
|
||||
this.top = t;
|
||||
this.bottom = this.bot = b;
|
||||
this.left = l;
|
||||
this.right = r;
|
||||
this.xMid = this.xMiddle = mix(l, r, 0.5);
|
||||
this.yMid = this.yMiddle = mix(b, t, 0.5);
|
||||
}
|
||||
|
||||
withTop(t) {
|
||||
return new Box(t, this.bottom, this.left, this.right);
|
||||
}
|
||||
withBottom(b) {
|
||||
return new Box(this.top, b, this.left, this.right);
|
||||
}
|
||||
withLeft(l) {
|
||||
return new Box(this.top, this.bottom, l, this.right);
|
||||
}
|
||||
withRight(r) {
|
||||
return new Box(this.top, this.bottom, this.left, r);
|
||||
}
|
||||
|
||||
withXPadding(d) {
|
||||
return new Box(this.top, this.bottom, this.left + d, this.right - d);
|
||||
}
|
||||
withYPadding(d) {
|
||||
return new Box(this.top - d, this.bottom + d, this.left, this.right);
|
||||
}
|
||||
|
||||
mixX(t) {
|
||||
return mix(this.left, this.right, t);
|
||||
}
|
||||
mixXMidLeft(t) {
|
||||
return mix(this.xMid, this.left, t);
|
||||
}
|
||||
mixXMidRight(t) {
|
||||
return mix(this.xMid, this.right, t);
|
||||
}
|
||||
mixY(t) {
|
||||
return mix(this.bottom, this.top, t);
|
||||
}
|
||||
}
|
||||
|
||||
exports.Box = Box;
|
160
font-src/support/geometry/curve-util.js
Normal file
160
font-src/support/geometry/curve-util.js
Normal file
|
@ -0,0 +1,160 @@
|
|||
"use strict";
|
||||
|
||||
const TypoGeom = require("typo-geom");
|
||||
const Point = require("./point");
|
||||
const Transform = require("./transform");
|
||||
|
||||
exports.SPIRO_PRECISION = 1 / 2;
|
||||
exports.OCCURRENT_PRECISION = 1 / 16;
|
||||
exports.GEOMETRY_PRECISION = 1 / 4;
|
||||
exports.BOOLE_RESOLUTION = 0x4000;
|
||||
|
||||
exports.OffsetCurve = class OffsetCurve {
|
||||
constructor(bone, offset, contrast) {
|
||||
this.bone = bone;
|
||||
this.offset = offset;
|
||||
this.contrast = contrast;
|
||||
}
|
||||
eval(t) {
|
||||
const c = this.bone.eval(t);
|
||||
const d = this.bone.derivative(t);
|
||||
const absD = Math.hypot(d.x, d.y);
|
||||
return {
|
||||
x: c.x - (d.y / absD) * this.offset * this.contrast,
|
||||
y: c.y + (d.x / absD) * this.offset
|
||||
};
|
||||
}
|
||||
derivative(t) {
|
||||
const DELTA = 1 / 0x10000;
|
||||
const forward = this.eval(t + DELTA);
|
||||
const backward = this.eval(t - DELTA);
|
||||
return {
|
||||
x: (forward.x - backward.x) / (2 * DELTA),
|
||||
y: (forward.y - backward.y) / (2 * DELTA)
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
exports.ReverseCurve = class ReverseCurve {
|
||||
constructor(original) {
|
||||
this.m_original = original;
|
||||
}
|
||||
eval(t) {
|
||||
return this.m_original.eval(1 - t);
|
||||
}
|
||||
derivative(t) {
|
||||
return -this.m_original.derivative(1 - t);
|
||||
}
|
||||
};
|
||||
|
||||
exports.convertShapeToArcs = function convertShapeToArcs(shape) {
|
||||
return shape.map(convertContourToArcs);
|
||||
};
|
||||
|
||||
function contourToRep(contour) {
|
||||
let c = [];
|
||||
for (const z of contour) c.push({ type: z.type, x: z.x, y: z.y });
|
||||
return c;
|
||||
}
|
||||
exports.shapeToRep = function (shape) {
|
||||
return shape.map(contourToRep);
|
||||
};
|
||||
|
||||
function repToContour(contourRep) {
|
||||
let c = [];
|
||||
for (const z of contourRep) c.push(Point.fromXY(z.type, z.x, z.y));
|
||||
return c;
|
||||
}
|
||||
exports.repToShape = function (shapeRep) {
|
||||
return shapeRep.map(repToContour);
|
||||
};
|
||||
|
||||
function convertContourToArcs(contour) {
|
||||
if (!contour || !contour.length) return [];
|
||||
|
||||
const newContour = [];
|
||||
let z0 = Point.from(Point.Type.Corner, contour[0]);
|
||||
|
||||
for (let j = 1; j < contour.length; j++) {
|
||||
const z = contour[j];
|
||||
switch (z.type) {
|
||||
case Point.Type.CubicStart: {
|
||||
const z1 = z;
|
||||
const z2 = contour[j + 1];
|
||||
const z3 = contour[j + 2];
|
||||
newContour.push(
|
||||
new TypoGeom.Arcs.Bez3(
|
||||
z0,
|
||||
Point.from(Point.Type.CubicStart, z1),
|
||||
Point.from(Point.Type.CubicEnd, z2),
|
||||
Point.from(Point.Type.Corner, z3)
|
||||
)
|
||||
);
|
||||
z0 = z3;
|
||||
j += 2;
|
||||
break;
|
||||
}
|
||||
case Point.Type.Quadratic: {
|
||||
const zc = z;
|
||||
let zf = contour[j + 1] || contour[0];
|
||||
const zfIsCorner = zf.type === Point.Type.contour;
|
||||
if (!zfIsCorner) zf = Point.from(Point.Type.Corner, zc).mix(0.5, zf);
|
||||
|
||||
newContour.push(
|
||||
new TypoGeom.Arcs.Bez3(
|
||||
z0,
|
||||
Point.from(Point.Type.CubicStart, z0).mix(2 / 3, zc),
|
||||
Point.from(Point.Type.CubicEnd, zf).mix(2 / 3, zc),
|
||||
Point.from(Point.Type.Corner, zf)
|
||||
)
|
||||
);
|
||||
|
||||
z0 = zf;
|
||||
if (zfIsCorner) j++;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
newContour.push(
|
||||
TypoGeom.Arcs.Bez3.fromStraightSegment(
|
||||
new TypoGeom.Arcs.StraightSegment(z0, Point.from(Point.Type.Corner, z))
|
||||
)
|
||||
);
|
||||
z0 = z;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return newContour;
|
||||
}
|
||||
|
||||
exports.BezToContoursSink = class BezToContoursSink {
|
||||
constructor(gizmo) {
|
||||
this.gizmo = gizmo || Transform.Id();
|
||||
this.contours = [];
|
||||
this.lastContour = [];
|
||||
}
|
||||
beginShape() {}
|
||||
endShape() {
|
||||
if (this.lastContour.length) {
|
||||
this.contours.push(this.lastContour);
|
||||
}
|
||||
this.lastContour = [];
|
||||
}
|
||||
moveTo(x, y) {
|
||||
this.endShape();
|
||||
this.lastContour.push(Point.transformedXY(this.gizmo, Point.Type.Corner, x, y));
|
||||
}
|
||||
lineTo(x, y) {
|
||||
this.lastContour.push(Point.transformedXY(this.gizmo, Point.Type.Corner, x, y));
|
||||
}
|
||||
curveTo(xc, yc, x, y) {
|
||||
this.lastContour.push(Point.transformedXY(this.gizmo, Point.Type.Quadratic, xc, yc));
|
||||
this.lastContour.push(Point.transformedXY(this.gizmo, Point.Type.Corner, x, y));
|
||||
}
|
||||
cubicTo(x1, y1, x2, y2, x, y) {
|
||||
this.lastContour.push(Point.transformedXY(this.gizmo, Point.Type.CubicStart, x1, y1));
|
||||
this.lastContour.push(Point.transformedXY(this.gizmo, Point.Type.CubicEnd, x2, y2));
|
||||
this.lastContour.push(Point.transformedXY(this.gizmo, Point.Type.Corner, x, y));
|
||||
}
|
||||
};
|
498
font-src/support/geometry/index.js
Normal file
498
font-src/support/geometry/index.js
Normal file
|
@ -0,0 +1,498 @@
|
|||
"use strict";
|
||||
|
||||
const crypto = require("crypto");
|
||||
const TypoGeom = require("typo-geom");
|
||||
const SpiroJs = require("spiro");
|
||||
|
||||
const Point = require("./point");
|
||||
const Transform = require("./transform");
|
||||
const CurveUtil = require("./curve-util");
|
||||
const { SpiroExpander } = require("./spiro-expand");
|
||||
const Format = require("../util/formatter");
|
||||
|
||||
class GeometryBase {
|
||||
asContours() {
|
||||
throw new Error("Unimplemented");
|
||||
}
|
||||
asReferences() {
|
||||
throw new Error("Unimplemented");
|
||||
}
|
||||
unlinkReferences() {
|
||||
return this;
|
||||
}
|
||||
filterTag(fn) {
|
||||
return this;
|
||||
}
|
||||
isEmpty() {
|
||||
return true;
|
||||
}
|
||||
measureComplexity() {
|
||||
return 0;
|
||||
}
|
||||
toShapeStringOrNull() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
class ContourGeometry extends GeometryBase {
|
||||
constructor(points) {
|
||||
super();
|
||||
this.m_points = [];
|
||||
for (const z of points) {
|
||||
this.m_points.push(Point.from(z.type, z));
|
||||
}
|
||||
}
|
||||
asContours() {
|
||||
if (this.isEmpty()) return [];
|
||||
let c1 = [];
|
||||
for (const z of this.m_points) c1.push(Point.from(z.type, z));
|
||||
return [c1];
|
||||
}
|
||||
asReferences() {
|
||||
return null;
|
||||
}
|
||||
filterTag(fn) {
|
||||
return this;
|
||||
}
|
||||
isEmpty() {
|
||||
return !this.m_points.length;
|
||||
}
|
||||
measureComplexity() {
|
||||
for (const z of this.m_points) {
|
||||
if (!isFinite(z.x) || !isFinite(z.y)) return 0xffff;
|
||||
}
|
||||
return this.m_points.length;
|
||||
}
|
||||
toShapeStringOrNull() {
|
||||
return Format.struct(`ContourGeometry`, Format.list(this.m_points.map(Format.typedPoint)));
|
||||
}
|
||||
}
|
||||
|
||||
class SpiroGeometry extends GeometryBase {
|
||||
constructor(gizmo, closed, knots) {
|
||||
super();
|
||||
this.m_knots = [];
|
||||
for (const k of knots) {
|
||||
this.m_knots.push({ type: k.type, x: k.x, y: k.y });
|
||||
}
|
||||
this.m_closed = closed;
|
||||
this.m_gizmo = gizmo;
|
||||
this.m_cachedContours = null;
|
||||
}
|
||||
asContours() {
|
||||
if (this.m_cachedContours) return this.m_cachedContours;
|
||||
const s = new CurveUtil.BezToContoursSink(this.m_gizmo);
|
||||
SpiroJs.spiroToBezierOnContext(this.m_knots, this.m_closed, s, CurveUtil.SPIRO_PRECISION);
|
||||
this.m_cachedContours = s.contours;
|
||||
return this.m_cachedContours;
|
||||
}
|
||||
asReferences() {
|
||||
return null;
|
||||
}
|
||||
filterTag(fn) {
|
||||
return this;
|
||||
}
|
||||
isEmpty() {
|
||||
return !this.m_knots.length;
|
||||
}
|
||||
measureComplexity() {
|
||||
for (const z of this.m_knots) {
|
||||
if (!isFinite(z.x) || !isFinite(z.y)) return 0xffff;
|
||||
}
|
||||
return this.m_knots.length;
|
||||
}
|
||||
toShapeStringOrNull() {
|
||||
return Format.struct(
|
||||
"SpiroGeometry",
|
||||
Format.gizmo(this.m_gizmo),
|
||||
this.m_closed,
|
||||
Format.list(this.m_knots.map(Format.typedPoint))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class DiSpiroGeometry extends GeometryBase {
|
||||
constructor(gizmo, contrast, closed, biKnots) {
|
||||
super();
|
||||
|
||||
this.m_biKnots = [];
|
||||
for (const k of biKnots) this.m_biKnots.push(k.clone());
|
||||
|
||||
this.m_closed = closed;
|
||||
this.m_gizmo = gizmo;
|
||||
this.m_contrast = contrast;
|
||||
|
||||
this.m_cachedExpansionResults = null;
|
||||
this.m_cachedContours = null;
|
||||
}
|
||||
|
||||
asContours() {
|
||||
if (this.m_cachedContours) return this.m_cachedContours;
|
||||
const { lhs, rhs } = this.expand();
|
||||
|
||||
let rawGeometry;
|
||||
if (this.m_closed) {
|
||||
rawGeometry = new CombineGeometry([
|
||||
new SpiroGeometry(Transform.Id(), this.m_closed, lhs.slice(0, -1)),
|
||||
new SpiroGeometry(Transform.Id(), this.m_closed, rhs.reverse().slice(0, -1))
|
||||
]);
|
||||
} else {
|
||||
lhs[0].type = lhs[lhs.length - 1].type = "corner";
|
||||
rhs[0].type = rhs[rhs.length - 1].type = "corner";
|
||||
const allKnots = lhs.concat(rhs.reverse());
|
||||
rawGeometry = new SpiroGeometry(Transform.Id(), true, allKnots);
|
||||
}
|
||||
this.m_cachedContours = rawGeometry.asContours();
|
||||
return this.m_cachedContours;
|
||||
}
|
||||
expand() {
|
||||
if (this.m_cachedExpansionResults) return this.m_cachedExpansionResults;
|
||||
const expander = new SpiroExpander(
|
||||
this.m_gizmo,
|
||||
this.m_contrast,
|
||||
this.m_closed,
|
||||
this.m_biKnots.map(k => k.clone())
|
||||
);
|
||||
expander.initializeNormals();
|
||||
expander.iterateNormals();
|
||||
expander.iterateNormals();
|
||||
this.m_cachedExpansionResults = expander.expand();
|
||||
return this.m_cachedExpansionResults;
|
||||
}
|
||||
|
||||
asReferences() {
|
||||
return null;
|
||||
}
|
||||
filterTag(fn) {
|
||||
return this;
|
||||
}
|
||||
isEmpty() {
|
||||
return !this.m_biKnots.length;
|
||||
}
|
||||
measureComplexity() {
|
||||
for (const z of this.m_biKnots) {
|
||||
if (!isFinite(z.x) || !isFinite(z.y)) return 0xffff;
|
||||
}
|
||||
return this.m_biKnots.length;
|
||||
}
|
||||
|
||||
toShapeStringOrNull() {
|
||||
return Format.struct(
|
||||
Format.gizmo(this.m_gizmo),
|
||||
Format.n(this.m_contrast),
|
||||
this.m_closed,
|
||||
Format.list(this.m_biKnots.map(z => z.toShapeString()))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ReferenceGeometry extends GeometryBase {
|
||||
constructor(glyph, x, y) {
|
||||
super();
|
||||
if (!glyph || !glyph.geometry) throw new TypeError("Invalid glyph");
|
||||
this.m_glyph = glyph;
|
||||
this.m_x = x || 0;
|
||||
this.m_y = y || 0;
|
||||
}
|
||||
unwrap() {
|
||||
return new TransformedGeometry(
|
||||
this.m_glyph.geometry,
|
||||
Transform.Translate(this.m_x, this.m_y)
|
||||
);
|
||||
}
|
||||
asContours() {
|
||||
if (this.isEmpty()) return [];
|
||||
return this.unwrap().asContours();
|
||||
}
|
||||
asReferences() {
|
||||
if (this.isEmpty()) return [];
|
||||
return [{ glyph: this.m_glyph, x: this.m_x, y: this.m_y }];
|
||||
}
|
||||
filterTag(fn) {
|
||||
if (this.isEmpty()) return null;
|
||||
return this.unwrap().filterTag(fn);
|
||||
}
|
||||
isEmpty() {
|
||||
if (!this.m_glyph || !this.m_glyph.geometry) return true;
|
||||
return this.m_glyph.geometry.isEmpty();
|
||||
}
|
||||
measureComplexity() {
|
||||
return this.m_glyph.geometry.measureComplexity();
|
||||
}
|
||||
unlinkReferences() {
|
||||
return this.unwrap().unlinkReferences();
|
||||
}
|
||||
toShapeStringOrNull() {
|
||||
let sTarget = this.m_glyph.geometry.toShapeStringOrNull();
|
||||
if (!sTarget) return null;
|
||||
return Format.struct("ReferenceGeometry", sTarget, Format.n(this.m_x), Format.n(this.m_y));
|
||||
}
|
||||
}
|
||||
|
||||
class TaggedGeometry extends GeometryBase {
|
||||
constructor(g, tag) {
|
||||
super();
|
||||
this.m_geom = g;
|
||||
this.m_tag = tag;
|
||||
}
|
||||
asContours() {
|
||||
return this.m_geom.asContours();
|
||||
}
|
||||
asReferences() {
|
||||
return this.m_geom.asReferences();
|
||||
}
|
||||
filterTag(fn) {
|
||||
if (!fn(this.m_tag)) return null;
|
||||
else return new TaggedGeometry(this.m_geom.filterTag(fn), this.m_tag);
|
||||
}
|
||||
isEmpty() {
|
||||
return this.m_geom.isEmpty();
|
||||
}
|
||||
measureComplexity() {
|
||||
return this.m_geom.measureComplexity();
|
||||
}
|
||||
unlinkReferences() {
|
||||
return this.m_geom.unlinkReferences();
|
||||
}
|
||||
toShapeStringOrNull() {
|
||||
return this.m_geom.toShapeStringOrNull();
|
||||
}
|
||||
}
|
||||
|
||||
class TransformedGeometry extends GeometryBase {
|
||||
constructor(g, tfm) {
|
||||
super();
|
||||
this.m_geom = g;
|
||||
this.m_transform = tfm;
|
||||
}
|
||||
asContours() {
|
||||
let result = [];
|
||||
for (const c of this.m_geom.asContours()) {
|
||||
let c1 = [];
|
||||
for (const z of c) c1.push(Point.transformed(this.m_transform, z));
|
||||
result.push(c1);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
asReferences() {
|
||||
if (!Transform.isTranslate(this.m_transform)) return null;
|
||||
const rs = this.m_geom.asReferences();
|
||||
if (!rs) return null;
|
||||
|
||||
let result = [];
|
||||
for (const { glyph, x, y } of rs)
|
||||
result.push({ glyph, x: x + this.m_transform.x, y: y + this.m_transform.y });
|
||||
return result;
|
||||
}
|
||||
filterTag(fn) {
|
||||
const e = this.m_geom.filterTag(fn);
|
||||
if (!e) return null;
|
||||
return new TransformedGeometry(e, this.m_transform);
|
||||
}
|
||||
isEmpty() {
|
||||
return this.m_geom.isEmpty();
|
||||
}
|
||||
measureComplexity() {
|
||||
return this.m_geom.measureComplexity();
|
||||
}
|
||||
unlinkReferences() {
|
||||
const unwrapped = this.m_geom.unlinkReferences();
|
||||
if (Transform.isIdentity(this.m_transform)) {
|
||||
return unwrapped;
|
||||
} else if (
|
||||
unwrapped instanceof TransformedGeometry &&
|
||||
Transform.isTranslate(this.m_transform) &&
|
||||
Transform.isTranslate(unwrapped.m_transform)
|
||||
) {
|
||||
return new TransformedGeometry(
|
||||
unwrapped.m_geom,
|
||||
Transform.Translate(
|
||||
this.m_transform.x + unwrapped.m_transform.x,
|
||||
this.m_transform.y + unwrapped.m_transform.y
|
||||
)
|
||||
);
|
||||
} else {
|
||||
return new TransformedGeometry(unwrapped, this.m_transform);
|
||||
}
|
||||
}
|
||||
toShapeStringOrNull() {
|
||||
const sTarget = this.m_geom.toShapeStringOrNull();
|
||||
if (!sTarget) return null;
|
||||
return Format.struct(TransformedGeometry, sTarget, Format.gizmo(this.m_transform));
|
||||
}
|
||||
}
|
||||
|
||||
class CombineGeometry extends GeometryBase {
|
||||
constructor(parts) {
|
||||
super();
|
||||
this.m_parts = parts || [];
|
||||
}
|
||||
with(g) {
|
||||
if (g instanceof CombineGeometry) {
|
||||
return new CombineGeometry([...this.m_parts, ...g.m_parts]);
|
||||
} else {
|
||||
return new CombineGeometry([...this.m_parts, g]);
|
||||
}
|
||||
}
|
||||
asContours() {
|
||||
let results = [];
|
||||
for (const part of this.m_parts) {
|
||||
for (const c of part.asContours()) {
|
||||
results.push(c);
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
asReferences() {
|
||||
let results = [];
|
||||
for (const part of this.m_parts) {
|
||||
const rs = part.asReferences();
|
||||
if (!rs) return null;
|
||||
for (const c of rs) {
|
||||
results.push(c);
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
filterTag(fn) {
|
||||
let filtered = [];
|
||||
for (const part of this.m_parts) {
|
||||
const fp = part.filterTag(fn);
|
||||
if (fp) filtered.push(fp);
|
||||
}
|
||||
return new CombineGeometry(filtered);
|
||||
}
|
||||
isEmpty() {
|
||||
for (const part of this.m_parts) if (!part.isEmpty()) return false;
|
||||
return true;
|
||||
}
|
||||
measureComplexity() {
|
||||
let s = 0;
|
||||
for (const part of this.m_parts) s += part.measureComplexity();
|
||||
}
|
||||
|
||||
unlinkReferences() {
|
||||
let parts = [];
|
||||
for (const part of this.m_parts) {
|
||||
const unwrapped = part.unlinkReferences();
|
||||
if (unwrapped instanceof CombineGeometry) {
|
||||
for (const p of unwrapped.m_parts) parts.push(p);
|
||||
} else {
|
||||
parts.push(unwrapped);
|
||||
}
|
||||
}
|
||||
return new CombineGeometry(parts);
|
||||
}
|
||||
toShapeStringOrNull() {
|
||||
let sParts = [];
|
||||
for (const item of this.m_parts) {
|
||||
const sPart = item.toShapeStringOrNull();
|
||||
if (!sPart) return null;
|
||||
sParts.push(sPart);
|
||||
}
|
||||
return Format.struct("CombineGeometry", Format.list(sParts));
|
||||
}
|
||||
}
|
||||
|
||||
class BooleanGeometry extends GeometryBase {
|
||||
constructor(operator, operands) {
|
||||
super();
|
||||
this.m_operator = operator;
|
||||
this.m_operands = operands;
|
||||
this.m_resolved = null;
|
||||
}
|
||||
asContours() {
|
||||
if (this.m_resolved) return this.m_resolved;
|
||||
this.m_resolved = this.asContoursImpl();
|
||||
return this.m_resolved;
|
||||
}
|
||||
asContoursImpl() {
|
||||
if (this.m_operands.length === 0) return [];
|
||||
|
||||
let arcs = CurveUtil.convertShapeToArcs(this.m_operands[0].asContours());
|
||||
if (this.m_operands.length === 1) {
|
||||
arcs = TypoGeom.Boolean.removeOverlap(
|
||||
arcs,
|
||||
TypoGeom.Boolean.PolyFillType.pftNonZero,
|
||||
CurveUtil.BOOLE_RESOLUTION
|
||||
);
|
||||
}
|
||||
for (let j = 1; j < this.m_operands.length; j++) {
|
||||
arcs = TypoGeom.Boolean.combine(
|
||||
this.m_operator,
|
||||
arcs,
|
||||
CurveUtil.convertShapeToArcs(this.m_operands[j].asContours()),
|
||||
TypoGeom.Boolean.PolyFillType.pftNonZero,
|
||||
TypoGeom.Boolean.PolyFillType.pftNonZero,
|
||||
CurveUtil.BOOLE_RESOLUTION
|
||||
);
|
||||
}
|
||||
const ctx = new CurveUtil.BezToContoursSink();
|
||||
TypoGeom.ShapeConv.transferBezArcShape(arcs, ctx);
|
||||
return ctx.contours;
|
||||
}
|
||||
asReferences() {
|
||||
return null;
|
||||
}
|
||||
filterTag(fn) {
|
||||
let filtered = [];
|
||||
for (const operand of this.m_operands) {
|
||||
const fp = operand.filterTag(fn);
|
||||
if (fp) filtered.push(fp);
|
||||
}
|
||||
return new BooleanGeometry(this.m_operator, filtered);
|
||||
}
|
||||
isEmpty() {
|
||||
for (const operand of this.m_operands) if (!operand.isEmpty()) return false;
|
||||
return true;
|
||||
}
|
||||
measureComplexity() {
|
||||
let s = 0;
|
||||
for (const operand of this.m_operands) s += operand.measureComplexity();
|
||||
}
|
||||
unlinkReferences() {
|
||||
if (this.m_operands.length === 0) return new CombineGeometry([]);
|
||||
if (this.m_operands.length === 1) return this.m_operands[0].unlinkReferences();
|
||||
|
||||
let operands = [];
|
||||
for (const operand of this.m_operands) {
|
||||
operands.push(operand.unlinkReferences());
|
||||
}
|
||||
return new BooleanGeometry(this.m_operator, operands);
|
||||
}
|
||||
toShapeStringOrNull() {
|
||||
let sParts = [];
|
||||
for (const item of this.m_operands) {
|
||||
const sPart = item.toShapeStringOrNull();
|
||||
if (!sPart) return null;
|
||||
sParts.push(sPart);
|
||||
}
|
||||
return Format.struct("BooleanGeometry", this.m_operator, Format.list(sParts));
|
||||
}
|
||||
}
|
||||
|
||||
exports.hashGeometry = function (geom) {
|
||||
const s = geom.toShapeStringOrNull();
|
||||
if (!s) return null;
|
||||
return crypto.createHash("sha256").update(s).digest("hex");
|
||||
};
|
||||
|
||||
function combineWith(a, b) {
|
||||
if (a instanceof CombineGeometry) {
|
||||
return a.with(b);
|
||||
} else {
|
||||
return new CombineGeometry([a, b]);
|
||||
}
|
||||
}
|
||||
|
||||
exports.GeometryBase = GeometryBase;
|
||||
exports.SpiroGeometry = SpiroGeometry;
|
||||
exports.DiSpiroGeometry = DiSpiroGeometry;
|
||||
exports.ContourGeometry = ContourGeometry;
|
||||
exports.ReferenceGeometry = ReferenceGeometry;
|
||||
exports.TaggedGeometry = TaggedGeometry;
|
||||
exports.TransformedGeometry = TransformedGeometry;
|
||||
exports.CombineGeometry = CombineGeometry;
|
||||
exports.BooleanGeometry = BooleanGeometry;
|
||||
|
||||
exports.combineWith = combineWith;
|
63
font-src/support/geometry/point.js
Normal file
63
font-src/support/geometry/point.js
Normal file
|
@ -0,0 +1,63 @@
|
|||
"use strict";
|
||||
|
||||
class Point {
|
||||
constructor(type, x, y) {
|
||||
this.type = type;
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
get on() {
|
||||
throw new Error("Unreachable");
|
||||
}
|
||||
get cubic() {
|
||||
throw new Error("Unreachable");
|
||||
}
|
||||
add(z2) {
|
||||
return this.addScale(1, z2);
|
||||
}
|
||||
addScale(scale, z2) {
|
||||
return new Point(this.type, this.x + scale * z2.x, this.y + scale * z2.y);
|
||||
}
|
||||
mix(scale, z2) {
|
||||
return new Point(
|
||||
this.type,
|
||||
this.x + scale * (z2.x - this.x),
|
||||
this.y + scale * (z2.y - this.y)
|
||||
);
|
||||
}
|
||||
scale(t) {
|
||||
return new Point(this.type, t * this.x, t * this.y);
|
||||
}
|
||||
round(d) {
|
||||
return new Point(this.type, Math.round(d * this.x) / d, Math.round(d * this.y) / d);
|
||||
}
|
||||
|
||||
static from(type, z) {
|
||||
return new Point(type, z.x || 0, z.y || 0);
|
||||
}
|
||||
static fromXY(type, x, y) {
|
||||
return new Point(type, x || 0, y || 0);
|
||||
}
|
||||
static transformed(tfm, z) {
|
||||
return Point.transformedXY(tfm, z.type, z.x, z.y);
|
||||
}
|
||||
static transformedXY(tfm, type, x, y) {
|
||||
return new Point(
|
||||
type,
|
||||
x * tfm.xx + y * tfm.yx + tfm.x || 0,
|
||||
x * tfm.xy + y * tfm.yy + tfm.y || 0
|
||||
);
|
||||
}
|
||||
static translated(z, dx, dy) {
|
||||
return new Point(z.type, z.x + dx || 0, z.y + dy || 0);
|
||||
}
|
||||
}
|
||||
|
||||
Point.Type = {
|
||||
Corner: 0,
|
||||
CubicStart: 1,
|
||||
CubicEnd: 2,
|
||||
Quadratic: 3
|
||||
};
|
||||
|
||||
module.exports = Point;
|
239
font-src/support/geometry/spiro-expand.js
Normal file
239
font-src/support/geometry/spiro-expand.js
Normal file
|
@ -0,0 +1,239 @@
|
|||
"use strict";
|
||||
|
||||
const SpiroJs = require("spiro");
|
||||
|
||||
const { linreg } = require("../utils");
|
||||
const Format = require("../util/formatter");
|
||||
|
||||
class BiKnot {
|
||||
constructor(type, x, y, d1, d2) {
|
||||
this.type = type;
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.d1 = d1;
|
||||
this.d2 = d2;
|
||||
this.origTangent = null;
|
||||
this.proposedNormal = null;
|
||||
this.unimportant = 0;
|
||||
}
|
||||
clone() {
|
||||
const k1 = new BiKnot(this.type, this.x, this.y, this.d1, this.d2);
|
||||
k1.origTangent = this.origTangent;
|
||||
k1.proposedNormal = this.proposedNormal;
|
||||
k1.unimportant = this.unimportant;
|
||||
return k1;
|
||||
}
|
||||
|
||||
toShapeString() {
|
||||
return Format.tuple(
|
||||
this.type,
|
||||
Format.n(this.x),
|
||||
Format.n(this.y),
|
||||
this.d1 == null ? "" : Format.n(this.d1),
|
||||
this.d2 == null ? "" : Format.n(this.d2),
|
||||
this.origTangent
|
||||
? Format.tuple(Format.n(this.origTangent.x), Format.n(this.origTangent.y))
|
||||
: "",
|
||||
this.proposedNormal
|
||||
? Format.tuple(Format.n(this.proposedNormal.x), Format.n(this.proposedNormal.y))
|
||||
: "",
|
||||
this.unimportant
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class BiKnotCollector {
|
||||
constructor(gizmo, contrast) {
|
||||
this.gizmo = gizmo;
|
||||
this.contrast = contrast;
|
||||
|
||||
this.controlKnots = [];
|
||||
this.defaultD1 = 0;
|
||||
this.defaultD2 = 0;
|
||||
}
|
||||
|
||||
pushKnot(type, x, y) {
|
||||
const tfZ = this.gizmo.applyXY(x, y);
|
||||
const k0 = this.controlKnots[this.controlKnots.length - 1];
|
||||
if (k0) {
|
||||
this.controlKnots.push(new BiKnot(type, tfZ.x, tfZ.y, k0.d1, k0.d2));
|
||||
} else {
|
||||
this.controlKnots.push(new BiKnot(type, tfZ.x, tfZ.y, this.defaultD1, this.defaultD2));
|
||||
}
|
||||
}
|
||||
|
||||
setWidth(l, r) {
|
||||
const k0 = this.controlKnots[this.controlKnots.length - 1];
|
||||
if (k0) {
|
||||
(k0.d1 = l), (k0.d2 = r);
|
||||
} else {
|
||||
(this.defaultD1 = l), (this.defaultD2 = r);
|
||||
}
|
||||
}
|
||||
headsTo(direction) {
|
||||
const k0 = this.controlKnots[this.controlKnots.length - 1];
|
||||
if (k0) k0.proposedNormal = direction;
|
||||
}
|
||||
setType(type) {
|
||||
const k0 = this.controlKnots[this.controlKnots.length - 1];
|
||||
if (k0) k0.type = type;
|
||||
}
|
||||
setUnimportant() {
|
||||
const k0 = this.controlKnots[this.controlKnots.length - 1];
|
||||
if (k0) k0.unimportant = 1;
|
||||
}
|
||||
}
|
||||
|
||||
class SpiroExpander {
|
||||
constructor(gizmo, contrast, closed, cks) {
|
||||
this.gizmo = gizmo;
|
||||
this.contrast = contrast;
|
||||
this.closed = closed;
|
||||
this.controlKnots = cks;
|
||||
}
|
||||
|
||||
initializeNormals() {
|
||||
const normalRectifier = new NormalRectifier(this.controlKnots, this.gizmo);
|
||||
SpiroJs.spiroToArcsOnContext(this.controlKnots, this.closed, normalRectifier);
|
||||
}
|
||||
|
||||
iterateNormals() {
|
||||
const centerBone = this.getPass2Knots();
|
||||
const normalRectifier = new NormalRectifier(this.controlKnots, this.gizmo);
|
||||
SpiroJs.spiroToArcsOnContext(centerBone, this.closed, normalRectifier);
|
||||
}
|
||||
getPass2Knots() {
|
||||
const expanded = this.expand(this.contrast);
|
||||
const middles = [];
|
||||
for (let j = 0; j < this.controlKnots.length; j++) {
|
||||
const lhs = this.gizmo.unapply(expanded.lhs[j]);
|
||||
const rhs = this.gizmo.unapply(expanded.rhs[j]);
|
||||
middles[j] = {
|
||||
x: 0.5 * (lhs.x + rhs.x),
|
||||
y: 0.5 * (lhs.y + rhs.y),
|
||||
type: this.controlKnots[j].type,
|
||||
unimportant: this.controlKnots[j].unimportant
|
||||
};
|
||||
}
|
||||
return middles;
|
||||
}
|
||||
|
||||
expand() {
|
||||
const lhs = [],
|
||||
rhs = [];
|
||||
// Initialize knots
|
||||
for (let j = 0; j < this.controlKnots.length; j++) {
|
||||
const knot = this.controlKnots[j];
|
||||
lhs[j] = {
|
||||
type: knot.type,
|
||||
unimportant: knot.unimportant,
|
||||
x: 0,
|
||||
y: 0
|
||||
};
|
||||
rhs[j] = {
|
||||
type: reverseKnotType(knot.type),
|
||||
unimportant: knot.unimportant,
|
||||
x: 0,
|
||||
y: 0
|
||||
};
|
||||
}
|
||||
|
||||
// Create important knots
|
||||
for (let j = 0; j < this.controlKnots.length; j++) {
|
||||
const knot = this.controlKnots[j];
|
||||
if (knot.unimportant) continue;
|
||||
|
||||
let dx, dy;
|
||||
if (knot.proposedNormal) {
|
||||
dx = knot.proposedNormal.x;
|
||||
dy = knot.proposedNormal.y;
|
||||
} else {
|
||||
dx = normalX(knot.origTangent, this.contrast);
|
||||
dy = normalY(knot.origTangent, this.contrast);
|
||||
}
|
||||
lhs[j].x = knot.x + knot.d1 * dx;
|
||||
lhs[j].y = knot.y + knot.d1 * dy;
|
||||
|
||||
rhs[j].x = knot.x - knot.d2 * dx;
|
||||
rhs[j].y = knot.y - knot.d2 * dy;
|
||||
}
|
||||
this.interpolateUnimportantKnots(lhs, rhs);
|
||||
return { lhs, rhs };
|
||||
}
|
||||
|
||||
interpolateUnimportantKnots(lhs, rhs) {
|
||||
for (let j = 0; j < this.controlKnots.length; j++) {
|
||||
const knot = this.controlKnots[j];
|
||||
if (!knot.unimportant) continue;
|
||||
let jBefore, jAfter;
|
||||
for (jBefore = j - 1; cyNth(this.controlKnots, jBefore).unimportant; jBefore--);
|
||||
for (jAfter = j + 1; cyNth(this.controlKnots, jAfter).unimportant; jAfter++);
|
||||
|
||||
const knotBefore = this.gizmo.unapply(cyNth(this.controlKnots, jBefore)),
|
||||
knotAfter = this.gizmo.unapply(cyNth(this.controlKnots, jAfter)),
|
||||
ref = this.gizmo.unapply(knot),
|
||||
lhsBefore = this.gizmo.unapply(cyNth(lhs, jBefore)),
|
||||
lhsAfter = this.gizmo.unapply(cyNth(lhs, jAfter)),
|
||||
rhsBefore = this.gizmo.unapply(cyNth(rhs, jBefore)),
|
||||
rhsAfter = this.gizmo.unapply(cyNth(rhs, jAfter));
|
||||
|
||||
const lhsTf = this.gizmo.applyXY(
|
||||
linreg(knotBefore.x, lhsBefore.x, knotAfter.x, lhsAfter.x, ref.x),
|
||||
linreg(knotBefore.y, lhsBefore.y, knotAfter.y, lhsAfter.y, ref.y)
|
||||
);
|
||||
const rhsTf = this.gizmo.applyXY(
|
||||
linreg(knotBefore.x, rhsBefore.x, knotAfter.x, rhsAfter.x, ref.x),
|
||||
linreg(knotBefore.y, rhsBefore.y, knotAfter.y, rhsAfter.y, ref.y)
|
||||
);
|
||||
|
||||
(lhs[j].x = lhsTf.x), (lhs[j].y = lhsTf.y);
|
||||
(rhs[j].x = rhsTf.x), (rhs[j].y = rhsTf.y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class NormalRectifier {
|
||||
constructor(stage1ControlKnots, gizmo) {
|
||||
this.gizmo = gizmo;
|
||||
this.controlKnots = stage1ControlKnots;
|
||||
this.nKnotsProcessed = 0;
|
||||
}
|
||||
|
||||
beginShape() {}
|
||||
endShape() {}
|
||||
moveTo(x, y) {
|
||||
this.nKnotsProcessed += 1;
|
||||
}
|
||||
arcTo(arc, x, y) {
|
||||
if (this.nKnotsProcessed === 1) {
|
||||
const d = this.gizmo.applyOffsetXY(arc.deriveX0, arc.deriveY0);
|
||||
if (isTangentValid(d)) this.controlKnots[0].origTangent = d;
|
||||
else throw new Error("NaN angle detected.");
|
||||
}
|
||||
if (this.controlKnots[this.nKnotsProcessed]) {
|
||||
const d = this.gizmo.applyOffsetXY(arc.deriveX1, arc.deriveY1);
|
||||
if (isTangentValid(d)) this.controlKnots[this.nKnotsProcessed].origTangent = d;
|
||||
else throw new Error("NaN angle detected.");
|
||||
}
|
||||
this.nKnotsProcessed += 1;
|
||||
}
|
||||
}
|
||||
|
||||
function isTangentValid(d) {
|
||||
return isFinite(d.x) && isFinite(d.y);
|
||||
}
|
||||
function normalX(tangent, contrast) {
|
||||
return contrast * (-tangent.y / Math.hypot(tangent.x, tangent.y));
|
||||
}
|
||||
function normalY(tangent) {
|
||||
return tangent.x / Math.hypot(tangent.x, tangent.y);
|
||||
}
|
||||
function reverseKnotType(ty) {
|
||||
return ty === "left" ? "right" : ty === "right" ? "left" : ty;
|
||||
}
|
||||
function cyNth(a, j) {
|
||||
return a[j % a.length];
|
||||
}
|
||||
|
||||
exports.BiKnotCollector = BiKnotCollector;
|
||||
exports.SpiroExpander = SpiroExpander;
|
70
font-src/support/geometry/transform.js
Normal file
70
font-src/support/geometry/transform.js
Normal file
|
@ -0,0 +1,70 @@
|
|||
"use strict";
|
||||
|
||||
module.exports = class Transform {
|
||||
constructor(xx, yx, xy, yy, x, y) {
|
||||
this.xx = xx;
|
||||
this.yx = yx;
|
||||
this.xy = xy;
|
||||
this.yy = yy;
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
static Id() {
|
||||
return new Transform(1, 0, 0, 1, 0, 0);
|
||||
}
|
||||
|
||||
static Translate(x, y) {
|
||||
return new Transform(1, 0, 0, 1, x, y);
|
||||
}
|
||||
|
||||
apply(pt) {
|
||||
return this.applyXY(pt.x, pt.y);
|
||||
}
|
||||
applyXY(x, y) {
|
||||
return {
|
||||
x: x * this.xx + y * this.yx + this.x,
|
||||
y: x * this.xy + y * this.yy + this.y
|
||||
};
|
||||
}
|
||||
applyOffset(delta) {
|
||||
return this.applyOffsetXY(delta.x, delta.y);
|
||||
}
|
||||
applyOffsetXY(deltaX, deltaY) {
|
||||
return {
|
||||
x: deltaX * this.xx + deltaY * this.yx,
|
||||
y: deltaX * this.xy + deltaY * this.yy
|
||||
};
|
||||
}
|
||||
unapply(pt) {
|
||||
const xx = pt.x - this.x;
|
||||
const yy = pt.y - this.y;
|
||||
const denom = this.xx * this.yy - this.xy * this.yx;
|
||||
return {
|
||||
x: (xx * this.yy - yy * this.yx) / denom,
|
||||
y: (yy * this.xx - xx * this.xy) / denom
|
||||
};
|
||||
}
|
||||
inverse() {
|
||||
const denom = this.xx * this.yy - this.xy * this.yx;
|
||||
return new Transform(
|
||||
this.yy / denom,
|
||||
-this.yx / denom,
|
||||
-this.xy / denom,
|
||||
this.xx / denom,
|
||||
-(this.x * this.yy - this.y * this.yx) / denom,
|
||||
-(-this.x * this.xy + this.y * this.xx) / denom
|
||||
);
|
||||
}
|
||||
|
||||
toString() {
|
||||
return `[[${this.xx} ${this.xy}] [${this.yx} ${this.yy}]] + [[${this.x}] [${this.y}]]`;
|
||||
}
|
||||
|
||||
static isTranslate(tfm) {
|
||||
return tfm.xx === 1 && tfm.yy === 1 && tfm.xy === 0 && tfm.yx === 0;
|
||||
}
|
||||
static isIdentity(tfm) {
|
||||
return this.isTranslate(tfm) && tfm.x === 0 && tfm.y === 0;
|
||||
}
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue