diff --git a/src/app/components/Grid/compound-link/compound-link.component.ts b/src/app/components/Grid/compound-link/compound-link.component.ts index 0164631b..582ce87b 100644 --- a/src/app/components/Grid/compound-link/compound-link.component.ts +++ b/src/app/components/Grid/compound-link/compound-link.component.ts @@ -12,6 +12,7 @@ import { UnitConversionService } from 'src/app/services/unit-conversion.service' import { NotificationService } from 'src/app/services/notification.service'; import { AnimationService } from 'src/app/services/animation.service'; +import {UndoRedoService} from "../../../services/undo-redo.service"; @Component({ selector: '[app-compound-link]', @@ -27,13 +28,14 @@ export class CompoundLinkComponent extends AbstractInteractiveComponent { private svgPathService: SVGPathService, private unitConversionService: UnitConversionService, private notificationService: NotificationService, - public override animationService: AnimationService + public override animationService: AnimationService, + private undoRedoService: UndoRedoService ) { super(interactionService,animationService); } override registerInteractor(): Interactor { - return new CompoundLinkInteractor(this.compoundLink, this.stateService, this.notificationService); + return new CompoundLinkInteractor(this.compoundLink, this.stateService, this.notificationService, this.undoRedoService); } getColor():string{ @@ -49,7 +51,8 @@ export class CompoundLinkComponent extends AbstractInteractiveComponent { let allCoordsAsArray: Coord[] = Array.from(allUniqueJointCoords,(coord,index) =>{ return this.unitConversionService.modelCoordToSVGCoord(coord); }); - return this.svgPathService.getSingleLinkDrawnPath(allCoordsAsArray, radius); + return this.svgPathService.calculateCompoundPath(this.compoundLink.links, allCoordsAsArray, radius); + //return this.svgPathService.getSingleLinkDrawnPath(allCoordsAsArray, radius); } getStrokeColor(): string{ if (this.getInteractor().isSelected) { diff --git a/src/app/components/Grid/graph/graph.component.html b/src/app/components/Grid/graph/graph.component.html index 6cc560c4..422ff9c8 100644 --- a/src/app/components/Grid/graph/graph.component.html +++ b/src/app/components/Grid/graph/graph.component.html @@ -9,7 +9,7 @@ - + diff --git a/src/app/components/Grid/graph/graph.component.ts b/src/app/components/Grid/graph/graph.component.ts index cf7eee07..7e4badfd 100644 --- a/src/app/components/Grid/graph/graph.component.ts +++ b/src/app/components/Grid/graph/graph.component.ts @@ -113,6 +113,15 @@ export class GraphComponent { return Array.from(this.stateService.getMechanism().getTrajectories()); } + public getOneJoint(id: number): Joint | null { + for (const joint of this.stateService.getMechanism().getJoints()) { + if (joint.id === id) { + return joint; + } + } + return null; + } + public getLinks(): Link[] { return Array.from(this.stateService.getMechanism().getIndependentLinks()); } diff --git a/src/app/components/Grid/joint/joint.component.css b/src/app/components/Grid/joint/joint.component.css index 7492bee3..f4906834 100644 --- a/src/app/components/Grid/joint/joint.component.css +++ b/src/app/components/Grid/joint/joint.component.css @@ -9,5 +9,5 @@ .weld-icon{ padding: 3px; - filter: invert(100%) sepia(100%) saturate(1%) hue-rotate(343deg) brightness(102%) contrast(101%); + filter: sepia(100%) saturate(1%) hue-rotate(343deg) brightness(102%) contrast(101%); } diff --git a/src/app/components/Grid/joint/joint.component.html b/src/app/components/Grid/joint/joint.component.html index 00baab50..e40eeec2 100644 --- a/src/app/components/Grid/joint/joint.component.html +++ b/src/app/components/Grid/joint/joint.component.html @@ -52,27 +52,7 @@ /> } - - @if(isWelded()){ - - - - - - } - @if (isRef() && !this.joint.isHidden){ + @if (isRef() && !this.joint.isHidden && !this.joint.isTracer){ + } @else if (this.joint.isTracer && !this.joint.isGrounded) { + + } @else { + + } + + @if(isWelded()){ + + + + + } - @else { - - } @if(isHovered() || isSelected() || showIDLabels) { diff --git a/src/app/components/Grid/link/link.component.ts b/src/app/components/Grid/link/link.component.ts index b3f0659c..300309fa 100644 --- a/src/app/components/Grid/link/link.component.ts +++ b/src/app/components/Grid/link/link.component.ts @@ -447,7 +447,36 @@ export class LinkComponent coord = this.unitConversionService.modelCoordToSVGCoord(coord); allCoords.push(coord); } - return this.svgPathService.getSingleLinkDrawnPath(allCoords, radius); + if (this.link.isCircle) { + let pathStr = ''; + let i: number = 0; + let foundInput: boolean = false; + let inputCoords: Coord; + for (const [jID, currentJ] of this.link.joints) { + if (!foundInput && currentJ.isGrounded) { + inputCoords = this.unitConversionService.modelCoordToSVGCoord(currentJ.coords); + pathStr = this.svgPathService.getCircularLink(inputCoords, this.farthestPoint(currentJ.coords), 18 + 10); + foundInput = true; + } + } + + return pathStr; + + } else { + return this.svgPathService.getSingleLinkDrawnPath(allCoords, radius); + } + + } + + farthestPoint(center: Coord): number { + let radius: number = 0; + for (const [jID, currentJ] of this.link.joints) { + const currentR = center.getDistanceTo(currentJ.coords); + if (currentR > radius) { + radius = currentR; + } + } + return radius; } getLengthSVG() { diff --git a/src/app/components/Grid/trajectory/trajectory.component.html b/src/app/components/Grid/trajectory/trajectory.component.html index 31e3f3ed..1fce83e1 100644 --- a/src/app/components/Grid/trajectory/trajectory.component.html +++ b/src/app/components/Grid/trajectory/trajectory.component.html @@ -1,11 +1,28 @@ - + } @else { + + } + diff --git a/src/app/components/Grid/trajectory/trajectory.component.ts b/src/app/components/Grid/trajectory/trajectory.component.ts index bd36c385..205d1232 100644 --- a/src/app/components/Grid/trajectory/trajectory.component.ts +++ b/src/app/components/Grid/trajectory/trajectory.component.ts @@ -2,6 +2,7 @@ import { Component, Input } from '@angular/core'; import { Coord } from 'src/app/model/coord'; import { SVGPathService } from 'src/app/services/svg-path.service'; import { UnitConversionService } from 'src/app/services/unit-conversion.service'; +import {Joint} from "../../../model/joint"; @Component({ selector: '[app-trajectory]', @@ -11,6 +12,7 @@ import { UnitConversionService } from 'src/app/services/unit-conversion.service' export class TrajectoryComponent { @Input() trajectory!: Coord[]; + @Input() joint!: Joint | null; constructor( private svgPathService: SVGPathService, diff --git a/src/app/components/ToolBar/undo-redo-panel/action.ts b/src/app/components/ToolBar/undo-redo-panel/action.ts index 7d0a5076..1bef510a 100644 --- a/src/app/components/ToolBar/undo-redo-panel/action.ts +++ b/src/app/components/ToolBar/undo-redo-panel/action.ts @@ -24,6 +24,11 @@ export interface Action { linkTracerData?: LinkTracerSnapshot; linkForceData?: LinkForceSnapshot; + compoundLinkData?: CompoundLinkSnapshot; + compoundLinkDataArray?: CompoundLinkSnapshot[]; + compoundExtraLinkData?: LinkSnapshot[]; + compoundExtraJointsData?: JointSnapshot[][]; + oldJointPositions?: Array<{ jointId: number; coords: { x: number; y: number }; @@ -49,6 +54,15 @@ export interface Action { newAngle?: number; } +export interface CompoundLinkSnapshot { + id: number; + linkIds: number[]; + name: string; + mass: number; + locked: boolean; + color: string; +} + export interface LinkSnapshot { id: number; jointIds: number[]; @@ -57,6 +71,7 @@ export interface LinkSnapshot { angle: number; locked: boolean; color: string; + isCircle?: boolean; } export interface JointSnapshot { @@ -72,6 +87,8 @@ export interface JointSnapshot { locked: boolean; isHidden: boolean; isReference: boolean; + isTracer: boolean; + isPartOfWelded: boolean; } export interface LinkLockSnapshot { @@ -94,6 +111,8 @@ export interface LinkTracerSnapshot { linkId: number; jointId: number; coords: { x: number; y: number }; + tracerModelPos?: Coord; + tracerSVGPos?: Coord; } export interface LinkForceSnapshot { diff --git a/src/app/controllers/compound-link-interactor.ts b/src/app/controllers/compound-link-interactor.ts index b238bd8a..cbed1c2b 100644 --- a/src/app/controllers/compound-link-interactor.ts +++ b/src/app/controllers/compound-link-interactor.ts @@ -6,6 +6,8 @@ import { Mechanism } from "../model/mechanism"; import { StateService } from "../services/state.service"; import { ContextMenuOption, Interactor } from "./interactor"; import { NotificationService } from "../services/notification.service"; +import type {CompoundLinkSnapshot, JointSnapshot, LinkSnapshot} from "../components/ToolBar/undo-redo-panel/action"; +import {UndoRedoService} from "../services/undo-redo.service"; /* This interactor defines the following behaviors: @@ -17,7 +19,7 @@ export class CompoundLinkInteractor extends Interactor { private jointsStartPosModel: Map = new Map(); private lastNotificationTime = 0; - constructor(public compoundLink: CompoundLink, private stateService: StateService, private notificationService: NotificationService) { + constructor(public compoundLink: CompoundLink, private stateService: StateService, private notificationService: NotificationService, private undoRedoService: UndoRedoService) { super(true, true); this.onDragStart$.subscribe(() => { @@ -77,6 +79,7 @@ export class CompoundLinkInteractor extends Interactor { } const mechanism: Mechanism = this.stateService.getMechanism(); let modelPosAtRightClick = this.getMousePos().model; + let svgPosAtRightClick = this.getMousePos().svg; availableContext.push( { icon: "assets/contextMenuIcons/addLink.svg", @@ -87,7 +90,40 @@ export class CompoundLinkInteractor extends Interactor { { icon: "assets/contextMenuIcons/addTracer.svg", label: "Attach Tracer Point", - action: () => { mechanism.addJointToLink(this.compoundLink.id, modelPosAtRightClick) }, + action: () => { + // snapshot existing joint‐IDs + let beforeIds: number[] = []; + Array.from(this.compoundLink.links.values()).forEach((l) => { + Array.from(l.joints.keys()).forEach((jID) => { + beforeIds.push(jID); + }) + }); + + mechanism.addTracerPointWelded(this.compoundLink.id, modelPosAtRightClick, svgPosAtRightClick) + + // find exactly which joint is new + let afterIds: number[] = []; + Array.from(this.compoundLink.links.values()).forEach((l) => { + Array.from(l.joints.keys()).forEach((jID) => { + afterIds.push(jID); + }) + }); + const newId = afterIds.find((id) => !beforeIds.includes(id))!; + const newJoint = mechanism.getJoint(newId); + newJoint.isTracer = true; + + // recordAction with a real jointId + this.undoRedoService.recordAction({ + type: 'addTracerCompound', + linkTracerData: { + linkId: this.compoundLink.id, + jointId: newId, + coords: { x: newJoint.coords.x, y: newJoint.coords.y }, + tracerModelPos: modelPosAtRightClick, + tracerSVGPos: svgPosAtRightClick, + } + }); + }, disabled: false }, { @@ -97,15 +133,72 @@ export class CompoundLinkInteractor extends Interactor { disabled: false }, { - label: this.compoundLink.lock ? "Unlock Compound Link" : "Lock Compound Link", + label: this.compoundLink.lock ? "Unlock Welded Link" : "Lock Welded Link", icon: this.compoundLink.lock ? "assets/contextMenuIcons/unlock.svg" : "assets/contextMenuIcons/lock.svg", action: () => { this.compoundLink.lock = (!this.compoundLink.lock) }, disabled: false }, { icon: "assets/contextMenuIcons/trash.svg", - label: "Delete Link", - action: () => { mechanism.removeLink(this.compoundLink.id) }, + label: "Delete Welded Link", + action: () => { + + const compoundLinkData: CompoundLinkSnapshot = { + id: this.compoundLink.id, + linkIds: Array.from(this.compoundLink.links.values()).map((l) => l.id), + name: this.compoundLink.name, + mass: this.compoundLink.mass, + locked: this.compoundLink.lock, + color: this.compoundLink.color, + } + + const linkData: LinkSnapshot[] = Array.from( + this.compoundLink.links.values() + ).map((l) => ({ + id: l.id, + jointIds: Array.from(l.joints.values()).map((j) => j.id), + name: l.name, + mass: l.mass, + angle: l.angle, + locked: l.locked, + color: l.color, + isCircle: l.isCircle, + })); + + let extraJointsData: JointSnapshot[][] = Array.from( + this.compoundLink.links.values()).map((l, index) => ( + Array.from( + l.joints.values() + ).map((j) => ({ + id: j.id, + coords: { x: j.coords.x, y: j.coords.y }, + name: j.name, + type: j.type, + angle: j.angle, + isGrounded: j.isGrounded, + isWelded: j.isWelded, + isInput: j.isInput, + inputSpeed: j.speed, + locked: j.locked, + isHidden: j.hidden, + isReference: j.reference, + isTracer: j.isTracer, + isPartOfWelded: j.isPartOfWelded, + })) + )); + + this.undoRedoService.recordAction({ + type: "deleteCompoundLink", + compoundLinkData, + compoundExtraLinkData: linkData, + compoundExtraJointsData: extraJointsData, + }); + + + mechanism.removeCompoundLinkByID(this.compoundLink.id) + this.stateService.getMechanism().notifyChange(); + + }, disabled: false }, ); diff --git a/src/app/controllers/joint-interactor.ts b/src/app/controllers/joint-interactor.ts index 0241a7fa..e22e99d7 100644 --- a/src/app/controllers/joint-interactor.ts +++ b/src/app/controllers/joint-interactor.ts @@ -5,7 +5,7 @@ import { InteractionService } from '../services/interaction.service'; import { StateService } from '../services/state.service'; import { CreateLinkFromJointCapture } from './click-capture/create-link-from-joint-capture'; import { ContextMenuOption, Interactor } from './interactor'; -import { Action } from '../components/ToolBar/undo-redo-panel/action'; +import {Action, CompoundLinkSnapshot} from '../components/ToolBar/undo-redo-panel/action'; import { NotificationService } from '../services/notification.service'; import { UndoRedoService } from '../services/undo-redo.service'; import {PositionSolverService} from "../services/kinematic-solver.service"; @@ -142,6 +142,8 @@ export class JointInteractor extends Interactor { locked: prevJoint.locked, isHidden: prevJoint.isHidden, isReference: prevJoint.isReference, + isTracer: prevJoint.isTracer, + isPartOfWelded: prevJoint.isPartOfWelded, }; let prevLinksData = connectedLinks.map((link) => { @@ -340,6 +342,8 @@ export class JointInteractor extends Interactor { const mechanism = this.stateService.getMechanism(); const jointToDelete = mechanism.getJoint(this.joint.id); + + const jointData = { id: jointToDelete.id, coords: { x: jointToDelete.coords.x, y: jointToDelete.coords.y }, @@ -353,6 +357,8 @@ export class JointInteractor extends Interactor { locked: jointToDelete.locked, isHidden: jointToDelete.isHidden, isReference: jointToDelete.isReference, + isTracer: jointToDelete.isTracer, + isPartOfWelded: jointToDelete.isPartOfWelded, }; const connectedLinks = @@ -373,6 +379,22 @@ export class JointInteractor extends Interactor { }; }); + const connectedWeldedLinks = mechanism.getConnectedCompoundLinks(jointToDelete); + const weldedlinksData: CompoundLinkSnapshot[] = connectedWeldedLinks.map((clink) => { + // If link.joints is a Map, convert to Array first + const linkIdsArray: number[] = Array.from(clink.links.values()).map( + (l) => l.id + ); + return { + id: clink.id, + linkIds: linkIdsArray, + name: clink.name, + mass: clink.mass, + locked: clink.lock, + color: clink.color, + }; + }); + const allJointsSnapshot = mechanism .getArrayOfJoints() .filter((j) => j.id !== jointToDelete.id) @@ -389,6 +411,8 @@ export class JointInteractor extends Interactor { locked: j.locked, isHidden: j.isHidden, isReference: j.isReference, + isTracer: j.isTracer, + isPartOfWelded: j.isPartOfWelded, })); new Set(mechanism.getArrayOfJoints().map((j) => j.id)); mechanism.removeJoint(this.joint.id); @@ -404,6 +428,7 @@ export class JointInteractor extends Interactor { jointId: jointToDelete.id, jointData, linksData, + compoundLinkDataArray: weldedlinksData, extraJointsData: extraJointsSnapshots, }; @@ -426,6 +451,7 @@ export class JointInteractor extends Interactor { // ── after ── capture.onClick$.subscribe((mousePos) => { const mech = this.stateService.getMechanism(); + this.joint.isTracer = false; // make the link (and possibly a new joint) const hovered = capture.getHoveringJoint(); @@ -459,6 +485,8 @@ export class JointInteractor extends Interactor { locked: newJoint.locked, isHidden: newJoint.hidden, isReference: newJoint.reference, + isTracer: newJoint.isTracer, + isPartOfWelded: newJoint.isPartOfWelded, }); } diff --git a/src/app/controllers/link-interactor.ts b/src/app/controllers/link-interactor.ts index 84a634b3..3169ea31 100644 --- a/src/app/controllers/link-interactor.ts +++ b/src/app/controllers/link-interactor.ts @@ -219,6 +219,7 @@ export class LinkInteractor extends Interactor { locked: j.locked, isHidden: j.hidden, isReference: j.reference, + isTracer: j.isTracer, }; }); @@ -226,6 +227,8 @@ export class LinkInteractor extends Interactor { (js) => js.coords.x === start.x && js.coords.y === start.y )!.id; + mech.getJoint(attachJointId).isTracer = false; + this.undoRedoService.recordAction({ type: 'addLinkToLink', parentLinkId: this.link.id, @@ -258,6 +261,7 @@ export class LinkInteractor extends Interactor { const afterIds = Array.from(this.link.joints.keys()); const newId = afterIds.find((id) => !beforeIds.includes(id))!; const newJoint = this.link.joints.get(newId)!; + newJoint.isTracer = true; // recordAction with a real jointId this.undoRedoService.recordAction({ @@ -313,6 +317,7 @@ export class LinkInteractor extends Interactor { angle: this.link.angle, locked: this.link.locked, color: this.link.color, + isCircle: this.link.isCircle, }; const extraJointsData: JointSnapshot[] = Array.from( @@ -330,6 +335,8 @@ export class LinkInteractor extends Interactor { locked: j.locked, isHidden: j.hidden, isReference: j.reference, + isTracer: j.isTracer, + isPartOfWelded: j.isPartOfWelded, })); this.undoRedoService.recordAction({ @@ -345,6 +352,36 @@ export class LinkInteractor extends Interactor { }, disabled: false, + }, + { + icon: this.link.isCircle + ? 'assets/contextMenuIcons/uncircular.svg' + : 'assets/contextMenuIcons/circleInputLink.svg', + label: this.link.isCircle ? 'Make Bar' : 'Make Circular', + action: () => { + let hasGroundJoint = false; + for (const joint of this.link.getJoints()) { + if (joint.isGrounded) { + hasGroundJoint = true; + } + } + + if (!hasGroundJoint) { + // prevents click if link does not have a ground joint + return; + } + + this.link.isCircle = !this.link.isCircle; + + this.undoRedoService.recordAction({ + //specifies that it only needs the action name and link id + type: "circleLink", + linkId: this.link.id, + }); + + }, + disabled:!this.link.getJoints().some(joint => joint.isGrounded), + } ); } diff --git a/src/app/controllers/svg-interactor.ts b/src/app/controllers/svg-interactor.ts index 4cd36859..eed92661 100644 --- a/src/app/controllers/svg-interactor.ts +++ b/src/app/controllers/svg-interactor.ts @@ -93,7 +93,9 @@ export class SvgInteractor extends Interactor { inputSpeed: j.speed, locked: j.locked, isHidden: j.hidden, - isReference:j.reference + isReference:j.reference, + isTracer: j.isTracer, + isPartOfWelded: j.isPartOfWelded, }; }); @@ -105,7 +107,8 @@ export class SvgInteractor extends Interactor { mass: createdLink.mass, angle: createdLink.angle, locked: createdLink.locked, - color: createdLink.color + color: createdLink.color, + isCircle: createdLink.isCircle, }; this.undoRedoService.recordAction({ diff --git a/src/app/model/joint.ts b/src/app/model/joint.ts index c4282c95..da8398ea 100644 --- a/src/app/model/joint.ts +++ b/src/app/model/joint.ts @@ -23,6 +23,8 @@ export class Joint { private _isReference: boolean; private _isGenerated: boolean; private _rpmSpeed: number; + private _isTracer: boolean = false; + private _isPartOfWelded: boolean = false; isInput$ = this._isInput.asObservable(); isGrounded$ = this._isGrounded.asObservable(); @@ -119,6 +121,13 @@ export class Joint { get isGenerated(): boolean { return this._isGenerated; } + get isTracer(): boolean { + return this._isTracer; + } + + get isPartOfWelded(): boolean { + return this._isPartOfWelded; + } getInputObservable() { return this.isInput$; @@ -176,6 +185,13 @@ export class Joint { set generated(val: boolean) { this._isGenerated = val; } + set isTracer(val: boolean) { + this._isTracer = val; + } + + set isPartOfWelded(val: boolean) { + this._isPartOfWelded = val; + } //----------------------------Joint Modification with modifying other variables---------------------------- addGround() { diff --git a/src/app/model/link.ts b/src/app/model/link.ts index 790a2879..9317177c 100644 --- a/src/app/model/link.ts +++ b/src/app/model/link.ts @@ -1,6 +1,8 @@ import { Coord } from './coord'; import { Joint } from './joint'; import { Force } from './force'; +import {UnitConversionService} from "../services/unit-conversion.service"; +import {SVGPathService} from "../services/svg-path.service"; export interface RigidBody { getJoints(): Joint[]; @@ -17,6 +19,7 @@ export class Link implements RigidBody { private _color: string = ''; private _isLocked: boolean; private _angle: number; + private _isCircle: boolean = false; // These fields are only for supporting CUSTOM centerOfMass logic private _customCOMAnchors: [number, number] | null = null; // We store IDs so we can find the correct joints later even if we have references. @@ -45,7 +48,7 @@ export class Link implements RigidBody { this._color = this.linkColorOptions[id % this.linkColorOptions.length]; this._isLocked = false; this._angle = 0; - + if (Array.isArray(jointAORJoints)) { jointAORJoints.forEach((joint) => { this._joints.set(joint.id, joint); @@ -117,6 +120,10 @@ export class Link implements RigidBody { return parseFloat(posangle.toFixed(3)); } + get isCircle(): boolean { + return this._isCircle; + } + //setters set name(value: string) { this._name = value; @@ -138,6 +145,10 @@ export class Link implements RigidBody { this._angle = ((value % 360) + 360) % 360; } + set isCircle(value: boolean) { + this._isCircle = value; + } + // In the future, we need to check if this custom center of mass is valid. setCenterOfMass(newX: number, newY: number) { this._centerOfMass.x = newX; @@ -183,7 +194,7 @@ export class Link implements RigidBody { this._customCOMAnchors = null; this._customCOMRadii = null; this._centerOfMass = this.calculateCenterOfMass(); - + // Rebind forces to the new COM definition this.rebindForcesAfterCenterOfMassChanged(); } @@ -262,7 +273,7 @@ export class Link implements RigidBody { } // MQP 24-25: I don't think this works - // MQP 25-26: No, this doesn't work for all shapes, it only works if we have solid filled nice shapes + // MQP 25-26: No, this doesn't work for all shapes, it only works if we have solid filled nice shapes // like triangle, square,... Hope that another MQP can find a better way to calculate it. calculateCenterOfMass(): Coord { @@ -270,20 +281,20 @@ export class Link implements RigidBody { if (!this._customCenterOfMass) { this._centerOfMass = this.calculateCOMUsingAverageMethod(); } - + // if using CUSTOM const customCOM = this.calculateCOMUsingCircleMethod(); if (!customCOM) { // if circle method fails, fallback to avarage method this._centerOfMass = this.calculateCOMUsingAverageMethod(); - + if (!this._COMNeedReset) { // need to reset everything related to custom Center of mass this._customCenterOfMass = false; this._customCOMAnchors = null; this._customCOMRadii = null; - } + } } - + return this._centerOfMass; } @@ -358,25 +369,25 @@ export class Link implements RigidBody { if (d < eps && Math.abs(r0 - r1) < eps) { console.log('two circle centers are almost the same'); return []; - } + } // same center with different radius if (d < eps) { console.log('same center with different radius') - return []; - } + return []; + } // Circles too far apart if (d > r0 + r1 + eps) { console.log('Circles too far apart'); return []; - } + } // one circle inside the other without touching if (d < Math.abs(r0 - r1) - eps) { console.log('one circle inside the other without touching'); return []; - } + } // Compute intersection(s), reading this article at section "Intersection of two circles" https://paulbourke.net/geometry/circlesphere/ const a = (r0 * r0 - r1 * r1 + d * d) / (2 * d); @@ -402,11 +413,11 @@ export class Link implements RigidBody { // console.log('1 intersection between circles'); return [new Coord(point2_x, point2_y)]; } - + const p1 = new Coord(point2_x - vx, point2_y + vy); const p2 = new Coord(point2_x + vx, point2_y - vy); return [p1, p2]; - } + } getMidpoint(joint1: Joint, joint2: Joint): Coord { let x: number; @@ -627,6 +638,42 @@ export class Link implements RigidBody { return false; } } + // checks if coord is in link. Assumes coords have been converted into svg coords from model coords + containsCoord(coord: Coord): boolean { + // below is getting the path string of this link + let joints: IterableIterator = this.joints.values(); + let allCoords: Coord[] = []; + let unitConversionService: UnitConversionService = new UnitConversionService(); + for (let joint of joints) { + let coord: Coord = joint._coords; + coord = unitConversionService.modelCoordToSVGCoord(coord); + allCoords.push(coord); + } + const pathString = new SVGPathService(unitConversionService).getSingleLinkDrawnPath(allCoords, 30); + + // NS below is used to create an SVGPathElement instead of an unknown HTML element, by using namespace + const NS = "http://www.w3.org/2000/svg"; + const path = document.createElementNS(NS, "path"); + + // creating path element from path string that is invisible + path.setAttribute("d", pathString); + path.setAttribute("fill", "black"); + + // checking that svg exists before calculating for whether the coordinate is within the path or not + const svg = document.querySelector('svg'); + if (svg != null) { + path.setAttribute("visibility", "hidden"); + svg?.appendChild(path); + + // check if coord is within the path or not + let pointTwo = new DOMPoint(coord.x, coord.y); + const isInPath = path.isPointInFill(pointTwo); + path.remove(); + return isInPath; + } + return false; + } + moveCoordinates(coord: Coord) { for (const jointID of this._joints.keys()) { const joint = this._joints.get(jointID)!; diff --git a/src/app/model/mechanism.ts b/src/app/model/mechanism.ts index c66d77e2..d5e2481a 100644 --- a/src/app/model/mechanism.ts +++ b/src/app/model/mechanism.ts @@ -725,13 +725,17 @@ export class Mechanism { * * @param {number} linkID * @param {Coord} coord + * @param {isTracer} boolean * @memberof Mechanism */ - addJointToLink(linkID: number, coord: Coord) { + addJointToLink(linkID: number, coord: Coord, isTracer?: boolean) { this.executeLinkAction(linkID, (link) => { let jointA = new Joint(this._jointIDCount, coord); this._jointIDCount++; this._joints.set(jointA.id, jointA); + if (isTracer) { + jointA.isTracer = true; + } link.addTracer(jointA); }); } @@ -752,6 +756,95 @@ export class Mechanism { }); } + /** + * attaches a tracer point(effectively a joint) to an existing link, but this is from a welded link. + * + * @param {number} linkID + * @param {Coord} coord + * @memberof Mechanism + */ + addTracerPointWelded(linkID: number, coordModel: Coord, coordSVG: Coord) { + this.executeLinkAction(linkID, (link) => { + let jointA = new Joint(this._jointIDCount, coordModel); + this._jointIDCount++; + if (this.isMechanismWelded()) { + jointA.isTracer = true; + jointA.isPartOfWelded = true; + } + + let foundLink: boolean = false; + let i: number = 0; + while (i < this._links.size && !foundLink) { + let currentLink = this._links.get(i); + const inLink = currentLink?.containsCoord(coordSVG); + + if (inLink) { + foundLink = true; + } else { + i++; + } + } + + this._joints.set(jointA.id, jointA); + + const theLink = this._links.get(i); + if (foundLink && theLink != undefined) { + theLink.addTracer(jointA); + } else { + link.addTracer(jointA); + } + }); + } + + /** + * attaches a tracer point(effectively a joint) to an existing link, but this is from a welded link. + * + * @param {number} linkID + * @param {Coord} coord + * @memberof Mechanism + */ + _addJointToWelded(linkID: number, jointA: Joint, coordSVG: Coord) { + this.executeLinkAction(linkID, (link) => { + + let foundLink: boolean = false; + let i: number = 0; + while (i < this._links.size && !foundLink) { + let currentLink = this._links.get(i); + const inLink = currentLink?.containsCoord(coordSVG); + + if (inLink) { + foundLink = true; + } else { + i++; + } + } + + this._joints.set(jointA.id, jointA); + + const theLink = this._links.get(i); + if (foundLink && theLink != undefined) { + theLink.addTracer(jointA); + } else { + link.addTracer(jointA); + } + }); + } + + /** + * determines whether the current mechanism is a welded linkage. + * + * @memberof Mechanism + * @return returns true if the mechanism has a welded joint, false otherwise + */ + isMechanismWelded(): boolean { + for (const [id, j] of this._joints) { + if (j.isWelded) { + return true; + } + } + return false; + } + /** * attaches a new link to another link at a point along the existing link which is not a joint. * @@ -882,6 +975,18 @@ export class Mechanism { } } + // first removes every link within the compound link, then the compound link itself; finds compound link from id + public removeCompoundLinkByID(compoundLinkID: number) { + var compoundLink = this._compoundLinks.get(compoundLinkID); + if (compoundLink != undefined) { + for (let link of compoundLink.links.values()) { + this.removeLink(link.id); + } + this._compoundLinks.delete(compoundLink.id); + } + + } + // first removes every link within the compound link, then the compound link itself public removeCompoundLink(compoundLink: CompoundLink) { for (let link of compoundLink.links.values()) { @@ -1129,7 +1234,7 @@ export class Mechanism { } return connectedLinks; } - private getConnectedCompoundLinks(joint: Joint): CompoundLink[] { + getConnectedCompoundLinks(joint: Joint): CompoundLink[] { let connectedCompoundLinks: CompoundLink[] = []; for (let link of this._compoundLinks.values()) { if (link.containsJoint(joint.id)) { diff --git a/src/app/services/decoder.service.ts b/src/app/services/decoder.service.ts index cb529289..5e0c6cbb 100644 --- a/src/app/services/decoder.service.ts +++ b/src/app/services/decoder.service.ts @@ -98,6 +98,8 @@ export class DecoderService { } // Pass the reconstructed mechanism data to the state service. + console.log('fullData: Compound Link'); + console.log(fullData.decodedCompoundLinks); stateService.reconstructMechanism(fullData); if (compactData.u) { @@ -210,6 +212,8 @@ export class DecoderService { isHidden: this.convertBoolean(row[11]), isReference: this.convertBoolean(row[12]), isGenerated: this.convertBoolean(row[13]), + isPartOfWelded: this.convertBoolean(row[14]), + isTracer: this.convertBoolean(row[15]), })); const decodedLinks: any[] = (compactData.l || []).map((row: any[]) => ({ @@ -224,6 +228,7 @@ export class DecoderService { locked: this.convertBoolean(row[8]), length: this.convertNumber(row[9], useDecoding), angle: this.convertNumber(row[10], useDecoding), + isCircle: this.convertBoolean(row[11]), })); const decodedCompoundLinks: any[] = (compactData.c || []).map( diff --git a/src/app/services/encoder.service.ts b/src/app/services/encoder.service.ts index 1f4b7557..0161618f 100644 --- a/src/app/services/encoder.service.ts +++ b/src/app/services/encoder.service.ts @@ -195,6 +195,8 @@ export class EncoderService { j.isHidden, j.isReference, j.isGenerated, + j.isPartOfWelded, + j.isTracer, ]), l: mechanism.getArrayOfLinks().map((l: Link) => [ l.id, @@ -212,6 +214,7 @@ export class EncoderService { l.locked, l.length, l.angle, + l.isCircle, ]), c: mechanism.getArrayOfCompoundLinks().map((cl: CompoundLink) => [ cl.id, diff --git a/src/app/services/state.service.ts b/src/app/services/state.service.ts index 9ae5f47e..cb3aa983 100644 --- a/src/app/services/state.service.ts +++ b/src/app/services/state.service.ts @@ -119,6 +119,8 @@ export class StateService { newJoint.hidden = Boolean(joint.isHidden); newJoint.reference = Boolean(joint.isReference); newJoint.generated = Boolean(joint.isGenerated); + newJoint.isTracer = Boolean(joint.isTracer); + newJoint.isPartOfWelded = Boolean(joint.isPartOfWelded); if (Boolean(joint.isInput)) { newJoint.addInput(); } @@ -145,13 +147,14 @@ export class StateService { for (const x of jointsArray) { console.log(x.id); } - console.log(link, link.id); - let newLink = new Link(link.id, jointsArray); + console.log(link, Number(link.id)); + let newLink = new Link(Number(link.id), jointsArray); newLink.name = link.name; newLink.mass = link.mass; newLink.angle = link.angle; newLink.locked = Boolean(link.locked); newLink.color = link.color; + newLink.isCircle = Boolean(link.isCircle); this.mechanism._addLink(newLink); } @@ -159,6 +162,7 @@ export class StateService { //Compound Links if (rawData.decodedCompoundLinks) { + console.log("WELDED LINKS") for (const compoundlink of rawData.decodedCompoundLinks) { let linksArray: Link[] = compoundlink.links .split('|') diff --git a/src/app/services/svg-path.service.ts b/src/app/services/svg-path.service.ts index 19a0fec9..dadb1788 100644 --- a/src/app/services/svg-path.service.ts +++ b/src/app/services/svg-path.service.ts @@ -1,12 +1,18 @@ import {Injectable} from '@angular/core'; import {Coord} from 'src/app/model/coord' +import {Link} from "../model/link"; +import {UnitConversionService} from "./unit-conversion.service"; +import {arc} from "d3-shape"; +import {intersection} from "d3"; @Injectable({ providedIn: 'root', }) export class SVGPathService { - constructor() {} + private scale = 1; + + constructor(private unitConversionService: UnitConversionService) {}; // Generates an SVG path string representing a single connection based on provided coordinates and radius. getSingleLinkDrawnPath(allCoords: Coord[], radius: number): string{ @@ -23,6 +29,19 @@ export class SVGPathService { return this.calculateConvexPath(hullCoords,radius); } + // draws the circular link for an input link + // radius should be positive and inputCoords should already be converted to SVG + // offset is accounting for joint radius + getCircularLink(inputCoords: Coord, radius: number, offset: number): string { + let pathData = ''; + const convertedRadius = this.unitConversionService.modelNumToSVGNum(radius) + offset; + pathData += `M ${inputCoords.x - convertedRadius},${inputCoords.y} `; // moving to center of the circle + pathData += `A ${convertedRadius},${convertedRadius} 0 1 0 ${inputCoords.x + convertedRadius},${inputCoords.y} `; // drawing first semicircle + pathData += `A ${convertedRadius},${convertedRadius} 0 1 0 ${inputCoords.x - convertedRadius},${inputCoords.y} Z`; // drawing second semicircle + + return pathData; + } + // Creates an SVG path string tracing the trajectory through all provided coordinates with the given radius. getTrajectoryDrawnPath(allCoords: Coord[], radius: number): string { if (allCoords.length === 0) { @@ -53,6 +72,7 @@ export class SVGPathService { return this.calculateConvexPath(hullCoords,radius); } + // Identifies if all coordinates lie on a straight line and returns the endpoints if they are collinear. private findCollinearCoords(coords: Coord[]): Coord[] | undefined { @@ -251,4 +271,962 @@ export class SVGPathService { return pathData; } + // checks if the path string is starting or ending a path; returns true if it is + isNewShape(pathString: string): boolean { + return pathString === '' || pathString.substring(pathString.length - 2) === 'Z '; + } + + // used for creating a concave fillet between two lines + computeArcPointsAndRadius(line1: [Coord, Coord, Coord | null, Link], line2:[Coord, Coord, Coord | null, Link], arcRadius: number):[Coord, Coord, number] { + // modify line1 end point and line2 start point to create an arc between them + const arcOffset = Math.min(arcRadius, line1[0].getDistanceTo(line1[1]) / 2, line2[0].getDistanceTo(line2[1]) / 2); + const line1OffsetPoint:Coord = line1[0].clone() + .subtract(line1[1]) + .normalize() + .multiply(arcOffset) + .add(line1[1]); + + const line2OffsetPoint:Coord = line2[1].clone() + .subtract(line2[0]) + .normalize() + .multiply(arcOffset) + .add(line2[0]); + + // find angle between two lines + const line2Angle = Math.atan2(line2[1].y - line2[0].y, line2[1].x - line2[0].x); + const line1Angle = Math.atan2(line1[1].y - line1[0].y, line1[1].x - line1[0].x); + const angleBetweenLines = line2Angle - line1Angle; + const radius = arcOffset * Math.tan((Math.PI - angleBetweenLines) / 2); + + return [line1OffsetPoint, line2OffsetPoint, radius]; + } + + // recalculates angle to be in between -pi and pi. + angleToPI(line1: [Coord, Coord, Coord | null, Link], line2:[Coord, Coord, Coord | null, Link]) { + // calculate angle of each individual line, given in radians + let line1Angle = Math.atan2(line1[1].y - line1[0].y, line1[1].x - line1[0].x); + let line2Angle = Math.atan2(line2[1].y - line2[0].y, line2[1].x - line2[0].x); + + // angle in between the lines + let angle = line1Angle - line2Angle; + + // recalculate the angle to be between -pi and pi + angle = angle % (2 * Math.PI); + if (angle > Math.PI) { + angle -= 2 * Math.PI; + } + if (angle < -Math.PI) { + angle += 2 * Math.PI; + } + return angle; + } + + // checks if two numbers are loosely equal, bounded by a delta + twoNumsLooselyEquals(a: number, b: number, delta: number = 0.00001): boolean { + return Math.abs(a - b) <= delta; + } + + // checks if a point is on a line + // return true if the point is on the line segment defined by lineStart and lineEnd + // return false otherwise + isPointOnLine(point: Coord, lineStart: Coord, lineEnd: Coord, delta: number = 0.000001): boolean { + // check if the point is within a small range of the line segment. + let range = 0.00001; + if ( + point.x < Math.min(lineStart.x, lineEnd.x) - range || + point.x > Math.max(lineStart.x, lineEnd.x) + range || + point.y < Math.min(lineStart.y, lineEnd.y) - range || + point.y > Math.max(lineStart.y, lineEnd.y) + range + ) { + return false; + } + + // check if three points are collinear + const crossProduct = + (point.x - lineStart.x) * (lineEnd.y - lineStart.y) - + (point.y - lineStart.y) * (lineEnd.x - lineStart.x); + + if (Math.abs(crossProduct) > delta) { + return false; + } + + // point is collinear and in between the range + return true; + } + + // checks if a point is on an arc + // return true if the point is within the arc + // return false if the point is outside the circle or if the point is on the circle, but not within the arc + isPointInArc(intersection: Coord, arcStart: Coord, arcEnd: Coord, arcCenter: Coord, radius: number, delta: number = 0.0001): boolean { + // the arc always goes from the start point to the end point in a clockwise direction. + const dx = intersection.x - arcCenter.x; + const dy = intersection.y - arcCenter.y; + + const dist = Math.sqrt(dx * dx + dy * dy); + + // is point on the circle the arc is on? + if (Math.abs(dist - radius) > delta) { + return false; + } + + // use cross product to determine whether the point is within the arc + const crossProduct1 = + (arcStart.x - arcCenter.x) * (intersection.y - arcCenter.y) - + (arcStart.y - arcCenter.y) * (intersection.x - arcCenter.x); + const crossProduct2 = + (intersection.x - arcCenter.x) * (arcEnd.y - arcCenter.y) - + (intersection.y - arcCenter.y) * (arcEnd.x - arcCenter.x); + + // if the crossProducts are both greater than negative delta, then the point is within the arc + return (crossProduct1 >= -delta && crossProduct2 >= -delta); + } + + // https://stackoverflow.com/questions/13937782/calculating-the-point-of-intersection-of-two-lines + // line intercept math by Paul Bourke http://paulbourke.net/geometry/pointlineplane/ + // Determine the intersection point of two line segments + // Return undefined if the lines don't intersect + lineLineIntersect(line1Start: Coord, line1End: Coord, line2Start: Coord, line2End: Coord): Coord[] | undefined { + let x1 = line1Start.x; + let y1 = line1Start.y; + let x2 = line1End.x; + let y2 = line1End.y; + let x3 = line2Start.x; + let y3 = line2Start.y; + let x4 = line2End.x; + let y4 = line2End.y; + + let delta = 0 + + // check if none of the lines are of length 0 + if ((this.twoNumsLooselyEquals(x1, x2) && this.twoNumsLooselyEquals(y1, y2)) || (this.twoNumsLooselyEquals(x3, x4) && this.twoNumsLooselyEquals(y3, y4))) { + return undefined; + } + + let denominator = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1); + + // check if lines are parallel + if (denominator === 0) { + return undefined; + } + + let ua = ((x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3)) / denominator; + let ub = ((x2 - x1) * (y1 - y3) - (y2 - y1) * (x1 - x3)) / denominator; + + // check if intersection is not within the segments + if (ua <= 0 || ua >= 1 || ub <= 0 || ub >= 1) { + return undefined; + } + + // return Coord with x and y coordinates of intersection + let x = x1 + ua * (x2 - x1); + let y = y1 + ua * (y2 - y1); + + let intersection = new Coord(x, y); + + // checks if the intersection is an end point of the segments + if (intersection.equals(line1Start, this.scale) || + intersection.equals(line1End, this.scale) || + intersection.equals(line2Start, this.scale) || + intersection.equals(line2End, this.scale)) { + return undefined; + } + + return [intersection]; + } + + // return the intersection points between a line and a circle. + // if the line is tangent to the circle, then return the point of tangency. + // if the line does not intersect the circle, return undefined. + lineCircleIntersect(lineStart: Coord, lineEnd: Coord, circleCenter: Coord, circleRadius: number): Coord[] | undefined { + // check if the line is vertical or not + if (this.twoNumsLooselyEquals(lineEnd.x, lineStart.x)) { + // line is vertical so equation is x = c + let c = lineStart.x; + + // find equation of the circle in the form (x - h)^2 + (y - k)^2 = r^2 + let h = circleCenter.x; + let k = circleCenter.y; + let r = circleRadius; + + // substitute x = c into the circle equation and solve for y + // this will give a quadratic equation in the form ay^2 + by + c = 0 + let a = 1; // coefficient of y^2 + let b = -2 * k; // coefficient of y + let d = c - h; // constant term divided by 2 + let e = d * d + k * k - r * r; // constant term + + // find the discriminant of the quadratic equation + // this will determine how many solutions there are + let D = b * b - 4 * a * e; // discriminant + + + if (D < 0) { + // if D is negative, then there are no real solutions and the line does not intersect the circle + return undefined; + } else if (D === 0) { + // if D is zero, then there is one real solution and the line is tangent to the circle + let y = -b / (2 * a); // solution for y + return [new Coord(c, y)]; // return the point of tangency as an array of one coordinate object + } else if (D > 0) { + // if D is positive, then there are two real solutions and the line intersects the circle at two points + let y1 = (-b + Math.sqrt(D)) / (2 * a); // first solution for y + let y2 = (-b - Math.sqrt(D)) / (2 * a); // second solution for y + return [new Coord(c, y1), new Coord(c, y2)]; // return the intersection points as an array of two coordinate objects + } + } else { + let intersections: Coord[] = []; + let slope = (lineStart.y - lineEnd.y) / (lineStart.x - lineEnd.x); + let y_intercept = lineStart.y - slope * lineStart.x; + + let a = 1 + slope * slope; + let b = 2 * slope * (y_intercept - circleCenter.y) - 2 * circleCenter.x; + let c = + circleCenter.x * circleCenter.x + + (y_intercept - circleCenter.y) * (y_intercept - circleCenter.y) - + circleRadius * circleRadius; + + let discriminant = b * b - 4 * a * c; + const tolerance = 0.00001; + if (discriminant < -tolerance) { + // line doesn't touch circle + return undefined; + } else if (discriminant < tolerance) { + // line is tangent to circle + let x = -b / (2 * a); + let y = slope * x + y_intercept; + intersections.push(new Coord(x, y)); + return intersections; + } else { + // line intersects circle in two places + let x1 = (-b + Math.sqrt(discriminant)) / (2 * a); + let y1 = slope * x1 + y_intercept; + intersections.push(new Coord(x1, y1)); + let x2 = (-b - Math.sqrt(discriminant)) / (2 * a); + let y2 = slope * x2 + y_intercept; + intersections.push(new Coord(x2, y2)); + return intersections; + } + } + return undefined; + } + + // return the first intersection point between a line and an arc. The first is the one closest to the lineStart point. + // if the line is tangent to the arc, then return the point of tangency closest to the lineStart point. + // if the line does not intersect the arc, return undefined. + lineArcIntersect(lineStart: Coord, lineEnd: Coord, arcStart: Coord, arcEnd: Coord, arcCenter: Coord, findIntersectionCloseTo: Coord, arcRadius: number): Coord[] | undefined { + // find the intersection points between the line and the circle defined by the arc + let intersections: Coord[] | undefined = this.lineCircleIntersect(lineStart, lineEnd, arcCenter, arcRadius); + + intersections = intersections?.filter((intersection) => { + return this.isPointOnLine(intersection, lineStart, lineEnd); + }); + + if (intersections === undefined || intersections.length === 0) { + return undefined; + } + + // check if the intersection points are within the arc + let returnIntersection: Coord[] = []; + for (let intersection of intersections) { + if ( + this.isPointInArc(intersection, arcStart, arcEnd, arcCenter, arcRadius) + ) { + returnIntersection.push(intersection); + } + } + return returnIntersection; + } + + // return the intersection points between two circles. + // if the circles do not intersect, return undefined. + circleCircleIntersect(center: Coord, radius: number, center2: Coord, radius2: number): [Coord[] | undefined, boolean] { + let x1 = center.x; + let y1 = center.y; + let x2 = center2.x; + let y2 = center2.y; + + let dx = x2 - x1; + let dy = y2 - y1; + let d = Math.sqrt(dx * dx + dy * dy); + + // Circles are separate + if (d > radius + radius2) { + return [undefined, false]; + } + + // One circle is contained within the other + if (d < Math.abs(radius - radius2)) { + return [undefined, false]; + } + + // Circles are coincident + if (d === 0 && radius === radius2) { + return [undefined, true]; + } + + // calculate intersection points + let a = (radius * radius - radius2 * radius2 + d * d) / (2 * d); + let h = Math.sqrt(radius * radius - a * a); + let x3 = x1 + (a * dx) / d; + let y3 = y1 + (a * dy) / d; + let x4 = x3 + (h * dy) / d; + let y4 = y3 - (h * dx) / d; + let x5 = x3 - (h * dy) / d; + let y5 = y3 + (h * dx) / d; + + return [[new Coord(x4, y4), new Coord(x5, y5)], false]; + } + + // return the first intersection point between two arcs, the one closest to the startPosition point + // if the arcs are tangent, then return the point of tangency closest to the startPosition point + // if the arcs do not intersect, return undefined + arcArcIntersect(startPosition: Coord, endPosition: Coord, center: Coord, startPosition2: Coord, endPosition2: Coord, center2: Coord, radius: number): Coord[] | undefined { + // find the intersection points between the two circles defined by the arcs + let [intersections, coincident]: [Coord[] | undefined, boolean] = this.circleCircleIntersect(center, radius, center2, radius); + + if (coincident) { + // circles are coincident, so we need to check if the arcs intersect + // check if the start and end points of the arcs are within the other arc + + let allImportantIntersections: Coord[] = []; + + if (this.isPointInArc(startPosition, startPosition2, endPosition2, center, radius)) { + allImportantIntersections.push(startPosition); + } + if (this.isPointInArc(endPosition, startPosition2, endPosition2, center, radius)) { + allImportantIntersections.push(endPosition); + } + if (this.isPointInArc(startPosition2, startPosition, endPosition, center, radius)) { + allImportantIntersections.push(startPosition2); + } + if (this.isPointInArc(endPosition2, startPosition, endPosition, center, radius)) { + allImportantIntersections.push(endPosition2); + } + + // if there are no intersections, then the arcs do not intersect. + if (allImportantIntersections.length === 0) { + return undefined; + } + + return allImportantIntersections; + } + + // check if no intersections + if (intersections === undefined) { + return undefined; + } + + // check if intersection points are within the arcs + let closestIntersection: Coord | undefined; + + for (let intersection of intersections) { + if ( + this.isPointInArc(intersection, startPosition, endPosition, center, radius) && + this.isPointInArc(intersection, startPosition2, endPosition2, center2, radius) && + !intersection.equals(startPosition, this.scale) && + !intersection.equals(endPosition, this.scale) && + !intersection.equals(startPosition2, this.scale) && + !intersection.equals(endPosition2, this.scale) + ) { + if (!closestIntersection) { + // for first iteration only + closestIntersection = intersection; + } else if (intersection.getDistanceTo(startPosition) < closestIntersection.getDistanceTo(startPosition)) { + closestIntersection = intersection; + } + } + } + + if (closestIntersection === undefined) { + return undefined; + } else { + return [closestIntersection]; + } + } + + // finds intersection between line/arc and line/arc + intersectsWith(line1: [Coord, Coord, Coord | null, Link], line2: [Coord, Coord, Coord | null, Link], radius: number): Coord[] | undefined { + if (line1[2] === null && line2[2] === null) { + // line and line intersection + return this.lineLineIntersect(line1[0], line1[1], line2[0], line2[1]); + } else if (line1[2] === null && line2[2] !== null) { + // line and arc intersection + return this.lineArcIntersect(line1[0], line1[1], line2[0], line2[1], line2[2], line1[0], radius); + } else if (line1[2] !== null && line2[2] === null) { + // arc and line intersection + return this.lineArcIntersect(line2[0], line2[1], line1[0], line1[1], line1[2], line1[0], radius); + } else if (line1[2] !== null && line2[2] !== null) { + // arc and arc intersection + return this.arcArcIntersect(line1[0], line1[1], line1[2], line2[0], line2[1], line2[2], radius); + } else { + // error + return undefined; + } + } + + // splits the intersections by which lines each one was in + groupIntersectionsByPath(lines: [Coord, Coord, Coord | null, Link][], intersections: {point: Coord, i: number}[]): Coord[][] { + return lines.map((_, index) => { + return intersections.filter(p => p.i === index) + .map(p => (p.point)); + }); + } + + // calculates projection of a point on a line + projectPointOnLine(p: Coord, lineStart: Coord, lineEnd: Coord) { + const startEndX = lineEnd.x - lineStart.x; + const startEndY = lineEnd.y - lineStart.y; + + const startPX = p.x - lineStart.x; + const startPY = p.y - lineStart.y; + + const startEndLenSq = startEndX * startEndX + startEndY * startEndY; + // is normalized, so between 0 and 1 + return (startPX * startEndX + startPY * startEndY) / startEndLenSq; + } + + // split a line by the intersection points + splitLineByIntersections(line: [Coord, Coord, Coord | null, Link], points: Coord[]): [Coord, Coord, Coord | null, Link][] { + // calculate the projection and filter out any points with t below 0 or above 1 + let tPoints = points.map(point => { + let t = this.projectPointOnLine(point, line[0], line[1]); + return {p: point, t: t}; + }).filter(point => { + return point.t > 0 && point.t < 1; + }); + + // sort according to t, the projection + tPoints.sort((a, b) => { + return a.t - b.t; + }); + + // add endpoints + const orderedPoints = [{p: line[0], t: 0}, ...tPoints, {p: line[1], t: 1}]; + + // rebuild line segments + let segments: [Coord, Coord, Coord | null, Link][] = []; + for (let i = 0; i < orderedPoints.length - 1; i++) { + const pointOne = orderedPoints[i]; + const pointTwo = orderedPoints[i + 1]; + segments.push([pointOne.p, pointTwo.p, line[2], line[3]]); + } + return segments; + } + + // split an arc along intersection points + splitArcByIntersection(arc: [Coord, Coord, Coord | null, Link], points: Coord[]): [Coord, Coord, Coord | null, Link][] { + if (arc[2] !== null) { + // getting angles of every point from the center of the arc + let intersectionsWithAngles:{p:Coord, angle: number}[] = points.map((p) => ({ + p: p, + angle: Math.atan2(p.y - (arc[2]?.y ?? 0), p.x - (arc[2]?.x ?? 0)) + })); + + // change range from [0 to 2pi] and compare angle distance from start angle of arc + const twoPI = 2 * Math.PI; + // making sure arc angle is between [0, 2pi] + const startArcAngle = (Math.atan2(arc[0].y - (arc[2]?.y ?? 0), arc[0].x - (arc[2]?.x ?? 0)) % twoPI + twoPI) % twoPI; + let resultOrderedPoints = intersectionsWithAngles.sort((a, b) => { + const aFromStart = (a.angle % twoPI + twoPI) % twoPI - startArcAngle; + const bFromStart = (b.angle % twoPI + twoPI) % twoPI - startArcAngle; + + const posAAngle = (aFromStart + twoPI) % twoPI; + const posBAngle = (bFromStart + twoPI) % twoPI; + return posAAngle - posBAngle; + }); + + // removes any intersection points that are at the end points + resultOrderedPoints = resultOrderedPoints.filter((point) => { + return !((this.twoNumsLooselyEquals(point.p.x, arc[0].x) && this.twoNumsLooselyEquals(point.p.y, arc[0].y)) || (this.twoNumsLooselyEquals(point.p.x, arc[1].x) && this.twoNumsLooselyEquals(point.p.y, arc[1].y))); + }); + + resultOrderedPoints = [{p: arc[0], angle: 0}, ...resultOrderedPoints, {p: arc[1], angle: Math.atan2(arc[1].y - arc[2].y, arc[1].x - arc[2].x)}]; + + let segments: [Coord, Coord, Coord | null, Link][] = []; + for (let i = 0; i < resultOrderedPoints.length - 1; i++) { + const pointOne = resultOrderedPoints[i]; + const pointTwo = resultOrderedPoints[i + 1]; + segments.push([pointOne.p, pointTwo.p, arc[2], arc[3]]); + } + return segments; + } else { + return []; + } + } + + // determines whether two lines have the same contents, so they are equal + equalLines(line1: [Coord, Coord, Coord | null, Link], line2: [Coord, Coord, Coord | null, Link]): boolean { + if ( + line1[0].equals(line2[0], 1) && + line1[1].equals(line2[1], 1) + ) { + if (line1[2] === null && line2[2] === null) { + return true; + } else if (line1[2] !== null && + line2[2] !== null && + line1[2].equals(line2[2], 1)) { + return true; + } + } + return false; + } + + // checks if point is on the shape outline + isPointOnLink(point: Coord, externalLines:[Coord, Coord, Coord | null, Link][], radius: number): boolean { + let pointOnLink = false; + externalLines.forEach((line) => { + let isSegmentOnLine: boolean; + if (line[2] !== null) { + isSegmentOnLine = this.isPointInArc(point, line[0], line[1], line[2], radius); + } else { + isSegmentOnLine = this.isPointOnLine(point, line[0], line[1]); + } + if (isSegmentOnLine) { + pointOnLink = true; + } + }) + return pointOnLink; + } + + isPointInsideLink(startPosition: Coord, startLink: Link, externalLines:[Coord, Coord, Coord | null, Link][], radius: number): boolean { + // check if the point is inside the shape created by the lines + // draw a line that is infinitely long and check if it intersects with the shape an odd number of times + const infiniteLine: [Coord, Coord, Coord | null, Link] = [startPosition, new Coord(startPosition.x + 10000, startPosition.y), null, startLink]; + + let intersections = 0; + externalLines.forEach((line) => { + const intersectionPoints = this.intersectsWith(infiniteLine, line, radius); + + if (intersectionPoints !== undefined) { + // check for duplicate intersection points + let duplicatePoints: number[] = []; + intersectionPoints.forEach((point, index) => { + for (let j = index + 1; j < intersectionPoints.length; j++) { + if (this.twoNumsLooselyEquals(point.x, intersectionPoints[j].x) && this.twoNumsLooselyEquals(point.y, intersectionPoints[j].y)) { + duplicatePoints.push(index); + break; + } + } + }); + + let removedDuplicateIntersectionPoints: Coord[] = []; + intersectionPoints.forEach((point, i) => { + if (!duplicatePoints.includes(i)) { + removedDuplicateIntersectionPoints.push(point); + } + }); + + // check if the line from the link is an arc and the intersection is perpendicular (tangent) + let tangentIntersectionPoints: number[] = []; + removedDuplicateIntersectionPoints.forEach((point, index) => { + let infiniteLineSlope: number = (point.y - startPosition.y) / (point.x - startPosition.x); + let arcSlope: number = 0; + if (line[2] !== null) { + arcSlope = (point.y - line[2].y) / (point.x - line[2].x); + } + if (this.twoNumsLooselyEquals(arcSlope * infiniteLineSlope, -1)) { + tangentIntersectionPoints.push(index); + } else if (arcSlope === 0 && (infiniteLineSlope === Number.NEGATIVE_INFINITY || infiniteLineSlope === Number.POSITIVE_INFINITY)) { + tangentIntersectionPoints.push(index); + } else if ((arcSlope === Number.NEGATIVE_INFINITY || arcSlope === Number.POSITIVE_INFINITY) && infiniteLineSlope === 0) { + tangentIntersectionPoints.push(index); + } + }); + + let finalIntersectionPoints: {x: number, y: number}[] = []; + removedDuplicateIntersectionPoints.forEach((point, i) => { + if (!tangentIntersectionPoints.includes(i)) { + finalIntersectionPoints.push(point); + } + }); + + intersections += finalIntersectionPoints.length; + } + + }); + + // If the number of intersections is odd, then the point is inside the shape + return intersections % 2 === 1; + } + + // calculates whether a line is contained within a link + // used to determine whether to get rid of a line + isLineContained(line: [Coord, Coord, Coord | null, Link], linkExternalLines: [Coord, Coord, Coord | null, Link][], radius: number, intersectionPoints: {point: Coord, i: number}[]): boolean { + const origStartOnLink: boolean = this.isPointOnLink(line[0], linkExternalLines, radius); + const origEndOnLink: boolean = this.isPointOnLink(line[1], linkExternalLines, radius); + if (origStartOnLink && origEndOnLink) { + let tempShortenedLine:[Coord, Coord, Coord | null, Link] = this.shortenBy(line, 0.02 * this.scale); + + const startOnLink: boolean = this.isPointOnLink(tempShortenedLine[0], linkExternalLines, radius); + const endOnLink: boolean = this.isPointOnLink(tempShortenedLine[1], linkExternalLines, radius); + + return (this.isPointInsideLink(tempShortenedLine[0], tempShortenedLine[3], linkExternalLines, radius) || startOnLink) && + (this.isPointInsideLink(tempShortenedLine[1], tempShortenedLine[3], linkExternalLines, radius) || endOnLink); + } else { + return (this.isPointInsideLink(line[0], line[3], linkExternalLines, radius) || origStartOnLink) && + (this.isPointInsideLink(line[1], line[3], linkExternalLines, radius) || origEndOnLink); + } + } + + // this shortens segments by shortenNum and returns new segment + shortenBy(line: [Coord, Coord, Coord | null, Link], shortenNum: number): [Coord, Coord, Coord | null, Link] { + let shortenedLine:[Coord, Coord, Coord | null, Link]; + if (line[2] == null) { + shortenedLine = [line[0].clone(), line[1].clone(), line[2], line[3]]; + const angle = Math.atan2(line[1].y - line[0].y, line[1].x - line[0].x); + const shortenByVector = new Coord(Math.cos(angle), Math.sin(angle)).scale( + shortenNum / 2 + ); + shortenedLine[0] = shortenedLine[0].add(shortenByVector); + shortenedLine[1] = shortenedLine[1].subtract(shortenByVector); + return shortenedLine; + } else { + shortenedLine = [line[0].clone(), line[1].clone(), line[2]?.clone(), line[3]]; + + let radius = line[0].getDistanceTo(line[2]); + let angleToShortenBy = shortenNum / radius; + angleToShortenBy /= 2; + + let startAngle = line[0].getAngleTo(line[2]); + let endAngle = line[1].getAngleTo(line[2]); + + startAngle += angleToShortenBy; + endAngle -= angleToShortenBy; + + shortenedLine[0] = line[2].clone()?.add( + new Coord(Math.cos(startAngle), Math.sin(startAngle)).scale(radius) + ); + shortenedLine[1] = line[2].clone()?.add( + new Coord(Math.cos(endAngle), Math.sin(endAngle)).scale(radius) + ); + + return shortenedLine; + } + } + + // This function returns the lines used to calculate and save the lines of a link + solveForExternalLines(subLinks: Map, radius: number): [Coord, Coord, Coord | null, Link][] { + // create a list of all the external lines of all the sublinks + /* First Coord is starting position + Second Coord is ending position + boolean is true if the line is an arc + Last is the center Coord of an arc, null if it is just a line + */ + let externalLines: [Coord, Coord, Coord | null, Link][] = []; + let allLinkExternalLines: Map = new Map(); + + // find and add all external lines for sublinks + for (let link of subLinks.values()) { + let allCoords: Coord[] = []; + for(let joint of link.joints.values()) { + allCoords.push(joint._coords); + } + allCoords = Array.from(allCoords, (coord) => { + return this.unitConversionService.modelCoordToSVGCoord(coord); + }); + + let linkExternalLines: [Coord, Coord, Coord | null, Link][] = []; + + // find the hull points using the coords + let hullPoints: Coord[] = this.grahamScan(allCoords); + let collinearCoords: Coord[] | undefined = this.findCollinearCoords(allCoords); + + if (collinearCoords !== undefined) { + // Calculate perpendicular direction vectors for the line + const dirFirstToSecond = this.perpendicularDirection(collinearCoords[0], collinearCoords[1]); + const dirSecondToFirst = this.perpendicularDirection(collinearCoords[1], collinearCoords[0]); + + // Line from first to second Point + const point1: Coord = new Coord(collinearCoords[0].x + dirFirstToSecond.x * radius, collinearCoords[0].y + dirFirstToSecond.y * radius); + const point2: Coord = new Coord(collinearCoords[1].x + dirFirstToSecond.x * radius, collinearCoords[1].y + dirFirstToSecond.y * radius); + externalLines.push([point1.clone(), point2.clone(), null, link]); + linkExternalLines.push([point1.clone(), point2.clone(), null, link]); + + // Arc around first joint + const point3: Coord = new Coord(collinearCoords[1].x + dirSecondToFirst.x * radius, collinearCoords[1].y + dirSecondToFirst.y * radius); + externalLines.push([point2.clone(), point3.clone(), collinearCoords[1].clone(), link]); + linkExternalLines.push([point2.clone(), point3.clone(), collinearCoords[1].clone(), link]); + + // Line from second back to first Point + const point4: Coord = new Coord(collinearCoords[0].x + dirSecondToFirst.x * radius, collinearCoords[0].y + dirSecondToFirst.y * radius); + externalLines.push([point3.clone(), point4.clone(), null, link]); + linkExternalLines.push([point3.clone(), point4.clone(), null, link]); + + // Arc around the second joint + const point5: Coord = new Coord(collinearCoords[0].x + dirFirstToSecond.x * radius, collinearCoords[0].y + dirFirstToSecond.y * radius); + externalLines.push([point4.clone(), point5.clone(), collinearCoords[0].clone(), link]); + linkExternalLines.push([point4.clone(), point5.clone(), collinearCoords[0].clone(), link]); + } else { + if (hullPoints.length < 3) { + throw new Error('At least three points are required to create a path with rounded corners.'); + } + + // console.log("hull points"); + // console.log(hullPoints); + + // Start the path, moving the pointer to the first correct point + const dirFirstToSecondInit = this.perpendicularDirection(hullPoints[0], hullPoints[1]); + + // last position + let lastPosition: Coord = new Coord(hullPoints[0].x + dirFirstToSecondInit.x * radius, hullPoints[0].y + dirFirstToSecondInit.y * radius); + + //iterate over all the points, one line and one arc at a time + for (let i = 0; i < hullPoints.length; i++) { + //get current points to look at. + const c0 = hullPoints[i]; + const c1 = hullPoints[(i + 1) % hullPoints.length]; + const c2 = hullPoints[(i + 2) % hullPoints.length]; + //get the Perpendicular Direction for the Path + const dirFirstToSecond = this.perpendicularDirection(c0, c1); + const dirSecondToThird = this.perpendicularDirection(c1, c2); + + // Line from first joint to second joint + let point1: Coord = new Coord(c1.x + dirFirstToSecond.x * radius, c1.y + dirFirstToSecond.y * radius); + externalLines.push([lastPosition, point1, null, link]); + linkExternalLines.push([lastPosition, point1, null, link]); + + // Arc around second joint + let point2: Coord = new Coord(c1.x+ dirSecondToThird.x * radius, c1.y + dirSecondToThird.y * radius); + let point2Center: Coord = c1.clone(); + externalLines.push([point1, point2, point2Center, link]); + linkExternalLines.push([point1, point2, point2Center, link]); + lastPosition = point2.clone(); + } + } + + allLinkExternalLines.set(link, linkExternalLines); + } + + // need to process the external lines we got + let stopCounter = 10000; + + // will hold the intersection points of all external lines + // x is the x coord of intersection, y is y coord of intersection, and i is index in external lines array + let allIntersectionPoints: {point: Coord, i: number}[] = []; + + // for each external line, need to check for intersections with all other external lines + for (let i = 0; i < externalLines.length - 1; i++) { + const line1: [Coord, Coord, Coord | null, Link] = externalLines[i]; + for (let j = i + 1; j < externalLines.length; j++) { + if (stopCounter-- < 0) { + console.error("Error, stop counter is at 0"); + return []; + } + + const line2: [Coord, Coord, Coord | null, Link] = externalLines[j]; + + // check if lines intersect, if they do save the intersections + const intersection = this.intersectsWith(line1, line2, radius); + + // make sure intersection is not the end points of a segment before adding in the intersection for each segment + // intersection points are saved along with the segment the intersection is on + if (intersection !== undefined) { + intersection.forEach((point: Coord) => { + if (!(line1[1].equals(point, this.scale) || line1[0].equals(point, this.scale))) { + allIntersectionPoints.push({point: point, i: i}); + } + if (!(line2[0].equals(point, this.scale) || line2[1].equals(point, this.scale))) { + allIntersectionPoints.push({point: point, i: j}); + } + }); + } + + } + } + + // checking if there are any duplicate intersection points for each line + let duplicatePoints: number[] = []; + allIntersectionPoints.forEach((point, index) => { + for (let j = index + 1; j < allIntersectionPoints.length; j++) { + if (this.twoNumsLooselyEquals(point.point.x, allIntersectionPoints[j].point.x) && this.twoNumsLooselyEquals(point.point.y, allIntersectionPoints[j].point.y) && point.i === allIntersectionPoints[j].i) { + duplicatePoints.push(index); + break; + } + } + }); + + // removing the duplicate intersection points, if any + let removedDuplicatePoints: {point: Coord, i: number}[] = []; + allIntersectionPoints.forEach((point, index) => { + if (!duplicatePoints.includes(index)) { + removedDuplicatePoints.push(point); + } + }); + + // grouping intersections by line + let intersectionsByCoords: Coord[][] = this.groupIntersectionsByPath(externalLines, removedDuplicatePoints); + + // will hold the new external lines with the intersections considered + let intersectionExternalLines: [Coord, Coord, Coord | null, Link][] = []; + + // splitting each line by intersections + for (let i = 0; i < externalLines.length; i++) { + if (externalLines[i][2] === null) { + // segment is a line + let lineSegments = this.splitLineByIntersections(externalLines[i], intersectionsByCoords[i]); + intersectionExternalLines.push(...lineSegments); + } else { + // segment is an arc + let arcSegments = this.splitArcByIntersection(externalLines[i], intersectionsByCoords[i]); + intersectionExternalLines.push(...arcSegments); + } + } + + // check for duplicate lines + let duplicateLines: [Coord, Coord, Coord | null, Link][] = []; + for (const line of intersectionExternalLines) { + const duplicateFound = intersectionExternalLines.find(line2 => { + return line !== line2 && this.equalLines(line, line2); + }); + const inDuplicateLines = duplicateLines.find((line2) => { + return this.equalLines(line2, line); + }); + if (duplicateFound && !inDuplicateLines) { + duplicateLines.push(line); + } + } + + // if a segment is fully inside another link, remove it + intersectionExternalLines = intersectionExternalLines.filter((line) => { + return ![... subLinks.values()].some((link) => { + if (link === line[3]) { + return false; + } else { + let linkLines = allLinkExternalLines.get(link); + if (linkLines) { + return this.isLineContained(line, linkLines, radius, allIntersectionPoints); + } else { + return false; + } + } + }) + }); + + // duplicate lines are only added if we detect a gap in the path + // need to loop again and check for a gap if a new segment was added + let addedNewLines: boolean = false; + let firstTime: boolean = true; + let linesToCheck: [Coord, Coord, Coord | null, Link][] = intersectionExternalLines; + let newLinesToAdd: [Coord, Coord, Coord | null, Link][]; + while (firstTime || addedNewLines) { + firstTime = false; + addedNewLines = false; + + newLinesToAdd = []; + for (let i = 0; i < linesToCheck.length; i++) { + const pointToSearch: Coord = linesToCheck[i][1]; + const found = intersectionExternalLines.find((line2) => { + return line2[0].equals(pointToSearch, 1); + }); + if (!found) { + const lineToAdd = duplicateLines.find((line2) => { + return line2[0].looselyEquals(pointToSearch, 1); + }); + if (lineToAdd !== undefined) { + newLinesToAdd.push(lineToAdd); + addedNewLines = true; + } + } + } + linesToCheck = newLinesToAdd; + + // add in the new lines + intersectionExternalLines.push(...newLinesToAdd); + } + + //Remove very short lines + intersectionExternalLines = intersectionExternalLines.filter((line) => { + return (line[0].getDistanceTo(line[1]) > 0.00001 * 1); + }); + + return intersectionExternalLines; + } + + calculateCompoundPath(subLinks: Map, allCoordsList: Coord[], r: number) { + // allExternalLines will hold the path coordinates for each line and arc for all the coords + let allExternalLines: [Coord, Coord, Coord | null, Link][] = this.solveForExternalLines(subLinks, r); + //console.log(allExternalLines); + + if (allExternalLines.length < 1) { + return ''; + } + + // converting external lines list into a set, which we will delete lines we have used + const externalLinesSet = new Set(allExternalLines); + + let pathString = ''; + + let timeoutCounter = 1000; + + // this is the offset for concave curve radius + const offsetRadius = 3; + + while (externalLinesSet.size > 1) { + // this is the first line from the set + let currentLine: [Coord, Coord, Coord | null, Link] = externalLinesSet.values().next().value; + + let beginningPoint: Coord = currentLine[1].clone(); + + while (!currentLine[1].equals(beginningPoint, 1) || this.isNewShape(pathString)) { + if (timeoutCounter-- < 0) { + console.log("Timeout counter reached!") + return pathString; + } + + // find the next line that starts at the end of the current line + const nextLine: [Coord, Coord, Coord | null, Link] | undefined = [...externalLinesSet].find((line) => { + return line[0].looselyEquals(currentLine[1], 1); + }); + + if (!nextLine) { + break; + } + externalLinesSet.delete(nextLine); + + // when there are two lines intersecting, create a fillet between them + if (currentLine[2] === null && nextLine[2] === null && + // checking if angle between the two lines is greater than 10 degrees + Math.abs(this.angleToPI(nextLine, currentLine)) > (10 * Math.PI / 180)) { + let [currentLineOffsetPoint, nextLineOffsetPoint, radius] = this.computeArcPointsAndRadius(currentLine, nextLine, r * offsetRadius); + + currentLine[1] = currentLineOffsetPoint; + nextLine[0] = nextLineOffsetPoint; + + if (this.isNewShape(pathString)) { + pathString += 'M ' + currentLine[1].x + ', ' + currentLine[1].y + ' '; + } else { + pathString = this.pathStringForLine(currentLine, pathString); + } + + pathString += 'A ' + radius + ', ' + radius + ' 0 0 0 ' + nextLine[0].x + ', ' + nextLine[0].y + ' '; + } else { + + // otherwise, just draw a line between the two points + if (this.isNewShape(pathString)) { + pathString += 'M ' + currentLine[1].x + ', ' + currentLine[1].y + ' '; + } else { + pathString = this.pathStringForLine(currentLine, pathString); + } + } + currentLine = nextLine; + } + pathString = this.pathStringForLine(currentLine, pathString); + pathString += 'Z '; + } + return pathString; + } + + // calculate and return the path string plus the path of the line + pathStringForLine(line: [Coord, Coord, Coord | null, Link], pathString: string):string { + if (line[2] !== null) { + // if the line is an arc + pathString += 'A ' + line[0].getDistanceTo(line[2]) + ', ' + line[0].getDistanceTo(line[2]) + ' 0 0 1 ' + line[1].x + ', ' + line[1].y + ' '; + } else { + // if the line is just a line + pathString += 'L ' + line[1].x + ', ' + line[1].y + ' '; + } + return pathString; + } + + } diff --git a/src/app/services/undo-redo.service.ts b/src/app/services/undo-redo.service.ts index 5bdce848..23957651 100644 --- a/src/app/services/undo-redo.service.ts +++ b/src/app/services/undo-redo.service.ts @@ -1,14 +1,15 @@ -import {Action} from '../components/ToolBar/undo-redo-panel/action'; -import {Coord} from '../model/coord'; -import {Link} from '../model/link'; -import {Joint} from '../model/joint'; -import {Mechanism} from '../model/mechanism'; -import {Subject} from 'rxjs'; -import {Injectable} from '@angular/core'; -import {StateService} from './state.service'; -import {isUndefined} from 'lodash'; -import {Position} from '../model/position'; -import {Force} from '../model/force'; +import { Action } from '../components/ToolBar/undo-redo-panel/action'; +import { Coord } from '../model/coord'; +import { Link } from '../model/link'; +import { Joint } from '../model/joint'; +import { Mechanism } from '../model/mechanism'; +import { Subject } from 'rxjs'; +import { Injectable } from '@angular/core'; +import { StateService } from './state.service'; +import { isUndefined } from 'lodash'; +import { Position } from '../model/position'; +import { Force } from '../model/force'; +import {CompoundLink} from "../model/compound-link"; @Injectable({ providedIn: 'root', @@ -262,7 +263,8 @@ export class UndoRedoService { const tr = action.linkTracerData!; this.mechanism.addJointToLink( tr.linkId, - new Coord(tr.coords.x, tr.coords.y) + new Coord(tr.coords.x, tr.coords.y), + true ); break; case 'addLinkToLink': @@ -299,6 +301,24 @@ export class UndoRedoService { //reverses the lock link.locked = !locked break; + } case 'circleLink': { + //get the specified link from the action and see if it's a circle + const link = this.mechanism.getLink(action.linkId!)!; + const circular = link.isCircle; + //reverse the lock + link.isCircle = !circular + break; + } case 'deleteCompoundLink': { + this.mechanism.removeCompoundLinkByID(action.compoundLinkData!.id); + break; + } case 'addTracerCompound': { + const tr = action.linkTracerData!; + this.mechanism.addTracerPointWelded( + tr.linkId, + tr.tracerModelPos!, + tr.tracerSVGPos!, + ); + break; } default: @@ -312,11 +332,6 @@ export class UndoRedoService { // Applies the inverse of an action to the Mechanism (undo logic). private applyInverseAction(action: Action): void { switch (action.type) { - case 'addWeld': - if (action.jointId !== undefined) { - this.mechanism.removeWeld(action.jointId); - } - break; case 'addInput': if (action.jointId !== undefined) { this.mechanism.removeInput(action.jointId); @@ -327,14 +342,14 @@ export class UndoRedoService { this.mechanism.addInput(action.jointId); } break; - case 'removeSlider': + case 'addGround': if (action.jointId !== undefined) { - this.mechanism.addSlider(action.jointId); + this.mechanism.removeGround(action.jointId); } break; - case 'removeWeld': + case 'removeGround': if (action.jointId !== undefined) { - this.mechanism.addWeld(action.jointId); + this.mechanism.addGround(action.jointId); } break; case 'generateFourBar': @@ -343,6 +358,11 @@ export class UndoRedoService { case 'generateSixBar': this.generateSixBarSubject.next(); break; + case 'addSlider': + if (action.jointId !== undefined) { + this.mechanism.removeSlider(action.jointId); + } + break; case 'setSynthesisLength': this.mechanism.setCouplerLength(action.oldDistance as number); break; @@ -379,9 +399,21 @@ export class UndoRedoService { this.mechanism.setPositionAngle(action.oldAngle, action.linkId); } break; - case 'addSlider': // Restores the link/joints from snapshot if user - case 'addGround': // performs an undo on add slider, add ground, - case 'removeGround': // remove ground, or delete joint + case 'removeSlider': + if (action.jointId !== undefined) { + this.mechanism.addSlider(action.jointId); + } + break; + case 'addWeld': + if (action.jointId !== undefined) { + this.mechanism.removeWeld(action.jointId); + } + break; + case 'removeWeld': + if (action.jointId !== undefined) { + this.mechanism.addWeld(action.jointId); + } + break; case 'deleteJoint': // Restore main joint: if (action.jointData) { @@ -410,6 +442,22 @@ export class UndoRedoService { } }); } + + if (action.compoundLinkDataArray) { + action.compoundLinkDataArray.forEach((cLinkSnap) => { + const cLinks = cLinkSnap.linkIds.map((lid) => + this.mechanism.getLink(lid) + ); + if (cLinks.every((l) => l !== undefined)) { + const restoredCompoundLink = new CompoundLink(cLinkSnap.id, cLinks); + restoredCompoundLink.name = cLinkSnap.name; + restoredCompoundLink.mass = cLinkSnap.mass; + restoredCompoundLink.lock = cLinkSnap.locked; + restoredCompoundLink.color = cLinkSnap.color; + this.mechanism._addCompoundLink(restoredCompoundLink); + } + }); + } break; case 'moveJoint': if (action.jointId != null && action.oldCoords) { @@ -530,6 +578,102 @@ export class UndoRedoService { link.locked = !locked break; } + case 'circleLink': { + //get the specified link from the action and see if it's a circle + const link = this.mechanism.getLink(action.linkId!)!; + const circular = link.isCircle + //reverse the lock + link.isCircle = !circular + break; + } + case 'deleteCompoundLink': { + // rebuilding joints + if (action.compoundExtraJointsData) { + action.compoundExtraJointsData!.forEach((joints) => { + joints.forEach((j) => { + if (!this.mechanism.getJoint(j.id)) { + this.restoreJointFromSnapshot(j); + } + }) + }) + } + + // rebuilding links + if (action.compoundExtraLinkData) { + action.compoundExtraLinkData!.forEach((link) => { + // check that joints exist + const jointObjs = link.jointIds.map((id) => { + const j = this.mechanism.getJoint(id); + if (!j) console.warn(`undo deleteLink: joint ${id} still missing`); + return j!; + }); + + if (jointObjs.every((j) => j != null)) { + const linkRestored = new Link(link.id, jointObjs); + linkRestored.name = link.name; + linkRestored.mass = link.mass; + linkRestored.angle = link.angle; + linkRestored.locked = link.locked; + linkRestored.color = link.color; + this.mechanism._addLink(linkRestored); + } + }) + } + + // rebuilding compound link + const cl = action.compoundLinkData!; + const linkObjs = cl.linkIds.map((id) => { + const l = this.mechanism.getLink(id); + if (!l) console.warn(`undo deleteCompoundLink: link ${id} still missing`); + return l!; + }); + if (linkObjs.every((l) => l != null)) { + const linkRestored = new CompoundLink(cl.id, linkObjs); + linkRestored.name = cl.name; + linkRestored.mass = cl.mass; + linkRestored.lock = cl.locked; + linkRestored.color = cl.color; + this.mechanism._addCompoundLink(linkRestored); + } + + break; + } + case 'addTracerCompound': { + const tr = action.linkTracerData!; + const allCompoundLinks = this.mechanism.getCompoundLinks(); + let linkObj; + for (const c of allCompoundLinks) { + if (c.id == tr.linkId) { + linkObj = c; + } + } + + if (linkObj == undefined) { + console.warn( + `Undo addTracerCompound: no compound link found)` + ); + break; + } + + let allJoints: Joint[] = []; + Array.from(linkObj!.links!.values()).forEach((l) => { + Array.from(l.joints.values()).forEach((j) => { + allJoints.push(j); + }) + }); + + const toRemove = allJoints.find( + (j) => j.coords.x === tr.coords.x && j.coords.y === tr.coords.y + ); + if (toRemove) { + this.mechanism.removeJoint(toRemove.id); + } else { + console.warn( + `Undo addTracerCompound: no tracer found at (${tr.coords.x},${tr.coords.y})` + ); + } + break; + } default: console.error('No inverse defined for action type:', action.type); } @@ -552,6 +696,8 @@ export class UndoRedoService { restoredJoint.locked = jointSnapshot!.locked; restoredJoint.hidden = jointSnapshot!.isHidden; restoredJoint.reference = jointSnapshot!.isReference; + restoredJoint.isTracer = jointSnapshot!.isTracer; + restoredJoint.isPartOfWelded = jointSnapshot!.isPartOfWelded; this.mechanism._addJoint(restoredJoint); } } diff --git a/src/app/services/unit-conversion.service.ts b/src/app/services/unit-conversion.service.ts index cd3290e0..19cca1c8 100644 --- a/src/app/services/unit-conversion.service.ts +++ b/src/app/services/unit-conversion.service.ts @@ -31,4 +31,10 @@ export class UnitConversionService{ return new Coord(convertedX,convertedY); } + // Converts model number to SVG number by applying the defined scale factor. + public modelNumToSVGNum(modelNum: number): number{ + let convertedNum: number = modelNum * this.MODEL_TO_SVG_SCALE; + return convertedNum; + } + } diff --git a/src/assets/contextMenuIcons/circleInputLink.svg b/src/assets/contextMenuIcons/circleInputLink.svg new file mode 100644 index 00000000..f9c74c99 --- /dev/null +++ b/src/assets/contextMenuIcons/circleInputLink.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/contextMenuIcons/uncircular.svg b/src/assets/contextMenuIcons/uncircular.svg new file mode 100644 index 00000000..00f29921 --- /dev/null +++ b/src/assets/contextMenuIcons/uncircular.svg @@ -0,0 +1 @@ + \ No newline at end of file